svelte 5.55.10 → 5.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/compiler/index.js +1 -1
  2. package/package.json +7 -4
  3. package/src/compiler/errors.js +18 -0
  4. package/src/compiler/legacy.js +4 -0
  5. package/src/compiler/phases/1-parse/acorn.js +44 -1
  6. package/src/compiler/phases/1-parse/index.js +4 -1
  7. package/src/compiler/phases/1-parse/state/tag.js +91 -3
  8. package/src/compiler/phases/2-analyze/index.js +5 -0
  9. package/src/compiler/phases/2-analyze/visitors/CallExpression.js +3 -0
  10. package/src/compiler/phases/2-analyze/visitors/ConstTag.js +2 -25
  11. package/src/compiler/phases/2-analyze/visitors/DeclarationTag.js +58 -0
  12. package/src/compiler/phases/2-analyze/visitors/Identifier.js +1 -1
  13. package/src/compiler/phases/3-transform/client/transform-client.js +5 -15
  14. package/src/compiler/phases/3-transform/client/transform-template/index.js +40 -3
  15. package/src/compiler/phases/3-transform/client/utils.js +21 -0
  16. package/src/compiler/phases/3-transform/client/visitors/ConstTag.js +13 -24
  17. package/src/compiler/phases/3-transform/client/visitors/DeclarationTag.js +87 -0
  18. package/src/compiler/phases/3-transform/client/visitors/Fragment.js +2 -5
  19. package/src/compiler/phases/3-transform/client/visitors/RegularElement.js +30 -8
  20. package/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js +7 -2
  21. package/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +12 -2
  22. package/src/compiler/phases/3-transform/server/transform-server.js +2 -0
  23. package/src/compiler/phases/3-transform/server/visitors/ConstTag.js +9 -24
  24. package/src/compiler/phases/3-transform/server/visitors/DeclarationTag.js +85 -0
  25. package/src/compiler/phases/3-transform/server/visitors/RegularElement.js +24 -7
  26. package/src/compiler/phases/3-transform/utils.js +1 -0
  27. package/src/compiler/phases/nodes.js +1 -0
  28. package/src/compiler/print/index.js +42 -0
  29. package/src/compiler/utils/builders.js +2 -1
  30. package/src/internal/client/dom/blocks/boundary.js +1 -1
  31. package/src/internal/client/dom/operations.js +12 -2
  32. package/src/internal/client/reactivity/batch.js +0 -21
  33. package/src/internal/client/reactivity/props.js +6 -6
  34. package/src/internal/client/reactivity/sources.js +1 -2
  35. package/src/internal/client/runtime.js +4 -8
  36. package/src/utils.js +1 -1
  37. package/src/version.js +1 -1
  38. package/types/index.d.ts +7 -0
  39. package/types/index.d.ts.map +1 -1
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "svelte",
3
3
  "description": "Cybernetically enhanced web apps",
4
4
  "license": "MIT",
5
- "version": "5.55.10",
5
+ "version": "5.56.0",
6
6
  "type": "module",
7
7
  "types": "./types/index.d.ts",
8
8
  "engines": {
@@ -139,20 +139,22 @@
139
139
  ],
140
140
  "devDependencies": {
141
141
  "@jridgewell/trace-mapping": "^0.3.25",
142
- "@playwright/test": "^1.58.0",
142
+ "@playwright/test": "^1.60.0",
143
143
  "@rollup/plugin-commonjs": "^28.0.1",
144
144
  "@rollup/plugin-node-resolve": "^15.3.0",
145
145
  "@rollup/plugin-terser": "^0.4.4",
146
146
  "@rollup/plugin-virtual": "^3.0.2",
147
147
  "@types/aria-query": "^5.0.4",
148
148
  "@types/node": "^20.11.5",
149
+ "baseline-browser-mapping": "^2.10.32",
149
150
  "dts-buddy": "^0.5.5",
150
151
  "esbuild": "^0.25.10",
151
152
  "rollup": "^4.59.0",
152
153
  "source-map": "^0.7.4",
153
154
  "tinyglobby": "^0.2.12",
154
155
  "typescript": "^5.5.4",
155
- "vitest": "^2.1.9"
156
+ "vitest": "^2.1.9",
157
+ "web-features": "^3.29.0"
156
158
  },
157
159
  "dependencies": {
158
160
  "@jridgewell/remapping": "^2.3.4",
@@ -178,9 +180,10 @@
178
180
  "check": "tsc --project tsconfig.runtime.json && tsc && cd ./tests/types && tsc",
179
181
  "check:tsgo": "tsgo --project tsconfig.runtime.json --skipLibCheck && tsgo --skipLibCheck",
180
182
  "check:watch": "tsc --watch",
181
- "generate": "node scripts/process-messages && node ./scripts/generate-types.js",
183
+ "generate": "node scripts/process-messages && node ./scripts/generate-types.js && pnpm generate:browser-support",
182
184
  "generate:version": "node ./scripts/generate-version.js",
183
185
  "generate:types": "node ./scripts/generate-types.js && tsc -p tsconfig.generated.json",
186
+ "generate:browser-support": "node ./scripts/generate-browser-support.ts",
184
187
  "knip": "pnpm dlx knip"
185
188
  }
186
189
  }
@@ -1004,6 +1004,24 @@ export function debug_tag_invalid_arguments(node) {
1004
1004
  e(node, 'debug_tag_invalid_arguments', `{@debug ...} arguments must be identifiers, not arbitrary expressions\nhttps://svelte.dev/e/debug_tag_invalid_arguments`);
1005
1005
  }
1006
1006
 
1007
+ /**
1008
+ * Declaration tags must be `let` or `const` declarations
1009
+ * @param {null | number | NodeLike} node
1010
+ * @returns {never}
1011
+ */
1012
+ export function declaration_tag_invalid_type(node) {
1013
+ e(node, 'declaration_tag_invalid_type', `Declaration tags must be \`let\` or \`const\` declarations\nhttps://svelte.dev/e/declaration_tag_invalid_type`);
1014
+ }
1015
+
1016
+ /**
1017
+ * Declaration tags cannot be used in legacy mode
1018
+ * @param {null | number | NodeLike} node
1019
+ * @returns {never}
1020
+ */
1021
+ export function declaration_tag_no_legacy_mode(node) {
1022
+ e(node, 'declaration_tag_no_legacy_mode', `Declaration tags cannot be used in legacy mode\nhttps://svelte.dev/e/declaration_tag_no_legacy_mode`);
1023
+ }
1024
+
1007
1025
  /**
1008
1026
  * Directive value must be a JavaScript expression enclosed in curly braces
1009
1027
  * @param {null | number | NodeLike} node
@@ -262,6 +262,10 @@ export function convert(source, ast) {
262
262
  };
263
263
  },
264
264
  // @ts-ignore
265
+ DeclarationTag(node) {
266
+ return node;
267
+ },
268
+ // @ts-ignore
265
269
  KeyBlock(node, { visit }) {
266
270
  remove_surrounding_whitespace_nodes(node.fragment.nodes);
267
271
  return {
@@ -1,10 +1,11 @@
1
- /** @import { Comment, Program } from 'estree' */
1
+ /** @import { Comment, Program, Statement } from 'estree' */
2
2
  /** @import { AST } from '#compiler' */
3
3
  /** @import { Parser } from './index.js' */
4
4
  import * as acorn from 'acorn';
5
5
  import { walk } from 'zimmerframe';
6
6
  import { tsPlugin } from '@sveltejs/acorn-typescript';
7
7
  import * as e from '../../errors.js';
8
+ import { find_matching_bracket } from './utils/bracket.js';
8
9
 
9
10
  const JSParser = acorn.Parser;
10
11
  const TSParser = JSParser.extend(tsPlugin());
@@ -98,6 +99,48 @@ export function parse_expression_at(parser, source, index) {
98
99
  }
99
100
  }
100
101
 
102
+ /**
103
+ * @param {Parser} parser
104
+ * @param {string} source
105
+ * @param {number} index
106
+ * @returns {Statement}
107
+ */
108
+ export function parse_statement_at(parser, source, index) {
109
+ const acorn = parser.ts ? TSParser : JSParser;
110
+ let end = find_matching_bracket(source, index, '{');
111
+ if (end === undefined) e.unexpected_eof(source.length);
112
+
113
+ while (source[end - 1] === ';') {
114
+ end -= 1;
115
+ }
116
+
117
+ const padded_source = `${' '.repeat(index)}${source.slice(index, end)}`;
118
+ const { onComment, add_comments } = get_comment_handlers(
119
+ padded_source,
120
+ parser.root.comments,
121
+ index
122
+ );
123
+
124
+ try {
125
+ const ast = acorn.parse(padded_source, {
126
+ onComment,
127
+ sourceType: 'module',
128
+ ecmaVersion: 16,
129
+ locations: true
130
+ });
131
+
132
+ add_comments(ast);
133
+
134
+ const statement = /** @type {Statement} */ (
135
+ /** @type {unknown} */ (/** @type {Program} */ (ast).body[0])
136
+ );
137
+ statement.end = Math.min(/** @type {number} */ (statement.end), end);
138
+ return statement;
139
+ } catch (e) {
140
+ handle_parse_error(e);
141
+ }
142
+ }
143
+
101
144
  const regex_position_indicator = / \(\d+:\d+\)$/;
102
145
 
103
146
  /**
@@ -302,7 +302,10 @@ export class Parser {
302
302
  }
303
303
 
304
304
  pop() {
305
- this.fragments.pop();
305
+ const fragment = this.fragments.pop();
306
+ if (fragment?.metadata.transparent && fragment.nodes.some((n) => n.type === 'DeclarationTag')) {
307
+ fragment.metadata.transparent = false;
308
+ }
306
309
  return this.stack.pop();
307
310
  }
308
311
 
@@ -1,16 +1,20 @@
1
- /** @import { ArrowFunctionExpression, Expression, Identifier, Pattern } from 'estree' */
1
+ /** @import { ArrowFunctionExpression, Expression, Identifier, Pattern, VariableDeclaration } from 'estree' */
2
2
  /** @import { AST } from '#compiler' */
3
3
  /** @import { Parser } from '../index.js' */
4
4
  import { walk } from 'zimmerframe';
5
5
  import * as e from '../../../errors.js';
6
6
  import { ExpressionMetadata } from '../../nodes.js';
7
- import { parse_expression_at } from '../acorn.js';
7
+ import { parse_expression_at, parse_statement_at } from '../acorn.js';
8
8
  import read_pattern from '../read/context.js';
9
9
  import read_expression, { get_loose_identifier } from '../read/expression.js';
10
10
  import { create_fragment } from '../utils/create.js';
11
- import { match_bracket } from '../utils/bracket.js';
11
+ import { find_matching_bracket, match_bracket } from '../utils/bracket.js';
12
12
 
13
13
  const regex_whitespace_with_closing_curly_brace = /\s*}/y;
14
+ const regex_supported_declaration = /(?:let|const)\b/y;
15
+ // All except `type` are reserved keywords and cannot be used as variable names.
16
+ // For type we check if it's not something like `type .x` / `type ()` / `type % 2` / ...
17
+ const regex_unsupported_declaration = /(?:(?:var|interface|enum)\b)|(?:type\s+[^?.(`<[&|%^}])/y;
14
18
 
15
19
  const pointy_bois = { '<': '>' };
16
20
 
@@ -31,6 +35,20 @@ export default function tag(parser) {
31
35
  }
32
36
  }
33
37
 
38
+ const declaration = read_declaration(parser);
39
+ if (declaration) {
40
+ parser.append({
41
+ type: 'DeclarationTag',
42
+ start,
43
+ end: parser.index,
44
+ declaration: /** @type {VariableDeclaration} */ (declaration),
45
+ metadata: {
46
+ expression: new ExpressionMetadata()
47
+ }
48
+ });
49
+ return;
50
+ }
51
+
34
52
  const expression = read_expression(parser);
35
53
 
36
54
  parser.allow_whitespace();
@@ -47,6 +65,76 @@ export default function tag(parser) {
47
65
  });
48
66
  }
49
67
 
68
+ /**
69
+ * @param {Parser} parser
70
+ * @returns {null | import('estree').VariableDeclaration}
71
+ */
72
+ function read_declaration(parser) {
73
+ const start = parser.index;
74
+
75
+ const unsupported = parser.match_regex(regex_unsupported_declaration);
76
+ if (unsupported) {
77
+ e.declaration_tag_invalid_type({ start, end: start + unsupported.length });
78
+ }
79
+
80
+ if (!parser.match_regex(regex_supported_declaration)) {
81
+ return null;
82
+ }
83
+
84
+ /** @type {import('estree').Statement | import('estree').VariableDeclaration} */
85
+ let declaration;
86
+ try {
87
+ declaration = parse_statement_at(parser, parser.template, start);
88
+ } catch (error) {
89
+ if (!parser.loose) throw error;
90
+
91
+ const end = find_matching_bracket(parser.template, start, '{');
92
+ if (end === undefined) throw error;
93
+
94
+ parser.index = end;
95
+ const kind = parser.template.startsWith('const', start) ? 'const' : 'let';
96
+
97
+ declaration = {
98
+ type: 'VariableDeclaration',
99
+ kind,
100
+ declarations: [
101
+ {
102
+ type: 'VariableDeclarator',
103
+ id: {
104
+ type: 'Identifier',
105
+ name: '',
106
+ start: parser.index,
107
+ end: parser.index
108
+ },
109
+ init: null,
110
+ start: parser.index,
111
+ end: parser.index
112
+ }
113
+ ],
114
+ start,
115
+ end
116
+ };
117
+ }
118
+
119
+ if (declaration.type !== 'VariableDeclaration') {
120
+ e.declaration_tag_invalid_type({
121
+ start: declaration.start ?? start,
122
+ end: declaration.end ?? parser.index
123
+ });
124
+ }
125
+
126
+ // TODO support using
127
+ if (declaration.kind !== 'let' && declaration.kind !== 'const') {
128
+ e.declaration_tag_invalid_type(declaration);
129
+ }
130
+
131
+ parser.index = /** @type {number} */ (declaration.end);
132
+ parser.allow_whitespace();
133
+ parser.eat('}', true);
134
+
135
+ return declaration;
136
+ }
137
+
50
138
  /** @param {Parser} parser */
51
139
  function open(parser) {
52
140
  let start = parser.index - 2;
@@ -36,6 +36,7 @@ import { ClassDeclaration } from './visitors/ClassDeclaration.js';
36
36
  import { ClassDirective } from './visitors/ClassDirective.js';
37
37
  import { Component } from './visitors/Component.js';
38
38
  import { ConstTag } from './visitors/ConstTag.js';
39
+ import { DeclarationTag } from './visitors/DeclarationTag.js';
39
40
  import { DebugTag } from './visitors/DebugTag.js';
40
41
  import { EachBlock } from './visitors/EachBlock.js';
41
42
  import { ExportDefaultDeclaration } from './visitors/ExportDefaultDeclaration.js';
@@ -157,6 +158,7 @@ const visitors = {
157
158
  ClassDirective,
158
159
  Component,
159
160
  ConstTag,
161
+ DeclarationTag,
160
162
  DebugTag,
161
163
  EachBlock,
162
164
  ExportDefaultDeclaration,
@@ -312,6 +314,7 @@ export function analyze_module(source, options) {
312
314
  options: /** @type {ValidatedCompileOptions} */ (options),
313
315
  fragment: null,
314
316
  parent_element: null,
317
+ in_declaration_tag: false,
315
318
  reactive_statement: null,
316
319
  derived_function_depth: -1
317
320
  },
@@ -718,6 +721,7 @@ export function analyze_component(root, source, options) {
718
721
  ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
719
722
  fragment: ast === template.ast ? ast : null,
720
723
  parent_element: null,
724
+ in_declaration_tag: false,
721
725
  has_props_rune: false,
722
726
  component_slots: new Set(),
723
727
  expression: null,
@@ -785,6 +789,7 @@ export function analyze_component(root, source, options) {
785
789
  options,
786
790
  fragment: ast === template.ast ? ast : null,
787
791
  parent_element: null,
792
+ in_declaration_tag: false,
788
793
  has_props_rune: false,
789
794
  ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
790
795
  reactive_statement: null,
@@ -255,6 +255,9 @@ export function CallExpression(node, context) {
255
255
  if (expression.has_await) {
256
256
  context.state.analysis.async_deriveds.add(node);
257
257
  }
258
+
259
+ // Tell surrounding declaration tag about metadata for correct calculation of blockers etc
260
+ if (context.state.in_declaration_tag) context.state.expression?.merge(expression);
258
261
  } else if (rune === '$inspect') {
259
262
  context.next({ ...context.state, function_depth: context.state.function_depth + 1 });
260
263
  } else {
@@ -1,8 +1,8 @@
1
1
  /** @import { AST } from '#compiler' */
2
2
  /** @import { Context } from '../types' */
3
3
  import * as e from '../../../errors.js';
4
- import * as b from '#compiler/builders';
5
4
  import { validate_opening_tag } from './shared/utils.js';
5
+ import { mark_async_declaration } from './DeclarationTag.js';
6
6
 
7
7
  /**
8
8
  * @param {AST.ConstTag} node
@@ -44,28 +44,5 @@ export function ConstTag(node, context) {
44
44
  derived_function_depth: context.state.function_depth + 1
45
45
  });
46
46
 
47
- const has_await = node.metadata.expression.has_await;
48
- const blockers = [...node.metadata.expression.dependencies]
49
- .map((dep) => dep.blocker)
50
- .filter((b) => b !== null && b.object !== context.state.async_consts?.id);
51
-
52
- if (has_await || context.state.async_consts || blockers.length > 0) {
53
- const run = (context.state.async_consts ??= {
54
- id: context.state.analysis.root.unique('promises'),
55
- declaration_count: 0
56
- });
57
- node.metadata.promises_id = run.id;
58
-
59
- const bindings = context.state.scope.get_bindings(declaration);
60
-
61
- // keep the counter in sync with the number of thunks pushed in ConstTag in transform
62
- // TODO 6.0 once non-async and non-runes mode is gone investigate making this more robust
63
- // via something like the approach in https://github.com/sveltejs/svelte/pull/18032
64
- const length = run.declaration_count + (blockers.length > 0 ? 1 : 0);
65
- run.declaration_count += blockers.length > 0 ? 2 : 1;
66
- const blocker = b.member(run.id, b.literal(length), true);
67
- for (const binding of bindings) {
68
- binding.blocker = blocker;
69
- }
70
- }
47
+ mark_async_declaration(context, node.metadata, [declaration]);
71
48
  }
@@ -0,0 +1,58 @@
1
+ /** @import { AST } from '#compiler' */
2
+ /** @import { Context } from '../types' */
3
+ import * as b from '#compiler/builders';
4
+ import * as e from '../../../errors.js';
5
+ import { validate_opening_tag } from './shared/utils.js';
6
+
7
+ /**
8
+ * @param {AST.DeclarationTag} node
9
+ * @param {Context} context
10
+ */
11
+ export function DeclarationTag(node, context) {
12
+ validate_opening_tag(node, context.state, node.declaration.kind[0]);
13
+ if (!context.state.analysis.runes && !context.state.analysis.maybe_runes) {
14
+ e.declaration_tag_no_legacy_mode(node);
15
+ }
16
+
17
+ context.visit(node.declaration, {
18
+ ...context.state,
19
+ in_declaration_tag: true,
20
+ expression: node.metadata.expression
21
+ });
22
+
23
+ mark_async_declaration(context, node.metadata, node.declaration.declarations);
24
+ }
25
+
26
+ /**
27
+ * @param {Context} context
28
+ * @param {AST.ConstTag['metadata'] | AST.DeclarationTag['metadata']} metadata
29
+ * @param {import('estree').VariableDeclarator[]} declarations
30
+ */
31
+ export function mark_async_declaration(context, metadata, declarations) {
32
+ const has_await = metadata.expression.has_await;
33
+ const blockers = [...metadata.expression.dependencies]
34
+ .map((dep) => dep.blocker)
35
+ .filter((b) => b !== null && b.object !== context.state.async_consts?.id);
36
+
37
+ if (has_await || context.state.async_consts || blockers.length > 0) {
38
+ const run = (context.state.async_consts ??= {
39
+ id: context.state.analysis.root.unique('promises'),
40
+ declaration_count: 0
41
+ });
42
+ metadata.promises_id = run.id;
43
+
44
+ const bindings = declarations.flatMap((declaration) =>
45
+ context.state.scope.get_bindings(declaration)
46
+ );
47
+
48
+ // keep the counter in sync with the number of thunks pushed in transform
49
+ // TODO 6.0 once non-async and non-runes mode is gone investigate making this more robust
50
+ // via something like the approach in https://github.com/sveltejs/svelte/pull/18032
51
+ const length = run.declaration_count + (blockers.length > 0 ? 1 : 0);
52
+ run.declaration_count += blockers.length > 0 ? 2 : 1;
53
+ const blocker = b.member(run.id, b.literal(length), true);
54
+ for (const binding of bindings) {
55
+ binding.blocker = blocker;
56
+ }
57
+ }
58
+ }
@@ -162,7 +162,7 @@ export function Identifier(node, context) {
162
162
  if (binding.metadata?.is_template_declaration && context.state.options.experimental.async) {
163
163
  let snippet_name;
164
164
 
165
- // Find out if this references a {@const ...} declaration of an implicit children snippet
165
+ // Find out if this references a {@const ...}/{let/const ...} declaration of an implicit children snippet
166
166
  // when it is itself inside a snippet block at the same level. If so, error.
167
167
  for (let i = context.path.length - 1; i >= 0; i--) {
168
168
  const parent = context.path[i];
@@ -4,7 +4,7 @@
4
4
  /** @import { Visitors, ComponentClientTransformState, ClientTransformState } from './types' */
5
5
  import { walk } from 'zimmerframe';
6
6
  import * as b from '#compiler/builders';
7
- import { build_getter, is_state_source } from './utils.js';
7
+ import { build_getter, get_transform } from './utils.js';
8
8
  import { render_stylesheet } from '../css/index.js';
9
9
  import { dev, filename } from '../../../state.js';
10
10
  import { AnimateDirective } from './visitors/AnimateDirective.js';
@@ -22,6 +22,7 @@ import { ClassBody } from './visitors/ClassBody.js';
22
22
  import { Comment } from './visitors/Comment.js';
23
23
  import { Component } from './visitors/Component.js';
24
24
  import { ConstTag } from './visitors/ConstTag.js';
25
+ import { DeclarationTag } from './visitors/DeclarationTag.js';
25
26
  import { DebugTag } from './visitors/DebugTag.js';
26
27
  import { EachBlock } from './visitors/EachBlock.js';
27
28
  import { ExportNamedDeclaration } from './visitors/ExportNamedDeclaration.js';
@@ -66,20 +67,7 @@ const visitors = {
66
67
  const scope = state.scopes.get(node);
67
68
 
68
69
  if (scope && scope !== state.scope) {
69
- const transform = { ...state.transform };
70
-
71
- for (const [name, binding] of scope.declarations) {
72
- if (
73
- binding.kind === 'normal' ||
74
- // Reads of `$state(...)` declarations are not
75
- // transformed if they are never reassigned
76
- (binding.kind === 'state' && !is_state_source(binding, state.analysis))
77
- ) {
78
- delete transform[name];
79
- }
80
- }
81
-
82
- next({ ...state, transform, scope });
70
+ next({ ...state, transform: get_transform(scope, state), scope });
83
71
  } else {
84
72
  next();
85
73
  }
@@ -99,6 +87,7 @@ const visitors = {
99
87
  Comment,
100
88
  Component,
101
89
  ConstTag,
90
+ DeclarationTag,
102
91
  DebugTag,
103
92
  EachBlock,
104
93
  ExportNamedDeclaration,
@@ -152,6 +141,7 @@ export function client_component(analysis, options) {
152
141
  scopes: analysis.module.scopes,
153
142
  is_instance: false,
154
143
  hoisted: [b.import_all('$', 'svelte/internal/client'), ...analysis.instance_body.hoisted],
144
+ templates: new Map(),
155
145
  node: /** @type {any} */ (null), // populated by the root node
156
146
  legacy_reactive_imports: [],
157
147
  legacy_reactive_statements: new Map(),
@@ -1,3 +1,4 @@
1
+ /** @import { TemplateLiteral } from 'estree' */
1
2
  /** @import { Namespace } from '#compiler' */
2
3
  /** @import { ComponentClientTransformState } from '../types.js' */
3
4
  /** @import { Node } from './types.js' */
@@ -31,14 +32,29 @@ function build_locations(nodes) {
31
32
 
32
33
  /**
33
34
  * @param {ComponentClientTransformState} state
34
- * @param {Namespace} namespace
35
+ * @param {string} name
35
36
  * @param {number} [flags]
36
37
  */
37
- export function transform_template(state, namespace, flags = 0) {
38
+ export function transform_template(state, name, flags = 0) {
39
+ const namespace = state.metadata.namespace;
38
40
  const tree = state.options.fragments === 'tree';
39
41
 
40
42
  const expression = tree ? state.template.as_tree() : state.template.as_html();
41
43
 
44
+ const key =
45
+ tree || dev
46
+ ? null
47
+ : get_template_key(
48
+ /** @type {TemplateLiteral} */ (expression),
49
+ state.metadata.namespace,
50
+ flags
51
+ );
52
+
53
+ if (key !== null) {
54
+ const existing = state.templates.get(key);
55
+ if (existing !== undefined) return existing;
56
+ }
57
+
42
58
  if (tree) {
43
59
  if (namespace === 'svg') flags |= TEMPLATE_USE_SVG;
44
60
  if (namespace === 'mathml') flags |= TEMPLATE_USE_MATHML;
@@ -63,5 +79,26 @@ export function transform_template(state, namespace, flags = 0) {
63
79
  );
64
80
  }
65
81
 
66
- return call;
82
+ const id = state.scope.root.unique(name);
83
+ state.hoisted.push(b.var(id, call));
84
+
85
+ if (key !== null) {
86
+ state.templates.set(key, id);
87
+ }
88
+
89
+ return id;
90
+ }
91
+
92
+ /**
93
+ * Returns a stable key for templates that are safe to deduplicate - plain
94
+ * `$.from_html`/`from_svg`/`from_mathml` factories with literal arguments - or `null`
95
+ * for anything else. Dev-mode templates are wrapped in `$.add_locations(...)`, which
96
+ * embeds per-call-site locations, so they never produce a key and are never shared.
97
+ * @param {TemplateLiteral} template
98
+ * @param {Namespace} namespace
99
+ * @param {number} flags
100
+ * @returns {string | null}
101
+ */
102
+ function get_template_key(template, namespace, flags) {
103
+ return `${namespace} ${flags} ${template.quasis[0].value.raw}`;
67
104
  }
@@ -179,3 +179,24 @@ export function create_derived(state, expression, async = false) {
179
179
  return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', thunk);
180
180
  }
181
181
  }
182
+
183
+ /**
184
+ * @param {Scope} scope
185
+ * @param {ClientTransformState} state
186
+ */
187
+ export function get_transform(scope, state) {
188
+ const transform = { ...state.transform };
189
+
190
+ for (const [name, binding] of scope.declarations) {
191
+ if (
192
+ binding.kind === 'normal' ||
193
+ // Reads of `$state(...)` declarations are not
194
+ // transformed if they are never reassigned
195
+ (binding.kind === 'state' && !is_state_source(binding, state.analysis))
196
+ ) {
197
+ delete transform[name];
198
+ }
199
+ }
200
+
201
+ return transform;
202
+ }
@@ -7,6 +7,7 @@ import * as b from '#compiler/builders';
7
7
  import { create_derived } from '../utils.js';
8
8
  import { get_value } from './shared/declarations.js';
9
9
  import { build_expression } from './shared/utils.js';
10
+ import { add_async_declaration } from './DeclarationTag.js';
10
11
 
11
12
  /**
12
13
  * @param {AST.ConstTag} node
@@ -26,7 +27,7 @@ export function ConstTag(node, context) {
26
27
 
27
28
  context.state.transform[declaration.id.name] = { read: get_value };
28
29
 
29
- add_const_declaration(context.state, declaration.id, expression, node.metadata);
30
+ add_const_declaration(context, declaration.id, expression, node.metadata);
30
31
  } else {
31
32
  const identifiers = extract_identifiers(declaration.id);
32
33
  const tmp = b.id(context.state.scope.generate('computed_const'));
@@ -63,7 +64,7 @@ export function ConstTag(node, context) {
63
64
  expression = b.call('$.tag', expression, b.literal('[@const]'));
64
65
  }
65
66
 
66
- add_const_declaration(context.state, tmp, expression, node.metadata);
67
+ add_const_declaration(context, tmp, expression, node.metadata);
67
68
 
68
69
  for (const node of identifiers) {
69
70
  context.state.transform[node.name] = {
@@ -74,38 +75,26 @@ export function ConstTag(node, context) {
74
75
  }
75
76
 
76
77
  /**
77
- * @param {ComponentContext['state']} state
78
+ * @param {ComponentContext} context
78
79
  * @param {Identifier} id
79
80
  * @param {Expression} expression
80
81
  * @param {AST.ConstTag['metadata']} metadata
81
82
  */
82
- function add_const_declaration(state, id, expression, metadata) {
83
+ function add_const_declaration(context, id, expression, metadata) {
83
84
  // we need to eagerly evaluate the expression in order to hit any
84
85
  // 'Cannot access x before initialization' errors
85
86
  const after = dev ? [b.stmt(b.call('$.get', id))] : [];
86
87
 
87
- const blockers = [...metadata.expression.dependencies]
88
- .map((dep) => dep.blocker)
89
- .filter((b) => b !== null && b.object !== state.async_consts?.id);
90
-
91
88
  if (metadata.promises_id) {
92
- const run = (state.async_consts ??= {
93
- id: metadata.promises_id,
94
- thunks: []
95
- });
96
-
97
- state.consts.push(b.let(id));
98
-
99
- if (blockers.length === 1) {
100
- run.thunks.push(b.thunk(b.member(/** @type {Expression} */ (blockers[0]), 'promise')));
101
- } else if (blockers.length > 0) {
102
- run.thunks.push(b.thunk(b.call('$.wait', b.array(blockers))));
103
- }
104
-
105
- // keep the number of thunks pushed in sync with ConstTag in analysis phase
106
- const assignment = b.assignment('=', id, expression);
107
- run.thunks.push(b.thunk(assignment, metadata.expression.has_await));
89
+ add_async_declaration(
90
+ context,
91
+ metadata,
92
+ [id],
93
+ [b.stmt(b.assignment('=', id, expression))],
94
+ 'let'
95
+ );
108
96
  } else {
97
+ const { state } = context;
109
98
  state.consts.push(b.const(id, expression));
110
99
  state.consts.push(...after);
111
100
  }