diff --git a/code/e2e-sandbox/framework-vue3.spec.ts b/code/e2e-sandbox/framework-vue3.spec.ts index abc38c7a632d..91051bc5be85 100644 --- a/code/e2e-sandbox/framework-vue3.spec.ts +++ b/code/e2e-sandbox/framework-vue3.spec.ts @@ -46,4 +46,42 @@ test.describe('Vue 3', () => { await sbPage.selectToolbar('[aria-label^="Internationalization locale"]', 'text=/Español/'); await expect(sbPage.previewRoot()).toContainText('Hola'); }); + + // TODO: Re-enable this test once vue-component-meta is the default docgen and supports Vue 3.5 syntax. + test.skip('docs preserve unicode prop defaults from vue-component-meta', async ({ page }) => { + const sbPage = new SbPage(page, expect); + const docsPages = [ + { + title: 'stories/renderers/vue3_vue3-vite-default-ts/component-meta/unicode-with-defaults', + expectedDefaults: [ + ['Greeting', 'こんにちは'], + ['size', '大きい'], + ['icon', '🚀'], + ], + }, + { + title: 'stories/renderers/vue3_vue3-vite-default-ts/component-meta/unicode-define-props', + expectedDefaults: [ + ['Greeting', '你好'], + ['size', '大きい'], + ['icon', '✨'], + ], + }, + ]; + + for (const { title, expectedDefaults } of docsPages) { + await sbPage.deepLinkToStory(storybookUrl, title, 'docs'); + + const argsTable = sbPage.previewRoot().locator('.docblock-argstable'); + await expect(argsTable).toBeVisible(); + + for (const [propName, defaultValue] of expectedDefaults) { + await expect(argsTable.getByRole('row', { name: new RegExp(propName) })).toContainText( + defaultValue + ); + } + + await expect(argsTable).not.toContainText('\\u'); + } + }); }); diff --git a/code/frameworks/vue3-vite/package.json b/code/frameworks/vue3-vite/package.json index 9b6fc9154927..4637ba74d736 100644 --- a/code/frameworks/vue3-vite/package.json +++ b/code/frameworks/vue3-vite/package.json @@ -53,7 +53,7 @@ "@storybook/vue3": "workspace:*", "magic-string": "^0.30.0", "typescript": "^5.9.3", - "vue-component-meta": "^2.0.0", + "vue-component-meta": "^3.2.7", "vue-docgen-api": "^4.75.1" }, "devDependencies": { diff --git a/code/renderers/vue3/src/docs/tests-meta-components/meta-components.ts b/code/renderers/vue3/src/docs/tests-meta-components/meta-components.ts index 10b687fb67b0..a5c42d74e1aa 100644 --- a/code/renderers/vue3/src/docs/tests-meta-components/meta-components.ts +++ b/code/renderers/vue3/src/docs/tests-meta-components/meta-components.ts @@ -1,10 +1,146 @@ import { TypeSystem } from 'storybook/internal/docs-tools'; import type { VueDocgenInfo } from '../../../../../frameworks/vue3-vite/src'; +import type { Declaration } from 'vue-component-meta'; +import type ts from 'typescript'; type TestComponent = { __docgenInfo: VueDocgenInfo<'vue-component-meta'> }; +type LooseTestComponent = { + __docgenInfo: { + type: number; + props: unknown[]; + events: unknown[]; + slots: unknown[]; + exposed: unknown[]; + }; +}; + +const ensureArray = (value: unknown): unknown[] => (Array.isArray(value) ? value : []); + +const ensureMetaMethods = (entry: Record, allowUndefinedType = false) => { + if (!Array.isArray(entry.declarations)) { + entry.declarations = []; + } + + if (typeof entry.getDeclarations !== 'function') { + entry.getDeclarations = () => entry.declarations as Declaration[]; + } + + if (typeof entry.getTypeObject !== 'function') { + entry.getTypeObject = () => { + if (allowUndefinedType) { + return undefined; + } + + return entry.rawType as ts.Type; + }; + } +}; + +const normalizePropertySchema = (schema: unknown) => { + if (!schema || typeof schema !== 'object') { + return; + } + + const schemaEntry = schema as Record; + if ( + schemaEntry.kind === 'object' && + schemaEntry.schema && + typeof schemaEntry.schema === 'object' + ) { + Object.values(schemaEntry.schema as Record).forEach((property) => { + normalizeProperty(property); + }); + return; + } + + const nestedSchema = ensureArray(schemaEntry.schema); + nestedSchema.forEach((nested) => { + if (!nested || typeof nested !== 'object') { + return; + } + + normalizePropertySchema(nested); + }); +}; + +const normalizeProperty = (property: unknown) => { + if (!property || typeof property !== 'object') { + return; + } + + const propertyEntry = property as Record; + ensureMetaMethods(propertyEntry); + normalizePropertySchema(propertyEntry.schema); +}; + +const normalizeEvent = (event: unknown) => { + if (!event || typeof event !== 'object') { + return; + } + + const eventEntry = event as Record; + if (!Array.isArray(eventEntry.tags)) { + eventEntry.tags = []; + } + ensureMetaMethods(eventEntry, true); + + ensureArray(eventEntry.schema).forEach((schema) => { + normalizePropertySchema(schema); + }); +}; + +const normalizeSlotOrExpose = (entry: unknown) => { + if (!entry || typeof entry !== 'object') { + return; + } + + const slotOrExposeEntry = entry as Record; + if (!Array.isArray(slotOrExposeEntry.tags)) { + slotOrExposeEntry.tags = []; + } + ensureMetaMethods(slotOrExposeEntry); + normalizePropertySchema(slotOrExposeEntry.schema); +}; + +const hasMetaMethods = (entry: unknown) => { + if (!entry || typeof entry !== 'object') { + return false; + } -export const referenceTypeProps: TestComponent = { + const value = entry as Record; + return typeof value.getDeclarations === 'function' && typeof value.getTypeObject === 'function'; +}; + +const isTestComponent = (component: LooseTestComponent): component is TestComponent => { + const { __docgenInfo } = component; + + const propsOk = __docgenInfo.props.every(hasMetaMethods); + const eventsOk = __docgenInfo.events.every(hasMetaMethods); + const slotsOk = __docgenInfo.slots.every( + (slot) => hasMetaMethods(slot) && Array.isArray((slot as Record).tags) + ); + const exposedOk = __docgenInfo.exposed.every(hasMetaMethods); + + return propsOk && eventsOk && slotsOk && exposedOk; +}; + +const normalizeComponentMetaFixture = (component: LooseTestComponent): TestComponent => { + const { __docgenInfo } = component; + + __docgenInfo.props.forEach(normalizeProperty); + __docgenInfo.events.forEach(normalizeEvent); + __docgenInfo.slots.forEach(normalizeSlotOrExpose); + __docgenInfo.exposed.forEach(normalizeSlotOrExpose); + + if (!isTestComponent(component)) { + throw new TypeError('Failed to normalize vue-component-meta fixture.'); + } + + return component; +}; + +export const referenceTypeProps: TestComponent = normalizeComponentMetaFixture({ __docgenInfo: { type: 1, props: [ @@ -473,7 +609,7 @@ export const referenceTypeProps: TestComponent = { slots: [], exposed: [], }, -}; +}); export const mockExtractComponentPropsReturn = [ { @@ -1211,7 +1347,7 @@ export const mockExtractComponentPropsReturn = [ }, ]; -export const referenceTypeEvents: TestComponent = { +export const referenceTypeEvents: TestComponent = normalizeComponentMetaFixture({ __docgenInfo: { type: 1, props: [], @@ -1298,7 +1434,7 @@ export const referenceTypeEvents: TestComponent = { slots: [], exposed: [], }, -}; +}); export const mockExtractComponentEventsReturn = [ { @@ -1408,7 +1544,7 @@ export const mockExtractComponentEventsReturn = [ }, ]; -export const templateSlots: TestComponent = { +export const templateSlots: TestComponent = normalizeComponentMetaFixture({ __docgenInfo: { type: 1, props: [], @@ -1504,7 +1640,7 @@ export const templateSlots: TestComponent = { ], exposed: [], }, -}; +}); export const mockExtractComponentSlotsReturn = [ { diff --git a/code/renderers/vue3/template/stories_vue3-vite-default-ts/component-meta/UnicodeDefineProps.stories.ts b/code/renderers/vue3/template/stories_vue3-vite-default-ts/component-meta/UnicodeDefineProps.stories.ts new file mode 100644 index 000000000000..e0587c8973cf --- /dev/null +++ b/code/renderers/vue3/template/stories_vue3-vite-default-ts/component-meta/UnicodeDefineProps.stories.ts @@ -0,0 +1,14 @@ +import type { Meta, StoryObj } from '@storybook/vue3'; + +import Component from './unicode-defaults/destructured-define-props.vue'; + +const meta = { + component: Component, + tags: ['autodocs'], + title: 'stories/renderers/vue3_vue3-vite-default-ts/component-meta/unicode-define-props', +} satisfies Meta; + +type Story = StoryObj; +export default meta; + +export const Default: Story = {}; diff --git a/code/renderers/vue3/template/stories_vue3-vite-default-ts/component-meta/UnicodeWithDefaults.stories.ts b/code/renderers/vue3/template/stories_vue3-vite-default-ts/component-meta/UnicodeWithDefaults.stories.ts new file mode 100644 index 000000000000..4ff71419fe61 --- /dev/null +++ b/code/renderers/vue3/template/stories_vue3-vite-default-ts/component-meta/UnicodeWithDefaults.stories.ts @@ -0,0 +1,14 @@ +import type { Meta, StoryObj } from '@storybook/vue3'; + +import Component from './unicode-defaults/with-defaults.vue'; + +const meta = { + component: Component, + tags: ['autodocs'], + title: 'stories/renderers/vue3_vue3-vite-default-ts/component-meta/unicode-with-defaults', +} satisfies Meta; + +type Story = StoryObj; +export default meta; + +export const Default: Story = {}; diff --git a/code/renderers/vue3/template/stories_vue3-vite-default-ts/component-meta/unicode-defaults/destructured-define-props.vue b/code/renderers/vue3/template/stories_vue3-vite-default-ts/component-meta/unicode-defaults/destructured-define-props.vue new file mode 100644 index 000000000000..49e06735848c --- /dev/null +++ b/code/renderers/vue3/template/stories_vue3-vite-default-ts/component-meta/unicode-defaults/destructured-define-props.vue @@ -0,0 +1,24 @@ + + + diff --git a/code/renderers/vue3/template/stories_vue3-vite-default-ts/component-meta/unicode-defaults/with-defaults.vue b/code/renderers/vue3/template/stories_vue3-vite-default-ts/component-meta/unicode-defaults/with-defaults.vue new file mode 100644 index 000000000000..8b01563149fa --- /dev/null +++ b/code/renderers/vue3/template/stories_vue3-vite-default-ts/component-meta/unicode-defaults/with-defaults.vue @@ -0,0 +1,27 @@ + + + diff --git a/yarn.lock b/yarn.lock index 1077f1ac70d7..3908e125f72b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10358,7 +10358,7 @@ __metadata: magic-string: "npm:^0.30.0" typescript: "npm:^5.9.3" vite: "npm:^7.0.4" - vue-component-meta: "npm:^2.0.0" + vue-component-meta: "npm:^3.2.7" vue-docgen-api: "npm:^4.75.1" peerDependencies: storybook: "workspace:^" @@ -12859,15 +12859,6 @@ __metadata: languageName: node linkType: hard -"@volar/language-core@npm:2.4.15": - version: 2.4.15 - resolution: "@volar/language-core@npm:2.4.15" - dependencies: - "@volar/source-map": "npm:2.4.15" - checksum: 10c0/34ebd2a0f19ff5ee676d990dd3cc63a72867ab39a39071b0699486cf6e1304cad136cace78a55eaba41340720aa60863a4a9646f338bcca4bb9e0e22bd5d431e - languageName: node - linkType: hard - "@volar/language-core@npm:2.4.27": version: 2.4.27 resolution: "@volar/language-core@npm:2.4.27" @@ -12886,13 +12877,6 @@ __metadata: languageName: node linkType: hard -"@volar/source-map@npm:2.4.15": - version: 2.4.15 - resolution: "@volar/source-map@npm:2.4.15" - checksum: 10c0/f1c353774c1e758d99ebadc2fffd416eeba5fa5f461597cef5d201ca4ea5a81ad9d7eb1c418d61b3cadaed1db2fef3fa0a29e85a02f08bba6fab04578736959a - languageName: node - linkType: hard - "@volar/source-map@npm:2.4.27": version: 2.4.27 resolution: "@volar/source-map@npm:2.4.27" @@ -12907,17 +12891,6 @@ __metadata: languageName: node linkType: hard -"@volar/typescript@npm:2.4.15": - version: 2.4.15 - resolution: "@volar/typescript@npm:2.4.15" - dependencies: - "@volar/language-core": "npm:2.4.15" - path-browserify: "npm:^1.0.1" - vscode-uri: "npm:^3.0.8" - checksum: 10c0/f8d9e2f34e3d47edd3db51c23a4b14c48098e145c31566e463ec468a472bb3679426652c73a605a501977690749f7330c463a110283ce5d2c221f411b5fd889b - languageName: node - linkType: hard - "@volar/typescript@npm:2.4.27": version: 2.4.27 resolution: "@volar/typescript@npm:2.4.27" @@ -12929,7 +12902,7 @@ __metadata: languageName: node linkType: hard -"@volar/typescript@npm:~2.4.28": +"@volar/typescript@npm:2.4.28, @volar/typescript@npm:~2.4.28": version: 2.4.28 resolution: "@volar/typescript@npm:2.4.28" dependencies: @@ -12990,49 +12963,33 @@ __metadata: languageName: node linkType: hard -"@vue/compiler-vue2@npm:^2.7.16": - version: 2.7.16 - resolution: "@vue/compiler-vue2@npm:2.7.16" - dependencies: - de-indent: "npm:^1.0.2" - he: "npm:^1.2.0" - checksum: 10c0/c76c3fad770b9a7da40b314116cc9da173da20e5fd68785c8ed8dd8a87d02f239545fa296e16552e040ec86b47bfb18283b39447b250c2e76e479bd6ae475bb3 - languageName: node - linkType: hard - -"@vue/language-core@npm:2.2.12": - version: 2.2.12 - resolution: "@vue/language-core@npm:2.2.12" +"@vue/language-core@npm:3.2.1": + version: 3.2.1 + resolution: "@vue/language-core@npm:3.2.1" dependencies: - "@volar/language-core": "npm:2.4.15" + "@volar/language-core": "npm:2.4.27" "@vue/compiler-dom": "npm:^3.5.0" - "@vue/compiler-vue2": "npm:^2.7.16" "@vue/shared": "npm:^3.5.0" - alien-signals: "npm:^1.0.3" - minimatch: "npm:^9.0.3" + alien-signals: "npm:^3.0.0" muggle-string: "npm:^0.4.1" path-browserify: "npm:^1.0.1" - peerDependencies: - typescript: "*" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10c0/b69864af058d757f9774390f4ced8e79d569703088c708736aba987c3a2f4072f2aa92dee81051891bd060998c5383dd857b9107e2c9f6c3ba0f288f826f521e + picomatch: "npm:^4.0.2" + checksum: 10c0/d83021599d6efbd8a54e8f8ac0c8eed3af44ada111a5f403c0c44537adc66a6bf03668de5786828d41dc38226d8372e821be23df4cc0ea506155985440e74406 languageName: node linkType: hard -"@vue/language-core@npm:3.2.1": - version: 3.2.1 - resolution: "@vue/language-core@npm:3.2.1" +"@vue/language-core@npm:3.3.2": + version: 3.3.2 + resolution: "@vue/language-core@npm:3.3.2" dependencies: - "@volar/language-core": "npm:2.4.27" + "@volar/language-core": "npm:2.4.28" "@vue/compiler-dom": "npm:^3.5.0" "@vue/shared": "npm:^3.5.0" - alien-signals: "npm:^3.0.0" + alien-signals: "npm:^3.2.0" muggle-string: "npm:^0.4.1" path-browserify: "npm:^1.0.1" - picomatch: "npm:^4.0.2" - checksum: 10c0/d83021599d6efbd8a54e8f8ac0c8eed3af44ada111a5f403c0c44537adc66a6bf03668de5786828d41dc38226d8372e821be23df4cc0ea506155985440e74406 + picomatch: "npm:^4.0.4" + checksum: 10c0/f80870a9383c8721b50281e54c5b9f3513d082e71fb6654c7da11de07b2dafa666c0bdadabee559f5284fcd44825ba2064c148fd22f230f3e6646debb5a69fbe languageName: node linkType: hard @@ -13542,14 +13499,7 @@ __metadata: languageName: node linkType: hard -"alien-signals@npm:^1.0.3": - version: 1.0.13 - resolution: "alien-signals@npm:1.0.13" - checksum: 10c0/7b0ec8305eaacf810ab3fa7e2045c343e695dca510befc148ccef26cb59b8f159d9d56772fbd8767f85cbccc6fadcc09b9f90a4e1e1206d5f726b8995f24dbe4 - languageName: node - linkType: hard - -"alien-signals@npm:^3.0.0": +"alien-signals@npm:^3.0.0, alien-signals@npm:^3.2.0": version: 3.2.1 resolution: "alien-signals@npm:3.2.1" checksum: 10c0/4c4064faa208126177224d1ed6a2310687d452dec0771994e276d9af4c72e853fcb969ae4a7fcd034b1d1b9accb9500f4941178326eeea1cb8f64ec612853ef8 @@ -16749,13 +16699,6 @@ __metadata: languageName: node linkType: hard -"de-indent@npm:^1.0.2": - version: 1.0.2 - resolution: "de-indent@npm:1.0.2" - checksum: 10c0/7058ce58abd6dfc123dd204e36be3797abd419b59482a634605420f47ae97639d0c183ec5d1b904f308a01033f473673897afc2bd59bc620ebf1658763ef4291 - languageName: node - linkType: hard - "debug@npm:2.6.9, debug@npm:^2.1.3, debug@npm:^2.2.0, debug@npm:^2.6.8": version: 2.6.9 resolution: "debug@npm:2.6.9" @@ -34198,24 +34141,23 @@ __metadata: languageName: node linkType: hard -"vue-component-meta@npm:^2.0.0": - version: 2.2.12 - resolution: "vue-component-meta@npm:2.2.12" +"vue-component-meta@npm:^3.2.7": + version: 3.3.2 + resolution: "vue-component-meta@npm:3.3.2" dependencies: - "@volar/typescript": "npm:2.4.15" - "@vue/language-core": "npm:2.2.12" + "@volar/typescript": "npm:2.4.28" + "@vue/language-core": "npm:3.3.2" path-browserify: "npm:^1.0.1" - vue-component-type-helpers: "npm:2.2.12" peerDependencies: typescript: "*" peerDependenciesMeta: typescript: optional: true - checksum: 10c0/2171fa5618b7799ac440ffcbce0a103181a1562d529ed6090304e290808d0c544cce08d7d8c82b68d32b5e9de5dce28e1e47bd931c0eb849261f23dfc5754150 + checksum: 10c0/d78dd5c19474605e96812b6a81987c4163de29a704c3dc3ae7aaf627e9533af4e0af0db6323e0295e46d98c2146abb21912428e6f27c11051ef5876573f21d3d languageName: node linkType: hard -"vue-component-type-helpers@npm:2.2.12, vue-component-type-helpers@npm:^2.0.0": +"vue-component-type-helpers@npm:^2.0.0": version: 2.2.12 resolution: "vue-component-type-helpers@npm:2.2.12" checksum: 10c0/ce15a2cdec4a57262a292ac1565aa18da9be73f3dfbcf28758a8a541199944bfff1aefb75802d6de5d955a5529c9667f1b651c659d14815c075e8965ac05175d