From cb60841d98d3a9a25744ab0fc5fa35e1c7b08a65 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 28 Oct 2026 12:21:06 +0200 Subject: [PATCH 1/2] feat: emit cross-file types and generics on vue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vue imports the types its script reads by name, exports a module's other type exports in source order, writes `generic="…"` on a generic component's script, and prints the props as a type literal where a member comes from a package, which compiler-sfc cannot resolve under TypeScript 7 (ADR-0067, ADR-0068). The shared codegen helper writes the `import type` lines and re-exports the other script targets will call. Adds the twelve types and generics cases and `diagnostics/generic-slot-props`, with Vue's consumer fixtures, and quarantines the other targets' failing cells until their types lanes. A case Angular cannot compile yet runs its browser spec against a stand-in that fails every browser layer, so its cells can be quarantined. The L4-consumer canary also widens exported types and type re-exports. Co-authored-by: multica-agent --- ...ic-components-and-exported-public-types.md | 14 +- packages/analyzer/test/corpus.test.ts | 4 +- packages/codegen/README.md | 9 +- packages/codegen/src/exports.ts | 86 +- packages/codegen/src/index.ts | 12 +- packages/codegen/src/kinds.ts | 17 +- packages/codegen/src/references.ts | 46 +- packages/codegen/test/codegen.test.ts | 85 ++ packages/target-angular/test/helpers.ts | 16 +- packages/target-astro/test/workspace.ts | 10 +- packages/target-qwik/test/goldens.ts | 12 +- packages/target-react/test/fixtures.ts | 11 +- packages/target-solid/test/fixtures.ts | 11 +- packages/target-svelte/test/helpers.ts | 13 +- packages/target-svelte/test/typecheck.test.ts | 4 +- packages/target-vue/README.md | 21 +- packages/target-vue/src/script.ts | 74 +- packages/target-vue/src/toolchain/index.ts | 16 +- packages/target-vue/test/emit.test.ts | 74 ++ .../target-vue/test/framework-compile.test.ts | 39 +- packages/target-vue/test/helpers.ts | 8 + packages/testing/README.md | 21 +- packages/testing/src/browser/capabilities.ts | 13 + packages/testing/src/harness.ts | 7 + packages/testing/src/layers.ts | 31 + packages/testing/src/node/index.ts | 3 + packages/testing/src/node/matrix.ts | 19 +- packages/testing/src/node/summary.ts | 29 +- packages/testing/src/setup.ts | 23 +- packages/testing/test/summary.test.ts | 34 +- tests/integration/README.md | 22 +- .../generic-slot-props/Options.uf.tsx | 16 + .../__expected__/diagnostics.json | 19 + .../__expected__/ssr.default.html | 4 + .../__output__/angular/options.ts | 81 ++ .../__output__/astro/Options.astro | 23 + .../generic-slot-props/__output__/ir.json | 229 ++++++ .../__output__/qwik/Options.tsx | 32 + .../__output__/react/Options.tsx | 35 + .../__output__/solid/Options.tsx | 22 + .../__output__/svelte/Options.svelte | 28 + .../__output__/vue/Options.vue | 12 + .../diagnostics/generic-slot-props/case.json | 6 + .../cases/generics/constrained/Table.uf.tsx | 25 + .../constrained/__expected__/aria.people.yaml | 9 + .../constrained/__expected__/diagnostics.json | 1 + .../constrained/__expected__/dom.people.html | 23 + .../__expected__/geometry.people.json | 757 ++++++++++++++++++ .../constrained/__expected__/ssr.people.html | 23 + .../constrained/__output__/angular/table.ts | 68 ++ .../constrained/__output__/astro/Table.astro | 32 + .../generics/constrained/__output__/ir.json | 322 ++++++++ .../constrained/__output__/qwik/Table.tsx | 33 + .../constrained/__output__/react/Table.tsx | 42 + .../constrained/__output__/solid/Table.tsx | 33 + .../__output__/svelte/Table.svelte | 33 + .../constrained/__output__/vue/Table.vue | 20 + .../__screenshots__/people-chromium-linux.png | Bin 0 -> 5272 bytes .../cases/generics/constrained/case.json | 14 + .../generics/constrained/constrained.test.ts | 23 + .../cases/generics/list/List.uf.tsx | 18 + .../list/__expected__/aria.numbers.yaml | 4 + .../list/__expected__/aria.words.yaml | 3 + .../list/__expected__/diagnostics.json | 1 + .../list/__expected__/dom.numbers.html | 11 + .../generics/list/__expected__/dom.words.html | 8 + .../list/__expected__/geometry.numbers.json | 347 ++++++++ .../list/__expected__/geometry.words.json | 259 ++++++ .../list/__expected__/ssr.numbers.html | 11 + .../generics/list/__expected__/ssr.words.html | 8 + .../generics/list/__output__/angular/list.ts | 71 ++ .../generics/list/__output__/astro/List.astro | 27 + .../cases/generics/list/__output__/ir.json | 324 ++++++++ .../generics/list/__output__/qwik/List.tsx | 30 + .../generics/list/__output__/react/List.tsx | 40 + .../generics/list/__output__/solid/List.tsx | 31 + .../list/__output__/svelte/List.svelte | 28 + .../generics/list/__output__/vue/List.vue | 19 + .../numbers-chromium-linux.png | Bin 0 -> 943 bytes .../__screenshots__/words-chromium-linux.png | Bin 0 -> 803 bytes .../integration/cases/generics/list/case.json | 7 + .../cases/generics/list/list.test.ts | 21 + .../cases/generics/scoped-slot/Fruits.uf.tsx | 21 + .../cases/generics/scoped-slot/Picker.uf.tsx | 18 + .../scoped-slot/__expected__/aria.basket.yaml | 3 + .../scoped-slot/__expected__/diagnostics.json | 36 + .../scoped-slot/__expected__/dom.basket.html | 12 + .../__expected__/geometry.basket.json | 415 ++++++++++ .../scoped-slot/__expected__/ssr.basket.html | 12 + .../scoped-slot/__output__/angular/fruits.ts | 61 ++ .../scoped-slot/__output__/angular/picker.ts | 85 ++ .../scoped-slot/__output__/astro/Fruits.astro | 31 + .../scoped-slot/__output__/astro/Picker.astro | 26 + .../scoped-slot/__output__/ir.Picker.json | 244 ++++++ .../generics/scoped-slot/__output__/ir.json | 289 +++++++ .../scoped-slot/__output__/qwik/Fruits.tsx | 36 + .../scoped-slot/__output__/qwik/Picker.tsx | 36 + .../scoped-slot/__output__/react/Fruits.tsx | 42 + .../scoped-slot/__output__/react/Picker.tsx | 39 + .../scoped-slot/__output__/solid/Fruits.tsx | 32 + .../scoped-slot/__output__/solid/Picker.tsx | 26 + .../__output__/svelte/Fruits.svelte | 28 + .../__output__/svelte/Picker.svelte | 31 + .../scoped-slot/__output__/vue/Fruits.vue | 18 + .../scoped-slot/__output__/vue/Picker.vue | 16 + .../__screenshots__/basket-chromium-linux.png | Bin 0 -> 2370 bytes .../cases/generics/scoped-slot/case.json | 14 + .../generics/scoped-slot/scoped-slot.test.ts | 15 + .../cases/generics/select/Select.uf.tsx | 23 + .../cases/generics/select/SizeForm.uf.tsx | 17 + .../select/__expected__/aria.initial.yaml | 7 + .../select/__expected__/aria.large.yaml | 7 + .../select/__expected__/diagnostics.json | 36 + .../select/__expected__/dom.initial.html | 19 + .../select/__expected__/dom.large.html | 19 + .../select/__expected__/geometry.initial.json | 580 ++++++++++++++ .../select/__expected__/geometry.large.json | 580 ++++++++++++++ .../select/__expected__/ssr.default.html | 19 + .../select/__expected__/trace.large.json | 39 + .../select/__output__/angular/select.ts | 73 ++ .../select/__output__/angular/size-form.ts | 59 ++ .../select/__output__/astro/Select.astro | 30 + .../select/__output__/astro/SizeForm.astro | 27 + .../generics/select/__output__/ir.Select.json | 342 ++++++++ .../cases/generics/select/__output__/ir.json | 313 ++++++++ .../select/__output__/qwik/Select.tsx | 67 ++ .../select/__output__/qwik/SizeForm.tsx | 33 + .../select/__output__/react/Select.tsx | 56 ++ .../select/__output__/react/SizeForm.tsx | 31 + .../select/__output__/solid/Select.tsx | 68 ++ .../select/__output__/solid/SizeForm.tsx | 24 + .../select/__output__/svelte/Select.svelte | 36 + .../select/__output__/svelte/SizeForm.svelte | 26 + .../generics/select/__output__/vue/Select.vue | 15 + .../select/__output__/vue/SizeForm.vue | 16 + .../initial-chromium-linux.png | Bin 0 -> 2050 bytes .../__screenshots__/large-chromium-linux.png | Bin 0 -> 1897 bytes .../cases/generics/select/case.json | 4 + .../cases/generics/select/select.test.ts | 22 + .../types/child-type-import/Field.uf.tsx | 13 + .../cases/types/child-type-import/Form.uf.tsx | 11 + .../__expected__/aria.email.yaml | 3 + .../__expected__/diagnostics.json | 1 + .../__expected__/dom.email.html | 6 + .../__expected__/geometry.email.json | 249 ++++++ .../__expected__/ssr.email.html | 6 + .../__output__/angular/field.ts | 62 ++ .../__output__/angular/form.ts | 54 ++ .../__output__/astro/Field.astro | 26 + .../__output__/astro/Form.astro | 22 + .../__output__/ir.Field.json | 178 ++++ .../child-type-import/__output__/ir.json | 221 +++++ .../__output__/qwik/Field.tsx | 26 + .../__output__/qwik/Form.tsx | 21 + .../__output__/react/Field.tsx | 35 + .../__output__/react/Form.tsx | 29 + .../__output__/solid/Field.tsx | 22 + .../__output__/solid/Form.tsx | 19 + .../__output__/svelte/Field.svelte | 23 + .../__output__/svelte/Form.svelte | 21 + .../__output__/vue/Field.vue | 12 + .../child-type-import/__output__/vue/Form.vue | 11 + .../__screenshots__/email-chromium-linux.png | Bin 0 -> 789 bytes .../cases/types/child-type-import/case.json | 7 + .../child-type-import.test.ts | 16 + .../cases/types/exported-types/Button.uf.tsx | 18 + .../__expected__/aria.secondary.yaml | 1 + .../__expected__/diagnostics.json | 1 + .../__expected__/dom.secondary.html | 3 + .../__expected__/geometry.secondary.json | 93 +++ .../__expected__/ssr.secondary.html | 3 + .../__output__/angular/button.ts | 67 ++ .../__output__/astro/Button.astro | 29 + .../types/exported-types/__output__/ir.json | 214 +++++ .../exported-types/__output__/qwik/Button.tsx | 32 + .../__output__/react/Button.tsx | 56 ++ .../__output__/solid/Button.tsx | 43 + .../__output__/svelte/Button.svelte | 30 + .../exported-types/__output__/vue/Button.vue | 16 + .../secondary-chromium-linux.png | Bin 0 -> 1107 bytes .../cases/types/exported-types/case.json | 6 + .../exported-types/exported-types.test.ts | 16 + .../cases/types/extends-chain/Button.uf.tsx | 9 + .../__expected__/aria.disabled.yaml | 1 + .../__expected__/aria.enabled.yaml | 1 + .../__expected__/diagnostics.json | 1 + .../__expected__/dom.disabled.html | 3 + .../__expected__/dom.enabled.html | 3 + .../__expected__/geometry.disabled.json | 93 +++ .../__expected__/geometry.enabled.json | 93 +++ .../__expected__/ssr.disabled.html | 3 + .../__expected__/ssr.enabled.html | 3 + .../extends-chain/__output__/angular/base.ts | 4 + .../__output__/angular/button.ts | 64 ++ .../extends-chain/__output__/angular/types.ts | 9 + .../__output__/astro/Button.astro | 24 + .../extends-chain/__output__/astro/base.ts | 4 + .../extends-chain/__output__/astro/types.ts | 9 + .../types/extends-chain/__output__/ir.json | 266 ++++++ .../extends-chain/__output__/qwik/Button.tsx | 27 + .../extends-chain/__output__/qwik/base.ts | 4 + .../extends-chain/__output__/qwik/types.ts | 9 + .../extends-chain/__output__/react/Button.tsx | 37 + .../extends-chain/__output__/react/base.ts | 4 + .../extends-chain/__output__/react/types.ts | 9 + .../extends-chain/__output__/solid/Button.tsx | 26 + .../extends-chain/__output__/solid/base.ts | 4 + .../extends-chain/__output__/solid/types.ts | 9 + .../__output__/svelte/Button.svelte | 25 + .../extends-chain/__output__/svelte/base.ts | 4 + .../extends-chain/__output__/svelte/types.ts | 9 + .../extends-chain/__output__/vue/Button.vue | 9 + .../extends-chain/__output__/vue/base.ts | 4 + .../extends-chain/__output__/vue/types.ts | 9 + .../disabled-chromium-linux.png | Bin 0 -> 629 bytes .../enabled-chromium-linux.png | Bin 0 -> 983 bytes .../cases/types/extends-chain/base.ts | 4 + .../cases/types/extends-chain/case.json | 7 + .../types/extends-chain/extends-chain.test.ts | 21 + .../cases/types/extends-chain/types.ts | 9 + .../types/imported-interface/Badge.uf.tsx | 5 + .../__expected__/aria.danger.yaml | 1 + .../__expected__/aria.default-tone.yaml | 1 + .../__expected__/diagnostics.json | 1 + .../__expected__/dom.danger.html | 3 + .../__expected__/dom.default-tone.html | 3 + .../__expected__/geometry.danger.json | 93 +++ .../__expected__/geometry.default-tone.json | 93 +++ .../__expected__/ssr.danger.html | 3 + .../__expected__/ssr.default-tone.html | 3 + .../__output__/angular/badge.ts | 59 ++ .../__output__/angular/types.ts | 6 + .../__output__/astro/Badge.astro | 21 + .../__output__/astro/types.ts | 6 + .../imported-interface/__output__/ir.json | 207 +++++ .../__output__/qwik/Badge.tsx | 24 + .../__output__/qwik/types.ts | 6 + .../__output__/react/Badge.tsx | 48 ++ .../__output__/react/types.ts | 6 + .../__output__/solid/Badge.tsx | 35 + .../__output__/solid/types.ts | 6 + .../__output__/svelte/Badge.svelte | 22 + .../__output__/svelte/types.ts | 6 + .../__output__/vue/Badge.vue | 9 + .../__output__/vue/types.ts | 6 + .../__screenshots__/danger-chromium-linux.png | Bin 0 -> 1211 bytes .../default-tone-chromium-linux.png | Bin 0 -> 741 bytes .../cases/types/imported-interface/case.json | 7 + .../imported-interface.test.ts | 20 + .../cases/types/imported-interface/types.ts | 6 + .../types/intersection-props/Chip.uf.tsx | 7 + .../__expected__/aria.accent.yaml | 1 + .../__expected__/aria.small.yaml | 1 + .../__expected__/diagnostics.json | 1 + .../__expected__/dom.accent.html | 3 + .../__expected__/dom.small.html | 3 + .../__expected__/geometry.accent.json | 93 +++ .../__expected__/geometry.small.json | 93 +++ .../__expected__/ssr.accent.html | 3 + .../__expected__/ssr.small.html | 3 + .../__output__/angular/chip.ts | 61 ++ .../__output__/angular/types.ts | 7 + .../__output__/astro/Chip.astro | 23 + .../__output__/astro/types.ts | 7 + .../intersection-props/__output__/ir.json | 260 ++++++ .../__output__/qwik/Chip.tsx | 26 + .../__output__/qwik/types.ts | 7 + .../__output__/react/Chip.tsx | 51 ++ .../__output__/react/types.ts | 7 + .../__output__/solid/Chip.tsx | 35 + .../__output__/solid/types.ts | 7 + .../__output__/svelte/Chip.svelte | 24 + .../__output__/svelte/types.ts | 7 + .../__output__/vue/Chip.vue | 11 + .../__output__/vue/types.ts | 7 + .../__screenshots__/accent-chromium-linux.png | Bin 0 -> 609 bytes .../__screenshots__/small-chromium-linux.png | Bin 0 -> 799 bytes .../cases/types/intersection-props/case.json | 7 + .../intersection-props.test.ts | 20 + .../cases/types/intersection-props/types.ts | 7 + .../cases/types/package-types/Status.uf.tsx | 12 + .../__expected__/aria.closed.yaml | 1 + .../package-types/__expected__/aria.open.yaml | 1 + .../__expected__/diagnostics.json | 1 + .../__expected__/dom.closed.html | 3 + .../package-types/__expected__/dom.open.html | 3 + .../__expected__/geometry.closed.json | 93 +++ .../__expected__/geometry.open.json | 93 +++ .../__expected__/ssr.closed.html | 3 + .../package-types/__expected__/ssr.open.html | 3 + .../__output__/angular/status.ts | 59 ++ .../__output__/astro/Status.astro | 23 + .../types/package-types/__output__/ir.json | 201 +++++ .../package-types/__output__/qwik/Status.tsx | 22 + .../package-types/__output__/react/Status.tsx | 31 + .../package-types/__output__/solid/Status.tsx | 18 + .../__output__/svelte/Status.svelte | 24 + .../package-types/__output__/vue/Status.vue | 11 + .../__screenshots__/closed-chromium-linux.png | Bin 0 -> 1720 bytes .../__screenshots__/open-chromium-linux.png | Bin 0 -> 1401 bytes .../cases/types/package-types/case.json | 7 + .../types/package-types/package-types.test.ts | 20 + .../cases/types/shared-type/Palette.uf.tsx | 13 + .../cases/types/shared-type/Swatch.uf.tsx | 5 + .../shared-type/__expected__/aria.accent.yaml | 1 + .../shared-type/__expected__/diagnostics.json | 1 + .../shared-type/__expected__/dom.accent.html | 5 + .../__expected__/geometry.accent.json | 171 ++++ .../shared-type/__expected__/ssr.accent.html | 5 + .../shared-type/__output__/angular/palette.ts | 54 ++ .../shared-type/__output__/angular/swatch.ts | 55 ++ .../shared-type/__output__/angular/types.ts | 2 + .../__output__/astro/Palette.astro | 22 + .../shared-type/__output__/astro/Swatch.astro | 22 + .../shared-type/__output__/astro/types.ts | 2 + .../shared-type/__output__/ir.Swatch.json | 158 ++++ .../types/shared-type/__output__/ir.json | 218 +++++ .../shared-type/__output__/qwik/Palette.tsx | 21 + .../shared-type/__output__/qwik/Swatch.tsx | 24 + .../shared-type/__output__/qwik/types.ts | 2 + .../shared-type/__output__/react/Palette.tsx | 44 + .../shared-type/__output__/react/Swatch.tsx | 47 ++ .../shared-type/__output__/react/types.ts | 2 + .../shared-type/__output__/solid/Palette.tsx | 23 + .../shared-type/__output__/solid/Swatch.tsx | 34 + .../shared-type/__output__/solid/types.ts | 2 + .../__output__/svelte/Palette.svelte | 21 + .../__output__/svelte/Swatch.svelte | 22 + .../shared-type/__output__/svelte/types.ts | 2 + .../shared-type/__output__/vue/Palette.vue | 14 + .../shared-type/__output__/vue/Swatch.vue | 9 + .../types/shared-type/__output__/vue/types.ts | 2 + .../__screenshots__/accent-chromium-linux.png | Bin 0 -> 1005 bytes .../cases/types/shared-type/case.json | 7 + .../types/shared-type/shared-type.test.ts | 14 + .../cases/types/shared-type/types.ts | 2 + .../types/utility-types/TextField.uf.tsx | 14 + .../__expected__/aria.every-member.yaml | 3 + .../__expected__/aria.required-only.yaml | 2 + .../__expected__/diagnostics.json | 1 + .../__expected__/dom.every-member.html | 4 + .../__expected__/dom.required-only.html | 4 + .../__expected__/geometry.every-member.json | 171 ++++ .../__expected__/geometry.required-only.json | 171 ++++ .../__expected__/ssr.every-member.html | 4 + .../__expected__/ssr.required-only.html | 4 + .../__output__/angular/text-field.ts | 68 ++ .../utility-types/__output__/angular/types.ts | 7 + .../__output__/astro/TextField.astro | 24 + .../utility-types/__output__/astro/types.ts | 7 + .../types/utility-types/__output__/ir.json | 329 ++++++++ .../__output__/qwik/TextField.tsx | 24 + .../utility-types/__output__/qwik/types.ts | 7 + .../__output__/react/TextField.tsx | 35 + .../utility-types/__output__/react/types.ts | 7 + .../__output__/solid/TextField.tsx | 22 + .../utility-types/__output__/solid/types.ts | 7 + .../__output__/svelte/TextField.svelte | 33 + .../utility-types/__output__/svelte/types.ts | 7 + .../__output__/vue/TextField.vue | 17 + .../utility-types/__output__/vue/types.ts | 7 + .../every-member-chromium-linux.png | Bin 0 -> 3004 bytes .../required-only-chromium-linux.png | Bin 0 -> 789 bytes .../cases/types/utility-types/case.json | 14 + .../cases/types/utility-types/types.ts | 7 + .../types/utility-types/utility-types.test.ts | 25 + tests/integration/harness/canaries.ts | 96 ++- .../integration/harness/canaries.unit.test.ts | 52 +- tests/integration/harness/compile.test.ts | 9 + .../harness/coverage-exemptions.ts | 5 - tests/integration/harness/no-output.ts | 75 +- .../harness/no-output.unit.test.ts | 171 +++- tests/integration/harness/projects.ts | 11 +- .../integration/harness/quarantine/angular.ts | 38 +- tests/integration/harness/quarantine/astro.ts | 35 +- tests/integration/harness/quarantine/qwik.ts | 35 +- tests/integration/harness/quarantine/react.ts | 35 +- tests/integration/harness/quarantine/solid.ts | 34 +- .../integration/harness/quarantine/svelte.ts | 35 +- tests/toolchains/angular/output.oxlintrc.json | 6 +- tests/toolchains/astro/output.oxlintrc.json | 6 +- tests/toolchains/qwik/output.oxlintrc.json | 6 +- tests/toolchains/react/output.oxlintrc.json | 6 +- tests/toolchains/solid/output.oxlintrc.json | 6 +- tests/toolchains/svelte/output.oxlintrc.json | 6 +- .../generics/constrained/Consumer.vue | 7 + .../generics/constrained/MisuseProp.vue | 8 + .../vue/consumers/generics/list/Consumer.vue | 7 + .../consumers/generics/list/MisuseProp.vue | 8 + .../generics/scoped-slot/Consumer.vue | 11 + .../generics/scoped-slot/MisuseSlot.vue | 12 + .../consumers/generics/select/Consumer.vue | 11 + .../consumers/generics/select/MisuseModel.vue | 12 + .../types/child-type-import/Consumer.vue | 10 + .../types/child-type-import/MisuseProp.vue | 8 + .../types/child-type-import/MisuseType.vue | 12 + .../types/exported-types/Consumer.vue | 13 + .../types/exported-types/MisuseProp.vue | 8 + .../types/exported-types/MisuseType.vue | 15 + .../types/extends-chain/Consumer.vue | 7 + .../types/extends-chain/MisuseProp.vue | 8 + .../types/imported-interface/Consumer.vue | 7 + .../types/imported-interface/MisuseProp.vue | 8 + .../types/intersection-props/Consumer.vue | 7 + .../types/intersection-props/MisuseProp.vue | 8 + .../types/package-types/Consumer.vue | 7 + .../types/package-types/MisuseProp.vue | 8 + .../consumers/types/shared-type/Consumer.vue | 11 + .../types/shared-type/MisuseProp.vue | 8 + .../types/shared-type/MisuseType.vue | 12 + .../types/utility-types/Consumer.vue | 11 + .../types/utility-types/MisuseProp.vue | 8 + .../types/utility-types/MisuseType.vue | 12 + tests/toolchains/vue/output.oxlintrc.json | 6 +- 414 files changed, 15675 insertions(+), 163 deletions(-) create mode 100644 tests/integration/cases/diagnostics/generic-slot-props/Options.uf.tsx create mode 100644 tests/integration/cases/diagnostics/generic-slot-props/__expected__/diagnostics.json create mode 100644 tests/integration/cases/diagnostics/generic-slot-props/__expected__/ssr.default.html create mode 100644 tests/integration/cases/diagnostics/generic-slot-props/__output__/angular/options.ts create mode 100644 tests/integration/cases/diagnostics/generic-slot-props/__output__/astro/Options.astro create mode 100644 tests/integration/cases/diagnostics/generic-slot-props/__output__/ir.json create mode 100644 tests/integration/cases/diagnostics/generic-slot-props/__output__/qwik/Options.tsx create mode 100644 tests/integration/cases/diagnostics/generic-slot-props/__output__/react/Options.tsx create mode 100644 tests/integration/cases/diagnostics/generic-slot-props/__output__/solid/Options.tsx create mode 100644 tests/integration/cases/diagnostics/generic-slot-props/__output__/svelte/Options.svelte create mode 100644 tests/integration/cases/diagnostics/generic-slot-props/__output__/vue/Options.vue create mode 100644 tests/integration/cases/diagnostics/generic-slot-props/case.json create mode 100644 tests/integration/cases/generics/constrained/Table.uf.tsx create mode 100644 tests/integration/cases/generics/constrained/__expected__/aria.people.yaml create mode 100644 tests/integration/cases/generics/constrained/__expected__/diagnostics.json create mode 100644 tests/integration/cases/generics/constrained/__expected__/dom.people.html create mode 100644 tests/integration/cases/generics/constrained/__expected__/geometry.people.json create mode 100644 tests/integration/cases/generics/constrained/__expected__/ssr.people.html create mode 100644 tests/integration/cases/generics/constrained/__output__/angular/table.ts create mode 100644 tests/integration/cases/generics/constrained/__output__/astro/Table.astro create mode 100644 tests/integration/cases/generics/constrained/__output__/ir.json create mode 100644 tests/integration/cases/generics/constrained/__output__/qwik/Table.tsx create mode 100644 tests/integration/cases/generics/constrained/__output__/react/Table.tsx create mode 100644 tests/integration/cases/generics/constrained/__output__/solid/Table.tsx create mode 100644 tests/integration/cases/generics/constrained/__output__/svelte/Table.svelte create mode 100644 tests/integration/cases/generics/constrained/__output__/vue/Table.vue create mode 100644 tests/integration/cases/generics/constrained/__screenshots__/people-chromium-linux.png create mode 100644 tests/integration/cases/generics/constrained/case.json create mode 100644 tests/integration/cases/generics/constrained/constrained.test.ts create mode 100644 tests/integration/cases/generics/list/List.uf.tsx create mode 100644 tests/integration/cases/generics/list/__expected__/aria.numbers.yaml create mode 100644 tests/integration/cases/generics/list/__expected__/aria.words.yaml create mode 100644 tests/integration/cases/generics/list/__expected__/diagnostics.json create mode 100644 tests/integration/cases/generics/list/__expected__/dom.numbers.html create mode 100644 tests/integration/cases/generics/list/__expected__/dom.words.html create mode 100644 tests/integration/cases/generics/list/__expected__/geometry.numbers.json create mode 100644 tests/integration/cases/generics/list/__expected__/geometry.words.json create mode 100644 tests/integration/cases/generics/list/__expected__/ssr.numbers.html create mode 100644 tests/integration/cases/generics/list/__expected__/ssr.words.html create mode 100644 tests/integration/cases/generics/list/__output__/angular/list.ts create mode 100644 tests/integration/cases/generics/list/__output__/astro/List.astro create mode 100644 tests/integration/cases/generics/list/__output__/ir.json create mode 100644 tests/integration/cases/generics/list/__output__/qwik/List.tsx create mode 100644 tests/integration/cases/generics/list/__output__/react/List.tsx create mode 100644 tests/integration/cases/generics/list/__output__/solid/List.tsx create mode 100644 tests/integration/cases/generics/list/__output__/svelte/List.svelte create mode 100644 tests/integration/cases/generics/list/__output__/vue/List.vue create mode 100644 tests/integration/cases/generics/list/__screenshots__/numbers-chromium-linux.png create mode 100644 tests/integration/cases/generics/list/__screenshots__/words-chromium-linux.png create mode 100644 tests/integration/cases/generics/list/case.json create mode 100644 tests/integration/cases/generics/list/list.test.ts create mode 100644 tests/integration/cases/generics/scoped-slot/Fruits.uf.tsx create mode 100644 tests/integration/cases/generics/scoped-slot/Picker.uf.tsx create mode 100644 tests/integration/cases/generics/scoped-slot/__expected__/aria.basket.yaml create mode 100644 tests/integration/cases/generics/scoped-slot/__expected__/diagnostics.json create mode 100644 tests/integration/cases/generics/scoped-slot/__expected__/dom.basket.html create mode 100644 tests/integration/cases/generics/scoped-slot/__expected__/geometry.basket.json create mode 100644 tests/integration/cases/generics/scoped-slot/__expected__/ssr.basket.html create mode 100644 tests/integration/cases/generics/scoped-slot/__output__/angular/fruits.ts create mode 100644 tests/integration/cases/generics/scoped-slot/__output__/angular/picker.ts create mode 100644 tests/integration/cases/generics/scoped-slot/__output__/astro/Fruits.astro create mode 100644 tests/integration/cases/generics/scoped-slot/__output__/astro/Picker.astro create mode 100644 tests/integration/cases/generics/scoped-slot/__output__/ir.Picker.json create mode 100644 tests/integration/cases/generics/scoped-slot/__output__/ir.json create mode 100644 tests/integration/cases/generics/scoped-slot/__output__/qwik/Fruits.tsx create mode 100644 tests/integration/cases/generics/scoped-slot/__output__/qwik/Picker.tsx create mode 100644 tests/integration/cases/generics/scoped-slot/__output__/react/Fruits.tsx create mode 100644 tests/integration/cases/generics/scoped-slot/__output__/react/Picker.tsx create mode 100644 tests/integration/cases/generics/scoped-slot/__output__/solid/Fruits.tsx create mode 100644 tests/integration/cases/generics/scoped-slot/__output__/solid/Picker.tsx create mode 100644 tests/integration/cases/generics/scoped-slot/__output__/svelte/Fruits.svelte create mode 100644 tests/integration/cases/generics/scoped-slot/__output__/svelte/Picker.svelte create mode 100644 tests/integration/cases/generics/scoped-slot/__output__/vue/Fruits.vue create mode 100644 tests/integration/cases/generics/scoped-slot/__output__/vue/Picker.vue create mode 100644 tests/integration/cases/generics/scoped-slot/__screenshots__/basket-chromium-linux.png create mode 100644 tests/integration/cases/generics/scoped-slot/case.json create mode 100644 tests/integration/cases/generics/scoped-slot/scoped-slot.test.ts create mode 100644 tests/integration/cases/generics/select/Select.uf.tsx create mode 100644 tests/integration/cases/generics/select/SizeForm.uf.tsx create mode 100644 tests/integration/cases/generics/select/__expected__/aria.initial.yaml create mode 100644 tests/integration/cases/generics/select/__expected__/aria.large.yaml create mode 100644 tests/integration/cases/generics/select/__expected__/diagnostics.json create mode 100644 tests/integration/cases/generics/select/__expected__/dom.initial.html create mode 100644 tests/integration/cases/generics/select/__expected__/dom.large.html create mode 100644 tests/integration/cases/generics/select/__expected__/geometry.initial.json create mode 100644 tests/integration/cases/generics/select/__expected__/geometry.large.json create mode 100644 tests/integration/cases/generics/select/__expected__/ssr.default.html create mode 100644 tests/integration/cases/generics/select/__expected__/trace.large.json create mode 100644 tests/integration/cases/generics/select/__output__/angular/select.ts create mode 100644 tests/integration/cases/generics/select/__output__/angular/size-form.ts create mode 100644 tests/integration/cases/generics/select/__output__/astro/Select.astro create mode 100644 tests/integration/cases/generics/select/__output__/astro/SizeForm.astro create mode 100644 tests/integration/cases/generics/select/__output__/ir.Select.json create mode 100644 tests/integration/cases/generics/select/__output__/ir.json create mode 100644 tests/integration/cases/generics/select/__output__/qwik/Select.tsx create mode 100644 tests/integration/cases/generics/select/__output__/qwik/SizeForm.tsx create mode 100644 tests/integration/cases/generics/select/__output__/react/Select.tsx create mode 100644 tests/integration/cases/generics/select/__output__/react/SizeForm.tsx create mode 100644 tests/integration/cases/generics/select/__output__/solid/Select.tsx create mode 100644 tests/integration/cases/generics/select/__output__/solid/SizeForm.tsx create mode 100644 tests/integration/cases/generics/select/__output__/svelte/Select.svelte create mode 100644 tests/integration/cases/generics/select/__output__/svelte/SizeForm.svelte create mode 100644 tests/integration/cases/generics/select/__output__/vue/Select.vue create mode 100644 tests/integration/cases/generics/select/__output__/vue/SizeForm.vue create mode 100644 tests/integration/cases/generics/select/__screenshots__/initial-chromium-linux.png create mode 100644 tests/integration/cases/generics/select/__screenshots__/large-chromium-linux.png create mode 100644 tests/integration/cases/generics/select/case.json create mode 100644 tests/integration/cases/generics/select/select.test.ts create mode 100644 tests/integration/cases/types/child-type-import/Field.uf.tsx create mode 100644 tests/integration/cases/types/child-type-import/Form.uf.tsx create mode 100644 tests/integration/cases/types/child-type-import/__expected__/aria.email.yaml create mode 100644 tests/integration/cases/types/child-type-import/__expected__/diagnostics.json create mode 100644 tests/integration/cases/types/child-type-import/__expected__/dom.email.html create mode 100644 tests/integration/cases/types/child-type-import/__expected__/geometry.email.json create mode 100644 tests/integration/cases/types/child-type-import/__expected__/ssr.email.html create mode 100644 tests/integration/cases/types/child-type-import/__output__/angular/field.ts create mode 100644 tests/integration/cases/types/child-type-import/__output__/angular/form.ts create mode 100644 tests/integration/cases/types/child-type-import/__output__/astro/Field.astro create mode 100644 tests/integration/cases/types/child-type-import/__output__/astro/Form.astro create mode 100644 tests/integration/cases/types/child-type-import/__output__/ir.Field.json create mode 100644 tests/integration/cases/types/child-type-import/__output__/ir.json create mode 100644 tests/integration/cases/types/child-type-import/__output__/qwik/Field.tsx create mode 100644 tests/integration/cases/types/child-type-import/__output__/qwik/Form.tsx create mode 100644 tests/integration/cases/types/child-type-import/__output__/react/Field.tsx create mode 100644 tests/integration/cases/types/child-type-import/__output__/react/Form.tsx create mode 100644 tests/integration/cases/types/child-type-import/__output__/solid/Field.tsx create mode 100644 tests/integration/cases/types/child-type-import/__output__/solid/Form.tsx create mode 100644 tests/integration/cases/types/child-type-import/__output__/svelte/Field.svelte create mode 100644 tests/integration/cases/types/child-type-import/__output__/svelte/Form.svelte create mode 100644 tests/integration/cases/types/child-type-import/__output__/vue/Field.vue create mode 100644 tests/integration/cases/types/child-type-import/__output__/vue/Form.vue create mode 100644 tests/integration/cases/types/child-type-import/__screenshots__/email-chromium-linux.png create mode 100644 tests/integration/cases/types/child-type-import/case.json create mode 100644 tests/integration/cases/types/child-type-import/child-type-import.test.ts create mode 100644 tests/integration/cases/types/exported-types/Button.uf.tsx create mode 100644 tests/integration/cases/types/exported-types/__expected__/aria.secondary.yaml create mode 100644 tests/integration/cases/types/exported-types/__expected__/diagnostics.json create mode 100644 tests/integration/cases/types/exported-types/__expected__/dom.secondary.html create mode 100644 tests/integration/cases/types/exported-types/__expected__/geometry.secondary.json create mode 100644 tests/integration/cases/types/exported-types/__expected__/ssr.secondary.html create mode 100644 tests/integration/cases/types/exported-types/__output__/angular/button.ts create mode 100644 tests/integration/cases/types/exported-types/__output__/astro/Button.astro create mode 100644 tests/integration/cases/types/exported-types/__output__/ir.json create mode 100644 tests/integration/cases/types/exported-types/__output__/qwik/Button.tsx create mode 100644 tests/integration/cases/types/exported-types/__output__/react/Button.tsx create mode 100644 tests/integration/cases/types/exported-types/__output__/solid/Button.tsx create mode 100644 tests/integration/cases/types/exported-types/__output__/svelte/Button.svelte create mode 100644 tests/integration/cases/types/exported-types/__output__/vue/Button.vue create mode 100644 tests/integration/cases/types/exported-types/__screenshots__/secondary-chromium-linux.png create mode 100644 tests/integration/cases/types/exported-types/case.json create mode 100644 tests/integration/cases/types/exported-types/exported-types.test.ts create mode 100644 tests/integration/cases/types/extends-chain/Button.uf.tsx create mode 100644 tests/integration/cases/types/extends-chain/__expected__/aria.disabled.yaml create mode 100644 tests/integration/cases/types/extends-chain/__expected__/aria.enabled.yaml create mode 100644 tests/integration/cases/types/extends-chain/__expected__/diagnostics.json create mode 100644 tests/integration/cases/types/extends-chain/__expected__/dom.disabled.html create mode 100644 tests/integration/cases/types/extends-chain/__expected__/dom.enabled.html create mode 100644 tests/integration/cases/types/extends-chain/__expected__/geometry.disabled.json create mode 100644 tests/integration/cases/types/extends-chain/__expected__/geometry.enabled.json create mode 100644 tests/integration/cases/types/extends-chain/__expected__/ssr.disabled.html create mode 100644 tests/integration/cases/types/extends-chain/__expected__/ssr.enabled.html create mode 100644 tests/integration/cases/types/extends-chain/__output__/angular/base.ts create mode 100644 tests/integration/cases/types/extends-chain/__output__/angular/button.ts create mode 100644 tests/integration/cases/types/extends-chain/__output__/angular/types.ts create mode 100644 tests/integration/cases/types/extends-chain/__output__/astro/Button.astro create mode 100644 tests/integration/cases/types/extends-chain/__output__/astro/base.ts create mode 100644 tests/integration/cases/types/extends-chain/__output__/astro/types.ts create mode 100644 tests/integration/cases/types/extends-chain/__output__/ir.json create mode 100644 tests/integration/cases/types/extends-chain/__output__/qwik/Button.tsx create mode 100644 tests/integration/cases/types/extends-chain/__output__/qwik/base.ts create mode 100644 tests/integration/cases/types/extends-chain/__output__/qwik/types.ts create mode 100644 tests/integration/cases/types/extends-chain/__output__/react/Button.tsx create mode 100644 tests/integration/cases/types/extends-chain/__output__/react/base.ts create mode 100644 tests/integration/cases/types/extends-chain/__output__/react/types.ts create mode 100644 tests/integration/cases/types/extends-chain/__output__/solid/Button.tsx create mode 100644 tests/integration/cases/types/extends-chain/__output__/solid/base.ts create mode 100644 tests/integration/cases/types/extends-chain/__output__/solid/types.ts create mode 100644 tests/integration/cases/types/extends-chain/__output__/svelte/Button.svelte create mode 100644 tests/integration/cases/types/extends-chain/__output__/svelte/base.ts create mode 100644 tests/integration/cases/types/extends-chain/__output__/svelte/types.ts create mode 100644 tests/integration/cases/types/extends-chain/__output__/vue/Button.vue create mode 100644 tests/integration/cases/types/extends-chain/__output__/vue/base.ts create mode 100644 tests/integration/cases/types/extends-chain/__output__/vue/types.ts create mode 100644 tests/integration/cases/types/extends-chain/__screenshots__/disabled-chromium-linux.png create mode 100644 tests/integration/cases/types/extends-chain/__screenshots__/enabled-chromium-linux.png create mode 100644 tests/integration/cases/types/extends-chain/base.ts create mode 100644 tests/integration/cases/types/extends-chain/case.json create mode 100644 tests/integration/cases/types/extends-chain/extends-chain.test.ts create mode 100644 tests/integration/cases/types/extends-chain/types.ts create mode 100644 tests/integration/cases/types/imported-interface/Badge.uf.tsx create mode 100644 tests/integration/cases/types/imported-interface/__expected__/aria.danger.yaml create mode 100644 tests/integration/cases/types/imported-interface/__expected__/aria.default-tone.yaml create mode 100644 tests/integration/cases/types/imported-interface/__expected__/diagnostics.json create mode 100644 tests/integration/cases/types/imported-interface/__expected__/dom.danger.html create mode 100644 tests/integration/cases/types/imported-interface/__expected__/dom.default-tone.html create mode 100644 tests/integration/cases/types/imported-interface/__expected__/geometry.danger.json create mode 100644 tests/integration/cases/types/imported-interface/__expected__/geometry.default-tone.json create mode 100644 tests/integration/cases/types/imported-interface/__expected__/ssr.danger.html create mode 100644 tests/integration/cases/types/imported-interface/__expected__/ssr.default-tone.html create mode 100644 tests/integration/cases/types/imported-interface/__output__/angular/badge.ts create mode 100644 tests/integration/cases/types/imported-interface/__output__/angular/types.ts create mode 100644 tests/integration/cases/types/imported-interface/__output__/astro/Badge.astro create mode 100644 tests/integration/cases/types/imported-interface/__output__/astro/types.ts create mode 100644 tests/integration/cases/types/imported-interface/__output__/ir.json create mode 100644 tests/integration/cases/types/imported-interface/__output__/qwik/Badge.tsx create mode 100644 tests/integration/cases/types/imported-interface/__output__/qwik/types.ts create mode 100644 tests/integration/cases/types/imported-interface/__output__/react/Badge.tsx create mode 100644 tests/integration/cases/types/imported-interface/__output__/react/types.ts create mode 100644 tests/integration/cases/types/imported-interface/__output__/solid/Badge.tsx create mode 100644 tests/integration/cases/types/imported-interface/__output__/solid/types.ts create mode 100644 tests/integration/cases/types/imported-interface/__output__/svelte/Badge.svelte create mode 100644 tests/integration/cases/types/imported-interface/__output__/svelte/types.ts create mode 100644 tests/integration/cases/types/imported-interface/__output__/vue/Badge.vue create mode 100644 tests/integration/cases/types/imported-interface/__output__/vue/types.ts create mode 100644 tests/integration/cases/types/imported-interface/__screenshots__/danger-chromium-linux.png create mode 100644 tests/integration/cases/types/imported-interface/__screenshots__/default-tone-chromium-linux.png create mode 100644 tests/integration/cases/types/imported-interface/case.json create mode 100644 tests/integration/cases/types/imported-interface/imported-interface.test.ts create mode 100644 tests/integration/cases/types/imported-interface/types.ts create mode 100644 tests/integration/cases/types/intersection-props/Chip.uf.tsx create mode 100644 tests/integration/cases/types/intersection-props/__expected__/aria.accent.yaml create mode 100644 tests/integration/cases/types/intersection-props/__expected__/aria.small.yaml create mode 100644 tests/integration/cases/types/intersection-props/__expected__/diagnostics.json create mode 100644 tests/integration/cases/types/intersection-props/__expected__/dom.accent.html create mode 100644 tests/integration/cases/types/intersection-props/__expected__/dom.small.html create mode 100644 tests/integration/cases/types/intersection-props/__expected__/geometry.accent.json create mode 100644 tests/integration/cases/types/intersection-props/__expected__/geometry.small.json create mode 100644 tests/integration/cases/types/intersection-props/__expected__/ssr.accent.html create mode 100644 tests/integration/cases/types/intersection-props/__expected__/ssr.small.html create mode 100644 tests/integration/cases/types/intersection-props/__output__/angular/chip.ts create mode 100644 tests/integration/cases/types/intersection-props/__output__/angular/types.ts create mode 100644 tests/integration/cases/types/intersection-props/__output__/astro/Chip.astro create mode 100644 tests/integration/cases/types/intersection-props/__output__/astro/types.ts create mode 100644 tests/integration/cases/types/intersection-props/__output__/ir.json create mode 100644 tests/integration/cases/types/intersection-props/__output__/qwik/Chip.tsx create mode 100644 tests/integration/cases/types/intersection-props/__output__/qwik/types.ts create mode 100644 tests/integration/cases/types/intersection-props/__output__/react/Chip.tsx create mode 100644 tests/integration/cases/types/intersection-props/__output__/react/types.ts create mode 100644 tests/integration/cases/types/intersection-props/__output__/solid/Chip.tsx create mode 100644 tests/integration/cases/types/intersection-props/__output__/solid/types.ts create mode 100644 tests/integration/cases/types/intersection-props/__output__/svelte/Chip.svelte create mode 100644 tests/integration/cases/types/intersection-props/__output__/svelte/types.ts create mode 100644 tests/integration/cases/types/intersection-props/__output__/vue/Chip.vue create mode 100644 tests/integration/cases/types/intersection-props/__output__/vue/types.ts create mode 100644 tests/integration/cases/types/intersection-props/__screenshots__/accent-chromium-linux.png create mode 100644 tests/integration/cases/types/intersection-props/__screenshots__/small-chromium-linux.png create mode 100644 tests/integration/cases/types/intersection-props/case.json create mode 100644 tests/integration/cases/types/intersection-props/intersection-props.test.ts create mode 100644 tests/integration/cases/types/intersection-props/types.ts create mode 100644 tests/integration/cases/types/package-types/Status.uf.tsx create mode 100644 tests/integration/cases/types/package-types/__expected__/aria.closed.yaml create mode 100644 tests/integration/cases/types/package-types/__expected__/aria.open.yaml create mode 100644 tests/integration/cases/types/package-types/__expected__/diagnostics.json create mode 100644 tests/integration/cases/types/package-types/__expected__/dom.closed.html create mode 100644 tests/integration/cases/types/package-types/__expected__/dom.open.html create mode 100644 tests/integration/cases/types/package-types/__expected__/geometry.closed.json create mode 100644 tests/integration/cases/types/package-types/__expected__/geometry.open.json create mode 100644 tests/integration/cases/types/package-types/__expected__/ssr.closed.html create mode 100644 tests/integration/cases/types/package-types/__expected__/ssr.open.html create mode 100644 tests/integration/cases/types/package-types/__output__/angular/status.ts create mode 100644 tests/integration/cases/types/package-types/__output__/astro/Status.astro create mode 100644 tests/integration/cases/types/package-types/__output__/ir.json create mode 100644 tests/integration/cases/types/package-types/__output__/qwik/Status.tsx create mode 100644 tests/integration/cases/types/package-types/__output__/react/Status.tsx create mode 100644 tests/integration/cases/types/package-types/__output__/solid/Status.tsx create mode 100644 tests/integration/cases/types/package-types/__output__/svelte/Status.svelte create mode 100644 tests/integration/cases/types/package-types/__output__/vue/Status.vue create mode 100644 tests/integration/cases/types/package-types/__screenshots__/closed-chromium-linux.png create mode 100644 tests/integration/cases/types/package-types/__screenshots__/open-chromium-linux.png create mode 100644 tests/integration/cases/types/package-types/case.json create mode 100644 tests/integration/cases/types/package-types/package-types.test.ts create mode 100644 tests/integration/cases/types/shared-type/Palette.uf.tsx create mode 100644 tests/integration/cases/types/shared-type/Swatch.uf.tsx create mode 100644 tests/integration/cases/types/shared-type/__expected__/aria.accent.yaml create mode 100644 tests/integration/cases/types/shared-type/__expected__/diagnostics.json create mode 100644 tests/integration/cases/types/shared-type/__expected__/dom.accent.html create mode 100644 tests/integration/cases/types/shared-type/__expected__/geometry.accent.json create mode 100644 tests/integration/cases/types/shared-type/__expected__/ssr.accent.html create mode 100644 tests/integration/cases/types/shared-type/__output__/angular/palette.ts create mode 100644 tests/integration/cases/types/shared-type/__output__/angular/swatch.ts create mode 100644 tests/integration/cases/types/shared-type/__output__/angular/types.ts create mode 100644 tests/integration/cases/types/shared-type/__output__/astro/Palette.astro create mode 100644 tests/integration/cases/types/shared-type/__output__/astro/Swatch.astro create mode 100644 tests/integration/cases/types/shared-type/__output__/astro/types.ts create mode 100644 tests/integration/cases/types/shared-type/__output__/ir.Swatch.json create mode 100644 tests/integration/cases/types/shared-type/__output__/ir.json create mode 100644 tests/integration/cases/types/shared-type/__output__/qwik/Palette.tsx create mode 100644 tests/integration/cases/types/shared-type/__output__/qwik/Swatch.tsx create mode 100644 tests/integration/cases/types/shared-type/__output__/qwik/types.ts create mode 100644 tests/integration/cases/types/shared-type/__output__/react/Palette.tsx create mode 100644 tests/integration/cases/types/shared-type/__output__/react/Swatch.tsx create mode 100644 tests/integration/cases/types/shared-type/__output__/react/types.ts create mode 100644 tests/integration/cases/types/shared-type/__output__/solid/Palette.tsx create mode 100644 tests/integration/cases/types/shared-type/__output__/solid/Swatch.tsx create mode 100644 tests/integration/cases/types/shared-type/__output__/solid/types.ts create mode 100644 tests/integration/cases/types/shared-type/__output__/svelte/Palette.svelte create mode 100644 tests/integration/cases/types/shared-type/__output__/svelte/Swatch.svelte create mode 100644 tests/integration/cases/types/shared-type/__output__/svelte/types.ts create mode 100644 tests/integration/cases/types/shared-type/__output__/vue/Palette.vue create mode 100644 tests/integration/cases/types/shared-type/__output__/vue/Swatch.vue create mode 100644 tests/integration/cases/types/shared-type/__output__/vue/types.ts create mode 100644 tests/integration/cases/types/shared-type/__screenshots__/accent-chromium-linux.png create mode 100644 tests/integration/cases/types/shared-type/case.json create mode 100644 tests/integration/cases/types/shared-type/shared-type.test.ts create mode 100644 tests/integration/cases/types/shared-type/types.ts create mode 100644 tests/integration/cases/types/utility-types/TextField.uf.tsx create mode 100644 tests/integration/cases/types/utility-types/__expected__/aria.every-member.yaml create mode 100644 tests/integration/cases/types/utility-types/__expected__/aria.required-only.yaml create mode 100644 tests/integration/cases/types/utility-types/__expected__/diagnostics.json create mode 100644 tests/integration/cases/types/utility-types/__expected__/dom.every-member.html create mode 100644 tests/integration/cases/types/utility-types/__expected__/dom.required-only.html create mode 100644 tests/integration/cases/types/utility-types/__expected__/geometry.every-member.json create mode 100644 tests/integration/cases/types/utility-types/__expected__/geometry.required-only.json create mode 100644 tests/integration/cases/types/utility-types/__expected__/ssr.every-member.html create mode 100644 tests/integration/cases/types/utility-types/__expected__/ssr.required-only.html create mode 100644 tests/integration/cases/types/utility-types/__output__/angular/text-field.ts create mode 100644 tests/integration/cases/types/utility-types/__output__/angular/types.ts create mode 100644 tests/integration/cases/types/utility-types/__output__/astro/TextField.astro create mode 100644 tests/integration/cases/types/utility-types/__output__/astro/types.ts create mode 100644 tests/integration/cases/types/utility-types/__output__/ir.json create mode 100644 tests/integration/cases/types/utility-types/__output__/qwik/TextField.tsx create mode 100644 tests/integration/cases/types/utility-types/__output__/qwik/types.ts create mode 100644 tests/integration/cases/types/utility-types/__output__/react/TextField.tsx create mode 100644 tests/integration/cases/types/utility-types/__output__/react/types.ts create mode 100644 tests/integration/cases/types/utility-types/__output__/solid/TextField.tsx create mode 100644 tests/integration/cases/types/utility-types/__output__/solid/types.ts create mode 100644 tests/integration/cases/types/utility-types/__output__/svelte/TextField.svelte create mode 100644 tests/integration/cases/types/utility-types/__output__/svelte/types.ts create mode 100644 tests/integration/cases/types/utility-types/__output__/vue/TextField.vue create mode 100644 tests/integration/cases/types/utility-types/__output__/vue/types.ts create mode 100644 tests/integration/cases/types/utility-types/__screenshots__/every-member-chromium-linux.png create mode 100644 tests/integration/cases/types/utility-types/__screenshots__/required-only-chromium-linux.png create mode 100644 tests/integration/cases/types/utility-types/case.json create mode 100644 tests/integration/cases/types/utility-types/types.ts create mode 100644 tests/integration/cases/types/utility-types/utility-types.test.ts create mode 100644 tests/toolchains/vue/consumers/generics/constrained/Consumer.vue create mode 100644 tests/toolchains/vue/consumers/generics/constrained/MisuseProp.vue create mode 100644 tests/toolchains/vue/consumers/generics/list/Consumer.vue create mode 100644 tests/toolchains/vue/consumers/generics/list/MisuseProp.vue create mode 100644 tests/toolchains/vue/consumers/generics/scoped-slot/Consumer.vue create mode 100644 tests/toolchains/vue/consumers/generics/scoped-slot/MisuseSlot.vue create mode 100644 tests/toolchains/vue/consumers/generics/select/Consumer.vue create mode 100644 tests/toolchains/vue/consumers/generics/select/MisuseModel.vue create mode 100644 tests/toolchains/vue/consumers/types/child-type-import/Consumer.vue create mode 100644 tests/toolchains/vue/consumers/types/child-type-import/MisuseProp.vue create mode 100644 tests/toolchains/vue/consumers/types/child-type-import/MisuseType.vue create mode 100644 tests/toolchains/vue/consumers/types/exported-types/Consumer.vue create mode 100644 tests/toolchains/vue/consumers/types/exported-types/MisuseProp.vue create mode 100644 tests/toolchains/vue/consumers/types/exported-types/MisuseType.vue create mode 100644 tests/toolchains/vue/consumers/types/extends-chain/Consumer.vue create mode 100644 tests/toolchains/vue/consumers/types/extends-chain/MisuseProp.vue create mode 100644 tests/toolchains/vue/consumers/types/imported-interface/Consumer.vue create mode 100644 tests/toolchains/vue/consumers/types/imported-interface/MisuseProp.vue create mode 100644 tests/toolchains/vue/consumers/types/intersection-props/Consumer.vue create mode 100644 tests/toolchains/vue/consumers/types/intersection-props/MisuseProp.vue create mode 100644 tests/toolchains/vue/consumers/types/package-types/Consumer.vue create mode 100644 tests/toolchains/vue/consumers/types/package-types/MisuseProp.vue create mode 100644 tests/toolchains/vue/consumers/types/shared-type/Consumer.vue create mode 100644 tests/toolchains/vue/consumers/types/shared-type/MisuseProp.vue create mode 100644 tests/toolchains/vue/consumers/types/shared-type/MisuseType.vue create mode 100644 tests/toolchains/vue/consumers/types/utility-types/Consumer.vue create mode 100644 tests/toolchains/vue/consumers/types/utility-types/MisuseProp.vue create mode 100644 tests/toolchains/vue/consumers/types/utility-types/MisuseType.vue diff --git a/docs/adrs/0068-generic-components-and-exported-public-types.md b/docs/adrs/0068-generic-components-and-exported-public-types.md index f8014758e..8450b1a4d 100644 --- a/docs/adrs/0068-generic-components-and-exported-public-types.md +++ b/docs/adrs/0068-generic-components-and-exported-public-types.md @@ -3,7 +3,7 @@ - **Status:** Proposed - **Date:** 2026-10-24 - **Plan:** §4.2, §5.6 (layer 4), §6, §7.2 (L4), §9 M5; G2, P2, P4, P6; ADR-0033, ADR-0034, - ADR-0059, ADR-0067; amends ADR-0034 and ADR-0059 + ADR-0050, ADR-0059, ADR-0067; amends ADR-0034, ADR-0050 and ADR-0059 ## Context @@ -82,8 +82,16 @@ green in its own lane: `types/imported-interface`, `types/extends-chain`, `types `types/exported-types`, `generics/list`, `generics/select`, `generics/scoped-slot`, `generics/constrained`, and `diagnostics/generic-slot-props`. -**What it amends.** ADR-0034 accepts a generic component and a generic props declaration. ADR-0059 -gains the `type` kind and the generic-slot-props gap. +- **A harness rule for the types lanes:** a case whose L3 is quarantined on a target whose browser + build type-checks its output (Angular's) gets the browser stand-in of ADR-0050, so its spec + loads; its tests run, every browser layer fails with the stand-in's cause and is quarantined, + and the summary excuses their scenarios there, until the L3 entry goes. +- A diagnostics case its target has output for (UF4004 is a note) records its browser layers + `skip(no spec: …)` in the compile project: it has no spec, and its server render checks it. + +**What it amends.** ADR-0034 accepts a generic component and a generic props declaration. ADR-0050's +stand-in also serves a case its target cannot compile yet. ADR-0059 gains the `type` kind and the +generic-slot-props gap. ## Consequences diff --git a/packages/analyzer/test/corpus.test.ts b/packages/analyzer/test/corpus.test.ts index c31a7835c..23652894c 100644 --- a/packages/analyzer/test/corpus.test.ts +++ b/packages/analyzer/test/corpus.test.ts @@ -52,9 +52,9 @@ const NAMED_OTHERWISE: Readonly> = { /** * The diagnostics cases whose code `compile()` reports after the analyser, which finds nothing: - * the codegen scoper's (ADR-0063). + * the codegen scoper's (ADR-0063), and the capability check's (ADR-0068). */ -const AFTER_ANALYSIS: ReadonlySet = new Set(["unscopable-selector"]); +const AFTER_ANALYSIS: ReadonlySet = new Set(["unscopable-selector", "generic-slot-props"]); /** The warnings the corpus's feature cases hold on purpose. */ const WARNINGS: Readonly> = { diff --git a/packages/codegen/README.md b/packages/codegen/README.md index 42851028a..a112e7bf5 100644 --- a/packages/codegen/README.md +++ b/packages/codegen/README.md @@ -223,7 +223,14 @@ rewrite })` prints a component's root element or fragment, every node and attrib component's output keeps as written (a styling engine's, ADR-0064), and `addPassThroughImports` adds them to an `ImportSet`, which prints them after its sorted imports, in the IR's order; `componentTypes` and `typeDeclarationCode` copy the type declarations a component's props reach, exported as - the source does. `ImportSet` sorts imports and renames one that clashes with a name in its + the source does. `addTypeImports` adds the names of the module's `import type` declarations that + what a component's output prints reads (`liveTypeNames`), by the source's own names, from + `typeSpecifier`: a `.ts` module's or a package's as written, a `.uf.tsx` module's from the + output of its main component (`./Field.vue`, `./Field`); `typeExportsCode` writes the module's + type exports, an exported type no component reaches and a re-export, in the output of its main + component, each with its span so a target places it in source order (ADR-0067, ADR-0068). A + prop another file declares (`declaredBy`) is written in that file's names, so neither these nor + `liveTypes` and the kinds look its type up in the component's own module. `ImportSet` sorts imports and renames one that clashes with a name in its `NameScope`: a target reserves the source's names (`sourceNames`: the component, its types, bindings, every variable its expressions and setup code read or declare (`codeNames`), its functions' parameters, and the types its setup's annotations name (`typeNames`)) and claims its diff --git a/packages/codegen/src/exports.ts b/packages/codegen/src/exports.ts index 1cbab2ee2..c77091bcb 100644 --- a/packages/codegen/src/exports.ts +++ b/packages/codegen/src/exports.ts @@ -1,7 +1,12 @@ import type * as AST from "@oxc-project/types"; -import type { TypeDeclaration, UfComponent, UfExport, UfModule } from "@unframework/ir"; +import type { Span, TypeDeclaration, UfComponent, UfExport, UfModule } from "@unframework/ir"; +import { keyOwner } from "./imports.ts"; +import type { ImportSet } from "./imports.ts"; import { exportDefault, exportList, exportNamed, variableDeclaration } from "./js/builders.ts"; +import { parseStatementsSource } from "./parse.ts"; +import { liveTypeNames } from "./references.ts"; +import { collectNames } from "./rewrite.ts"; /** * Declares a component with the source's export shape. A lone default export or a lone export @@ -55,9 +60,84 @@ export function componentTypes(component: UfComponent, module: UfModule): TypeDe /** * A type declaration as the source writes it, exported when the source exports it: the - * outputs keep the export, which consumers type their props with (M5). Copied as text, never - * re-printed: oxc-codegen drops comments, and a member's doc comment is part of the type. + * outputs keep the export, which consumers type their props with (ADR-0068). Copied as text, + * never re-printed: oxc-codegen drops comments, and a member's doc comment is part of the type. */ export function typeDeclarationCode(declaration: TypeDeclaration): string { return declaration.exported ? `export ${declaration.code}` : declaration.code; } + +/** + * The specifier an output imports a module's types from (ADR-0067): a `.uf.tsx` module's are + * exported by the output of its main component (`keyOwner`), `file(name)` naming a component's + * output file as for `childImports`; any other specifier, a `.ts` module's or a package's, is + * kept as written, since every output tree holds the `.ts` modules unchanged (ADR-0070). + */ +export function typeSpecifier( + specifier: string, + module: UfModule, + file: (name: string) => string, +): string { + if (!specifier.endsWith(".uf.tsx")) return specifier; + const entry = module.imports?.find((each) => each.specifier === specifier); + // A module the source imports no component of has no API here: its main component is named + // as its file, as the analyser names a module's default export. + const stem = specifier.slice(specifier.lastIndexOf("/") + 1, -".uf.tsx".length); + const owner = (entry && keyOwner(entry.api.components)) ?? stem; + return `${specifier.slice(0, specifier.lastIndexOf("/") + 1)}${file(owner)}`; +} + +/** + * Adds the `import type` names a component's output reads (ADR-0067): each name of the module's + * type imports that what the output prints reads (`liveTypeNames`), under the source's own + * local name, from `typeSpecifier`. The output of the module's main component also writes the + * module's type exports (`typeExports`), and reads what their declarations name. + */ +export function addTypeImports( + component: UfComponent, + module: UfModule, + imports: ImportSet, + file: (name: string) => string, +): void { + if (!module.typeImports?.length) return; + const read = liveTypeNames(component, module, { client: true }); + if (keyOwner(module.components, module.exports) === component.name) { + for (const entry of module.typeExports ?? []) { + if (entry.kind !== "Declaration") continue; + const [statement] = parseStatementsSource(entry.code).statements; + collectNames(statement, read); + } + } + for (const entry of module.typeImports) { + for (const name of entry.names) { + if (!read.has(name.local)) continue; + imports.add(typeSpecifier(entry.specifier, module, file), name.imported, { + type: true, + local: name.local, + exact: true, + }); + } + } +} + +/** + * The module's type exports, as the output of its main component writes them (ADR-0068): an + * exported type no component reaches as written, and a re-export from `typeSpecifier`, each with + * its span, so a target can place it in source order among the types it copies. Every other + * component's output writes none. + */ +export function typeExportsCode( + component: UfComponent, + module: UfModule, + file: (name: string) => string, +): { code: string; span: Span }[] { + if (keyOwner(module.components, module.exports) !== component.name) return []; + return (module.typeExports ?? []).map((entry) => { + if (entry.kind === "Declaration") return { code: `export ${entry.code}`, span: entry.span }; + const names = entry.names.map(({ imported, exported }) => + imported === exported ? imported : `${imported} as ${exported}`, + ); + const specifier = JSON.stringify(typeSpecifier(entry.specifier, module, file)); + return { code: `export type { ${names.join(", ")} } from ${specifier};`, span: entry.span }; + }); +} diff --git a/packages/codegen/src/index.ts b/packages/codegen/src/index.ts index c1f4c074e..be1449a60 100644 --- a/packages/codegen/src/index.ts +++ b/packages/codegen/src/index.ts @@ -27,7 +27,15 @@ export type { ComponentModule } from "./js/print.ts"; export { Placeholders } from "./js/placeholders.ts"; export { isAlwaysNullish, nullishText, syntacticNullishness, textTemplate } from "./js/text.ts"; export type { Nullishness, TextPart } from "./js/text.ts"; -export { componentTypes, exportDeclaration, exportsOf, typeDeclarationCode } from "./exports.ts"; +export { + addTypeImports, + componentTypes, + exportDeclaration, + exportsOf, + typeDeclarationCode, + typeExportsCode, + typeSpecifier, +} from "./exports.ts"; export { bindingOf, codeKind, @@ -53,7 +61,7 @@ export { parseStatementsSource, } from "./parse.ts"; export type { CodeKind, ExpressionComment, ParsedExpression, ParsedStatements } from "./parse.ts"; -export { liveBindings, liveTypes, referencedBindings } from "./references.ts"; +export { liveBindings, liveTypeNames, liveTypes, referencedBindings } from "./references.ts"; export type { LiveOptions, ReferencedOptions } from "./references.ts"; export { functionBodyText, diff --git a/packages/codegen/src/kinds.ts b/packages/codegen/src/kinds.ts index d6fc51930..b34a6d5b3 100644 --- a/packages/codegen/src/kinds.ts +++ b/packages/codegen/src/kinds.ts @@ -154,6 +154,8 @@ class Kinds { readonly #module: UfModule; /** The declarations and bindings being read, against cycles. */ readonly #reading = new Set(); + /** Set while a prop another file declares is read: its names are not the module's. */ + #foreign = false; constructor(component: UfComponent, module: UfModule) { this.#component = component; @@ -194,7 +196,9 @@ class Kinds { /** Whether a local `type` alias names only primitives: an interface is an object. */ #alias(name: string): boolean { - const declaration = this.#module.types.find((each) => each.name === name); + const declaration = this.#foreign + ? undefined + : this.#module.types.find((each) => each.name === name); if (!declaration || this.#reading.has(name)) return false; const alias = /^type\s+[A-Za-z_$][\w$]*\s*=\s*([\s\S]*?);?\s*$/.exec(declaration.code); if (!alias) return false; @@ -377,7 +381,16 @@ class Kinds { switch (binding.kind) { case "prop": { const prop = this.#component.props.find((each) => each.binding === binding.id); - return prop !== undefined && this.type(prop.type.code); + if (prop === undefined) return false; + // A member another file declares is written in that file's names (ADR-0067): none of + // the module's own aliases is the one it names. + if (!prop.declaredBy) return this.type(prop.type.code); + this.#foreign = true; + try { + return this.type(prop.type.code); + } finally { + this.#foreign = false; + } } case "state": { const item = this.#component.setup.find( diff --git a/packages/codegen/src/references.ts b/packages/codegen/src/references.ts index 0542742e1..e73f56e01 100644 --- a/packages/codegen/src/references.ts +++ b/packages/codegen/src/references.ts @@ -247,15 +247,39 @@ export function liveTypes( module: UfModule, options: LiveOptions, ): TypeDeclaration[] { + const { declared, reached } = reachTypes(component, module, options); + return [...declared.values()].filter((declaration) => reached.has(declaration.name)); +} + +/** + * Every type name what a component's output prints reads, as {@link liveTypes} walks it: the + * names of the declarations it copies, and the names they and the component's own annotations + * read, such as the types it imports (ADR-0067), which a target imports only where they are read. + */ +export function liveTypeNames( + component: UfComponent, + module: UfModule, + options: LiveOptions, +): Set { + return reachTypes(component, module, options).names; +} + +function reachTypes( + component: UfComponent, + module: UfModule, + options: LiveOptions, +): { declared: Map; reached: Set; names: Set } { const declared = new Map( module.types .filter((declaration) => component.types.includes(declaration.name)) .map((declaration) => [declaration.name, declaration]), ); const reached = new Set(); + const names = new Set(); const pending: string[] = []; - const reach = (names: Iterable) => { - for (const name of names) { + const reach = (found: Iterable) => { + for (const name of found) { + names.add(name); if (!declared.has(name) || reached.has(name)) continue; reached.add(name); pending.push(name); @@ -271,7 +295,13 @@ export function liveTypes( code(written.body); }; if (component.propsParameter) type(component.propsParameter.type); - for (const prop of component.props) type(prop.type); + // A member another file declares is written in that file's names (ADR-0067), which the + // component's module does not declare or import. + for (const prop of component.props) if (!prop.declaredBy) type(prop.type); + for (const parameter of component.typeParameters ?? []) { + type(parameter.constraint); + type(parameter.default); + } // A slot's props are its public type, as a prop's is (ADR-0054). for (const slot of component.slots?.slots ?? []) type(slot.props); for (const declaration of declared.values()) { @@ -346,12 +376,12 @@ export function liveTypes( } for (let name = pending.pop(); name !== undefined; name = pending.pop()) { const [statement] = parseStatementsSource(declared.get(name)!.code).statements; - const names = new Set(); - collectNames(statement, names); - names.delete(name); - reach(names); + const found = new Set(); + collectNames(statement, found); + found.delete(name); + reach(found); } - return [...declared.values()].filter((declaration) => reached.has(declaration.name)); + return { declared, reached, names }; } /** Adds to `found` the bindings code references: read, called, written or emitted through. */ diff --git a/packages/codegen/test/codegen.test.ts b/packages/codegen/test/codegen.test.ts index 016d0e084..3662e8137 100644 --- a/packages/codegen/test/codegen.test.ts +++ b/packages/codegen/test/codegen.test.ts @@ -4,6 +4,7 @@ import { createBinding, createComponent, createElement, + createExport, createFor, createInterpolation, createModule, @@ -20,6 +21,7 @@ import { transformWithOxc } from "vite"; import { describe, expect, it } from "vitest"; import { + addTypeImports, componentTypes, exportDeclaration, formatOutput, @@ -40,6 +42,8 @@ import { printProgram, sourceNames, typeDeclarationCode, + typeExportsCode, + typeSpecifier, } from "../src/index.ts"; import type { JsxDialect } from "../src/index.ts"; import { expressionAt } from "./expressions.ts"; @@ -387,6 +391,87 @@ describe("type declarations", () => { }); }); +describe("type imports and exports (ADR-0067, ADR-0068)", () => { + /** A module whose main component, Badge, reads `Tone` and not `Hint`, which a member another file declares names. */ + function badgeModule() { + const props = [ + createProp("tone", false, createTypeText("Tone", at), at), + createProp("hint", true, createTypeText("Hint", at), at, undefined, undefined, { + file: "types.ts", + declaration: "Base", + span: at, + }), + ]; + const badge = createComponent("Badge", createElement("p", [], [], at), at, props); + badge.propsParameter = createPropsParameter( + "object", + createTypeText("BadgeProps", at), + at, + "props", + ); + const other = createComponent("Other", createElement("p", [], [], at), at); + const module = createModule( + "Badge.uf.tsx", + [badge, other], + [createExport("default", "Badge", at)], + [], + ); + module.typeImports = [ + { + specifier: "./types", + names: [ + { imported: "BadgeProps", local: "BadgeProps", span: at }, + { imported: "Tone", local: "Tone", span: at }, + { imported: "Hint", local: "Hint", span: at }, + { imported: "Size", local: "SizeName", span: at }, + ], + span: at, + }, + ]; + module.typeExports = [ + { kind: "Declaration", name: "Sizes", code: "type Sizes = SizeName[];", span: span(40, 64) }, + { + kind: "ReExport", + specifier: "./Field.uf.tsx", + names: [{ imported: "FieldProps", exported: "Props", span: at }], + span: span(10, 30), + }, + ]; + return { module, badge, other }; + } + + it("imports the names the output reads, as the source binds them, and none a foreign member names", () => { + const { module, badge, other } = badgeModule(); + const imports = new ImportSet(); + addTypeImports(badge, module, imports, (name) => `${name}.vue`); + expect(printProgram(js.program(imports.toDeclarations())).trim()).toBe( + 'import type { BadgeProps, Size as SizeName, Tone } from "./types";', + ); + // Another component of the module reads none, and writes none of its exports. + const others = new ImportSet(); + addTypeImports(other, module, others, (name) => `${name}.vue`); + expect(others.size).toBe(0); + expect(typeExportsCode(other, module, (name) => `${name}.vue`)).toEqual([]); + }); + + it("writes the module's type exports in its main component's output, from each target's file", () => { + const { module, badge } = badgeModule(); + expect(typeExportsCode(badge, module, (name) => `${name}.vue`)).toEqual([ + { code: "export type Sizes = SizeName[];", span: span(40, 64) }, + { code: 'export type { FieldProps as Props } from "./Field.vue";', span: span(10, 30) }, + ]); + }); + + it("imports a .uf.tsx module's types from its main component's output, and keeps any other specifier", () => { + const module = createModule("Form.uf.tsx"); + expect(typeSpecifier("./types", module, (name) => `${name}.vue`)).toBe("./types"); + expect(typeSpecifier("rxjs", module, (name) => `${name}.vue`)).toBe("rxjs"); + expect(typeSpecifier("../shared/Field.uf.tsx", module, (name) => name.toLowerCase())).toBe( + "../shared/field", + ); + }); +}); + describe("builders", () => { const print = (node: Parameters[0]) => printExpression(node); diff --git a/packages/target-angular/test/helpers.ts b/packages/target-angular/test/helpers.ts index dc0af9a3c..a7e347f93 100644 --- a/packages/target-angular/test/helpers.ts +++ b/packages/target-angular/test/helpers.ts @@ -49,9 +49,11 @@ function filesUnder(directory: string): string[] { .toSorted(); } -/** The committed Angular golden outputs of the integration corpus. */ +/** The committed Angular golden outputs of the integration corpus, but its types lane's. */ export function goldenFiles(): string[] { - return filesUnder(casesDir).filter((path) => /\/__output__\/angular\/[^/]+\.ts$/.test(path)); + return filesUnder(casesDir).filter( + (path) => /\/__output__\/angular\/[^/]+\.ts$/.test(path) && !TYPES_LANE.test(path), + ); } /** @@ -73,7 +75,8 @@ export function corpusSources(): { name: string; file: string; source: string }[ !name.startsWith("diagnostics/") && !COMPOSITION.test(name) && !STYLED_COMPOSITION.test(name) && - !BINDING_COMPOSITION.test(name), + !BINDING_COMPOSITION.test(name) && + !TYPES_LANE.test(`${name}/`), ); } @@ -92,6 +95,13 @@ const STYLED_COMPOSITION = */ const BINDING_COMPOSITION = /^bindings\/(?:style-expression|style-spread)$/; +/** + * M5's types and generics cases (ADR-0067, ADR-0068), which Angular emits from its types lane, layer + * 23 of M5 (UXF-465): until then `harness/quarantine/angular.ts` lists their failing cells, and the lane + * brings them back here. + */ +const TYPES_LANE = /(?:^|\/)(?:types|generics|diagnostics\/generic-slot-props)\//; + /** * The areas of M3's composition cases, which Angular reports (UF1002) until its M3 lane * (UXF-318) emits composition: `harness/quarantine/angular.ts` lists them, and the lane brings diff --git a/packages/target-astro/test/workspace.ts b/packages/target-astro/test/workspace.ts index ec231685c..f64988505 100644 --- a/packages/target-astro/test/workspace.ts +++ b/packages/target-astro/test/workspace.ts @@ -12,9 +12,17 @@ export const integrationRoot: string = join(repo, "tests", "integration"); /** The Astro toolchain directory: `@astrojs/check` on TypeScript 6, and its tsconfig. */ export const toolchainDir: string = join(repo, "tests", "toolchains", "astro"); -/** Every committed golden Astro output in the corpus. */ +/** + * M5's types and generics cases (ADR-0067, ADR-0068), which Astro emits from its types lane, layer + * 28 of M5 (UXF-467): until then `harness/quarantine/astro.ts` lists their failing cells, and the lane + * brings them back here. + */ +const TYPES_LANE = /(?:^|\/)(?:types|generics|diagnostics\/generic-slot-props)\//; + +/** Every committed golden Astro output in the corpus, but the cases of its types lane. */ export function goldenAstroFiles(): ToolchainFile[] { return globSync("cases/**/__output__/astro/*.astro", { cwd: integrationRoot }) + .filter((file) => !TYPES_LANE.test(file)) .toSorted() .map((file) => { const path = join(integrationRoot, file); diff --git a/packages/target-qwik/test/goldens.ts b/packages/target-qwik/test/goldens.ts index 2bbe6a343..69726fc1e 100644 --- a/packages/target-qwik/test/goldens.ts +++ b/packages/target-qwik/test/goldens.ts @@ -24,13 +24,21 @@ function expectsErrors(golden: string): boolean { ); } +/** + * M5's types and generics cases (ADR-0067, ADR-0068), which Qwik emits from its types lane, layer + * 25 of M5 (UXF-466): until then `harness/quarantine/qwik.ts` lists their failing cells, and the lane + * brings them back here. + */ +const TYPES_LANE = /(?:^|\/)(?:types|generics|diagnostics\/generic-slot-props)\//; + /** * The absolute paths of the goldens matching `pattern` (from the repository's root), sorted, but - * the scoped sheets (`.css`), which the compile project parses instead (ADR-0063). + * the scoped sheets (`.css`), which the compile project parses instead (ADR-0063), and + * the cases of its types lane (`TYPES_LANE`). */ export function goldens(pattern: string): string[] { return globSync(pattern, { cwd: repo }) .toSorted() .map((file) => join(repo, file)) - .filter((file) => !file.endsWith(".css") && !expectsErrors(file)); + .filter((file) => !file.endsWith(".css") && !expectsErrors(file) && !TYPES_LANE.test(file)); } diff --git a/packages/target-react/test/fixtures.ts b/packages/target-react/test/fixtures.ts index 75f1e71e1..780b7620e 100644 --- a/packages/target-react/test/fixtures.ts +++ b/packages/target-react/test/fixtures.ts @@ -130,16 +130,23 @@ export function lower(source: string, file = "Fixture.uf.tsx"): UfModule { return module; } +/** + * M5's types and generics cases (ADR-0067, ADR-0068), which React emits from its types lane, layer + * 16 of M5 (UXF-462): until then `harness/quarantine/react.ts` lists their failing cells, and the lane + * brings them back here. + */ +const TYPES_LANE = /(?:^|\/)(?:types|generics|diagnostics\/generic-slot-props)\//; + /** * Every committed golden output of a target, read from the integration corpus, but the scoped * sheets (`.css`), which no framework compiler or checker reads: the compile project - * parses them instead (ADR-0063). + * parses them instead (ADR-0063), and the cases of its types lane (`TYPES_LANE`). */ export function goldens(targetName: string): ToolchainFile[] { const pattern = new RegExp(`(?:^|/)__output__/${targetName}/[^/]+$`); return readdirSync(cases, { recursive: true }) .map((entry) => String(entry).split("\\").join("/")) - .filter((entry) => pattern.test(entry) && !entry.endsWith(".css")) + .filter((entry) => pattern.test(entry) && !entry.endsWith(".css") && !TYPES_LANE.test(entry)) .toSorted() .map((entry) => { const path = join(cases, entry); diff --git a/packages/target-solid/test/fixtures.ts b/packages/target-solid/test/fixtures.ts index 6b96a0ddd..4437ba382 100644 --- a/packages/target-solid/test/fixtures.ts +++ b/packages/target-solid/test/fixtures.ts @@ -88,16 +88,23 @@ export function emit( return { files: target.emit(component, context), reported }; } +/** + * M5's types and generics cases (ADR-0067, ADR-0068), which Solid emits from its types lane, layer + * 19 of M5 (UXF-463): until then `harness/quarantine/solid.ts` lists their failing cells, and the lane + * brings them back here. + */ +const TYPES_LANE = /(?:^|\/)(?:types|generics|diagnostics\/generic-slot-props)\//; + /** * Every committed golden output of a target, read from the integration corpus, but the scoped * sheets (`.css`), which no framework compiler or checker reads: the compile project - * parses them instead (ADR-0063). + * parses them instead (ADR-0063), and the cases of its types lane (`TYPES_LANE`). */ export function goldens(targetName: string): ToolchainFile[] { const pattern = new RegExp(`(?:^|/)__output__/${targetName}/[^/]+$`); return readdirSync(cases, { recursive: true }) .map((entry) => String(entry).split("\\").join("/")) - .filter((entry) => pattern.test(entry) && !entry.endsWith(".css")) + .filter((entry) => pattern.test(entry) && !entry.endsWith(".css") && !TYPES_LANE.test(entry)) .toSorted() .map((entry) => { const path = join(cases, entry); diff --git a/packages/target-svelte/test/helpers.ts b/packages/target-svelte/test/helpers.ts index 1ea778b07..99de214cc 100644 --- a/packages/target-svelte/test/helpers.ts +++ b/packages/target-svelte/test/helpers.ts @@ -27,9 +27,18 @@ function filesUnder(directory: string): string[] { .toSorted(); } -/** The committed Svelte golden outputs of the integration corpus. */ +/** + * M5's types and generics cases (ADR-0067, ADR-0068), which Svelte emits from its types lane, layer + * 21 of M5 (UXF-464): until then `harness/quarantine/svelte.ts` lists their failing cells, and the lane + * brings them back here. + */ +export const TYPES_LANE: RegExp = /(?:^|\/)(?:types|generics|diagnostics\/generic-slot-props)\//; + +/** The committed Svelte golden outputs of the integration corpus, but its types lane's. */ export function goldenFiles(): string[] { - return filesUnder(casesDir).filter((path) => /\/__output__\/svelte\/[^/]+\.svelte$/.test(path)); + return filesUnder(casesDir).filter( + (path) => /\/__output__\/svelte\/[^/]+\.svelte$/.test(path) && !TYPES_LANE.test(path), + ); } /** diff --git a/packages/target-svelte/test/typecheck.test.ts b/packages/target-svelte/test/typecheck.test.ts index ad267b0a0..3826cf829 100644 --- a/packages/target-svelte/test/typecheck.test.ts +++ b/packages/target-svelte/test/typecheck.test.ts @@ -12,6 +12,7 @@ import { packageDir, removeScratch, toolchainDir, + TYPES_LANE, writeScratch, } from "./helpers.ts"; @@ -57,7 +58,8 @@ describe("svelte typecheck", () => { expect(golden.length).toBeGreaterThan(0); // Each case in a directory of its own, where its sources' outputs import each other. const fresh: string[] = []; - for (const { modules } of corpus()) { + for (const { name, modules } of corpus()) { + if (TYPES_LANE.test(`${name}/`)) continue; const emitted: Record = {}; for (const module of modules) { for (const file of await emitFormatted(module)) emitted[file.path] = file.contents; diff --git a/packages/target-vue/README.md b/packages/target-vue/README.md index f8f7bda41..4d935a77f 100644 --- a/packages/target-vue/README.md +++ b/packages/target-vue/README.md @@ -14,10 +14,20 @@ setup as written, `.value` included (ADR-0045 to ADR-0049). In order: 1. **The imports** from `vue` (`ref`, `shallowRef`, `computed`, `watch`, `watchPostEffect`, `onMounted`, `onUnmounted`, `useTemplateRef`, `useId`, `nextTick`), each under a name no - source name takes. + source name takes, and the source's `import type` names the script reads, by name + (ADR-0067): from a `.ts` module or a package as written, `import type { BadgeProps } from +"./types"`, and from a child's output, `import Field, { type FieldProps } from "./Field.vue"`. 2. **The type declarations** the component reaches (its props, its events and its setup code), - copied as written. -3. **`defineProps

()`** with the source's annotation. + copied as written, and in the output of the module's main component its other type exports, + an exported type no component reaches and a re-export (`export type { Tone } from "./types"`), + all in source order (ADR-0068). +3. **`defineProps

()`** with the source's annotation, which compiler-sfc resolves through the + output tree: relative modules, `extends` chains, intersections and the five utility types. + A member a package declares is the exception: compiler-sfc resolves a package only through + TypeScript's classic API, which TypeScript 7 lacks, so the props are printed as a type literal + of their members, `defineProps<{ closed: boolean; label: string }>()` (ADR-0067). A generic + component's block takes its type parameters, ``, + block: ``, rewrite: template, listeners, components, @@ -252,15 +259,68 @@ function keyBlock(component: UfComponent, module: UfModule, names: VueNames): st } return `export const ${key.name}: ${injectionKey}<${type}> = Symbol(${JSON.stringify(key.description)});`; }); - const types = componentTypes(component, module).map(typeDeclarationCode); + addTypeImports(component, module, imports, (name) => `${name}.vue`); const code = [ printProgram(js.program(imports.toDeclarations())).trim(), - ...types, + ...moduleTypes(component, module), declarations.join("\n"), ].join("\n\n"); return ``; } +/** + * The types a component's output declares, in source order: those it reaches, copied as + * written, and in the output of the module's main component the module's type exports + * (ADR-0068). + */ +function moduleTypes(component: UfComponent, module: UfModule): string[] { + return [ + ...componentTypes(component, module).map((declaration) => ({ + code: typeDeclarationCode(declaration), + span: declaration.span, + })), + ...typeExportsCode(component, module, (name) => `${name}.vue`), + ] + .toSorted((a, b) => a.span.start - b.span.start) + .map(({ code }) => code); +} + +/** + * The `generic` attribute of a generic component's ` + +

    + {#each options as option (option.value)} +
  • {@render optionSnippet?.({ option })}
  • + {/each} +
diff --git a/tests/integration/cases/diagnostics/generic-slot-props/__output__/vue/Options.vue b/tests/integration/cases/diagnostics/generic-slot-props/__output__/vue/Options.vue new file mode 100644 index 000000000..129252ccb --- /dev/null +++ b/tests/integration/cases/diagnostics/generic-slot-props/__output__/vue/Options.vue @@ -0,0 +1,12 @@ + + + diff --git a/tests/integration/cases/diagnostics/generic-slot-props/case.json b/tests/integration/cases/diagnostics/generic-slot-props/case.json new file mode 100644 index 000000000..524731c2d --- /dev/null +++ b/tests/integration/cases/diagnostics/generic-slot-props/case.json @@ -0,0 +1,6 @@ +{ + "description": "A generic component's scoped slot whose props name its type parameter: Angular's fill takes the constraint (UF4004, info).", + "ssr": { + "default": { "props": { "options": [{ "value": "a" }, { "value": "b" }] } } + } +} diff --git a/tests/integration/cases/generics/constrained/Table.uf.tsx b/tests/integration/cases/generics/constrained/Table.uf.tsx new file mode 100644 index 000000000..403216f0e --- /dev/null +++ b/tests/integration/cases/generics/constrained/Table.uf.tsx @@ -0,0 +1,25 @@ +// A type parameter with a constraint (ADR-0068): the rows are keyed and labelled by members the +// constraint declares. +export interface TableProps { + caption: string; + rows: T[]; +} + +export default function Table({ + caption, + rows, +}: TableProps) { + return ( + + + + {rows.map((row) => ( + + + + + ))} + +
{caption}
{row.id}{row.label}
+ ); +} diff --git a/tests/integration/cases/generics/constrained/__expected__/aria.people.yaml b/tests/integration/cases/generics/constrained/__expected__/aria.people.yaml new file mode 100644 index 000000000..cb51ab411 --- /dev/null +++ b/tests/integration/cases/generics/constrained/__expected__/aria.people.yaml @@ -0,0 +1,9 @@ +- table "People": + - caption: People + - rowgroup: + - row "ada Ada Lovelace": + - rowheader "ada" + - cell "Ada Lovelace" + - row "grace Grace Hopper": + - rowheader "grace" + - cell "Grace Hopper" diff --git a/tests/integration/cases/generics/constrained/__expected__/diagnostics.json b/tests/integration/cases/generics/constrained/__expected__/diagnostics.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/tests/integration/cases/generics/constrained/__expected__/diagnostics.json @@ -0,0 +1 @@ +[] diff --git a/tests/integration/cases/generics/constrained/__expected__/dom.people.html b/tests/integration/cases/generics/constrained/__expected__/dom.people.html new file mode 100644 index 000000000..e8d1dabf0 --- /dev/null +++ b/tests/integration/cases/generics/constrained/__expected__/dom.people.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + +
+ "People" +
+ "ada" + + "Ada Lovelace" +
+ "grace" + + "Grace Hopper" +
diff --git a/tests/integration/cases/generics/constrained/__expected__/geometry.people.json b/tests/integration/cases/generics/constrained/__expected__/geometry.people.json new file mode 100644 index 000000000..63523a8b4 --- /dev/null +++ b/tests/integration/cases/generics/constrained/__expected__/geometry.people.json @@ -0,0 +1,757 @@ +{ + "version": 1, + "nodes": { + "table[0]": { + "box": [ + 8, + 8, + 160.984375, + 82 + ], + "style": { + "display": "table", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "border-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "table[0]/caption[0]": { + "box": [ + 8, + 8, + 160.984375, + 24 + ], + "style": { + "display": "table-caption", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "-webkit-center", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "table[0]/caption[0]/#text[0]": { + "box": [ + [ + 62.453125, + 10, + 52.0625, + 20 + ] + ] + }, + "table[0]/tbody[0]": { + "box": [ + 10, + 34, + 156.984375, + 54 + ], + "style": { + "display": "table-row-group", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "middle", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "table[0]/tbody[0]/tr[0]": { + "box": [ + 10, + 34, + 156.984375, + 26 + ], + "style": { + "display": "table-row", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "middle", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "table[0]/tbody[0]/tr[0]/th[0]": { + "box": [ + 10, + 34, + 46.640625, + 26 + ], + "style": { + "display": "table-cell", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "1px", + "padding-right": "1px", + "padding-bottom": "1px", + "padding-left": "1px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "700", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "center", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "middle", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "table[0]/tbody[0]/tr[0]/th[0]/#text[0]": { + "box": [ + [ + 18.984375, + 37, + 28.671875, + 20 + ] + ] + }, + "table[0]/tbody[0]/tr[0]/td[0]": { + "box": [ + 58.640625, + 34, + 108.34375, + 26 + ], + "style": { + "display": "table-cell", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "1px", + "padding-right": "1px", + "padding-bottom": "1px", + "padding-left": "1px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "middle", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "table[0]/tbody[0]/tr[0]/td[0]/#text[0]": { + "box": [ + [ + 59.640625, + 37, + 101.984375, + 20 + ] + ] + }, + "table[0]/tbody[0]/tr[1]": { + "box": [ + 10, + 62, + 156.984375, + 26 + ], + "style": { + "display": "table-row", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "middle", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "table[0]/tbody[0]/tr[1]/th[0]": { + "box": [ + 10, + 62, + 46.640625, + 26 + ], + "style": { + "display": "table-cell", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "1px", + "padding-right": "1px", + "padding-bottom": "1px", + "padding-left": "1px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "700", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "center", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "middle", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "table[0]/tbody[0]/tr[1]/th[0]/#text[0]": { + "box": [ + [ + 11, + 65, + 44.640625, + 20 + ] + ] + }, + "table[0]/tbody[0]/tr[1]/td[0]": { + "box": [ + 58.640625, + 62, + 108.34375, + 26 + ], + "style": { + "display": "table-cell", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "1px", + "padding-right": "1px", + "padding-bottom": "1px", + "padding-left": "1px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "middle", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "table[0]/tbody[0]/tr[1]/td[0]/#text[0]": { + "box": [ + [ + 59.640625, + 65, + 106.34375, + 20 + ] + ] + } + } +} diff --git a/tests/integration/cases/generics/constrained/__expected__/ssr.people.html b/tests/integration/cases/generics/constrained/__expected__/ssr.people.html new file mode 100644 index 000000000..e8d1dabf0 --- /dev/null +++ b/tests/integration/cases/generics/constrained/__expected__/ssr.people.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + +
+ "People" +
+ "ada" + + "Ada Lovelace" +
+ "grace" + + "Grace Hopper" +
diff --git a/tests/integration/cases/generics/constrained/__output__/angular/table.ts b/tests/integration/cases/generics/constrained/__output__/angular/table.ts new file mode 100644 index 000000000..042ee6ebe --- /dev/null +++ b/tests/integration/cases/generics/constrained/__output__/angular/table.ts @@ -0,0 +1,68 @@ +import { + Component, + Directive, + ElementRef, + type OnInit, + Renderer2, + inject, + input, +} from "@angular/core"; + +export interface TableProps { + caption: string; + rows: T[]; +} + +/** The scope attributes taken off each host, for the roots its component renders later. */ +const scopes = new WeakMap(); + +/** + * Gives a root node of its component the scope attributes the component's styled parents put on + * its host (ADR-0063), and takes them off the host, which keeps only `display: contents`. + */ +@Directive({ selector: "[ufScope]" }) +export class ScopeAttributes implements OnInit { + readonly ufScope = input.required(); + private readonly element = inject>(ElementRef); + private readonly renderer = inject(Renderer2); + + ngOnInit(): void { + const host = this.ufScope(); + let names = scopes.get(host); + if (!names) { + names = host.getAttributeNames().filter((name) => name.startsWith("data-uf-")); + for (const name of names) this.renderer.removeAttribute(host, name); + scopes.set(host, names); + } + for (const name of names) this.renderer.setAttribute(this.element.nativeElement, name, ""); + } +} + +@Component({ + selector: "uf-table", + imports: [ScopeAttributes], + host: { "[attr.class]": "null", "[attr.style]": "'display: contents;'" }, + preserveWhitespaces: false, + template: ` + @let caption = this.caption(); + @let rows = this.rows(); + + + + @for (row of rows; track row.id) { + + + + + } + +
{{ caption }}
{{ row.id }}{{ row.label }}
+ `, +}) +export default class Table { + readonly caption = input.required(); + readonly rows = input.required(); + readonly class = input(); + readonly style = input>(); + protected readonly host = inject>(ElementRef).nativeElement; +} diff --git a/tests/integration/cases/generics/constrained/__output__/astro/Table.astro b/tests/integration/cases/generics/constrained/__output__/astro/Table.astro new file mode 100644 index 000000000..17df68420 --- /dev/null +++ b/tests/integration/cases/generics/constrained/__output__/astro/Table.astro @@ -0,0 +1,32 @@ +--- +export interface TableProps { + caption: string; + rows: T[]; +} + +interface Props extends TableProps { + class?: string; + style?: Record; +} + +const { caption, rows, class: className, style } = Astro.props; + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} +--- + + + + + {rows.map((row) => ( + + + + + ))} + +
{caption}
{row.id}{row.label}
diff --git a/tests/integration/cases/generics/constrained/__output__/ir.json b/tests/integration/cases/generics/constrained/__output__/ir.json new file mode 100644 index 000000000..f3e8dcfbf --- /dev/null +++ b/tests/integration/cases/generics/constrained/__output__/ir.json @@ -0,0 +1,322 @@ +{ + "irVersion": 1, + "file": "generics/constrained/Table.uf.tsx", + "components": [ + { + "name": "Table", + "span": { + "start": 227, + "end": 605 + }, + "typeParameters": [ + { + "name": "T", + "constraint": { + "code": "{ id: string; label: string }", + "span": { + "start": 267, + "end": 296 + } + }, + "span": { + "start": 257, + "end": 296 + } + } + ], + "props": [ + { + "name": "caption", + "optional": false, + "type": { + "code": "string", + "span": { + "start": 203, + "end": 209 + } + }, + "binding": "caption@302", + "span": { + "start": 194, + "end": 210 + } + }, + { + "name": "rows", + "optional": false, + "type": { + "code": "T[]", + "span": { + "start": 219, + "end": 222 + } + }, + "binding": "rows@313", + "span": { + "start": 213, + "end": 223 + } + } + ], + "propsParameter": { + "form": "destructured", + "type": { + "code": "TableProps", + "span": { + "start": 322, + "end": 335 + } + }, + "span": { + "start": 298, + "end": 335 + } + }, + "types": [ + "TableProps" + ], + "bindings": [ + { + "id": "caption@302", + "name": "caption", + "kind": "prop", + "span": { + "start": 302, + "end": 309 + } + }, + { + "id": "rows@313", + "name": "rows", + "kind": "prop", + "span": { + "start": 313, + "end": 317 + } + }, + { + "id": "row@430", + "name": "row", + "kind": "loopVar", + "span": { + "start": 430, + "end": 433 + } + } + ], + "setup": [], + "render": { + "kind": "Element", + "tag": "table", + "attributes": [], + "children": [ + { + "kind": "Element", + "tag": "caption", + "attributes": [], + "children": [ + { + "kind": "Interpolation", + "value": { + "code": "caption", + "span": { + "start": 378, + "end": 385 + }, + "refs": [ + { + "kind": "Binding", + "binding": "caption@302", + "span": { + "start": 378, + "end": 385 + } + } + ] + }, + "span": { + "start": 377, + "end": 386 + } + } + ], + "span": { + "start": 368, + "end": 396 + } + }, + { + "kind": "Element", + "tag": "tbody", + "attributes": [], + "children": [ + { + "kind": "For", + "source": { + "code": "rows", + "span": { + "start": 420, + "end": 424 + }, + "refs": [ + { + "kind": "Binding", + "binding": "rows@313", + "span": { + "start": 420, + "end": 424 + } + } + ] + }, + "item": "row@430", + "key": { + "code": "row.id", + "span": { + "start": 459, + "end": 465 + }, + "refs": [ + { + "kind": "Binding", + "binding": "row@430", + "span": { + "start": 459, + "end": 462 + } + } + ] + }, + "body": { + "kind": "Element", + "tag": "tr", + "attributes": [], + "children": [ + { + "kind": "Element", + "tag": "th", + "attributes": [ + { + "kind": "Static", + "name": "scope", + "value": "row", + "span": { + "start": 484, + "end": 495 + } + } + ], + "children": [ + { + "kind": "Interpolation", + "value": { + "code": "row.id", + "span": { + "start": 497, + "end": 503 + }, + "refs": [ + { + "kind": "Binding", + "binding": "row@430", + "span": { + "start": 497, + "end": 500 + } + } + ] + }, + "span": { + "start": 496, + "end": 504 + } + } + ], + "span": { + "start": 480, + "end": 509 + } + }, + { + "kind": "Element", + "tag": "td", + "attributes": [], + "children": [ + { + "kind": "Interpolation", + "value": { + "code": "row.label", + "span": { + "start": 527, + "end": 536 + }, + "refs": [ + { + "kind": "Binding", + "binding": "row@430", + "span": { + "start": 527, + "end": 530 + } + } + ] + }, + "span": { + "start": 526, + "end": 537 + } + } + ], + "span": { + "start": 522, + "end": 542 + } + } + ], + "span": { + "start": 450, + "end": 558 + } + }, + "span": { + "start": 420, + "end": 569 + } + } + ], + "span": { + "start": 403, + "end": 585 + } + } + ], + "span": { + "start": 354, + "end": 598 + } + } + } + ], + "exports": [ + { + "kind": "default", + "name": "default", + "local": "Table", + "span": { + "start": 227, + "end": 605 + } + } + ], + "types": [ + { + "name": "TableProps", + "exported": true, + "code": "interface TableProps {\n caption: string;\n rows: T[];\n}", + "span": { + "start": 128, + "end": 225 + } + } + ] +} diff --git a/tests/integration/cases/generics/constrained/__output__/qwik/Table.tsx b/tests/integration/cases/generics/constrained/__output__/qwik/Table.tsx new file mode 100644 index 000000000..aa3267387 --- /dev/null +++ b/tests/integration/cases/generics/constrained/__output__/qwik/Table.tsx @@ -0,0 +1,33 @@ +import { type CSSProperties, type ClassList, component$ } from "@qwik.dev/core"; + +export interface TableProps { + caption: string; + rows: T[]; +} + +export default component$ & { class?: ClassList; style?: CSSProperties }>( + ({ caption, rows, class: className, style, ...attributes }) => { + return ( + + + + {rows.map((row) => ( + + + + + ))} + +
{caption}
{row.id}{row.label}
+ ); + }, +); + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/generics/constrained/__output__/react/Table.tsx b/tests/integration/cases/generics/constrained/__output__/react/Table.tsx new file mode 100644 index 000000000..c87c7920a --- /dev/null +++ b/tests/integration/cases/generics/constrained/__output__/react/Table.tsx @@ -0,0 +1,42 @@ +import type { CSSProperties } from "react"; + +export interface TableProps { + caption: string; + rows: T[]; +} + +export interface TableAttributes { + className?: string; + style?: CSSProperties; +} + +export default function Table({ + caption, + rows, + className, + style, + ...attributes +}: TableProps & TableAttributes) { + return ( + + + + {rows.map((row) => ( + + + + + ))} + +
{caption}
{row.id}{row.label}
+ ); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/generics/constrained/__output__/solid/Table.tsx b/tests/integration/cases/generics/constrained/__output__/solid/Table.tsx new file mode 100644 index 000000000..90f701ace --- /dev/null +++ b/tests/integration/cases/generics/constrained/__output__/solid/Table.tsx @@ -0,0 +1,33 @@ +import { For, type JSX } from "solid-js"; + +export interface TableProps { + caption: string; + rows: T[]; +} + +export default function Table( + props: TableProps & { class?: string; style?: JSX.CSSProperties }, +) { + return ( + + + + + {(row) => ( + + + + + )} + + +
{props.caption}
{row.id}{row.label}
+ ); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} diff --git a/tests/integration/cases/generics/constrained/__output__/svelte/Table.svelte b/tests/integration/cases/generics/constrained/__output__/svelte/Table.svelte new file mode 100644 index 000000000..8317ff1e6 --- /dev/null +++ b/tests/integration/cases/generics/constrained/__output__/svelte/Table.svelte @@ -0,0 +1,33 @@ + + + + + + + {#each rows as row (row.id)} + + + + {/each} + +
{caption}
{row.id}{row.label}
diff --git a/tests/integration/cases/generics/constrained/__output__/vue/Table.vue b/tests/integration/cases/generics/constrained/__output__/vue/Table.vue new file mode 100644 index 000000000..4e02aa361 --- /dev/null +++ b/tests/integration/cases/generics/constrained/__output__/vue/Table.vue @@ -0,0 +1,20 @@ + + + diff --git a/tests/integration/cases/generics/constrained/__screenshots__/people-chromium-linux.png b/tests/integration/cases/generics/constrained/__screenshots__/people-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..9f472652de5d30a207713faa3eaaf778551e856d GIT binary patch literal 5272 zcma)=Ra6v!v&K<6R*(>sl18Mv1f-XgTDohO?q=5o76j>#2I=lDDWy9WWu?18T4JyN zhx>N#x%XjazVkiJ%gpbbFY<$i5`f?p0R{#J0H`dl{cqO&D=WOG|9bpd5g7&s2?kL9 zy{=!@p{cZAw(xav_f8;PMbe%Pf4T;P&aMqVwkE62kgh*LPIO898yjoFoRE^$GCzX! zsFD$#t$!j1YQvEeRkFS}oBfB@9lb$~67g9SpYLF9CJ|SznGov>n1)F~@7eM3)K<~V zPv8(d4LmOfQHCkRYM2aV2-#E9|J%4%Y9F`I+itU*zJSTrm+uuLoK^MXFN!dnah}XS zkT{xGCv;P^{MHoI1I9&_Tb{y@$%4O@1O9}@CPe|wiG*0*<2B8Dg8MP!YXrG>M_zcFL$9tprWFuyxUW(uNfGC+&1+?c9^NnK<()NFDG(;7D z_>!uIe7$Xf+8xYdjr(2Oh>4rPjF2d6ca~@_8WzZOQc1LX>8dr&dAP1=>g#x_x05Eh z#0#LAQ3O%w+CG+BD#;Pgkk>g^x?mY{4Qr=MnZqnZL(}>*7}*Rz)#c7B>uD)SqP|aa zuIT8j@E^XeVs5Q2?q%x#1L0b72y%voq%h}|L{;@h*EIB~vapR>tFGCq9~*XlKuU&y9U0fgWcg|Dq&&tBfuE!5Q$>wC}TQ#*W}!g{G9~>d+2D zXuMVH@fGXWmKDv~27bVUl%!LJF)n!g+Tv?w$dn@DLq1V;vbU3CbN~Mm*(JR|dOq_#3duf~K!ts>MJ6(sh zr9XuHZoCF={ipLQ#KFg>^I^5)sH}o=E1VCh3viQ98EklYcGLIuS<;8V^x`2MM7^$e zc!rR=r%(OeZpRWN6tr)gy|xV1Xgr7=%RZ~e7zVp488gn7Gc)L3Tu^<;~>G6v8{5r?< zxU7$EbVE3sq=(r%L?fg(D^Kd*NlpOb$R=Zy4fXx^B_5>&-nAS&BfrT89b2GO!9RGy zE{B;=ljX+hQ8ms>eAqvTW<{F1n!2DP4JY$84gc9?KpI*JXU@nghkt%`emMuFVtXKdmARNPCUPX zsvk##bb1daac1zG8F}Zv^SP~&4#{VbMkVC$Eh@ldAtRk&n`a=5h%$Y?`R_l|Kz8In5*ffZ0b`-1NwCS^u*Ue5$OGFb$_QjB zQ!!Mi8X-9All6BM@N5X~Fj>f#!@gENbDD?6frY z%wl6}V)?O`;X9(=SAv@L*_}pK(?~~^GdpSpwQSH7dhc|vreJeWL5=D-xgz!-5A{VI zywKQ=E+PsjmT$K$ZMQ97eFGNKGWhRsDcE{3O2HOs!5FP zM3dwuzF<@cy-M_RB0A+!Aq37Q3(LWc;)UHB4Cute_w07ou}=)ofmlJfnCj~JIkx`& z@|A1pzY1dFk@dxn(KwoFG}j%+;r}*YmDib1^zglX+@9=4UvCUbL!s@Qz^;B#h%Kn> zgg#4Urte`qcNu+l=juwd=XF1%uE*f*f4lbC{PE`ItQ(D<-CzNVzx%Y!q@YtsBC5vS zmk1b~rp}mJgF-;XP#JQbQ3$~|f^Bsh75gqN(;pryBpE-ns1)5v7d>+9wj>KJ4E$JjCi3CvKLik*xJOCQ(Uvg#V zKHlcn`ji~VzzESue`Gr%<#jBLDf}yG>s!M9DCMSt%8$!LT8xW$8@qc#V=VIHLtj&V8B(j-y?HcTFyxe2*M`*l0!+Gbx}BpeX_riQy4!r zX?*Fz0<s{Ip!b!So$0Py^J5yLU}HV;p9>YCyrwK_`#6P|kxfI#!dpWz zV6t%12g$?*BNONd<~I4rP?ic7^^icVCbPuYGWv06Fir?S&;5)|+Cd3ctZq_zbJ!v+ z;XC=@&vKkQjt|mjA$Gf7V)t@Zo-Zt?t52_B(T&vn-C9!AHOIR?>02~P^>f#S8GPZF~1Y*#RlGPOqh4?Rt4-|$n>CfVZ) zk3}=GuMyY-%n(_J;MPX4BYcmnVjG81Zu#;^U2ksa^yx0hP2nhZZNCx*;B@n9{SoHy zoZnz2f?^IpKp~w;rmW@9$VEGtbKqa?{1Y`;AZzH`Immk*pv0pNsJC7ssCaIe=csGu zL-i-)naxNb+l&eLO;;n&a%1kU=HG05N<$4$S0Z(7XzZF6eOS|O$fQ{ z49TGLdweVyp@0HW%6u22G1(#%KI?Jppc~qs^eM2rjnpRNwfEo1Z34U4`c$ zweWwhf##`y&)q4XrV{P<`A-2Uq8fNFUrHLTcbFwN0rz9@DWs)+*g+&xU4#>>yGs)q z+78Er--4(xAfizsCvu<#i3c~63KyERe(l0_-XNmS$+P`k+1ZW4o%X~#f0lT$F372P z6(XSnc8ob+MLGdsb1olYx**ujvcrcBc0W;G8NSd@lvQZA9hB>dLD{-E<&W6ehaF82 z8wStN0le`vJ;Sv|4s)f(cw6oR~MQ0HarXof5Waso!QAGUnC~%jKDy_l-PdBW5m@4&iqqP_pi_5lNm& z*FJ8-jMSFh`*74>(?{`hr_@p&C|6%^u{|gOW5ant;9~C97m9Hm?xvtoD?XXXT+mc# z7+av9kejT0y^dG#+qajGqj_gUa9CxSfJ}3TAep?H{*Z6hY0_88SP!K>VRAAgof=2p z4+yCP*U!t6_WAa4WI)7PJC5qR-Y0|si*U=Nm$Bn?N8?Pa6y1%i%?wJM5m5tRE7+tdS?kxnw$W+^Oyp622So^w>$+)5k z%z4tF*8gGao7#ncp433`0BnRtphkK7oVK3GsQGZo+6y)=4A=aHDU)b7`>@f`AyZ6qXNe(jN9Ddib7P2Y-K0RAn1le)uf6+%4|i#cOFvk zcr(%;UVKnG@ygKH4Ae=kXyg#Fe##sZ>Dr+}*P2B__?Cx1ZA<$Y6F-(XNV*QMIw+NF z{c#(`T$>EK?Fj6o9H^$yaCWu>h*I6o=nVH#gX!|*##MDZ)4{vM&A`wE9F2wJ7hnYY%Ju|O=h3TINZaSu1FY)d77`S^`-?M;nj_;2IvgOsxbAFZ$J4ZQjY26CnwQw6xIa30>-Ep7m;zqrN!19h8m;A z=;sJT$BP`PmjW6b8~@+WO-j~p zJ0ePUTn2EAmOxUAFv0OYqr2{MNJ>oC#!`$@QpTW}Y4dxBdOqHwGNm#}*>cNt4@~mH zVY6z%0!*|G#L7$R4cQjIrgDH?8!d8#m$>CP*iEEb7Xt(9{eQB64x!6Cmf+%5{{e<@ z&TD|cOR2}U@c8@@c6tZ#wtcUB^7kdGUag7mm8ZoKc$L0ld>{=R81IHbTkMqx@==X{2AqA(mffIco){Nmy1}vlTDLtyUvnmmjpA3YAA%FJT zh&&-&W57VkGjF~E2B~r~HUnX&KX{`)8PPP1)=2~g%aNp%)%k=mHlE#7ZTAmY3k)9V zbM83!!7z@2&5IBMz?h>HF+Fa=i9vC-ptY^Qf`o?{GJ92&3gRjS1d_ioDxec%T8^cE@uP_%bCH;nXkK^7H8hpDZQPI50 z*^Q6g4D8X%d5%(my>T~pUf zR?5nmp*|!aV#)Nyb9ir~aa`WYBEFcgiG!SdIk^vM!#_}fGEX0oo}qF!fx>Y*A`Y7= z$o%@3cIoR8_-LBGrNJ6mgV&YBp}o<#3)$>Lhp7>4xE&I>pWkNo+R+`-5wmo*=W4T@ zGA!M=jm?fDwuE?MQGzt-mVb3Zm%26{x*m^Gk!D51V@Zn^OX*tk*C9tS551Q0Z#B)) zq-qzdf=20JxGxg6S|Sxa2$+{PrA3zDxF=I+7X@#ui#okv9} zJ+d%E>D9Y}wt^$oyuP}o=pA(`Hq^Loz0y4TORx~R!4;$R)!NqPRo*MgNUoGlE`RN} zMnzxNRNm)^dCbEWM6@pLVmhC)WiU3X`<2voUJ)%z_ZUgVUd3uyUfzncEqAVTklZjU zW+|T$(F?AJ-TfeL?2 { + it("renders a row for each item", async () => { + const view = await mountScenario(Table, "people"); + await view.expectParity("people"); + await expect.element(view.getByRole("table", { name: "People" })).toBeVisible(); + await expect.element(view.getByRole("rowheader", { name: "ada" })).toBeVisible(); + await expect.element(view.getByRole("cell", { name: "Grace Hopper" })).toBeVisible(); + }); +}); diff --git a/tests/integration/cases/generics/list/List.uf.tsx b/tests/integration/cases/generics/list/List.uf.tsx new file mode 100644 index 000000000..c60375978 --- /dev/null +++ b/tests/integration/cases/generics/list/List.uf.tsx @@ -0,0 +1,18 @@ +// A component generic over its items (ADR-0068): `T` is what its consumer's `items` holds. +export interface ListProps { + label: string; + items: T[]; + selected?: T; +} + +export default function List({ label, items, selected }: ListProps) { + return ( +
    + {items.map((item, index) => ( +
  • + {String(item)} +
  • + ))} +
+ ); +} diff --git a/tests/integration/cases/generics/list/__expected__/aria.numbers.yaml b/tests/integration/cases/generics/list/__expected__/aria.numbers.yaml new file mode 100644 index 000000000..5a9871d77 --- /dev/null +++ b/tests/integration/cases/generics/list/__expected__/aria.numbers.yaml @@ -0,0 +1,4 @@ +- list "Numbers": + - listitem: "1" + - listitem: "2" + - listitem: "3" diff --git a/tests/integration/cases/generics/list/__expected__/aria.words.yaml b/tests/integration/cases/generics/list/__expected__/aria.words.yaml new file mode 100644 index 000000000..abaa27c58 --- /dev/null +++ b/tests/integration/cases/generics/list/__expected__/aria.words.yaml @@ -0,0 +1,3 @@ +- list "Words": + - listitem: a + - listitem: b diff --git a/tests/integration/cases/generics/list/__expected__/diagnostics.json b/tests/integration/cases/generics/list/__expected__/diagnostics.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/tests/integration/cases/generics/list/__expected__/diagnostics.json @@ -0,0 +1 @@ +[] diff --git a/tests/integration/cases/generics/list/__expected__/dom.numbers.html b/tests/integration/cases/generics/list/__expected__/dom.numbers.html new file mode 100644 index 000000000..7b046cf93 --- /dev/null +++ b/tests/integration/cases/generics/list/__expected__/dom.numbers.html @@ -0,0 +1,11 @@ +
    +
  • + "1" +
  • +
  • + "2" +
  • +
  • + "3" +
  • +
diff --git a/tests/integration/cases/generics/list/__expected__/dom.words.html b/tests/integration/cases/generics/list/__expected__/dom.words.html new file mode 100644 index 000000000..2fb1ad015 --- /dev/null +++ b/tests/integration/cases/generics/list/__expected__/dom.words.html @@ -0,0 +1,8 @@ +
    +
  • + "a" +
  • +
  • + "b" +
  • +
diff --git a/tests/integration/cases/generics/list/__expected__/geometry.numbers.json b/tests/integration/cases/generics/list/__expected__/geometry.numbers.json new file mode 100644 index 000000000..1e8e4d630 --- /dev/null +++ b/tests/integration/cases/generics/list/__expected__/geometry.numbers.json @@ -0,0 +1,347 @@ +{ + "version": 1, + "nodes": { + "ul[0]": { + "box": [ + 8, + 24, + 49.890625, + 72 + ], + "style": { + "display": "block", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "16px", + "margin-right": "0px", + "margin-bottom": "16px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "40px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "ul[0]/li[0]": { + "box": [ + 48, + 24, + 9.890625, + 24 + ], + "style": { + "display": "list-item", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "ul[0]/li[0]/#text[0]": { + "box": [ + [ + 48, + 26, + 6.515625, + 20 + ] + ] + }, + "ul[0]/li[1]": { + "box": [ + 48, + 48, + 9.890625, + 24 + ], + "style": { + "display": "list-item", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "ul[0]/li[1]/#text[0]": { + "box": [ + [ + 48, + 50, + 9.765625, + 20 + ] + ] + }, + "ul[0]/li[2]": { + "box": [ + 48, + 72, + 9.890625, + 24 + ], + "style": { + "display": "list-item", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "ul[0]/li[2]/#text[0]": { + "box": [ + [ + 48, + 74, + 9.890625, + 20 + ] + ] + } + } +} diff --git a/tests/integration/cases/generics/list/__expected__/geometry.words.json b/tests/integration/cases/generics/list/__expected__/geometry.words.json new file mode 100644 index 000000000..031a5f217 --- /dev/null +++ b/tests/integration/cases/generics/list/__expected__/geometry.words.json @@ -0,0 +1,259 @@ +{ + "version": 1, + "nodes": { + "ul[0]": { + "box": [ + 8, + 24, + 49.796875, + 48 + ], + "style": { + "display": "block", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "16px", + "margin-right": "0px", + "margin-bottom": "16px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "40px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "ul[0]/li[0]": { + "box": [ + 48, + 24, + 9.796875, + 24 + ], + "style": { + "display": "list-item", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "ul[0]/li[0]/#text[0]": { + "box": [ + [ + 48, + 26, + 8.984375, + 20 + ] + ] + }, + "ul[0]/li[1]": { + "box": [ + 48, + 48, + 9.796875, + 24 + ], + "style": { + "display": "list-item", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "ul[0]/li[1]/#text[0]": { + "box": [ + [ + 48, + 50, + 9.796875, + 20 + ] + ] + } + } +} diff --git a/tests/integration/cases/generics/list/__expected__/ssr.numbers.html b/tests/integration/cases/generics/list/__expected__/ssr.numbers.html new file mode 100644 index 000000000..7b046cf93 --- /dev/null +++ b/tests/integration/cases/generics/list/__expected__/ssr.numbers.html @@ -0,0 +1,11 @@ +
    +
  • + "1" +
  • +
  • + "2" +
  • +
  • + "3" +
  • +
diff --git a/tests/integration/cases/generics/list/__expected__/ssr.words.html b/tests/integration/cases/generics/list/__expected__/ssr.words.html new file mode 100644 index 000000000..2fb1ad015 --- /dev/null +++ b/tests/integration/cases/generics/list/__expected__/ssr.words.html @@ -0,0 +1,8 @@ +
    +
  • + "a" +
  • +
  • + "b" +
  • +
diff --git a/tests/integration/cases/generics/list/__output__/angular/list.ts b/tests/integration/cases/generics/list/__output__/angular/list.ts new file mode 100644 index 000000000..d495c7ab7 --- /dev/null +++ b/tests/integration/cases/generics/list/__output__/angular/list.ts @@ -0,0 +1,71 @@ +import { + Component, + Directive, + ElementRef, + type OnInit, + Renderer2, + inject, + input, +} from "@angular/core"; + +export interface ListProps { + label: string; + items: T[]; + selected?: T; +} + +/** The scope attributes taken off each host, for the roots its component renders later. */ +const scopes = new WeakMap(); + +/** + * Gives a root node of its component the scope attributes the component's styled parents put on + * its host (ADR-0063), and takes them off the host, which keeps only `display: contents`. + */ +@Directive({ selector: "[ufScope]" }) +export class ScopeAttributes implements OnInit { + readonly ufScope = input.required(); + private readonly element = inject>(ElementRef); + private readonly renderer = inject(Renderer2); + + ngOnInit(): void { + const host = this.ufScope(); + let names = scopes.get(host); + if (!names) { + names = host.getAttributeNames().filter((name) => name.startsWith("data-uf-")); + for (const name of names) this.renderer.removeAttribute(host, name); + scopes.set(host, names); + } + for (const name of names) this.renderer.setAttribute(this.element.nativeElement, name, ""); + } +} + +@Component({ + selector: "uf-list", + imports: [ScopeAttributes], + host: { "[attr.class]": "null", "[attr.style]": "'display: contents;'" }, + preserveWhitespaces: false, + template: ` + @let label = this.label(); + @let items = this.items(); + @let selected = this.selected(); +
    + @for (item of items; track index; let index = $index) { +
  • {{ String(item) }}
  • + } +
+ `, +}) +export default class List { + readonly label = input.required(); + readonly items = input.required(); + readonly selected = input(); + readonly class = input(); + readonly style = input>(); + protected readonly String = String; + protected readonly host = inject>(ElementRef).nativeElement; +} diff --git a/tests/integration/cases/generics/list/__output__/astro/List.astro b/tests/integration/cases/generics/list/__output__/astro/List.astro new file mode 100644 index 000000000..cd528c110 --- /dev/null +++ b/tests/integration/cases/generics/list/__output__/astro/List.astro @@ -0,0 +1,27 @@ +--- +export interface ListProps { + label: string; + items: T[]; + selected?: T; +} + +interface Props extends ListProps { + class?: string; + style?: Record; +} + +const { label, items, selected, class: className, style } = Astro.props; + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} +--- + +
    + {items.map((item) => ( +
  • {String(item)}
  • + ))} +
diff --git a/tests/integration/cases/generics/list/__output__/ir.json b/tests/integration/cases/generics/list/__output__/ir.json new file mode 100644 index 000000000..bd16131d8 --- /dev/null +++ b/tests/integration/cases/generics/list/__output__/ir.json @@ -0,0 +1,324 @@ +{ + "irVersion": 1, + "file": "generics/list/List.uf.tsx", + "components": [ + { + "name": "List", + "span": { + "start": 174, + "end": 469 + }, + "typeParameters": [ + { + "name": "T", + "span": { + "start": 203, + "end": 204 + } + } + ], + "props": [ + { + "name": "label", + "optional": false, + "type": { + "code": "string", + "span": { + "start": 133, + "end": 139 + } + }, + "binding": "label@208", + "span": { + "start": 126, + "end": 140 + } + }, + { + "name": "items", + "optional": false, + "type": { + "code": "T[]", + "span": { + "start": 150, + "end": 153 + } + }, + "binding": "items@215", + "span": { + "start": 143, + "end": 154 + } + }, + { + "name": "selected", + "optional": true, + "type": { + "code": "T", + "span": { + "start": 168, + "end": 169 + } + }, + "binding": "selected@222", + "span": { + "start": 157, + "end": 170 + } + } + ], + "propsParameter": { + "form": "destructured", + "type": { + "code": "ListProps", + "span": { + "start": 234, + "end": 246 + } + }, + "span": { + "start": 206, + "end": 246 + } + }, + "types": [ + "ListProps" + ], + "bindings": [ + { + "id": "label@208", + "name": "label", + "kind": "prop", + "span": { + "start": 208, + "end": 213 + } + }, + { + "id": "items@215", + "name": "items", + "kind": "prop", + "span": { + "start": 215, + "end": 220 + } + }, + { + "id": "selected@222", + "name": "selected", + "kind": "prop", + "span": { + "start": 222, + "end": 230 + } + }, + { + "id": "item@307", + "name": "item", + "kind": "loopVar", + "span": { + "start": 307, + "end": 311 + } + }, + { + "id": "index@313", + "name": "index", + "kind": "loopVar", + "span": { + "start": 313, + "end": 318 + } + } + ], + "setup": [], + "render": { + "kind": "Element", + "tag": "ul", + "attributes": [ + { + "kind": "Bound", + "name": "aria-label", + "value": { + "code": "label", + "span": { + "start": 281, + "end": 286 + }, + "refs": [ + { + "kind": "Binding", + "binding": "label@208", + "span": { + "start": 281, + "end": 286 + } + } + ] + }, + "span": { + "start": 269, + "end": 287 + } + } + ], + "children": [ + { + "kind": "For", + "source": { + "code": "items", + "span": { + "start": 296, + "end": 301 + }, + "refs": [ + { + "kind": "Binding", + "binding": "items@215", + "span": { + "start": 296, + "end": 301 + } + } + ] + }, + "item": "item@307", + "index": "index@313", + "key": { + "code": "index", + "span": { + "start": 342, + "end": 347 + }, + "refs": [ + { + "kind": "Binding", + "binding": "index@313", + "span": { + "start": 342, + "end": 347 + } + } + ] + }, + "body": { + "kind": "Element", + "tag": "li", + "attributes": [ + { + "kind": "Bound", + "name": "aria-current", + "value": { + "code": "item === selected ? \"true\" : undefined", + "span": { + "start": 363, + "end": 401 + }, + "refs": [ + { + "kind": "Binding", + "binding": "item@307", + "span": { + "start": 363, + "end": 367 + } + }, + { + "kind": "Binding", + "binding": "selected@222", + "span": { + "start": 372, + "end": 380 + } + }, + { + "kind": "Global", + "name": "undefined", + "span": { + "start": 392, + "end": 401 + } + } + ] + }, + "span": { + "start": 349, + "end": 402 + } + } + ], + "children": [ + { + "kind": "Interpolation", + "value": { + "code": "String(item)", + "span": { + "start": 415, + "end": 427 + }, + "refs": [ + { + "kind": "Global", + "name": "String", + "span": { + "start": 415, + "end": 421 + } + }, + { + "kind": "Binding", + "binding": "item@307", + "span": { + "start": 422, + "end": 426 + } + } + ] + }, + "span": { + "start": 414, + "end": 428 + } + } + ], + "span": { + "start": 333, + "end": 442 + } + }, + "span": { + "start": 296, + "end": 451 + } + } + ], + "span": { + "start": 265, + "end": 462 + } + } + } + ], + "exports": [ + { + "kind": "default", + "name": "default", + "local": "List", + "span": { + "start": 174, + "end": 469 + } + } + ], + "types": [ + { + "name": "ListProps", + "exported": true, + "code": "interface ListProps {\n label: string;\n items: T[];\n selected?: T;\n}", + "span": { + "start": 99, + "end": 172 + } + } + ] +} diff --git a/tests/integration/cases/generics/list/__output__/qwik/List.tsx b/tests/integration/cases/generics/list/__output__/qwik/List.tsx new file mode 100644 index 000000000..13b1cf5e2 --- /dev/null +++ b/tests/integration/cases/generics/list/__output__/qwik/List.tsx @@ -0,0 +1,30 @@ +import { type CSSProperties, type ClassList, component$ } from "@qwik.dev/core"; + +export interface ListProps { + label: string; + items: T[]; + selected?: T; +} + +export default component$ & { class?: ClassList; style?: CSSProperties }>( + ({ label, items, selected, class: className, style, ...attributes }) => { + return ( +
    + {items.map((item, index) => ( +
  • + {String(item)} +
  • + ))} +
+ ); + }, +); + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/generics/list/__output__/react/List.tsx b/tests/integration/cases/generics/list/__output__/react/List.tsx new file mode 100644 index 000000000..faf54e83c --- /dev/null +++ b/tests/integration/cases/generics/list/__output__/react/List.tsx @@ -0,0 +1,40 @@ +import type { CSSProperties } from "react"; + +export interface ListProps { + label: string; + items: T[]; + selected?: T; +} + +export interface ListAttributes { + className?: string; + style?: CSSProperties; +} + +export default function List({ + label, + items, + selected, + className, + style, + ...attributes +}: ListProps & ListAttributes) { + return ( +
    + {items.map((item, index) => ( +
  • + {String(item)} +
  • + ))} +
+ ); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/generics/list/__output__/solid/List.tsx b/tests/integration/cases/generics/list/__output__/solid/List.tsx new file mode 100644 index 000000000..b05652920 --- /dev/null +++ b/tests/integration/cases/generics/list/__output__/solid/List.tsx @@ -0,0 +1,31 @@ +import { For, type JSX } from "solid-js"; + +export interface ListProps { + label: string; + items: T[]; + selected?: T; +} + +export default function List(props: ListProps & { class?: string; style?: JSX.CSSProperties }) { + return ( +
    + + {(item) => ( +
  • {String(item)}
  • + )} +
    +
+ ); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} diff --git a/tests/integration/cases/generics/list/__output__/svelte/List.svelte b/tests/integration/cases/generics/list/__output__/svelte/List.svelte new file mode 100644 index 000000000..dda742692 --- /dev/null +++ b/tests/integration/cases/generics/list/__output__/svelte/List.svelte @@ -0,0 +1,28 @@ + + + + +
    + {#each items as item, index (index)} +
  • {String(item)}
  • + {/each} +
diff --git a/tests/integration/cases/generics/list/__output__/vue/List.vue b/tests/integration/cases/generics/list/__output__/vue/List.vue new file mode 100644 index 000000000..d70b8d971 --- /dev/null +++ b/tests/integration/cases/generics/list/__output__/vue/List.vue @@ -0,0 +1,19 @@ + + + diff --git a/tests/integration/cases/generics/list/__screenshots__/numbers-chromium-linux.png b/tests/integration/cases/generics/list/__screenshots__/numbers-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..6fc746e96bdfe166a355ff830d6154eb5aee2ca8 GIT binary patch literal 943 zcmeAS@N?(olHy`uVBq!ia0vp^PC#72!2~2vUf*28z`&g1>EaktG3U)JTc6tw5^d{4 zRrfgTeYjRqB8YS4yOwSNF6W~gD<7N17+#4!JT33$hc;`|#nxA)-(3;DTLc}9+9Nh3?9f}@nj1U4Z& zOvY7#OD3L|ZU0>ucP{8ZXY=9{i`IRx?>*LY)-d+W6}ENeTvx^SZTWLP-agOHG%c5P zd18c3z)T+3>NIvXixoX)(vS zgR;`o+fCgzH@|xF zz9w;&Z`F0-h|e|?jISzJEoDIU2BX9WAB{F6PobGvo8Ru5clvf(Zt0PG8toGca<$TWcSeNtZpL{9rH?F~N*|dNt^NGoMyzl8r`7NOGg_LTF%v(u^UwU~zUE)?LMQJ(_&g+vZLi1eL&dMuDmND==jOis@#Dv$J)1U7 zTBf;MIMuA6(){^GnH_6)8ElE0Uqo2R-XN@0wG#l*j7pYu!A* zrK?k7kNCI*{x@T9n-wgY`J&VDp=*&r^d`O?`60XKAHT2i?6#xAI`x*so9Di)mGNJE zrs>@2@Av=z{ru=#qL}~P{~F7knoK4r@Tzz)b6#=)G8xXUJQc%tD1x^%@+kK@fpy7O z=Uq=MUbS|whOW=)q{tV0vp&zeRO-v%zoYewY`A4-*N-PL>z3Xxe0)v8d(X=6g$M2# zK!G*`VwDikas~KmWdG_I2gCYMD15efxU`XajFkTzH9$eQ}e;gD~IJ z2j4?n=XuRMk#ArB)Hzf)sXYBHBY(xZ%^w}t@8SL(S`ht8r}L0TcDvTcC82w8{ zICt-TaJv8g&cu|I-g>USKr-bdwA&%UWosZf&vTkQDep*Mh1re l|9ALY^a5r(Hc)>?F!>IrO literal 0 HcmV?d00001 diff --git a/tests/integration/cases/generics/list/__screenshots__/words-chromium-linux.png b/tests/integration/cases/generics/list/__screenshots__/words-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..6b3fff6b154ba074a62a9f9512e269d45ec94b86 GIT binary patch literal 803 zcmeAS@N?(olHy`uVBq!ia0vp^PC%T%!2~4Lf{dFO7?^H&x;TbZ%y~0QH|Ms4MBDn{ zrE!kh{6}-vg$M*>rk!22s`!9Nf9u&b)4Xh+ZS0x(G=i7c_Sr(#C|C1sr*6E#OgFXdC=rx`0cO%d_P2o|_S08Lmc%>`P=t#it5b}hVaLInc<=vRnDsBM%NyHS^i}e zxAmE;0h5f6cNZ;{o^{IU`ldgDsllx4m+yKS6{P+!Q^@tB#$(U;qjeL)LPF1G$;isi zoiisUHn#8_KR^F-{&h3gC1-u-Z+jY5HS=T5w})5P9NS#AN;Aaxi~4(}tG9oBVVJym zyXcO2uf&!v3F?=RI$5+!V9O&}_UplS3umhK+wM9O9G9`|>b10UAfe)u%=r#8^D`@j zemvay(CGG$c-eQx3%{II7kjbsci!t?&vssw?wb*N^|6y)#j@=}6TO#8F3Zfg8oY6N w6LD#1n#*H(U<&{LKZxm9Cy>DgO5@B7|FlF@8&;pX2NYrOboFyt=akR{07X!I761SM literal 0 HcmV?d00001 diff --git a/tests/integration/cases/generics/list/case.json b/tests/integration/cases/generics/list/case.json new file mode 100644 index 000000000..403034602 --- /dev/null +++ b/tests/integration/cases/generics/list/case.json @@ -0,0 +1,7 @@ +{ + "description": "A component generic over its items, mounted with numbers and with strings.", + "ssr": { + "numbers": { "props": { "label": "Numbers", "items": [1, 2, 3], "selected": 2 } }, + "words": { "props": { "label": "Words", "items": ["a", "b"] } } + } +} diff --git a/tests/integration/cases/generics/list/list.test.ts b/tests/integration/cases/generics/list/list.test.ts new file mode 100644 index 000000000..29aea2b4e --- /dev/null +++ b/tests/integration/cases/generics/list/list.test.ts @@ -0,0 +1,21 @@ +// generics/list: `function List(props: ListProps)`, written in each target's generic form +// (ADR-0068). Each test mounts it with its own type argument, which the scenario's props hold. +import { describeTargets, it, mountScenario } from "@unframework/testing"; +import { expect } from "vitest"; + +import List from "./List.uf.tsx"; + +describeTargets("generics/list", () => { + it("renders items of any type", async () => { + const view = await mountScenario(List, "numbers"); + await view.expectParity("numbers"); + await expect.element(view.getByRole("listitem").nth(1)).toHaveTextContent("2"); + await expect.element(view.getByRole("listitem").nth(1)).toHaveAttribute("aria-current", "true"); + }); + + it("renders items of another type", async () => { + const view = await mountScenario(List, "words"); + await view.expectParity("words"); + await expect.element(view.getByRole("list", { name: "Words" })).toHaveTextContent("ab"); + }); +}); diff --git a/tests/integration/cases/generics/scoped-slot/Fruits.uf.tsx b/tests/integration/cases/generics/scoped-slot/Fruits.uf.tsx new file mode 100644 index 000000000..27a26a7c1 --- /dev/null +++ b/tests/integration/cases/generics/scoped-slot/Fruits.uf.tsx @@ -0,0 +1,21 @@ +import Picker from "./Picker.uf.tsx"; + +interface Fruit { + name: string; + count: number; +} + +// The harness parent (ADR-0057): its fill reads the members of its own item type. +export default function Fruits({ fruits }: { fruits: Fruit[] }) { + return ( + + {{ + item: ({ item }) => ( + + {item.name} × {item.count} + + ), + }} + + ); +} diff --git a/tests/integration/cases/generics/scoped-slot/Picker.uf.tsx b/tests/integration/cases/generics/scoped-slot/Picker.uf.tsx new file mode 100644 index 000000000..0f3acf32b --- /dev/null +++ b/tests/integration/cases/generics/scoped-slot/Picker.uf.tsx @@ -0,0 +1,18 @@ +import { defineSlots } from "unframework"; +import type { Element } from "unframework"; + +// A scoped slot whose props name the type parameter (ADR-0068): a consumer's fill reads `T`. +export interface PickerProps { + items: T[]; +} + +export default function Picker({ items }: PickerProps) { + const slots = defineSlots<{ item?(props: { item: T }): Element }>(); + return ( +
    + {items.map((item, index) => ( +
  • {slots.item?.({ item })}
  • + ))} +
+ ); +} diff --git a/tests/integration/cases/generics/scoped-slot/__expected__/aria.basket.yaml b/tests/integration/cases/generics/scoped-slot/__expected__/aria.basket.yaml new file mode 100644 index 000000000..25ce8c921 --- /dev/null +++ b/tests/integration/cases/generics/scoped-slot/__expected__/aria.basket.yaml @@ -0,0 +1,3 @@ +- list: + - listitem: Apple × 3 + - listitem: Pear × 1 diff --git a/tests/integration/cases/generics/scoped-slot/__expected__/diagnostics.json b/tests/integration/cases/generics/scoped-slot/__expected__/diagnostics.json new file mode 100644 index 000000000..499c36c42 --- /dev/null +++ b/tests/integration/cases/generics/scoped-slot/__expected__/diagnostics.json @@ -0,0 +1,36 @@ +[ + { + "code": "UF4004", + "severity": "info", + "message": "The angular target does not support generic-slot-props: Angular infers a component's type parameter from its inputs alone, and a slot's directive cannot infer it, so a scoped slot's context takes the parameter's constraint, where the other targets type the consumer's fill by the type argument (ADR-0068).", + "file": "generics/scoped-slot/Fruits.uf.tsx", + "start": { + "line": 13, + "column": 9, + "offset": 298 + }, + "end": { + "line": 17, + "column": 10, + "offset": 403 + }, + "target": "angular" + }, + { + "code": "UF4004", + "severity": "info", + "message": "The angular target does not support generic-slot-props: Angular infers a component's type parameter from its inputs alone, and a slot's directive cannot infer it, so a scoped slot's context takes the parameter's constraint, where the other targets type the consumer's fill by the type argument (ADR-0068).", + "file": "generics/scoped-slot/Picker.uf.tsx", + "start": { + "line": 10, + "column": 31, + "offset": 326 + }, + "end": { + "line": 10, + "column": 65, + "offset": 360 + }, + "target": "angular" + } +] diff --git a/tests/integration/cases/generics/scoped-slot/__expected__/dom.basket.html b/tests/integration/cases/generics/scoped-slot/__expected__/dom.basket.html new file mode 100644 index 000000000..b006a70ce --- /dev/null +++ b/tests/integration/cases/generics/scoped-slot/__expected__/dom.basket.html @@ -0,0 +1,12 @@ +
    +
  • + + "Apple × 3" + +
  • +
  • + + "Pear × 1" + +
  • +
diff --git a/tests/integration/cases/generics/scoped-slot/__expected__/geometry.basket.json b/tests/integration/cases/generics/scoped-slot/__expected__/geometry.basket.json new file mode 100644 index 000000000..a4cd7be73 --- /dev/null +++ b/tests/integration/cases/generics/scoped-slot/__expected__/geometry.basket.json @@ -0,0 +1,415 @@ +{ + "version": 1, + "nodes": { + "ul[0]": { + "box": [ + 8, + 24, + 113.3125, + 48 + ], + "style": { + "display": "block", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "16px", + "margin-right": "0px", + "margin-bottom": "16px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "40px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "ul[0]/li[0]": { + "box": [ + 48, + 24, + 73.3125, + 24 + ], + "style": { + "display": "list-item", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "ul[0]/li[0]/span[0]": { + "box": [ + 48, + 26, + 73.3125, + 20 + ], + "style": { + "display": "inline", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "ul[0]/li[0]/span[0]/#text[0]": { + "box": [ + [ + 48, + 26, + 73.3125, + 20 + ] + ] + }, + "ul[0]/li[1]": { + "box": [ + 48, + 48, + 73.3125, + 24 + ], + "style": { + "display": "list-item", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "ul[0]/li[1]/span[0]": { + "box": [ + 48, + 50, + 60.71875, + 20 + ], + "style": { + "display": "inline", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "ul[0]/li[1]/span[0]/#text[0]": { + "box": [ + [ + 48, + 50, + 60.71875, + 20 + ] + ] + } + } +} diff --git a/tests/integration/cases/generics/scoped-slot/__expected__/ssr.basket.html b/tests/integration/cases/generics/scoped-slot/__expected__/ssr.basket.html new file mode 100644 index 000000000..b006a70ce --- /dev/null +++ b/tests/integration/cases/generics/scoped-slot/__expected__/ssr.basket.html @@ -0,0 +1,12 @@ +
    +
  • + + "Apple × 3" + +
  • +
  • + + "Pear × 1" + +
  • +
diff --git a/tests/integration/cases/generics/scoped-slot/__output__/angular/fruits.ts b/tests/integration/cases/generics/scoped-slot/__output__/angular/fruits.ts new file mode 100644 index 000000000..2aef2355f --- /dev/null +++ b/tests/integration/cases/generics/scoped-slot/__output__/angular/fruits.ts @@ -0,0 +1,61 @@ +import Picker, { PickerItem } from "./picker"; +import { + Component, + Directive, + ElementRef, + type OnInit, + Renderer2, + inject, + input, +} from "@angular/core"; + +interface Fruit { + name: string; + count: number; +} + +/** The scope attributes taken off each host, for the roots its component renders later. */ +const scopes = new WeakMap(); + +/** + * Gives a root node of its component the scope attributes the component's styled parents put on + * its host (ADR-0063), and takes them off the host, which keeps only `display: contents`. + */ +@Directive({ selector: "[ufScope]" }) +export class ScopeAttributes implements OnInit { + readonly ufScope = input.required(); + private readonly element = inject>(ElementRef); + private readonly renderer = inject(Renderer2); + + ngOnInit(): void { + const host = this.ufScope(); + let names = scopes.get(host); + if (!names) { + names = host.getAttributeNames().filter((name) => name.startsWith("data-uf-")); + for (const name of names) this.renderer.removeAttribute(host, name); + scopes.set(host, names); + } + for (const name of names) this.renderer.setAttribute(this.element.nativeElement, name, ""); + } +} + +@Component({ + selector: "uf-fruits", + imports: [Picker, PickerItem, ScopeAttributes], + host: { "[attr.class]": "null", "[attr.style]": "'display: contents;'" }, + preserveWhitespaces: false, + template: ` + @let fruits = this.fruits(); + + + {{ item.name }} × {{ item.count }} + + + `, +}) +export default class Fruits { + readonly fruits = input.required(); + readonly class = input(); + readonly style = input>(); + protected readonly host = inject>(ElementRef).nativeElement; +} diff --git a/tests/integration/cases/generics/scoped-slot/__output__/angular/picker.ts b/tests/integration/cases/generics/scoped-slot/__output__/angular/picker.ts new file mode 100644 index 000000000..dd8020741 --- /dev/null +++ b/tests/integration/cases/generics/scoped-slot/__output__/angular/picker.ts @@ -0,0 +1,85 @@ +import { NgTemplateOutlet } from "@angular/common"; +import { + Component, + Directive, + ElementRef, + type OnInit, + Renderer2, + TemplateRef, + contentChild, + inject, + input, +} from "@angular/core"; + +export interface PickerProps { + items: T[]; +} + +/** What a template of `Picker`'s `item` slot reads: the slot's props. */ +export type PickerItemContext = { item: T }; + +/** A template of `Picker`'s `item` slot, its `let-` variables typed (ADR-0056). */ +@Directive({ selector: "ng-template[ufPickerItem]" }) +export class PickerItem { + static ngTemplateContextGuard( + _directive: PickerItem, + _context: unknown, + ): _context is PickerItemContext & { $implicit: PickerItemContext } { + return true; + } +} + +/** The scope attributes taken off each host, for the roots its component renders later. */ +const scopes = new WeakMap(); + +/** + * Gives a root node of its component the scope attributes the component's styled parents put on + * its host (ADR-0063), and takes them off the host, which keeps only `display: contents`. + */ +@Directive({ selector: "[ufScope]" }) +export class ScopeAttributes implements OnInit { + readonly ufScope = input.required(); + private readonly element = inject>(ElementRef); + private readonly renderer = inject(Renderer2); + + ngOnInit(): void { + const host = this.ufScope(); + let names = scopes.get(host); + if (!names) { + names = host.getAttributeNames().filter((name) => name.startsWith("data-uf-")); + for (const name of names) this.renderer.removeAttribute(host, name); + scopes.set(host, names); + } + for (const name of names) this.renderer.setAttribute(this.element.nativeElement, name, ""); + } +} + +@Component({ + selector: "uf-picker", + imports: [NgTemplateOutlet, ScopeAttributes], + host: { "[attr.class]": "null", "[attr.style]": "'display: contents;'" }, + preserveWhitespaces: false, + template: ` + @let items = this.items(); +
    + @for (item of items; track index; let index = $index) { +
  • + @if (this.itemTemplate(); as template) { + + } +
  • + } +
+ + `, +}) +export default class Picker { + readonly items = input.required(); + readonly class = input(); + readonly style = input>(); + protected readonly itemTemplate = contentChild(PickerItem, { read: TemplateRef }); + protected readonly host = inject>(ElementRef).nativeElement; +} diff --git a/tests/integration/cases/generics/scoped-slot/__output__/astro/Fruits.astro b/tests/integration/cases/generics/scoped-slot/__output__/astro/Fruits.astro new file mode 100644 index 000000000..eeeafaf02 --- /dev/null +++ b/tests/integration/cases/generics/scoped-slot/__output__/astro/Fruits.astro @@ -0,0 +1,31 @@ +--- +import Picker from "./Picker.astro"; + +interface Fruit { + name: string; + count: number; +} + +interface Props { + fruits: Fruit[]; + class?: string; + style?: Record; +} + +const { fruits, class: className, style } = Astro.props; + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} +--- + + {item.name} × {item.count}} + {...scopeAttributes(Astro.props)} +/> diff --git a/tests/integration/cases/generics/scoped-slot/__output__/astro/Picker.astro b/tests/integration/cases/generics/scoped-slot/__output__/astro/Picker.astro new file mode 100644 index 000000000..97cafe046 --- /dev/null +++ b/tests/integration/cases/generics/scoped-slot/__output__/astro/Picker.astro @@ -0,0 +1,26 @@ +--- +export interface PickerProps { + items: T[]; +} + +interface Props extends PickerProps { + item?: (props: { item: T }) => unknown; + class?: string; + style?: Record; +} + +const { items, item: item_1, class: className, style } = Astro.props; + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} +--- + +
    + {items.map((item) => ( +
  • {item_1?.({ item })}
  • + ))} +
diff --git a/tests/integration/cases/generics/scoped-slot/__output__/ir.Picker.json b/tests/integration/cases/generics/scoped-slot/__output__/ir.Picker.json new file mode 100644 index 000000000..8c9ede11c --- /dev/null +++ b/tests/integration/cases/generics/scoped-slot/__output__/ir.Picker.json @@ -0,0 +1,244 @@ +{ + "irVersion": 1, + "file": "generics/scoped-slot/Picker.uf.tsx", + "components": [ + { + "name": "Picker", + "span": { + "start": 233, + "end": 503 + }, + "typeParameters": [ + { + "name": "T", + "span": { + "start": 264, + "end": 265 + } + } + ], + "props": [ + { + "name": "items", + "optional": false, + "type": { + "code": "T[]", + "span": { + "start": 225, + "end": 228 + } + }, + "binding": "items@269", + "span": { + "start": 218, + "end": 229 + } + } + ], + "propsParameter": { + "form": "destructured", + "type": { + "code": "PickerProps", + "span": { + "start": 278, + "end": 292 + } + }, + "span": { + "start": 267, + "end": 292 + } + }, + "types": [ + "PickerProps" + ], + "slots": { + "binding": "slots@304", + "type": { + "code": "{ item?(props: { item: T }): Element }", + "span": { + "start": 324, + "end": 362 + } + }, + "slots": [ + { + "name": "item", + "optional": true, + "props": { + "code": "{ item: T }", + "span": { + "start": 339, + "end": 350 + } + }, + "references": [ + "T" + ], + "span": { + "start": 326, + "end": 360 + } + } + ], + "span": { + "start": 298, + "end": 366 + } + }, + "bindings": [ + { + "id": "items@269", + "name": "items", + "kind": "prop", + "span": { + "start": 269, + "end": 274 + } + }, + { + "id": "slots@304", + "name": "slots", + "kind": "slots", + "span": { + "start": 304, + "end": 309 + } + }, + { + "id": "item@405", + "name": "item", + "kind": "loopVar", + "span": { + "start": 405, + "end": 409 + } + }, + { + "id": "index@411", + "name": "index", + "kind": "loopVar", + "span": { + "start": 411, + "end": 416 + } + } + ], + "setup": [], + "render": { + "kind": "Element", + "tag": "ul", + "attributes": [], + "children": [ + { + "kind": "For", + "source": { + "code": "items", + "span": { + "start": 394, + "end": 399 + }, + "refs": [ + { + "kind": "Binding", + "binding": "items@269", + "span": { + "start": 394, + "end": 399 + } + } + ] + }, + "item": "item@405", + "index": "index@411", + "key": { + "code": "index", + "span": { + "start": 440, + "end": 445 + }, + "refs": [ + { + "kind": "Binding", + "binding": "index@411", + "span": { + "start": 440, + "end": 445 + } + } + ] + }, + "body": { + "kind": "Element", + "tag": "li", + "attributes": [], + "children": [ + { + "kind": "SlotOutlet", + "slot": "item", + "props": { + "code": "{ item }", + "span": { + "start": 461, + "end": 469 + }, + "refs": [ + { + "kind": "Binding", + "binding": "item@405", + "span": { + "start": 463, + "end": 467 + }, + "shorthand": true + } + ] + }, + "fallback": [], + "span": { + "start": 447, + "end": 471 + } + } + ], + "span": { + "start": 431, + "end": 476 + } + }, + "span": { + "start": 394, + "end": 485 + } + } + ], + "span": { + "start": 382, + "end": 496 + } + } + } + ], + "exports": [ + { + "kind": "default", + "name": "default", + "local": "Picker", + "span": { + "start": 233, + "end": 503 + } + } + ], + "types": [ + { + "name": "PickerProps", + "exported": true, + "code": "interface PickerProps {\n items: T[];\n}", + "span": { + "start": 189, + "end": 231 + } + } + ] +} diff --git a/tests/integration/cases/generics/scoped-slot/__output__/ir.json b/tests/integration/cases/generics/scoped-slot/__output__/ir.json new file mode 100644 index 000000000..0ef395c87 --- /dev/null +++ b/tests/integration/cases/generics/scoped-slot/__output__/ir.json @@ -0,0 +1,289 @@ +{ + "irVersion": 1, + "file": "generics/scoped-slot/Fruits.uf.tsx", + "components": [ + { + "name": "Fruits", + "span": { + "start": 176, + "end": 434 + }, + "props": [ + { + "name": "fruits", + "optional": false, + "type": { + "code": "Fruit[]", + "span": { + "start": 229, + "end": 236 + } + }, + "binding": "fruits@209", + "span": { + "start": 221, + "end": 236 + } + } + ], + "propsParameter": { + "form": "destructured", + "type": { + "code": "{ fruits: Fruit[] }", + "span": { + "start": 219, + "end": 238 + } + }, + "span": { + "start": 207, + "end": 238 + } + }, + "types": [ + "Fruit" + ], + "bindings": [ + { + "id": "fruits@209", + "name": "fruits", + "kind": "prop", + "span": { + "start": 209, + "end": 215 + } + }, + { + "id": "item@307", + "name": "item", + "kind": "slotScope", + "span": { + "start": 307, + "end": 311 + } + } + ], + "setup": [], + "render": { + "kind": "Fragment", + "children": [ + { + "kind": "Component", + "component": "Picker", + "attributes": [ + { + "kind": "Prop", + "name": "items", + "value": { + "code": "fruits", + "span": { + "start": 272, + "end": 278 + }, + "refs": [ + { + "kind": "Binding", + "binding": "fruits@209", + "span": { + "start": 272, + "end": 278 + } + } + ] + }, + "span": { + "start": 265, + "end": 279 + } + } + ], + "fills": [ + { + "slot": "item", + "parameter": { + "pattern": { + "code": "{ item }", + "names": [ + "item" + ], + "span": { + "start": 305, + "end": 313 + } + }, + "span": { + "start": 305, + "end": 313 + } + }, + "children": [ + { + "kind": "Element", + "tag": "span", + "attributes": [], + "children": [ + { + "kind": "Interpolation", + "value": { + "code": "item.name", + "span": { + "start": 350, + "end": 359 + }, + "refs": [ + { + "kind": "Binding", + "binding": "item@307", + "span": { + "start": 350, + "end": 354 + } + } + ] + }, + "span": { + "start": 349, + "end": 360 + } + }, + { + "kind": "Text", + "value": " × ", + "span": { + "start": 360, + "end": 363 + } + }, + { + "kind": "Interpolation", + "value": { + "code": "item.count", + "span": { + "start": 364, + "end": 374 + }, + "refs": [ + { + "kind": "Binding", + "binding": "item@307", + "span": { + "start": 364, + "end": 368 + } + } + ] + }, + "span": { + "start": 363, + "end": 375 + } + } + ], + "span": { + "start": 330, + "end": 393 + } + } + ], + "span": { + "start": 298, + "end": 403 + } + } + ], + "span": { + "start": 257, + "end": 427 + } + } + ], + "span": { + "start": 257, + "end": 427 + } + } + } + ], + "exports": [ + { + "kind": "default", + "name": "default", + "local": "Fruits", + "span": { + "start": 176, + "end": 434 + } + } + ], + "types": [ + { + "name": "Fruit", + "exported": false, + "code": "interface Fruit {\n name: string;\n count: number;\n}", + "span": { + "start": 39, + "end": 91 + } + } + ], + "imports": [ + { + "specifier": "./Picker.uf.tsx", + "file": "Picker.uf.tsx", + "api": { + "file": "Picker.uf.tsx", + "components": [ + { + "name": "Picker", + "export": "default", + "props": [ + { + "name": "items", + "optional": false, + "type": "T[]", + "references": [ + "T" + ] + } + ], + "events": [], + "models": [], + "slots": [ + { + "name": "item", + "optional": true, + "props": "{ item: T }", + "references": [ + "T" + ] + } + ], + "exposes": [], + "inheritAttrs": true, + "root": "element", + "typeParameters": [ + "T" + ], + "rootTag": "ul" + } + ], + "keys": [] + }, + "names": [ + { + "kind": "Component", + "imported": "default", + "local": "Picker", + "span": { + "start": 7, + "end": 13 + } + } + ], + "span": { + "start": 0, + "end": 37 + } + } + ] +} diff --git a/tests/integration/cases/generics/scoped-slot/__output__/qwik/Fruits.tsx b/tests/integration/cases/generics/scoped-slot/__output__/qwik/Fruits.tsx new file mode 100644 index 000000000..3a9d1586a --- /dev/null +++ b/tests/integration/cases/generics/scoped-slot/__output__/qwik/Fruits.tsx @@ -0,0 +1,36 @@ +import Picker from "./Picker"; +import { type CSSProperties, type ClassList, component$ } from "@qwik.dev/core"; + +interface Fruit { + name: string; + count: number; +} + +export default component$<{ fruits: Fruit[] } & { class?: ClassList; style?: CSSProperties }>( + ({ fruits, class: className, style, ...attributes }) => { + return ( + <> + ( + + {item.name} × {item.count} + + )} + class={className} + style={style} + /> + + ); + }, +); + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/generics/scoped-slot/__output__/qwik/Picker.tsx b/tests/integration/cases/generics/scoped-slot/__output__/qwik/Picker.tsx new file mode 100644 index 000000000..25f4a2611 --- /dev/null +++ b/tests/integration/cases/generics/scoped-slot/__output__/qwik/Picker.tsx @@ -0,0 +1,36 @@ +import { + type CSSProperties, + type ClassList, + type JSXOutput, + type QRL, + component$, +} from "@qwik.dev/core"; + +export interface PickerProps { + items: T[]; +} + +export interface PickerSlots { + item$?: QRL<(props: { item: T }) => JSXOutput>; +} + +export default component$< + PickerProps & PickerSlots & { class?: ClassList; style?: CSSProperties } +>(({ items, item$, class: className, style, ...attributes }) => { + return ( +
    + {items.map((item, index) => ( +
  • {item$?.({ item })}
  • + ))} +
+ ); +}); + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/generics/scoped-slot/__output__/react/Fruits.tsx b/tests/integration/cases/generics/scoped-slot/__output__/react/Fruits.tsx new file mode 100644 index 000000000..3d048fea0 --- /dev/null +++ b/tests/integration/cases/generics/scoped-slot/__output__/react/Fruits.tsx @@ -0,0 +1,42 @@ +import Picker from "./Picker"; +import type { CSSProperties } from "react"; + +interface Fruit { + name: string; + count: number; +} + +export interface FruitsAttributes { + className?: string; + style?: CSSProperties; +} + +export default function Fruits({ + fruits, + className, + style, + ...attributes +}: { fruits: Fruit[] } & FruitsAttributes) { + return ( + ( + + {item.name} × {item.count} + + )} + /> + ); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/generics/scoped-slot/__output__/react/Picker.tsx b/tests/integration/cases/generics/scoped-slot/__output__/react/Picker.tsx new file mode 100644 index 000000000..633d532cc --- /dev/null +++ b/tests/integration/cases/generics/scoped-slot/__output__/react/Picker.tsx @@ -0,0 +1,39 @@ +import type { CSSProperties, ReactNode } from "react"; + +export interface PickerProps { + items: T[]; +} + +export interface PickerSlots { + renderItem?: (props: { item: T }) => ReactNode; +} + +export interface PickerAttributes { + className?: string; + style?: CSSProperties; +} + +export default function Picker({ + items, + renderItem, + className, + style, + ...attributes +}: PickerProps & PickerSlots & PickerAttributes) { + return ( +
    + {items.map((item, index) => ( +
  • {renderItem?.({ item })}
  • + ))} +
+ ); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/generics/scoped-slot/__output__/solid/Fruits.tsx b/tests/integration/cases/generics/scoped-slot/__output__/solid/Fruits.tsx new file mode 100644 index 000000000..8c15f8e3f --- /dev/null +++ b/tests/integration/cases/generics/scoped-slot/__output__/solid/Fruits.tsx @@ -0,0 +1,32 @@ +import Picker from "./Picker"; +import type { JSX } from "solid-js"; + +interface Fruit { + name: string; + count: number; +} + +export default function Fruits( + props: { fruits: Fruit[] } & { class?: string; style?: JSX.CSSProperties }, +) { + return ( + ( + + {item.name} × {item.count} + + )} + /> + ); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} diff --git a/tests/integration/cases/generics/scoped-slot/__output__/solid/Picker.tsx b/tests/integration/cases/generics/scoped-slot/__output__/solid/Picker.tsx new file mode 100644 index 000000000..57e4819c7 --- /dev/null +++ b/tests/integration/cases/generics/scoped-slot/__output__/solid/Picker.tsx @@ -0,0 +1,26 @@ +import { For, type JSX } from "solid-js"; + +export interface PickerProps { + items: T[]; +} + +export interface PickerSlots { + item?: (props: { item: T }) => JSX.Element; +} + +export default function Picker( + props: PickerProps & PickerSlots & { class?: string; style?: JSX.CSSProperties }, +) { + return ( +
    + {(item) =>
  • {props.item?.({ item })}
  • }
    +
+ ); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} diff --git a/tests/integration/cases/generics/scoped-slot/__output__/svelte/Fruits.svelte b/tests/integration/cases/generics/scoped-slot/__output__/svelte/Fruits.svelte new file mode 100644 index 000000000..665966c0f --- /dev/null +++ b/tests/integration/cases/generics/scoped-slot/__output__/svelte/Fruits.svelte @@ -0,0 +1,28 @@ + + + + + + {#snippet item({ item })} + {item.name} × {item.count} + {/snippet} + diff --git a/tests/integration/cases/generics/scoped-slot/__output__/svelte/Picker.svelte b/tests/integration/cases/generics/scoped-slot/__output__/svelte/Picker.svelte new file mode 100644 index 000000000..7a812e5a6 --- /dev/null +++ b/tests/integration/cases/generics/scoped-slot/__output__/svelte/Picker.svelte @@ -0,0 +1,31 @@ + + + + +
    + {#each items as item, index (index)} +
  • {@render itemSnippet?.({ item })}
  • + {/each} +
diff --git a/tests/integration/cases/generics/scoped-slot/__output__/vue/Fruits.vue b/tests/integration/cases/generics/scoped-slot/__output__/vue/Fruits.vue new file mode 100644 index 000000000..006e388c6 --- /dev/null +++ b/tests/integration/cases/generics/scoped-slot/__output__/vue/Fruits.vue @@ -0,0 +1,18 @@ + + + diff --git a/tests/integration/cases/generics/scoped-slot/__output__/vue/Picker.vue b/tests/integration/cases/generics/scoped-slot/__output__/vue/Picker.vue new file mode 100644 index 000000000..67e1c653e --- /dev/null +++ b/tests/integration/cases/generics/scoped-slot/__output__/vue/Picker.vue @@ -0,0 +1,16 @@ + + + diff --git a/tests/integration/cases/generics/scoped-slot/__screenshots__/basket-chromium-linux.png b/tests/integration/cases/generics/scoped-slot/__screenshots__/basket-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..af7120718f16c62f9a20877a26e076c311c54f87 GIT binary patch literal 2370 zcmb`J`9BkmAIC?|vPcQz<7mpLoo>Q9VITtGIf+*{Q)d4vK2;nLGNaNBkZA-at|&XZ*K z%L0t=57=!tiTd-)KO!O`Z63-eGCF6Ad z>)lQA;I}uCAe%fQ?;TzoI%au$TtsT9ZwR7ap6zV+VBz>3cUA|NY+&8J?&bV-iP8m) zU2cVLC&;*_p_aXoxqyzMgUuF3D0wkA<=ti;s!Ow61X)hWetYs!ZD zlg&9!d=}$kqo-fifC2kGz}G&Ok#n3hadWZCdng#E@;qQsD9Uz<^l@&^4>tn`l*LUK z=axRHIj4c{))(r1#wzwq&)y24nxwItlxCl&UgYWOm{N$N(P#%d>!`Zt7&JN#`~FWW z?qZ6y9pE0#@02U>#gx&;2a3IcPwE1D_Q&u?=PvJ7!Tf*K`}*p1UbWyD!0 zw$${UAbT;?EQV|KYt!Jbt;j6N!tcvZ^KIL@-ZFT}=P$Z;Ii}x@p}$ggzXOM0j68X2 zE&mQZ7hFk|kCGA5gkeeD@s-|_XEiJ>lovh6S%Zs(Ki_k_7K1zjZzBwZwqjq@^w1;s zs$VYVdAiR^9WBlH>}F4VUG;&kkLHbGcUB_Svw$x#y^Q1o@*>E%-NoZ~Ka=)E(@UTu zfKyZf7O-f>po1US+o*E5aO90_{Km{IX@yy$ ztm^vYUW$ScM@`u2>Bf|6t?lG8+ZR=bpqnKXpv>o$OQ5I@Mk?BJj>Y!-(0Pq`(=RN; zN*cz|P|9HS6NW^gaYY#|I4ao*eaRWkBnA|JHbol$~=*QuQQJtp$)eCur~y%Ej%_BVMR z-BnK?i2({O4C`=xOPNCRY9JifBrp8_ggQ{bcPs8rdvne#RU9m5M;Ui+9|`BZYJG!6 z2}IPrmUf=U{^fp8<$5KkYJa7%Sf8%z?U0L@j{M;LD&c3QhHDQ;tXg+@HD4!{k>*J+|*`0xh%(i*}pB9n@klf4@x>j@03QcCsMg!8O>V5M3hxg4CPq%6cBq+f#FFzf;ZX#N3; z_&*@3QY4?^ci+!#n9HL0eQbGk7&cGqVwI)d#K$jsnP-%Fsj%>GbJUOBt@Mxa#lzNG z=10TeA}z4@QmIMveAkNVh9^ddj+MSw;Qb$LI_)f^P#z4n9)O;?P(N0#`{Qh@k7fIzFEYj?d7U)XBtz>cq-9}e=T4u}ny z5L$M_>qRqiMx>vnV4V28GHU145VatbAPp8i8y-V6B8;4+)YR0(ifaZl=et+ODw(&l zva+c4uW?C?%*X86n>O)!)^ve#)lpOWSj&4s>D*Yb{;X~|(oIw>R|}fY9*ke&+lmeR zJ$mj+sS+8uLx}6N;8Lz1;ZC!a0$ZCxoXP!BdB&Aw-20vk^sBn2Z zO+=cw5M^tJ{;@5M-J0z{4Cs?@=Q8DKkn&|CaXoeiV9w>NEV)LZQezd5v0<>a%~P32 zx=cu;V2MwN)5k^MJw+uRJ^}bDXL{^Kl>_Pd`QuEN!)P~^j)$^XW}{}y>xm{q(Q(6# z+1||C!fs2pqjik0Yd64=mq|n#RxIkDe<=Iacax=Mv~t-HAvTPpe_sv$CmK`O69GCt j-q5DHoh%&xRiIM { + it("renders the fill with each item", async () => { + const view = await mountScenario(Fruits, "basket"); + await view.expectParity("basket"); + await expect.element(view.getByRole("listitem").nth(0)).toHaveTextContent("Apple × 3"); + await expect.element(view.getByRole("listitem").nth(1)).toHaveTextContent("Pear × 1"); + }); +}); diff --git a/tests/integration/cases/generics/select/Select.uf.tsx b/tests/integration/cases/generics/select/Select.uf.tsx new file mode 100644 index 000000000..c63d92838 --- /dev/null +++ b/tests/integration/cases/generics/select/Select.uf.tsx @@ -0,0 +1,23 @@ +import { defineModel } from "unframework"; + +// A generic model (ADR-0068): the value is one of the consumer's options. +export interface SelectProps { + label: string; + options: readonly T[]; +} + +export default function Select({ label, options }: SelectProps) { + const value = defineModel("value", { required: true }); + return ( + + ); +} diff --git a/tests/integration/cases/generics/select/SizeForm.uf.tsx b/tests/integration/cases/generics/select/SizeForm.uf.tsx new file mode 100644 index 000000000..e11520c73 --- /dev/null +++ b/tests/integration/cases/generics/select/SizeForm.uf.tsx @@ -0,0 +1,17 @@ +import { ref } from "unframework"; + +import Select from "./Select.uf.tsx"; + +type Size = "s" | "m" | "l"; + +// The harness parent (ADR-0057): it binds the generic model to a state of its own type. +export default function SizeForm() { + const sizes: Size[] = ["s", "m", "l"]; + const size = ref("m"); + return ( +
+ + + + + + + + "Chosen: m" + +
diff --git a/tests/integration/cases/generics/select/__expected__/dom.large.html b/tests/integration/cases/generics/select/__expected__/dom.large.html new file mode 100644 index 000000000..65de5ef15 --- /dev/null +++ b/tests/integration/cases/generics/select/__expected__/dom.large.html @@ -0,0 +1,19 @@ +
+ + + "Chosen: l" + +
diff --git a/tests/integration/cases/generics/select/__expected__/geometry.initial.json b/tests/integration/cases/generics/select/__expected__/geometry.initial.json new file mode 100644 index 000000000..24f452bc5 --- /dev/null +++ b/tests/integration/cases/generics/select/__expected__/geometry.initial.json @@ -0,0 +1,580 @@ +{ + "version": 1, + "nodes": { + "section[0]": { + "box": [ + 8, + 8, + 151.671875, + 24 + ], + "style": { + "display": "block", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "section[0]/label[0]": { + "box": [ + 8, + 10, + 70.578125, + 20 + ], + "style": { + "display": "inline", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "section[0]/label[0]/#text[0]": { + "box": [ + [ + 8, + 10, + 36.578125, + 20 + ] + ] + }, + "section[0]/label[0]/select[0]": { + "box": [ + 44.578125, + 11, + 34, + 20 + ], + "style": { + "display": "inline-block", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "border-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "1px", + "border-right-width": "1px", + "border-bottom-width": "1px", + "border-left-width": "1px", + "border-top-style": "solid", + "border-right-style": "solid", + "border-bottom-style": "solid", + "border-left-style": "solid", + "border-top-color": "rgb(118, 118, 118)", + "border-right-color": "rgb(118, 118, 118)", + "border-bottom-color": "rgb(118, 118, 118)", + "border-left-color": "rgb(118, 118, 118)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgb(239, 239, 239)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "13.3333px", + "font-weight": "400", + "font-style": "normal", + "line-height": "normal", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "pre", + "vertical-align": "baseline", + "overflow-x": "clip", + "overflow-y": "clip", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "center", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "section[0]/label[0]/select[0]/option[0]": { + "box": [ + 0, + 0, + 0, + 0 + ], + "style": { + "display": "block", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "2px", + "padding-bottom": "1px", + "padding-left": "2px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "13.3333px", + "font-weight": "400", + "font-style": "normal", + "line-height": "normal", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "nowrap", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "center", + "row-gap": "6.66667px", + "column-gap": "6.66667px", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "section[0]/label[0]/select[0]/option[0]/#text[0]": { + "box": [] + }, + "section[0]/label[0]/select[0]/option[1]": { + "box": [ + 0, + 0, + 0, + 0 + ], + "style": { + "display": "block", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "2px", + "padding-bottom": "1px", + "padding-left": "2px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "13.3333px", + "font-weight": "400", + "font-style": "normal", + "line-height": "normal", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "nowrap", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "center", + "row-gap": "6.66667px", + "column-gap": "6.66667px", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "section[0]/label[0]/select[0]/option[1]/#text[0]": { + "box": [] + }, + "section[0]/label[0]/select[0]/option[2]": { + "box": [ + 0, + 0, + 0, + 0 + ], + "style": { + "display": "block", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "2px", + "padding-bottom": "1px", + "padding-left": "2px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "13.3333px", + "font-weight": "400", + "font-style": "normal", + "line-height": "normal", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "nowrap", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "center", + "row-gap": "6.66667px", + "column-gap": "6.66667px", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "section[0]/label[0]/select[0]/option[2]/#text[0]": { + "box": [] + }, + "section[0]/output[0]": { + "box": [ + 78.578125, + 10, + 81.09375, + 20 + ], + "style": { + "display": "inline", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "section[0]/output[0]/#text[0]": { + "box": [ + [ + 78.578125, + 10, + 81.09375, + 20 + ] + ] + } + } +} diff --git a/tests/integration/cases/generics/select/__expected__/geometry.large.json b/tests/integration/cases/generics/select/__expected__/geometry.large.json new file mode 100644 index 000000000..b4645fb28 --- /dev/null +++ b/tests/integration/cases/generics/select/__expected__/geometry.large.json @@ -0,0 +1,580 @@ +{ + "version": 1, + "nodes": { + "section[0]": { + "box": [ + 8, + 8, + 141.53125, + 24 + ], + "style": { + "display": "block", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "section[0]/label[0]": { + "box": [ + 8, + 10, + 70.578125, + 20 + ], + "style": { + "display": "inline", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "section[0]/label[0]/#text[0]": { + "box": [ + [ + 8, + 10, + 36.578125, + 20 + ] + ] + }, + "section[0]/label[0]/select[0]": { + "box": [ + 44.578125, + 11, + 34, + 20 + ], + "style": { + "display": "inline-block", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "border-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "1px", + "border-right-width": "1px", + "border-bottom-width": "1px", + "border-left-width": "1px", + "border-top-style": "solid", + "border-right-style": "solid", + "border-bottom-style": "solid", + "border-left-style": "solid", + "border-top-color": "rgb(118, 118, 118)", + "border-right-color": "rgb(118, 118, 118)", + "border-bottom-color": "rgb(118, 118, 118)", + "border-left-color": "rgb(118, 118, 118)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgb(239, 239, 239)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "13.3333px", + "font-weight": "400", + "font-style": "normal", + "line-height": "normal", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "pre", + "vertical-align": "baseline", + "overflow-x": "clip", + "overflow-y": "clip", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "center", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "section[0]/label[0]/select[0]/option[0]": { + "box": [ + 0, + 0, + 0, + 0 + ], + "style": { + "display": "block", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "2px", + "padding-bottom": "1px", + "padding-left": "2px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "13.3333px", + "font-weight": "400", + "font-style": "normal", + "line-height": "normal", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "nowrap", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "center", + "row-gap": "6.66667px", + "column-gap": "6.66667px", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "section[0]/label[0]/select[0]/option[0]/#text[0]": { + "box": [] + }, + "section[0]/label[0]/select[0]/option[1]": { + "box": [ + 0, + 0, + 0, + 0 + ], + "style": { + "display": "block", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "2px", + "padding-bottom": "1px", + "padding-left": "2px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "13.3333px", + "font-weight": "400", + "font-style": "normal", + "line-height": "normal", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "nowrap", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "center", + "row-gap": "6.66667px", + "column-gap": "6.66667px", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "section[0]/label[0]/select[0]/option[1]/#text[0]": { + "box": [] + }, + "section[0]/label[0]/select[0]/option[2]": { + "box": [ + 0, + 0, + 0, + 0 + ], + "style": { + "display": "block", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "2px", + "padding-bottom": "1px", + "padding-left": "2px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "13.3333px", + "font-weight": "400", + "font-style": "normal", + "line-height": "normal", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "nowrap", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "center", + "row-gap": "6.66667px", + "column-gap": "6.66667px", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "section[0]/label[0]/select[0]/option[2]/#text[0]": { + "box": [] + }, + "section[0]/output[0]": { + "box": [ + 78.578125, + 10, + 70.953125, + 20 + ], + "style": { + "display": "inline", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "section[0]/output[0]/#text[0]": { + "box": [ + [ + 78.578125, + 10, + 70.953125, + 20 + ] + ] + } + } +} diff --git a/tests/integration/cases/generics/select/__expected__/ssr.default.html b/tests/integration/cases/generics/select/__expected__/ssr.default.html new file mode 100644 index 000000000..a69caddf5 --- /dev/null +++ b/tests/integration/cases/generics/select/__expected__/ssr.default.html @@ -0,0 +1,19 @@ +
+ + + "Chosen: m" + +
diff --git a/tests/integration/cases/generics/select/__expected__/trace.large.json b/tests/integration/cases/generics/select/__expected__/trace.large.json new file mode 100644 index 000000000..84f447777 --- /dev/null +++ b/tests/integration/cases/generics/select/__expected__/trace.large.json @@ -0,0 +1,39 @@ +{ + "version": 1, + "steps": [ + { + "action": "selectOptions getByRole('combobox', { name: 'Size' }) [\"l\"]", + "dom": [ + "
", + " ", + " ", + " \"Chosen: l\"", + " ", + "
" + ], + "aria": [ + "- region \"Size\":", + " - text: Size", + " - combobox \"Size\":", + " - option \"s\"", + " - option \"m\"", + " - option \"l\" [selected]", + " - status: \"Chosen: l\"" + ], + "events": {} + } + ] +} diff --git a/tests/integration/cases/generics/select/__output__/angular/select.ts b/tests/integration/cases/generics/select/__output__/angular/select.ts new file mode 100644 index 000000000..17519af9c --- /dev/null +++ b/tests/integration/cases/generics/select/__output__/angular/select.ts @@ -0,0 +1,73 @@ +import { isPlatformServer } from "@angular/common"; +import { + Component, + Directive, + ElementRef, + type OnInit, + PLATFORM_ID, + Renderer2, + inject, + input, + model, +} from "@angular/core"; + +export interface SelectProps { + label: string; + options: readonly T[]; +} + +/** The scope attributes taken off each host, for the roots its component renders later. */ +const scopes = new WeakMap(); + +/** + * Gives a root node of its component the scope attributes the component's styled parents put on + * its host (ADR-0063), and takes them off the host, which keeps only `display: contents`. + */ +@Directive({ selector: "[ufScope]" }) +export class ScopeAttributes implements OnInit { + readonly ufScope = input.required(); + private readonly element = inject>(ElementRef); + private readonly renderer = inject(Renderer2); + + ngOnInit(): void { + const host = this.ufScope(); + let names = scopes.get(host); + if (!names) { + names = host.getAttributeNames().filter((name) => name.startsWith("data-uf-")); + for (const name of names) this.renderer.removeAttribute(host, name); + scopes.set(host, names); + } + for (const name of names) this.renderer.setAttribute(this.element.nativeElement, name, ""); + } +} + +@Component({ + selector: "uf-select", + imports: [ScopeAttributes], + host: { "[attr.class]": "null", "[attr.style]": "'display: contents;'" }, + preserveWhitespaces: false, + template: ` + @let label = this.label(); + @let options = this.options(); + + `, +}) +export default class Select { + readonly label = input.required(); + readonly options = input.required(); + readonly class = input(); + readonly style = input>(); + readonly value = model.required(); + protected readonly onServer = isPlatformServer(inject(PLATFORM_ID)); + protected readonly host = inject>(ElementRef).nativeElement; +} diff --git a/tests/integration/cases/generics/select/__output__/angular/size-form.ts b/tests/integration/cases/generics/select/__output__/angular/size-form.ts new file mode 100644 index 000000000..32bc0551c --- /dev/null +++ b/tests/integration/cases/generics/select/__output__/angular/size-form.ts @@ -0,0 +1,59 @@ +import Select from "./select"; +import { + Component, + Directive, + ElementRef, + type OnInit, + Renderer2, + inject, + input, + signal, +} from "@angular/core"; + +type Size = "s" | "m" | "l"; + +/** The scope attributes taken off each host, for the roots its component renders later. */ +const scopes = new WeakMap(); + +/** + * Gives a root node of its component the scope attributes the component's styled parents put on + * its host (ADR-0063), and takes them off the host, which keeps only `display: contents`. + */ +@Directive({ selector: "[ufScope]" }) +export class ScopeAttributes implements OnInit { + readonly ufScope = input.required(); + private readonly element = inject>(ElementRef); + private readonly renderer = inject(Renderer2); + + ngOnInit(): void { + const host = this.ufScope(); + let names = scopes.get(host); + if (!names) { + names = host.getAttributeNames().filter((name) => name.startsWith("data-uf-")); + for (const name of names) this.renderer.removeAttribute(host, name); + scopes.set(host, names); + } + for (const name of names) this.renderer.setAttribute(this.element.nativeElement, name, ""); + } +} + +@Component({ + selector: "uf-size-form", + imports: [Select, ScopeAttributes], + host: { "[attr.class]": "null", "[attr.style]": "'display: contents;'" }, + preserveWhitespaces: false, + template: ` + @let size = this.size(); +
+ + Chosen: {{ size }} +
+ `, +}) +export default class SizeForm { + readonly class = input(); + readonly style = input>(); + protected readonly sizes: Size[] = ["s", "m", "l"]; + protected readonly size = signal("m"); + protected readonly host = inject>(ElementRef).nativeElement; +} diff --git a/tests/integration/cases/generics/select/__output__/astro/Select.astro b/tests/integration/cases/generics/select/__output__/astro/Select.astro new file mode 100644 index 000000000..2efe86ef2 --- /dev/null +++ b/tests/integration/cases/generics/select/__output__/astro/Select.astro @@ -0,0 +1,30 @@ +--- +export interface SelectProps { + label: string; + options: readonly T[]; +} + +interface Props extends SelectProps { + value: T; + class?: string; + style?: Record; +} + +const { label, options, value, class: className, style } = Astro.props; + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} +--- + + diff --git a/tests/integration/cases/generics/select/__output__/astro/SizeForm.astro b/tests/integration/cases/generics/select/__output__/astro/SizeForm.astro new file mode 100644 index 000000000..855ce0f2f --- /dev/null +++ b/tests/integration/cases/generics/select/__output__/astro/SizeForm.astro @@ -0,0 +1,27 @@ +--- +import Select from "./Select.astro"; + +type Size = "s" | "m" | "l"; + +interface Props { + class?: string; + style?: Record; +} + +const { class: className, style } = Astro.props; + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} + +const sizes: Size[] = ["s", "m", "l"]; +const size = "m" as Size; +--- + +
+ setValue(element.value)}> + {options.map((option) => ( + + ))} + + + ); + }, +); + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/generics/select/__output__/qwik/SizeForm.tsx b/tests/integration/cases/generics/select/__output__/qwik/SizeForm.tsx new file mode 100644 index 000000000..ac5057b47 --- /dev/null +++ b/tests/integration/cases/generics/select/__output__/qwik/SizeForm.tsx @@ -0,0 +1,33 @@ +import Select from "./Select"; +import { type CSSProperties, type ClassList, component$, useSignal } from "@qwik.dev/core"; + +type Size = "s" | "m" | "l"; + +const sizes: Size[] = ["s", "m", "l"]; + +export default component$<{ class?: ClassList; style?: CSSProperties }>( + ({ class: className, style, ...attributes }) => { + const size = useSignal("m"); + + return ( +
+ { + setLocalValue({ value: event.target.value }); + onValueChange?.(event.target.value); + }} + > + {options.map((option) => ( + + ))} + + + ); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/generics/select/__output__/react/SizeForm.tsx b/tests/integration/cases/generics/select/__output__/react/SizeForm.tsx new file mode 100644 index 000000000..4373d97e4 --- /dev/null +++ b/tests/integration/cases/generics/select/__output__/react/SizeForm.tsx @@ -0,0 +1,31 @@ +import Select from "./Select"; +import { type CSSProperties, useState } from "react"; + +type Size = "s" | "m" | "l"; + +export interface SizeFormAttributes { + className?: string; + style?: CSSProperties; +} + +const sizes: Size[] = ["s", "m", "l"]; + +export default function SizeForm({ className, style, ...attributes }: SizeFormAttributes) { + const [size, setSize] = useState("m"); + + return ( +
+ setValue(event.currentTarget.value)} + ref={(select) => selectOptions(select, value)} + > + + {(option) => ( + + )} + + + + ); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} + +/** + * Selects a ` + Chosen: {size()} +
+ ); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} diff --git a/tests/integration/cases/generics/select/__output__/svelte/Select.svelte b/tests/integration/cases/generics/select/__output__/svelte/Select.svelte new file mode 100644 index 000000000..dddf47560 --- /dev/null +++ b/tests/integration/cases/generics/select/__output__/svelte/Select.svelte @@ -0,0 +1,36 @@ + + + + + diff --git a/tests/integration/cases/generics/select/__output__/svelte/SizeForm.svelte b/tests/integration/cases/generics/select/__output__/svelte/SizeForm.svelte new file mode 100644 index 000000000..d2eb971ca --- /dev/null +++ b/tests/integration/cases/generics/select/__output__/svelte/SizeForm.svelte @@ -0,0 +1,26 @@ + + + + +
+ + + ); +} diff --git a/tests/integration/cases/types/child-type-import/Form.uf.tsx b/tests/integration/cases/types/child-type-import/Form.uf.tsx new file mode 100644 index 000000000..34e5e17af --- /dev/null +++ b/tests/integration/cases/types/child-type-import/Form.uf.tsx @@ -0,0 +1,11 @@ +import Field from "./Field.uf.tsx"; +import type { FieldProps } from "./Field.uf.tsx"; + +// A props type a child's module exports, imported from the child (ADR-0067). +export default function Form({ field }: { field: FieldProps }) { + return ( +
+ + + ); +} diff --git a/tests/integration/cases/types/child-type-import/__expected__/aria.email.yaml b/tests/integration/cases/types/child-type-import/__expected__/aria.email.yaml new file mode 100644 index 000000000..5f2ccaf6f --- /dev/null +++ b/tests/integration/cases/types/child-type-import/__expected__/aria.email.yaml @@ -0,0 +1,3 @@ +- form "Sign up": + - text: Email + - textbox "Email" diff --git a/tests/integration/cases/types/child-type-import/__expected__/diagnostics.json b/tests/integration/cases/types/child-type-import/__expected__/diagnostics.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/tests/integration/cases/types/child-type-import/__expected__/diagnostics.json @@ -0,0 +1 @@ +[] diff --git a/tests/integration/cases/types/child-type-import/__expected__/dom.email.html b/tests/integration/cases/types/child-type-import/__expected__/dom.email.html new file mode 100644 index 000000000..06d6dd9f3 --- /dev/null +++ b/tests/integration/cases/types/child-type-import/__expected__/dom.email.html @@ -0,0 +1,6 @@ +
+ +
diff --git a/tests/integration/cases/types/child-type-import/__expected__/geometry.email.json b/tests/integration/cases/types/child-type-import/__expected__/geometry.email.json new file mode 100644 index 000000000..5470e39f5 --- /dev/null +++ b/tests/integration/cases/types/child-type-import/__expected__/geometry.email.json @@ -0,0 +1,249 @@ +{ + "version": 1, + "nodes": { + "form[0]": { + "box": [ + 8, + 8, + 240.375, + 24 + ], + "style": { + "display": "block", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "form[0]/label[0]": { + "box": [ + 8, + 10, + 240.375, + 20 + ], + "style": { + "display": "inline", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "form[0]/label[0]/#text[0]": { + "box": [ + [ + 8, + 10, + 40.375, + 20 + ] + ] + }, + "form[0]/label[0]/input[0]": { + "box": [ + 48.375, + 10, + 200, + 22 + ], + "style": { + "display": "inline-block", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "1px", + "padding-right": "2px", + "padding-bottom": "1px", + "padding-left": "2px", + "border-top-width": "2px", + "border-right-width": "2px", + "border-bottom-width": "2px", + "border-left-width": "2px", + "border-top-style": "inset", + "border-right-style": "inset", + "border-bottom-style": "inset", + "border-left-style": "inset", + "border-top-color": "rgb(118, 118, 118)", + "border-right-color": "rgb(118, 118, 118)", + "border-bottom-color": "rgb(118, 118, 118)", + "border-left-color": "rgb(118, 118, 118)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgb(255, 255, 255)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "13.3333px", + "font-weight": "400", + "font-style": "normal", + "line-height": "normal", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "clip", + "overflow-y": "clip", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + } + } +} diff --git a/tests/integration/cases/types/child-type-import/__expected__/ssr.email.html b/tests/integration/cases/types/child-type-import/__expected__/ssr.email.html new file mode 100644 index 000000000..aa1b22e51 --- /dev/null +++ b/tests/integration/cases/types/child-type-import/__expected__/ssr.email.html @@ -0,0 +1,6 @@ +
+ +
diff --git a/tests/integration/cases/types/child-type-import/__output__/angular/field.ts b/tests/integration/cases/types/child-type-import/__output__/angular/field.ts new file mode 100644 index 000000000..21c64a6c2 --- /dev/null +++ b/tests/integration/cases/types/child-type-import/__output__/angular/field.ts @@ -0,0 +1,62 @@ +import { + Component, + Directive, + ElementRef, + type OnInit, + Renderer2, + inject, + input, +} from "@angular/core"; + +export interface FieldProps { + name: string; + label: string; +} + +/** The scope attributes taken off each host, for the roots its component renders later. */ +const scopes = new WeakMap(); + +/** + * Gives a root node of its component the scope attributes the component's styled parents put on + * its host (ADR-0063), and takes them off the host, which keeps only `display: contents`. + */ +@Directive({ selector: "[ufScope]" }) +export class ScopeAttributes implements OnInit { + readonly ufScope = input.required(); + private readonly element = inject>(ElementRef); + private readonly renderer = inject(Renderer2); + + ngOnInit(): void { + const host = this.ufScope(); + let names = scopes.get(host); + if (!names) { + names = host.getAttributeNames().filter((name) => name.startsWith("data-uf-")); + for (const name of names) this.renderer.removeAttribute(host, name); + scopes.set(host, names); + } + for (const name of names) this.renderer.setAttribute(this.element.nativeElement, name, ""); + } +} + +@Component({ + selector: "uf-field", + imports: [ScopeAttributes], + host: { "[attr.class]": "null", "[attr.style]": "'display: contents;'" }, + preserveWhitespaces: false, + template: ` + @let name = this.name(); + @let label = this.label(); + + `, +}) +export default class Field { + readonly name = input.required(); + readonly label = input.required(); + readonly class = input(); + readonly style = input>(); + protected readonly host = inject>(ElementRef).nativeElement; +} diff --git a/tests/integration/cases/types/child-type-import/__output__/angular/form.ts b/tests/integration/cases/types/child-type-import/__output__/angular/form.ts new file mode 100644 index 000000000..e859ef782 --- /dev/null +++ b/tests/integration/cases/types/child-type-import/__output__/angular/form.ts @@ -0,0 +1,54 @@ +import Field from "./field"; +import { + Component, + Directive, + ElementRef, + type OnInit, + Renderer2, + inject, + input, +} from "@angular/core"; + +/** The scope attributes taken off each host, for the roots its component renders later. */ +const scopes = new WeakMap(); + +/** + * Gives a root node of its component the scope attributes the component's styled parents put on + * its host (ADR-0063), and takes them off the host, which keeps only `display: contents`. + */ +@Directive({ selector: "[ufScope]" }) +export class ScopeAttributes implements OnInit { + readonly ufScope = input.required(); + private readonly element = inject>(ElementRef); + private readonly renderer = inject(Renderer2); + + ngOnInit(): void { + const host = this.ufScope(); + let names = scopes.get(host); + if (!names) { + names = host.getAttributeNames().filter((name) => name.startsWith("data-uf-")); + for (const name of names) this.renderer.removeAttribute(host, name); + scopes.set(host, names); + } + for (const name of names) this.renderer.setAttribute(this.element.nativeElement, name, ""); + } +} + +@Component({ + selector: "uf-form", + imports: [Field, ScopeAttributes], + host: { "[attr.class]": "null", "[attr.style]": "'display: contents;'" }, + preserveWhitespaces: false, + template: ` + @let field = this.field(); +
+ + + `, +}) +export default class Form { + readonly field = input.required(); + readonly class = input(); + readonly style = input>(); + protected readonly host = inject>(ElementRef).nativeElement; +} diff --git a/tests/integration/cases/types/child-type-import/__output__/astro/Field.astro b/tests/integration/cases/types/child-type-import/__output__/astro/Field.astro new file mode 100644 index 000000000..6cb3ddd0c --- /dev/null +++ b/tests/integration/cases/types/child-type-import/__output__/astro/Field.astro @@ -0,0 +1,26 @@ +--- +export interface FieldProps { + name: string; + label: string; +} + +interface Props extends FieldProps { + class?: string; + style?: Record; +} + +const { name, label, class: className, style } = Astro.props; + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} +--- + + diff --git a/tests/integration/cases/types/child-type-import/__output__/astro/Form.astro b/tests/integration/cases/types/child-type-import/__output__/astro/Form.astro new file mode 100644 index 000000000..126eb3337 --- /dev/null +++ b/tests/integration/cases/types/child-type-import/__output__/astro/Form.astro @@ -0,0 +1,22 @@ +--- +import Field from "./Field.astro"; + +interface Props { + field: FieldProps; + class?: string; + style?: Record; +} + +const { field, class: className, style } = Astro.props; + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} +--- + +
+ + diff --git a/tests/integration/cases/types/child-type-import/__output__/ir.Field.json b/tests/integration/cases/types/child-type-import/__output__/ir.Field.json new file mode 100644 index 000000000..e87fb157a --- /dev/null +++ b/tests/integration/cases/types/child-type-import/__output__/ir.Field.json @@ -0,0 +1,178 @@ +{ + "irVersion": 1, + "file": "types/child-type-import/Field.uf.tsx", + "components": [ + { + "name": "Field", + "span": { + "start": 66, + "end": 211 + }, + "props": [ + { + "name": "name", + "optional": false, + "type": { + "code": "string", + "span": { + "start": 38, + "end": 44 + } + }, + "binding": "name@98", + "span": { + "start": 32, + "end": 45 + } + }, + { + "name": "label", + "optional": false, + "type": { + "code": "string", + "span": { + "start": 55, + "end": 61 + } + }, + "binding": "label@104", + "span": { + "start": 48, + "end": 62 + } + } + ], + "propsParameter": { + "form": "destructured", + "type": { + "code": "FieldProps", + "span": { + "start": 113, + "end": 123 + } + }, + "span": { + "start": 96, + "end": 123 + } + }, + "types": [ + "FieldProps" + ], + "bindings": [ + { + "id": "name@98", + "name": "name", + "kind": "prop", + "span": { + "start": 98, + "end": 102 + } + }, + { + "id": "label@104", + "name": "label", + "kind": "prop", + "span": { + "start": 104, + "end": 109 + } + } + ], + "setup": [], + "render": { + "kind": "Element", + "tag": "label", + "attributes": [], + "children": [ + { + "kind": "Interpolation", + "value": { + "code": "label", + "span": { + "start": 157, + "end": 162 + }, + "refs": [ + { + "kind": "Binding", + "binding": "label@104", + "span": { + "start": 157, + "end": 162 + } + } + ] + }, + "span": { + "start": 156, + "end": 163 + } + }, + { + "kind": "Element", + "tag": "input", + "attributes": [ + { + "kind": "Bound", + "name": "name", + "value": { + "code": "name", + "span": { + "start": 183, + "end": 187 + }, + "refs": [ + { + "kind": "Binding", + "binding": "name@98", + "span": { + "start": 183, + "end": 187 + } + } + ] + }, + "span": { + "start": 177, + "end": 188 + } + } + ], + "children": [], + "span": { + "start": 170, + "end": 191 + } + } + ], + "span": { + "start": 142, + "end": 204 + } + } + } + ], + "exports": [ + { + "kind": "default", + "name": "default", + "local": "Field", + "span": { + "start": 66, + "end": 211 + } + } + ], + "types": [ + { + "name": "FieldProps", + "exported": true, + "code": "interface FieldProps {\n name: string;\n label: string;\n}", + "span": { + "start": 7, + "end": 64 + } + } + ] +} diff --git a/tests/integration/cases/types/child-type-import/__output__/ir.json b/tests/integration/cases/types/child-type-import/__output__/ir.json new file mode 100644 index 000000000..7a7d89d38 --- /dev/null +++ b/tests/integration/cases/types/child-type-import/__output__/ir.json @@ -0,0 +1,221 @@ +{ + "irVersion": 1, + "file": "types/child-type-import/Form.uf.tsx", + "components": [ + { + "name": "Form", + "span": { + "start": 165, + "end": 345 + }, + "props": [ + { + "name": "field", + "optional": false, + "type": { + "code": "FieldProps", + "span": { + "start": 214, + "end": 224 + } + }, + "binding": "field@196", + "span": { + "start": 207, + "end": 224 + } + } + ], + "propsParameter": { + "form": "destructured", + "type": { + "code": "{ field: FieldProps }", + "span": { + "start": 205, + "end": 226 + } + }, + "span": { + "start": 194, + "end": 226 + } + }, + "types": [], + "bindings": [ + { + "id": "field@196", + "name": "field", + "kind": "prop", + "span": { + "start": 196, + "end": 201 + } + } + ], + "setup": [], + "render": { + "kind": "Element", + "tag": "form", + "attributes": [ + { + "kind": "Static", + "name": "aria-label", + "value": "Sign up", + "span": { + "start": 251, + "end": 271 + } + } + ], + "children": [ + { + "kind": "Component", + "component": "Field", + "attributes": [ + { + "kind": "Prop", + "name": "name", + "value": { + "code": "field.name", + "span": { + "start": 292, + "end": 302 + }, + "refs": [ + { + "kind": "Binding", + "binding": "field@196", + "span": { + "start": 292, + "end": 297 + } + } + ] + }, + "span": { + "start": 286, + "end": 303 + } + }, + { + "kind": "Prop", + "name": "label", + "value": { + "code": "field.label", + "span": { + "start": 311, + "end": 322 + }, + "refs": [ + { + "kind": "Binding", + "binding": "field@196", + "span": { + "start": 311, + "end": 316 + } + } + ] + }, + "span": { + "start": 304, + "end": 323 + } + } + ], + "fills": [], + "span": { + "start": 279, + "end": 326 + } + } + ], + "span": { + "start": 245, + "end": 338 + } + } + } + ], + "exports": [ + { + "kind": "default", + "name": "default", + "local": "Form", + "span": { + "start": 165, + "end": 345 + } + } + ], + "types": [], + "imports": [ + { + "specifier": "./Field.uf.tsx", + "file": "Field.uf.tsx", + "api": { + "file": "Field.uf.tsx", + "components": [ + { + "name": "Field", + "export": "default", + "props": [ + { + "name": "name", + "optional": false, + "type": "string" + }, + { + "name": "label", + "optional": false, + "type": "string" + } + ], + "events": [], + "models": [], + "slots": [], + "exposes": [], + "inheritAttrs": true, + "root": "element", + "rootTag": "label" + } + ], + "keys": [] + }, + "names": [ + { + "kind": "Component", + "imported": "default", + "local": "Field", + "span": { + "start": 7, + "end": 12 + } + } + ], + "span": { + "start": 0, + "end": 35 + } + } + ], + "typeImports": [ + { + "specifier": "./Field.uf.tsx", + "names": [ + { + "imported": "FieldProps", + "local": "FieldProps", + "span": { + "start": 50, + "end": 60 + } + } + ], + "span": { + "start": 36, + "end": 85 + } + } + ] +} diff --git a/tests/integration/cases/types/child-type-import/__output__/qwik/Field.tsx b/tests/integration/cases/types/child-type-import/__output__/qwik/Field.tsx new file mode 100644 index 000000000..5880c057c --- /dev/null +++ b/tests/integration/cases/types/child-type-import/__output__/qwik/Field.tsx @@ -0,0 +1,26 @@ +import { type CSSProperties, type ClassList, component$ } from "@qwik.dev/core"; + +export interface FieldProps { + name: string; + label: string; +} + +export default component$( + ({ name, label, class: className, style, ...attributes }) => { + return ( + + ); + }, +); + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/types/child-type-import/__output__/qwik/Form.tsx b/tests/integration/cases/types/child-type-import/__output__/qwik/Form.tsx new file mode 100644 index 000000000..49f421108 --- /dev/null +++ b/tests/integration/cases/types/child-type-import/__output__/qwik/Form.tsx @@ -0,0 +1,21 @@ +import Field from "./Field"; +import { type CSSProperties, type ClassList, component$ } from "@qwik.dev/core"; + +export default component$<{ field: FieldProps } & { class?: ClassList; style?: CSSProperties }>( + ({ field, class: className, style, ...attributes }) => { + return ( +
+ + + ); + }, +); + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/types/child-type-import/__output__/react/Field.tsx b/tests/integration/cases/types/child-type-import/__output__/react/Field.tsx new file mode 100644 index 000000000..b76980e6c --- /dev/null +++ b/tests/integration/cases/types/child-type-import/__output__/react/Field.tsx @@ -0,0 +1,35 @@ +import type { CSSProperties } from "react"; + +export interface FieldProps { + name: string; + label: string; +} + +export interface FieldAttributes { + className?: string; + style?: CSSProperties; +} + +export default function Field({ + name, + label, + className, + style, + ...attributes +}: FieldProps & FieldAttributes) { + return ( + + ); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/types/child-type-import/__output__/react/Form.tsx b/tests/integration/cases/types/child-type-import/__output__/react/Form.tsx new file mode 100644 index 000000000..6c9e1d7d2 --- /dev/null +++ b/tests/integration/cases/types/child-type-import/__output__/react/Form.tsx @@ -0,0 +1,29 @@ +import Field from "./Field"; +import type { CSSProperties } from "react"; + +export interface FormAttributes { + className?: string; + style?: CSSProperties; +} + +export default function Form({ + field, + className, + style, + ...attributes +}: { field: FieldProps } & FormAttributes) { + return ( +
+ + + ); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/types/child-type-import/__output__/solid/Field.tsx b/tests/integration/cases/types/child-type-import/__output__/solid/Field.tsx new file mode 100644 index 000000000..d1bb2a55d --- /dev/null +++ b/tests/integration/cases/types/child-type-import/__output__/solid/Field.tsx @@ -0,0 +1,22 @@ +import type { JSX } from "solid-js"; + +export interface FieldProps { + name: string; + label: string; +} + +export default function Field(props: FieldProps & { class?: string; style?: JSX.CSSProperties }) { + return ( + + ); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} diff --git a/tests/integration/cases/types/child-type-import/__output__/solid/Form.tsx b/tests/integration/cases/types/child-type-import/__output__/solid/Form.tsx new file mode 100644 index 000000000..2063a569d --- /dev/null +++ b/tests/integration/cases/types/child-type-import/__output__/solid/Form.tsx @@ -0,0 +1,19 @@ +import Field from "./Field"; +import type { JSX } from "solid-js"; + +export default function Form( + props: { field: FieldProps } & { class?: string; style?: JSX.CSSProperties }, +) { + return ( +
+ + + ); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} diff --git a/tests/integration/cases/types/child-type-import/__output__/svelte/Field.svelte b/tests/integration/cases/types/child-type-import/__output__/svelte/Field.svelte new file mode 100644 index 000000000..c2ce87caf --- /dev/null +++ b/tests/integration/cases/types/child-type-import/__output__/svelte/Field.svelte @@ -0,0 +1,23 @@ + + + + + diff --git a/tests/integration/cases/types/child-type-import/__output__/svelte/Form.svelte b/tests/integration/cases/types/child-type-import/__output__/svelte/Form.svelte new file mode 100644 index 000000000..5a1d060bd --- /dev/null +++ b/tests/integration/cases/types/child-type-import/__output__/svelte/Form.svelte @@ -0,0 +1,21 @@ + + + + +
+ + diff --git a/tests/integration/cases/types/child-type-import/__output__/vue/Field.vue b/tests/integration/cases/types/child-type-import/__output__/vue/Field.vue new file mode 100644 index 000000000..4465315a8 --- /dev/null +++ b/tests/integration/cases/types/child-type-import/__output__/vue/Field.vue @@ -0,0 +1,12 @@ + + + diff --git a/tests/integration/cases/types/child-type-import/__output__/vue/Form.vue b/tests/integration/cases/types/child-type-import/__output__/vue/Form.vue new file mode 100644 index 000000000..d843fae9e --- /dev/null +++ b/tests/integration/cases/types/child-type-import/__output__/vue/Form.vue @@ -0,0 +1,11 @@ + + + diff --git a/tests/integration/cases/types/child-type-import/__screenshots__/email-chromium-linux.png b/tests/integration/cases/types/child-type-import/__screenshots__/email-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..270dcd943cc75133377188db721bc87a62a9e0fb GIT binary patch literal 789 zcmV+w1M2*VP)PlRR+p6xV zE99n0D#~4xy9+b>f9e)ym=pZ$dn~ZOSS$#D#-0%Xy&(gjJ7fTKhYW!3kO9ygG61?m z20(Yn0O$@G0Nw4A@i{v?BLJIoad9!QcXS!(0xH^MI0db@f$9MYh8*OuI+>K*^RD zITw-u?AVH)_~6)2dCDea8z_r%MgX4H1N(ulPV&7lE3+vxC&t9~XH~1DD3YKXQ`=9< z`!!TsrJ>85o*N{kPTo;u-Npo9|d+_Q7K`!Y?_x%U|9rU1Xit)&6}F@ z<}L%$_I&52izSEDvX;MyF`<(~YTgd1Nev~J-86NA#JSn5WFsohTltf;xcI|ZJgO&N zt5sGl&u3X)mi*iGyi3l}!JJ?UfbrP+8d;Nv{%P7+(~N)r@oTXVS#W$n07hfm&kL!x z7V_AS;=r9O$3ir70x%k@dScmj{lNXAr(4)^%=Om`3~F1($6}UxX2?uc#na?!PF@m# zu~;<^Twi`TxxByO`P(mb>XNmYeljb))AQFa&b!gktM}74Z6XN3SnTcOJ9AwHREw5S z%l>og=%4WHG;G}PNwu5z2MlUC6gPull<@2{=nfeG-5~>@yS)*9dmZo8!)6WNWf@}z z0Cowr5a { + it("passes the typed prop on to the child", async () => { + const view = await mountScenario(Form, "email"); + await view.expectParity("email"); + await expect + .element(view.getByRole("textbox", { name: "Email" })) + .toHaveAttribute("name", "email"); + }); +}); diff --git a/tests/integration/cases/types/exported-types/Button.uf.tsx b/tests/integration/cases/types/exported-types/Button.uf.tsx new file mode 100644 index 000000000..d23ae902a --- /dev/null +++ b/tests/integration/cases/types/exported-types/Button.uf.tsx @@ -0,0 +1,18 @@ +// Every type the module exports is exported by each output (ADR-0068): one the props reach, and +// one no component reaches. +export type ButtonTone = "primary" | "secondary"; + +export type ButtonSize = "small" | "large"; + +export interface ButtonProps { + label: string; + tone?: ButtonTone; +} + +export default function Button({ label, tone = "primary" }: ButtonProps) { + return ( + + ); +} diff --git a/tests/integration/cases/types/exported-types/__expected__/aria.secondary.yaml b/tests/integration/cases/types/exported-types/__expected__/aria.secondary.yaml new file mode 100644 index 000000000..43d43d54c --- /dev/null +++ b/tests/integration/cases/types/exported-types/__expected__/aria.secondary.yaml @@ -0,0 +1 @@ +- button "Cancel" diff --git a/tests/integration/cases/types/exported-types/__expected__/diagnostics.json b/tests/integration/cases/types/exported-types/__expected__/diagnostics.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/tests/integration/cases/types/exported-types/__expected__/diagnostics.json @@ -0,0 +1 @@ +[] diff --git a/tests/integration/cases/types/exported-types/__expected__/dom.secondary.html b/tests/integration/cases/types/exported-types/__expected__/dom.secondary.html new file mode 100644 index 000000000..79d72877b --- /dev/null +++ b/tests/integration/cases/types/exported-types/__expected__/dom.secondary.html @@ -0,0 +1,3 @@ + diff --git a/tests/integration/cases/types/exported-types/__expected__/geometry.secondary.json b/tests/integration/cases/types/exported-types/__expected__/geometry.secondary.json new file mode 100644 index 000000000..ec81d669b --- /dev/null +++ b/tests/integration/cases/types/exported-types/__expected__/geometry.secondary.json @@ -0,0 +1,93 @@ +{ + "version": 1, + "nodes": { + "button[0]": { + "box": [ + 8, + 10, + 59.71875, + 22 + ], + "style": { + "display": "inline-block", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "border-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "1px", + "padding-right": "6px", + "padding-bottom": "1px", + "padding-left": "6px", + "border-top-width": "2px", + "border-right-width": "2px", + "border-bottom-width": "2px", + "border-left-width": "2px", + "border-top-style": "outset", + "border-right-style": "outset", + "border-bottom-style": "outset", + "border-left-style": "outset", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgb(239, 239, 239)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "13.3333px", + "font-weight": "400", + "font-style": "normal", + "line-height": "normal", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "center", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "button[0]/#text[0]": { + "box": [ + [ + 16, + 13, + 43.71875, + 16 + ] + ] + } + } +} diff --git a/tests/integration/cases/types/exported-types/__expected__/ssr.secondary.html b/tests/integration/cases/types/exported-types/__expected__/ssr.secondary.html new file mode 100644 index 000000000..79d72877b --- /dev/null +++ b/tests/integration/cases/types/exported-types/__expected__/ssr.secondary.html @@ -0,0 +1,3 @@ + diff --git a/tests/integration/cases/types/exported-types/__output__/angular/button.ts b/tests/integration/cases/types/exported-types/__output__/angular/button.ts new file mode 100644 index 000000000..cdde86b31 --- /dev/null +++ b/tests/integration/cases/types/exported-types/__output__/angular/button.ts @@ -0,0 +1,67 @@ +import { + Component, + Directive, + ElementRef, + type OnInit, + Renderer2, + inject, + input, +} from "@angular/core"; + +export type ButtonTone = "primary" | "secondary"; + +export interface ButtonProps { + label: string; + tone?: ButtonTone; +} + +/** The scope attributes taken off each host, for the roots its component renders later. */ +const scopes = new WeakMap(); + +/** + * Gives a root node of its component the scope attributes the component's styled parents put on + * its host (ADR-0063), and takes them off the host, which keeps only `display: contents`. + */ +@Directive({ selector: "[ufScope]" }) +export class ScopeAttributes implements OnInit { + readonly ufScope = input.required(); + private readonly element = inject>(ElementRef); + private readonly renderer = inject(Renderer2); + + ngOnInit(): void { + const host = this.ufScope(); + let names = scopes.get(host); + if (!names) { + names = host.getAttributeNames().filter((name) => name.startsWith("data-uf-")); + for (const name of names) this.renderer.removeAttribute(host, name); + scopes.set(host, names); + } + for (const name of names) this.renderer.setAttribute(this.element.nativeElement, name, ""); + } +} + +@Component({ + selector: "uf-button", + imports: [ScopeAttributes], + host: { "[attr.class]": "null", "[attr.style]": "'display: contents;'" }, + preserveWhitespaces: false, + template: ` + @let label = this.label(); + @let tone = this.tone(); + + `, +}) +export default class Button { + readonly label = input.required(); + readonly tone = input("primary", { + transform: (value) => (value === undefined ? "primary" : value), + }); + readonly class = input(); + readonly style = input>(); + protected readonly host = inject>(ElementRef).nativeElement; +} diff --git a/tests/integration/cases/types/exported-types/__output__/astro/Button.astro b/tests/integration/cases/types/exported-types/__output__/astro/Button.astro new file mode 100644 index 000000000..cb8a8015c --- /dev/null +++ b/tests/integration/cases/types/exported-types/__output__/astro/Button.astro @@ -0,0 +1,29 @@ +--- +export type ButtonTone = "primary" | "secondary"; + +export interface ButtonProps { + label: string; + tone?: ButtonTone; +} + +interface Props extends ButtonProps { + class?: string; + style?: Record; +} + +const { label, tone = "primary", class: className, style } = Astro.props; + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} +--- + + diff --git a/tests/integration/cases/types/exported-types/__output__/ir.json b/tests/integration/cases/types/exported-types/__output__/ir.json new file mode 100644 index 000000000..120a373fa --- /dev/null +++ b/tests/integration/cases/types/exported-types/__output__/ir.json @@ -0,0 +1,214 @@ +{ + "irVersion": 1, + "file": "types/exported-types/Button.uf.tsx", + "components": [ + { + "name": "Button", + "span": { + "start": 294, + "end": 473 + }, + "props": [ + { + "name": "label", + "optional": false, + "type": { + "code": "string", + "span": { + "start": 262, + "end": 268 + } + }, + "binding": "label@327", + "span": { + "start": 255, + "end": 269 + } + }, + { + "name": "tone", + "optional": true, + "type": { + "code": "ButtonTone", + "span": { + "start": 279, + "end": 289 + } + }, + "default": { + "code": "\"primary\"", + "span": { + "start": 341, + "end": 350 + }, + "refs": [] + }, + "binding": "tone@334", + "span": { + "start": 272, + "end": 290 + } + } + ], + "propsParameter": { + "form": "destructured", + "type": { + "code": "ButtonProps", + "span": { + "start": 354, + "end": 365 + } + }, + "span": { + "start": 325, + "end": 365 + } + }, + "types": [ + "ButtonTone", + "ButtonProps" + ], + "bindings": [ + { + "id": "label@327", + "name": "label", + "kind": "prop", + "span": { + "start": 327, + "end": 332 + } + }, + { + "id": "tone@334", + "name": "tone", + "kind": "prop", + "span": { + "start": 334, + "end": 338 + } + } + ], + "setup": [], + "render": { + "kind": "Element", + "tag": "button", + "attributes": [ + { + "kind": "Static", + "name": "type", + "value": "button", + "span": { + "start": 392, + "end": 405 + } + }, + { + "kind": "Class", + "items": [ + { + "kind": "Dynamic", + "value": { + "code": "`button button-${tone}`", + "span": { + "start": 413, + "end": 436 + }, + "refs": [ + { + "kind": "Binding", + "binding": "tone@334", + "span": { + "start": 430, + "end": 434 + } + } + ] + }, + "span": { + "start": 413, + "end": 436 + } + } + ], + "span": { + "start": 406, + "end": 437 + } + } + ], + "children": [ + { + "kind": "Interpolation", + "value": { + "code": "label", + "span": { + "start": 446, + "end": 451 + }, + "refs": [ + { + "kind": "Binding", + "binding": "label@327", + "span": { + "start": 446, + "end": 451 + } + } + ] + }, + "span": { + "start": 445, + "end": 452 + } + } + ], + "span": { + "start": 384, + "end": 466 + } + } + } + ], + "exports": [ + { + "kind": "default", + "name": "default", + "local": "Button", + "span": { + "start": 294, + "end": 473 + } + } + ], + "types": [ + { + "name": "ButtonTone", + "exported": true, + "code": "type ButtonTone = \"primary\" | \"secondary\";", + "span": { + "start": 133, + "end": 175 + } + }, + { + "name": "ButtonProps", + "exported": true, + "code": "interface ButtonProps {\n label: string;\n tone?: ButtonTone;\n}", + "span": { + "start": 229, + "end": 292 + } + } + ], + "typeExports": [ + { + "kind": "Declaration", + "name": "ButtonSize", + "code": "type ButtonSize = \"small\" | \"large\";", + "span": { + "start": 184, + "end": 220 + } + } + ] +} diff --git a/tests/integration/cases/types/exported-types/__output__/qwik/Button.tsx b/tests/integration/cases/types/exported-types/__output__/qwik/Button.tsx new file mode 100644 index 000000000..ddc0cd4f4 --- /dev/null +++ b/tests/integration/cases/types/exported-types/__output__/qwik/Button.tsx @@ -0,0 +1,32 @@ +import { type CSSProperties, type ClassList, component$ } from "@qwik.dev/core"; + +export type ButtonTone = "primary" | "secondary"; + +export interface ButtonProps { + label: string; + tone?: ButtonTone; +} + +export default component$( + ({ label, tone = "primary", class: className, style, ...attributes }) => { + return ( + + ); + }, +); + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/types/exported-types/__output__/react/Button.tsx b/tests/integration/cases/types/exported-types/__output__/react/Button.tsx new file mode 100644 index 000000000..b1bfbe821 --- /dev/null +++ b/tests/integration/cases/types/exported-types/__output__/react/Button.tsx @@ -0,0 +1,56 @@ +import type { CSSProperties } from "react"; + +export type ButtonTone = "primary" | "secondary"; + +export interface ButtonProps { + label: string; + tone?: ButtonTone; +} + +export interface ButtonAttributes { + className?: string; + style?: CSSProperties; +} + +export default function Button({ + label, + tone = "primary", + className, + style, + ...attributes +}: ButtonProps & ButtonAttributes) { + return ( + + ); +} + +/** Joins class names, and the keys of an object's truthy entries, into one `className`. */ +function cx(...parts: unknown[]): string { + const names: string[] = []; + for (const part of parts) { + if (typeof part === "string") { + if (part) names.push(part); + } else if (part && typeof part === "object") { + for (const [key, on] of Object.entries(part)) { + if (on) names.push(key); + } + } + } + return names.join(" "); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/types/exported-types/__output__/solid/Button.tsx b/tests/integration/cases/types/exported-types/__output__/solid/Button.tsx new file mode 100644 index 000000000..3be1f3155 --- /dev/null +++ b/tests/integration/cases/types/exported-types/__output__/solid/Button.tsx @@ -0,0 +1,43 @@ +import { type JSX, mergeProps } from "solid-js"; + +export type ButtonTone = "primary" | "secondary"; + +export interface ButtonProps { + label: string; + tone?: ButtonTone; +} + +export default function Button( + rawProps: ButtonProps & { class?: string; style?: JSX.CSSProperties }, +) { + const props = mergeProps({ tone: "primary" } satisfies Partial, rawProps); + return ( + + ); +} + +/** Joins class names: strings as they are, and the names of an object's truthy entries. */ +function cx(...parts: unknown[]): string { + const names: string[] = []; + for (const part of parts) { + if (typeof part === "string") names.push(part); + else if (part && typeof part === "object") { + for (const [name, on] of Object.entries(part)) if (on) names.push(name); + } + } + return names.join(" "); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} diff --git a/tests/integration/cases/types/exported-types/__output__/svelte/Button.svelte b/tests/integration/cases/types/exported-types/__output__/svelte/Button.svelte new file mode 100644 index 000000000..deaa9c24f --- /dev/null +++ b/tests/integration/cases/types/exported-types/__output__/svelte/Button.svelte @@ -0,0 +1,30 @@ + + + + + diff --git a/tests/integration/cases/types/exported-types/__output__/vue/Button.vue b/tests/integration/cases/types/exported-types/__output__/vue/Button.vue new file mode 100644 index 000000000..aadd181ec --- /dev/null +++ b/tests/integration/cases/types/exported-types/__output__/vue/Button.vue @@ -0,0 +1,16 @@ + + + diff --git a/tests/integration/cases/types/exported-types/__screenshots__/secondary-chromium-linux.png b/tests/integration/cases/types/exported-types/__screenshots__/secondary-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..ef09c9266910ce11eb7df5e4c80042f0bf2544fc GIT binary patch literal 1107 zcmV-Z1g!gsP)rOIXM{$g}{A4)3mFrD>t34=}KdGUldZPsnlS%S=V&^`m?l$0S1qsu3ljRj0V=V zs`l^cDI!pqonKgz{Tu^6F9cQ7IV7hx0~{ERQ6foDZEbEYwtEU@78Zju_$VEI68^i1 z!Dj}eR{a#&1Psdvku;BT=_5XMSdL5G;Qbz5s;xauPqC5w1b zZMVOaZ&?^y*akOl2!RYnAft+bkm%z{xFTBHv?hM;a@8lOHo ziC#%0ay7%|cnSo4UE%Z57u%*`x3CX#uxHxFmSweS#eD1bZiZ$vxC29rWcYy0=M|;C zR0!Rc3DI}L;fho`d`^^No!vN8qfXGwYHY4JXer3jN<`FOuq%g{2t%Ip(P4HrQc~mM z2RnQn3({!YvO6-^4(#HBB&)8pVq;bp`rP$^zWy-lQl{o$|f z^1&CSRp(I*M#c4~*4EZmGMOBFGi`{53HY8qMw~WDpyU@nwUDq$m*K?T>U6p*vSDxg zbRnC~=JWZ!Q=@L{y3ls-=RX={`i>4 { + it("renders with its exported props type", async () => { + const view = await mountScenario(Button, "secondary"); + await view.expectParity("secondary"); + await expect + .element(view.getByRole("button", { name: "Cancel" })) + .toHaveClass("button button-secondary"); + }); +}); diff --git a/tests/integration/cases/types/extends-chain/Button.uf.tsx b/tests/integration/cases/types/extends-chain/Button.uf.tsx new file mode 100644 index 000000000..5c1a2e82c --- /dev/null +++ b/tests/integration/cases/types/extends-chain/Button.uf.tsx @@ -0,0 +1,9 @@ +import type { ButtonProps } from "./types"; + +export default function Button({ id, label, disabled = false }: ButtonProps) { + return ( + + ); +} diff --git a/tests/integration/cases/types/extends-chain/__expected__/aria.disabled.yaml b/tests/integration/cases/types/extends-chain/__expected__/aria.disabled.yaml new file mode 100644 index 000000000..afe2f66a6 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__expected__/aria.disabled.yaml @@ -0,0 +1 @@ +- button "Delete" [disabled] diff --git a/tests/integration/cases/types/extends-chain/__expected__/aria.enabled.yaml b/tests/integration/cases/types/extends-chain/__expected__/aria.enabled.yaml new file mode 100644 index 000000000..cdd6d5c45 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__expected__/aria.enabled.yaml @@ -0,0 +1 @@ +- button "Save" diff --git a/tests/integration/cases/types/extends-chain/__expected__/diagnostics.json b/tests/integration/cases/types/extends-chain/__expected__/diagnostics.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__expected__/diagnostics.json @@ -0,0 +1 @@ +[] diff --git a/tests/integration/cases/types/extends-chain/__expected__/dom.disabled.html b/tests/integration/cases/types/extends-chain/__expected__/dom.disabled.html new file mode 100644 index 000000000..b5f9b7c3c --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__expected__/dom.disabled.html @@ -0,0 +1,3 @@ + diff --git a/tests/integration/cases/types/extends-chain/__expected__/dom.enabled.html b/tests/integration/cases/types/extends-chain/__expected__/dom.enabled.html new file mode 100644 index 000000000..540d4ce57 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__expected__/dom.enabled.html @@ -0,0 +1,3 @@ + diff --git a/tests/integration/cases/types/extends-chain/__expected__/geometry.disabled.json b/tests/integration/cases/types/extends-chain/__expected__/geometry.disabled.json new file mode 100644 index 000000000..9c70335be --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__expected__/geometry.disabled.json @@ -0,0 +1,93 @@ +{ + "version": 1, + "nodes": { + "button[0]": { + "box": [ + 8, + 10, + 56.390625, + 22 + ], + "style": { + "display": "inline-block", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "border-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "1px", + "padding-right": "6px", + "padding-bottom": "1px", + "padding-left": "6px", + "border-top-width": "2px", + "border-right-width": "2px", + "border-bottom-width": "2px", + "border-left-width": "2px", + "border-top-style": "outset", + "border-right-style": "outset", + "border-bottom-style": "outset", + "border-left-style": "outset", + "border-top-color": "rgba(118, 118, 118, 0.3)", + "border-right-color": "rgba(118, 118, 118, 0.3)", + "border-bottom-color": "rgba(118, 118, 118, 0.3)", + "border-left-color": "rgba(118, 118, 118, 0.3)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgba(16, 16, 16, 0.3)", + "background-color": "rgba(239, 239, 239, 0.3)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "13.3333px", + "font-weight": "400", + "font-style": "normal", + "line-height": "normal", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "center", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgba(16, 16, 16, 0.3)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "button[0]/#text[0]": { + "box": [ + [ + 16, + 13, + 40.390625, + 16 + ] + ] + } + } +} diff --git a/tests/integration/cases/types/extends-chain/__expected__/geometry.enabled.json b/tests/integration/cases/types/extends-chain/__expected__/geometry.enabled.json new file mode 100644 index 000000000..6a184b7d9 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__expected__/geometry.enabled.json @@ -0,0 +1,93 @@ +{ + "version": 1, + "nodes": { + "button[0]": { + "box": [ + 8, + 10, + 46.84375, + 22 + ], + "style": { + "display": "inline-block", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "border-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "1px", + "padding-right": "6px", + "padding-bottom": "1px", + "padding-left": "6px", + "border-top-width": "2px", + "border-right-width": "2px", + "border-bottom-width": "2px", + "border-left-width": "2px", + "border-top-style": "outset", + "border-right-style": "outset", + "border-bottom-style": "outset", + "border-left-style": "outset", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgb(239, 239, 239)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "13.3333px", + "font-weight": "400", + "font-style": "normal", + "line-height": "normal", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "center", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "button[0]/#text[0]": { + "box": [ + [ + 16, + 13, + 30.84375, + 16 + ] + ] + } + } +} diff --git a/tests/integration/cases/types/extends-chain/__expected__/ssr.disabled.html b/tests/integration/cases/types/extends-chain/__expected__/ssr.disabled.html new file mode 100644 index 000000000..b5f9b7c3c --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__expected__/ssr.disabled.html @@ -0,0 +1,3 @@ + diff --git a/tests/integration/cases/types/extends-chain/__expected__/ssr.enabled.html b/tests/integration/cases/types/extends-chain/__expected__/ssr.enabled.html new file mode 100644 index 000000000..540d4ce57 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__expected__/ssr.enabled.html @@ -0,0 +1,3 @@ + diff --git a/tests/integration/cases/types/extends-chain/__output__/angular/base.ts b/tests/integration/cases/types/extends-chain/__output__/angular/base.ts new file mode 100644 index 000000000..8589f6896 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__output__/angular/base.ts @@ -0,0 +1,4 @@ +// The root of the chain, two modules away from the component. +export interface Identified { + id: string; +} diff --git a/tests/integration/cases/types/extends-chain/__output__/angular/button.ts b/tests/integration/cases/types/extends-chain/__output__/angular/button.ts new file mode 100644 index 000000000..06668a999 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__output__/angular/button.ts @@ -0,0 +1,64 @@ +import { + Component, + Directive, + ElementRef, + type OnInit, + Renderer2, + inject, + input, +} from "@angular/core"; + +/** The scope attributes taken off each host, for the roots its component renders later. */ +const scopes = new WeakMap(); + +/** + * Gives a root node of its component the scope attributes the component's styled parents put on + * its host (ADR-0063), and takes them off the host, which keeps only `display: contents`. + */ +@Directive({ selector: "[ufScope]" }) +export class ScopeAttributes implements OnInit { + readonly ufScope = input.required(); + private readonly element = inject>(ElementRef); + private readonly renderer = inject(Renderer2); + + ngOnInit(): void { + const host = this.ufScope(); + let names = scopes.get(host); + if (!names) { + names = host.getAttributeNames().filter((name) => name.startsWith("data-uf-")); + for (const name of names) this.renderer.removeAttribute(host, name); + scopes.set(host, names); + } + for (const name of names) this.renderer.setAttribute(this.element.nativeElement, name, ""); + } +} + +@Component({ + selector: "uf-button", + imports: [ScopeAttributes], + host: { "[attr.class]": "null", "[attr.style]": "'display: contents;'" }, + preserveWhitespaces: false, + template: ` + @let disabled = this.disabled(); + @let label = this.label(); + @let id = this.id(); + + `, +}) +export default class Button { + readonly disabled = input(false, { + transform: (value) => (value === undefined ? false : value), + }); + readonly label = input.required(); + readonly id = input.required(); + readonly class = input(); + readonly style = input>(); + protected readonly host = inject>(ElementRef).nativeElement; +} diff --git a/tests/integration/cases/types/extends-chain/__output__/angular/types.ts b/tests/integration/cases/types/extends-chain/__output__/angular/types.ts new file mode 100644 index 000000000..f6e125b61 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__output__/angular/types.ts @@ -0,0 +1,9 @@ +import type { Identified } from "./base"; + +export interface Labelled extends Identified { + label: string; +} + +export interface ButtonProps extends Labelled { + disabled?: boolean; +} diff --git a/tests/integration/cases/types/extends-chain/__output__/astro/Button.astro b/tests/integration/cases/types/extends-chain/__output__/astro/Button.astro new file mode 100644 index 000000000..fd211de47 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__output__/astro/Button.astro @@ -0,0 +1,24 @@ +--- +interface Props extends ButtonProps { + class?: string; + style?: Record; +} + +const { id, label, disabled = false, class: className, style } = Astro.props; + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} +--- + + diff --git a/tests/integration/cases/types/extends-chain/__output__/astro/base.ts b/tests/integration/cases/types/extends-chain/__output__/astro/base.ts new file mode 100644 index 000000000..8589f6896 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__output__/astro/base.ts @@ -0,0 +1,4 @@ +// The root of the chain, two modules away from the component. +export interface Identified { + id: string; +} diff --git a/tests/integration/cases/types/extends-chain/__output__/astro/types.ts b/tests/integration/cases/types/extends-chain/__output__/astro/types.ts new file mode 100644 index 000000000..f6e125b61 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__output__/astro/types.ts @@ -0,0 +1,9 @@ +import type { Identified } from "./base"; + +export interface Labelled extends Identified { + label: string; +} + +export interface ButtonProps extends Labelled { + disabled?: boolean; +} diff --git a/tests/integration/cases/types/extends-chain/__output__/ir.json b/tests/integration/cases/types/extends-chain/__output__/ir.json new file mode 100644 index 000000000..db5eb925e --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__output__/ir.json @@ -0,0 +1,266 @@ +{ + "irVersion": 1, + "file": "types/extends-chain/Button.uf.tsx", + "components": [ + { + "name": "Button", + "span": { + "start": 45, + "end": 224 + }, + "props": [ + { + "name": "disabled", + "optional": true, + "type": { + "code": "boolean", + "span": { + "start": 109, + "end": 120 + } + }, + "default": { + "code": "false", + "span": { + "start": 100, + "end": 105 + }, + "refs": [] + }, + "binding": "disabled@89", + "declaredBy": { + "file": "types/extends-chain/types.ts", + "declaration": "ButtonProps", + "span": { + "start": 160, + "end": 179 + } + }, + "span": { + "start": 109, + "end": 120 + } + }, + { + "name": "label", + "optional": false, + "type": { + "code": "string", + "span": { + "start": 109, + "end": 120 + } + }, + "binding": "label@82", + "declaredBy": { + "file": "types/extends-chain/types.ts", + "declaration": "Labelled", + "span": { + "start": 92, + "end": 106 + } + }, + "span": { + "start": 109, + "end": 120 + } + }, + { + "name": "id", + "optional": false, + "type": { + "code": "string", + "span": { + "start": 109, + "end": 120 + } + }, + "binding": "id@78", + "declaredBy": { + "file": "types/extends-chain/base.ts", + "declaration": "Identified", + "span": { + "start": 95, + "end": 106 + } + }, + "span": { + "start": 109, + "end": 120 + } + } + ], + "propsParameter": { + "form": "destructured", + "type": { + "code": "ButtonProps", + "span": { + "start": 109, + "end": 120 + } + }, + "span": { + "start": 76, + "end": 120 + } + }, + "types": [], + "bindings": [ + { + "id": "id@78", + "name": "id", + "kind": "prop", + "span": { + "start": 78, + "end": 80 + } + }, + { + "id": "label@82", + "name": "label", + "kind": "prop", + "span": { + "start": 82, + "end": 87 + } + }, + { + "id": "disabled@89", + "name": "disabled", + "kind": "prop", + "span": { + "start": 89, + "end": 97 + } + } + ], + "setup": [], + "render": { + "kind": "Element", + "tag": "button", + "attributes": [ + { + "kind": "Bound", + "name": "id", + "value": { + "code": "id", + "span": { + "start": 151, + "end": 153 + }, + "refs": [ + { + "kind": "Binding", + "binding": "id@78", + "span": { + "start": 151, + "end": 153 + } + } + ] + }, + "span": { + "start": 147, + "end": 154 + } + }, + { + "kind": "Static", + "name": "type", + "value": "button", + "span": { + "start": 155, + "end": 168 + } + }, + { + "kind": "Bound", + "name": "disabled", + "value": { + "code": "disabled", + "span": { + "start": 179, + "end": 187 + }, + "refs": [ + { + "kind": "Binding", + "binding": "disabled@89", + "span": { + "start": 179, + "end": 187 + } + } + ] + }, + "span": { + "start": 169, + "end": 188 + } + } + ], + "children": [ + { + "kind": "Interpolation", + "value": { + "code": "label", + "span": { + "start": 197, + "end": 202 + }, + "refs": [ + { + "kind": "Binding", + "binding": "label@82", + "span": { + "start": 197, + "end": 202 + } + } + ] + }, + "span": { + "start": 196, + "end": 203 + } + } + ], + "span": { + "start": 139, + "end": 217 + } + } + } + ], + "exports": [ + { + "kind": "default", + "name": "default", + "local": "Button", + "span": { + "start": 45, + "end": 224 + } + } + ], + "types": [], + "typeImports": [ + { + "specifier": "./types", + "names": [ + { + "imported": "ButtonProps", + "local": "ButtonProps", + "span": { + "start": 14, + "end": 25 + } + } + ], + "span": { + "start": 0, + "end": 43 + } + } + ] +} diff --git a/tests/integration/cases/types/extends-chain/__output__/qwik/Button.tsx b/tests/integration/cases/types/extends-chain/__output__/qwik/Button.tsx new file mode 100644 index 000000000..422b659e5 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__output__/qwik/Button.tsx @@ -0,0 +1,27 @@ +import { type CSSProperties, type ClassList, component$ } from "@qwik.dev/core"; + +export default component$( + ({ id, label, disabled = false, class: className, style, ...attributes }) => { + return ( + + ); + }, +); + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/types/extends-chain/__output__/qwik/base.ts b/tests/integration/cases/types/extends-chain/__output__/qwik/base.ts new file mode 100644 index 000000000..8589f6896 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__output__/qwik/base.ts @@ -0,0 +1,4 @@ +// The root of the chain, two modules away from the component. +export interface Identified { + id: string; +} diff --git a/tests/integration/cases/types/extends-chain/__output__/qwik/types.ts b/tests/integration/cases/types/extends-chain/__output__/qwik/types.ts new file mode 100644 index 000000000..f6e125b61 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__output__/qwik/types.ts @@ -0,0 +1,9 @@ +import type { Identified } from "./base"; + +export interface Labelled extends Identified { + label: string; +} + +export interface ButtonProps extends Labelled { + disabled?: boolean; +} diff --git a/tests/integration/cases/types/extends-chain/__output__/react/Button.tsx b/tests/integration/cases/types/extends-chain/__output__/react/Button.tsx new file mode 100644 index 000000000..1bf2ecf9f --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__output__/react/Button.tsx @@ -0,0 +1,37 @@ +import type { CSSProperties } from "react"; + +export interface ButtonAttributes { + className?: string; + style?: CSSProperties; +} + +export default function Button({ + id, + label, + disabled = false, + className, + style, + ...attributes +}: ButtonProps & ButtonAttributes) { + return ( + + ); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/types/extends-chain/__output__/react/base.ts b/tests/integration/cases/types/extends-chain/__output__/react/base.ts new file mode 100644 index 000000000..8589f6896 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__output__/react/base.ts @@ -0,0 +1,4 @@ +// The root of the chain, two modules away from the component. +export interface Identified { + id: string; +} diff --git a/tests/integration/cases/types/extends-chain/__output__/react/types.ts b/tests/integration/cases/types/extends-chain/__output__/react/types.ts new file mode 100644 index 000000000..f6e125b61 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__output__/react/types.ts @@ -0,0 +1,9 @@ +import type { Identified } from "./base"; + +export interface Labelled extends Identified { + label: string; +} + +export interface ButtonProps extends Labelled { + disabled?: boolean; +} diff --git a/tests/integration/cases/types/extends-chain/__output__/solid/Button.tsx b/tests/integration/cases/types/extends-chain/__output__/solid/Button.tsx new file mode 100644 index 000000000..1137b7139 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__output__/solid/Button.tsx @@ -0,0 +1,26 @@ +import { type JSX, mergeProps } from "solid-js"; + +export default function Button( + rawProps: ButtonProps & { class?: string; style?: JSX.CSSProperties }, +) { + const props = mergeProps({ disabled: false } satisfies Partial, rawProps); + return ( + + ); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} diff --git a/tests/integration/cases/types/extends-chain/__output__/solid/base.ts b/tests/integration/cases/types/extends-chain/__output__/solid/base.ts new file mode 100644 index 000000000..8589f6896 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__output__/solid/base.ts @@ -0,0 +1,4 @@ +// The root of the chain, two modules away from the component. +export interface Identified { + id: string; +} diff --git a/tests/integration/cases/types/extends-chain/__output__/solid/types.ts b/tests/integration/cases/types/extends-chain/__output__/solid/types.ts new file mode 100644 index 000000000..f6e125b61 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__output__/solid/types.ts @@ -0,0 +1,9 @@ +import type { Identified } from "./base"; + +export interface Labelled extends Identified { + label: string; +} + +export interface ButtonProps extends Labelled { + disabled?: boolean; +} diff --git a/tests/integration/cases/types/extends-chain/__output__/svelte/Button.svelte b/tests/integration/cases/types/extends-chain/__output__/svelte/Button.svelte new file mode 100644 index 000000000..44036258c --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__output__/svelte/Button.svelte @@ -0,0 +1,25 @@ + + + + + diff --git a/tests/integration/cases/types/extends-chain/__output__/svelte/base.ts b/tests/integration/cases/types/extends-chain/__output__/svelte/base.ts new file mode 100644 index 000000000..8589f6896 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__output__/svelte/base.ts @@ -0,0 +1,4 @@ +// The root of the chain, two modules away from the component. +export interface Identified { + id: string; +} diff --git a/tests/integration/cases/types/extends-chain/__output__/svelte/types.ts b/tests/integration/cases/types/extends-chain/__output__/svelte/types.ts new file mode 100644 index 000000000..f6e125b61 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__output__/svelte/types.ts @@ -0,0 +1,9 @@ +import type { Identified } from "./base"; + +export interface Labelled extends Identified { + label: string; +} + +export interface ButtonProps extends Labelled { + disabled?: boolean; +} diff --git a/tests/integration/cases/types/extends-chain/__output__/vue/Button.vue b/tests/integration/cases/types/extends-chain/__output__/vue/Button.vue new file mode 100644 index 000000000..80eb7e79e --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__output__/vue/Button.vue @@ -0,0 +1,9 @@ + + + diff --git a/tests/integration/cases/types/extends-chain/__output__/vue/base.ts b/tests/integration/cases/types/extends-chain/__output__/vue/base.ts new file mode 100644 index 000000000..8589f6896 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__output__/vue/base.ts @@ -0,0 +1,4 @@ +// The root of the chain, two modules away from the component. +export interface Identified { + id: string; +} diff --git a/tests/integration/cases/types/extends-chain/__output__/vue/types.ts b/tests/integration/cases/types/extends-chain/__output__/vue/types.ts new file mode 100644 index 000000000..f6e125b61 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/__output__/vue/types.ts @@ -0,0 +1,9 @@ +import type { Identified } from "./base"; + +export interface Labelled extends Identified { + label: string; +} + +export interface ButtonProps extends Labelled { + disabled?: boolean; +} diff --git a/tests/integration/cases/types/extends-chain/__screenshots__/disabled-chromium-linux.png b/tests/integration/cases/types/extends-chain/__screenshots__/disabled-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..d57e50907a29b54a8639c3b9f94b30feb133a1b3 GIT binary patch literal 629 zcmV-*0*d{KP)o zf%0PJd7d2-=C|7oM#?W3FO#=nGI<*&leb|qc^fA4z_G*Oa6X@}*DLgo*=)94E;pM^ zK=b1J&PB#9m&XV$ z8GL)q7#CS4IE$X6HHtjr@Qt+S8#?*}#w!4vQRPlqjGp8yhD28FO6{OaTJ#O7c4bnE zzs+){R7$f#K6Hh=AT^bF8`>1mY#|^x>)1mGqJxBK=dB6Fk{NBB%z2$Yr75xu5Rr+z z$VIcXj|8qdo!l8i{|YIhgM6De$|rrarJ!=4b>&3n^G6YKj%NK|g4oj*8WkjJQs~5+ zH5Qnx3Sd<}^Tw{yBq~VMr0~T|Qhw+fpd8EU)y{avXtkJof}Ad&_I1C2)%ylQ!XT-% z>l-??d;MQ7&GCI7qdiSJrOQ`czokQYbN0Ogo=INPs9Y9B78xWF?>)|bzhAG{&=y)U zR8`>06cwasj>qG6yY(IIW-oaUU2?xDNYn)Qk>8c+rX6}uxJYl`=xGGqv_p@f@~Fsw z;3e=I)o7mujezNy!Z4Y<4U@^+Fd4JAKL7v#|Nj8&1GfMG00v1!K~w_(R_|%21~*D5 P00000NkvXXu0mjfvfCdS literal 0 HcmV?d00001 diff --git a/tests/integration/cases/types/extends-chain/__screenshots__/enabled-chromium-linux.png b/tests/integration/cases/types/extends-chain/__screenshots__/enabled-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..daf5ca880c3d0ccff2aca3688c8db44932a1f9d5 GIT binary patch literal 983 zcmV;|11S87P)tt^?Ae@Pw1GH6JM|GsiO_kqvzR%YGKL79YeV(|yyaXf5 zKVXy@s}GN{`tTU550A0>@R%4dx4F4_etv#-b_POXc6N4ZYHD$D(J$bZ-${Wxk>!XS9=DQ;;*UfbVDifoir6H29|VT;N! zFaMNai_jhyK{3f2y98qzK}g`Q?sjdA9f**4HSUMIFp8v`fZwbha2)6nVpAY`@^~>( z-AY_ZG1oKM8irAc>ycAi%(yKe~Jd)eMQ+Aq$7<7VN(%=G`6b<=je z>+#%bPUaolH4URfD8mIX_E>>fGnjmr7#bGhDuNX2rY%Hya%?hAxjTp>?XLHbU<_At z0572A@{8oMw}%_qY@s6UZ74mx@}#nTOogZz5hLK*$PlTjkk#5Gp|mwie7MT$6Qz#h zbX(6W2RCjP5Q4#-A6Pwm``bD}>GZWrA(H2XMw?I@x9U20ymPYb%}}$jP`I)Jj}ofb z(sk2qJCd4+GLbzPiA3c5*7a@}n!wKC0qrzO<+{;-&(zXum8K;qcUci}-5w7i=PeQM zHuTdQBQUF_qmI_p9Hhqk#Rj5;43F#e-Tt<5aU*p<)4+j^GlcI&Wo=_cG@t%=dwP0$ zW-hx6lCNInVmw4kxx3c0xtV|O-CrnmAc}r*0x!xcaym@Ba$PB*yz-(bbqjMd>>;-w zG~rS{je(57E7hlpj#=iY)+7bZ|j_ETlkdzdj0m|Nl*OxJ@^ULYPD9Y{d8)` zUF!iw>4yT%3sOt~zfREj@AykkX{;*)e+C1T_~xE9l&mWQVZ$^qC4RHF2K)gZ;jVln z7 { + it("renders every member of the chain", async () => { + const view = await mountScenario(Button, "enabled"); + await view.expectParity("enabled"); + await expect.element(view.getByRole("button", { name: "Save" })).toHaveAttribute("id", "save"); + await expect.element(view.getByRole("button", { name: "Save" })).toBeEnabled(); + }); + + it("renders the inherited props with its own", async () => { + const view = await mountScenario(Button, "disabled"); + await view.expectParity("disabled"); + await expect.element(view.getByRole("button", { name: "Delete" })).toBeDisabled(); + }); +}); diff --git a/tests/integration/cases/types/extends-chain/types.ts b/tests/integration/cases/types/extends-chain/types.ts new file mode 100644 index 000000000..f6e125b61 --- /dev/null +++ b/tests/integration/cases/types/extends-chain/types.ts @@ -0,0 +1,9 @@ +import type { Identified } from "./base"; + +export interface Labelled extends Identified { + label: string; +} + +export interface ButtonProps extends Labelled { + disabled?: boolean; +} diff --git a/tests/integration/cases/types/imported-interface/Badge.uf.tsx b/tests/integration/cases/types/imported-interface/Badge.uf.tsx new file mode 100644 index 000000000..0c4a10dee --- /dev/null +++ b/tests/integration/cases/types/imported-interface/Badge.uf.tsx @@ -0,0 +1,5 @@ +import type { BadgeProps } from "./types"; + +export default function Badge({ label, tone = "info" }: BadgeProps) { + return {label}; +} diff --git a/tests/integration/cases/types/imported-interface/__expected__/aria.danger.yaml b/tests/integration/cases/types/imported-interface/__expected__/aria.danger.yaml new file mode 100644 index 000000000..6e926934e --- /dev/null +++ b/tests/integration/cases/types/imported-interface/__expected__/aria.danger.yaml @@ -0,0 +1 @@ +- text: Overdue diff --git a/tests/integration/cases/types/imported-interface/__expected__/aria.default-tone.yaml b/tests/integration/cases/types/imported-interface/__expected__/aria.default-tone.yaml new file mode 100644 index 000000000..52818f623 --- /dev/null +++ b/tests/integration/cases/types/imported-interface/__expected__/aria.default-tone.yaml @@ -0,0 +1 @@ +- text: New diff --git a/tests/integration/cases/types/imported-interface/__expected__/diagnostics.json b/tests/integration/cases/types/imported-interface/__expected__/diagnostics.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/tests/integration/cases/types/imported-interface/__expected__/diagnostics.json @@ -0,0 +1 @@ +[] diff --git a/tests/integration/cases/types/imported-interface/__expected__/dom.danger.html b/tests/integration/cases/types/imported-interface/__expected__/dom.danger.html new file mode 100644 index 000000000..f8db5127a --- /dev/null +++ b/tests/integration/cases/types/imported-interface/__expected__/dom.danger.html @@ -0,0 +1,3 @@ + + "Overdue" + diff --git a/tests/integration/cases/types/imported-interface/__expected__/dom.default-tone.html b/tests/integration/cases/types/imported-interface/__expected__/dom.default-tone.html new file mode 100644 index 000000000..667537a5a --- /dev/null +++ b/tests/integration/cases/types/imported-interface/__expected__/dom.default-tone.html @@ -0,0 +1,3 @@ + + "New" + diff --git a/tests/integration/cases/types/imported-interface/__expected__/geometry.danger.json b/tests/integration/cases/types/imported-interface/__expected__/geometry.danger.json new file mode 100644 index 000000000..912d22b9c --- /dev/null +++ b/tests/integration/cases/types/imported-interface/__expected__/geometry.danger.json @@ -0,0 +1,93 @@ +{ + "version": 1, + "nodes": { + "span[0]": { + "box": [ + 8, + 10, + 64.59375, + 20 + ], + "style": { + "display": "inline", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "span[0]/#text[0]": { + "box": [ + [ + 8, + 10, + 64.59375, + 20 + ] + ] + } + } +} diff --git a/tests/integration/cases/types/imported-interface/__expected__/geometry.default-tone.json b/tests/integration/cases/types/imported-interface/__expected__/geometry.default-tone.json new file mode 100644 index 000000000..48dbbdaf3 --- /dev/null +++ b/tests/integration/cases/types/imported-interface/__expected__/geometry.default-tone.json @@ -0,0 +1,93 @@ +{ + "version": 1, + "nodes": { + "span[0]": { + "box": [ + 8, + 10, + 34.1875, + 20 + ], + "style": { + "display": "inline", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "span[0]/#text[0]": { + "box": [ + [ + 8, + 10, + 34.1875, + 20 + ] + ] + } + } +} diff --git a/tests/integration/cases/types/imported-interface/__expected__/ssr.danger.html b/tests/integration/cases/types/imported-interface/__expected__/ssr.danger.html new file mode 100644 index 000000000..f8db5127a --- /dev/null +++ b/tests/integration/cases/types/imported-interface/__expected__/ssr.danger.html @@ -0,0 +1,3 @@ + + "Overdue" + diff --git a/tests/integration/cases/types/imported-interface/__expected__/ssr.default-tone.html b/tests/integration/cases/types/imported-interface/__expected__/ssr.default-tone.html new file mode 100644 index 000000000..667537a5a --- /dev/null +++ b/tests/integration/cases/types/imported-interface/__expected__/ssr.default-tone.html @@ -0,0 +1,3 @@ + + "New" + diff --git a/tests/integration/cases/types/imported-interface/__output__/angular/badge.ts b/tests/integration/cases/types/imported-interface/__output__/angular/badge.ts new file mode 100644 index 000000000..77930b73e --- /dev/null +++ b/tests/integration/cases/types/imported-interface/__output__/angular/badge.ts @@ -0,0 +1,59 @@ +import { + Component, + Directive, + ElementRef, + type OnInit, + Renderer2, + inject, + input, +} from "@angular/core"; + +/** The scope attributes taken off each host, for the roots its component renders later. */ +const scopes = new WeakMap(); + +/** + * Gives a root node of its component the scope attributes the component's styled parents put on + * its host (ADR-0063), and takes them off the host, which keeps only `display: contents`. + */ +@Directive({ selector: "[ufScope]" }) +export class ScopeAttributes implements OnInit { + readonly ufScope = input.required(); + private readonly element = inject>(ElementRef); + private readonly renderer = inject(Renderer2); + + ngOnInit(): void { + const host = this.ufScope(); + let names = scopes.get(host); + if (!names) { + names = host.getAttributeNames().filter((name) => name.startsWith("data-uf-")); + for (const name of names) this.renderer.removeAttribute(host, name); + scopes.set(host, names); + } + for (const name of names) this.renderer.setAttribute(this.element.nativeElement, name, ""); + } +} + +@Component({ + selector: "uf-badge", + imports: [ScopeAttributes], + host: { "[attr.class]": "null", "[attr.style]": "'display: contents;'" }, + preserveWhitespaces: false, + template: ` + @let label = this.label(); + @let tone = this.tone(); + {{ label }} + `, +}) +export default class Badge { + readonly label = input.required(); + readonly tone = input<"info" | "danger", "info" | "danger" | undefined>("info", { + transform: (value) => (value === undefined ? "info" : value), + }); + readonly class = input(); + readonly style = input>(); + protected readonly host = inject>(ElementRef).nativeElement; +} diff --git a/tests/integration/cases/types/imported-interface/__output__/angular/types.ts b/tests/integration/cases/types/imported-interface/__output__/angular/types.ts new file mode 100644 index 000000000..87fc7b9bb --- /dev/null +++ b/tests/integration/cases/types/imported-interface/__output__/angular/types.ts @@ -0,0 +1,6 @@ +// The props of `Badge`, declared in a module of their own, as a design system declares them. +export interface BadgeProps { + /** The text the badge shows. */ + label: string; + tone?: "info" | "danger"; +} diff --git a/tests/integration/cases/types/imported-interface/__output__/astro/Badge.astro b/tests/integration/cases/types/imported-interface/__output__/astro/Badge.astro new file mode 100644 index 000000000..82464c610 --- /dev/null +++ b/tests/integration/cases/types/imported-interface/__output__/astro/Badge.astro @@ -0,0 +1,21 @@ +--- +interface Props extends BadgeProps { + class?: string; + style?: Record; +} + +const { label, tone = "info", class: className, style } = Astro.props; + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} +--- + +{label} diff --git a/tests/integration/cases/types/imported-interface/__output__/astro/types.ts b/tests/integration/cases/types/imported-interface/__output__/astro/types.ts new file mode 100644 index 000000000..87fc7b9bb --- /dev/null +++ b/tests/integration/cases/types/imported-interface/__output__/astro/types.ts @@ -0,0 +1,6 @@ +// The props of `Badge`, declared in a module of their own, as a design system declares them. +export interface BadgeProps { + /** The text the badge shows. */ + label: string; + tone?: "info" | "danger"; +} diff --git a/tests/integration/cases/types/imported-interface/__output__/ir.json b/tests/integration/cases/types/imported-interface/__output__/ir.json new file mode 100644 index 000000000..16db57d27 --- /dev/null +++ b/tests/integration/cases/types/imported-interface/__output__/ir.json @@ -0,0 +1,207 @@ +{ + "irVersion": 1, + "file": "types/imported-interface/Badge.uf.tsx", + "components": [ + { + "name": "Badge", + "span": { + "start": 44, + "end": 176 + }, + "props": [ + { + "name": "label", + "optional": false, + "type": { + "code": "string", + "span": { + "start": 100, + "end": 110 + } + }, + "binding": "label@76", + "declaredBy": { + "file": "types/imported-interface/types.ts", + "declaration": "BadgeProps", + "span": { + "start": 161, + "end": 175 + } + }, + "span": { + "start": 100, + "end": 110 + } + }, + { + "name": "tone", + "optional": true, + "type": { + "code": "\"info\" | \"danger\"", + "span": { + "start": 100, + "end": 110 + } + }, + "default": { + "code": "\"info\"", + "span": { + "start": 90, + "end": 96 + }, + "refs": [] + }, + "binding": "tone@83", + "declaredBy": { + "file": "types/imported-interface/types.ts", + "declaration": "BadgeProps", + "span": { + "start": 178, + "end": 203 + } + }, + "span": { + "start": 100, + "end": 110 + } + } + ], + "propsParameter": { + "form": "destructured", + "type": { + "code": "BadgeProps", + "span": { + "start": 100, + "end": 110 + } + }, + "span": { + "start": 74, + "end": 110 + } + }, + "types": [], + "bindings": [ + { + "id": "label@76", + "name": "label", + "kind": "prop", + "span": { + "start": 76, + "end": 81 + } + }, + { + "id": "tone@83", + "name": "tone", + "kind": "prop", + "span": { + "start": 83, + "end": 87 + } + } + ], + "setup": [], + "render": { + "kind": "Element", + "tag": "span", + "attributes": [ + { + "kind": "Class", + "items": [ + { + "kind": "Dynamic", + "value": { + "code": "`badge badge-${tone}`", + "span": { + "start": 136, + "end": 157 + }, + "refs": [ + { + "kind": "Binding", + "binding": "tone@83", + "span": { + "start": 151, + "end": 155 + } + } + ] + }, + "span": { + "start": 136, + "end": 157 + } + } + ], + "span": { + "start": 129, + "end": 158 + } + } + ], + "children": [ + { + "kind": "Interpolation", + "value": { + "code": "label", + "span": { + "start": 160, + "end": 165 + }, + "refs": [ + { + "kind": "Binding", + "binding": "label@76", + "span": { + "start": 160, + "end": 165 + } + } + ] + }, + "span": { + "start": 159, + "end": 166 + } + } + ], + "span": { + "start": 123, + "end": 173 + } + } + } + ], + "exports": [ + { + "kind": "default", + "name": "default", + "local": "Badge", + "span": { + "start": 44, + "end": 176 + } + } + ], + "types": [], + "typeImports": [ + { + "specifier": "./types", + "names": [ + { + "imported": "BadgeProps", + "local": "BadgeProps", + "span": { + "start": 14, + "end": 24 + } + } + ], + "span": { + "start": 0, + "end": 42 + } + } + ] +} diff --git a/tests/integration/cases/types/imported-interface/__output__/qwik/Badge.tsx b/tests/integration/cases/types/imported-interface/__output__/qwik/Badge.tsx new file mode 100644 index 000000000..f1f6359e9 --- /dev/null +++ b/tests/integration/cases/types/imported-interface/__output__/qwik/Badge.tsx @@ -0,0 +1,24 @@ +import { type CSSProperties, type ClassList, component$ } from "@qwik.dev/core"; + +export default component$( + ({ label, tone = "info", class: className, style, ...attributes }) => { + return ( + + {label} + + ); + }, +); + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/types/imported-interface/__output__/qwik/types.ts b/tests/integration/cases/types/imported-interface/__output__/qwik/types.ts new file mode 100644 index 000000000..87fc7b9bb --- /dev/null +++ b/tests/integration/cases/types/imported-interface/__output__/qwik/types.ts @@ -0,0 +1,6 @@ +// The props of `Badge`, declared in a module of their own, as a design system declares them. +export interface BadgeProps { + /** The text the badge shows. */ + label: string; + tone?: "info" | "danger"; +} diff --git a/tests/integration/cases/types/imported-interface/__output__/react/Badge.tsx b/tests/integration/cases/types/imported-interface/__output__/react/Badge.tsx new file mode 100644 index 000000000..999c33653 --- /dev/null +++ b/tests/integration/cases/types/imported-interface/__output__/react/Badge.tsx @@ -0,0 +1,48 @@ +import type { CSSProperties } from "react"; + +export interface BadgeAttributes { + className?: string; + style?: CSSProperties; +} + +export default function Badge({ + label, + tone = "info", + className, + style, + ...attributes +}: BadgeProps & BadgeAttributes) { + return ( + + {label} + + ); +} + +/** Joins class names, and the keys of an object's truthy entries, into one `className`. */ +function cx(...parts: unknown[]): string { + const names: string[] = []; + for (const part of parts) { + if (typeof part === "string") { + if (part) names.push(part); + } else if (part && typeof part === "object") { + for (const [key, on] of Object.entries(part)) { + if (on) names.push(key); + } + } + } + return names.join(" "); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/types/imported-interface/__output__/react/types.ts b/tests/integration/cases/types/imported-interface/__output__/react/types.ts new file mode 100644 index 000000000..87fc7b9bb --- /dev/null +++ b/tests/integration/cases/types/imported-interface/__output__/react/types.ts @@ -0,0 +1,6 @@ +// The props of `Badge`, declared in a module of their own, as a design system declares them. +export interface BadgeProps { + /** The text the badge shows. */ + label: string; + tone?: "info" | "danger"; +} diff --git a/tests/integration/cases/types/imported-interface/__output__/solid/Badge.tsx b/tests/integration/cases/types/imported-interface/__output__/solid/Badge.tsx new file mode 100644 index 000000000..2041257c3 --- /dev/null +++ b/tests/integration/cases/types/imported-interface/__output__/solid/Badge.tsx @@ -0,0 +1,35 @@ +import { type JSX, mergeProps } from "solid-js"; + +export default function Badge( + rawProps: BadgeProps & { class?: string; style?: JSX.CSSProperties }, +) { + const props = mergeProps({ tone: "info" } satisfies Partial, rawProps); + return ( + + {props.label} + + ); +} + +/** Joins class names: strings as they are, and the names of an object's truthy entries. */ +function cx(...parts: unknown[]): string { + const names: string[] = []; + for (const part of parts) { + if (typeof part === "string") names.push(part); + else if (part && typeof part === "object") { + for (const [name, on] of Object.entries(part)) if (on) names.push(name); + } + } + return names.join(" "); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} diff --git a/tests/integration/cases/types/imported-interface/__output__/solid/types.ts b/tests/integration/cases/types/imported-interface/__output__/solid/types.ts new file mode 100644 index 000000000..87fc7b9bb --- /dev/null +++ b/tests/integration/cases/types/imported-interface/__output__/solid/types.ts @@ -0,0 +1,6 @@ +// The props of `Badge`, declared in a module of their own, as a design system declares them. +export interface BadgeProps { + /** The text the badge shows. */ + label: string; + tone?: "info" | "danger"; +} diff --git a/tests/integration/cases/types/imported-interface/__output__/svelte/Badge.svelte b/tests/integration/cases/types/imported-interface/__output__/svelte/Badge.svelte new file mode 100644 index 000000000..d3553083c --- /dev/null +++ b/tests/integration/cases/types/imported-interface/__output__/svelte/Badge.svelte @@ -0,0 +1,22 @@ + + + + +{label} diff --git a/tests/integration/cases/types/imported-interface/__output__/svelte/types.ts b/tests/integration/cases/types/imported-interface/__output__/svelte/types.ts new file mode 100644 index 000000000..87fc7b9bb --- /dev/null +++ b/tests/integration/cases/types/imported-interface/__output__/svelte/types.ts @@ -0,0 +1,6 @@ +// The props of `Badge`, declared in a module of their own, as a design system declares them. +export interface BadgeProps { + /** The text the badge shows. */ + label: string; + tone?: "info" | "danger"; +} diff --git a/tests/integration/cases/types/imported-interface/__output__/vue/Badge.vue b/tests/integration/cases/types/imported-interface/__output__/vue/Badge.vue new file mode 100644 index 000000000..6cefb2a21 --- /dev/null +++ b/tests/integration/cases/types/imported-interface/__output__/vue/Badge.vue @@ -0,0 +1,9 @@ + + + diff --git a/tests/integration/cases/types/imported-interface/__output__/vue/types.ts b/tests/integration/cases/types/imported-interface/__output__/vue/types.ts new file mode 100644 index 000000000..87fc7b9bb --- /dev/null +++ b/tests/integration/cases/types/imported-interface/__output__/vue/types.ts @@ -0,0 +1,6 @@ +// The props of `Badge`, declared in a module of their own, as a design system declares them. +export interface BadgeProps { + /** The text the badge shows. */ + label: string; + tone?: "info" | "danger"; +} diff --git a/tests/integration/cases/types/imported-interface/__screenshots__/danger-chromium-linux.png b/tests/integration/cases/types/imported-interface/__screenshots__/danger-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..cefb1a0f117dc4110a592864ef19636aea64ef4b GIT binary patch literal 1211 zcmV;s1VsCZP)W#~f4zor|KPD9o&eE;Dpde5fvSP#xx=>X?eEqAJW` z4yuEyFda%|zyE8Jm&x=x)3ZO4SZ`OWZ};1k-+Ovg#CaT;IFADp=W$@- zJPu5p$AO9SIPd_@y5U8_C?SMKSt(lW{0A^i$p|f!asGX5;jC2=dSC3vRihz-TISK- zOU%mVLmXuTMmI_-r9VWVr}unj+A0cT=^y`5#Xg*^bs`DFh(g43f}H;+?y0k_YFe8h z%cBKh3lt=I;x|QFTGSxS zdcDF!p5uBMrLTpJA(l>*%{vk>p6yaTQax;kM| z-7Z+0PD#;pt1M`{Ae-4%eMSfl)2elPPm%6pl8DZ^2X|}RbY15l;{8Lx!1X<3L~CPu zKBaz3;H}&yg@-qT*s&)NtOSjs3X5mnk^0sqBNsH9?X)6XeIMtkdRSXY>kfv zkLSiE^^eY=vlUuoao&+qmed_QRY6FSR&>78ZMeJ6##oJjTYDA&V24bfX5fbu8&QcN z=~@kx8AkThk}H52SwseIKrw8koR@EBHz13i>5UtJDe;!Zgp%hDaJEwJ^@a!|?!wa} z%m)ou-QCmgZq5Mql51|6oK^wav15F8x{zTYD{iT1`={%lQ9pNEeSLNI?aL=ieE!Yp z<+Wau=#o-zY&Ue_iX?Cm+HtnRXsxd=f7<_S8RgTLM{lpLY#8SeoS}z3&==r+?@rb7 z3VGO_leTpQeSuq93rm>})dK*5XD<$a`SWHI&0NhJ008#F*+{Q3Yfm;88>u&oN2@sa zZ1=n0{(3KrOnmSN&b__@cwT1p#%-*nUAf#PLp29j(!`Q3tCDvdEIwzZaoIEsVrgxWjF&RnaFga&(IT&mkd zXN@DQPh*x;s%;g|SX^B!#orSH=Xn$twGed?vy|&q>^Sn8H`MNIr)n#!I1Ki~WHt0} z!ILe&>6@14Ns^tOU(cSOe|4B-SrniR>1de*uYTxW|12x&f;HcS(l@`)U7Z)NkE=4q z&@nRh10HZ$tc2d*+2PUY#mw^HI7Ma5aXmA!uY(awhC}Rpd6*|&73VNlHARDG6;XC_ zJ}>>w?m2OoyMyTi?x~G1jqC+& z-xz!kZ3**0&L0(~?~>!d#CaT;IFADp=W$@-JPu5p$AP~A00960VIcJV00006Nkl~>&D9Cb;~GhB7f7DYxC}x@4maXkLL3^xR-ed?rVBk_w=&v>1Ey1%etqRbx$wr zo((c1vM}&zTq%2X*tSJP8E*`@Gnq9~9M7g7w$D&=%a8Y?;AusLO=c8vO5E zBiq3=9url>8*ElJ1&>2USr})fJYQwqB#cHyyQ-{dI13hyvARdsF{9&B*;~tig`+45 z@z*@b{@7Wr8JWKymo4xxvVuo`Fs|H6BM%!<70vnFMpnh-cv_a?{Q0yfs#+tA;O%9= z0*|KYYjlz&x%k~?WuByxd}>5p=6O*yqF|;>xiAp>jDWUY_M>sH=IP7lK-N+(p>7-p zbcDcP$N+#4GtLjtj3c z<1F3VS;OY#d!mls?LXVyeR23<3J-WT_E#2%K z#zKS%!4PaE+mZc?^uN!#r { + it("renders the label with the default tone", async () => { + const view = await mountScenario(Badge, "default-tone"); + await view.expectParity("default-tone"); + await expect.element(view.getByText("New")).toHaveClass("badge-info"); + }); + + it("renders the tone it is given", async () => { + const view = await mountScenario(Badge, "danger"); + await view.expectParity("danger"); + await expect.element(view.getByText("Overdue")).toHaveClass("badge-danger"); + }); +}); diff --git a/tests/integration/cases/types/imported-interface/types.ts b/tests/integration/cases/types/imported-interface/types.ts new file mode 100644 index 000000000..87fc7b9bb --- /dev/null +++ b/tests/integration/cases/types/imported-interface/types.ts @@ -0,0 +1,6 @@ +// The props of `Badge`, declared in a module of their own, as a design system declares them. +export interface BadgeProps { + /** The text the badge shows. */ + label: string; + tone?: "info" | "danger"; +} diff --git a/tests/integration/cases/types/intersection-props/Chip.uf.tsx b/tests/integration/cases/types/intersection-props/Chip.uf.tsx new file mode 100644 index 000000000..39b54558b --- /dev/null +++ b/tests/integration/cases/types/intersection-props/Chip.uf.tsx @@ -0,0 +1,7 @@ +import type { Sized, Toned } from "./types"; + +type ChipProps = Sized & Toned & { label: string }; + +export default function Chip({ size, tone = "neutral", label }: ChipProps) { + return {label}; +} diff --git a/tests/integration/cases/types/intersection-props/__expected__/aria.accent.yaml b/tests/integration/cases/types/intersection-props/__expected__/aria.accent.yaml new file mode 100644 index 000000000..49d76ddff --- /dev/null +++ b/tests/integration/cases/types/intersection-props/__expected__/aria.accent.yaml @@ -0,0 +1 @@ +- text: Live diff --git a/tests/integration/cases/types/intersection-props/__expected__/aria.small.yaml b/tests/integration/cases/types/intersection-props/__expected__/aria.small.yaml new file mode 100644 index 000000000..48358dcd0 --- /dev/null +++ b/tests/integration/cases/types/intersection-props/__expected__/aria.small.yaml @@ -0,0 +1 @@ +- text: Draft diff --git a/tests/integration/cases/types/intersection-props/__expected__/diagnostics.json b/tests/integration/cases/types/intersection-props/__expected__/diagnostics.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/tests/integration/cases/types/intersection-props/__expected__/diagnostics.json @@ -0,0 +1 @@ +[] diff --git a/tests/integration/cases/types/intersection-props/__expected__/dom.accent.html b/tests/integration/cases/types/intersection-props/__expected__/dom.accent.html new file mode 100644 index 000000000..5480c83ad --- /dev/null +++ b/tests/integration/cases/types/intersection-props/__expected__/dom.accent.html @@ -0,0 +1,3 @@ + + "Live" + diff --git a/tests/integration/cases/types/intersection-props/__expected__/dom.small.html b/tests/integration/cases/types/intersection-props/__expected__/dom.small.html new file mode 100644 index 000000000..d0e3140f1 --- /dev/null +++ b/tests/integration/cases/types/intersection-props/__expected__/dom.small.html @@ -0,0 +1,3 @@ + + "Draft" + diff --git a/tests/integration/cases/types/intersection-props/__expected__/geometry.accent.json b/tests/integration/cases/types/intersection-props/__expected__/geometry.accent.json new file mode 100644 index 000000000..0ebbb087a --- /dev/null +++ b/tests/integration/cases/types/intersection-props/__expected__/geometry.accent.json @@ -0,0 +1,93 @@ +{ + "version": 1, + "nodes": { + "span[0]": { + "box": [ + 8, + 10, + 30.9375, + 20 + ], + "style": { + "display": "inline", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "span[0]/#text[0]": { + "box": [ + [ + 8, + 10, + 30.9375, + 20 + ] + ] + } + } +} diff --git a/tests/integration/cases/types/intersection-props/__expected__/geometry.small.json b/tests/integration/cases/types/intersection-props/__expected__/geometry.small.json new file mode 100644 index 000000000..21b7178de --- /dev/null +++ b/tests/integration/cases/types/intersection-props/__expected__/geometry.small.json @@ -0,0 +1,93 @@ +{ + "version": 1, + "nodes": { + "span[0]": { + "box": [ + 8, + 10, + 37.171875, + 20 + ], + "style": { + "display": "inline", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "span[0]/#text[0]": { + "box": [ + [ + 8, + 10, + 37.171875, + 20 + ] + ] + } + } +} diff --git a/tests/integration/cases/types/intersection-props/__expected__/ssr.accent.html b/tests/integration/cases/types/intersection-props/__expected__/ssr.accent.html new file mode 100644 index 000000000..5480c83ad --- /dev/null +++ b/tests/integration/cases/types/intersection-props/__expected__/ssr.accent.html @@ -0,0 +1,3 @@ + + "Live" + diff --git a/tests/integration/cases/types/intersection-props/__expected__/ssr.small.html b/tests/integration/cases/types/intersection-props/__expected__/ssr.small.html new file mode 100644 index 000000000..d0e3140f1 --- /dev/null +++ b/tests/integration/cases/types/intersection-props/__expected__/ssr.small.html @@ -0,0 +1,3 @@ + + "Draft" + diff --git a/tests/integration/cases/types/intersection-props/__output__/angular/chip.ts b/tests/integration/cases/types/intersection-props/__output__/angular/chip.ts new file mode 100644 index 000000000..16fe80cd1 --- /dev/null +++ b/tests/integration/cases/types/intersection-props/__output__/angular/chip.ts @@ -0,0 +1,61 @@ +import { + Component, + Directive, + ElementRef, + type OnInit, + Renderer2, + inject, + input, +} from "@angular/core"; + +/** The scope attributes taken off each host, for the roots its component renders later. */ +const scopes = new WeakMap(); + +/** + * Gives a root node of its component the scope attributes the component's styled parents put on + * its host (ADR-0063), and takes them off the host, which keeps only `display: contents`. + */ +@Directive({ selector: "[ufScope]" }) +export class ScopeAttributes implements OnInit { + readonly ufScope = input.required(); + private readonly element = inject>(ElementRef); + private readonly renderer = inject(Renderer2); + + ngOnInit(): void { + const host = this.ufScope(); + let names = scopes.get(host); + if (!names) { + names = host.getAttributeNames().filter((name) => name.startsWith("data-uf-")); + for (const name of names) this.renderer.removeAttribute(host, name); + scopes.set(host, names); + } + for (const name of names) this.renderer.setAttribute(this.element.nativeElement, name, ""); + } +} + +@Component({ + selector: "uf-chip", + imports: [ScopeAttributes], + host: { "[attr.class]": "null", "[attr.style]": "'display: contents;'" }, + preserveWhitespaces: false, + template: ` + @let size = this.size(); + @let tone = this.tone(); + @let label = this.label(); + {{ label }} + `, +}) +export default class Chip { + readonly size = input.required<"small" | "large">(); + readonly tone = input<"neutral" | "accent", "neutral" | "accent" | undefined>("neutral", { + transform: (value) => (value === undefined ? "neutral" : value), + }); + readonly label = input.required(); + readonly class = input(); + readonly style = input>(); + protected readonly host = inject>(ElementRef).nativeElement; +} diff --git a/tests/integration/cases/types/intersection-props/__output__/angular/types.ts b/tests/integration/cases/types/intersection-props/__output__/angular/types.ts new file mode 100644 index 000000000..2070c397e --- /dev/null +++ b/tests/integration/cases/types/intersection-props/__output__/angular/types.ts @@ -0,0 +1,7 @@ +export interface Sized { + size: "small" | "large"; +} + +export interface Toned { + tone?: "neutral" | "accent"; +} diff --git a/tests/integration/cases/types/intersection-props/__output__/astro/Chip.astro b/tests/integration/cases/types/intersection-props/__output__/astro/Chip.astro new file mode 100644 index 000000000..7353470a2 --- /dev/null +++ b/tests/integration/cases/types/intersection-props/__output__/astro/Chip.astro @@ -0,0 +1,23 @@ +--- +type ChipProps = Sized & Toned & { label: string }; + +interface Props extends ChipProps { + class?: string; + style?: Record; +} + +const { size, tone = "neutral", label, class: className, style } = Astro.props; + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} +--- + +{label} diff --git a/tests/integration/cases/types/intersection-props/__output__/astro/types.ts b/tests/integration/cases/types/intersection-props/__output__/astro/types.ts new file mode 100644 index 000000000..2070c397e --- /dev/null +++ b/tests/integration/cases/types/intersection-props/__output__/astro/types.ts @@ -0,0 +1,7 @@ +export interface Sized { + size: "small" | "large"; +} + +export interface Toned { + tone?: "neutral" | "accent"; +} diff --git a/tests/integration/cases/types/intersection-props/__output__/ir.json b/tests/integration/cases/types/intersection-props/__output__/ir.json new file mode 100644 index 000000000..fc48d991b --- /dev/null +++ b/tests/integration/cases/types/intersection-props/__output__/ir.json @@ -0,0 +1,260 @@ +{ + "irVersion": 1, + "file": "types/intersection-props/Chip.uf.tsx", + "components": [ + { + "name": "Chip", + "span": { + "start": 99, + "end": 249 + }, + "props": [ + { + "name": "size", + "optional": false, + "type": { + "code": "\"small\" | \"large\"", + "span": { + "start": 163, + "end": 172 + } + }, + "binding": "size@130", + "declaredBy": { + "file": "types/intersection-props/types.ts", + "declaration": "Sized", + "span": { + "start": 27, + "end": 51 + } + }, + "span": { + "start": 163, + "end": 172 + } + }, + { + "name": "tone", + "optional": true, + "type": { + "code": "\"neutral\" | \"accent\"", + "span": { + "start": 163, + "end": 172 + } + }, + "default": { + "code": "\"neutral\"", + "span": { + "start": 143, + "end": 152 + }, + "refs": [] + }, + "binding": "tone@136", + "declaredBy": { + "file": "types/intersection-props/types.ts", + "declaration": "Toned", + "span": { + "start": 82, + "end": 110 + } + }, + "span": { + "start": 163, + "end": 172 + } + }, + { + "name": "label", + "optional": false, + "type": { + "code": "string", + "span": { + "start": 88, + "end": 94 + } + }, + "binding": "label@154", + "span": { + "start": 81, + "end": 94 + } + } + ], + "propsParameter": { + "form": "destructured", + "type": { + "code": "ChipProps", + "span": { + "start": 163, + "end": 172 + } + }, + "span": { + "start": 128, + "end": 172 + } + }, + "types": [ + "ChipProps" + ], + "bindings": [ + { + "id": "size@130", + "name": "size", + "kind": "prop", + "span": { + "start": 130, + "end": 134 + } + }, + { + "id": "tone@136", + "name": "tone", + "kind": "prop", + "span": { + "start": 136, + "end": 140 + } + }, + { + "id": "label@154", + "name": "label", + "kind": "prop", + "span": { + "start": 154, + "end": 159 + } + } + ], + "setup": [], + "render": { + "kind": "Element", + "tag": "span", + "attributes": [ + { + "kind": "Class", + "items": [ + { + "kind": "Dynamic", + "value": { + "code": "`chip chip-${size} chip-${tone}`", + "span": { + "start": 198, + "end": 230 + }, + "refs": [ + { + "kind": "Binding", + "binding": "size@130", + "span": { + "start": 211, + "end": 215 + } + }, + { + "kind": "Binding", + "binding": "tone@136", + "span": { + "start": 224, + "end": 228 + } + } + ] + }, + "span": { + "start": 198, + "end": 230 + } + } + ], + "span": { + "start": 191, + "end": 231 + } + } + ], + "children": [ + { + "kind": "Interpolation", + "value": { + "code": "label", + "span": { + "start": 233, + "end": 238 + }, + "refs": [ + { + "kind": "Binding", + "binding": "label@154", + "span": { + "start": 233, + "end": 238 + } + } + ] + }, + "span": { + "start": 232, + "end": 239 + } + } + ], + "span": { + "start": 185, + "end": 246 + } + } + } + ], + "exports": [ + { + "kind": "default", + "name": "default", + "local": "Chip", + "span": { + "start": 99, + "end": 249 + } + } + ], + "types": [ + { + "name": "ChipProps", + "exported": false, + "code": "type ChipProps = Sized & Toned & { label: string };", + "span": { + "start": 46, + "end": 97 + } + } + ], + "typeImports": [ + { + "specifier": "./types", + "names": [ + { + "imported": "Sized", + "local": "Sized", + "span": { + "start": 14, + "end": 19 + } + }, + { + "imported": "Toned", + "local": "Toned", + "span": { + "start": 21, + "end": 26 + } + } + ], + "span": { + "start": 0, + "end": 44 + } + } + ] +} diff --git a/tests/integration/cases/types/intersection-props/__output__/qwik/Chip.tsx b/tests/integration/cases/types/intersection-props/__output__/qwik/Chip.tsx new file mode 100644 index 000000000..97299be76 --- /dev/null +++ b/tests/integration/cases/types/intersection-props/__output__/qwik/Chip.tsx @@ -0,0 +1,26 @@ +import { type CSSProperties, type ClassList, component$ } from "@qwik.dev/core"; + +type ChipProps = Sized & Toned & { label: string }; + +export default component$( + ({ size, tone = "neutral", label, class: className, style, ...attributes }) => { + return ( + + {label} + + ); + }, +); + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/types/intersection-props/__output__/qwik/types.ts b/tests/integration/cases/types/intersection-props/__output__/qwik/types.ts new file mode 100644 index 000000000..2070c397e --- /dev/null +++ b/tests/integration/cases/types/intersection-props/__output__/qwik/types.ts @@ -0,0 +1,7 @@ +export interface Sized { + size: "small" | "large"; +} + +export interface Toned { + tone?: "neutral" | "accent"; +} diff --git a/tests/integration/cases/types/intersection-props/__output__/react/Chip.tsx b/tests/integration/cases/types/intersection-props/__output__/react/Chip.tsx new file mode 100644 index 000000000..00142aa1d --- /dev/null +++ b/tests/integration/cases/types/intersection-props/__output__/react/Chip.tsx @@ -0,0 +1,51 @@ +import type { CSSProperties } from "react"; + +type ChipProps = Sized & Toned & { label: string }; + +export interface ChipAttributes { + className?: string; + style?: CSSProperties; +} + +export default function Chip({ + size, + tone = "neutral", + label, + className, + style, + ...attributes +}: ChipProps & ChipAttributes) { + return ( + + {label} + + ); +} + +/** Joins class names, and the keys of an object's truthy entries, into one `className`. */ +function cx(...parts: unknown[]): string { + const names: string[] = []; + for (const part of parts) { + if (typeof part === "string") { + if (part) names.push(part); + } else if (part && typeof part === "object") { + for (const [key, on] of Object.entries(part)) { + if (on) names.push(key); + } + } + } + return names.join(" "); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/types/intersection-props/__output__/react/types.ts b/tests/integration/cases/types/intersection-props/__output__/react/types.ts new file mode 100644 index 000000000..2070c397e --- /dev/null +++ b/tests/integration/cases/types/intersection-props/__output__/react/types.ts @@ -0,0 +1,7 @@ +export interface Sized { + size: "small" | "large"; +} + +export interface Toned { + tone?: "neutral" | "accent"; +} diff --git a/tests/integration/cases/types/intersection-props/__output__/solid/Chip.tsx b/tests/integration/cases/types/intersection-props/__output__/solid/Chip.tsx new file mode 100644 index 000000000..f0ee46a45 --- /dev/null +++ b/tests/integration/cases/types/intersection-props/__output__/solid/Chip.tsx @@ -0,0 +1,35 @@ +import { type JSX, mergeProps } from "solid-js"; + +type ChipProps = Sized & Toned & { label: string }; + +export default function Chip(rawProps: ChipProps & { class?: string; style?: JSX.CSSProperties }) { + const props = mergeProps({ tone: "neutral" } satisfies Partial, rawProps); + return ( + + {props.label} + + ); +} + +/** Joins class names: strings as they are, and the names of an object's truthy entries. */ +function cx(...parts: unknown[]): string { + const names: string[] = []; + for (const part of parts) { + if (typeof part === "string") names.push(part); + else if (part && typeof part === "object") { + for (const [name, on] of Object.entries(part)) if (on) names.push(name); + } + } + return names.join(" "); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} diff --git a/tests/integration/cases/types/intersection-props/__output__/solid/types.ts b/tests/integration/cases/types/intersection-props/__output__/solid/types.ts new file mode 100644 index 000000000..2070c397e --- /dev/null +++ b/tests/integration/cases/types/intersection-props/__output__/solid/types.ts @@ -0,0 +1,7 @@ +export interface Sized { + size: "small" | "large"; +} + +export interface Toned { + tone?: "neutral" | "accent"; +} diff --git a/tests/integration/cases/types/intersection-props/__output__/svelte/Chip.svelte b/tests/integration/cases/types/intersection-props/__output__/svelte/Chip.svelte new file mode 100644 index 000000000..3d1dc1a36 --- /dev/null +++ b/tests/integration/cases/types/intersection-props/__output__/svelte/Chip.svelte @@ -0,0 +1,24 @@ + + + + +{label} diff --git a/tests/integration/cases/types/intersection-props/__output__/svelte/types.ts b/tests/integration/cases/types/intersection-props/__output__/svelte/types.ts new file mode 100644 index 000000000..2070c397e --- /dev/null +++ b/tests/integration/cases/types/intersection-props/__output__/svelte/types.ts @@ -0,0 +1,7 @@ +export interface Sized { + size: "small" | "large"; +} + +export interface Toned { + tone?: "neutral" | "accent"; +} diff --git a/tests/integration/cases/types/intersection-props/__output__/vue/Chip.vue b/tests/integration/cases/types/intersection-props/__output__/vue/Chip.vue new file mode 100644 index 000000000..952aa0c3d --- /dev/null +++ b/tests/integration/cases/types/intersection-props/__output__/vue/Chip.vue @@ -0,0 +1,11 @@ + + + diff --git a/tests/integration/cases/types/intersection-props/__output__/vue/types.ts b/tests/integration/cases/types/intersection-props/__output__/vue/types.ts new file mode 100644 index 000000000..2070c397e --- /dev/null +++ b/tests/integration/cases/types/intersection-props/__output__/vue/types.ts @@ -0,0 +1,7 @@ +export interface Sized { + size: "small" | "large"; +} + +export interface Toned { + tone?: "neutral" | "accent"; +} diff --git a/tests/integration/cases/types/intersection-props/__screenshots__/accent-chromium-linux.png b/tests/integration/cases/types/intersection-props/__screenshots__/accent-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..cf8664f055fbaf20064fc37beb47814b378bbe34 GIT binary patch literal 609 zcmV-n0-pVeP)aS9zy6M`cmOk-6Y1GZz$*mBnR>ppIN& zH};MkkL`{(k)LY*y!ZY3<;(Zl!{u@b?hOyXeInV!Nj7nkO`K#CC)vbFHgUGtIK8OK zsxfYJrJEL*u*P_>TMpw(-WlB)v?&81rZmeCM&SS1#DSDIii6#AtGe;MI4#+%bKyx& z%JNk7{^nvik>ivfjBKWyl_lfTrtAG4Y*sNU}`T5d_uoo{&1 z;RQ58+VNzp6b2SN$T*`qM;OoXubn8X81+VN$<7Y?4JmkmtW@E?hgj00Fea-jIrz)Z z^QVsvu6X$5&Bdj2&ig&u0333Gt&pZSQdk^vHKqe1cy#>s%eUnXoZi>V)*F z6|L$?sH3qDA(~SVQfAi&;|HxeyRC}<(r>=I!E_zCZgUW4Io)_Sl<|tTb!}l*^X%tt zNZGVnoSd<0nAN8R?qxlhym#z_78Smo4fuL~_U!z(@#7aCOm=< zOuQS|tNHU!^Ks(n_+>j|I3Dd_@Bf;9u*Qz9Da$B=TlenY=3hwPH*u0poMaOx*~CdU vagt4(9{>OV|NkZ$NRt2n00v1!K~w_(x(5DFtb8dD00000NkvXXu0mjf-R2{s literal 0 HcmV?d00001 diff --git a/tests/integration/cases/types/intersection-props/__screenshots__/small-chromium-linux.png b/tests/integration/cases/types/intersection-props/__screenshots__/small-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..1ce03902b9028c998764c658d482cdb463a6d92a GIT binary patch literal 799 zcmV+)1K|9LP)GW&#Uq(YH?}ae5}87$(y46arqYdU zWQw;sQ^-c9`lfQz)!Zd&D&0@GV@M^w;Uu5o@$ufXEwMTr4#C6b5qR*licQceHbJY{ z1g&Bdw2Dp8Do&s*7|VHi{~K1PPw~trPLz*>NwNY}teb|RK?y8s$3j!@_O1Zv?0u$@ zb-ze6vRHDOou0AL3yW7zzZ@U#?>vFJmlj|l#5;SvV(Z}R`6UfZ2xwsGRzT+^bWWky zo|!?KWf>K<8UDKbe)JOjcyHOaxvfXZdD%fGF%X1`B!(ATdU84Q!}l zM&DMS?uuatbfB2PnmnT`S3ylAMTJV4-quI_s&%9N4mAaG+#X_6Lvz&QuY zP+yT3xhNd0N;B4!!JR4QImy}8K;hVlyNBfXj1oxRbh6$ zOtFIG{POp3n8#$R1D2#-7Rx+#-Cn63k%{diwh_iRu;&t88I7xhqz}-PX!kq<<|qzC zN2Mtk`EVBd-tj5eJMjsc7+#W9RlXKiMXF<{Vr#bDqz{xSvBlMj6r@p{NMeIhr!JX! zg}&|`zJ4*numh&r-+t1VplRV0?D_|g9`8TjTwmYRBZXMg3oWUJOSJq`_tnnKGiIgv zuMwi$lon#6M`-cm{0=Ma2a^V96`P<{Y=Ty?30lP_Xce2_KT!Mw00960i4BJ&00006 dNkl { + it("renders every part's members", async () => { + const view = await mountScenario(Chip, "small"); + await view.expectParity("small"); + await expect.element(view.getByText("Draft")).toHaveClass("chip-small chip-neutral"); + }); + + it("renders the optional member it is given", async () => { + const view = await mountScenario(Chip, "accent"); + await view.expectParity("accent"); + await expect.element(view.getByText("Live")).toHaveClass("chip-large chip-accent"); + }); +}); diff --git a/tests/integration/cases/types/intersection-props/types.ts b/tests/integration/cases/types/intersection-props/types.ts new file mode 100644 index 000000000..2070c397e --- /dev/null +++ b/tests/integration/cases/types/intersection-props/types.ts @@ -0,0 +1,7 @@ +export interface Sized { + size: "small" | "large"; +} + +export interface Toned { + tone?: "neutral" | "accent"; +} diff --git a/tests/integration/cases/types/package-types/Status.uf.tsx b/tests/integration/cases/types/package-types/Status.uf.tsx new file mode 100644 index 000000000..b5d008054 --- /dev/null +++ b/tests/integration/cases/types/package-types/Status.uf.tsx @@ -0,0 +1,12 @@ +import type { SubscriptionLike } from "rxjs"; + +// A member a package declares: `closed`, from rxjs's `SubscriptionLike`. +export type StatusProps = Pick & { label: string }; + +export default function Status({ closed, label }: StatusProps) { + return ( +

+ {label}: {closed ? "closed" : "open"} +

+ ); +} diff --git a/tests/integration/cases/types/package-types/__expected__/aria.closed.yaml b/tests/integration/cases/types/package-types/__expected__/aria.closed.yaml new file mode 100644 index 000000000..bba124ef2 --- /dev/null +++ b/tests/integration/cases/types/package-types/__expected__/aria.closed.yaml @@ -0,0 +1 @@ +- paragraph: "Feed: closed" diff --git a/tests/integration/cases/types/package-types/__expected__/aria.open.yaml b/tests/integration/cases/types/package-types/__expected__/aria.open.yaml new file mode 100644 index 000000000..88979c8ca --- /dev/null +++ b/tests/integration/cases/types/package-types/__expected__/aria.open.yaml @@ -0,0 +1 @@ +- paragraph: "Feed: open" diff --git a/tests/integration/cases/types/package-types/__expected__/diagnostics.json b/tests/integration/cases/types/package-types/__expected__/diagnostics.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/tests/integration/cases/types/package-types/__expected__/diagnostics.json @@ -0,0 +1 @@ +[] diff --git a/tests/integration/cases/types/package-types/__expected__/dom.closed.html b/tests/integration/cases/types/package-types/__expected__/dom.closed.html new file mode 100644 index 000000000..d92c2ff33 --- /dev/null +++ b/tests/integration/cases/types/package-types/__expected__/dom.closed.html @@ -0,0 +1,3 @@ +

+ "Feed: closed" +

diff --git a/tests/integration/cases/types/package-types/__expected__/dom.open.html b/tests/integration/cases/types/package-types/__expected__/dom.open.html new file mode 100644 index 000000000..b40575f9f --- /dev/null +++ b/tests/integration/cases/types/package-types/__expected__/dom.open.html @@ -0,0 +1,3 @@ +

+ "Feed: open" +

diff --git a/tests/integration/cases/types/package-types/__expected__/geometry.closed.json b/tests/integration/cases/types/package-types/__expected__/geometry.closed.json new file mode 100644 index 000000000..f118ff65b --- /dev/null +++ b/tests/integration/cases/types/package-types/__expected__/geometry.closed.json @@ -0,0 +1,93 @@ +{ + "version": 1, + "nodes": { + "p[0]": { + "box": [ + 8, + 24, + 96.5625, + 24 + ], + "style": { + "display": "block", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "16px", + "margin-right": "0px", + "margin-bottom": "16px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "p[0]/#text[0]": { + "box": [ + [ + 8, + 26, + 96.5625, + 20 + ] + ] + } + } +} diff --git a/tests/integration/cases/types/package-types/__expected__/geometry.open.json b/tests/integration/cases/types/package-types/__expected__/geometry.open.json new file mode 100644 index 000000000..9197bb2ee --- /dev/null +++ b/tests/integration/cases/types/package-types/__expected__/geometry.open.json @@ -0,0 +1,93 @@ +{ + "version": 1, + "nodes": { + "p[0]": { + "box": [ + 8, + 24, + 84.546875, + 24 + ], + "style": { + "display": "block", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "16px", + "margin-right": "0px", + "margin-bottom": "16px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "p[0]/#text[0]": { + "box": [ + [ + 8, + 26, + 84.546875, + 20 + ] + ] + } + } +} diff --git a/tests/integration/cases/types/package-types/__expected__/ssr.closed.html b/tests/integration/cases/types/package-types/__expected__/ssr.closed.html new file mode 100644 index 000000000..d92c2ff33 --- /dev/null +++ b/tests/integration/cases/types/package-types/__expected__/ssr.closed.html @@ -0,0 +1,3 @@ +

+ "Feed: closed" +

diff --git a/tests/integration/cases/types/package-types/__expected__/ssr.open.html b/tests/integration/cases/types/package-types/__expected__/ssr.open.html new file mode 100644 index 000000000..b40575f9f --- /dev/null +++ b/tests/integration/cases/types/package-types/__expected__/ssr.open.html @@ -0,0 +1,3 @@ +

+ "Feed: open" +

diff --git a/tests/integration/cases/types/package-types/__output__/angular/status.ts b/tests/integration/cases/types/package-types/__output__/angular/status.ts new file mode 100644 index 000000000..5c583522c --- /dev/null +++ b/tests/integration/cases/types/package-types/__output__/angular/status.ts @@ -0,0 +1,59 @@ +import { + Component, + Directive, + ElementRef, + type OnInit, + Renderer2, + inject, + input, +} from "@angular/core"; + +export type StatusProps = Pick & { label: string }; + +/** The scope attributes taken off each host, for the roots its component renders later. */ +const scopes = new WeakMap(); + +/** + * Gives a root node of its component the scope attributes the component's styled parents put on + * its host (ADR-0063), and takes them off the host, which keeps only `display: contents`. + */ +@Directive({ selector: "[ufScope]" }) +export class ScopeAttributes implements OnInit { + readonly ufScope = input.required(); + private readonly element = inject>(ElementRef); + private readonly renderer = inject(Renderer2); + + ngOnInit(): void { + const host = this.ufScope(); + let names = scopes.get(host); + if (!names) { + names = host.getAttributeNames().filter((name) => name.startsWith("data-uf-")); + for (const name of names) this.renderer.removeAttribute(host, name); + scopes.set(host, names); + } + for (const name of names) this.renderer.setAttribute(this.element.nativeElement, name, ""); + } +} + +@Component({ + selector: "uf-status", + imports: [ScopeAttributes], + host: { "[attr.class]": "null", "[attr.style]": "'display: contents;'" }, + preserveWhitespaces: false, + template: ` + @let closed = this.closed(); + @let label = this.label(); +

{{ label }}: {{ closed ? "closed" : "open" }}

+ `, +}) +export default class Status { + readonly closed = input.required(); + readonly label = input.required(); + readonly class = input(); + readonly style = input>(); + protected readonly host = inject>(ElementRef).nativeElement; +} diff --git a/tests/integration/cases/types/package-types/__output__/astro/Status.astro b/tests/integration/cases/types/package-types/__output__/astro/Status.astro new file mode 100644 index 000000000..420e3a5d3 --- /dev/null +++ b/tests/integration/cases/types/package-types/__output__/astro/Status.astro @@ -0,0 +1,23 @@ +--- +export type StatusProps = Pick & { label: string }; + +interface Props extends StatusProps { + class?: string; + style?: Record; +} + +const { closed, label, class: className, style } = Astro.props; + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} +--- + +

{label}: {closed ? "closed" : "open"}

diff --git a/tests/integration/cases/types/package-types/__output__/ir.json b/tests/integration/cases/types/package-types/__output__/ir.json new file mode 100644 index 000000000..18df8c45e --- /dev/null +++ b/tests/integration/cases/types/package-types/__output__/ir.json @@ -0,0 +1,201 @@ +{ + "irVersion": 1, + "file": "types/package-types/Status.uf.tsx", + "components": [ + { + "name": "Status", + "span": { + "start": 202, + "end": 345 + }, + "props": [ + { + "name": "closed", + "optional": false, + "type": { + "code": "boolean", + "span": { + "start": 252, + "end": 263 + } + }, + "binding": "closed@235", + "declaredBy": { + "file": "pkg:rxjs/dist/types/internal/types.d.ts", + "declaration": "SubscriptionLike", + "span": { + "start": 2216, + "end": 2241 + } + }, + "span": { + "start": 252, + "end": 263 + } + }, + { + "name": "label", + "optional": false, + "type": { + "code": "string", + "span": { + "start": 191, + "end": 197 + } + }, + "binding": "label@243", + "span": { + "start": 184, + "end": 197 + } + } + ], + "propsParameter": { + "form": "destructured", + "type": { + "code": "StatusProps", + "span": { + "start": 252, + "end": 263 + } + }, + "span": { + "start": 233, + "end": 263 + } + }, + "types": [ + "StatusProps" + ], + "bindings": [ + { + "id": "closed@235", + "name": "closed", + "kind": "prop", + "span": { + "start": 235, + "end": 241 + } + }, + { + "id": "label@243", + "name": "label", + "kind": "prop", + "span": { + "start": 243, + "end": 248 + } + } + ], + "setup": [], + "render": { + "kind": "Element", + "tag": "p", + "attributes": [], + "children": [ + { + "kind": "Interpolation", + "value": { + "code": "label", + "span": { + "start": 293, + "end": 298 + }, + "refs": [ + { + "kind": "Binding", + "binding": "label@243", + "span": { + "start": 293, + "end": 298 + } + } + ] + }, + "span": { + "start": 292, + "end": 299 + } + }, + { + "kind": "Text", + "value": ": ", + "span": { + "start": 299, + "end": 301 + } + }, + { + "kind": "Interpolation", + "value": { + "code": "closed ? \"closed\" : \"open\"", + "span": { + "start": 302, + "end": 328 + }, + "refs": [ + { + "kind": "Binding", + "binding": "closed@235", + "span": { + "start": 302, + "end": 308 + } + } + ] + }, + "span": { + "start": 301, + "end": 329 + } + } + ], + "span": { + "start": 282, + "end": 338 + } + } + } + ], + "exports": [ + { + "kind": "default", + "name": "default", + "local": "Status", + "span": { + "start": 202, + "end": 345 + } + } + ], + "types": [ + { + "name": "StatusProps", + "exported": true, + "code": "type StatusProps = Pick & { label: string };", + "span": { + "start": 128, + "end": 200 + } + } + ], + "typeImports": [ + { + "specifier": "rxjs", + "names": [ + { + "imported": "SubscriptionLike", + "local": "SubscriptionLike", + "span": { + "start": 14, + "end": 30 + } + } + ], + "span": { + "start": 0, + "end": 45 + } + } + ] +} diff --git a/tests/integration/cases/types/package-types/__output__/qwik/Status.tsx b/tests/integration/cases/types/package-types/__output__/qwik/Status.tsx new file mode 100644 index 000000000..cbb979dbb --- /dev/null +++ b/tests/integration/cases/types/package-types/__output__/qwik/Status.tsx @@ -0,0 +1,22 @@ +import { type CSSProperties, type ClassList, component$ } from "@qwik.dev/core"; + +export type StatusProps = Pick & { label: string }; + +export default component$( + ({ closed, label, class: className, style, ...attributes }) => { + return ( +

+ {label}: {closed ? "closed" : "open"} +

+ ); + }, +); + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/types/package-types/__output__/react/Status.tsx b/tests/integration/cases/types/package-types/__output__/react/Status.tsx new file mode 100644 index 000000000..77cc48fb8 --- /dev/null +++ b/tests/integration/cases/types/package-types/__output__/react/Status.tsx @@ -0,0 +1,31 @@ +import type { CSSProperties } from "react"; + +export type StatusProps = Pick & { label: string }; + +export interface StatusAttributes { + className?: string; + style?: CSSProperties; +} + +export default function Status({ + closed, + label, + className, + style, + ...attributes +}: StatusProps & StatusAttributes) { + return ( +

+ {label}: {closed ? "closed" : "open"} +

+ ); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/types/package-types/__output__/solid/Status.tsx b/tests/integration/cases/types/package-types/__output__/solid/Status.tsx new file mode 100644 index 000000000..8f1094f32 --- /dev/null +++ b/tests/integration/cases/types/package-types/__output__/solid/Status.tsx @@ -0,0 +1,18 @@ +import type { JSX } from "solid-js"; + +export type StatusProps = Pick & { label: string }; + +export default function Status(props: StatusProps & { class?: string; style?: JSX.CSSProperties }) { + return ( +

+ {props.label}: {props.closed ? "closed" : "open"} +

+ ); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} diff --git a/tests/integration/cases/types/package-types/__output__/svelte/Status.svelte b/tests/integration/cases/types/package-types/__output__/svelte/Status.svelte new file mode 100644 index 000000000..2fc1b6174 --- /dev/null +++ b/tests/integration/cases/types/package-types/__output__/svelte/Status.svelte @@ -0,0 +1,24 @@ + + + + +

{label}: {closed ? "closed" : "open"}

diff --git a/tests/integration/cases/types/package-types/__output__/vue/Status.vue b/tests/integration/cases/types/package-types/__output__/vue/Status.vue new file mode 100644 index 000000000..fb74af5ca --- /dev/null +++ b/tests/integration/cases/types/package-types/__output__/vue/Status.vue @@ -0,0 +1,11 @@ + + + diff --git a/tests/integration/cases/types/package-types/__screenshots__/closed-chromium-linux.png b/tests/integration/cases/types/package-types/__screenshots__/closed-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..b631dfb84cb0c4281afc7eb74d21710eafb53ae6 GIT binary patch literal 1720 zcmb_d`y&&I0#;oLXM$H8cFlq@l7IahUV^UUdtsk1Y$ z9k$C_UT1lPl}BTQ8S)nLta00p9Osw&7n~oy=O6HW5B<;=v@}dKR8&;7ye=ZJyPB|z ziwE}ZGCJn*l!}U4oEO5~|8~)*!c&RwFY1e4AAJ|&_YY)%j{p6U=LceO?<)$yY62XR zbKucLvCKaQT=Qt6=5*b2qXD({%8^R*I)6JntbB#(q)?2wj4#>nf18`{9OZqDo*TEB z|FYQEmlN6B2`F%fdg8W?eB}}L@3Z>+UH{&IvOgS$X008>5akj{M=#sR`c{0f$=fbV z^fe~(l<&}xU5BwAid;YftHys2uC}EIRskCd*4;x~GL5bTSEH}&=&@F>XdXUeS08&S zGLETV`2g7UeF8V=C&Zz+V7%6nn(RmI63fJ8DeEJH!!=$iS+da8n}L;WFP7$AziX3Q z7O|<_qwFx$Ct8swvag`sB>A4j+rE&F6wSp}Bk`<3aPZ8cpNwlbFO>t;n0S! z-z7O6BWzY;by60P85zIv24ODZu`Mya2}vzQ)&gz@?mAWK@9OiLn-fHTy*q=_8-zI_bTT82E zD;>(Nwly2`qppr@(t7!gV6P+47{*E9R}c(X6A8o@r2oGm1=bM2KAnylmBcF+L~80d0iP%_+EPRurp8 zn_;Qfw((|>Q*-O6zx*i-WHdBdT^5qZt|5&Xkb>}q=J~#{L`*GD@^Hkat8_cQJf?2D zGw2=X=AAXg+szL3?<9u?o3EYT=N%0dX{m1@WXXvtLe&<-xLBLd|a1l7#DK6dOGDq zXPu9$V9)&ATvyhGGGlNZK6Y85n09q}7D(=Q`zpmgNoLWe%x#zuy5TvTWEkg5el}xE za!F5BVgM(dZX+%tWNmTw3MDfhOmZB;`?a#&W za$D5b!p{5uVaabGPTxmYNljtv`)V7UiKjTznXksgjvnF@60*y~$ewn?&lUTRUw)h> zt@X1Qy>o^Y*uHpSDHrnKIgJoJR&=|ipKW~Kt;r9Dx7fM;klz3!VpbyUe*hH!Q+ew@ zF#(^$J~E2TU#RvIwJ1>U@FtnHgH538m$)YrB+aa*kHaqXYGs&T6ZJh~TH6iG#kt z=Rlng5@54XE&bmaXnI;uNq^3sUrSHIJhmw3Hbnw~d_NLRwPOCr--yZDN`i(gx3&sJ$T_z3 z?VgDU;SJ~&dO*|Og%}H$*MP(~~ssZ+#1Vn$e@S&`Z8(rq| z;yaKo)T4En0HF#`m>Iv>VLP93f9&2dML-Sb(@!_2>ED_kM5` zfRUmwh-QGnhBL#&AmSVE(%(GK{~vpC)9&r;>@=pK;JZfcujARLa?h?DZI>TEx!b6C LA<+n~M=0%2_ytdJ literal 0 HcmV?d00001 diff --git a/tests/integration/cases/types/package-types/__screenshots__/open-chromium-linux.png b/tests/integration/cases/types/package-types/__screenshots__/open-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..4e752b3de18a42c3ff92cf2930391283e3f70213 GIT binary patch literal 1401 zcmbu9`#aMM0LFJNsVvpw(v3_{%#2a)%;wPCbIE1yo4FL(+-K$F(&|*Mc~mab=5)gu zl4Up=rPFo{(Xq{*`Pv8>n{wPE@Y4PHYDCJIt7e|4^W-nE>pa6bIE66qUQIa3G(@FH8 z9;T|%&=)HNGhYYRp6XF!Qkkr{Ac+e3t#xlCpK_rlcbd~bWJzB?{cBh9c+P0HFU3NJ zM4{NFHZuX>t9ji~zndMI1*>dQuYNbSP?k#-efkZ%yH>rB?hWQ^M5R;@FbTbgEf+pV ztDYaMz-F&cKJgpP%8fU&mwA0fi!as9zdf)pUR-hIsNJD79)o={EYmx(J;EI*+!K>J z%fR~^jCLm2LRyt~Z1eLMh(n2C52ShK58Gq*Lur)11n0iCMmdI@ck-Nr*Xt@QNn4g+ zOc2{yl7pG5#JS6F1d275-Pu7X==2?ZFK020(4N)o!hH=3T^n7DK$LrEvOF8tOfS1S z)5PcT48-Le&9Wk|M0UG=L5L(#oSqSEj?KKN@myaJ?X`MXo@IEVXG zrl@klyNX9N8X-J_k+8o}prX|1aIL+@9j(U^iYXV(!(B9B10h$k%TV%>nS20uFsrRz z&`1`vAzmlMj_rF3_JL2ragU$q2L;o~yQF4&`Ahamf?wF7SY^j2xIGHP70mt{^5wz< zY3cBn61S&z_?Yne*p+^Ww|bFNQ1HRBI=lCAu{k4~>{gh(A>#(LB--K#R#@dBI%>#( z8GiUEBxM9+&dFNO9Q)a3?Kv}V( z32pZS3{9T7Bo=fk0H3VuE=rAs>#19!?ye1^CZJYNc29o*V~V12mD#o@`fnxr+zdH> z<7*zbI)l?|asrv8BKN1WkbB}Mnk>}L4Ed+E^+>}Oa^qikt>VB>g#1EoMGd0u+nU}f z6p&0$%VK^`E@k5qJNV;G;bO|6eAuC_~0p^1Orz7kNX1P7&ayl1Q##S&`G zkcEKuX*!coov(;Ko3IqJF1_<~rrJjN2h>(D5y3>Z6q*6I1EL*$Va7C^=Rf5|r6&ce zJm{=z!err8Wvv!u`klzAxFp`X%^0ZLOUBQ5uq?Z>XPGAa4gL&zpVe~YZJJLe*{O_& zt1+BoX$=b>Q(Hb9d^1<89%krZ199yn3_uAVYIMDM>h{$N)A^NealfX9s=;%XrhAfo zYsYo2EO!hJvTWjBFfjuq5M5p0m@kx<;pGDmc1LTy?tv9K_q|1c-`WBy?2r$ZsM|Ku zEL;_h7`Q}wc82Jn!tZmpx|vHUk-;CYB`*&&NyHSKuVieSzoNEY9!S|dyT}jU%Dq{h zZN`I7ZuH!%02y>Hs?&zGiCT)(_}NV-yiJ;t`tWeTJH0W^57p~!FcVH3{+w6QhAl3| zfG!C { + it("renders an open subscription", async () => { + const view = await mountScenario(Status, "open"); + await view.expectParity("open"); + await expect.element(view.getByText("Feed: open")).toBeVisible(); + }); + + it("renders a closed subscription", async () => { + const view = await mountScenario(Status, "closed"); + await view.expectParity("closed"); + await expect.element(view.getByText("Feed: closed")).toBeVisible(); + }); +}); diff --git a/tests/integration/cases/types/shared-type/Palette.uf.tsx b/tests/integration/cases/types/shared-type/Palette.uf.tsx new file mode 100644 index 000000000..e326c2a12 --- /dev/null +++ b/tests/integration/cases/types/shared-type/Palette.uf.tsx @@ -0,0 +1,13 @@ +import Swatch from "./Swatch.uf.tsx"; +import type { Tone } from "./types"; + +// Consumers of `Palette` import the shared type from it. +export type { Tone } from "./types"; + +export default function Palette({ tone }: { tone: Tone }) { + return ( +
+ +
+ ); +} diff --git a/tests/integration/cases/types/shared-type/Swatch.uf.tsx b/tests/integration/cases/types/shared-type/Swatch.uf.tsx new file mode 100644 index 000000000..799b48aa3 --- /dev/null +++ b/tests/integration/cases/types/shared-type/Swatch.uf.tsx @@ -0,0 +1,5 @@ +import type { Tone } from "./types"; + +export default function Swatch({ tone }: { tone: Tone }) { + return {tone}; +} diff --git a/tests/integration/cases/types/shared-type/__expected__/aria.accent.yaml b/tests/integration/cases/types/shared-type/__expected__/aria.accent.yaml new file mode 100644 index 000000000..63b509c44 --- /dev/null +++ b/tests/integration/cases/types/shared-type/__expected__/aria.accent.yaml @@ -0,0 +1 @@ +- text: accent diff --git a/tests/integration/cases/types/shared-type/__expected__/diagnostics.json b/tests/integration/cases/types/shared-type/__expected__/diagnostics.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/tests/integration/cases/types/shared-type/__expected__/diagnostics.json @@ -0,0 +1 @@ +[] diff --git a/tests/integration/cases/types/shared-type/__expected__/dom.accent.html b/tests/integration/cases/types/shared-type/__expected__/dom.accent.html new file mode 100644 index 000000000..31afda8fd --- /dev/null +++ b/tests/integration/cases/types/shared-type/__expected__/dom.accent.html @@ -0,0 +1,5 @@ +
+ + "accent" + +
diff --git a/tests/integration/cases/types/shared-type/__expected__/geometry.accent.json b/tests/integration/cases/types/shared-type/__expected__/geometry.accent.json new file mode 100644 index 000000000..e8774b15e --- /dev/null +++ b/tests/integration/cases/types/shared-type/__expected__/geometry.accent.json @@ -0,0 +1,171 @@ +{ + "version": 1, + "nodes": { + "div[0]": { + "box": [ + 8, + 8, + 51.28125, + 24 + ], + "style": { + "display": "block", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "div[0]/span[0]": { + "box": [ + 8, + 10, + 51.28125, + 20 + ], + "style": { + "display": "inline", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "div[0]/span[0]/#text[0]": { + "box": [ + [ + 8, + 10, + 51.28125, + 20 + ] + ] + } + } +} diff --git a/tests/integration/cases/types/shared-type/__expected__/ssr.accent.html b/tests/integration/cases/types/shared-type/__expected__/ssr.accent.html new file mode 100644 index 000000000..31afda8fd --- /dev/null +++ b/tests/integration/cases/types/shared-type/__expected__/ssr.accent.html @@ -0,0 +1,5 @@ +
+ + "accent" + +
diff --git a/tests/integration/cases/types/shared-type/__output__/angular/palette.ts b/tests/integration/cases/types/shared-type/__output__/angular/palette.ts new file mode 100644 index 000000000..322b28d3a --- /dev/null +++ b/tests/integration/cases/types/shared-type/__output__/angular/palette.ts @@ -0,0 +1,54 @@ +import Swatch from "./swatch"; +import { + Component, + Directive, + ElementRef, + type OnInit, + Renderer2, + inject, + input, +} from "@angular/core"; + +/** The scope attributes taken off each host, for the roots its component renders later. */ +const scopes = new WeakMap(); + +/** + * Gives a root node of its component the scope attributes the component's styled parents put on + * its host (ADR-0063), and takes them off the host, which keeps only `display: contents`. + */ +@Directive({ selector: "[ufScope]" }) +export class ScopeAttributes implements OnInit { + readonly ufScope = input.required(); + private readonly element = inject>(ElementRef); + private readonly renderer = inject(Renderer2); + + ngOnInit(): void { + const host = this.ufScope(); + let names = scopes.get(host); + if (!names) { + names = host.getAttributeNames().filter((name) => name.startsWith("data-uf-")); + for (const name of names) this.renderer.removeAttribute(host, name); + scopes.set(host, names); + } + for (const name of names) this.renderer.setAttribute(this.element.nativeElement, name, ""); + } +} + +@Component({ + selector: "uf-palette", + imports: [Swatch, ScopeAttributes], + host: { "[attr.class]": "null", "[attr.style]": "'display: contents;'" }, + preserveWhitespaces: false, + template: ` + @let tone = this.tone(); +
+ +
+ `, +}) +export default class Palette { + readonly tone = input.required(); + readonly class = input(); + readonly style = input>(); + protected readonly host = inject>(ElementRef).nativeElement; +} diff --git a/tests/integration/cases/types/shared-type/__output__/angular/swatch.ts b/tests/integration/cases/types/shared-type/__output__/angular/swatch.ts new file mode 100644 index 000000000..bf5599719 --- /dev/null +++ b/tests/integration/cases/types/shared-type/__output__/angular/swatch.ts @@ -0,0 +1,55 @@ +import { + Component, + Directive, + ElementRef, + type OnInit, + Renderer2, + inject, + input, +} from "@angular/core"; + +/** The scope attributes taken off each host, for the roots its component renders later. */ +const scopes = new WeakMap(); + +/** + * Gives a root node of its component the scope attributes the component's styled parents put on + * its host (ADR-0063), and takes them off the host, which keeps only `display: contents`. + */ +@Directive({ selector: "[ufScope]" }) +export class ScopeAttributes implements OnInit { + readonly ufScope = input.required(); + private readonly element = inject>(ElementRef); + private readonly renderer = inject(Renderer2); + + ngOnInit(): void { + const host = this.ufScope(); + let names = scopes.get(host); + if (!names) { + names = host.getAttributeNames().filter((name) => name.startsWith("data-uf-")); + for (const name of names) this.renderer.removeAttribute(host, name); + scopes.set(host, names); + } + for (const name of names) this.renderer.setAttribute(this.element.nativeElement, name, ""); + } +} + +@Component({ + selector: "uf-swatch", + imports: [ScopeAttributes], + host: { "[attr.class]": "null", "[attr.style]": "'display: contents;'" }, + preserveWhitespaces: false, + template: ` + @let tone = this.tone(); + {{ tone }} + `, +}) +export default class Swatch { + readonly tone = input.required(); + readonly class = input(); + readonly style = input>(); + protected readonly host = inject>(ElementRef).nativeElement; +} diff --git a/tests/integration/cases/types/shared-type/__output__/angular/types.ts b/tests/integration/cases/types/shared-type/__output__/angular/types.ts new file mode 100644 index 000000000..6af162b17 --- /dev/null +++ b/tests/integration/cases/types/shared-type/__output__/angular/types.ts @@ -0,0 +1,2 @@ +// One type, shared by a component and its child. +export type Tone = "neutral" | "accent"; diff --git a/tests/integration/cases/types/shared-type/__output__/astro/Palette.astro b/tests/integration/cases/types/shared-type/__output__/astro/Palette.astro new file mode 100644 index 000000000..97d4f3f52 --- /dev/null +++ b/tests/integration/cases/types/shared-type/__output__/astro/Palette.astro @@ -0,0 +1,22 @@ +--- +import Swatch from "./Swatch.astro"; + +interface Props { + tone: Tone; + class?: string; + style?: Record; +} + +const { tone, class: className, style } = Astro.props; + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} +--- + +
+ +
diff --git a/tests/integration/cases/types/shared-type/__output__/astro/Swatch.astro b/tests/integration/cases/types/shared-type/__output__/astro/Swatch.astro new file mode 100644 index 000000000..83d25e292 --- /dev/null +++ b/tests/integration/cases/types/shared-type/__output__/astro/Swatch.astro @@ -0,0 +1,22 @@ +--- +interface Props { + tone: Tone; + class?: string; + style?: Record; +} + +const { tone, class: className, style } = Astro.props; + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} +--- + +{tone} diff --git a/tests/integration/cases/types/shared-type/__output__/astro/types.ts b/tests/integration/cases/types/shared-type/__output__/astro/types.ts new file mode 100644 index 000000000..6af162b17 --- /dev/null +++ b/tests/integration/cases/types/shared-type/__output__/astro/types.ts @@ -0,0 +1,2 @@ +// One type, shared by a component and its child. +export type Tone = "neutral" | "accent"; diff --git a/tests/integration/cases/types/shared-type/__output__/ir.Swatch.json b/tests/integration/cases/types/shared-type/__output__/ir.Swatch.json new file mode 100644 index 000000000..3288ed35c --- /dev/null +++ b/tests/integration/cases/types/shared-type/__output__/ir.Swatch.json @@ -0,0 +1,158 @@ +{ + "irVersion": 1, + "file": "types/shared-type/Swatch.uf.tsx", + "components": [ + { + "name": "Swatch", + "span": { + "start": 38, + "end": 160 + }, + "props": [ + { + "name": "tone", + "optional": false, + "type": { + "code": "Tone", + "span": { + "start": 87, + "end": 91 + } + }, + "binding": "tone@71", + "span": { + "start": 81, + "end": 91 + } + } + ], + "propsParameter": { + "form": "destructured", + "type": { + "code": "{ tone: Tone }", + "span": { + "start": 79, + "end": 93 + } + }, + "span": { + "start": 69, + "end": 93 + } + }, + "types": [], + "bindings": [ + { + "id": "tone@71", + "name": "tone", + "kind": "prop", + "span": { + "start": 71, + "end": 75 + } + } + ], + "setup": [], + "render": { + "kind": "Element", + "tag": "span", + "attributes": [ + { + "kind": "Class", + "items": [ + { + "kind": "Dynamic", + "value": { + "code": "`swatch swatch-${tone}`", + "span": { + "start": 119, + "end": 142 + }, + "refs": [ + { + "kind": "Binding", + "binding": "tone@71", + "span": { + "start": 136, + "end": 140 + } + } + ] + }, + "span": { + "start": 119, + "end": 142 + } + } + ], + "span": { + "start": 112, + "end": 143 + } + } + ], + "children": [ + { + "kind": "Interpolation", + "value": { + "code": "tone", + "span": { + "start": 145, + "end": 149 + }, + "refs": [ + { + "kind": "Binding", + "binding": "tone@71", + "span": { + "start": 145, + "end": 149 + } + } + ] + }, + "span": { + "start": 144, + "end": 150 + } + } + ], + "span": { + "start": 106, + "end": 157 + } + } + } + ], + "exports": [ + { + "kind": "default", + "name": "default", + "local": "Swatch", + "span": { + "start": 38, + "end": 160 + } + } + ], + "types": [], + "typeImports": [ + { + "specifier": "./types", + "names": [ + { + "imported": "Tone", + "local": "Tone", + "span": { + "start": 14, + "end": 18 + } + } + ], + "span": { + "start": 0, + "end": 36 + } + } + ] +} diff --git a/tests/integration/cases/types/shared-type/__output__/ir.json b/tests/integration/cases/types/shared-type/__output__/ir.json new file mode 100644 index 000000000..b0ba92593 --- /dev/null +++ b/tests/integration/cases/types/shared-type/__output__/ir.json @@ -0,0 +1,218 @@ +{ + "irVersion": 1, + "file": "types/shared-type/Palette.uf.tsx", + "components": [ + { + "name": "Palette", + "span": { + "start": 172, + "end": 315 + }, + "props": [ + { + "name": "tone", + "optional": false, + "type": { + "code": "Tone", + "span": { + "start": 222, + "end": 226 + } + }, + "binding": "tone@206", + "span": { + "start": 216, + "end": 226 + } + } + ], + "propsParameter": { + "form": "destructured", + "type": { + "code": "{ tone: Tone }", + "span": { + "start": 214, + "end": 228 + } + }, + "span": { + "start": 204, + "end": 228 + } + }, + "types": [], + "bindings": [ + { + "id": "tone@206", + "name": "tone", + "kind": "prop", + "span": { + "start": 206, + "end": 210 + } + } + ], + "setup": [], + "render": { + "kind": "Element", + "tag": "div", + "attributes": [ + { + "kind": "Static", + "name": "class", + "value": "palette", + "span": { + "start": 252, + "end": 267 + } + } + ], + "children": [ + { + "kind": "Component", + "component": "Swatch", + "attributes": [ + { + "kind": "Prop", + "name": "tone", + "value": { + "code": "tone", + "span": { + "start": 289, + "end": 293 + }, + "refs": [ + { + "kind": "Binding", + "binding": "tone@206", + "span": { + "start": 289, + "end": 293 + } + } + ] + }, + "span": { + "start": 283, + "end": 294 + } + } + ], + "fills": [], + "span": { + "start": 275, + "end": 297 + } + } + ], + "span": { + "start": 247, + "end": 308 + } + } + } + ], + "exports": [ + { + "kind": "default", + "name": "default", + "local": "Palette", + "span": { + "start": 172, + "end": 315 + } + } + ], + "types": [], + "imports": [ + { + "specifier": "./Swatch.uf.tsx", + "file": "Swatch.uf.tsx", + "api": { + "file": "Swatch.uf.tsx", + "components": [ + { + "name": "Swatch", + "export": "default", + "props": [ + { + "name": "tone", + "optional": false, + "type": "Tone", + "references": [ + "Tone" + ], + "literals": [ + "\"neutral\"", + "\"accent\"" + ] + } + ], + "events": [], + "models": [], + "slots": [], + "exposes": [], + "inheritAttrs": true, + "root": "element", + "rootTag": "span" + } + ], + "keys": [] + }, + "names": [ + { + "kind": "Component", + "imported": "default", + "local": "Swatch", + "span": { + "start": 7, + "end": 13 + } + } + ], + "span": { + "start": 0, + "end": 37 + } + } + ], + "typeImports": [ + { + "specifier": "./types", + "names": [ + { + "imported": "Tone", + "local": "Tone", + "span": { + "start": 52, + "end": 56 + } + } + ], + "span": { + "start": 38, + "end": 74 + } + } + ], + "typeExports": [ + { + "kind": "ReExport", + "specifier": "./types", + "names": [ + { + "imported": "Tone", + "exported": "Tone", + "span": { + "start": 148, + "end": 152 + } + } + ], + "span": { + "start": 134, + "end": 170 + } + } + ] +} diff --git a/tests/integration/cases/types/shared-type/__output__/qwik/Palette.tsx b/tests/integration/cases/types/shared-type/__output__/qwik/Palette.tsx new file mode 100644 index 000000000..0f18b183e --- /dev/null +++ b/tests/integration/cases/types/shared-type/__output__/qwik/Palette.tsx @@ -0,0 +1,21 @@ +import Swatch from "./Swatch"; +import { type CSSProperties, type ClassList, component$ } from "@qwik.dev/core"; + +export default component$<{ tone: Tone } & { class?: ClassList; style?: CSSProperties }>( + ({ tone, class: className, style, ...attributes }) => { + return ( +
+ +
+ ); + }, +); + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/types/shared-type/__output__/qwik/Swatch.tsx b/tests/integration/cases/types/shared-type/__output__/qwik/Swatch.tsx new file mode 100644 index 000000000..bc64ca1b5 --- /dev/null +++ b/tests/integration/cases/types/shared-type/__output__/qwik/Swatch.tsx @@ -0,0 +1,24 @@ +import { type CSSProperties, type ClassList, component$ } from "@qwik.dev/core"; + +export default component$<{ tone: Tone } & { class?: ClassList; style?: CSSProperties }>( + ({ tone, class: className, style, ...attributes }) => { + return ( + + {tone} + + ); + }, +); + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/types/shared-type/__output__/qwik/types.ts b/tests/integration/cases/types/shared-type/__output__/qwik/types.ts new file mode 100644 index 000000000..6af162b17 --- /dev/null +++ b/tests/integration/cases/types/shared-type/__output__/qwik/types.ts @@ -0,0 +1,2 @@ +// One type, shared by a component and its child. +export type Tone = "neutral" | "accent"; diff --git a/tests/integration/cases/types/shared-type/__output__/react/Palette.tsx b/tests/integration/cases/types/shared-type/__output__/react/Palette.tsx new file mode 100644 index 000000000..3f6e868ca --- /dev/null +++ b/tests/integration/cases/types/shared-type/__output__/react/Palette.tsx @@ -0,0 +1,44 @@ +import Swatch from "./Swatch"; +import type { CSSProperties } from "react"; + +export interface PaletteAttributes { + className?: string; + style?: CSSProperties; +} + +export default function Palette({ + tone, + className, + style, + ...attributes +}: { tone: Tone } & PaletteAttributes) { + return ( +
+ +
+ ); +} + +/** Joins class names, and the keys of an object's truthy entries, into one `className`. */ +function cx(...parts: unknown[]): string { + const names: string[] = []; + for (const part of parts) { + if (typeof part === "string") { + if (part) names.push(part); + } else if (part && typeof part === "object") { + for (const [key, on] of Object.entries(part)) { + if (on) names.push(key); + } + } + } + return names.join(" "); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/types/shared-type/__output__/react/Swatch.tsx b/tests/integration/cases/types/shared-type/__output__/react/Swatch.tsx new file mode 100644 index 000000000..aaed1b2f8 --- /dev/null +++ b/tests/integration/cases/types/shared-type/__output__/react/Swatch.tsx @@ -0,0 +1,47 @@ +import type { CSSProperties } from "react"; + +export interface SwatchAttributes { + className?: string; + style?: CSSProperties; +} + +export default function Swatch({ + tone, + className, + style, + ...attributes +}: { tone: Tone } & SwatchAttributes) { + return ( + + {tone} + + ); +} + +/** Joins class names, and the keys of an object's truthy entries, into one `className`. */ +function cx(...parts: unknown[]): string { + const names: string[] = []; + for (const part of parts) { + if (typeof part === "string") { + if (part) names.push(part); + } else if (part && typeof part === "object") { + for (const [key, on] of Object.entries(part)) { + if (on) names.push(key); + } + } + } + return names.join(" "); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/types/shared-type/__output__/react/types.ts b/tests/integration/cases/types/shared-type/__output__/react/types.ts new file mode 100644 index 000000000..6af162b17 --- /dev/null +++ b/tests/integration/cases/types/shared-type/__output__/react/types.ts @@ -0,0 +1,2 @@ +// One type, shared by a component and its child. +export type Tone = "neutral" | "accent"; diff --git a/tests/integration/cases/types/shared-type/__output__/solid/Palette.tsx b/tests/integration/cases/types/shared-type/__output__/solid/Palette.tsx new file mode 100644 index 000000000..368370fb7 --- /dev/null +++ b/tests/integration/cases/types/shared-type/__output__/solid/Palette.tsx @@ -0,0 +1,23 @@ +import Swatch from "./Swatch"; +import type { JSX } from "solid-js"; + +export default function Palette( + props: { tone: Tone } & { class?: string; style?: JSX.CSSProperties }, +) { + return ( +
+ +
+ ); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} diff --git a/tests/integration/cases/types/shared-type/__output__/solid/Swatch.tsx b/tests/integration/cases/types/shared-type/__output__/solid/Swatch.tsx new file mode 100644 index 000000000..3550569ca --- /dev/null +++ b/tests/integration/cases/types/shared-type/__output__/solid/Swatch.tsx @@ -0,0 +1,34 @@ +import type { JSX } from "solid-js"; + +export default function Swatch( + props: { tone: Tone } & { class?: string; style?: JSX.CSSProperties }, +) { + return ( + + {props.tone} + + ); +} + +/** Joins class names: strings as they are, and the names of an object's truthy entries. */ +function cx(...parts: unknown[]): string { + const names: string[] = []; + for (const part of parts) { + if (typeof part === "string") names.push(part); + else if (part && typeof part === "object") { + for (const [name, on] of Object.entries(part)) if (on) names.push(name); + } + } + return names.join(" "); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} diff --git a/tests/integration/cases/types/shared-type/__output__/solid/types.ts b/tests/integration/cases/types/shared-type/__output__/solid/types.ts new file mode 100644 index 000000000..6af162b17 --- /dev/null +++ b/tests/integration/cases/types/shared-type/__output__/solid/types.ts @@ -0,0 +1,2 @@ +// One type, shared by a component and its child. +export type Tone = "neutral" | "accent"; diff --git a/tests/integration/cases/types/shared-type/__output__/svelte/Palette.svelte b/tests/integration/cases/types/shared-type/__output__/svelte/Palette.svelte new file mode 100644 index 000000000..e79018950 --- /dev/null +++ b/tests/integration/cases/types/shared-type/__output__/svelte/Palette.svelte @@ -0,0 +1,21 @@ + + + + +
+ +
diff --git a/tests/integration/cases/types/shared-type/__output__/svelte/Swatch.svelte b/tests/integration/cases/types/shared-type/__output__/svelte/Swatch.svelte new file mode 100644 index 000000000..4a97f528a --- /dev/null +++ b/tests/integration/cases/types/shared-type/__output__/svelte/Swatch.svelte @@ -0,0 +1,22 @@ + + + + +{tone} diff --git a/tests/integration/cases/types/shared-type/__output__/svelte/types.ts b/tests/integration/cases/types/shared-type/__output__/svelte/types.ts new file mode 100644 index 000000000..6af162b17 --- /dev/null +++ b/tests/integration/cases/types/shared-type/__output__/svelte/types.ts @@ -0,0 +1,2 @@ +// One type, shared by a component and its child. +export type Tone = "neutral" | "accent"; diff --git a/tests/integration/cases/types/shared-type/__output__/vue/Palette.vue b/tests/integration/cases/types/shared-type/__output__/vue/Palette.vue new file mode 100644 index 000000000..4637416c6 --- /dev/null +++ b/tests/integration/cases/types/shared-type/__output__/vue/Palette.vue @@ -0,0 +1,14 @@ + + + diff --git a/tests/integration/cases/types/shared-type/__output__/vue/Swatch.vue b/tests/integration/cases/types/shared-type/__output__/vue/Swatch.vue new file mode 100644 index 000000000..2f4bd9624 --- /dev/null +++ b/tests/integration/cases/types/shared-type/__output__/vue/Swatch.vue @@ -0,0 +1,9 @@ + + + diff --git a/tests/integration/cases/types/shared-type/__output__/vue/types.ts b/tests/integration/cases/types/shared-type/__output__/vue/types.ts new file mode 100644 index 000000000..6af162b17 --- /dev/null +++ b/tests/integration/cases/types/shared-type/__output__/vue/types.ts @@ -0,0 +1,2 @@ +// One type, shared by a component and its child. +export type Tone = "neutral" | "accent"; diff --git a/tests/integration/cases/types/shared-type/__screenshots__/accent-chromium-linux.png b/tests/integration/cases/types/shared-type/__screenshots__/accent-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..98da76806e243a61247d79a616317c2fbca4014b GIT binary patch literal 1005 zcmV000A`NklY}=+I;KO>G99W9)r+d4sHi?>R+srueN50nHLF=w6hj5o z$5iH^D5whB%DX!|+0NeX^*YmWZ~U|MuP41vdV1dR>S7CGBK~0DM2)=v=;=Rm)Q)Fb#~UMRyHM1QlZ`U_wxGkoUqz?et=LE9C=4U zmEfs_-~6Hq0teG1X`FRkZ;|hk=YVQ*~6} zEL%_c&tX77SWZ@4rc+Y7LNdTu>k6!`m4|bV;d+~VoHufPWR2{6BKT%mNJ~kVR@+Do zNlt=w6bMNYWoe^&7P&{@Nr-W{_F#m=qs9!&?Y}2?gK^c`h*DII{O#tg=RG+-_C9!U z#iottN5Re$ck6pT40c%781w#*>)EsG?dMxvxoZEwr;&Mt_E2A zZEZ+az5e~(p9||s>Ugyj0PaNWko69^6b5gn1N@^;U%q~~*jAfHciwyNelVt2+WS1t zc#Ki#&cNtazg?vdf(SvQ>UPsutgiEP+3oxgN3eHgxWO?>q^WRYofZ1M{J8H8MXCD3 z+kZ#cTVZ?{1>T#ga4EXN)pf?jn~jGU$L5u5JyolF-Bww`E~`~rf|#>Ni^O$grTd}N zGG5fnMmJbtcNJ4E(HRRZ%42p^XP2%^1?x^V#X%{{fHof6=sBDR#c>S3qD_ zq { + it("passes the shared type on to the child", async () => { + const view = await mountScenario(Palette, "accent"); + await view.expectParity("accent"); + await expect.element(view.getByText("accent")).toHaveClass("swatch swatch-accent"); + }); +}); diff --git a/tests/integration/cases/types/shared-type/types.ts b/tests/integration/cases/types/shared-type/types.ts new file mode 100644 index 000000000..6af162b17 --- /dev/null +++ b/tests/integration/cases/types/shared-type/types.ts @@ -0,0 +1,2 @@ +// One type, shared by a component and its child. +export type Tone = "neutral" | "accent"; diff --git a/tests/integration/cases/types/utility-types/TextField.uf.tsx b/tests/integration/cases/types/utility-types/TextField.uf.tsx new file mode 100644 index 000000000..284ed13ce --- /dev/null +++ b/tests/integration/cases/types/utility-types/TextField.uf.tsx @@ -0,0 +1,14 @@ +import type { FieldOptions } from "./types"; + +// The five utility types of plan §5.5, over an imported interface. +export type TextFieldProps = Required> & + Readonly>>; + +export default function TextField({ name, label, placeholder, required }: TextFieldProps) { + return ( + + ); +} diff --git a/tests/integration/cases/types/utility-types/__expected__/aria.every-member.yaml b/tests/integration/cases/types/utility-types/__expected__/aria.every-member.yaml new file mode 100644 index 000000000..d32603ef6 --- /dev/null +++ b/tests/integration/cases/types/utility-types/__expected__/aria.every-member.yaml @@ -0,0 +1,3 @@ +- text: Email +- textbox "Email": + - /placeholder: you@example.com diff --git a/tests/integration/cases/types/utility-types/__expected__/aria.required-only.yaml b/tests/integration/cases/types/utility-types/__expected__/aria.required-only.yaml new file mode 100644 index 000000000..c47e28e91 --- /dev/null +++ b/tests/integration/cases/types/utility-types/__expected__/aria.required-only.yaml @@ -0,0 +1,2 @@ +- text: Email +- textbox "Email" diff --git a/tests/integration/cases/types/utility-types/__expected__/diagnostics.json b/tests/integration/cases/types/utility-types/__expected__/diagnostics.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/tests/integration/cases/types/utility-types/__expected__/diagnostics.json @@ -0,0 +1 @@ +[] diff --git a/tests/integration/cases/types/utility-types/__expected__/dom.every-member.html b/tests/integration/cases/types/utility-types/__expected__/dom.every-member.html new file mode 100644 index 000000000..fb3d578e3 --- /dev/null +++ b/tests/integration/cases/types/utility-types/__expected__/dom.every-member.html @@ -0,0 +1,4 @@ + diff --git a/tests/integration/cases/types/utility-types/__expected__/dom.required-only.html b/tests/integration/cases/types/utility-types/__expected__/dom.required-only.html new file mode 100644 index 000000000..0df55c017 --- /dev/null +++ b/tests/integration/cases/types/utility-types/__expected__/dom.required-only.html @@ -0,0 +1,4 @@ + diff --git a/tests/integration/cases/types/utility-types/__expected__/geometry.every-member.json b/tests/integration/cases/types/utility-types/__expected__/geometry.every-member.json new file mode 100644 index 000000000..9451f58a1 --- /dev/null +++ b/tests/integration/cases/types/utility-types/__expected__/geometry.every-member.json @@ -0,0 +1,171 @@ +{ + "version": 1, + "nodes": { + "label[0]": { + "box": [ + 8, + 10, + 240.375, + 20 + ], + "style": { + "display": "inline", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "label[0]/#text[0]": { + "box": [ + [ + 8, + 10, + 40.375, + 20 + ] + ] + }, + "label[0]/input[0]": { + "box": [ + 48.375, + 10, + 200, + 22 + ], + "style": { + "display": "inline-block", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "1px", + "padding-right": "2px", + "padding-bottom": "1px", + "padding-left": "2px", + "border-top-width": "2px", + "border-right-width": "2px", + "border-bottom-width": "2px", + "border-left-width": "2px", + "border-top-style": "inset", + "border-right-style": "inset", + "border-bottom-style": "inset", + "border-left-style": "inset", + "border-top-color": "rgb(118, 118, 118)", + "border-right-color": "rgb(118, 118, 118)", + "border-bottom-color": "rgb(118, 118, 118)", + "border-left-color": "rgb(118, 118, 118)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgb(255, 255, 255)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "13.3333px", + "font-weight": "400", + "font-style": "normal", + "line-height": "normal", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "clip", + "overflow-y": "clip", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + } + } +} diff --git a/tests/integration/cases/types/utility-types/__expected__/geometry.required-only.json b/tests/integration/cases/types/utility-types/__expected__/geometry.required-only.json new file mode 100644 index 000000000..9451f58a1 --- /dev/null +++ b/tests/integration/cases/types/utility-types/__expected__/geometry.required-only.json @@ -0,0 +1,171 @@ +{ + "version": 1, + "nodes": { + "label[0]": { + "box": [ + 8, + 10, + 240.375, + 20 + ], + "style": { + "display": "inline", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "0px", + "padding-right": "0px", + "padding-bottom": "0px", + "padding-left": "0px", + "border-top-width": "0px", + "border-right-width": "0px", + "border-bottom-width": "0px", + "border-left-width": "0px", + "border-top-style": "none", + "border-right-style": "none", + "border-bottom-style": "none", + "border-left-style": "none", + "border-top-color": "rgb(0, 0, 0)", + "border-right-color": "rgb(0, 0, 0)", + "border-bottom-color": "rgb(0, 0, 0)", + "border-left-color": "rgb(0, 0, 0)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgba(0, 0, 0, 0)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "16px", + "font-weight": "400", + "font-style": "normal", + "line-height": "24px", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "visible", + "overflow-y": "visible", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + }, + "label[0]/#text[0]": { + "box": [ + [ + 8, + 10, + 40.375, + 20 + ] + ] + }, + "label[0]/input[0]": { + "box": [ + 48.375, + 10, + 200, + 22 + ], + "style": { + "display": "inline-block", + "position": "static", + "top": "auto", + "right": "auto", + "bottom": "auto", + "left": "auto", + "float": "none", + "box-sizing": "content-box", + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "padding-top": "1px", + "padding-right": "2px", + "padding-bottom": "1px", + "padding-left": "2px", + "border-top-width": "2px", + "border-right-width": "2px", + "border-bottom-width": "2px", + "border-left-width": "2px", + "border-top-style": "inset", + "border-right-style": "inset", + "border-bottom-style": "inset", + "border-left-style": "inset", + "border-top-color": "rgb(118, 118, 118)", + "border-right-color": "rgb(118, 118, 118)", + "border-bottom-color": "rgb(118, 118, 118)", + "border-left-color": "rgb(118, 118, 118)", + "border-top-left-radius": "0px", + "border-top-right-radius": "0px", + "border-bottom-right-radius": "0px", + "border-bottom-left-radius": "0px", + "color": "rgb(0, 0, 0)", + "background-color": "rgb(255, 255, 255)", + "background-image": "none", + "opacity": "1", + "visibility": "visible", + "font-family": "\"UF Test Sans\"", + "font-size": "13.3333px", + "font-weight": "400", + "font-style": "normal", + "line-height": "normal", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-align": "start", + "text-decoration-line": "none", + "text-transform": "none", + "text-shadow": "none", + "white-space": "normal", + "vertical-align": "baseline", + "overflow-x": "clip", + "overflow-y": "clip", + "z-index": "auto", + "transform": "none", + "box-shadow": "none", + "outline-style": "none", + "outline-width": "3px", + "outline-color": "rgb(0, 0, 0)", + "outline-offset": "0px", + "list-style-type": "disc", + "flex-direction": "row", + "flex-wrap": "nowrap", + "justify-content": "normal", + "align-items": "normal", + "row-gap": "normal", + "column-gap": "normal", + "grid-template-columns": "none", + "grid-template-rows": "none" + } + } + } +} diff --git a/tests/integration/cases/types/utility-types/__expected__/ssr.every-member.html b/tests/integration/cases/types/utility-types/__expected__/ssr.every-member.html new file mode 100644 index 000000000..49f9ff34d --- /dev/null +++ b/tests/integration/cases/types/utility-types/__expected__/ssr.every-member.html @@ -0,0 +1,4 @@ + diff --git a/tests/integration/cases/types/utility-types/__expected__/ssr.required-only.html b/tests/integration/cases/types/utility-types/__expected__/ssr.required-only.html new file mode 100644 index 000000000..640477ecd --- /dev/null +++ b/tests/integration/cases/types/utility-types/__expected__/ssr.required-only.html @@ -0,0 +1,4 @@ + diff --git a/tests/integration/cases/types/utility-types/__output__/angular/text-field.ts b/tests/integration/cases/types/utility-types/__output__/angular/text-field.ts new file mode 100644 index 000000000..f6b80bda2 --- /dev/null +++ b/tests/integration/cases/types/utility-types/__output__/angular/text-field.ts @@ -0,0 +1,68 @@ +import { + Component, + Directive, + ElementRef, + type OnInit, + Renderer2, + inject, + input, +} from "@angular/core"; + +export type TextFieldProps = Required> & + Readonly>>; + +/** The scope attributes taken off each host, for the roots its component renders later. */ +const scopes = new WeakMap(); + +/** + * Gives a root node of its component the scope attributes the component's styled parents put on + * its host (ADR-0063), and takes them off the host, which keeps only `display: contents`. + */ +@Directive({ selector: "[ufScope]" }) +export class ScopeAttributes implements OnInit { + readonly ufScope = input.required(); + private readonly element = inject>(ElementRef); + private readonly renderer = inject(Renderer2); + + ngOnInit(): void { + const host = this.ufScope(); + let names = scopes.get(host); + if (!names) { + names = host.getAttributeNames().filter((name) => name.startsWith("data-uf-")); + for (const name of names) this.renderer.removeAttribute(host, name); + scopes.set(host, names); + } + for (const name of names) this.renderer.setAttribute(this.element.nativeElement, name, ""); + } +} + +@Component({ + selector: "uf-text-field", + imports: [ScopeAttributes], + host: { "[attr.class]": "null", "[attr.style]": "'display: contents;'" }, + preserveWhitespaces: false, + template: ` + @let name = this.name(); + @let label = this.label(); + @let placeholder = this.placeholder(); + @let required = this.required(); + + `, +}) +export default class TextField { + readonly name = input.required(); + readonly label = input.required(); + readonly placeholder = input(); + readonly required = input(); + readonly class = input(); + readonly style = input>(); + protected readonly host = inject>(ElementRef).nativeElement; +} diff --git a/tests/integration/cases/types/utility-types/__output__/angular/types.ts b/tests/integration/cases/types/utility-types/__output__/angular/types.ts new file mode 100644 index 000000000..591c2d480 --- /dev/null +++ b/tests/integration/cases/types/utility-types/__output__/angular/types.ts @@ -0,0 +1,7 @@ +// Every option of a field is optional: the component's props pick the ones it requires. +export interface FieldOptions { + name?: string; + label?: string; + placeholder?: string; + required?: boolean; +} diff --git a/tests/integration/cases/types/utility-types/__output__/astro/TextField.astro b/tests/integration/cases/types/utility-types/__output__/astro/TextField.astro new file mode 100644 index 000000000..858d501d3 --- /dev/null +++ b/tests/integration/cases/types/utility-types/__output__/astro/TextField.astro @@ -0,0 +1,24 @@ +--- +export type TextFieldProps = Required> & + Readonly>>; + +interface Props extends TextFieldProps { + class?: string; + style?: Record; +} + +const { name, label, placeholder, required, class: className, style } = Astro.props; + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} +--- + + diff --git a/tests/integration/cases/types/utility-types/__output__/astro/types.ts b/tests/integration/cases/types/utility-types/__output__/astro/types.ts new file mode 100644 index 000000000..591c2d480 --- /dev/null +++ b/tests/integration/cases/types/utility-types/__output__/astro/types.ts @@ -0,0 +1,7 @@ +// Every option of a field is optional: the component's props pick the ones it requires. +export interface FieldOptions { + name?: string; + label?: string; + placeholder?: string; + required?: boolean; +} diff --git a/tests/integration/cases/types/utility-types/__output__/ir.json b/tests/integration/cases/types/utility-types/__output__/ir.json new file mode 100644 index 000000000..63c599823 --- /dev/null +++ b/tests/integration/cases/types/utility-types/__output__/ir.json @@ -0,0 +1,329 @@ +{ + "irVersion": 1, + "file": "types/utility-types/TextField.uf.tsx", + "components": [ + { + "name": "TextField", + "span": { + "start": 252, + "end": 474 + }, + "props": [ + { + "name": "name", + "optional": false, + "type": { + "code": "string", + "span": { + "start": 326, + "end": 340 + } + }, + "binding": "name@288", + "declaredBy": { + "file": "types/utility-types/types.ts", + "declaration": "FieldOptions", + "span": { + "start": 123, + "end": 137 + } + }, + "span": { + "start": 326, + "end": 340 + } + }, + { + "name": "label", + "optional": false, + "type": { + "code": "string", + "span": { + "start": 326, + "end": 340 + } + }, + "binding": "label@294", + "declaredBy": { + "file": "types/utility-types/types.ts", + "declaration": "FieldOptions", + "span": { + "start": 140, + "end": 155 + } + }, + "span": { + "start": 326, + "end": 340 + } + }, + { + "name": "placeholder", + "optional": true, + "type": { + "code": "string", + "span": { + "start": 326, + "end": 340 + } + }, + "binding": "placeholder@301", + "declaredBy": { + "file": "types/utility-types/types.ts", + "declaration": "FieldOptions", + "span": { + "start": 158, + "end": 179 + } + }, + "span": { + "start": 326, + "end": 340 + } + }, + { + "name": "required", + "optional": true, + "type": { + "code": "boolean", + "span": { + "start": 326, + "end": 340 + } + }, + "binding": "required@314", + "declaredBy": { + "file": "types/utility-types/types.ts", + "declaration": "FieldOptions", + "span": { + "start": 182, + "end": 201 + } + }, + "span": { + "start": 326, + "end": 340 + } + } + ], + "propsParameter": { + "form": "destructured", + "type": { + "code": "TextFieldProps", + "span": { + "start": 326, + "end": 340 + } + }, + "span": { + "start": 286, + "end": 340 + } + }, + "types": [ + "TextFieldProps" + ], + "bindings": [ + { + "id": "name@288", + "name": "name", + "kind": "prop", + "span": { + "start": 288, + "end": 292 + } + }, + { + "id": "label@294", + "name": "label", + "kind": "prop", + "span": { + "start": 294, + "end": 299 + } + }, + { + "id": "placeholder@301", + "name": "placeholder", + "kind": "prop", + "span": { + "start": 301, + "end": 312 + } + }, + { + "id": "required@314", + "name": "required", + "kind": "prop", + "span": { + "start": 314, + "end": 322 + } + } + ], + "setup": [], + "render": { + "kind": "Element", + "tag": "label", + "attributes": [], + "children": [ + { + "kind": "Interpolation", + "value": { + "code": "label", + "span": { + "start": 374, + "end": 379 + }, + "refs": [ + { + "kind": "Binding", + "binding": "label@294", + "span": { + "start": 374, + "end": 379 + } + } + ] + }, + "span": { + "start": 373, + "end": 380 + } + }, + { + "kind": "Element", + "tag": "input", + "attributes": [ + { + "kind": "Bound", + "name": "name", + "value": { + "code": "name", + "span": { + "start": 400, + "end": 404 + }, + "refs": [ + { + "kind": "Binding", + "binding": "name@288", + "span": { + "start": 400, + "end": 404 + } + } + ] + }, + "span": { + "start": 394, + "end": 405 + } + }, + { + "kind": "Bound", + "name": "placeholder", + "value": { + "code": "placeholder", + "span": { + "start": 419, + "end": 430 + }, + "refs": [ + { + "kind": "Binding", + "binding": "placeholder@301", + "span": { + "start": 419, + "end": 430 + } + } + ] + }, + "span": { + "start": 406, + "end": 431 + } + }, + { + "kind": "Bound", + "name": "required", + "value": { + "code": "required", + "span": { + "start": 442, + "end": 450 + }, + "refs": [ + { + "kind": "Binding", + "binding": "required@314", + "span": { + "start": 442, + "end": 450 + } + } + ] + }, + "span": { + "start": 432, + "end": 451 + } + } + ], + "children": [], + "span": { + "start": 387, + "end": 454 + } + } + ], + "span": { + "start": 359, + "end": 467 + } + } + } + ], + "exports": [ + { + "kind": "default", + "name": "default", + "local": "TextField", + "span": { + "start": 252, + "end": 474 + } + } + ], + "types": [ + { + "name": "TextFieldProps", + "exported": true, + "code": "type TextFieldProps = Required> &\n Readonly>>;", + "span": { + "start": 121, + "end": 250 + } + } + ], + "typeImports": [ + { + "specifier": "./types", + "names": [ + { + "imported": "FieldOptions", + "local": "FieldOptions", + "span": { + "start": 14, + "end": 26 + } + } + ], + "span": { + "start": 0, + "end": 44 + } + } + ] +} diff --git a/tests/integration/cases/types/utility-types/__output__/qwik/TextField.tsx b/tests/integration/cases/types/utility-types/__output__/qwik/TextField.tsx new file mode 100644 index 000000000..d3e1c209d --- /dev/null +++ b/tests/integration/cases/types/utility-types/__output__/qwik/TextField.tsx @@ -0,0 +1,24 @@ +import { type CSSProperties, type ClassList, component$ } from "@qwik.dev/core"; + +export type TextFieldProps = Required> & + Readonly>>; + +export default component$( + ({ name, label, placeholder, required, class: className, style, ...attributes }) => { + return ( + + ); + }, +); + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/types/utility-types/__output__/qwik/types.ts b/tests/integration/cases/types/utility-types/__output__/qwik/types.ts new file mode 100644 index 000000000..591c2d480 --- /dev/null +++ b/tests/integration/cases/types/utility-types/__output__/qwik/types.ts @@ -0,0 +1,7 @@ +// Every option of a field is optional: the component's props pick the ones it requires. +export interface FieldOptions { + name?: string; + label?: string; + placeholder?: string; + required?: boolean; +} diff --git a/tests/integration/cases/types/utility-types/__output__/react/TextField.tsx b/tests/integration/cases/types/utility-types/__output__/react/TextField.tsx new file mode 100644 index 000000000..f4e6cc627 --- /dev/null +++ b/tests/integration/cases/types/utility-types/__output__/react/TextField.tsx @@ -0,0 +1,35 @@ +import type { CSSProperties } from "react"; + +export type TextFieldProps = Required> & + Readonly>>; + +export interface TextFieldAttributes { + className?: string; + style?: CSSProperties; +} + +export default function TextField({ + name, + label, + placeholder, + required, + className, + style, + ...attributes +}: TextFieldProps & TextFieldAttributes) { + return ( + + ); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const key of Object.keys(props)) { + if (key.startsWith("data-uf-")) scope[key] = ""; + } + return scope; +} diff --git a/tests/integration/cases/types/utility-types/__output__/react/types.ts b/tests/integration/cases/types/utility-types/__output__/react/types.ts new file mode 100644 index 000000000..591c2d480 --- /dev/null +++ b/tests/integration/cases/types/utility-types/__output__/react/types.ts @@ -0,0 +1,7 @@ +// Every option of a field is optional: the component's props pick the ones it requires. +export interface FieldOptions { + name?: string; + label?: string; + placeholder?: string; + required?: boolean; +} diff --git a/tests/integration/cases/types/utility-types/__output__/solid/TextField.tsx b/tests/integration/cases/types/utility-types/__output__/solid/TextField.tsx new file mode 100644 index 000000000..b844a8c91 --- /dev/null +++ b/tests/integration/cases/types/utility-types/__output__/solid/TextField.tsx @@ -0,0 +1,22 @@ +import type { JSX } from "solid-js"; + +export type TextFieldProps = Required> & + Readonly>>; + +export default function TextField( + props: TextFieldProps & { class?: string; style?: JSX.CSSProperties }, +) { + return ( + + ); +} + +/** The scope attributes of the styled parents that render this component, for its root. */ +function scopeAttributes(props: object): Record { + const scope: Record = {}; + for (const name of Object.keys(props)) if (name.startsWith("data-uf-")) scope[name] = ""; + return scope; +} diff --git a/tests/integration/cases/types/utility-types/__output__/solid/types.ts b/tests/integration/cases/types/utility-types/__output__/solid/types.ts new file mode 100644 index 000000000..591c2d480 --- /dev/null +++ b/tests/integration/cases/types/utility-types/__output__/solid/types.ts @@ -0,0 +1,7 @@ +// Every option of a field is optional: the component's props pick the ones it requires. +export interface FieldOptions { + name?: string; + label?: string; + placeholder?: string; + required?: boolean; +} diff --git a/tests/integration/cases/types/utility-types/__output__/svelte/TextField.svelte b/tests/integration/cases/types/utility-types/__output__/svelte/TextField.svelte new file mode 100644 index 000000000..a8adc1449 --- /dev/null +++ b/tests/integration/cases/types/utility-types/__output__/svelte/TextField.svelte @@ -0,0 +1,33 @@ + + + + + diff --git a/tests/integration/cases/types/utility-types/__output__/svelte/types.ts b/tests/integration/cases/types/utility-types/__output__/svelte/types.ts new file mode 100644 index 000000000..591c2d480 --- /dev/null +++ b/tests/integration/cases/types/utility-types/__output__/svelte/types.ts @@ -0,0 +1,7 @@ +// Every option of a field is optional: the component's props pick the ones it requires. +export interface FieldOptions { + name?: string; + label?: string; + placeholder?: string; + required?: boolean; +} diff --git a/tests/integration/cases/types/utility-types/__output__/vue/TextField.vue b/tests/integration/cases/types/utility-types/__output__/vue/TextField.vue new file mode 100644 index 000000000..c968c345c --- /dev/null +++ b/tests/integration/cases/types/utility-types/__output__/vue/TextField.vue @@ -0,0 +1,17 @@ + + + diff --git a/tests/integration/cases/types/utility-types/__output__/vue/types.ts b/tests/integration/cases/types/utility-types/__output__/vue/types.ts new file mode 100644 index 000000000..591c2d480 --- /dev/null +++ b/tests/integration/cases/types/utility-types/__output__/vue/types.ts @@ -0,0 +1,7 @@ +// Every option of a field is optional: the component's props pick the ones it requires. +export interface FieldOptions { + name?: string; + label?: string; + placeholder?: string; + required?: boolean; +} diff --git a/tests/integration/cases/types/utility-types/__screenshots__/every-member-chromium-linux.png b/tests/integration/cases/types/utility-types/__screenshots__/every-member-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..f505f84d973d164c6f23c5967ce3d2ccf5b8b52b GIT binary patch literal 3004 zcmV;t3q$mYP)=GLL?u|!$Y9mggT2JC^5H-X>jy6+;5>v&0vob^NWw0hQ?d}l1)9S3n!VEN zmOeB~?;#H<=Y=5AU@$ttW0}ST3XZ-fcX#fe}>=t7%uglq;3VR_; zvx7B&X?D)=Lc8L0WO89!f9W9QiA19#9t!+=^zFHkp2mv2 zEi6xspt8d^Je4}`p?1V*q$e1#=oy(x74)MZJ&}dNo0@1|#;8c~nScE4lP_oH6Pau# z5&P=1PyYU8Y`vs`D*;qW#d6Is0OpeUg=?dhPx@|Ilu|98z`($+l$jhJPPG&y;Ohwv zv`DE3=wK={WlbQvnaP2kN87%k;ox;jM{*eG5vNlN5f5br5hdRl{>P{(3ZyufjovTA zic$ldi$i5YtEnn9iDSdLRb6Lzo*@rPCc|@9$xzl~Pm)!4?_a{5u7iOVsbXa*5sQKQ zqn-TWgn;5IiV9E|&I+qlN)^*~5c7D+YD@*1fM%V%?Wh4J!x0r(t`K~ifG$-lvy_2(scHQLq;^dniImeIMS%e$)$KsF$e-hrEc80AHKx{ zxDlT(3gJkfi>T$6X49Z|>@IIq(8qY*U|5>aR22xJ@w*p@Px?IR8B`UZvr^(+>=43XCc(NoM<> zCqr#E)NwbL9>>#?-+7BYC5lm%H&m`8V0_3=q!#QE&aKs9k;imRl(mp(-Osq#;veK~S8Nv-&g1axBBLH+ZW+ z*xzG_5^Q*Vf?$P!m#a6P6FP--Tvni8IB2b*=4IqkXt*WU8)D}ftyHXN!qHp-mArzc z9-$rsFhb}<>uf{W*n&2Hz*(l5f?deiZBl=nTCc|i&Q0>tWgKggyDFi-ZyB@ z+sb4!x<4fB8*emLFD1k<{3x(|$uMvP` zy?#Esz9BQUD&y_81~U!y`Fs@(yp3_tRI3nBoMpq%`!~(~j&16i4y?~4NzjCKn4l<9 zQ))V}k_#QGx`}$Pq3f{Tb!da8S?}%0P}@m&Y?%`g+TSCk)pR@G??#DkKE1B+p@|6} zYJyQ&e;mz$i(^7{zY&dQpsSNdecbB0*Z4qqtcyEka3*PVg08|t1#7jM$=E1{qm8_Z zW*cVzxtt7*c_rtpjlS63LHWRO#O>GG$D$lQ4<#>8M^k9C4cVfH6I0*6jsz^TBLtjg zQIx7|yU&sU$++7&C?(g5&{{l^*U}Q?y-xJ989R&8(sZr_HCWorZSD6GieqVrCJF)# z=R`5N`fO9)TwhJgs#?w$wjPp3C4*1)M16Ek1Ma_F;GO2>0#;9ev$ z+RNkK>AMf_e0ThSYb+^6d%%UcK0Gy@8hHOf?;jta(z}29U}9)zssLUG#^raKvw8N8 zV6Ts9BD_=}8Hq@01lkd+()(%seVonQI(>y2*#d*de05_KOyvVxPyW!Dv< zuls(OUl+x6m2B-A9aXdG^lF(4^mm~o642hzkTM^C`p%N*Up*6L>#U7-a|EL z;P30$coJW|F*elJ*(Szjzt}+QGBnF`_G`6^-wr{cEJ0bC`sC?;<8OOWesW}nXjj)z zFI-H1{l!ZC_F9ACkmd1f>W{KqV>-+>8{_RC>X=`AI#q8fd!u*AdH^H{4D~AL#Lre5 zhl8PhYsu$|Y=B%@-nx|kGlv|IRM-IykMCgV=u}?yXRc^ajxMQP<6+-RgBn_(R}GrA zoxj43Heu1?QJL?)8|plFU_|=^6-d~4?wq)2)gA34u>9ff#YI5IX*Qa3-j8%qYUOAz ztGFSAas;B9S1rb+&>tlPUoeeM%+X&dU+7YIt*T<%}I zrbt*s+R(l(BM<%t)3E*Oie_&u^?7^<93ie~HWd)@9|w}wG-zM+qVuA(f< zWwly{Xtm`=htthpvRx)=mUHpk6@To_#T9=U$ok(K_k-UGnsfWm5BMEn+Ct$rfVo28 z@6ceHowu&w|6a%6smJ`boc#^;tEd4C<}$%v2-ECf4PcrbtN~23gEfF@cCZF8%?{Q8 yrrE(7z%)Dm0{{U3|0B}->;M1&21!IgR09Aj+EIR0Qp1@50000PlRR+p6xV zE99n0D#~4xy9+b>f9e)ym=pZ$dn~ZOSS$#D#-0%Xy&(gjJ7fTKhYW!3kO9ygG61?m z20(Yn0O$@G0Nw4A@i{v?BLJIoad9!QcXS!(0xH^MI0db@f$9MYh8*OuI+>K*^RD zITw-u?AVH)_~6)2dCDea8z_r%MgX4H1N(ulPV&7lE3+vxC&t9~XH~1DD3YKXQ`=9< z`!!TsrJ>85o*N{kPTo;u-Npo9|d+_Q7K`!Y?_x%U|9rU1Xit)&6}F@ z<}L%$_I&52izSEDvX;MyF`<(~YTgd1Nev~J-86NA#JSn5WFsohTltf;xcI|ZJgO&N zt5sGl&u3X)mi*iGyi3l}!JJ?UfbrP+8d;Nv{%P7+(~N)r@oTXVS#W$n07hfm&kL!x z7V_AS;=r9O$3ir70x%k@dScmj{lNXAr(4)^%=Om`3~F1($6}UxX2?uc#na?!PF@m# zu~;<^Twi`TxxByO`P(mb>XNmYeljb))AQFa&b!gktM}74Z6XN3SnTcOJ9AwHREw5S z%l>og=%4WHG;G}PNwu5z2MlUC6gPull<@2{=nfeG-5~>@yS)*9dmZo8!)6WNWf@}z z0Cowr5a { + it("renders the required members", async () => { + const view = await mountScenario(TextField, "required-only"); + await view.expectParity("required-only"); + await expect + .element(view.getByRole("textbox", { name: "Email" })) + .toHaveAttribute("name", "email"); + }); + + it("renders the optional members it is given", async () => { + const view = await mountScenario(TextField, "every-member"); + await view.expectParity("every-member"); + await expect + .element(view.getByRole("textbox", { name: "Email" })) + .toHaveAttribute("placeholder", "you@example.com"); + await expect.element(view.getByRole("textbox", { name: "Email" })).toBeRequired(); + }); +}); diff --git a/tests/integration/harness/canaries.ts b/tests/integration/harness/canaries.ts index 7d81b44e9..1c8df5a99 100644 --- a/tests/integration/harness/canaries.ts +++ b/tests/integration/harness/canaries.ts @@ -510,10 +510,11 @@ export const CANARIES: readonly Canary[] = [ component.render = createElement("div", [], [], component.render.span); // A loop variable is bound by its list, which is gone, and a setup binding by its // setup. The events stay declared: Angular's adapter binds each to its output, which - // must exist, and Vue's would pass an undeclared listener on to the root. + // must exist, and Vue's would pass an undeclared listener on to the root. So do the + // slots, whose declaration names its binding. component.setup = []; component.bindings = component.bindings.filter( - ({ kind }) => kind === "prop" || kind === "emit", + ({ kind }) => kind === "prop" || kind === "emit" || kind === "slots", ); } return copy; @@ -1249,13 +1250,20 @@ export function mismatchClosingTag(file: OutputFile): string { } /** - * An output whose props and events a consumer sees as `any`: the type each target's checker - * reads them from, the component's props parameter (React, Solid), its `$props()` (Svelte), its - * `Props` (Astro), or the type arguments of `component$` (Qwik), `defineProps`, `defineModel`, - * `defineEmits` and `defineSlots` (Vue), and `input`, `output` and `model` (Angular). An output - * with none, a component without props or events, is left as it is. + * An output whose props, events and exported types a consumer sees as `any`: the type each + * target's checker reads the props and events from, the component's props parameter (React, + * Solid), its `$props()` (Svelte), its `Props` (Astro), or the type arguments of `component$` + * (Qwik), `defineProps`, `defineModel`, `defineEmits` and `defineSlots` (Vue), and `input`, + * `output` and `model` (Angular); and every exported `interface` and type alias, in any output + * file, a copied `.ts` module included, so a consumer's misuse of an exported type goes unmet + * too (ADR-0068). An output with none of them is left as it is. */ function widenPublicTypes(file: OutputFile, target: string): string { + return anyExportedTypes(widenProps(file, target)); +} + +/** {@link widenPublicTypes}'s props and events. */ +function widenProps(file: OutputFile, target: string): string { const { contents } = file; switch (target) { case "react": @@ -1309,6 +1317,80 @@ function widenPublicTypes(file: OutputFile, target: string): string { } } +/** + * `contents` with each exported `interface` and type alias declared `any`, its type parameters + * kept, so every reference to it still checks: `export type ButtonProps = any;`, + * `export type ListProps = any;`. A type re-export, `export type { Tone } from "./types"`, + * declares each name it exports `any` in its place: the `.ts` module it names is copied into the + * output tree beside the outputs, where no output hook reaches (ADR-0070). + */ +export function anyExportedTypes(contents: string): string { + contents = contents.replace( + /\bexport\s+type\s*\{([^}]*)\}\s*from\s*(["'])[^"']*\2;?/g, + (_, names: string) => + names + .split(",") + .map((name) => + name + .trim() + .split(/\s+as\s+/) + .at(-1)!, + ) + .filter(Boolean) + .map((name) => `export type ${name} = any;`) + .join("\n"), + ); + let result = ""; + let from = 0; + for (const match of contents.matchAll(/\bexport\s+(interface|type)\s+([A-Za-z_$][\w$]*)/g)) { + if (match.index < from) continue; + let index = match.index + match[0].length; + while (/\s/.test(contents[index] ?? "")) index += 1; + let parameters = ""; + if (contents[index] === "<") { + const close = matchingBracket(contents, index, "<", ">"); + if (close === undefined) continue; + parameters = contents.slice(index, close + 1); + index = close + 1; + } + const end = + match[1] === "interface" + ? interfaceEnd(contents, index) + : statementEnd(contents, contents.indexOf("=", index)); + if (end === undefined) continue; + result += `${contents.slice(from, match.index)}export type ${match[2]}${parameters} = any;`; + from = end + 1; + } + return result + contents.slice(from); +} + +/** The index of the `}` that ends an interface whose header ends at `from`. */ +function interfaceEnd(contents: string, from: number): number | undefined { + const open = contents.indexOf("{", from); + return open === -1 ? undefined : matchingBracket(contents, open, "{", "}"); +} + +/** The index of the `;` that ends a type alias whose `=` is at `from`, outside any bracket. */ +function statementEnd(contents: string, from: number): number | undefined { + if (from === -1) return undefined; + let depth = 0; + for (let index = from + 1; index < contents.length; index += 1) { + const char = contents[index]!; + if (char === '"' || char === "'" || char === "`") { + const end = endOfString(contents, index); + if (end === undefined) return undefined; + index = end; + } else if ("{([<".includes(char)) { + depth += 1; + } else if ("})]".includes(char) || (char === ">" && contents[index - 1] !== "=")) { + depth -= 1; + } else if (char === ";" && depth === 0) { + return index; + } + } + return undefined; +} + /** `contents` with the type arguments after each match of `opening` (which ends at `<`) `any`. */ function anyTypeArguments(contents: string, opening: RegExp, type = "any"): string { let result = ""; diff --git a/tests/integration/harness/canaries.unit.test.ts b/tests/integration/harness/canaries.unit.test.ts index 1a0ca9163..656cb06c8 100644 --- a/tests/integration/harness/canaries.unit.test.ts +++ b/tests/integration/harness/canaries.unit.test.ts @@ -26,6 +26,7 @@ import { } from "../scripts/canaries.ts"; import { addRootAttribute, + anyExportedTypes, CANARIES, canaryCase, dropFirstRule, @@ -774,8 +775,9 @@ describe("canaries", () => { it("L4-consumer types each target's props `any` where its consumers' checker reads them", async () => { const result = await compileWith("L4-consumer"); + // The exported types are widened too, as the test of `types/exported-types` proves. const lines = (target: string) => - result.outputs[target]![0]!.contents.split("\n").filter((line) => /\bany\b/.test(line)); + result.outputs[target]![0]!.contents.split("\n").filter(widenedProps); expect(lines("react")).toEqual(["}: any) {"]); expect(result.outputs.react![0]!.contents).toMatch( /^export default function TaskList\(\{\n[\s\S]*?\n\}: any\) \{$/m, @@ -810,7 +812,7 @@ describe("canaries", () => { targets: [target], plugins: canaryPlugins("L4-consumer", target), }); - return outputs[target]![0]!.contents.split("\n").filter((line) => /\bany\b/.test(line)); + return outputs[target]![0]!.contents.split("\n").filter(widenedProps); }; expect(await lines("react")).toEqual(["}: any) {"]); const { outputs: react } = await compile(readFileSync(info.source, "utf8"), { @@ -859,6 +861,47 @@ describe("canaries", () => { ]); }); + it("L4-consumer types a module's exported types `any`, on a case whose consumers misuse one", async () => { + // The type directives of `types/exported-types` (ADR-0068): the props type and a type no + // component reaches, each declared `any` where the source declares it. + const info = listCases().find(({ id }) => id === "types/exported-types")!; + const { outputs } = await compile(readFileSync(info.source, "utf8"), { + filename: info.filename, + targets: ["vue"], + plugins: canaryPlugins("L4-consumer", "vue"), + }); + expect(outputs.vue![0]!.contents.split("\n").filter((line) => /\bany\b/.test(line))).toEqual([ + "export type ButtonTone = any;", + "export type ButtonSize = any;", + "export type ButtonProps = any;", + 'const { label, tone = "primary" } = defineProps>();', + ]); + }); + + it("widens an exported interface, a type alias and a type re-export, keeping type parameters", () => { + const widened = anyExportedTypes( + [ + 'import type { Item } from "./types";', + 'export type { Tone, Size as ButtonSize } from "./types";', + "export interface ListProps extends Base {", + " items: T[];", + " pick?: (item: T) => void;", + "}", + 'export type Picked = Pick &', + " Readonly<{ select: () => void }>;", + "type Local = string;", + ].join("\n"), + ); + expect(widened.split("\n")).toEqual([ + 'import type { Item } from "./types";', + "export type Tone = any;", + "export type ButtonSize = any;", + "export type ListProps = any;", + "export type Picked = any;", + "type Local = string;", + ]); + }); + it("L4-consumer applies to the cases with consumer fixtures, on each target that has them", () => { const canary = findCanary("L4-consumer"); const [withFixtures, without] = ["events/emit-payloads", "basics/hello"].map((id) => @@ -1448,3 +1491,8 @@ describe("every canary on the corpus", () => { }, ); }); + +/** A line the L4-consumer canary widened that is not an exported type's declaration. */ +function widenedProps(line: string): boolean { + return /\bany\b/.test(line) && !/^\s*export type [\w$]+(?:<.*>)? = any;$/.test(line); +} diff --git a/tests/integration/harness/compile.test.ts b/tests/integration/harness/compile.test.ts index f5f43461e..1f452e17f 100644 --- a/tests/integration/harness/compile.test.ts +++ b/tests/integration/harness/compile.test.ts @@ -16,6 +16,7 @@ import { checkLayers, diffLines, NO_OUTPUT_SKIP, + NO_SPEC_SKIP, settleArtefact, settleArtefactDirectory, } from "@unframework/testing/node"; @@ -61,6 +62,12 @@ const NO_OUTPUT_LAYERS: readonly LayerName[] = [ "L13", ]; +/** + * The layers only a browser project records, which none records for a case without a spec: a + * diagnostics case its target has output for (a capability's note, UF4004) skips them. + */ +const NO_SPEC_LAYERS: readonly LayerName[] = ["L7", "L8", "L9", "L10", "L11"]; + const validateIr = new Ajv({ allErrors: true, strict: true }).compile(irSchema); /** @@ -147,6 +154,8 @@ describe("compile", () => { }; if (hasErrors(caseDiagnostics(firsts(compiles)), target)) { for (const layer of NO_OUTPUT_LAYERS) checks[layer] = () => ({ skip: NO_OUTPUT_SKIP }); + } else if (!info.spec) { + for (const layer of NO_SPEC_LAYERS) checks[layer] = () => ({ skip: NO_SPEC_SKIP }); } await checkLayers( task, diff --git a/tests/integration/harness/coverage-exemptions.ts b/tests/integration/harness/coverage-exemptions.ts index c10a6072c..4e5be531c 100644 --- a/tests/integration/harness/coverage-exemptions.ts +++ b/tests/integration/harness/coverage-exemptions.ts @@ -31,7 +31,6 @@ function lanesBut( // M5's contract lands before what lowers and emits it (ADR-0067 to ADR-0070): each entry names // the layer of M5 whose case removes it. -const TYPES = "The types and generics cases land in layer 14 of M5."; const COMPOSABLES = "No case uses a composable or a module value until every target emits them: the composable and module cases land in layer 38 of M5 (ADR-0069)."; @@ -69,8 +68,6 @@ export const EXEMPT_CODES: Readonly> = { // Layer 8 of M5 resolves child APIs and the corpus's types through the oracle. // Layer 10 of M5 imports shared modules. // Layer 14 of M5 emits cross-file types and generics on Vue. - UF4004: - "The capability check reports it at a generic scoped slot, and the case lands with the generics cases in layer 14 of M5.", // Layer 32 of M5 builds the project graph. UF1205: "The project graph reports an import cycle, and the case lands in layer 32 of M5.", // Layer 38 of M5 lands the composable cases. @@ -103,8 +100,6 @@ export const EXEMPT_CAPABILITIES: Readonly< context: lanesBut("astro"), "reactive-context": lanesBut("astro"), "dynamic-component": lanesBut(), - "generic-components": TYPES, - "generic-slot-props": TYPES, composables: COMPOSABLES, "composable-effect-order": COMPOSABLES, }; diff --git a/tests/integration/harness/no-output.ts b/tests/integration/harness/no-output.ts index 229eed998..b560ca5df 100644 --- a/tests/integration/harness/no-output.ts +++ b/tests/integration/harness/no-output.ts @@ -7,8 +7,16 @@ // fails, so the spec loads, and the setup skips each of its tests on that target with the // errors the case expects (`no output: …`, a mechanical cause the summary excuses there alone). // The stand-in throws them if anything renders it. +// +// A target may also have output it cannot compile yet: a case whose L3 is quarantined there, +// where the target's browser build type-checks its output, so the component's own module would +// fail its spec's import and record no cell to quarantine. Its spec loads the same stand-in, but +// its tests are not skipped: the setup records every browser layer as failed with the stand-in's +// cause, so the case's browser entries quarantine real failures, and go stale once the target +// compiles the case and the L3 entry goes (ADR-0068, amending ADR-0050). import { dirname, resolve } from "node:path"; +import type { QuarantineEntry } from "@unframework/testing/node"; import type { Plugin } from "vite"; import { errorState, expectedDiagnostics } from "./cases.ts"; @@ -37,16 +45,67 @@ export function noOutputCases(cases: readonly CaseInfo[], target: string): Recor } /** - * The Vite plugin that serves the stand-ins: before the unplugin (both run `pre`, in order), it - * resolves a spec's relative import of such a case's `.uf.tsx` to the stand-in's id. During the - * dependency scan it leaves the import to the unplugin, as for any other case. + * The targets whose browser build type-checks its output, so a component that fails L3 fails its + * spec's import there: Angular's, through ngtsc (ADR-0027). Every other target's build erases the + * types, and runs the output L3 rejects. */ -export function noOutputPlugin( +const TYPE_CHECKED_BUILDS: ReadonlySet = new Set(["angular"]); + +/** + * The cases with a spec that a target cannot compile yet (ADR-0068), by id, each with the cause + * every browser layer of its tests fails with: the target's browser build type-checks its output, + * and the quarantine holds an L3 entry for the case there. None on any other target, and none + * once the entry is gone. + */ +export function standInCases( cases: readonly CaseInfo[], + target: string, + quarantine: readonly QuarantineEntry[], +): Record { + const found: Record = {}; + if (!TYPE_CHECKED_BUILDS.has(target)) return found; + for (const info of cases) { + const entry = quarantine.find( + (each) => each.case === info.id && each.target === target && each.layer === "L3", + ); + if (!info.spec || !entry) continue; + found[info.id] = + `${target} cannot compile this case yet, so its spec runs against a stand-in: its L3 is quarantined for ${entry.issue} (${entry.reason}), and its browser build type-checks the output.`; + } + return found; +} + +/** + * Why each case's spec gets a stand-in on a target, by id: a case the target has no output for + * (`noOutputCases`), and one it cannot compile yet (`standInCases`). + */ +export function standInReasons( noOutput: Readonly>, + standIns: Readonly>, +): Record { + return { + ...Object.fromEntries( + Object.entries(noOutput).map(([id, errors]) => [ + id, + `${id} has no output for this target: it expects ${errors}.`, + ]), + ), + ...standIns, + }; +} + +/** + * The Vite plugin that serves the stand-ins, each with its reason (`standInReasons`): before the + * unplugin (both run `pre`, in order), it resolves a spec's relative import of such a case's + * `.uf.tsx` to the stand-in's id. During the dependency scan it leaves the import to the + * unplugin, as for any other case. + */ +export function noOutputPlugin( + cases: readonly CaseInfo[], + reasons: Readonly>, ): Plugin { const byFile = new Map( - cases.filter((info) => Object.hasOwn(noOutput, info.id)).map((info) => [info.source, info.id]), + cases.filter((info) => Object.hasOwn(reasons, info.id)).map((info) => [info.source, info.id]), ); return { name: "uf-harness:no-output", @@ -61,10 +120,8 @@ export function noOutputPlugin( }, load(id) { if (!id.startsWith(NO_OUTPUT_PREFIX)) return null; - const caseId = id.slice(NO_OUTPUT_PREFIX.length); - const errors = noOutput[caseId]; - if (errors === undefined) return null; - return standIn(`${caseId} has no output for this target: it expects ${errors}.`); + const reason = reasons[id.slice(NO_OUTPUT_PREFIX.length)]; + return reason === undefined ? null : standIn(reason); }, }; } diff --git a/tests/integration/harness/no-output.unit.test.ts b/tests/integration/harness/no-output.unit.test.ts index efe5c7e9a..15dc3f894 100644 --- a/tests/integration/harness/no-output.unit.test.ts +++ b/tests/integration/harness/no-output.unit.test.ts @@ -1,14 +1,31 @@ // The browser projects' stand-ins (no-output.ts), on a corpus of their own: which cases a target -// has no output for, the import a stand-in replaces, and what the stand-in does. +// has no output for or cannot compile yet, the import a stand-in replaces, what the stand-in +// does, and what a test run against one records and how the summary judges it (ADR-0068). import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; +import { + BROWSER_LAYERS, + buildPartialMatrix, + recordLayer, + recordStandIn, + summarise, +} from "@unframework/testing/node"; +import type { LayerTask, QuarantineEntry, UfLayerMeta } from "@unframework/testing/node"; import type { Plugin } from "vite"; import { afterEach, describe, expect, it } from "vitest"; import { listCases } from "./cases.ts"; -import { NO_OUTPUT_PREFIX, noOutputCases, noOutputPlugin, standIn } from "./no-output.ts"; +import { + NO_OUTPUT_PREFIX, + noOutputCases, + noOutputPlugin, + standIn, + standInCases, + standInReasons, +} from "./no-output.ts"; +import { QUARANTINE } from "./quarantine.ts"; let corpus: string | undefined; afterEach(() => { @@ -109,7 +126,7 @@ describe("noOutputPlugin", () => { it("resolves a spec's import of such a component to a stand-in that loads, and nothing else", () => { const dir = corpusOfFour(); const cases = listCases(dir); - const plugin = noOutputPlugin(cases, noOutputCases(cases, "qwik")); + const plugin = noOutputPlugin(cases, standInReasons(noOutputCases(cases, "qwik"), {})); const importer = join(dir, "events/declared/declared.test.ts"); expect(plugin.enforce).toBe("pre"); expect(resolveWith(plugin, "./Box.uf.tsx", importer)).toBe( @@ -145,3 +162,151 @@ describe("noOutputPlugin", () => { expect(() => module.default()).toThrow(reason); }); }); + +/** An L3 entry for a case on a target, as a target's quarantine file holds it. */ +const l3 = (caseId: string, target: string): QuarantineEntry => ({ + case: caseId, + target, + layer: "L3", + reason: "imports no type yet", + issue: "UXF-0", +}); + +describe("standInCases", () => { + it("gives a case with an Angular L3 entry the stand-in on Angular, and none once it is gone", () => { + const cases = listCases(corpusOfFour()); + const found = standInCases(cases, "angular", [l3("basics/clean", "angular")]); + expect(found).toEqual({ + "basics/clean": + "angular cannot compile this case yet, so its spec runs against a stand-in: its L3 is quarantined for UXF-0 (imports no type yet), and its browser build type-checks the output.", + }); + expect(standInCases(cases, "angular", [])).toEqual({}); + // Another layer's entry, or another target's, leaves the case's own output. + expect( + standInCases(cases, "angular", [ + { ...l3("basics/clean", "angular"), layer: "L4" }, + l3("basics/clean", "react"), + ]), + ).toEqual({}); + }); + + it("gives none on a target whose browser build erases the types (React)", () => { + const cases = listCases(corpusOfFour()); + expect(standInCases(cases, "react", [l3("basics/clean", "react")])).toEqual({}); + }); + + it("leaves out a case without a spec, which no browser project loads", () => { + const cases = listCases(corpusOfFour()); + expect(standInCases(cases, "angular", [l3("diagnostics/broken", "angular")])).toEqual({}); + }); + + it("serves its stand-in with its own cause, never the no-output message", () => { + const dir = corpusOfFour(); + const cases = listCases(dir); + const standIns = standInCases(cases, "angular", [l3("basics/clean", "angular")]); + const plugin = noOutputPlugin(cases, standInReasons(noOutputCases(cases, "angular"), standIns)); + expect(resolveWith(plugin, "./Box.uf.tsx", join(dir, "basics/clean/clean.test.ts"))).toBe( + `${NO_OUTPUT_PREFIX}basics/clean`, + ); + expect(loadWith(plugin, `${NO_OUTPUT_PREFIX}basics/clean`)).toBe( + standIn(standIns["basics/clean"]!), + ); + }); + + it("stands in for the corpus's cases whose Angular L3 entry stands", () => { + const ids = Object.keys(standInCases(listCases(), "angular", QUARANTINE)).sort(); + const quarantined = [ + ...new Set( + QUARANTINE.filter((entry) => entry.target === "angular" && entry.layer === "L3").map( + (entry) => entry.case, + ), + ), + ].filter((id) => listCases().find((info) => info.id === id)?.spec); + expect(ids).toEqual(quarantined.sort()); + // Each has an Angular entry for every browser layer it fails, and only those. + for (const id of ids) { + expect( + QUARANTINE.filter((entry) => entry.case === id && entry.target === "angular") + .map((entry) => entry.layer) + .filter((layer) => BROWSER_LAYERS.includes(layer)), + id, + ).toEqual(BROWSER_LAYERS); + } + }); +}); + +describe("a test run against a stand-in", () => { + const cause = "angular cannot compile this case yet (fixture)."; + const subject = (quarantine: readonly QuarantineEntry[] = []) => ({ + case: "basics/clean", + target: "angular", + quarantine, + }); + + it("records every browser layer as failed with the stand-in's cause, and skips none", () => { + const task: LayerTask = { meta: {} }; + recordStandIn(task, subject(), cause); + expect(task.meta.uf).toEqual({ + case: "basics/clean", + target: "angular", + layers: Object.fromEntries( + BROWSER_LAYERS.map((layer) => [layer, { status: "fail", message: cause }]), + ), + standIn: cause, + }); + expect(task.meta.uf?.skipped).toBeUndefined(); + }); + + it("is quarantined layer by layer, and a later pass keeps the cell failed", () => { + const entries = BROWSER_LAYERS.map((layer) => ({ ...l3("basics/clean", "angular"), layer })); + const task: LayerTask = { meta: {} }; + recordStandIn(task, subject(entries), cause); + // The console is clean, and the mount's error is L8's: neither hides the stand-in's failure. + recordLayer(task, subject(entries), "L13", { status: "pass" }); + for (const layer of BROWSER_LAYERS) { + expect(task.meta.uf?.layers[layer], layer).toEqual({ status: "quarantined", issue: "UXF-0" }); + } + }); + + it("is excused from scenario parity with the reference, and its cells still fail", () => { + const reference: UfLayerMeta = { + case: "basics/clean", + target: "vue", + layers: { L7: { status: "pass" } }, + scenarios: ["initial"], + test: "basics/clean > renders", + }; + const task: LayerTask = { meta: {} }; + recordStandIn(task, subject(), cause); + const ours: UfLayerMeta = { ...task.meta.uf!, test: "basics/clean > renders" }; + const matrix = buildPartialMatrix({ + run: "all", + projects: ["browser:vue", "browser:angular"], + records: [ + { project: "browser:vue", record: reference }, + { project: "browser:angular", record: ours }, + ], + finishedAt: "2026-10-28T00:00:00.000Z", + mode: { update: false, pixels: "baseline", canary: null }, + filtered: null, + shard: null, + empty: [], + quarantine: [], + reference: "vue", + }); + const result = summarise([matrix], { + projects: ["browser:vue", "browser:angular"], + cases: ["basics/clean"], + targets: ["vue", "angular"], + liveLayers: ["L7"], + notLiveReason: "not live (fixture)", + }); + expect(result.matrix.tests["basics/clean"]?.angular).toEqual({ + "basics/clean > renders": { scenarios: [], standIn: cause }, + }); + // No scenario problem: only its cells, which its quarantine entries cover in the corpus. + expect(result.problems).toEqual( + BROWSER_LAYERS.map((layer) => `basics/clean › angular › ${layer}: fail: ${cause}`), + ); + }); +}); diff --git a/tests/integration/harness/projects.ts b/tests/integration/harness/projects.ts index 3acb6bad5..12d822a1e 100644 --- a/tests/integration/harness/projects.ts +++ b/tests/integration/harness/projects.ts @@ -41,7 +41,7 @@ import "./context.ts"; import { listCases } from "./cases.ts"; import { ENGINE_DECLARATIONS, enginePlugins } from "./engines.ts"; import { goldenGuard } from "./guard.ts"; -import { noOutputCases, noOutputPlugin } from "./no-output.ts"; +import { noOutputCases, noOutputPlugin, standInCases, standInReasons } from "./no-output.ts"; import { FAILURE_SCREENSHOTS_DIR, ROOT, toolchainDir } from "./paths.ts"; import { loadToolchain, REFERENCE } from "./targets.ts"; @@ -270,15 +270,17 @@ const browserProject: ProjectFactory = async (setup, target, toolchain, unframew const name = `browser:${target}`; const context = toolchainContext(target); const fragment = await toolchain.vite("browser", context); - // The cases this target has no output for: their specs load against a stand-in, so each test - // is skipped by the capability it requires (harness/no-output.ts). + // The cases this target has no output for, or cannot compile yet: their specs load against a + // stand-in, so each test is skipped with the expected errors, or fails every browser layer + // with the stand-in's cause (harness/no-output.ts). const cases = listCases(setup.harness.casesDir); const noOutput = noOutputCases(cases, target); + const standIns = standInCases(cases, target, setup.harness.quarantine); const config = mergeConfig( { root: ROOT, plugins: [ - noOutputPlugin(cases, noOutput), + noOutputPlugin(cases, standInReasons(noOutput, standIns)), noErrorBroadcast(), // The engines' plugins resolve the imports every output keeps as written (ADR-0064). ...(await enginePlugins()), @@ -334,6 +336,7 @@ const browserProject: ProjectFactory = async (setup, target, toolchain, unframew ufHarness: setup.harness, ufCapabilities: builtinTargets[target as TargetName].capabilities, ufNoOutput: noOutput, + ufStandIn: standIns, }, sequence: { groupOrder: groupOrder( diff --git a/tests/integration/harness/quarantine/angular.ts b/tests/integration/harness/quarantine/angular.ts index 14a7f878a..505f72a26 100644 --- a/tests/integration/harness/quarantine/angular.ts +++ b/tests/integration/harness/quarantine/angular.ts @@ -1,6 +1,38 @@ // The Angular quarantine (ADR-0057): Angular's known failures, each with its reason and issue. Only // entries whose target is "angular" belong here, so each target lane edits its own file. -import type { QuarantineEntry } from "@unframework/testing/node"; +import type { LayerName, QuarantineEntry } from "@unframework/testing/node"; -/** Empty: every live layer is green on Angular (ADR-0060). */ -export const QUARANTINE: readonly QuarantineEntry[] = []; +// M5 (UXF-443) +/** + * The browser layers of a case whose L3 entry stands: its spec runs against a stand-in, which + * fails each of them with that cause (`harness/no-output.ts`, ADR-0068). + */ +const STAND_IN: readonly LayerName[] = ["L7", "L8", "L9", "L10", "L11", "L13"]; + +/** + * The types and generics cells that fail until Angular's types lane (layer 23 of M5) emits + * cross-file types and generic components (ADR-0067, ADR-0068): Vue wrote their expectations first + * (layer 14), and the lane empties this list. + */ +const TYPES: Readonly> = { + "diagnostics/generic-slot-props": ["L3", "L4", "L6"], + "generics/constrained": ["L3", "L4", "L6", ...STAND_IN], + "generics/list": ["L3", "L4", "L6", ...STAND_IN], + "generics/scoped-slot": ["L3", "L4", "L6", ...STAND_IN], + "generics/select": ["L3", "L4", "L6", ...STAND_IN], + "types/child-type-import": ["L3", "L4", "L6", ...STAND_IN], + "types/package-types": ["L3", "L4", "L6", ...STAND_IN], + "types/shared-type": ["L3", "L4", "L6", ...STAND_IN], + "types/utility-types": ["L3", "L4", "L6", ...STAND_IN], +}; + +export const QUARANTINE: readonly QuarantineEntry[] = Object.entries(TYPES).flatMap( + ([id, layers]) => + layers.map((layer) => ({ + case: id, + target: "angular", + layer, + reason: "imports no type and declares no type parameter yet", + issue: "UXF-465", + })), +); diff --git a/tests/integration/harness/quarantine/astro.ts b/tests/integration/harness/quarantine/astro.ts index 591a6efa8..0d27da918 100644 --- a/tests/integration/harness/quarantine/astro.ts +++ b/tests/integration/harness/quarantine/astro.ts @@ -1,6 +1,35 @@ // The Astro quarantine (ADR-0057): Astro's known failures, each with its reason and issue. Only // entries whose target is "astro" belong here, so each target lane edits its own file. -import type { QuarantineEntry } from "@unframework/testing/node"; +import type { LayerName, QuarantineEntry } from "@unframework/testing/node"; -/** Empty: every live layer is green on Astro (ADR-0063). */ -export const QUARANTINE: readonly QuarantineEntry[] = []; +// M5 (UXF-443) +/** + * The types and generics cells that fail until Astro's types lane (layer 28 of M5) emits + * cross-file types and generic components (ADR-0067, ADR-0068): Vue wrote their expectations first + * (layer 14), and the lane empties this list. + */ +const TYPES: Readonly> = { + "diagnostics/generic-slot-props": ["L4"], + "generics/constrained": ["L4"], + "generics/list": ["L4"], + "generics/scoped-slot": ["L4"], + "generics/select": ["L4"], + "types/child-type-import": ["L4"], + "types/extends-chain": ["L4"], + "types/imported-interface": ["L4"], + "types/intersection-props": ["L4"], + "types/package-types": ["L4"], + "types/shared-type": ["L4"], + "types/utility-types": ["L4"], +}; + +export const QUARANTINE: readonly QuarantineEntry[] = Object.entries(TYPES).flatMap( + ([id, layers]) => + layers.map((layer) => ({ + case: id, + target: "astro", + layer, + reason: "imports no type and writes no generic form yet", + issue: "UXF-467", + })), +); diff --git a/tests/integration/harness/quarantine/qwik.ts b/tests/integration/harness/quarantine/qwik.ts index cab55a596..1b04186d3 100644 --- a/tests/integration/harness/quarantine/qwik.ts +++ b/tests/integration/harness/quarantine/qwik.ts @@ -1,6 +1,35 @@ // The Qwik quarantine (ADR-0057): Qwik's known failures, each with its reason and issue. Only // entries whose target is "qwik" belong here, so each target lane edits its own file. -import type { QuarantineEntry } from "@unframework/testing/node"; +import type { LayerName, QuarantineEntry } from "@unframework/testing/node"; -/** Empty: every live layer is green on Qwik. */ -export const QUARANTINE: readonly QuarantineEntry[] = []; +// M5 (UXF-443) +/** + * The types and generics cells that fail until Qwik's types lane (layer 25 of M5) emits + * cross-file types and generic components (ADR-0067, ADR-0068): Vue wrote their expectations first + * (layer 14), and the lane empties this list. + */ +const TYPES: Readonly> = { + "diagnostics/generic-slot-props": ["L4"], + "generics/constrained": ["L4"], + "generics/list": ["L4"], + "generics/scoped-slot": ["L4"], + "generics/select": ["L4"], + "types/child-type-import": ["L4"], + "types/extends-chain": ["L4"], + "types/imported-interface": ["L4"], + "types/intersection-props": ["L4"], + "types/package-types": ["L4"], + "types/shared-type": ["L4"], + "types/utility-types": ["L4"], +}; + +export const QUARANTINE: readonly QuarantineEntry[] = Object.entries(TYPES).flatMap( + ([id, layers]) => + layers.map((layer) => ({ + case: id, + target: "qwik", + layer, + reason: "imports no type and writes no generic form yet", + issue: "UXF-466", + })), +); diff --git a/tests/integration/harness/quarantine/react.ts b/tests/integration/harness/quarantine/react.ts index 412075c7f..c91c41499 100644 --- a/tests/integration/harness/quarantine/react.ts +++ b/tests/integration/harness/quarantine/react.ts @@ -1,6 +1,35 @@ // The React quarantine (ADR-0057): React's known failures, each with its reason and issue. Only // entries whose target is "react" belong here, so each target lane edits its own file. -import type { QuarantineEntry } from "@unframework/testing/node"; +import type { LayerName, QuarantineEntry } from "@unframework/testing/node"; -/** Empty: every live layer is green on React (ADR-0060). */ -export const QUARANTINE: readonly QuarantineEntry[] = []; +// M5 (UXF-443) +/** + * The types and generics cells that fail until React's types lane (layer 16 of M5) emits + * cross-file types and generic components (ADR-0067, ADR-0068): Vue wrote their expectations first + * (layer 14), and the lane empties this list. + */ +const TYPES: Readonly> = { + "diagnostics/generic-slot-props": ["L4"], + "generics/constrained": ["L4"], + "generics/list": ["L4"], + "generics/scoped-slot": ["L4"], + "generics/select": ["L4"], + "types/child-type-import": ["L4"], + "types/extends-chain": ["L4"], + "types/imported-interface": ["L4"], + "types/intersection-props": ["L4"], + "types/package-types": ["L4"], + "types/shared-type": ["L4"], + "types/utility-types": ["L4"], +}; + +export const QUARANTINE: readonly QuarantineEntry[] = Object.entries(TYPES).flatMap( + ([id, layers]) => + layers.map((layer) => ({ + case: id, + target: "react", + layer, + reason: "imports no type and writes no generic form yet", + issue: "UXF-462", + })), +); diff --git a/tests/integration/harness/quarantine/solid.ts b/tests/integration/harness/quarantine/solid.ts index 1efdfbefc..d3bf5ff38 100644 --- a/tests/integration/harness/quarantine/solid.ts +++ b/tests/integration/harness/quarantine/solid.ts @@ -1,5 +1,35 @@ // The Solid quarantine (ADR-0057): Solid's known failures, each with its reason and issue. Only // entries whose target is "solid" belong here, so each target lane edits its own file. -import type { QuarantineEntry } from "@unframework/testing/node"; +import type { LayerName, QuarantineEntry } from "@unframework/testing/node"; -export const QUARANTINE: readonly QuarantineEntry[] = []; +// M5 (UXF-443) +/** + * The types and generics cells that fail until Solid's types lane (layer 19 of M5) emits + * cross-file types and generic components (ADR-0067, ADR-0068): Vue wrote their expectations first + * (layer 14), and the lane empties this list. + */ +const TYPES: Readonly> = { + "diagnostics/generic-slot-props": ["L4"], + "generics/constrained": ["L4"], + "generics/list": ["L4"], + "generics/scoped-slot": ["L4"], + "generics/select": ["L4"], + "types/child-type-import": ["L4"], + "types/extends-chain": ["L4"], + "types/imported-interface": ["L4"], + "types/intersection-props": ["L4"], + "types/package-types": ["L4"], + "types/shared-type": ["L4"], + "types/utility-types": ["L4"], +}; + +export const QUARANTINE: readonly QuarantineEntry[] = Object.entries(TYPES).flatMap( + ([id, layers]) => + layers.map((layer) => ({ + case: id, + target: "solid", + layer, + reason: "imports no type and writes no generic form yet", + issue: "UXF-463", + })), +); diff --git a/tests/integration/harness/quarantine/svelte.ts b/tests/integration/harness/quarantine/svelte.ts index e708b2e24..8fd0b66b4 100644 --- a/tests/integration/harness/quarantine/svelte.ts +++ b/tests/integration/harness/quarantine/svelte.ts @@ -1,6 +1,35 @@ // The Svelte quarantine (ADR-0057): Svelte's known failures, each with its reason and issue. Only // entries whose target is "svelte" belong here, so each target lane edits its own file. -import type { QuarantineEntry } from "@unframework/testing/node"; +import type { LayerName, QuarantineEntry } from "@unframework/testing/node"; -/** Empty: every live layer is green on Svelte (ADR-0063). */ -export const QUARANTINE: readonly QuarantineEntry[] = []; +// M5 (UXF-443) +/** + * The types and generics cells that fail until Svelte's types lane (layer 21 of M5) emits + * cross-file types and generic components (ADR-0067, ADR-0068): Vue wrote their expectations first + * (layer 14), and the lane empties this list. + */ +const TYPES: Readonly> = { + "diagnostics/generic-slot-props": ["L4"], + "generics/constrained": ["L4"], + "generics/list": ["L4"], + "generics/scoped-slot": ["L4"], + "generics/select": ["L4"], + "types/child-type-import": ["L4"], + "types/extends-chain": ["L4"], + "types/imported-interface": ["L4"], + "types/intersection-props": ["L4"], + "types/package-types": ["L4"], + "types/shared-type": ["L4"], + "types/utility-types": ["L4"], +}; + +export const QUARANTINE: readonly QuarantineEntry[] = Object.entries(TYPES).flatMap( + ([id, layers]) => + layers.map((layer) => ({ + case: id, + target: "svelte", + layer, + reason: "imports no type and writes no generic form yet", + issue: "UXF-464", + })), +); diff --git a/tests/toolchains/angular/output.oxlintrc.json b/tests/toolchains/angular/output.oxlintrc.json index 725ad3167..ec3ccfc2d 100644 --- a/tests/toolchains/angular/output.oxlintrc.json +++ b/tests/toolchains/angular/output.oxlintrc.json @@ -21,14 +21,16 @@ "rules": { // P7, D11: an output imports only its framework, and (from M3) the outputs beside it, and // (from M4) the `virtual:` modules a styling engine's plugin registers, which it keeps as - // written (plan §4.4, ADR-0064). + // written (plan §4.4, ADR-0064). A type import is erased, so a package's types are imported + // by name, as the source imports them (ADR-0067). "no-restricted-imports": [ "error", { "patterns": [ { "group": ["*", "!@angular/core", "!@angular/common", "!virtual:*", "!./**", "!../**"], - "message": "An output imports only its framework (P7)." + "message": "An output imports only its framework (P7).", + "allowTypeImports": true } ] } diff --git a/tests/toolchains/astro/output.oxlintrc.json b/tests/toolchains/astro/output.oxlintrc.json index 0af14e939..a05a839fe 100644 --- a/tests/toolchains/astro/output.oxlintrc.json +++ b/tests/toolchains/astro/output.oxlintrc.json @@ -21,14 +21,16 @@ "rules": { // P7, D11: an output imports only its framework, and (from M3) the outputs beside it, and // (from M4) the `virtual:` modules a styling engine's plugin registers, which it keeps as - // written (plan §4.4, ADR-0064). + // written (plan §4.4, ADR-0064). A type import is erased, so a package's types are imported + // by name, as the source imports them (ADR-0067). "no-restricted-imports": [ "error", { "patterns": [ { "group": ["*", "!astro", "!astro/*", "!virtual:*", "!./**", "!../**"], - "message": "An output imports only its framework (P7)." + "message": "An output imports only its framework (P7).", + "allowTypeImports": true } ] } diff --git a/tests/toolchains/qwik/output.oxlintrc.json b/tests/toolchains/qwik/output.oxlintrc.json index f30b85329..0a0b798b8 100644 --- a/tests/toolchains/qwik/output.oxlintrc.json +++ b/tests/toolchains/qwik/output.oxlintrc.json @@ -21,14 +21,16 @@ "rules": { // P7, D11: an output imports only its framework, and (from M3) the outputs beside it, and // (from M4) the `virtual:` modules a styling engine's plugin registers, which it keeps as - // written (plan §4.4, ADR-0064). + // written (plan §4.4, ADR-0064). A type import is erased, so a package's types are imported + // by name, as the source imports them (ADR-0067). "no-restricted-imports": [ "error", { "patterns": [ { "group": ["*", "!@qwik.dev/core", "!@qwik.dev/core/*", "!virtual:*", "!./**", "!../**"], - "message": "An output imports only its framework (P7)." + "message": "An output imports only its framework (P7).", + "allowTypeImports": true } ] } diff --git a/tests/toolchains/react/output.oxlintrc.json b/tests/toolchains/react/output.oxlintrc.json index 8fcdc9ab0..587b6d06e 100644 --- a/tests/toolchains/react/output.oxlintrc.json +++ b/tests/toolchains/react/output.oxlintrc.json @@ -21,14 +21,16 @@ "rules": { // P7, D11: an output imports only its framework, and (from M3) the outputs beside it, and // (from M4) the `virtual:` modules a styling engine's plugin registers, which it keeps as - // written (plan §4.4, ADR-0064). + // written (plan §4.4, ADR-0064). A type import is erased, so a package's types are imported + // by name, as the source imports them (ADR-0067). "no-restricted-imports": [ "error", { "patterns": [ { "group": ["*", "!react", "!react/*", "!virtual:*", "!./**", "!../**"], - "message": "An output imports only its framework (P7)." + "message": "An output imports only its framework (P7).", + "allowTypeImports": true } ] } diff --git a/tests/toolchains/solid/output.oxlintrc.json b/tests/toolchains/solid/output.oxlintrc.json index 31124eaf3..f1c3387db 100644 --- a/tests/toolchains/solid/output.oxlintrc.json +++ b/tests/toolchains/solid/output.oxlintrc.json @@ -21,14 +21,16 @@ "rules": { // P7, D11: an output imports only its framework, and (from M3) the outputs beside it, and // (from M4) the `virtual:` modules a styling engine's plugin registers, which it keeps as - // written (plan §4.4, ADR-0064). + // written (plan §4.4, ADR-0064). A type import is erased, so a package's types are imported + // by name, as the source imports them (ADR-0067). "no-restricted-imports": [ "error", { "patterns": [ { "group": ["*", "!solid-js", "!solid-js/*", "!virtual:*", "!./**", "!../**"], - "message": "An output imports only its framework (P7)." + "message": "An output imports only its framework (P7).", + "allowTypeImports": true } ] } diff --git a/tests/toolchains/svelte/output.oxlintrc.json b/tests/toolchains/svelte/output.oxlintrc.json index ecf309b05..a03f28249 100644 --- a/tests/toolchains/svelte/output.oxlintrc.json +++ b/tests/toolchains/svelte/output.oxlintrc.json @@ -21,14 +21,16 @@ "rules": { // P7, D11: an output imports only its framework, and (from M3) the outputs beside it, and // (from M4) the `virtual:` modules a styling engine's plugin registers, which it keeps as - // written (plan §4.4, ADR-0064). + // written (plan §4.4, ADR-0064). A type import is erased, so a package's types are imported + // by name, as the source imports them (ADR-0067). "no-restricted-imports": [ "error", { "patterns": [ { "group": ["*", "!svelte", "!svelte/*", "!virtual:*", "!./**", "!../**"], - "message": "An output imports only its framework (P7)." + "message": "An output imports only its framework (P7).", + "allowTypeImports": true } ] } diff --git a/tests/toolchains/vue/consumers/generics/constrained/Consumer.vue b/tests/toolchains/vue/consumers/generics/constrained/Consumer.vue new file mode 100644 index 000000000..38a050c2a --- /dev/null +++ b/tests/toolchains/vue/consumers/generics/constrained/Consumer.vue @@ -0,0 +1,7 @@ + + + diff --git a/tests/toolchains/vue/consumers/generics/constrained/MisuseProp.vue b/tests/toolchains/vue/consumers/generics/constrained/MisuseProp.vue new file mode 100644 index 000000000..88a977288 --- /dev/null +++ b/tests/toolchains/vue/consumers/generics/constrained/MisuseProp.vue @@ -0,0 +1,8 @@ + + + diff --git a/tests/toolchains/vue/consumers/generics/list/Consumer.vue b/tests/toolchains/vue/consumers/generics/list/Consumer.vue new file mode 100644 index 000000000..d3a0aacda --- /dev/null +++ b/tests/toolchains/vue/consumers/generics/list/Consumer.vue @@ -0,0 +1,7 @@ + + + diff --git a/tests/toolchains/vue/consumers/generics/list/MisuseProp.vue b/tests/toolchains/vue/consumers/generics/list/MisuseProp.vue new file mode 100644 index 000000000..28c7db923 --- /dev/null +++ b/tests/toolchains/vue/consumers/generics/list/MisuseProp.vue @@ -0,0 +1,8 @@ + + + diff --git a/tests/toolchains/vue/consumers/generics/scoped-slot/Consumer.vue b/tests/toolchains/vue/consumers/generics/scoped-slot/Consumer.vue new file mode 100644 index 000000000..0ac2094a1 --- /dev/null +++ b/tests/toolchains/vue/consumers/generics/scoped-slot/Consumer.vue @@ -0,0 +1,11 @@ + + + diff --git a/tests/toolchains/vue/consumers/generics/scoped-slot/MisuseSlot.vue b/tests/toolchains/vue/consumers/generics/scoped-slot/MisuseSlot.vue new file mode 100644 index 000000000..fcfc39d90 --- /dev/null +++ b/tests/toolchains/vue/consumers/generics/scoped-slot/MisuseSlot.vue @@ -0,0 +1,12 @@ + + + diff --git a/tests/toolchains/vue/consumers/generics/select/Consumer.vue b/tests/toolchains/vue/consumers/generics/select/Consumer.vue new file mode 100644 index 000000000..b7daa2482 --- /dev/null +++ b/tests/toolchains/vue/consumers/generics/select/Consumer.vue @@ -0,0 +1,11 @@ + + + diff --git a/tests/toolchains/vue/consumers/generics/select/MisuseModel.vue b/tests/toolchains/vue/consumers/generics/select/MisuseModel.vue new file mode 100644 index 000000000..9166bebbc --- /dev/null +++ b/tests/toolchains/vue/consumers/generics/select/MisuseModel.vue @@ -0,0 +1,12 @@ + + + diff --git a/tests/toolchains/vue/consumers/types/child-type-import/Consumer.vue b/tests/toolchains/vue/consumers/types/child-type-import/Consumer.vue new file mode 100644 index 000000000..7ec71fd6d --- /dev/null +++ b/tests/toolchains/vue/consumers/types/child-type-import/Consumer.vue @@ -0,0 +1,10 @@ + + + diff --git a/tests/toolchains/vue/consumers/types/child-type-import/MisuseProp.vue b/tests/toolchains/vue/consumers/types/child-type-import/MisuseProp.vue new file mode 100644 index 000000000..218a8b2ad --- /dev/null +++ b/tests/toolchains/vue/consumers/types/child-type-import/MisuseProp.vue @@ -0,0 +1,8 @@ + + + diff --git a/tests/toolchains/vue/consumers/types/child-type-import/MisuseType.vue b/tests/toolchains/vue/consumers/types/child-type-import/MisuseType.vue new file mode 100644 index 000000000..fcfa736d7 --- /dev/null +++ b/tests/toolchains/vue/consumers/types/child-type-import/MisuseType.vue @@ -0,0 +1,12 @@ + + + diff --git a/tests/toolchains/vue/consumers/types/exported-types/Consumer.vue b/tests/toolchains/vue/consumers/types/exported-types/Consumer.vue new file mode 100644 index 000000000..e34caff4b --- /dev/null +++ b/tests/toolchains/vue/consumers/types/exported-types/Consumer.vue @@ -0,0 +1,13 @@ + + + diff --git a/tests/toolchains/vue/consumers/types/exported-types/MisuseProp.vue b/tests/toolchains/vue/consumers/types/exported-types/MisuseProp.vue new file mode 100644 index 000000000..16d1713b7 --- /dev/null +++ b/tests/toolchains/vue/consumers/types/exported-types/MisuseProp.vue @@ -0,0 +1,8 @@ + + + diff --git a/tests/toolchains/vue/consumers/types/exported-types/MisuseType.vue b/tests/toolchains/vue/consumers/types/exported-types/MisuseType.vue new file mode 100644 index 000000000..e8c0c1def --- /dev/null +++ b/tests/toolchains/vue/consumers/types/exported-types/MisuseType.vue @@ -0,0 +1,15 @@ + + + diff --git a/tests/toolchains/vue/consumers/types/extends-chain/Consumer.vue b/tests/toolchains/vue/consumers/types/extends-chain/Consumer.vue new file mode 100644 index 000000000..2b7d8a060 --- /dev/null +++ b/tests/toolchains/vue/consumers/types/extends-chain/Consumer.vue @@ -0,0 +1,7 @@ + + + diff --git a/tests/toolchains/vue/consumers/types/extends-chain/MisuseProp.vue b/tests/toolchains/vue/consumers/types/extends-chain/MisuseProp.vue new file mode 100644 index 000000000..b6ddfc7b3 --- /dev/null +++ b/tests/toolchains/vue/consumers/types/extends-chain/MisuseProp.vue @@ -0,0 +1,8 @@ + + + diff --git a/tests/toolchains/vue/consumers/types/imported-interface/Consumer.vue b/tests/toolchains/vue/consumers/types/imported-interface/Consumer.vue new file mode 100644 index 000000000..e3889f215 --- /dev/null +++ b/tests/toolchains/vue/consumers/types/imported-interface/Consumer.vue @@ -0,0 +1,7 @@ + + + diff --git a/tests/toolchains/vue/consumers/types/imported-interface/MisuseProp.vue b/tests/toolchains/vue/consumers/types/imported-interface/MisuseProp.vue new file mode 100644 index 000000000..ebcccc404 --- /dev/null +++ b/tests/toolchains/vue/consumers/types/imported-interface/MisuseProp.vue @@ -0,0 +1,8 @@ + + + diff --git a/tests/toolchains/vue/consumers/types/intersection-props/Consumer.vue b/tests/toolchains/vue/consumers/types/intersection-props/Consumer.vue new file mode 100644 index 000000000..e8067ab76 --- /dev/null +++ b/tests/toolchains/vue/consumers/types/intersection-props/Consumer.vue @@ -0,0 +1,7 @@ + + + diff --git a/tests/toolchains/vue/consumers/types/intersection-props/MisuseProp.vue b/tests/toolchains/vue/consumers/types/intersection-props/MisuseProp.vue new file mode 100644 index 000000000..80429729b --- /dev/null +++ b/tests/toolchains/vue/consumers/types/intersection-props/MisuseProp.vue @@ -0,0 +1,8 @@ + + + diff --git a/tests/toolchains/vue/consumers/types/package-types/Consumer.vue b/tests/toolchains/vue/consumers/types/package-types/Consumer.vue new file mode 100644 index 000000000..0841dc9b7 --- /dev/null +++ b/tests/toolchains/vue/consumers/types/package-types/Consumer.vue @@ -0,0 +1,7 @@ + + + diff --git a/tests/toolchains/vue/consumers/types/package-types/MisuseProp.vue b/tests/toolchains/vue/consumers/types/package-types/MisuseProp.vue new file mode 100644 index 000000000..d4aaeffef --- /dev/null +++ b/tests/toolchains/vue/consumers/types/package-types/MisuseProp.vue @@ -0,0 +1,8 @@ + + + diff --git a/tests/toolchains/vue/consumers/types/shared-type/Consumer.vue b/tests/toolchains/vue/consumers/types/shared-type/Consumer.vue new file mode 100644 index 000000000..242ef587d --- /dev/null +++ b/tests/toolchains/vue/consumers/types/shared-type/Consumer.vue @@ -0,0 +1,11 @@ + + + diff --git a/tests/toolchains/vue/consumers/types/shared-type/MisuseProp.vue b/tests/toolchains/vue/consumers/types/shared-type/MisuseProp.vue new file mode 100644 index 000000000..9112c40ab --- /dev/null +++ b/tests/toolchains/vue/consumers/types/shared-type/MisuseProp.vue @@ -0,0 +1,8 @@ + + + diff --git a/tests/toolchains/vue/consumers/types/shared-type/MisuseType.vue b/tests/toolchains/vue/consumers/types/shared-type/MisuseType.vue new file mode 100644 index 000000000..0c2079be2 --- /dev/null +++ b/tests/toolchains/vue/consumers/types/shared-type/MisuseType.vue @@ -0,0 +1,12 @@ + + + diff --git a/tests/toolchains/vue/consumers/types/utility-types/Consumer.vue b/tests/toolchains/vue/consumers/types/utility-types/Consumer.vue new file mode 100644 index 000000000..79fed3d26 --- /dev/null +++ b/tests/toolchains/vue/consumers/types/utility-types/Consumer.vue @@ -0,0 +1,11 @@ + + + diff --git a/tests/toolchains/vue/consumers/types/utility-types/MisuseProp.vue b/tests/toolchains/vue/consumers/types/utility-types/MisuseProp.vue new file mode 100644 index 000000000..26366f04a --- /dev/null +++ b/tests/toolchains/vue/consumers/types/utility-types/MisuseProp.vue @@ -0,0 +1,8 @@ + + + diff --git a/tests/toolchains/vue/consumers/types/utility-types/MisuseType.vue b/tests/toolchains/vue/consumers/types/utility-types/MisuseType.vue new file mode 100644 index 000000000..ba306ab07 --- /dev/null +++ b/tests/toolchains/vue/consumers/types/utility-types/MisuseType.vue @@ -0,0 +1,12 @@ + + + diff --git a/tests/toolchains/vue/output.oxlintrc.json b/tests/toolchains/vue/output.oxlintrc.json index 108eb630f..519fc46da 100644 --- a/tests/toolchains/vue/output.oxlintrc.json +++ b/tests/toolchains/vue/output.oxlintrc.json @@ -21,14 +21,16 @@ "rules": { // P7, D11: an output imports only its framework, and (from M3) the outputs beside it, and // (from M4) the `virtual:` modules a styling engine's plugin registers, which it keeps as - // written (plan §4.4, ADR-0064). + // written (plan §4.4, ADR-0064). A type import is erased, so a package's types are imported + // by name, as the source imports them (ADR-0067). "no-restricted-imports": [ "error", { "patterns": [ { "group": ["*", "!vue", "!vue/*", "!virtual:*", "!./**", "!../**"], - "message": "An output imports only its framework (P7)." + "message": "An output imports only its framework (P7).", + "allowTypeImports": true } ] } From 7266fd4510c17b20162ca3a4ca42937343513f7e Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 28 Oct 2026 13:06:17 +0200 Subject: [PATCH 2/2] fix: answer the first review of cross-file types and generics on vue Co-authored-by: multica-agent --- AGENTS.md | 4 +- packages/analyzer/src/analyze.ts | 31 ++- packages/codegen/README.md | 14 +- packages/codegen/src/exports.ts | 42 +++- packages/codegen/test/codegen.test.ts | 67 +++++- packages/compiler/test/types.test.ts | 29 +++ packages/target-vue/src/script.ts | 21 +- packages/target-vue/test/emit.test.ts | 45 ++-- packages/target-vue/test/typecheck.test.ts | 51 ++++- packages/testing/README.md | 10 +- .../cases/types/package-types/Status.uf.tsx | 13 +- .../__expected__/aria.closed.yaml | 1 - .../package-types/__expected__/aria.open.yaml | 1 - .../package-types/__expected__/aria.tick.yaml | 1 + .../package-types/__expected__/aria.tock.yaml | 1 + .../__expected__/dom.closed.html | 3 - .../package-types/__expected__/dom.open.html | 3 - .../package-types/__expected__/dom.tick.html | 3 + .../package-types/__expected__/dom.tock.html | 3 + ...eometry.closed.json => geometry.tick.json} | 4 +- ...{geometry.open.json => geometry.tock.json} | 4 +- .../__expected__/ssr.closed.html | 3 - .../package-types/__expected__/ssr.open.html | 3 - .../package-types/__expected__/ssr.tick.html | 3 + .../package-types/__expected__/ssr.tock.html | 3 + .../__output__/angular/status.ts | 12 +- .../package-types/__output__/angular/types.ts | 7 + .../__output__/astro/Status.astro | 6 +- .../package-types/__output__/astro/types.ts | 7 + .../types/package-types/__output__/ir.json | 214 +++++++++++++----- .../package-types/__output__/qwik/Status.tsx | 6 +- .../package-types/__output__/qwik/types.ts | 7 + .../package-types/__output__/react/Status.tsx | 7 +- .../package-types/__output__/react/types.ts | 7 + .../package-types/__output__/solid/Status.tsx | 4 +- .../package-types/__output__/solid/types.ts | 7 + .../__output__/svelte/Status.svelte | 6 +- .../package-types/__output__/svelte/types.ts | 7 + .../package-types/__output__/vue/Status.vue | 13 +- .../package-types/__output__/vue/types.ts | 7 + .../__screenshots__/closed-chromium-linux.png | Bin 1720 -> 0 bytes .../__screenshots__/open-chromium-linux.png | Bin 1401 -> 0 bytes .../__screenshots__/tick-chromium-linux.png | Bin 0 -> 2658 bytes .../__screenshots__/tock-chromium-linux.png | Bin 0 -> 2933 bytes .../cases/types/package-types/case.json | 6 +- .../types/package-types/package-types.test.ts | 21 +- .../cases/types/package-types/types.ts | 7 + tests/integration/harness/canaries.ts | 28 ++- .../integration/harness/canaries.unit.test.ts | 2 +- tests/integration/harness/canary-verdict.ts | 20 +- .../harness/canary-verdict.unit.test.ts | 50 ++++ tests/integration/harness/compile.test.ts | 11 +- .../integration/harness/quarantine/angular.ts | 3 +- .../types/package-types/Consumer.vue | 2 +- .../types/package-types/MisuseProp.vue | 4 +- 55 files changed, 649 insertions(+), 185 deletions(-) delete mode 100644 tests/integration/cases/types/package-types/__expected__/aria.closed.yaml delete mode 100644 tests/integration/cases/types/package-types/__expected__/aria.open.yaml create mode 100644 tests/integration/cases/types/package-types/__expected__/aria.tick.yaml create mode 100644 tests/integration/cases/types/package-types/__expected__/aria.tock.yaml delete mode 100644 tests/integration/cases/types/package-types/__expected__/dom.closed.html delete mode 100644 tests/integration/cases/types/package-types/__expected__/dom.open.html create mode 100644 tests/integration/cases/types/package-types/__expected__/dom.tick.html create mode 100644 tests/integration/cases/types/package-types/__expected__/dom.tock.html rename tests/integration/cases/types/package-types/__expected__/{geometry.closed.json => geometry.tick.json} (98%) rename tests/integration/cases/types/package-types/__expected__/{geometry.open.json => geometry.tock.json} (98%) delete mode 100644 tests/integration/cases/types/package-types/__expected__/ssr.closed.html delete mode 100644 tests/integration/cases/types/package-types/__expected__/ssr.open.html create mode 100644 tests/integration/cases/types/package-types/__expected__/ssr.tick.html create mode 100644 tests/integration/cases/types/package-types/__expected__/ssr.tock.html create mode 100644 tests/integration/cases/types/package-types/__output__/angular/types.ts create mode 100644 tests/integration/cases/types/package-types/__output__/astro/types.ts create mode 100644 tests/integration/cases/types/package-types/__output__/qwik/types.ts create mode 100644 tests/integration/cases/types/package-types/__output__/react/types.ts create mode 100644 tests/integration/cases/types/package-types/__output__/solid/types.ts create mode 100644 tests/integration/cases/types/package-types/__output__/svelte/types.ts create mode 100644 tests/integration/cases/types/package-types/__output__/vue/types.ts delete mode 100644 tests/integration/cases/types/package-types/__screenshots__/closed-chromium-linux.png delete mode 100644 tests/integration/cases/types/package-types/__screenshots__/open-chromium-linux.png create mode 100644 tests/integration/cases/types/package-types/__screenshots__/tick-chromium-linux.png create mode 100644 tests/integration/cases/types/package-types/__screenshots__/tock-chromium-linux.png create mode 100644 tests/integration/cases/types/package-types/types.ts diff --git a/AGENTS.md b/AGENTS.md index 7b6fedb67..106b24f9c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -232,7 +232,9 @@ Generate each of these with its command. The formatter and the linter skip them emit a small inline helper and declare it `emulated`. The one exception is a module a Vite plugin registers, whatever the engine: an import under a `passThrough` prefix (`virtual:` by default) reaches every output as written, and that plugin resolves it (§4.4, ADR-0004, - ADR-0064). Its module may import the engine's own runtime, such as `@styleframe/runtime`. + ADR-0064). Its module may import the engine's own runtime, such as `@styleframe/runtime`. A + type import is erased, so an output imports the types the source imports by name, a package's + included (ADR-0067). - **The IR is plain JSON (P5).** Every node carries a span. Add a field only when a consumer reads it. Keep `checkInvariants` true (ADR-0032). diff --git a/packages/analyzer/src/analyze.ts b/packages/analyzer/src/analyze.ts index a77fe6e15..632628299 100644 --- a/packages/analyzer/src/analyze.ts +++ b/packages/analyzer/src/analyze.ts @@ -137,15 +137,16 @@ const RELATIVE_OR_ABSOLUTE = /^(?:[./\\]|[A-Za-z]:)/; export const COMPONENT_SPECIFIER: RegExp = /\.uf\.tsx$/; /** - * The specifiers of the `.uf.tsx` modules a parsed module imports as values, in source order, - * each once: what `compile()` asks its resolver for before the module is analysed. + * The specifiers of the `.uf.tsx` modules a parsed module imports, values or types, in source + * order, each once: what `compile()` asks its resolver for before the module is analysed. */ export function componentImports(parsed: ParsedModule): string[] { const found = new Set(); for (const statement of parsed.program.body) { + // A type-only import too: its outputs import the types from the output of the module's main + // component, which only the module's API names (ADR-0067). if ( statement.type === "ImportDeclaration" && - statement.importKind !== "type" && COMPONENT_SPECIFIER.test(statement.source.value) ) { found.add(statement.source.value); @@ -1261,6 +1262,11 @@ function componentImport(statement: AST.ImportDeclaration, context: ModuleContex context.components.set(item.local, { name: item.local.name, api: component }); } if (!names.length) return; + // A type-only import of the module before this one recorded it without names. + const typesOnly = context.imports.findIndex( + (entry) => entry.specifier === specifier && !entry.names.length, + ); + if (typesOnly !== -1) context.imports.splice(typesOnly, 1); context.imports.push( createModuleImport(specifier, api.file, api, names, { start: statement.start, @@ -1498,6 +1504,7 @@ function checkImport(statement: AST.ImportDeclaration, context: ModuleContext): } if (statement.importKind === "type") { typeImport(statement, statement.specifiers, context); + if (COMPONENT_SPECIFIER.test(specifier)) typeOnlyModule(statement, context); return; } reporter.unsupported(statement, "Importing modules is not supported yet."); @@ -1577,6 +1584,24 @@ function sharedImport(statement: AST.ImportDeclaration, context: ModuleContext): ); } +/** + * A `.uf.tsx` module the source imports types from alone: recorded with its API and no names, so + * each output imports the types from the output of its main component, which the file's name + * does not give (ADR-0067). An unresolved one is left to the type oracle, which reports the types + * it cannot read (UF6001). + */ +function typeOnlyModule(statement: AST.ImportDeclaration, context: ModuleContext): void { + const specifier = statement.source.value; + const api = context.resolved.get(specifier); + if (!api || context.imports.some((entry) => entry.specifier === specifier)) return; + context.imports.push( + createModuleImport(specifier, api.file, api, [], { + start: statement.start, + end: statement.end, + }), + ); +} + /** * An import of types from a relative `.ts`, a `.uf.tsx`, a `.uf.ts` or a package (ADR-0067): * the analyser reads them through the type oracle's shapes, and every output re-imports them diff --git a/packages/codegen/README.md b/packages/codegen/README.md index a112e7bf5..bb8a75df6 100644 --- a/packages/codegen/README.md +++ b/packages/codegen/README.md @@ -226,12 +226,14 @@ rewrite })` prints a component's root element or fragment, every node and attrib the source does. `addTypeImports` adds the names of the module's `import type` declarations that what a component's output prints reads (`liveTypeNames`), by the source's own names, from `typeSpecifier`: a `.ts` module's or a package's as written, a `.uf.tsx` module's from the - output of its main component (`./Field.vue`, `./Field`); `typeExportsCode` writes the module's - type exports, an exported type no component reaches and a re-export, in the output of its main - component, each with its span so a target places it in source order (ADR-0067, ADR-0068). A - prop another file declares (`declaredBy`) is written in that file's names, so neither these nor - `liveTypes` and the kinds look its type up in the component's own module. `ImportSet` sorts imports and renames one that clashes with a name in its - `NameScope`: a target reserves the source's names (`sourceNames`: the component, its types, + output of its main component, which the module's import records with its API, a type-only one + included (`./TextField.vue` for `./text-field.uf.tsx`). `typeExportsCode` writes the module's + type exports in the output of its main component: an exported type no component reaches, a + re-export, and a re-export of an exported type only another component's output declares, each + with its span, so a target places it in source order (ADR-0067, ADR-0068). A prop another file + declares (`declaredBy`) is written in that file's names, so neither these nor `liveTypes` and + the kinds look its type up in the component's own module. `ImportSet` sorts imports and renames + one that clashes with a name in its `NameScope`: a target reserves the source's names (`sourceNames`: the component, its types, bindings, every variable its expressions and setup code read or declare (`codeNames`), its functions' parameters, and the types its setup's annotations name (`typeNames`)) and claims its own (`props`, `cx`, `Show`, `setCount`) from the same scope, so nothing it adds captures or diff --git a/packages/codegen/src/exports.ts b/packages/codegen/src/exports.ts index c77091bcb..4959460af 100644 --- a/packages/codegen/src/exports.ts +++ b/packages/codegen/src/exports.ts @@ -69,9 +69,11 @@ export function typeDeclarationCode(declaration: TypeDeclaration): string { /** * The specifier an output imports a module's types from (ADR-0067): a `.uf.tsx` module's are - * exported by the output of its main component (`keyOwner`), `file(name)` naming a component's - * output file as for `childImports`; any other specifier, a `.ts` module's or a package's, is - * kept as written, since every output tree holds the `.ts` modules unchanged (ADR-0070). + * exported by the output of its main component (`keyOwner` of the API the module's import + * records, a type-only one included), `file(name)` naming a component's output file as for + * `childImports`; any other specifier, a `.ts` module's or a package's, is kept as written, + * since every output tree holds the `.ts` modules unchanged (ADR-0070). A `.uf.tsx` module with + * no API here was not resolved, which fails the compile before any output. */ export function typeSpecifier( specifier: string, @@ -80,11 +82,13 @@ export function typeSpecifier( ): string { if (!specifier.endsWith(".uf.tsx")) return specifier; const entry = module.imports?.find((each) => each.specifier === specifier); - // A module the source imports no component of has no API here: its main component is named - // as its file, as the analyser names a module's default export. - const stem = specifier.slice(specifier.lastIndexOf("/") + 1, -".uf.tsx".length); - const owner = (entry && keyOwner(entry.api.components)) ?? stem; - return `${specifier.slice(0, specifier.lastIndexOf("/") + 1)}${file(owner)}`; + const owner = entry && keyOwner(entry.api.components); + if (!entry || owner === undefined) return specifier; + const directory = entry.file.includes("/") + ? entry.file.slice(0, entry.file.lastIndexOf("/") + 1) + : ""; + const path = `${directory}${file(owner)}`; + return path.startsWith("../") ? path : `./${path}`; } /** @@ -122,9 +126,10 @@ export function addTypeImports( /** * The module's type exports, as the output of its main component writes them (ADR-0068): an - * exported type no component reaches as written, and a re-export from `typeSpecifier`, each with - * its span, so a target can place it in source order among the types it copies. Every other - * component's output writes none. + * exported type no component reaches as written, a re-export from `typeSpecifier`, and a + * re-export of an exported type only another component's output declares, each with its span, + * so a target can place it in source order among the types it copies. Every other component's + * output writes none. */ export function typeExportsCode( component: UfComponent, @@ -132,7 +137,7 @@ export function typeExportsCode( file: (name: string) => string, ): { code: string; span: Span }[] { if (keyOwner(module.components, module.exports) !== component.name) return []; - return (module.typeExports ?? []).map((entry) => { + const exports = (module.typeExports ?? []).map((entry) => { if (entry.kind === "Declaration") return { code: `export ${entry.code}`, span: entry.span }; const names = entry.names.map(({ imported, exported }) => imported === exported ? imported : `${imported} as ${exported}`, @@ -140,4 +145,17 @@ export function typeExportsCode( const specifier = JSON.stringify(typeSpecifier(entry.specifier, module, file)); return { code: `export type { ${names.join(", ")} } from ${specifier};`, span: entry.span }; }); + // An exported type only another component of the module reaches is declared in that + // component's output: the main output re-exports it, so a consumer finds every exported type + // where it imports the module's (ADR-0068). + for (const declaration of module.types) { + if (!declaration.exported || component.types.includes(declaration.name)) continue; + const sibling = module.components.find((each) => each.types.includes(declaration.name)); + if (!sibling) continue; + exports.push({ + code: `export type { ${declaration.name} } from ${JSON.stringify(`./${file(sibling.name)}`)};`, + span: declaration.span, + }); + } + return exports.toSorted((a, b) => a.span.start - b.span.start); } diff --git a/packages/codegen/test/codegen.test.ts b/packages/codegen/test/codegen.test.ts index 3662e8137..3a11fd674 100644 --- a/packages/codegen/test/codegen.test.ts +++ b/packages/codegen/test/codegen.test.ts @@ -6,6 +6,7 @@ import { createElement, createExport, createFor, + createModuleImport, createInterpolation, createModule, createProp, @@ -16,7 +17,7 @@ import { createTypeText, span, } from "@unframework/ir"; -import type { ElementNode } from "@unframework/ir"; +import type { ComponentApi, ElementNode, ModuleApi } from "@unframework/ir"; import { transformWithOxc } from "vite"; import { describe, expect, it } from "vitest"; @@ -392,6 +393,25 @@ describe("type declarations", () => { }); describe("type imports and exports (ADR-0067, ADR-0068)", () => { + /** A `.uf.tsx` module's API, as the resolver gives it: its main component alone. */ + const moduleApi = (file: string, name: string): ModuleApi => ({ + file, + components: [ + { + name, + export: "default", + props: [], + events: [], + models: [], + slots: [], + exposes: [], + inheritAttrs: true, + root: "element", + } satisfies ComponentApi, + ], + keys: [], + }); + /** A module whose main component, Badge, reads `Tone` and not `Hint`, which a member another file declares names. */ function badgeModule() { const props = [ @@ -428,6 +448,16 @@ describe("type imports and exports (ADR-0067, ADR-0068)", () => { span: at, }, ]; + // Imported for its types alone: the analyser records its API, without names. + module.imports = [ + createModuleImport( + "./Field.uf.tsx", + "Field.uf.tsx", + moduleApi("Field.uf.tsx", "Field"), + [], + at, + ), + ]; module.typeExports = [ { kind: "Declaration", name: "Sizes", code: "type Sizes = SizeName[];", span: span(40, 64) }, { @@ -457,17 +487,46 @@ describe("type imports and exports (ADR-0067, ADR-0068)", () => { it("writes the module's type exports in its main component's output, from each target's file", () => { const { module, badge } = badgeModule(); expect(typeExportsCode(badge, module, (name) => `${name}.vue`)).toEqual([ - { code: "export type Sizes = SizeName[];", span: span(40, 64) }, { code: 'export type { FieldProps as Props } from "./Field.vue";', span: span(10, 30) }, + { code: "export type Sizes = SizeName[];", span: span(40, 64) }, + ]); + }); + + it("re-exports from the main output an exported type only another component's output declares", () => { + const tone = createTypeDeclaration("Tone", true, 'type Tone = "info";', span(0, 19)); + const badge = createComponent("Badge", createElement("p", [], [], at), at, [], undefined, [ + "Tone", + ]); + const pair = createComponent("Pair", createElement("p", [], [], at), at); + const module = createModule( + "Pair.uf.tsx", + [badge, pair], + [createExport("default", "Pair", at)], + [tone], + ); + expect(typeExportsCode(pair, module, (name) => `${name}.vue`)).toEqual([ + { code: 'export type { Tone } from "./Badge.vue";', span: span(0, 19) }, ]); + // Badge declares it already; it writes no export of the module's. + expect(typeExportsCode(badge, module, (name) => `${name}.vue`)).toEqual([]); }); it("imports a .uf.tsx module's types from its main component's output, and keeps any other specifier", () => { const module = createModule("Form.uf.tsx"); + module.imports = [ + createModuleImport( + "../shared/text-field.uf.tsx", + "../shared/text-field.uf.tsx", + moduleApi("../shared/text-field.uf.tsx", "TextField"), + [], + at, + ), + ]; expect(typeSpecifier("./types", module, (name) => `${name}.vue`)).toBe("./types"); expect(typeSpecifier("rxjs", module, (name) => `${name}.vue`)).toBe("rxjs"); - expect(typeSpecifier("../shared/Field.uf.tsx", module, (name) => name.toLowerCase())).toBe( - "../shared/field", + // The main component's file, never the module's own name. + expect(typeSpecifier("../shared/text-field.uf.tsx", module, (name) => `${name}.vue`)).toBe( + "../shared/TextField.vue", ); }); }); diff --git a/packages/compiler/test/types.test.ts b/packages/compiler/test/types.test.ts index cfe905df2..c681b57ab 100644 --- a/packages/compiler/test/types.test.ts +++ b/packages/compiler/test/types.test.ts @@ -192,6 +192,35 @@ describe("props types across files and packages", () => { } }); + it("imports types alone from a `.uf.tsx` module's main output, which its file name does not give", async () => { + // The resolver reads the module, so each output names its main component's file, never the + // module's own name (ADR-0067). + const files: Record = { + "text-field.uf.tsx": [ + "export interface TextFieldProps { label: string }", + "export default function TextField({ label }: TextFieldProps) { return {label}; }", + ].join("\n"), + "Form.uf.tsx": [ + 'import type { TextFieldProps } from "./text-field.uf.tsx";', + "export default function Form({ label }: TextFieldProps) { return

{label}

; }", + ].join("\n"), + }; + const read = async (path: string) => files[path.slice("/project/".length)]; + const result = await compile(files["Form.uf.tsx"]!, { + filename: "Form.uf.tsx", + targets: ["vue"], + typeOracle: createSyntacticOracle({ root: "/project", readFile: read }), + resolve: createFileResolver({ root: "/project", readFile: read }), + }); + expect(result.diagnostics).toEqual([]); + expect(result.ir!.imports).toEqual([ + expect.objectContaining({ specifier: "./text-field.uf.tsx", names: [] }), + ]); + expect(result.outputs.vue![0]!.contents).toContain( + 'import type { TextFieldProps } from "./TextField.vue";', + ); + }); + it.each([ [ "Partial", diff --git a/packages/target-vue/src/script.ts b/packages/target-vue/src/script.ts index d59862b4b..272a6a4d2 100644 --- a/packages/target-vue/src/script.ts +++ b/packages/target-vue/src/script.ts @@ -306,18 +306,25 @@ function genericAttribute(component: UfComponent): string { /** * The type `defineProps` takes (ADR-0067): the source's annotation, which compiler-sfc resolves * through the output tree, its relative modules, `extends` chains, intersections and the five - * utility types among them. A member a package declares is the one exception: compiler-sfc - * resolves a package only through TypeScript's classic API, which TypeScript 7 has not, so the - * props are printed as a type literal of their members' types, as their files write them. + * utility types among them. A member a package declares is the exception: compiler-sfc resolves + * a package only through TypeScript's classic API, which TypeScript 7 has not. So the props are + * printed as a type literal of their keys, which is all compiler-sfc needs: a member the + * component's module writes keeps its text, and a member another file declares is read off the + * annotation, `(TimeInterval)["value"]`, since its own text is in that file's names. The + * annotation's names are the ones the module imports, and a runtime type compiler-sfc cannot + * infer is `null`, which casts nothing, as on every other target (ADR-0034). */ function propsType(component: UfComponent, parameter: PropsParameter): string { if (!component.props.some((prop) => prop.declaredBy?.file.startsWith("pkg:"))) { return parameter.type.code; } - const members = component.props.map( - (prop) => - `${/^[A-Za-z_$][\w$]*$/.test(prop.name) ? prop.name : JSON.stringify(prop.name)}${prop.optional ? "?" : ""}: ${prop.type.code}`, - ); + const members = component.props.map((prop) => { + const key = /^[A-Za-z_$][\w$]*$/.test(prop.name) ? prop.name : JSON.stringify(prop.name); + const type = prop.declaredBy + ? `(${parameter.type.code})[${JSON.stringify(prop.name)}]` + : prop.type.code; + return `${key}${prop.optional ? "?" : ""}: ${type}`; + }); return `{ ${members.join("; ")} }`; } diff --git a/packages/target-vue/test/emit.test.ts b/packages/target-vue/test/emit.test.ts index a4b4c7638..853649661 100644 --- a/packages/target-vue/test/emit.test.ts +++ b/packages/target-vue/test/emit.test.ts @@ -381,14 +381,17 @@ export default function Picker({ value }: { value: T expect(picker).toContain(`")); + // The annotation's names, and only those: `Tone` is the file's, and `Observer` unread. + expect(script).toContain('import type { FieldOptions } from "./types";'); + expect(script).toContain('import type { SubscriptionLike } from "rxjs";'); + expect(script).not.toMatch(/\bObserver\b|\bTone\b/); + expect(script).toContain( + 'closed: (Pick & FieldOptions & { label: string })["closed"];', + ); + expect(script).toContain( + 'tone?: (Pick & FieldOptions & { label: string })["tone"];', ); + expect(script).toContain("label: string;"); }); }); diff --git a/packages/target-vue/test/typecheck.test.ts b/packages/target-vue/test/typecheck.test.ts index 0423d500b..ab3ff3dc0 100644 --- a/packages/target-vue/test/typecheck.test.ts +++ b/packages/target-vue/test/typecheck.test.ts @@ -5,7 +5,14 @@ import { afterAll, describe, expect, it } from "vitest"; import { vueEventInterface } from "../src/listeners.ts"; import { toolchain } from "../src/toolchain/index.ts"; -import { goldenFiles, removeScratch, toolchainDir, writeScratch } from "./helpers.ts"; +import { + emitFormatted, + goldenFiles, + lower, + removeScratch, + toolchainDir, + writeScratch, +} from "./helpers.ts"; import { M2_SHAPES } from "./lint-probes.ts"; const context = { toolchainDir, root: toolchainDir }; @@ -18,6 +25,48 @@ const checker = { timeout: 60_000 }; afterAll(removeScratch); describe("vue typecheck", () => { + it( + "exports every type the module exports from its main output, one a local component declares too", + checker, + async () => { + // ADR-0068: `Tone` is declared in Badge's output, which only Badge reaches, and a consumer + // imports it from the module's main output. + const module = lower( + [ + 'export type Tone = "info" | "danger";', + "function Badge({ tone }: { tone: Tone }) {", + " return {tone};", + "}", + "export default function Pair() {", + ' return

;', + "}", + ].join("\n"), + "Pair.uf.tsx", + ); + const outputs = Object.fromEntries( + (await emitFormatted(module)).map((file) => [file.path, file.contents]), + ); + expect(outputs["Pair.vue"]).toContain('export type { Tone } from "./Badge.vue";'); + const paths = writeScratch({ + ...outputs, + "Use.vue": [ + '", + "", + "", + "", + ].join("\n"), + }); + const results = await toolchain.typecheck(Object.values(paths), context); + for (const path of Object.values(paths)) expect(results.get(path), path).toEqual([]); + }, + ); + it("checks the corpus and an injected error in one run, by file", checker, async () => { const golden = goldenFiles(); expect(golden.length).toBeGreaterThan(0); diff --git a/packages/testing/README.md b/packages/testing/README.md index ce0eae04d..ba3b8b725 100644 --- a/packages/testing/README.md +++ b/packages/testing/README.md @@ -55,11 +55,11 @@ describeTargets("state/counter", () => { capability. Every test of a case the target has no output for (`test.provide.ufNoOutput`, by case id, with the errors it expects for the target; the harness gives its spec a stand-in for the component) is skipped there the same way, whatever it requires, with `no output: ` on every layer, and the summary excuses it on that target alone. Every test of a case - the target cannot compile yet (`test.provide.ufStandIn`, by case id, with the cause; the - harness gives its spec a stand-in too, ADR-0068) runs, and the setup records L7, L8, L9, L10, - L11 and L13 as failed with the cause first (`recordStandIn`), and `standIn` on the test's - record, so the summary excuses its scenarios on that target alone while its cells still fail. +errors>` on every layer, and the summary excuses it on that target alone. A case the target + cannot compile yet gets a stand-in too (`test.provide.ufStandIn`, by case id, with the cause, + ADR-0068). Its tests are not skipped: the setup first records L7, L8, L9, L10, L11 and L13 as + failed with the cause (`recordStandIn`), and `standIn` on the test's record. The summary + excuses its scenarios on that target alone, and its cells still fail. - **`view.user`** acts as a person does, through Playwright's real input (ADR-0050). An action is the inputs a person gives apart in time, each dispatched alone (the `ufInput` command): `click(target, { position? })` moves the mouse onto the element (Playwright's hover, which diff --git a/tests/integration/cases/types/package-types/Status.uf.tsx b/tests/integration/cases/types/package-types/Status.uf.tsx index b5d008054..8d19aad9a 100644 --- a/tests/integration/cases/types/package-types/Status.uf.tsx +++ b/tests/integration/cases/types/package-types/Status.uf.tsx @@ -1,12 +1,15 @@ -import type { SubscriptionLike } from "rxjs"; +import type { TimeInterval } from "rxjs"; -// A member a package declares: `closed`, from rxjs's `SubscriptionLike`. -export type StatusProps = Pick & { label: string }; +import type { Labelled } from "./types"; -export default function Status({ closed, label }: StatusProps) { +// Members a package declares, `value` and `interval` from rxjs's generic `TimeInterval`, beside +// one a `.ts` module declares. +type StatusProps = TimeInterval & Labelled; + +export default function Status({ value, interval, label }: StatusProps) { return (

- {label}: {closed ? "closed" : "open"} + {label}: {value} after {interval} ms

); } diff --git a/tests/integration/cases/types/package-types/__expected__/aria.closed.yaml b/tests/integration/cases/types/package-types/__expected__/aria.closed.yaml deleted file mode 100644 index bba124ef2..000000000 --- a/tests/integration/cases/types/package-types/__expected__/aria.closed.yaml +++ /dev/null @@ -1 +0,0 @@ -- paragraph: "Feed: closed" diff --git a/tests/integration/cases/types/package-types/__expected__/aria.open.yaml b/tests/integration/cases/types/package-types/__expected__/aria.open.yaml deleted file mode 100644 index 88979c8ca..000000000 --- a/tests/integration/cases/types/package-types/__expected__/aria.open.yaml +++ /dev/null @@ -1 +0,0 @@ -- paragraph: "Feed: open" diff --git a/tests/integration/cases/types/package-types/__expected__/aria.tick.yaml b/tests/integration/cases/types/package-types/__expected__/aria.tick.yaml new file mode 100644 index 000000000..6126db8e9 --- /dev/null +++ b/tests/integration/cases/types/package-types/__expected__/aria.tick.yaml @@ -0,0 +1 @@ +- paragraph: "Feed: tick after 250 ms" diff --git a/tests/integration/cases/types/package-types/__expected__/aria.tock.yaml b/tests/integration/cases/types/package-types/__expected__/aria.tock.yaml new file mode 100644 index 000000000..41ba01abb --- /dev/null +++ b/tests/integration/cases/types/package-types/__expected__/aria.tock.yaml @@ -0,0 +1 @@ +- paragraph: "Clock: tock after 1000 ms" diff --git a/tests/integration/cases/types/package-types/__expected__/dom.closed.html b/tests/integration/cases/types/package-types/__expected__/dom.closed.html deleted file mode 100644 index d92c2ff33..000000000 --- a/tests/integration/cases/types/package-types/__expected__/dom.closed.html +++ /dev/null @@ -1,3 +0,0 @@ -

- "Feed: closed" -

diff --git a/tests/integration/cases/types/package-types/__expected__/dom.open.html b/tests/integration/cases/types/package-types/__expected__/dom.open.html deleted file mode 100644 index b40575f9f..000000000 --- a/tests/integration/cases/types/package-types/__expected__/dom.open.html +++ /dev/null @@ -1,3 +0,0 @@ -

- "Feed: open" -

diff --git a/tests/integration/cases/types/package-types/__expected__/dom.tick.html b/tests/integration/cases/types/package-types/__expected__/dom.tick.html new file mode 100644 index 000000000..89f95045e --- /dev/null +++ b/tests/integration/cases/types/package-types/__expected__/dom.tick.html @@ -0,0 +1,3 @@ +

+ "Feed: tick after 250 ms" +

diff --git a/tests/integration/cases/types/package-types/__expected__/dom.tock.html b/tests/integration/cases/types/package-types/__expected__/dom.tock.html new file mode 100644 index 000000000..c3630b16b --- /dev/null +++ b/tests/integration/cases/types/package-types/__expected__/dom.tock.html @@ -0,0 +1,3 @@ +

+ "Clock: tock after 1000 ms" +

diff --git a/tests/integration/cases/types/package-types/__expected__/geometry.closed.json b/tests/integration/cases/types/package-types/__expected__/geometry.tick.json similarity index 98% rename from tests/integration/cases/types/package-types/__expected__/geometry.closed.json rename to tests/integration/cases/types/package-types/__expected__/geometry.tick.json index f118ff65b..b2297f6c7 100644 --- a/tests/integration/cases/types/package-types/__expected__/geometry.closed.json +++ b/tests/integration/cases/types/package-types/__expected__/geometry.tick.json @@ -5,7 +5,7 @@ "box": [ 8, 24, - 96.5625, + 173.5, 24 ], "style": { @@ -84,7 +84,7 @@ [ 8, 26, - 96.5625, + 173.5, 20 ] ] diff --git a/tests/integration/cases/types/package-types/__expected__/geometry.open.json b/tests/integration/cases/types/package-types/__expected__/geometry.tock.json similarity index 98% rename from tests/integration/cases/types/package-types/__expected__/geometry.open.json rename to tests/integration/cases/types/package-types/__expected__/geometry.tock.json index 9197bb2ee..bf97bb4d4 100644 --- a/tests/integration/cases/types/package-types/__expected__/geometry.open.json +++ b/tests/integration/cases/types/package-types/__expected__/geometry.tock.json @@ -5,7 +5,7 @@ "box": [ 8, 24, - 84.546875, + 192.328125, 24 ], "style": { @@ -84,7 +84,7 @@ [ 8, 26, - 84.546875, + 192.328125, 20 ] ] diff --git a/tests/integration/cases/types/package-types/__expected__/ssr.closed.html b/tests/integration/cases/types/package-types/__expected__/ssr.closed.html deleted file mode 100644 index d92c2ff33..000000000 --- a/tests/integration/cases/types/package-types/__expected__/ssr.closed.html +++ /dev/null @@ -1,3 +0,0 @@ -

- "Feed: closed" -

diff --git a/tests/integration/cases/types/package-types/__expected__/ssr.open.html b/tests/integration/cases/types/package-types/__expected__/ssr.open.html deleted file mode 100644 index b40575f9f..000000000 --- a/tests/integration/cases/types/package-types/__expected__/ssr.open.html +++ /dev/null @@ -1,3 +0,0 @@ -

- "Feed: open" -

diff --git a/tests/integration/cases/types/package-types/__expected__/ssr.tick.html b/tests/integration/cases/types/package-types/__expected__/ssr.tick.html new file mode 100644 index 000000000..89f95045e --- /dev/null +++ b/tests/integration/cases/types/package-types/__expected__/ssr.tick.html @@ -0,0 +1,3 @@ +

+ "Feed: tick after 250 ms" +

diff --git a/tests/integration/cases/types/package-types/__expected__/ssr.tock.html b/tests/integration/cases/types/package-types/__expected__/ssr.tock.html new file mode 100644 index 000000000..c3630b16b --- /dev/null +++ b/tests/integration/cases/types/package-types/__expected__/ssr.tock.html @@ -0,0 +1,3 @@ +

+ "Clock: tock after 1000 ms" +

diff --git a/tests/integration/cases/types/package-types/__output__/angular/status.ts b/tests/integration/cases/types/package-types/__output__/angular/status.ts index 5c583522c..13ae5b8fe 100644 --- a/tests/integration/cases/types/package-types/__output__/angular/status.ts +++ b/tests/integration/cases/types/package-types/__output__/angular/status.ts @@ -8,8 +8,6 @@ import { input, } from "@angular/core"; -export type StatusProps = Pick & { label: string }; - /** The scope attributes taken off each host, for the roots its component renders later. */ const scopes = new WeakMap(); @@ -41,18 +39,20 @@ export class ScopeAttributes implements OnInit { host: { "[attr.class]": "null", "[attr.style]": "'display: contents;'" }, preserveWhitespaces: false, template: ` - @let closed = this.closed(); + @let value = this.value(); + @let interval = this.interval(); @let label = this.label();

{{ label }}: {{ closed ? "closed" : "open" }}

+ >{{ label }}: {{ value }} after {{ interval }} ms

`, }) export default class Status { - readonly closed = input.required(); - readonly label = input.required(); + readonly value = input.required(); + readonly interval = input.required(); + readonly label = input.required