#!/usr/bin/env node import * as NodeFS from "node:fs";import * as NodePath from "node:path";import tailwindColors from "tailwindcss/colors";import { BUILT_IN_THEME_IDS, type BuiltInThemeId } from "@t3tools/shared/themePalettes"; import { getMobileThemeVariables, MOBILE_THEME_VARIABLE_NAMES, type MobileThemeAppearance, type MobileThemeVariables,} from "../src/lib/mobileTheme.ts"; const APPEARANCES = ["light", "dark"] as const;const GLOBAL_CSS_PATH = NodePath.resolve(import.meta.dirname, "../global.css");const GENERATED_CSS_PATH = NodePath.resolve(import.meta.dirname, "../generated-uniwind-themes.css");const GENERATED_NAMES_PATH = NodePath.resolve( import.meta.dirname, "../generated-uniwind-theme-names.json",);const GENERATED_DEFAULT_VARIABLES_PATH = NodePath.resolve( import.meta.dirname, "../generated-uniwind-default-theme-variables.json",); type TailwindColorFamily = keyof typeof tailwindColors;type TailwindColorShade = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950; const color = (family: TailwindColorFamily, shade?: TailwindColorShade, opacity = 1): string => { const familyColors = tailwindColors[family]; const value = typeof familyColors === "string" ? shade === undefined ? familyColors : undefined : shade === undefined ? undefined : familyColors[String(shade) as keyof typeof familyColors]; if (value === undefined) { throw new Error(`Unknown Tailwind color ${family}${shade === undefined ? "" : `-${shade}`}.`); } if (opacity === 1) return value; const percentage = Number((opacity * 100).toFixed(4)); const oklch = /^oklch\((.*)\)$/.exec(value); if (oklch) return `oklch(${oklch[1]} / ${percentage}%)`; if (value === "#fff") return `rgb(255 255 255 / ${percentage}%)`; if (value === "#000") return `rgb(0 0 0 / ${percentage}%)`; return `color-mix(in srgb, ${value} ${percentage}%, transparent)`;}; // These replace the remaining dark:* utility pairs. A registered palette theme is// neither literally `light` nor `dark`, so appearance-sensitive values must also be// represented as semantic variables for custom themes.const ADAPTIVE_COLORS = { "--color-adaptive-amber-50-950-a40": [color("amber", 50), color("amber", 950, 0.4)], "--color-adaptive-amber-200-900-a60": [color("amber", 200), color("amber", 900, 0.6)], "--color-adaptive-amber-500-a12-a16": [color("amber", 500, 0.12), color("amber", 500, 0.16)], "--color-adaptive-amber-700-300": [color("amber", 700), color("amber", 300)], "--color-adaptive-amber-700-400": [color("amber", 700), color("amber", 400)], "--color-adaptive-amber-800-200": [color("amber", 800), color("amber", 200)], "--color-adaptive-blue-50-blue-400-a14": [color("blue", 50), color("blue", 400, 0.14)], "--color-adaptive-blue-300-a50-blue-400-a28": [color("blue", 300, 0.5), color("blue", 400, 0.28)], "--color-adaptive-blue-500-a20-blue-400-a15": [color("blue", 500, 0.2), color("blue", 400, 0.15)], "--color-adaptive-blue-500-400": [color("blue", 500), color("blue", 400)], "--color-adaptive-blue-600-400": [color("blue", 600), color("blue", 400)], "--color-adaptive-black-a10-a25": [ color("black", undefined, 0.1), color("black", undefined, 0.25), ], "--color-adaptive-black-a15-a35": [ color("black", undefined, 0.15), color("black", undefined, 0.35), ], "--color-adaptive-emerald-500-a12-a16": [ color("emerald", 500, 0.12), color("emerald", 500, 0.16), ], "--color-adaptive-emerald-600-400": [color("emerald", 600), color("emerald", 400)], "--color-adaptive-emerald-700-300": [color("emerald", 700), color("emerald", 300)], "--color-adaptive-indigo-500-a12-a16": [color("indigo", 500, 0.12), color("indigo", 500, 0.16)], "--color-adaptive-indigo-600-300": [color("indigo", 600), color("indigo", 300)], "--color-adaptive-indigo-700-300": [color("indigo", 700), color("indigo", 300)], "--color-adaptive-neutral-100-900": [color("neutral", 100), color("neutral", 900)], "--color-adaptive-neutral-200-700-a60": [color("neutral", 200), color("neutral", 700, 0.6)], "--color-adaptive-neutral-200-800": [color("neutral", 200), color("neutral", 800)], "--color-adaptive-neutral-200-a70-white-a8": [ color("neutral", 200, 0.7), color("white", undefined, 0.08), ], "--color-adaptive-neutral-200-white-a6": [color("neutral", 200), color("white", undefined, 0.06)], "--color-adaptive-neutral-200-white-a8": [color("neutral", 200), color("white", undefined, 0.08)], "--color-adaptive-neutral-200-a80-white-a8": [ color("neutral", 200, 0.8), color("white", undefined, 0.08), ], "--color-adaptive-neutral-300-a60-white-a12": [ color("neutral", 300, 0.6), color("white", undefined, 0.12), ], "--color-adaptive-neutral-400-500": [color("neutral", 400), color("neutral", 500)], "--color-adaptive-neutral-400-a60-500-a60": [ color("neutral", 400, 0.6), color("neutral", 500, 0.6), ], "--color-adaptive-neutral-400-a80-500-a80": [ color("neutral", 400, 0.8), color("neutral", 500, 0.8), ], "--color-adaptive-neutral-500-a10-a16": [color("neutral", 500, 0.1), color("neutral", 500, 0.16)], "--color-adaptive-neutral-500-400": [color("neutral", 500), color("neutral", 400)], "--color-adaptive-neutral-500-500": [color("neutral", 500), color("neutral", 500)], "--color-adaptive-neutral-600-300": [color("neutral", 600), color("neutral", 300)], "--color-adaptive-neutral-600-400": [color("neutral", 600), color("neutral", 400)], "--color-adaptive-neutral-950-50": [color("neutral", 950), color("neutral", 50)], "--color-adaptive-red-50-950-a80": [color("red", 50), color("red", 950, 0.8)], "--color-adaptive-red-200-800": [color("red", 200), color("red", 800)], "--color-adaptive-red-600-a80-400-a80": [color("red", 600, 0.8), color("red", 400, 0.8)], "--color-adaptive-red-700-300": [color("red", 700), color("red", 300)], "--color-adaptive-rose-100-500-a18": [color("rose", 100), color("rose", 500, 0.18)], "--color-adaptive-rose-100-a80-500-a12": [color("rose", 100, 0.8), color("rose", 500, 0.12)], "--color-adaptive-rose-300-a70-400-a28": [color("rose", 300, 0.7), color("rose", 400, 0.28)], "--color-adaptive-rose-500-a12-a16": [color("rose", 500, 0.12), color("rose", 500, 0.16)], "--color-adaptive-rose-500-400": [color("rose", 500), color("rose", 400)], "--color-adaptive-rose-600-400": [color("rose", 600), color("rose", 400)], "--color-adaptive-rose-700-300": [color("rose", 700), color("rose", 300)], "--color-adaptive-sky-500-a12-a16": [color("sky", 500, 0.12), color("sky", 500, 0.16)], "--color-adaptive-sky-600-400": [color("sky", 600), color("sky", 400)], "--color-adaptive-sky-700-300": [color("sky", 700), color("sky", 300)], "--color-adaptive-violet-500-a12-a16": [color("violet", 500, 0.12), color("violet", 500, 0.16)], "--color-adaptive-violet-600-400": [color("violet", 600), color("violet", 400)], "--color-adaptive-violet-700-300": [color("violet", 700), color("violet", 300)], "--color-adaptive-white-neutral-950-a70": [color("white"), color("neutral", 950, 0.7)], "--color-adaptive-zinc-500-a12-a16": [color("zinc", 500, 0.12), color("zinc", 500, 0.16)], "--color-adaptive-zinc-500-400": [color("zinc", 500), color("zinc", 400)], "--color-adaptive-zinc-600-300": [color("zinc", 600), color("zinc", 300)],}; export const customThemeNames = BUILT_IN_THEME_IDS.flatMap((themeId) => APPEARANCES.map((appearance) => `${themeId}-${appearance}`),); const adaptiveVariablesFor = (appearance: MobileThemeAppearance) => Object.fromEntries( Object.entries(ADAPTIVE_COLORS).map(([name, values]) => [ name, values[appearance === "light" ? 0 : 1], ]), ); const variablesFor = (themeId: BuiltInThemeId, appearance: MobileThemeAppearance) => ({ ...getMobileThemeVariables(themeId, appearance), ...adaptiveVariablesFor(appearance),}); const renderVariant = (name: string, variables: Readonly<Record<string, string>>) => { const declarations = Object.entries(variables) .map(([variable, value]) => ` ${variable}: ${value};`) .join("\n"); return ` @variant ${name} {\n${declarations}\n }`;}; export const renderUniwindThemesCSS = () => { const variants = [ renderVariant("light", adaptiveVariablesFor("light")), renderVariant("dark", adaptiveVariablesFor("dark")), ...BUILT_IN_THEME_IDS.flatMap((themeId) => APPEARANCES.map((appearance) => renderVariant(`${themeId}-${appearance}`, variablesFor(themeId, appearance)), ), ), ]; return [ "/* Generated by scripts/generate-uniwind-themes.mts. Do not edit manually. */", "@layer theme {", " :root {", variants.join("\n\n"), " }", "}", "", ].join("\n");}; const readVariantBody = (css: string, appearance: MobileThemeAppearance): string => { const marker = `@variant ${appearance} {`; const markerIndex = css.indexOf(marker); if (markerIndex === -1) throw new Error(`Could not find ${marker} in global.css.`); const openingBraceIndex = css.indexOf("{", markerIndex); let depth = 0; for (let index = openingBraceIndex; index < css.length; index += 1) { if (css[index] === "{") depth += 1; if (css[index] !== "}") continue; depth -= 1; if (depth === 0) return css.slice(openingBraceIndex + 1, index); } throw new Error(`Could not find the end of ${marker} in global.css.`);}; export const readDefaultThemeVariables = (css: string) => Object.fromEntries( APPEARANCES.map((appearance) => { const body = readVariantBody(css, appearance); const variables = Object.fromEntries( MOBILE_THEME_VARIABLE_NAMES.map((name) => { const match = new RegExp(`^\\s*${name}:\\s*([^;]+);`, "mu").exec(body); if (!match?.[1]) { throw new Error(`Default ${appearance} theme is missing ${name}.`); } return [name, match[1].trim()]; }), ) as MobileThemeVariables; return [appearance, variables]; }), ) as Readonly<Record<MobileThemeAppearance, MobileThemeVariables>>; export const renderDefaultThemeVariablesJSON = (css: string) => `${JSON.stringify(readDefaultThemeVariables(css), null, 2)}\n`; export const getGeneratedUniwindThemeOutputs = (): ReadonlyArray< readonly [filename: string, contents: string]> => [ [GENERATED_CSS_PATH, renderUniwindThemesCSS()], [GENERATED_NAMES_PATH, `${JSON.stringify(customThemeNames, null, 2)}\n`], [ GENERATED_DEFAULT_VARIABLES_PATH, renderDefaultThemeVariablesJSON(NodeFS.readFileSync(GLOBAL_CSS_PATH, "utf8")), ],]; const writeFileAtomically = (filename: string, contents: string) => { const current = NodeFS.existsSync(filename) ? NodeFS.readFileSync(filename, "utf8") : null; if (current === contents) return; const temporaryFilename = `${filename}.${process.pid}.tmp`; try { NodeFS.writeFileSync(temporaryFilename, contents); NodeFS.renameSync(temporaryFilename, filename); } finally { if (NodeFS.existsSync(temporaryFilename)) NodeFS.unlinkSync(temporaryFilename); }}; if (import.meta.main) { const checkOnly = process.argv.includes("--check"); for (const [filename, contents] of getGeneratedUniwindThemeOutputs()) { if (checkOnly) { const current = NodeFS.existsSync(filename) ? NodeFS.readFileSync(filename, "utf8") : null; if (current !== contents) { console.error( `${NodePath.relative(process.cwd(), filename)} is stale. Run vp run --filter @t3tools/mobile generate.`, ); process.exitCode = 1; } continue; } // Metro watches the generated CSS. Replacing a complete temporary file keeps // Tailwind from compiling a partially rewritten theme file. writeFileAtomically(filename, contents); }}
#!/usr/bin/env node
import * as NodeFS from "node:fs";
import * as NodePath from "node:path";
import tailwindColors from "tailwindcss/colors";
import { BUILT_IN_THEME_IDS, type BuiltInThemeId } from "@t3tools/shared/themePalettes";
import {
getMobileThemeVariables,
MOBILE_THEME_VARIABLE_NAMES,
type MobileThemeAppearance,
type MobileThemeVariables,
} from "../src/lib/mobileTheme.ts";
const APPEARANCES = ["light", "dark"] as const;
const GLOBAL_CSS_PATH = NodePath.resolve(import.meta.dirname, "../global.css");
const GENERATED_CSS_PATH = NodePath.resolve(import.meta.dirname, "../generated-uniwind-themes.css");
const GENERATED_NAMES_PATH = NodePath.resolve(
import.meta.dirname,
"../generated-uniwind-theme-names.json",
);
const GENERATED_DEFAULT_VARIABLES_PATH = NodePath.resolve(
import.meta.dirname,
"../generated-uniwind-default-theme-variables.json",
);
type TailwindColorFamily = keyof typeof tailwindColors;
type TailwindColorShade = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950;
const color = (family: TailwindColorFamily, shade?: TailwindColorShade, opacity = 1): string => {
const familyColors = tailwindColors[family];
const value =
typeof familyColors === "string"
? shade === undefined
? familyColors
: undefined
: shade === undefined
? undefined
: familyColors[String(shade) as keyof typeof familyColors];
if (value === undefined) {
throw new Error(`Unknown Tailwind color ${family}${shade === undefined ? "" : `-${shade}`}.`);
}
if (opacity === 1) return value;
const percentage = Number((opacity * 100).toFixed(4));
const oklch = /^oklch\((.*)\)$/.exec(value);
if (oklch) return `oklch(${oklch[1]} / ${percentage}%)`;
if (value === "#fff") return `rgb(255 255 255 / ${percentage}%)`;
if (value === "#000") return `rgb(0 0 0 / ${percentage}%)`;
return `color-mix(in srgb, ${value} ${percentage}%, transparent)`;
};
// These replace the remaining dark:* utility pairs. A registered palette theme is
// neither literally `light` nor `dark`, so appearance-sensitive values must also be
// represented as semantic variables for custom themes.
const ADAPTIVE_COLORS = {
"--color-adaptive-amber-50-950-a40": [color("amber", 50), color("amber", 950, 0.4)],
"--color-adaptive-amber-200-900-a60": [color("amber", 200), color("amber", 900, 0.6)],
"--color-adaptive-amber-500-a12-a16": [color("amber", 500, 0.12), color("amber", 500, 0.16)],
"--color-adaptive-amber-700-300": [color("amber", 700), color("amber", 300)],
"--color-adaptive-amber-700-400": [color("amber", 700), color("amber", 400)],
"--color-adaptive-amber-800-200": [color("amber", 800), color("amber", 200)],
"--color-adaptive-blue-50-blue-400-a14": [color("blue", 50), color("blue", 400, 0.14)],
"--color-adaptive-blue-300-a50-blue-400-a28": [color("blue", 300, 0.5), color("blue", 400, 0.28)],
"--color-adaptive-blue-500-a20-blue-400-a15": [color("blue", 500, 0.2), color("blue", 400, 0.15)],
"--color-adaptive-blue-500-400": [color("blue", 500), color("blue", 400)],
"--color-adaptive-blue-600-400": [color("blue", 600), color("blue", 400)],
"--color-adaptive-black-a10-a25": [
color("black", undefined, 0.1),
color("black", undefined, 0.25),
],
"--color-adaptive-black-a15-a35": [
color("black", undefined, 0.15),
color("black", undefined, 0.35),
],
"--color-adaptive-emerald-500-a12-a16": [
color("emerald", 500, 0.12),
color("emerald", 500, 0.16),
],
"--color-adaptive-emerald-600-400": [color("emerald", 600), color("emerald", 400)],
"--color-adaptive-emerald-700-300": [color("emerald", 700), color("emerald", 300)],
"--color-adaptive-indigo-500-a12-a16": [color("indigo", 500, 0.12), color("indigo", 500, 0.16)],
"--color-adaptive-indigo-600-300": [color("indigo", 600), color("indigo", 300)],
"--color-adaptive-indigo-700-300": [color("indigo", 700), color("indigo", 300)],
"--color-adaptive-neutral-100-900": [color("neutral", 100), color("neutral", 900)],
"--color-adaptive-neutral-200-700-a60": [color("neutral", 200), color("neutral", 700, 0.6)],
"--color-adaptive-neutral-200-800": [color("neutral", 200), color("neutral", 800)],
"--color-adaptive-neutral-200-a70-white-a8": [
color("neutral", 200, 0.7),
color("white", undefined, 0.08),
],
"--color-adaptive-neutral-200-white-a6": [color("neutral", 200), color("white", undefined, 0.06)],
"--color-adaptive-neutral-200-white-a8": [color("neutral", 200), color("white", undefined, 0.08)],
"--color-adaptive-neutral-200-a80-white-a8": [
color("neutral", 200, 0.8),
color("white", undefined, 0.08),
],
"--color-adaptive-neutral-300-a60-white-a12": [
color("neutral", 300, 0.6),
color("white", undefined, 0.12),
],
"--color-adaptive-neutral-400-500": [color("neutral", 400), color("neutral", 500)],
"--color-adaptive-neutral-400-a60-500-a60": [
color("neutral", 400, 0.6),
color("neutral", 500, 0.6),
],
"--color-adaptive-neutral-400-a80-500-a80": [
color("neutral", 400, 0.8),
color("neutral", 500, 0.8),
],
"--color-adaptive-neutral-500-a10-a16": [color("neutral", 500, 0.1), color("neutral", 500, 0.16)],
"--color-adaptive-neutral-500-400": [color("neutral", 500), color("neutral", 400)],
"--color-adaptive-neutral-500-500": [color("neutral", 500), color("neutral", 500)],
"--color-adaptive-neutral-600-300": [color("neutral", 600), color("neutral", 300)],
"--color-adaptive-neutral-600-400": [color("neutral", 600), color("neutral", 400)],
"--color-adaptive-neutral-950-50": [color("neutral", 950), color("neutral", 50)],
"--color-adaptive-red-50-950-a80": [color("red", 50), color("red", 950, 0.8)],
"--color-adaptive-red-200-800": [color("red", 200), color("red", 800)],
"--color-adaptive-red-600-a80-400-a80": [color("red", 600, 0.8), color("red", 400, 0.8)],
"--color-adaptive-red-700-300": [color("red", 700), color("red", 300)],
"--color-adaptive-rose-100-500-a18": [color("rose", 100), color("rose", 500, 0.18)],
"--color-adaptive-rose-100-a80-500-a12": [color("rose", 100, 0.8), color("rose", 500, 0.12)],
"--color-adaptive-rose-300-a70-400-a28": [color("rose", 300, 0.7), color("rose", 400, 0.28)],
"--color-adaptive-rose-500-a12-a16": [color("rose", 500, 0.12), color("rose", 500, 0.16)],
"--color-adaptive-rose-500-400": [color("rose", 500), color("rose", 400)],
"--color-adaptive-rose-600-400": [color("rose", 600), color("rose", 400)],
"--color-adaptive-rose-700-300": [color("rose", 700), color("rose", 300)],
"--color-adaptive-sky-500-a12-a16": [color("sky", 500, 0.12), color("sky", 500, 0.16)],
"--color-adaptive-sky-600-400": [color("sky", 600), color("sky", 400)],
"--color-adaptive-sky-700-300": [color("sky", 700), color("sky", 300)],
"--color-adaptive-violet-500-a12-a16": [color("violet", 500, 0.12), color("violet", 500, 0.16)],
"--color-adaptive-violet-600-400": [color("violet", 600), color("violet", 400)],
"--color-adaptive-violet-700-300": [color("violet", 700), color("violet", 300)],
"--color-adaptive-white-neutral-950-a70": [color("white"), color("neutral", 950, 0.7)],
"--color-adaptive-zinc-500-a12-a16": [color("zinc", 500, 0.12), color("zinc", 500, 0.16)],
"--color-adaptive-zinc-500-400": [color("zinc", 500), color("zinc", 400)],
"--color-adaptive-zinc-600-300": [color("zinc", 600), color("zinc", 300)],
};
export const customThemeNames = BUILT_IN_THEME_IDS.flatMap((themeId) =>
APPEARANCES.map((appearance) => `${themeId}-${appearance}`),
);
const adaptiveVariablesFor = (appearance: MobileThemeAppearance) =>
Object.fromEntries(
Object.entries(ADAPTIVE_COLORS).map(([name, values]) => [
name,
values[appearance === "light" ? 0 : 1],
]),
);
const variablesFor = (themeId: BuiltInThemeId, appearance: MobileThemeAppearance) => ({
...getMobileThemeVariables(themeId, appearance),
...adaptiveVariablesFor(appearance),
});
const renderVariant = (name: string, variables: Readonly<Record<string, string>>) => {
const declarations = Object.entries(variables)
.map(([variable, value]) => ` ${variable}: ${value};`)
.join("\n");
return ` @variant ${name} {\n${declarations}\n }`;
};
export const renderUniwindThemesCSS = () => {
const variants = [
renderVariant("light", adaptiveVariablesFor("light")),
renderVariant("dark", adaptiveVariablesFor("dark")),
...BUILT_IN_THEME_IDS.flatMap((themeId) =>
APPEARANCES.map((appearance) =>
renderVariant(`${themeId}-${appearance}`, variablesFor(themeId, appearance)),
),
),
];
return [
"/* Generated by scripts/generate-uniwind-themes.mts. Do not edit manually. */",
"@layer theme {",
" :root {",
variants.join("\n\n"),
" }",
"}",
"",
].join("\n");
};
const readVariantBody = (css: string, appearance: MobileThemeAppearance): string => {
const marker = `@variant ${appearance} {`;
const markerIndex = css.indexOf(marker);
if (markerIndex === -1) throw new Error(`Could not find ${marker} in global.css.`);
const openingBraceIndex = css.indexOf("{", markerIndex);
let depth = 0;
for (let index = openingBraceIndex; index < css.length; index += 1) {
if (css[index] === "{") depth += 1;
if (css[index] !== "}") continue;
depth -= 1;
if (depth === 0) return css.slice(openingBraceIndex + 1, index);
}
throw new Error(`Could not find the end of ${marker} in global.css.`);
};
export const readDefaultThemeVariables = (css: string) =>
Object.fromEntries(
APPEARANCES.map((appearance) => {
const body = readVariantBody(css, appearance);
const variables = Object.fromEntries(
MOBILE_THEME_VARIABLE_NAMES.map((name) => {
const match = new RegExp(`^\\s*${name}:\\s*([^;]+);`, "mu").exec(body);
if (!match?.[1]) {
throw new Error(`Default ${appearance} theme is missing ${name}.`);
}
return [name, match[1].trim()];
}),
) as MobileThemeVariables;
return [appearance, variables];
}),
) as Readonly<Record<MobileThemeAppearance, MobileThemeVariables>>;
export const renderDefaultThemeVariablesJSON = (css: string) =>
`${JSON.stringify(readDefaultThemeVariables(css), null, 2)}\n`;
export const getGeneratedUniwindThemeOutputs = (): ReadonlyArray<
readonly [filename: string, contents: string]
> => [
[GENERATED_CSS_PATH, renderUniwindThemesCSS()],
[GENERATED_NAMES_PATH, `${JSON.stringify(customThemeNames, null, 2)}\n`],
[
GENERATED_DEFAULT_VARIABLES_PATH,
renderDefaultThemeVariablesJSON(NodeFS.readFileSync(GLOBAL_CSS_PATH, "utf8")),
],
];
const writeFileAtomically = (filename: string, contents: string) => {
const current = NodeFS.existsSync(filename) ? NodeFS.readFileSync(filename, "utf8") : null;
if (current === contents) return;
const temporaryFilename = `${filename}.${process.pid}.tmp`;
try {
NodeFS.writeFileSync(temporaryFilename, contents);
NodeFS.renameSync(temporaryFilename, filename);
} finally {
if (NodeFS.existsSync(temporaryFilename)) NodeFS.unlinkSync(temporaryFilename);
}
};
if (import.meta.main) {
const checkOnly = process.argv.includes("--check");
for (const [filename, contents] of getGeneratedUniwindThemeOutputs()) {
if (checkOnly) {
const current = NodeFS.existsSync(filename) ? NodeFS.readFileSync(filename, "utf8") : null;
if (current !== contents) {
console.error(
`${NodePath.relative(process.cwd(), filename)} is stale. Run vp run --filter @t3tools/mobile generate.`,
);
process.exitCode = 1;
}
continue;
}
// Metro watches the generated CSS. Replacing a complete temporary file keeps
// Tailwind from compiling a partially rewritten theme file.
writeFileAtomically(filename, contents);
}
}