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.
15const fs = require("node:fs");
16const path = require("node:path");
17const { createRequire } = require("node:module");
18
19function 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
66async 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
180main().catch((error) => {
181 console.error(error);
182 process.exit(1);
183});