Skip to content

Scanner's sources and build's candidates refuse a readonly array, though each is only read #20706

Open

Description

@YevheniiKotyrlo

What version of Tailwind CSS are you using?

v4.3.5, and the insiders build of main (0.0.0-insiders.a3cb19c)

What build tool (or framework if it abstracts the build tool) are you using?

None: TypeScript 7.0.2 (tsc) type-checking code that calls @tailwindcss/node and @tailwindcss/oxide

What version of Node.js are you using?

v26.12.1

What browser are you using?

N/A

What operating system are you using?

Windows 11

Reproduction URL

https://github.com/YevheniiKotyrlo/tailwindcss-readonly-inputs

Describe your issue

@tailwindcss/oxide declares interface ScannerOptions { sources?: Array<SourceEntry> }, and the objects compile from tailwindcss and from @tailwindcss/node resolve to declare build(candidates: string[]). The scanner only reads its sources and build only iterates its candidates, but code that holds either as a readonly array can hand it to neither without copying it into a mutable one:

export const scanned = (sources: readonly SourceEntry[]): readonly string[] => new Scanner({ sources }).scan();

export const builtWithNode = async (css: string, candidates: readonly string[]): Promise<string> => {
  const compiler = await compile(css, { base: process.cwd(), onDependency: () => undefined });
  return compiler.build(candidates);
};
  1. git clone https://github.com/YevheniiKotyrlo/tailwindcss-readonly-inputs && cd tailwindcss-readonly-inputs
  2. npm install
  3. npm run check, which type-checks index.ts with strict: true.

Expected: no errors: ScannerOptions.sources takes a readonly SourceEntry[] and build a readonly string[], while a mutable array still type-checks at both.

Actual, on the insiders build of main:

System: MINGW64_NT-10.0-26300, node v26.12.1, npm 12.2.1, tailwindcss 0.0.0-insiders.a3cb19c, @tailwindcss/node 0.0.0-insiders.a3cb19c, @tailwindcss/oxide 0.0.0-insiders.a3cb19c, typescript 7.0.2
index.ts(5,94): error TS4104: The type 'readonly SourceEntry[]' is 'readonly' and cannot be assigned to the mutable type 'SourceEntry[]'.
index.ts(9,25): error TS4104: The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.
index.ts(14,25): error TS4104: The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.
SUMMARY: defect: 3 call(s) refuse a readonly array, taking only a mutable one

npm run check exits 1 while any of the three refuses the readonly array and 0 once index.ts type-checks. The declarations come from crates/node/src/lib.rs (pub sources: Option<Vec<SourceEntry>>, which NAPI-RS writes as Array<SourceEntry>) and packages/tailwindcss/src/index.ts (build(candidates: string[])).

Activity

  1. YevheniiKotyrlo commented on Nov 11, 2026

    @YevheniiKotyrlo
    Author

    The patches I apply to the published 4.3.3 declarations, which type the three as taking a readonly array:

    # @tailwindcss/oxide
    diff --git a/index.d.ts b/index.d.ts
    --- a/index.d.ts
    +++ b/index.d.ts
    @@ -36,7 +36,7 @@ export interface GlobEntry {
     
     export interface ScannerOptions {
       /** Glob sources */
    -  sources?: Array<SourceEntry>
    +  sources?: ReadonlyArray<SourceEntry>
     }
     
     export interface SourceEntry {
    # tailwindcss
    diff --git a/dist/lib.d.mts b/dist/lib.d.mts
    --- a/dist/lib.d.mts
    +++ b/dist/lib.d.mts
    @@ -359,7 +359,7 @@ declare function compileAst(input: AstNode[], opts?: CompileOptions): Promise<{
         }[];
         root: Root;
         features: Features;
    -    build(candidates: string[]): AstNode[];
    +    build(candidates: readonly string[]): AstNode[];
     }>;
     
     declare function compile(css: string, opts?: CompileOptions): Promise<{
    @@ -370,7 +370,7 @@ declare function compile(css: string, opts?: CompileOptions): Promise<{
         }[];
         root: Root;
         features: Features;
    -    build(candidates: string[]): string;
    +    build(candidates: readonly string[]): string;
         buildSourceMap(): DecodedSourceMap;
     }>;
     declare function __unstable__loadDesignSystem(css: string, opts?: CompileOptions): Promise<DesignSystem>;
    # @tailwindcss/node
    diff --git a/dist/index.d.mts b/dist/index.d.mts
    --- a/dist/index.d.mts
    +++ b/dist/index.d.mts
    @@ -185,7 +185,7 @@ declare function compileAst(ast: AstNode[], options: CompileOptions): Promise<{
             pattern: string;
         } | null;
         features: Features;
    -    build(candidates: string[]): AstNode[];
    +    build(candidates: readonly string[]): AstNode[];
     }>;
     declare function compile(css: string, options: CompileOptions): Promise<{
         sources: {
    @@ -198,7 +198,7 @@ declare function compile(css: string, options: CompileOptions): Promise<{
             pattern: string;
         } | null;
         features: Features;
    -    build(candidates: string[]): string;
    +    build(candidates: readonly string[]): string;
         buildSourceMap(): tailwindcss.DecodedSourceMap;
     }>;
     declare function __unstable__loadDesignSystem(css: string, { base }: {
    diff --git a/dist/index.d.ts b/dist/index.d.ts
    --- a/dist/index.d.ts
    +++ b/dist/index.d.ts
    @@ -185,7 +185,7 @@ declare function compileAst(ast: AstNode[], options: CompileOptions): Promise<{
             pattern: string;
         } | null;
         features: Features;
    -    build(candidates: string[]): AstNode[];
    +    build(candidates: readonly string[]): AstNode[];
     }>;
     declare function compile(css: string, options: CompileOptions): Promise<{
         sources: {
    @@ -198,7 +198,7 @@ declare function compile(css: string, options: CompileOptions): Promise<{
             pattern: string;
         } | null;
         features: Features;
    -    build(candidates: string[]): string;
    +    build(candidates: readonly string[]): string;
         buildSourceMap(): tailwindcss.DecodedSourceMap;
     }>;
     declare function __unstable__loadDesignSystem(css: string, { base }: {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions