diff options
author | 2023-08-10 12:52:57 +0800 | |
---|---|---|
committer | 2023-08-10 12:52:57 +0800 | |
commit | 5b1e39ef6ec6dcebea96584f95d9530bd9aa715d (patch) | |
tree | 3a3b421b71a752f964b0c88e18824fc4c93570cd | |
parent | ea30a9d4f2d7a12345869e971f3051cf803dbe74 (diff) | |
download | astro-5b1e39ef6ec6dcebea96584f95d9530bd9aa715d.tar.gz astro-5b1e39ef6ec6dcebea96584f95d9530bd9aa715d.tar.zst astro-5b1e39ef6ec6dcebea96584f95d9530bd9aa715d.zip |
Move hoisted script analysis optimization as experimental (#8011)
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
-rw-r--r-- | .changeset/wild-jobs-tan.md | 5 | ||||
-rw-r--r-- | packages/astro/src/@types/astro.ts | 22 | ||||
-rw-r--r-- | packages/astro/src/core/build/plugins/index.ts | 2 | ||||
-rw-r--r-- | packages/astro/src/core/build/plugins/plugin-analyzer.ts | 92 | ||||
-rw-r--r-- | packages/astro/src/core/config/schema.ts | 2 | ||||
-rw-r--r-- | packages/astro/test/hoisted-imports.test.js | 3 |
6 files changed, 88 insertions, 38 deletions
diff --git a/.changeset/wild-jobs-tan.md b/.changeset/wild-jobs-tan.md new file mode 100644 index 000000000..8372c01b1 --- /dev/null +++ b/.changeset/wild-jobs-tan.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Move hoisted script analysis optimization behind the `experimental.optimizeHoistedScript` option diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 1e594332d..c2a7c3823 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1272,6 +1272,28 @@ export interface AstroUserConfig { * ``` */ viewTransitions?: boolean; + + /** + * @docs + * @name experimental.optimizeHoistedScript + * @type {boolean} + * @default `false` + * @version 2.10.4 + * @description + * Prevents unused components' scripts from being included in a page unexpectedly. + * The optimization is best-effort and may inversely miss including the used scripts. Make sure to double-check your built pages + * before publishing. + * Enable hoisted script analysis optimization by adding the experimental flag: + * + * ```js + * { + * experimental: { + * optimizeHoistedScript: true, + * }, + * } + * ``` + */ + optimizeHoistedScript?: boolean; }; // Legacy options to be removed diff --git a/packages/astro/src/core/build/plugins/index.ts b/packages/astro/src/core/build/plugins/index.ts index 3a44824d6..decfefd04 100644 --- a/packages/astro/src/core/build/plugins/index.ts +++ b/packages/astro/src/core/build/plugins/index.ts @@ -16,7 +16,7 @@ import { pluginSSR, pluginSSRSplit } from './plugin-ssr.js'; export function registerAllPlugins({ internals, options, register }: AstroBuildPluginContainer) { register(pluginComponentEntry(internals)); register(pluginAliasResolve(internals)); - register(pluginAnalyzer(internals)); + register(pluginAnalyzer(options, internals)); register(pluginInternals(internals)); register(pluginRenderers(options)); register(pluginMiddleware(options, internals)); diff --git a/packages/astro/src/core/build/plugins/plugin-analyzer.ts b/packages/astro/src/core/build/plugins/plugin-analyzer.ts index c05328005..f1229cb38 100644 --- a/packages/astro/src/core/build/plugins/plugin-analyzer.ts +++ b/packages/astro/src/core/build/plugins/plugin-analyzer.ts @@ -5,10 +5,10 @@ import type { BuildInternals } from '../internal.js'; import type { AstroBuildPlugin } from '../plugin.js'; import type { ExportDefaultDeclaration, ExportNamedDeclaration, ImportDeclaration } from 'estree'; -import { walk } from 'estree-walker'; import { PROPAGATED_ASSET_FLAG } from '../../../content/consts.js'; import { prependForwardSlash } from '../../../core/path.js'; import { getTopLevelPages, moduleIsTopLevelPage, walkParentInfos } from '../graph.js'; +import type { StaticBuildOptions } from '../types.js'; import { getPageDataByViteID, trackClientOnlyPageDatas } from '../internal.js'; function isPropagatedAsset(id: string) { @@ -30,29 +30,28 @@ async function doesParentImportChild( ): Promise<'no' | 'dynamic' | string[]> { if (!childInfo || !parentInfo.ast || !childExportNames) return 'no'; + // If we're dynamically importing the child, return `dynamic` directly to opt-out of optimization if (childExportNames === 'dynamic' || parentInfo.dynamicallyImportedIds?.includes(childInfo.id)) { return 'dynamic'; } const imports: Array<ImportDeclaration> = []; const exports: Array<ExportNamedDeclaration | ExportDefaultDeclaration> = []; - walk(parentInfo.ast, { - enter(node) { - if (node.type === 'ImportDeclaration') { - imports.push(node as ImportDeclaration); - } else if ( - node.type === 'ExportDefaultDeclaration' || - node.type === 'ExportNamedDeclaration' - ) { - exports.push(node as ExportNamedDeclaration | ExportDefaultDeclaration); - } - }, - }); - // All of the aliases the current component is imported as + for (const node of (parentInfo.ast as any).body) { + if (node.type === 'ImportDeclaration') { + imports.push(node); + } else if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportNamedDeclaration') { + exports.push(node); + } + } + + // All local import names that could be importing the child component const importNames: string[] = []; // All of the aliases the child component is exported as const exportNames: string[] = []; + // Iterate each import, find it they import the child component, if so, check if they + // import the child component name specifically. We can verify this with `childExportNames`. for (const node of imports) { const resolved = await this.resolve(node.source.value as string, parentInfo.id); if (!resolved || resolved.id !== childInfo.id) continue; @@ -67,14 +66,17 @@ async function doesParentImportChild( } } } + + // Iterate each export, find it they re-export the child component, and push the exported name to `exportNames` for (const node of exports) { if (node.type === 'ExportDefaultDeclaration') { if (node.declaration.type === 'Identifier' && importNames.includes(node.declaration.name)) { exportNames.push('default'); - // return } } else { - // handle `export { x } from 'something';`, where the export and import are in the same node + // Handle: + // export { Component } from './Component.astro' + // export { Component as AliasedComponent } from './Component.astro' if (node.source) { const resolved = await this.resolve(node.source.value as string, parentInfo.id); if (!resolved || resolved.id !== childInfo.id) continue; @@ -85,6 +87,9 @@ async function doesParentImportChild( } } } + // Handle: + // export const AliasedComponent = Component + // export const AliasedComponent = Component, let foo = 'bar' if (node.declaration) { if (node.declaration.type !== 'VariableDeclaration') continue; for (const declarator of node.declaration.declarations) { @@ -95,6 +100,9 @@ async function doesParentImportChild( } } } + // Handle: + // export { Component } + // export { Component as AliasedComponent } for (const specifier of node.specifiers) { if (importNames.includes(specifier.local.name)) { exportNames.push(specifier.exported.name); @@ -115,7 +123,10 @@ async function doesParentImportChild( return exportNames; } -export function vitePluginAnalyzer(internals: BuildInternals): VitePlugin { +export function vitePluginAnalyzer( + options: StaticBuildOptions, + internals: BuildInternals +): VitePlugin { function hoistedScriptScanner() { const uniqueHoistedIds = new Map<string, string>(); const pageScripts = new Map< @@ -139,6 +150,7 @@ export function vitePluginAnalyzer(internals: BuildInternals): VitePlugin { } if (hoistedScripts.size) { + // These variables are only used for hoisted script analysis optimization const depthsToChildren = new Map<number, ModuleInfo>(); const depthsToExportNames = new Map<number, string[] | 'dynamic'>(); // The component export from the original component file will always be default. @@ -147,25 +159,28 @@ export function vitePluginAnalyzer(internals: BuildInternals): VitePlugin { for (const [parentInfo, depth] of walkParentInfos(from, this, function until(importer) { return isPropagatedAsset(importer); })) { - depthsToChildren.set(depth, parentInfo); - // If at any point - if (depth > 0) { - // Check if the component is actually imported: - const childInfo = depthsToChildren.get(depth - 1); - const childExportNames = depthsToExportNames.get(depth - 1); - - const doesImport = await doesParentImportChild.call( - this, - parentInfo, - childInfo, - childExportNames - ); - - if (doesImport === 'no') { - // Break the search if the parent doesn't import the child. - continue; + // If hoisted script analysis optimization is enabled, try to analyse and bail early if possible + if (options.settings.config.experimental.optimizeHoistedScript) { + depthsToChildren.set(depth, parentInfo); + // If at any point + if (depth > 0) { + // Check if the component is actually imported: + const childInfo = depthsToChildren.get(depth - 1); + const childExportNames = depthsToExportNames.get(depth - 1); + + const doesImport = await doesParentImportChild.call( + this, + parentInfo, + childInfo, + childExportNames + ); + + if (doesImport === 'no') { + // Break the search if the parent doesn't import the child. + continue; + } + depthsToExportNames.set(depth, doesImport); } - depthsToExportNames.set(depth, doesImport); } if (isPropagatedAsset(parentInfo.id)) { @@ -310,13 +325,16 @@ export function vitePluginAnalyzer(internals: BuildInternals): VitePlugin { }; } -export function pluginAnalyzer(internals: BuildInternals): AstroBuildPlugin { +export function pluginAnalyzer( + options: StaticBuildOptions, + internals: BuildInternals +): AstroBuildPlugin { return { build: 'ssr', hooks: { 'build:before': () => { return { - vitePlugin: vitePluginAnalyzer(internals), + vitePlugin: vitePluginAnalyzer(options, internals), }; }, }, diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 34c1e5549..e7e504738 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -46,6 +46,7 @@ const ASTRO_CONFIG_DEFAULTS = { experimental: { assets: false, viewTransitions: false, + optimizeHoistedScript: false }, } satisfies AstroUserConfig & { server: { open: boolean } }; @@ -237,6 +238,7 @@ export const AstroConfigSchema = z.object({ .boolean() .optional() .default(ASTRO_CONFIG_DEFAULTS.experimental.viewTransitions), + optimizeHoistedScript: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.optimizeHoistedScript), }) .passthrough() .refine( diff --git a/packages/astro/test/hoisted-imports.test.js b/packages/astro/test/hoisted-imports.test.js index 973d7103e..c5ce98e05 100644 --- a/packages/astro/test/hoisted-imports.test.js +++ b/packages/astro/test/hoisted-imports.test.js @@ -8,6 +8,9 @@ describe('Hoisted Imports', () => { before(async () => { fixture = await loadFixture({ root: './fixtures/hoisted-imports/', + experimental: { + optimizeHoistedScript: true, + }, }); }); |