From af2cfc36cb7faf80cc96e435cd2b08818dadcbe9 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 28 Oct 2026 14:38:53 +0200 Subject: [PATCH 1/4] feat: emit cross-file types and generics on svelte Svelte imports the types a component reads by name, under the source's names: a child's from its output beside the child, a `.ts` module's or a package's from the specifier as written (ADR-0067). An output that exports a type declares its types and type imports in ` + @@ -32,6 +34,18 @@ A component is that line, an instance script when it takes props or has a setup, (`$`-prefixed) is named `props`. Script lines are indented, but for lines inside a multi-line literal, which the target finds by parsing the code. ``, which the instance script reads, so + a consumer imports them from the component's file, `import type { BadgeProps } from +"./Badge.svelte"`. A generic component writes its type parameters in the instance script's + `generics`, ``); + return result( + ``, + ); } // --- Composition --------------------------------------------------------------------------- @@ -443,6 +544,18 @@ class ScriptWriter { if (!names.length) this.#names.importPassThrough(specifier); for (const name of names) this.#names.importPassThrough(specifier, name); } + // The types it reads, by name, under the source's names (ADR-0067): a child's from its + // output, `./Field.svelte`. Codegen's helper picks them; the script writes its own imports. + const types = new ImportSet(); + addTypeImports(component, this.#module, types, (name) => `${name}.svelte`); + for (const declaration of types.toDeclarations()) { + for (const specifier of declaration.specifiers) { + if (specifier.type !== "ImportSpecifier") continue; + const { imported, local } = specifier; + const name = imported.type === "Identifier" ? imported.name : imported.value; + this.#names.importType(declaration.source.value, name, local.name); + } + } } /** A key's value type as written, and the ref it holds: the module's own, or an imported one's. */ @@ -458,18 +571,26 @@ class ScriptWriter { } /** - * The ``; } diff --git a/packages/target-svelte/test/emit.test.ts b/packages/target-svelte/test/emit.test.ts index 2caea3428..5e087f1cf 100644 --- a/packages/target-svelte/test/emit.test.ts +++ b/packages/target-svelte/test/emit.test.ts @@ -57,6 +57,10 @@ const svelteFile = (...blocks: string[]) => `${[OPTIONS, ...blocks].join("\n\n") /** A ``; +/** A ``; + /** The lines that end the script of a component whose root takes its styled parents' scope. */ const SCOPE_HELPER = [ "", @@ -198,9 +202,8 @@ describe("svelte props", () => { // `count` and `hidden` are read by no expression: declared, they would be unused variables. expect((await emitSource(SHAPES.destructured)).contents).toBe( svelteFile( - script( - ' import type { ClassValue } from "svelte/elements";', - "", + // An output that exports a type declares its types in `"), - ); + const start = contents.indexOf('", start)); } // ADR-0045 to ADR-0049: the setup in source order, as runes; listeners, template refs and ids. The @@ -944,3 +949,61 @@ describe("svelte composition", () => { expect(field).not.toContain("className"); }); }); + +// ADR-0067, ADR-0068: types imported by name, a module's type exports in `", + ].join("\n"), + ); + expect(palette!.split('import type { Tone } from "./types";')).toHaveLength(2); + }); + + it("writes a generic component's type parameters in the instance script's generics", async () => { + const { "Picker.svelte": picker } = await caseOutputs("generics/scoped-slot"); + expect(picker).toContain(' + +", + "", + '', + '', + "", + ].join("\n"), + }); + const results = await toolchain.typecheck(Object.values(paths), context); + expect(results.get(paths["Pick.svelte"]!)).toEqual([]); + expect(results.get(paths["Use.svelte"]!)?.map(({ code, line }) => ({ code, line }))).toEqual([ + { code: "TS2322", line: 6 }, + ]); + }, + ); + it("leaves the Svelte compiler's warnings to L3", checker, async () => { const contents = '\n\n
a
\n\n'; diff --git a/tests/integration/cases/bindings/assigned-values/__output__/svelte/ReviewSummary.svelte b/tests/integration/cases/bindings/assigned-values/__output__/svelte/ReviewSummary.svelte index a2451fcef..2862e764f 100644 --- a/tests/integration/cases/bindings/assigned-values/__output__/svelte/ReviewSummary.svelte +++ b/tests/integration/cases/bindings/assigned-values/__output__/svelte/ReviewSummary.svelte @@ -1,14 +1,16 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/integration/cases/jsx/quoted-strings/__output__/svelte/DraftNotice.svelte b/tests/integration/cases/jsx/quoted-strings/__output__/svelte/DraftNotice.svelte index 36cf13d60..b0ddf5d9a 100644 --- a/tests/integration/cases/jsx/quoted-strings/__output__/svelte/DraftNotice.svelte +++ b/tests/integration/cases/jsx/quoted-strings/__output__/svelte/DraftNotice.svelte @@ -1,12 +1,14 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/toolchains/svelte/consumers/generics/constrained/MisuseProp.svelte b/tests/toolchains/svelte/consumers/generics/constrained/MisuseProp.svelte new file mode 100644 index 000000000..0ab11494f --- /dev/null +++ b/tests/toolchains/svelte/consumers/generics/constrained/MisuseProp.svelte @@ -0,0 +1,6 @@ + + + +
diff --git a/tests/toolchains/svelte/consumers/generics/list/Consumer.svelte b/tests/toolchains/svelte/consumers/generics/list/Consumer.svelte new file mode 100644 index 000000000..ba7698504 --- /dev/null +++ b/tests/toolchains/svelte/consumers/generics/list/Consumer.svelte @@ -0,0 +1,5 @@ + + + diff --git a/tests/toolchains/svelte/consumers/generics/list/MisuseProp.svelte b/tests/toolchains/svelte/consumers/generics/list/MisuseProp.svelte new file mode 100644 index 000000000..9626407a3 --- /dev/null +++ b/tests/toolchains/svelte/consumers/generics/list/MisuseProp.svelte @@ -0,0 +1,6 @@ + + + + diff --git a/tests/toolchains/svelte/consumers/generics/scoped-slot/Consumer.svelte b/tests/toolchains/svelte/consumers/generics/scoped-slot/Consumer.svelte new file mode 100644 index 000000000..1ad6e96b0 --- /dev/null +++ b/tests/toolchains/svelte/consumers/generics/scoped-slot/Consumer.svelte @@ -0,0 +1,9 @@ + + + + {#snippet item({ item })}{item.name} × {item.count}{/snippet} + diff --git a/tests/toolchains/svelte/consumers/generics/scoped-slot/MisuseSlot.svelte b/tests/toolchains/svelte/consumers/generics/scoped-slot/MisuseSlot.svelte new file mode 100644 index 000000000..6b5de869e --- /dev/null +++ b/tests/toolchains/svelte/consumers/generics/scoped-slot/MisuseSlot.svelte @@ -0,0 +1,10 @@ + + + + + {#snippet item({ item })}{item.nmae}{/snippet} + diff --git a/tests/toolchains/svelte/consumers/generics/select/Consumer.svelte b/tests/toolchains/svelte/consumers/generics/select/Consumer.svelte new file mode 100644 index 000000000..d6a1016f0 --- /dev/null +++ b/tests/toolchains/svelte/consumers/generics/select/Consumer.svelte @@ -0,0 +1,7 @@ + + + diff --git a/tests/toolchains/svelte/consumers/types/child-type-import/Consumer.svelte b/tests/toolchains/svelte/consumers/types/child-type-import/Consumer.svelte new file mode 100644 index 000000000..ef831b298 --- /dev/null +++ b/tests/toolchains/svelte/consumers/types/child-type-import/Consumer.svelte @@ -0,0 +1,8 @@ + + +
diff --git a/tests/toolchains/svelte/consumers/types/child-type-import/MisuseProp.svelte b/tests/toolchains/svelte/consumers/types/child-type-import/MisuseProp.svelte new file mode 100644 index 000000000..f50d5af5e --- /dev/null +++ b/tests/toolchains/svelte/consumers/types/child-type-import/MisuseProp.svelte @@ -0,0 +1,6 @@ + + + + diff --git a/tests/toolchains/svelte/consumers/types/child-type-import/MisuseType.svelte b/tests/toolchains/svelte/consumers/types/child-type-import/MisuseType.svelte new file mode 100644 index 000000000..f47eb7d7e --- /dev/null +++ b/tests/toolchains/svelte/consumers/types/child-type-import/MisuseType.svelte @@ -0,0 +1,8 @@ + + + diff --git a/tests/toolchains/svelte/consumers/types/exported-types/Consumer.svelte b/tests/toolchains/svelte/consumers/types/exported-types/Consumer.svelte new file mode 100644 index 000000000..3d4649c48 --- /dev/null +++ b/tests/toolchains/svelte/consumers/types/exported-types/Consumer.svelte @@ -0,0 +1,8 @@ + + +