diff --git a/code/builders/builder-webpack5/src/presets/custom-webpack-preset.ts b/code/builders/builder-webpack5/src/presets/custom-webpack-preset.ts index 20947f59e5a9..91dc2e73e0c7 100644 --- a/code/builders/builder-webpack5/src/presets/custom-webpack-preset.ts +++ b/code/builders/builder-webpack5/src/presets/custom-webpack-preset.ts @@ -23,10 +23,7 @@ export const swc: PresetProperty<'swc'> = (config: Record): Record< safari: 15, firefox: 91, }, - // Transpiles the broken syntax to the closest non-broken modern syntax. - // E.g. it won't transpile parameter destructuring in Safari - // which would break how we detect if the mount context property is used in the play function. - bugfixes: config?.env?.bugfixes ?? true, + bugfixes: config?.env?.bugfixes ?? false, }, }; }; diff --git a/code/core/src/core-server/presets/common-preset.ts b/code/core/src/core-server/presets/common-preset.ts index 86f706ca4c34..9aa59d705539 100644 --- a/code/core/src/core-server/presets/common-preset.ts +++ b/code/core/src/core-server/presets/common-preset.ts @@ -120,7 +120,6 @@ export const babel = async (_: unknown, options: Options) => { [ '@babel/preset-env', { - bugfixes: true, targets: { // This is the same browser supports that we use to bundle our manager and preview code. chrome: 100, diff --git a/code/core/src/preview-api/modules/preview-web/render/mount-utils.test.ts b/code/core/src/preview-api/modules/preview-web/render/mount-utils.test.ts index 80c1a56211b1..d594c69af0d7 100644 --- a/code/core/src/preview-api/modules/preview-web/render/mount-utils.test.ts +++ b/code/core/src/preview-api/modules/preview-web/render/mount-utils.test.ts @@ -32,20 +32,112 @@ const LongDefinition = { }, }; -test('Detect destructure', () => { +const MethodProperty = { + async play({ + mount, + veryLongDefinitionnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn, + over, + multiple, + lines, + }: any) { + await mount(); + }, +}; + +const TranspiledDefinition = { + play: async (context: any) => { + const { + mount, + veryLongTranspiledDefinitionnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn, + over, + multiple, + lines, + } = context; + await mount(); + }, +}; + +const LateDestructuring = { + play: async (a: any) => { + console.log(a); + const { + mount, + veryLongTranspiledDefinitionnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn, + over, + multiple, + lines, + } = a; + await mount(); + }, +}; + +const WithComment = { + play: async (context: any) => { + const { + // a comment + mount, + } = context; + await mount(); + }, +}; + +const WithTrailingComment = { + play: async (context: any) => { + const { + mount, // a comment + } = context; + await mount(); + }, +}; + +const WithMultipleComments = { + play: async (context: any) => { + const { + mount, // a comment + // another comment + } = context; + await mount(); + }, +}; + +const WithBlockComments = { + play: async (context: any) => { + const { mount /* a comment */ } = context; + /* another comment */ + await mount(); + /* third comment */ + }, +}; + +const testingScope = { + mount: async (m: any) => { + return 'testingScope.mount'; + }, +}; + +const IncorrectMount = { + play: async (context: any) => { + const { mount } = testingScope; + const { mount: sbMount } = context; + mount(await sbMount()); + }, +}; + +test('Detect basic destructuring', () => { expect(getUsedProps(StoryWithContext.play)).toMatchInlineSnapshot(`[]`); expect(getUsedProps(StoryWitCanvasElement.play)).toMatchInlineSnapshot(` [ "canvasElement", ] `); - expect(getUsedProps(MountStory.play)).toMatchInlineSnapshot(` [ "mount", ] `); +}); +test('Detect multiline destructuring', () => { expect(getUsedProps(LongDefinition.play)).toMatchInlineSnapshot(` [ "mount", @@ -55,4 +147,63 @@ test('Detect destructure', () => { "lines", ] `); + expect(getUsedProps(MethodProperty.play)).toMatchInlineSnapshot(` + [ + "mount", + "veryLongDefinitionnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn", + "over", + "multiple", + "lines", + ] + `); +}); + +test('Detect transpiled destructuring', () => { + expect(getUsedProps(TranspiledDefinition.play)).toMatchInlineSnapshot(` + [ + "mount", + "veryLongTranspiledDefinitionnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn", + "over", + "multiple", + "lines", + ] + `); + + expect(getUsedProps(LateDestructuring.play)).toMatchInlineSnapshot(`[]`); +}); + +test('Detect with comment', () => { + expect(getUsedProps(WithComment.play)).toMatchInlineSnapshot(` + [ + "mount", + ] + `); +}); + +test('Detect with trailing comment', () => { + expect(getUsedProps(WithTrailingComment.play)).toMatchInlineSnapshot(` + [ + "mount", + ] + `); +}); + +test('Detect with multiple comments', () => { + expect(getUsedProps(WithMultipleComments.play)).toMatchInlineSnapshot(` + [ + "mount", + ] + `); +}); + +test('Detect with block comments', () => { + expect(getUsedProps(WithBlockComments.play)).toMatchInlineSnapshot(` + [ + "mount", + ] + `); +}); + +test('Detect incorrect mount', () => { + expect(getUsedProps(IncorrectMount.play)).toMatchInlineSnapshot(`[]`); }); diff --git a/code/core/src/preview-api/modules/preview-web/render/mount-utils.ts b/code/core/src/preview-api/modules/preview-web/render/mount-utils.ts index 4683ca743ac3..086ee3931f5a 100644 --- a/code/core/src/preview-api/modules/preview-web/render/mount-utils.ts +++ b/code/core/src/preview-api/modules/preview-web/render/mount-utils.ts @@ -4,32 +4,70 @@ export function mountDestructured(playFunction?: (...args: any[]) => any): boole return playFunction != null && getUsedProps(playFunction).includes('mount'); } -export function getUsedProps(fn: (...args: any[]) => any) { - const match = fn.toString().match(/[^(]*\(([^)]*)/); - - if (!match) { +/** + * Extracts a list of properties destructured from the argument of a play function, either inline or + * as the first statement in the body of the function. + * + * @param fn - The function to extract the properties from. + * @returns An array of property names. + */ +export function getUsedProps(fn: (...args: unknown[]) => unknown) { + const [, args, body] = fn.toString().match(/[^(]*\(([^)]+)\)(?:.*{([^]+)})?/) || []; + if (!args) { return []; } - const args = splitByComma(match[1]); - - if (!args.length) { + const [firstArg] = splitByComma(args); + if (!firstArg) { return []; } - const first = args[0]; + const [, destructuredProps] = firstArg.match(/^{([^]+)}$/) || []; + if (destructuredProps) { + return splitByComma(stripComments(destructuredProps)).map((prop) => + prop.replace(/:.*|=.*/g, '').trim() + ); + } - if (!(first.startsWith('{') && first.endsWith('}'))) { + if (!firstArg.match(/^[a-z_$][0-9a-z_$]*$/i)) { return []; } - const props = splitByComma(first.slice(1, -1).replace(/\s/g, '')).map((prop) => { - return prop.replace(/:.*|=.*/g, ''); - }); + const escapedArg = firstArg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const [, destructuredArg] = + body?.trim()?.match(new RegExp(`^(?:const|let|var)\\s*{([^}]+)}\\s*=\\s*${escapedArg};`)) || []; + if (destructuredArg) { + return splitByComma(stripComments(destructuredArg)).map((prop) => + prop.replace(/:.*|=.*/g, '').trim() + ); + } + + return []; +} - return props; +/** + * Strips JavaScript comments from a string. + * + * @param s - The string to strip comments from. + * @returns The string with comments removed. + */ +function stripComments(s: string): string { + // Remove single-line comments (// ...) + s = s.replace(/\/\/.*$/gm, ''); + // Remove multi-line comments (/* ... */) + s = s.replace(/\/\*[\s\S]*?\*\//g, ''); + return s; } +/** + * Splits a string by top-level commas, ignoring commas nested within curly or square brackets. + * + * This is useful for parsing function argument lists or destructured object patterns where elements + * inside nested structures (like { a, b: [x, y], c }) should not be split. + * + * @param s - The string to split. + * @returns An array of substrings split by top-level commas. + */ function splitByComma(s: string) { const result = []; const stack = []; diff --git a/code/frameworks/nextjs/src/babel/preset.ts b/code/frameworks/nextjs/src/babel/preset.ts index ce450eade352..a63ea2fef03d 100644 --- a/code/frameworks/nextjs/src/babel/preset.ts +++ b/code/frameworks/nextjs/src/babel/preset.ts @@ -92,7 +92,6 @@ export default (api: any, options: NextBabelPresetOptions = {}): BabelPreset => // In production/development this option is set to `false` so that webpack can handle import/export with tree-shaking modules: 'auto', exclude: ['transform-typeof-symbol'], - bugfixes: true, targets: { chrome: 100, safari: 15, diff --git a/code/frameworks/nextjs/src/preset.ts b/code/frameworks/nextjs/src/preset.ts index 53cf944db194..d4420baff8bd 100644 --- a/code/frameworks/nextjs/src/preset.ts +++ b/code/frameworks/nextjs/src/preset.ts @@ -126,7 +126,6 @@ export const babel: PresetProperty<'babel'> = async (baseConfig: TransformOption [ 'next/dist/compiled/babel/preset-env', { - bugfixes: true, targets: { chrome: 100, safari: 15, diff --git a/code/frameworks/nextjs/src/swc/next-swc-loader-patch.ts b/code/frameworks/nextjs/src/swc/next-swc-loader-patch.ts index 622e52e38406..ceac39b1f740 100644 --- a/code/frameworks/nextjs/src/swc/next-swc-loader-patch.ts +++ b/code/frameworks/nextjs/src/swc/next-swc-loader-patch.ts @@ -113,10 +113,6 @@ async function loaderTransform(this: any, parentTrace: any, source?: string, inp // modules. sourceFileName: filename, }; - // Transpiles the broken syntax to the closest non-broken modern syntax. - // E.g. it won't transpile parameter destructuring in Safari - // which would break how we detect if the mount context property is used in the play function. - programmaticOptions.env.bugfixes = true; if (!programmaticOptions.inputSourceMap) { delete programmaticOptions.inputSourceMap;