diff --git a/src/__tests__/compiler/compiler.test.tsx b/src/__tests__/compiler/compiler.test.tsx index 7a9fbea8..df14aa16 100644 --- a/src/__tests__/compiler/compiler.test.tsx +++ b/src/__tests__/compiler/compiler.test.tsx @@ -1,3 +1,4 @@ +import { debug } from "debug"; import { compile } from "react-native-css/compiler"; test("hello world", () => { @@ -26,6 +27,39 @@ test("hello world", () => { }); }); +test("compiles bytes exactly as it compiles their text", () => { + const css = `:root { font-size: 16px; } .my-class { padding: 1rem; }`; + const fromText = compile(css).stylesheet(); + + expect(compile(new TextEncoder().encode(css)).stylesheet()).toStrictEqual( + fromText, + ); + expect(compile(Buffer.from(css)).stylesheet()).toStrictEqual(fromText); +}); + +test("the debug log is handed the decoded text, not the byte values", () => { + const css = `:root { font-size: 16px; }`; + const written: string[] = []; + const emit = debug.log; + + // `enable` flips the instance the compiler module already built, so no reload is needed. + debug.enable("react-native-css:compiler"); + debug.log = (...args: unknown[]): void => { + written.push(args.map(String).join(" ")); + }; + + try { + compile(new TextEncoder().encode(css)); + } finally { + debug.log = emit; + debug.disable(); + } + + const joined = written.join("\n"); + expect(joined).toContain("font-size: 16px"); + expect(joined).not.toContain("58,114"); +}); + test("reads global CSS variables", () => { const compiled = compile( `@layer theme { diff --git a/src/__tests__/compiler/public-surface-node-types.test.ts b/src/__tests__/compiler/public-surface-node-types.test.ts new file mode 100644 index 00000000..2b2d0e78 --- /dev/null +++ b/src/__tests__/compiler/public-surface-node-types.test.ts @@ -0,0 +1,48 @@ +import path from "node:path"; + +import ts from "typescript"; + +// `native/conditions/index.d.ts` imports `StyleRule` from `react-native-css/compiler`, so a Node +// global in that surface is a TS2591 inside `node_modules` for any consumer without `@types/node`. +const COMPILER_ENTRY = path.join(__dirname, "../../compiler/index.ts"); + +const OPTIONS: ts.CompilerOptions = { + lib: ["lib.es2024.d.ts"], + module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.Bundler, + noEmit: true, + skipLibCheck: true, + strict: true, + target: ts.ScriptTarget.ES2022, + types: [], +}; + +function checkCompilerSurface( + options: ts.CompilerOptions, +): readonly ts.Diagnostic[] { + return ts.getPreEmitDiagnostics(ts.createProgram([COMPILER_ENTRY], options)); +} + +function namesMissing( + diagnostics: readonly ts.Diagnostic[], + name: string, +): readonly string[] { + return diagnostics + .map((diagnostic) => + ts.flattenDiagnosticMessageText(diagnostic.messageText, " "), + ) + .filter((message) => message.includes(`Cannot find name '${name}'`)); +} + +test("no Node global reaches the compiler's public surface", () => { + expect(namesMissing(checkCompilerSurface(OPTIONS), "Buffer")).toStrictEqual( + [], + ); +}); + +test("the probe reports a missing global when one is genuinely absent", () => { + // Without this, the assertion above is satisfied by a program that resolved nothing. + const onES5 = checkCompilerSurface({ ...OPTIONS, lib: ["lib.es5.d.ts"] }); + + expect(onES5.length).toBeGreaterThan(0); +}); diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 214cd615..e8581fbb 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -46,7 +46,10 @@ const defaultLogger = debug("react-native-css:compiler"); * @param options - Compiler options * @returns A `ReactNativeCssStyleSheet` that can be passed to `StyleSheet.register` or used with a custom runtime */ -export function compile(code: Buffer | string, options: CompilerOptions = {}) { +export function compile( + code: Uint8Array | string, + options: CompilerOptions = {}, +) { const { logger = defaultLogger } = options; const isLoggerEnabled = @@ -60,9 +63,13 @@ export function compile(code: Buffer | string, options: CompilerOptions = {}) { logger(`Features ${JSON.stringify(features)}`); + // Decoded once: a `Uint8Array` that is not a `Buffer` stringifies to its bytes, not its text. + const source = + typeof code === "string" ? code : new TextDecoder().decode(code); + if (process.env.NODE_ENV !== "production") { if (defaultLogger.enabled) { - defaultLogger(code.toString()); + defaultLogger(source); } } @@ -89,8 +96,7 @@ export function compile(code: Buffer | string, options: CompilerOptions = {}) { // :root { font-size: Npx } in the CSS to allow CSS-based configuration. let effectiveRem: number | false = options.inlineRem ?? undefined!; if (effectiveRem === undefined) { - const css = typeof code === "string" ? code : code.toString(); - const match = css.match(/:root\s*\{[^}]*font-size:\s*([\d.]+)px/); + const match = source.match(/:root\s*\{[^}]*font-size:\s*([\d.]+)px/); effectiveRem = match?.[1] ? parseFloat(match[1]) : 14; options.inlineRem = effectiveRem; }