| 1 | "use strict"; |
| 2 | |
| 3 | // Fixture for uniwind-platform-variants.test.ts. Run as a plain Node process |
| 4 | // (`node uniwind-platform-variants.fixture.cjs <fixtureDir>`) so it executes the |
| 5 | // real compiler with no test-runner transforms in the way. Prints one JSON |
| 6 | // object to stdout describing what each platform bundle compiled. |
| 7 | // |
| 8 | // It reproduces what the Metro transformer does for `global.css`: compile the |
| 9 | // Tailwind entry, then run uniwind's CSS processor and stylesheet serializer |
| 10 | // once per platform. Loading `dist/common` needs a small custom loader because |
| 11 | // uniwind declares `"type": "module"` over that CommonJS output and its |
| 12 | // internal `@/...` import alias only exists at uniwind's own build time; the |
| 13 | // loader hands unmodified file bytes to V8 and resolves those aliases the same |
| 14 | // way, so this runs the exact compiled code the Metro transformer bundles. |
| 15 | const fs = require("node:fs"); |
| 16 | const path = require("node:path"); |
| 17 | const { createRequire } = require("node:module"); |
| 18 | |
| 19 | function createUniwindCompilerLoader(uniwindPackageJson) { |
| 20 | const uniwindRoot = path.dirname(fs.realpathSync(uniwindPackageJson)); |
| 21 | const uniwindRequire = createRequire(path.join(uniwindRoot, "package.json")); |
| 22 | const cache = new Map(); |
| 23 | |
| 24 | const load = (absoluteFileWithoutExtension) => { |
| 25 | const resolved = [ |
| 26 | absoluteFileWithoutExtension, |
| 27 | `${absoluteFileWithoutExtension}.js`, |
| 28 | path.join(absoluteFileWithoutExtension, "index.js"), |
| 29 | ].find((candidate) => { |
| 30 | try { |
| 31 | return fs.statSync(candidate).isFile(); |
| 32 | } catch { |
| 33 | return false; |
| 34 | } |
| 35 | }); |
| 36 | if (resolved === undefined) { |
| 37 | throw new Error(`Cannot resolve uniwind module ${absoluteFileWithoutExtension}`); |
| 38 | } |
| 39 | const cached = cache.get(resolved); |
| 40 | if (cached !== undefined) return cached; |
| 41 | |
| 42 | const cjsModule = { exports: {} }; |
| 43 | |
| 44 | const localRequire = (request) => { |
| 45 | if (request.startsWith("@/")) { |
| 46 | return load(path.join(uniwindRoot, "dist/common", request.slice(2))); |
| 47 | } |
| 48 | if (request.startsWith(".")) { |
| 49 | return load(path.resolve(path.dirname(resolved), request)); |
| 50 | } |
| 51 | // uniwind's own dependencies (lightningcss, culori, ...) resolve through |
| 52 | // the package's dependency tree, like they do inside the Metro bundle. |
| 53 | return uniwindRequire(request); |
| 54 | }; |
| 55 | |
| 56 | const code = fs.readFileSync(resolved, "utf8"); |
| 57 | new Function("require", "module", "exports", code)(localRequire, cjsModule, cjsModule.exports); |
| 58 | cache.set(resolved, cjsModule.exports); |
| 59 | return cjsModule.exports; |
| 60 | }; |
| 61 | |
| 62 | const loadBundler = (request) => load(path.join(uniwindRoot, "dist/common", request)); |
| 63 | return { loadBundler, uniwindRequire, uniwindRoot }; |
| 64 | } |
| 65 | |
| 66 | async function main() { |
| 67 | const fixtureDir = fs.realpathSync(path.resolve(process.argv[2])); |
| 68 | const uniwindPackageJson = require.resolve("uniwind/package.json", { |
| 69 | paths: [fixtureDir], |
| 70 | }); |
| 71 | const { loadBundler, uniwindRequire, uniwindRoot } = |
| 72 | createUniwindCompilerLoader(uniwindPackageJson); |
| 73 | const { ProcessorBuilder } = loadBundler("bundler/css-processor/processor.js"); |
| 74 | const { addMetaToStylesTemplate } = loadBundler( |
| 75 | "bundler/css-processor/addMetaToStylesTemplate.js", |
| 76 | ); |
| 77 | const { compileNativeCSS } = loadBundler("bundler/css-compiler/compileNativeCSS.js"); |
| 78 | |
| 79 | const { compile } = uniwindRequire("@tailwindcss/node"); |
| 80 | const { Scanner } = uniwindRequire("@tailwindcss/oxide"); |
| 81 | const compiler = await compile(fs.readFileSync(path.join(fixtureDir, "global.css"), "utf8"), { |
| 82 | base: fixtureDir, |
| 83 | onDependency: () => {}, |
| 84 | }); |
| 85 | const scanner = new Scanner({ |
| 86 | sources: [...compiler.sources, { negated: false, pattern: "**/*.tsx", base: fixtureDir }], |
| 87 | }); |
| 88 | const tailwindCSS = await compiler.build(scanner.scan()); |
| 89 | |
| 90 | const platforms = {}; |
| 91 | // Every ios:/android: utility class Tailwind generated for the fixture. |
| 92 | const compiledGuardedClasses = {}; |
| 93 | for (const match of tailwindCSS.matchAll(/\.(ios|android)\\:([^{ ,]+) \{/g)) { |
| 94 | compiledGuardedClasses[`${match[1]}:${match[2]}`.replace(/\\(.)/g, "$1")] = true; |
| 95 | } |
| 96 | const platformBlock = (name) => |
| 97 | new RegExp(`@media ${name} \\{([\\s\\S]*?)\\n \\}`).exec(tailwindCSS)?.[1] ?? ""; |
| 98 | const utilityCountIn = (block) => (block.match(/^\s+\.[^ ]+ \{/gm) ?? []).length; |
| 99 | const tailwindChecks = { |
| 100 | iosBlocks: (tailwindCSS.match(/@media ios \{/g) ?? []).length, |
| 101 | androidBlocks: (tailwindCSS.match(/@media android \{/g) ?? []).length, |
| 102 | iosUtilities: utilityCountIn(platformBlock("ios")), |
| 103 | androidUtilities: utilityCountIn(platformBlock("android")), |
| 104 | }; |
| 105 | for (const platform of ["ios", "android"]) { |
| 106 | // Same config the app's withUniwindConfig produces (polyfills.rem: 14). |
| 107 | const bundlerConfig = { platform, themes: ["light", "dark"], polyfills: { rem: 14 } }; |
| 108 | const processor = new ProcessorBuilder(bundlerConfig); |
| 109 | processor.transform(tailwindCSS); |
| 110 | const compiled = addMetaToStylesTemplate(processor, platform); |
| 111 | const payload = compileNativeCSS(bundlerConfig, tailwindCSS); |
| 112 | const styles = {}; |
| 113 | for (const [className, entries] of Object.entries(compiled)) { |
| 114 | styles[className] = entries.map( |
| 115 | ({ native, minWidth, maxWidth, active, focus, disabled, dataAttributes }) => ({ |
| 116 | native, |
| 117 | minWidth, |
| 118 | maxWidth, |
| 119 | active: active ?? null, |
| 120 | focus: focus ?? null, |
| 121 | disabled: disabled ?? null, |
| 122 | dataAttributes: dataAttributes ?? null, |
| 123 | }), |
| 124 | ); |
| 125 | } |
| 126 | platforms[platform] = { |
| 127 | styles, |
| 128 | payloadIncludesAllCompiled: Object.keys(compiled).every((className) => |
| 129 | payload.includes(`"${className}"`), |
| 130 | ), |
| 131 | // A utility dropped from this platform's stylesheet must not appear |
| 132 | // anywhere in the payload the bundle embeds for this platform. |
| 133 | payloadLeaks: Object.keys(compiledGuardedClasses) |
| 134 | .filter((className) => compiled[className] === undefined) |
| 135 | .filter((className) => payload.includes(`"${className}"`)), |
| 136 | }; |
| 137 | } |
| 138 | |
| 139 | // Exercise the real Metro transformer entry point (the shipped |
| 140 | // `dist/metro/transformer.cjs`, not a copy) with a stubbed downstream worker, |
| 141 | // to check what the global.css virtual module actually becomes — including |
| 142 | // the native styles fingerprint that lets dev reloads skip reinitializing. |
| 143 | const transformerPath = path.join(uniwindRoot, "dist/metro/transformer.cjs"); |
| 144 | let reinitCode = ""; |
| 145 | const Module = require("node:module"); |
| 146 | const origLoad = Module._load; |
| 147 | Module._load = function (request, ...rest) { |
| 148 | if (request === "metro-transform-worker") { |
| 149 | return { |
| 150 | transform: async (_config, _root, _file, data) => { |
| 151 | reinitCode = data.toString(); |
| 152 | return { output: [{ data: {} }] }; |
| 153 | }, |
| 154 | }; |
| 155 | } |
| 156 | return origLoad.call(this, request, ...rest); |
| 157 | }; |
| 158 | process.chdir(fixtureDir); |
| 159 | try { |
| 160 | const { transform } = require(transformerPath); |
| 161 | await transform( |
| 162 | { uniwind: { cssEntryFile: "./global.css", isExpoProject: false } }, |
| 163 | fixtureDir, |
| 164 | "global.css", |
| 165 | Buffer.from(""), |
| 166 | { platform: "ios", type: "module" }, |
| 167 | ); |
| 168 | } finally { |
| 169 | Module._load = origLoad; |
| 170 | } |
| 171 | const transformerCheck = { |
| 172 | reinitPayload: reinitCode.includes("Uniwind.__reinit(rt =>"), |
| 173 | fingerprintArg: /, '[0-9a-f]{64}'\);\s*$/.test(reinitCode), |
| 174 | themesArg: reinitCode.includes("['light', 'dark']"), |
| 175 | }; |
| 176 | |
| 177 | process.stdout.write(JSON.stringify({ tailwindChecks, transformerCheck, platforms })); |
| 178 | } |
| 179 | |
| 180 | main().catch((error) => { |
| 181 | console.error(error); |
| 182 | process.exit(1); |
| 183 | }); |