From 33026a70e1a00a8fa027b9b4a3d987d3fc67eea2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 25 Nov 2024 16:49:00 -0500 Subject: [PATCH] link snippets to render tags and vice versa --- .../src/compiler/phases/1-parse/state/tag.js | 2 +- .../src/compiler/phases/2-analyze/index.js | 14 +++- .../phases/2-analyze/visitors/RenderTag.js | 26 ++++++-- .../phases/2-analyze/visitors/SnippetBlock.js | 2 + .../2-analyze/visitors/shared/component.js | 64 ++++++++++++++++++- .../svelte/src/compiler/phases/types.d.ts | 11 ++++ .../svelte/src/compiler/types/template.d.ts | 10 +-- 7 files changed, 114 insertions(+), 15 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index 27926ba529..9c1e6c27b2 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -613,7 +613,7 @@ function special(parser) { metadata: { dynamic: false, args_with_call_expression: new Set(), - snippets: [] + snippets: new Set() } }); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 0d8caa0a4b..552abc447d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -438,7 +438,9 @@ export function analyze_component(root, source, options) { : '', keyframes: [] }, - source + source, + snippet_renderers: new Map(), + snippets: new Set() }; if (!runes) { @@ -698,6 +700,16 @@ export function analyze_component(root, source, options) { ); } + for (const [node, resolved] of analysis.snippet_renderers) { + if (!resolved) { + node.metadata.snippets = analysis.snippets; + } + + for (const snippet of node.metadata.snippets) { + snippet.metadata.sites.add(node); + } + } + if ( analysis.uses_render_tags && (analysis.uses_slots || (!analysis.custom_element && analysis.slot_names.size > 0)) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js index 9a7ec897dd..ce6f677a2b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js @@ -17,12 +17,28 @@ export function RenderTag(node, context) { const callee = unwrap_optional(node.expression).callee; - node.metadata.dynamic = - callee.type !== 'Identifier' || context.state.scope.get(callee.name)?.kind !== 'normal'; - - // TODO populate node.metadata.snippets with all the locally-defined snippets that this - // could refer to, and create bidirectional links + const binding = callee.type === 'Identifier' ? context.state.scope.get(callee.name) : undefined; + + node.metadata.dynamic = binding?.kind !== 'normal'; + + /** If we can't unambiguously resolve this to a declaration, we + * must assume the worst and link the render tag to every snippet + */ + let resolved = + callee.type === 'Identifier' && + (!binding || + binding.declaration_kind === 'import' || + binding.kind === 'prop' || + binding.kind === 'rest_prop' || + binding.kind === 'bindable_prop' || + binding?.initial?.type === 'SnippetBlock'); + + if (binding?.initial?.type === 'SnippetBlock') { + // if this render tag unambiguously references a local snippet, our job is easy + node.metadata.snippets.add(binding.initial); + } + context.state.analysis.snippet_renderers.set(node, resolved); context.state.analysis.uses_render_tags = true; const raw_args = unwrap_optional(node.expression).arguments; diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js index 356cf2eeae..c94b65e361 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js @@ -8,6 +8,8 @@ import * as e from '../../../errors.js'; * @param {Context} context */ export function SnippetBlock(node, context) { + context.state.analysis.snippets.add(node); + validate_block_not_empty(node.body, context); if (context.state.analysis.runes) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js index 05edc55488..58cf0398aa 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js @@ -1,4 +1,5 @@ /** @import { AST } from '#compiler' */ +/** @import { Expression } from 'estree' */ /** @import { AnalysisState, Context } from '../../types' */ import * as e from '../../../../errors.js'; import { get_attribute_expression, is_expression_attribute } from '../../../../utils/ast.js'; @@ -15,10 +16,67 @@ import { mark_subtree_dynamic } from './fragment.js'; * @param {Context} context */ export function visit_component(node, context) { - node.metadata.snippets = []; + // link this node to all the snippets that it could render, so that we can prune CSS correctly + node.metadata.snippets = new Set(); - // TODO populate node.metadata.snippets with all the locally-defined snippets that this - // could refer to, and create bidirectional links + let resolved = true; + + for (const attribute of node.attributes) { + /** @type {Expression | undefined} */ + let expression; + + if (attribute.type === 'SpreadAttribute' || attribute.type === 'BindDirective') { + resolved = false; + continue; + } + + if (attribute.type !== 'Attribute' || attribute.value === true) { + continue; + } + + if (Array.isArray(attribute.value)) { + if (attribute.value.length === 1 && attribute.value[0].type === 'ExpressionTag') { + expression = attribute.value[0].expression; + } + } else { + expression = attribute.value.expression; + } + + if (!expression) continue; + + if (expression.type === 'Identifier') { + const binding = context.state.scope.get(expression.name); + + if ( + binding && + binding.declaration_kind !== 'import' && + binding.kind !== 'prop' && + binding.kind !== 'rest_prop' && + binding.kind !== 'bindable_prop' && + binding.initial?.type !== 'SnippetBlock' + ) { + resolved = false; + } + + if (binding?.initial?.type === 'SnippetBlock') { + node.metadata.snippets.add(binding.initial); + } + } else { + // we can't safely know which snippets this component could render, + // so we deopt. this _could_ result in unused CSS not being discarded + resolved = false; + } + } + + if (resolved) { + for (const child of node.fragment.nodes) { + if (child.type === 'SnippetBlock') { + node.metadata.snippets.add(child); + } + } + } + + context.state.analysis.snippet_renderers.set(node, resolved); mark_subtree_dynamic(context.path); diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index f001a0c48c..a58f9422f7 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -72,6 +72,17 @@ export interface ComponentAnalysis extends Analysis { keyframes: string[]; }; source: string; + /** + * Every render tag/component, and whether it could be definitively resolved or not + */ + snippet_renderers: Map< + AST.RenderTag | AST.Component | AST.SvelteComponent | AST.SvelteSelf, + boolean + >; + /** + * Every snippet that is declared locally + */ + snippets: Set; } declare module 'estree' { diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index cd359616a1..02c81b0657 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -170,7 +170,7 @@ export namespace AST { path: SvelteNode[]; /** The set of locally-defined snippets that this render tag could correspond to, * used for CSS pruning purposes */ - snippets: SnippetBlock[]; + snippets: Set; }; } @@ -285,7 +285,7 @@ export namespace AST { dynamic: boolean; /** The set of locally-defined snippets that this component tag could render, * used for CSS pruning purposes */ - snippets: SnippetBlock[]; + snippets: Set; }; } @@ -328,7 +328,7 @@ export namespace AST { scopes: Record; /** The set of locally-defined snippets that this component tag could render, * used for CSS pruning purposes */ - snippets: SnippetBlock[]; + snippets: Set; }; } @@ -382,7 +382,7 @@ export namespace AST { scopes: Record; /** The set of locally-defined snippets that this component tag could render, * used for CSS pruning purposes */ - snippets: SnippetBlock[]; + snippets: Set; }; } @@ -456,7 +456,7 @@ export namespace AST { metadata: { /** The set of components/render tags that could render this snippet, * used for CSS pruning */ - sites: Set; + sites: Set; }; }