Merge branch 'main' into support-await-finally-block

pull/12312/head
Rich Harris 2 years ago committed by GitHub
commit 5136b80e71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: simpler effect DOM boundaries

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: correct start of `{:else if}` and `{:else}`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: bail out of event hoisting when referencing store subscriptions

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: support contenteditable binding undefined fallback

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: reverse parent/child order in invalid HTML warning

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: make store initialization logic simpler

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: reorder reactive statements during migration

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: bump dts-buddy for better type generation

@ -0,0 +1,5 @@
---
"svelte": patch
---
breaking: expose `CompileError` interface, not class

@ -0,0 +1,5 @@
---
'svelte': patch
---
breaking: play transitions on `mount` by default

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: make props optional during SSR

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: make `<select>` `<option value>` behavior consistent

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure each blocks properly handle $state.frozen objects in prod

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure rest props access on hoisted event handlers works

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: stricter control flow syntax validation in runes mode

@ -0,0 +1,5 @@
---
'svelte': patch
---
breaking: rename `svelte/reactivity` helpers to include `Svelte` prefix

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: disallow accessing internal Svelte props

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: better `<svelte:element>` SSR output

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: avoid duplicate signal dependencies

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: make media bindings more robust

@ -19,6 +19,7 @@
"angry-wasps-help",
"beige-cobras-smoke",
"beige-flies-wash",
"beige-gifts-appear",
"beige-mirrors-listen",
"beige-rabbits-shave",
"beige-seas-share",
@ -77,6 +78,7 @@
"cold-cheetahs-judge",
"cold-lamps-accept",
"cold-masks-learn",
"cold-teachers-turn",
"cool-actors-tan",
"cool-ants-leave",
"cool-comics-marry",
@ -87,6 +89,7 @@
"cool-roses-trade",
"cuddly-pianos-drop",
"cuddly-points-tickle",
"curly-cooks-cheer",
"curly-lizards-dream",
"curvy-buses-laugh",
"curvy-cups-cough",
@ -108,6 +111,8 @@
"dry-eggs-play",
"dry-eggs-retire",
"dry-fans-march",
"dry-ghosts-prove",
"dry-parrots-bathe",
"dry-pillows-exist",
"dull-coins-vanish",
"dull-donkeys-smell",
@ -129,6 +134,7 @@
"eighty-lizards-notice",
"eleven-avocados-walk",
"eleven-beers-yell",
"eleven-cows-judge",
"eleven-cycles-applaud",
"eleven-hounds-pump",
"empty-bags-heal",
@ -141,6 +147,7 @@
"empty-horses-tell",
"empty-tools-share",
"fair-bags-smoke",
"fair-beers-help",
"fair-crabs-check",
"fair-pianos-talk",
"fair-spies-repeat",
@ -152,6 +159,7 @@
"fast-donkeys-pay",
"fast-penguins-matter",
"fast-weeks-clean",
"few-cheetahs-taste",
"few-clouds-shop",
"few-mugs-fail",
"few-teachers-know",
@ -182,6 +190,7 @@
"fresh-walls-bathe",
"fresh-weeks-trade",
"friendly-candles-relate",
"friendly-clouds-rhyme",
"friendly-lies-camp",
"funny-cooks-clean",
"funny-dragons-double",
@ -208,6 +217,7 @@
"good-cars-visit",
"good-pianos-jump",
"good-plums-type",
"good-rice-tap",
"good-rivers-yawn",
"good-roses-argue",
"gorgeous-boxes-design",
@ -229,11 +239,13 @@
"happy-suits-film",
"healthy-ants-film",
"healthy-planes-vanish",
"healthy-zebras-accept",
"heavy-comics-move",
"heavy-doors-applaud",
"heavy-ducks-leave",
"heavy-ears-rule",
"hip-balloons-begin",
"hip-garlics-tap",
"hip-pumpkins-boil",
"honest-buses-add",
"honest-dragons-turn",
@ -258,6 +270,7 @@
"itchy-lions-wash",
"itchy-panthers-shave",
"itchy-peaches-compare",
"itchy-pianos-marry",
"itchy-terms-guess",
"khaki-cheetahs-refuse",
"khaki-cooks-develop",
@ -293,16 +306,19 @@
"lemon-trees-act",
"light-badgers-glow",
"light-days-clean",
"light-hounds-carry",
"light-humans-hang",
"light-penguins-invent",
"light-pens-watch",
"little-ligers-exist",
"little-pans-jog",
"long-buckets-lay",
"long-carrots-sneeze",
"long-crews-return",
"long-humans-repair",
"long-lobsters-mate",
"loud-cheetahs-flow",
"loud-insects-arrive",
"loud-mugs-smile",
"loud-numbers-flow",
"loud-penguins-unite",
@ -312,6 +328,7 @@
"lovely-carpets-lick",
"lovely-houses-own",
"lovely-items-turn",
"lovely-ravens-crash",
"lovely-rules-eat",
"lovely-zebras-own",
"lucky-colts-remember",
@ -328,6 +345,8 @@
"mighty-cooks-scream",
"mighty-files-hammer",
"mighty-frogs-obey",
"mighty-paws-smash",
"mighty-shoes-nail",
"modern-apricots-promise",
"modern-fishes-double",
"moody-carrots-lay",
@ -352,6 +371,7 @@
"new-brooms-grin",
"new-rabbits-flow",
"nice-avocados-move",
"nice-jobs-breathe",
"nine-cooks-join",
"ninety-days-visit",
"ninety-dingos-walk",
@ -368,6 +388,7 @@
"old-mails-sneeze",
"old-oranges-compete",
"olive-apples-lick",
"olive-cobras-wonder",
"olive-kangaroos-brake",
"olive-mice-fix",
"olive-moons-act",
@ -383,10 +404,12 @@
"pink-bikes-agree",
"pink-goats-promise",
"pink-mayflies-tie",
"plenty-clouds-reply",
"plenty-elephants-fry",
"plenty-starfishes-dress",
"plenty-zoos-fix",
"polite-dolphins-care",
"polite-peas-mate",
"polite-pumpkins-guess",
"polite-ravens-study",
"polite-ways-serve",
@ -395,6 +418,7 @@
"poor-seahorses-flash",
"popular-apes-bathe",
"popular-cameras-tie",
"popular-feet-rule",
"popular-games-hug",
"popular-ligers-perform",
"popular-mangos-rest",
@ -449,6 +473,7 @@
"serious-poems-brake",
"serious-socks-cover",
"serious-zebras-scream",
"seven-bees-tell",
"seven-deers-jam",
"seven-garlics-serve",
"seven-hornets-smile",
@ -480,11 +505,14 @@
"silver-sheep-knock",
"six-bears-trade",
"six-boats-shave",
"six-chicken-kneel",
"six-gorillas-obey",
"sixty-items-crash",
"sixty-numbers-hope",
"sixty-pandas-rush",
"sixty-plants-cover",
"sleepy-cats-eat",
"sleepy-dogs-sit",
"slimy-clouds-talk",
"slimy-hairs-impress",
"slimy-laws-explode",
@ -497,15 +525,18 @@
"slow-plums-chew",
"slow-wombats-reply",
"small-apples-eat",
"small-owls-remain",
"small-papayas-laugh",
"small-sheep-type",
"small-spiders-fail",
"smart-cherries-leave",
"smart-fans-crash",
"smart-grapes-mate",
"smart-parents-swim",
"smart-spiders-fetch",
"smart-turkeys-tell",
"smart-zebras-pay",
"smart-zoos-vanish",
"smooth-kids-protect",
"smooth-pens-protect",
"smooth-rings-rush",
@ -582,6 +613,7 @@
"thin-foxes-lick",
"thin-spoons-float",
"thin-years-rhyme",
"thirty-flies-push",
"thirty-flowers-sit",
"thirty-ghosts-fix",
"thirty-impalas-repair",
@ -599,13 +631,16 @@
"tidy-chefs-taste",
"tidy-deers-hope",
"tidy-fans-relax",
"tidy-lizards-happen",
"tidy-starfishes-allow",
"tiny-kings-whisper",
"tiny-meals-deliver",
"tiny-moose-kiss",
"tiny-taxis-whisper",
"tough-buckets-thank",
"tough-radios-punch",
"tough-tomatoes-explain",
"tricky-laws-bathe",
"twelve-beans-drive",
"twelve-cows-learn",
"twelve-dragons-join",
@ -624,6 +659,7 @@
"violet-mugs-behave",
"violet-pigs-jam",
"warm-cherries-shake",
"warm-waves-reply",
"weak-drinks-speak",
"weak-frogs-bow",
"weak-terms-destroy",
@ -633,9 +669,11 @@
"wet-wombats-repeat",
"wicked-clouds-exercise",
"wicked-doors-train",
"wicked-emus-drive",
"wicked-hairs-cheer",
"wicked-wasps-allow",
"wicked-ways-reply",
"wild-cows-chew",
"wild-foxes-wonder",
"wild-moose-compare",
"wise-apples-care",
@ -648,6 +686,8 @@
"wise-ties-clap",
"witty-bikes-shave",
"witty-camels-warn",
"witty-hornets-think",
"witty-phones-retire",
"witty-readers-provide",
"witty-steaks-dream",
"witty-tomatoes-care",

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure `$$slots` returns a record of booleans

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: resolve legacy component props equality for mutations

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: allow `let props = $props()` and optimize prop read access

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: lazily create a derived for each read method on `SvelteDate.prototype`

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: align warning and error objects, add frame property

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly compile $effect.root in svelte modules

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: reconnected deep derived signals to graph

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: single-pass hydration

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: tidy up store logic

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: make `$state` component exports settable

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure HMR doesn't mess with anchor nodes

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: improve type arguments for Snippet and $bindable

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure `bind:this` works with component with no return value

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure `$effect.root` is ignored on the server

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: enable local transitions on `svelte:element`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: deconflict multiple snippets of the same name

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: handle duplicate signal dependencies gracefully

@ -1,6 +1,38 @@
import svelte_config from '@sveltejs/eslint-config';
import lube from 'eslint-plugin-lube';
const no_compiler_imports = {
meta: {
type: /** @type {const} */ ('problem'),
docs: {
description:
'Enforce that there are no imports to the compiler in runtime code. ' +
'This prevent accidental inclusion of the compiler runtime and ' +
"ensures that TypeScript does not pick up more ambient types (for example from Node) that shouldn't be available in the browser."
}
},
create(context) {
return {
Program: () => {
// Do a simple string search because ESLint doesn't provide a way to check JSDoc comments.
// The string search could in theory yield false positives, but in practice it's unlikely.
const text = context.sourceCode.getText();
const idx = Math.max(text.indexOf('../compiler/'), text.indexOf('#compiler'));
if (idx !== -1) {
context.report({
loc: {
start: context.sourceCode.getLocFromIndex(idx),
end: context.sourceCode.getLocFromIndex(idx + 12)
},
message:
'References to compiler code are forbidden in runtime code (both for type and value imports)'
});
}
}
};
}
};
/** @type {import('eslint').Linter.FlatConfig[]} */
export default [
...svelte_config,
@ -11,7 +43,8 @@ export default [
}
},
plugins: {
lube
lube,
custom: { rules: { no_compiler_imports } }
},
rules: {
'@typescript-eslint/await-thenable': 'error',
@ -37,6 +70,13 @@ export default [
'no-console': 'off'
}
},
{
files: ['packages/svelte/src/**/*'],
ignores: ['packages/svelte/src/compiler/**/*'],
rules: {
'custom/no_compiler_imports': 'error'
}
},
{
ignores: [
'**/*.d.ts',

@ -5,7 +5,7 @@
"private": true,
"type": "module",
"license": "MIT",
"packageManager": "pnpm@9.2.0",
"packageManager": "pnpm@9.4.0",
"engines": {
"pnpm": "^9.0.0"
},
@ -30,18 +30,18 @@
},
"devDependencies": {
"@changesets/cli": "^2.27.6",
"@sveltejs/eslint-config": "^7.0.1",
"@sveltejs/eslint-config": "^8.0.1",
"@svitejs/changesets-changelog-github-compact": "^1.1.0",
"@types/node": "^20.11.5",
"@vitest/coverage-v8": "^1.2.1",
"eslint": "^9.0.0",
"eslint": "^9.6.0",
"eslint-plugin-lube": "^0.4.3",
"jsdom": "22.0.0",
"playwright": "^1.41.1",
"prettier": "^3.2.4",
"prettier-plugin-svelte": "^3.1.2",
"typescript": "^5.5.2",
"typescript-eslint": "^8.0.0-alpha.20",
"typescript-eslint": "^8.0.0-alpha.34",
"v8-natives": "^1.2.5",
"vitest": "^1.2.1"
},

@ -1,5 +1,133 @@
# svelte
## 5.0.0-next.178
### Patch Changes
- fix: reconnected deep derived signals to graph ([#12350](https://github.com/sveltejs/svelte/pull/12350))
## 5.0.0-next.177
### Patch Changes
- breaking: play transitions on `mount` by default ([#12351](https://github.com/sveltejs/svelte/pull/12351))
- fix: make `<select>` `<option value>` behavior consistent ([#12316](https://github.com/sveltejs/svelte/pull/12316))
- chore: stricter control flow syntax validation in runes mode ([#12342](https://github.com/sveltejs/svelte/pull/12342))
- fix: resolve legacy component props equality for mutations ([#12348](https://github.com/sveltejs/svelte/pull/12348))
- fix: make `$state` component exports settable ([#12345](https://github.com/sveltejs/svelte/pull/12345))
## 5.0.0-next.176
### Patch Changes
- fix: correct start of `{:else if}` and `{:else}` ([#12043](https://github.com/sveltejs/svelte/pull/12043))
- fix: reverse parent/child order in invalid HTML warning ([#12336](https://github.com/sveltejs/svelte/pull/12336))
- fix: reorder reactive statements during migration ([#12329](https://github.com/sveltejs/svelte/pull/12329))
- feat: better `<svelte:element>` SSR output ([#12339](https://github.com/sveltejs/svelte/pull/12339))
- chore: align warning and error objects, add frame property ([#12326](https://github.com/sveltejs/svelte/pull/12326))
- fix: ensure `$effect.root` is ignored on the server ([#12332](https://github.com/sveltejs/svelte/pull/12332))
- fix: enable local transitions on `svelte:element` ([#12346](https://github.com/sveltejs/svelte/pull/12346))
## 5.0.0-next.175
### Patch Changes
- fix: correctly compile $effect.root in svelte modules ([#12315](https://github.com/sveltejs/svelte/pull/12315))
- fix: ensure `bind:this` works with component with no return value ([#12290](https://github.com/sveltejs/svelte/pull/12290))
## 5.0.0-next.174
### Patch Changes
- fix: bail out of event hoisting when referencing store subscriptions ([#12301](https://github.com/sveltejs/svelte/pull/12301))
- chore: make store initialization logic simpler ([#12281](https://github.com/sveltejs/svelte/pull/12281))
- fix: make props optional during SSR ([#12284](https://github.com/sveltejs/svelte/pull/12284))
- fix: ensure each blocks properly handle $state.frozen objects in prod ([#12305](https://github.com/sveltejs/svelte/pull/12305))
- fix: ensure rest props access on hoisted event handlers works ([#12298](https://github.com/sveltejs/svelte/pull/12298))
- fix: lazily create a derived for each read method on `SvelteDate.prototype` ([#12110](https://github.com/sveltejs/svelte/pull/12110))
## 5.0.0-next.173
### Patch Changes
- chore: tidy up store logic ([#12277](https://github.com/sveltejs/svelte/pull/12277))
## 5.0.0-next.172
### Patch Changes
- fix: handle duplicate signal dependencies gracefully ([#12261](https://github.com/sveltejs/svelte/pull/12261))
## 5.0.0-next.171
### Patch Changes
- feat: simpler effect DOM boundaries ([#12258](https://github.com/sveltejs/svelte/pull/12258))
## 5.0.0-next.170
### Patch Changes
- fix: bump dts-buddy for better type generation ([#12262](https://github.com/sveltejs/svelte/pull/12262))
- breaking: expose `CompileError` interface, not class ([#12255](https://github.com/sveltejs/svelte/pull/12255))
## 5.0.0-next.169
### Patch Changes
- breaking: rename `svelte/reactivity` helpers to include `Svelte` prefix ([#12248](https://github.com/sveltejs/svelte/pull/12248))
- fix: avoid duplicate signal dependencies ([#12245](https://github.com/sveltejs/svelte/pull/12245))
## 5.0.0-next.168
### Patch Changes
- fix: ensure HMR doesn't mess with anchor nodes ([#12242](https://github.com/sveltejs/svelte/pull/12242))
- fix: deconflict multiple snippets of the same name ([#12221](https://github.com/sveltejs/svelte/pull/12221))
## 5.0.0-next.167
### Patch Changes
- fix: make more types from `svelte/compiler` public ([#12189](https://github.com/sveltejs/svelte/pull/12189))
- fix: support contenteditable binding undefined fallback ([#12210](https://github.com/sveltejs/svelte/pull/12210))
- breaking: prevent usage of arguments keyword in certain places ([#12191](https://github.com/sveltejs/svelte/pull/12191))
- fix(types): export CompileResult and Warning ([#12212](https://github.com/sveltejs/svelte/pull/12212))
- fix: ensure element dir properties persist with text changes ([#12204](https://github.com/sveltejs/svelte/pull/12204))
- fix: disallow accessing internal Svelte props ([#12207](https://github.com/sveltejs/svelte/pull/12207))
- fix: make media bindings more robust ([#12206](https://github.com/sveltejs/svelte/pull/12206))
- fix: allow slot attribute inside snippets ([#12188](https://github.com/sveltejs/svelte/pull/12188))
- feat: allow `let props = $props()` and optimize prop read access ([#12201](https://github.com/sveltejs/svelte/pull/12201))
- feat: improve type arguments for Snippet and $bindable ([#12197](https://github.com/sveltejs/svelte/pull/12197))
## 5.0.0-next.166
### Patch Changes

@ -78,6 +78,10 @@
> Cannot use `$props()` more than once
## props_illegal_name
> Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals)
## props_invalid_identifier
> `$props()` can only be used with an object destructuring pattern

@ -88,6 +88,10 @@
> Block was left open
## block_unexpected_character
> Expected a `%character%` character immediately following the opening bracket
## block_unexpected_close
> Unexpected block closing tag

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.0.0-next.166",
"version": "5.0.0-next.178",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -97,6 +97,7 @@
},
"homepage": "https://svelte.dev",
"keywords": [
"svelte",
"UI",
"framework",
"templates",
@ -105,7 +106,7 @@
"scripts": {
"build": "node scripts/process-messages && rollup -c && pnpm generate:types && node scripts/check-treeshakeability.js",
"dev": "node scripts/process-messages && rollup -cw",
"check": "tsc && cd ./tests/types && tsc",
"check": "tsc --project tsconfig.runtime.json && tsc && cd ./tests/types && tsc",
"check:watch": "tsc --watch",
"generate:version": "node ./scripts/generate-version.js",
"generate:types": "node ./scripts/generate-types.js && tsc -p tsconfig.generated.json",
@ -122,7 +123,7 @@
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-virtual": "^3.0.2",
"@types/aria-query": "^5.0.4",
"dts-buddy": "^0.5.0",
"dts-buddy": "^0.5.1",
"esbuild": "^0.19.11",
"rollup": "^4.9.5",
"source-map": "^0.7.4",

@ -113,7 +113,7 @@ const bundle = await bundle_code(
).js.code
);
if (!bundle.includes('hydrate_nodes') && !bundle.includes('hydrate_anchor')) {
if (!bundle.includes('hydrate_node') && !bundle.includes('hydrate_next')) {
// eslint-disable-next-line no-console
console.error(`✅ Hydration code treeshakeable`);
} else {

@ -1,55 +1,17 @@
import * as state from './state.js';
import { CompileDiagnostic } from './utils/compile_diagnostic.js';
/** @typedef {{ start?: number, end?: number }} NodeLike */
// interface is duplicated between here (used internally) and ./interfaces.js
// (exposed publicly), and I'm not sure how to avoid that
export class CompileError extends Error {
export class InternalCompileError extends CompileDiagnostic {
name = 'CompileError';
filename = state.filename;
/** @type {import('#compiler').CompileError['position']} */
position = undefined;
/** @type {import('#compiler').CompileError['start']} */
start = undefined;
/** @type {import('#compiler').CompileError['end']} */
end = undefined;
/**
*
* @param {string} code
* @param {string} message
* @param {[number, number] | undefined} position
*/
constructor(code, message, position) {
super(message);
this.code = code;
this.position = position;
if (position) {
this.start = state.locator(position[0]);
this.end = state.locator(position[1]);
}
}
toString() {
let out = `${this.name}: ${this.message}`;
out += `\n(${this.code})`;
if (this.filename) {
out += `\n${this.filename}`;
if (this.start) {
out += `${this.start.line}:${this.start.column}`;
}
}
return out;
super(code, message, position);
}
}
@ -63,10 +25,10 @@ function e(node, code, message) {
const start = typeof node === 'number' ? node : node?.start;
const end = typeof node === 'number' ? node : node?.end;
throw new CompileError(
throw new InternalCompileError(
code,
message,
start !== undefined && end !== undefined ? [start, end] : undefined
start !== undefined ? [start, end ?? start] : undefined
);
}

@ -1,7 +1,21 @@
import { filename, locator, warnings, ignore_stack, ignore_map } from './state.js';
import { warnings, ignore_stack, ignore_map } from './state.js';
import { CompileDiagnostic } from './utils/compile_diagnostic.js';
/** @typedef {{ start?: number, end?: number }} NodeLike */
export class InternalCompileWarning extends CompileDiagnostic {
name = 'CompileWarning';
/**
* @param {string} code
* @param {string} message
* @param {[number, number] | undefined} position
*/
constructor(code, message, position) {
super(code, message, position);
}
}
/**
* @param {null | NodeLike} node
* @param {string} code
@ -14,13 +28,13 @@ function w(node, code, message) {
}
if (stack && stack.at(-1)?.has(code)) return;
warnings.push({
code,
message,
filename,
start: node?.start !== undefined ? locator(node.start) : undefined,
end: node?.end !== undefined ? locator(node.end) : undefined
});
warnings.push(
new InternalCompileWarning(
code,
message,
node && node.start !== undefined ? [node.start, node.end ?? node.start] : undefined
)
);
}
export const codes = CODES;

@ -306,7 +306,7 @@ declare function $props(): any;
*
* https://svelte-5-preview.vercel.app/docs/runes#$bindable
*/
declare function $bindable<T>(t?: T): T;
declare function $bindable<T>(fallback?: T): T;
/**
* Inspects one or more values whenever they, or the properties they contain, change. Example:

@ -1,3 +1,4 @@
/** @import { FlipParams, AnimationConfig } from './public.js' */
import { cubicOut } from '../easing/index.js';
/**
@ -7,8 +8,8 @@ import { cubicOut } from '../easing/index.js';
* https://svelte.dev/docs/svelte-animate#flip
* @param {Element} node
* @param {{ from: DOMRect; to: DOMRect }} fromTo
* @param {import('./public.js').FlipParams} params
* @returns {import('./public.js').AnimationConfig}
* @param {FlipParams} params
* @returns {AnimationConfig}
*/
export function flip(node, { from, to }, params = {}) {
const style = getComputedStyle(node);

@ -1,51 +1,18 @@
/* This file is generated by scripts/process-messages/index.js. Do not edit! */
import * as state from './state.js';
import { CompileDiagnostic } from './utils/compile_diagnostic.js';
/** @typedef {{ start?: number, end?: number }} NodeLike */
// interface is duplicated between here (used internally) and ./interfaces.js
// (exposed publicly), and I'm not sure how to avoid that
export class CompileError extends Error {
export class InternalCompileError extends CompileDiagnostic {
name = 'CompileError';
filename = state.filename;
/** @type {import('#compiler').CompileError['position']} */
position = undefined;
/** @type {import('#compiler').CompileError['start']} */
start = undefined;
/** @type {import('#compiler').CompileError['end']} */
end = undefined;
/**
*
* @param {string} code
* @param {string} message
* @param {[number, number] | undefined} position
*/
constructor(code, message, position) {
super(message);
this.code = code;
this.position = position;
if (position) {
this.start = state.locator(position[0]);
this.end = state.locator(position[1]);
}
}
toString() {
let out = `${this.name}: ${this.message}`;
out += `\n(${this.code})`;
if (this.filename) {
out += `\n${this.filename}`;
if (this.start) {
out += `${this.start.line}:${this.start.column}`;
}
}
return out;
super(code, message, position);
}
}
@ -59,7 +26,7 @@ function e(node, code, message) {
const start = typeof node === 'number' ? node : node?.start;
const end = typeof node === 'number' ? node : node?.end;
throw new CompileError(code, message, start !== undefined && end !== undefined ? [start, end] : undefined);
throw new InternalCompileError(code, message, start !== undefined ? [start, end ?? start] : undefined);
}
/**
@ -276,6 +243,15 @@ export function props_duplicate(node) {
e(node, "props_duplicate", "Cannot use `$props()` more than once");
}
/**
* Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals)
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function props_illegal_name(node) {
e(node, "props_illegal_name", "Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals)");
}
/**
* `$props()` can only be used with an object destructuring pattern
* @param {null | number | NodeLike} node
@ -754,6 +730,16 @@ export function block_unclosed(node) {
e(node, "block_unclosed", "Block was left open");
}
/**
* Expected a `%character%` character immediately following the opening bracket
* @param {null | number | NodeLike} node
* @param {string} character
* @returns {never}
*/
export function block_unexpected_character(node, character) {
e(node, "block_unexpected_character", `Expected a \`${character}\` character immediately following the opening bracket`);
}
/**
* Unexpected block closing tag
* @param {null | number | NodeLike} node

@ -1,3 +1,5 @@
/** @import { LegacyRoot } from './types/legacy-nodes.js' */
/** @import { CompileOptions, CompileResult, ValidatedCompileOptions, ModuleCompileOptions, Root } from '#compiler' */
import { walk as zimmerframe_walk } from 'zimmerframe';
import { convert } from './legacy.js';
import { parse as parse_acorn } from './phases/1-parse/acorn.js';
@ -14,8 +16,8 @@ export { default as preprocess } from './preprocess/index.js';
*
* https://svelte.dev/docs/svelte-compiler#svelte-compile
* @param {string} source The component source code
* @param {import('#compiler').CompileOptions} options The compiler options
* @returns {import('#compiler').CompileResult}
* @param {CompileOptions} options The compiler options
* @returns {CompileResult}
*/
export function compile(source, options) {
const validated = validate_component_options(options, '');
@ -25,7 +27,7 @@ export function compile(source, options) {
const { customElement: customElementOptions, ...parsed_options } = parsed.options || {};
/** @type {import('#compiler').ValidatedCompileOptions} */
/** @type {ValidatedCompileOptions} */
const combined_options = {
...validated,
...parsed_options,
@ -52,8 +54,8 @@ export function compile(source, options) {
*
* https://svelte.dev/docs/svelte-compiler#svelte-compile
* @param {string} source The component source code
* @param {import('#compiler').ModuleCompileOptions} options
* @returns {import('#compiler').CompileResult}
* @param {ModuleCompileOptions} options
* @returns {CompileResult}
*/
export function compileModule(source, options) {
const validated = validate_module_options(options, '');
@ -73,7 +75,7 @@ export function compileModule(source, options) {
* @overload
* @param {string} source
* @param {{ filename?: string; modern: true }} options
* @returns {import('#compiler').Root}
* @returns {Root}
*/
/**
@ -86,7 +88,7 @@ export function compileModule(source, options) {
* @overload
* @param {string} source
* @param {{ filename?: string; modern?: false }} [options]
* @returns {import('./types/legacy-nodes.js').LegacyRoot}
* @returns {LegacyRoot}
*/
/**
@ -98,7 +100,7 @@ export function compileModule(source, options) {
* https://svelte.dev/docs/svelte-compiler#svelte-parse
* @param {string} source
* @param {{ filename?: string; rootDir?: string; modern?: boolean }} [options]
* @returns {import('#compiler').Root | import('./types/legacy-nodes.js').LegacyRoot}
* @returns {Root | LegacyRoot}
*/
export function parse(source, { filename, rootDir, modern } = {}) {
state.reset(source, { filename, rootDir }); // TODO it's weird to require filename/rootDir here. reconsider the API
@ -109,7 +111,7 @@ export function parse(source, { filename, rootDir, modern } = {}) {
/**
* @param {string} source
* @param {import('#compiler').Root} ast
* @param {Root} ast
* @param {boolean | undefined} modern
*/
function to_public_ast(source, ast, modern) {
@ -139,6 +141,5 @@ export function walk() {
);
}
export { CompileError } from './errors.js';
export { VERSION } from '../version.js';
export { migrate } from './migrate/index.js';

@ -383,11 +383,12 @@ export function convert(source, ast) {
};
}
const start = node.elseif ? node.consequent.nodes[0].start : node.start;
remove_surrounding_whitespace_nodes(node.consequent.nodes);
return {
type: 'IfBlock',
start: node.start,
start,
end: node.end,
expression: node.test,
children: node.consequent.nodes.map(

@ -1,3 +1,9 @@
/** @import { VariableDeclarator, Node, Identifier } from 'estree' */
/** @import { SvelteNode } from '../types/template.js' */
/** @import { Visitors } from 'zimmerframe' */
/** @import { ComponentAnalysis } from '../phases/types.js' */
/** @import { Scope } from '../phases/scope.js' */
/** @import * as Compiler from '#compiler' */
import MagicString from 'magic-string';
import { walk } from 'zimmerframe';
import { parse } from '../phases/1-parse/index.js';
@ -24,7 +30,7 @@ export function migrate(source) {
const { customElement: customElementOptions, ...parsed_options } = parsed.options || {};
/** @type {import('#compiler').ValidatedCompileOptions} */
/** @type {Compiler.ValidatedCompileOptions} */
const combined_options = {
...validate_component_options({}, ''),
...parsed_options,
@ -139,6 +145,47 @@ export function migrate(source) {
}
}
/**
* If true, then we need to move all reactive statements to the end of the script block,
* in their correct order. Svelte 4 reordered reactive statements, $derived/$effect.pre
* don't have this behavior.
*/
let needs_reordering = false;
for (const [node, { dependencies }] of state.analysis.reactive_statements) {
/** @type {Compiler.Binding[]} */
let ids = [];
if (
node.body.type === 'ExpressionStatement' &&
node.body.expression.type === 'AssignmentExpression'
) {
ids = extract_identifiers(node.body.expression.left)
.map((id) => state.scope.get(id.name))
.filter((id) => !!id);
}
if (
dependencies.some(
(dep) =>
!ids.includes(dep) &&
/** @type {number} */ (dep.node.start) > /** @type {number} */ (node.start)
)
) {
needs_reordering = true;
break;
}
}
if (needs_reordering) {
const nodes = Array.from(state.analysis.reactive_statements.keys());
for (const node of nodes) {
const { start, end } = get_node_range(source, node);
str.appendLeft(end, '\n');
str.move(start, end, /** @type {number} */ (parsed.instance?.content.end));
str.remove(start - (source[start - 2] === '\r' ? 2 : 1), start);
}
}
if (state.needs_run && !added_legacy_import) {
if (parsed.instance) {
str.appendRight(
@ -160,9 +207,9 @@ export function migrate(source) {
/**
* @typedef {{
* scope: import('../phases/scope.js').Scope;
* scope: Scope;
* str: MagicString;
* analysis: import('../phases/types.js').ComponentAnalysis;
* analysis: ComponentAnalysis;
* indent: string;
* props: Array<{ local: string; exported: string; init: string; bindable: boolean; slot_name?: string; optional: boolean; type: string; comment?: string }>;
* props_insertion_point: number;
@ -175,7 +222,7 @@ export function migrate(source) {
* }} State
*/
/** @type {import('zimmerframe').Visitors<import('../types/template.js').SvelteNode, State>} */
/** @type {Visitors<SvelteNode, State>} */
const instance_script = {
_(node, { state, next }) {
// @ts-expect-error
@ -281,9 +328,7 @@ const instance_script = {
// }
}
const binding = /** @type {import('#compiler').Binding} */ (
state.scope.get(declarator.id.name)
);
const binding = /** @type {Compiler.Binding} */ (state.scope.get(declarator.id.name));
if (
state.analysis.uses_props &&
@ -373,7 +418,7 @@ const instance_script = {
/** @type {number} */ (node.body.expression.start),
'let '
);
state.str.prependLeft(
state.str.prependRight(
/** @type {number} */ (node.body.expression.right.start),
'$derived('
);
@ -384,14 +429,14 @@ const instance_script = {
');'
);
} else {
state.str.appendRight(/** @type {number} */ (node.end), ');');
state.str.appendLeft(/** @type {number} */ (node.end), ');');
}
return;
} else {
for (const binding of reassigned_bindings) {
if (binding && ids.includes(binding.node)) {
// implicitly-declared variable which we need to make explicit
state.str.prependLeft(
state.str.prependRight(
/** @type {number} */ (node.start),
`let ${binding.node.name}${binding.kind === 'state' ? ' = $state()' : ''};\n${state.indent}`
);
@ -424,12 +469,12 @@ const instance_script = {
[/** @type {number} */ (node.body.end), state.end]
]
});
state.str.appendRight(/** @type {number} */ (node.end), `\n${state.indent}});`);
state.str.appendLeft(/** @type {number} */ (node.end), `\n${state.indent}});`);
}
}
};
/** @type {import('zimmerframe').Visitors<import('../types/template.js').SvelteNode, State>} */
/** @type {Visitors<SvelteNode, State>} */
const template = {
Identifier(node, { state, path }) {
handle_identifier(node, state, path);
@ -543,15 +588,15 @@ const template = {
};
/**
* @param {import('estree').VariableDeclarator} declarator
* @param {VariableDeclarator} declarator
* @param {MagicString} str
* @param {import('#compiler').SvelteNode[]} path
* @param {Compiler.SvelteNode[]} path
*/
function extract_type_and_comment(declarator, str, path) {
const parent = path.at(-1);
// Try to find jsdoc above the declaration
let comment_node = /** @type {import('estree').Node} */ (parent)?.leadingComments?.at(-1);
let comment_node = /** @type {Node} */ (parent)?.leadingComments?.at(-1);
if (comment_node?.type !== 'Block') comment_node = undefined;
const comment_start = /** @type {any} */ (comment_node)?.start;
@ -590,11 +635,11 @@ function extract_type_and_comment(declarator, str, path) {
}
/**
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement | import('#compiler').SvelteWindow | import('#compiler').SvelteDocument | import('#compiler').SvelteBody} element
* @param {Compiler.RegularElement | Compiler.SvelteElement | Compiler.SvelteWindow | Compiler.SvelteDocument | Compiler.SvelteBody} element
* @param {State} state
*/
function handle_events(element, state) {
/** @type {Map<string, import('#compiler').OnDirective[]>} */
/** @type {Map<string, Compiler.OnDirective[]>} */
const handlers = new Map();
for (const attribute of element.attributes) {
if (attribute.type !== 'OnDirective') continue;
@ -805,7 +850,31 @@ function get_indent(state, ...nodes) {
}
/**
* @param {import('#compiler').OnDirective} last
* Returns start and end of the node. If the start is preceeded with white-space-only before a line break,
* the start will be the start of the line.
* @param {string} source
* @param {Node} node
*/
function get_node_range(source, node) {
let start = /** @type {number} */ (node.start);
let end = /** @type {number} */ (node.end);
let idx = start;
while (source[idx - 1] !== '\n' && source[idx - 1] !== '\r') {
idx--;
if (source[idx] !== ' ' && source[idx] !== '\t') {
idx = start;
break;
}
}
start = idx;
return { start, end };
}
/**
* @param {Compiler.OnDirective} last
* @param {State} state
*/
function generate_event_name(last, state) {
@ -821,7 +890,7 @@ function generate_event_name(last, state) {
}
/**
* @param {import('estree').Identifier} node
* @param {Identifier} node
* @param {State} state
* @param {any[]} path
*/

@ -1,8 +1,9 @@
/** @import { TemplateNode, Fragment, Root, SvelteOptionsRaw } from '#compiler' */
// @ts-expect-error acorn type definitions are borked in the release we use
import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment.js';
import { regex_whitespace } from '../patterns.js';
import { reserved } from './utils/names.js';
import { reserved } from '../../../constants.js';
import full_char_code_at from './utils/full_char_code_at.js';
import * as e from '../../errors.js';
import { create_fragment } from './utils/create.js';
@ -26,13 +27,13 @@ export class Parser {
/** Whether we're parsing in TypeScript mode */
ts = false;
/** @type {import('#compiler').TemplateNode[]} */
/** @type {TemplateNode[]} */
stack = [];
/** @type {import('#compiler').Fragment[]} */
/** @type {Fragment[]} */
fragments = [];
/** @type {import('#compiler').Root} */
/** @type {Root} */
root;
/** @type {Record<string, boolean>} */
@ -120,9 +121,7 @@ export class Parser {
(thing) => thing.type === 'SvelteOptions'
);
if (options_index !== -1) {
const options = /** @type {import('#compiler').SvelteOptionsRaw} */ (
this.root.fragment.nodes[options_index]
);
const options = /** @type {SvelteOptionsRaw} */ (this.root.fragment.nodes[options_index]);
this.root.fragment.nodes.splice(options_index, 1);
this.root.options = read_options(options);
// We need this for the old AST format
@ -289,7 +288,7 @@ export class Parser {
/**
* @param {string} template
* @returns {import('#compiler').Root}
* @returns {Root}
*/
export function parse(template) {
const parser = new Parser(template);

@ -1,3 +1,6 @@
/** @import { Location } from 'locate-character' */
/** @import { Pattern } from 'estree' */
/** @import { Parser } from '../index.js' */
// @ts-expect-error acorn type definitions are borked in the release we use
import { isIdentifierStart } from 'acorn';
import full_char_code_at from '../utils/full_char_code_at.js';
@ -13,8 +16,8 @@ import * as e from '../../../errors.js';
import { locator } from '../../../state.js';
/**
* @param {import('../index.js').Parser} parser
* @returns {import('estree').Pattern}
* @param {Parser} parser
* @returns {Pattern}
*/
export default function read_pattern(parser) {
const start = parser.index;
@ -30,8 +33,8 @@ export default function read_pattern(parser) {
name,
start,
loc: {
start: /** @type {import('locate-character').Location} */ (locator(start)),
end: /** @type {import('locate-character').Location} */ (locator(parser.index))
start: /** @type {Location} */ (locator(start)),
end: /** @type {Location} */ (locator(parser.index))
},
end: parser.index,
typeAnnotation: annotation
@ -95,7 +98,7 @@ export default function read_pattern(parser) {
}
/**
* @param {import('../index.js').Parser} parser
* @param {Parser} parser
* @returns {any}
*/
function read_type_annotation(parser) {

@ -1,14 +1,16 @@
/** @import { ObjectExpression } from 'estree' */
/** @import { SvelteOptionsRaw, Root, SvelteOptions } from '#compiler' */
import { namespace_mathml, namespace_svg } from '../../../../constants.js';
import * as e from '../../../errors.js';
const regex_valid_tag_name = /^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/;
/**
* @param {import('#compiler').SvelteOptionsRaw} node
* @returns {import('#compiler').Root['options']}
* @param {SvelteOptionsRaw} node
* @returns {Root['options']}
*/
export default function read_options(node) {
/** @type {import('#compiler').SvelteOptions} */
/** @type {SvelteOptions} */
const component_options = {
start: node.start,
end: node.end,
@ -37,7 +39,7 @@ export default function read_options(node) {
break; // eslint doesn't know this is unnecessary
}
case 'customElement': {
/** @type {import('#compiler').SvelteOptions['customElement']} */
/** @type {SvelteOptions['customElement']} */
const ce = { tag: '' };
const { value } = attribute;
@ -86,8 +88,7 @@ export default function read_options(node) {
e.svelte_options_invalid_customelement_props(attribute);
}
ce.props = {};
for (const property of /** @type {import('estree').ObjectExpression} */ (props)
.properties) {
for (const property of /** @type {ObjectExpression} */ (props).properties) {
if (
property.type !== 'Property' ||
property.computed ||

@ -1,3 +1,6 @@
/** @import { Program } from 'estree' */
/** @import { Attribute, SpreadAttribute, Directive, Script } from '#compiler' */
/** @import { Parser } from '../index.js' */
import * as acorn from '../acorn.js';
import { regex_not_newline_characters } from '../../patterns.js';
import * as e from '../../../errors.js';
@ -29,10 +32,10 @@ function get_context(attributes) {
}
/**
* @param {import('../index.js').Parser} parser
* @param {Parser} parser
* @param {number} start
* @param {Array<import('#compiler').Attribute | import('#compiler').SpreadAttribute | import('#compiler').Directive>} attributes
* @returns {import('#compiler').Script}
* @param {Array<Attribute | SpreadAttribute | Directive>} attributes
* @returns {Script}
*/
export function read_script(parser, start, attributes) {
const script_start = parser.index;
@ -45,7 +48,7 @@ export function read_script(parser, start, attributes) {
parser.template.slice(0, script_start).replace(regex_not_newline_characters, ' ') + data;
parser.read(regex_starts_with_closing_script_tag);
/** @type {import('estree').Program} */
/** @type {Program} */
let ast;
try {

@ -1,3 +1,5 @@
/** @import { Attribute, SpreadAttribute, Directive, Css } from '#compiler' */
/** @import { Parser } from '../index.js' */
import * as e from '../../../errors.js';
const REGEX_MATCHER = /^[~^$*|]?=/;
@ -14,10 +16,10 @@ const REGEX_COMMENT_CLOSE = /\*\//;
const REGEX_HTML_COMMENT_CLOSE = /-->/;
/**
* @param {import('../index.js').Parser} parser
* @param {Parser} parser
* @param {number} start
* @param {Array<import('#compiler').Attribute | import('#compiler').SpreadAttribute | import('#compiler').Directive>} attributes
* @returns {import('#compiler').Css.StyleSheet}
* @param {Array<Attribute | SpreadAttribute | Directive>} attributes
* @returns {Css.StyleSheet}
*/
export default function read_style(parser, start, attributes) {
const content_start = parser.index;
@ -42,12 +44,12 @@ export default function read_style(parser, start, attributes) {
}
/**
* @param {import('../index.js').Parser} parser
* @param {Parser} parser
* @param {string} close
* @returns {any[]}
*/
function read_body(parser, close) {
/** @type {Array<import('#compiler').Css.Rule | import('#compiler').Css.Atrule>} */
/** @type {Array<Css.Rule | Css.Atrule>} */
const children = [];
while (parser.index < parser.template.length) {
@ -68,8 +70,8 @@ function read_body(parser, close) {
}
/**
* @param {import('../index.js').Parser} parser
* @returns {import('#compiler').Css.Atrule}
* @param {Parser} parser
* @returns {Css.Atrule}
*/
function read_at_rule(parser) {
const start = parser.index;
@ -79,7 +81,7 @@ function read_at_rule(parser) {
const prelude = read_value(parser);
/** @type {import('#compiler').Css.Block | null} */
/** @type {Css.Block | null} */
let block = null;
if (parser.match('{')) {
@ -101,8 +103,8 @@ function read_at_rule(parser) {
}
/**
* @param {import('../index.js').Parser} parser
* @returns {import('#compiler').Css.Rule}
* @param {Parser} parser
* @returns {Css.Rule}
*/
function read_rule(parser) {
const start = parser.index;
@ -122,12 +124,12 @@ function read_rule(parser) {
}
/**
* @param {import('../index.js').Parser} parser
* @param {Parser} parser
* @param {boolean} [inside_pseudo_class]
* @returns {import('#compiler').Css.SelectorList}
* @returns {Css.SelectorList}
*/
function read_selector_list(parser, inside_pseudo_class = false) {
/** @type {import('#compiler').Css.ComplexSelector[]} */
/** @type {Css.ComplexSelector[]} */
const children = [];
allow_comment_or_whitespace(parser);
@ -158,20 +160,20 @@ function read_selector_list(parser, inside_pseudo_class = false) {
}
/**
* @param {import('../index.js').Parser} parser
* @param {Parser} parser
* @param {boolean} [inside_pseudo_class]
* @returns {import('#compiler').Css.ComplexSelector}
* @returns {Css.ComplexSelector}
*/
function read_selector(parser, inside_pseudo_class = false) {
const list_start = parser.index;
/** @type {import('#compiler').Css.RelativeSelector[]} */
/** @type {Css.RelativeSelector[]} */
const children = [];
/**
* @param {import('#compiler').Css.Combinator | null} combinator
* @param {Css.Combinator | null} combinator
* @param {number} start
* @returns {import('#compiler').Css.RelativeSelector}
* @returns {Css.RelativeSelector}
*/
function create_selector(combinator, start) {
return {
@ -188,7 +190,7 @@ function read_selector(parser, inside_pseudo_class = false) {
};
}
/** @type {import('#compiler').Css.RelativeSelector} */
/** @type {Css.RelativeSelector} */
let relative_selector = create_selector(null, parser.index);
while (parser.index < parser.template.length) {
@ -245,7 +247,7 @@ function read_selector(parser, inside_pseudo_class = false) {
} else if (parser.eat(':')) {
const name = read_identifier(parser);
/** @type {null | import('#compiler').Css.SelectorList} */
/** @type {null | Css.SelectorList} */
let args = null;
if (parser.eat('(')) {
@ -373,8 +375,8 @@ function read_selector(parser, inside_pseudo_class = false) {
}
/**
* @param {import('../index.js').Parser} parser
* @returns {import('#compiler').Css.Combinator | null}
* @param {Parser} parser
* @returns {Css.Combinator | null}
*/
function read_combinator(parser) {
const start = parser.index;
@ -408,15 +410,15 @@ function read_combinator(parser) {
}
/**
* @param {import('../index.js').Parser} parser
* @returns {import('#compiler').Css.Block}
* @param {Parser} parser
* @returns {Css.Block}
*/
function read_block(parser) {
const start = parser.index;
parser.eat('{', true);
/** @type {Array<import('#compiler').Css.Declaration | import('#compiler').Css.Rule | import('#compiler').Css.Atrule>} */
/** @type {Array<Css.Declaration | Css.Rule | Css.Atrule>} */
const children = [];
while (parser.index < parser.template.length) {
@ -442,8 +444,8 @@ function read_block(parser) {
/**
* Reads a declaration, rule or at-rule
*
* @param {import('../index.js').Parser} parser
* @returns {import('#compiler').Css.Declaration | import('#compiler').Css.Rule | import('#compiler').Css.Atrule}
* @param {Parser} parser
* @returns {Css.Declaration | Css.Rule | Css.Atrule}
*/
function read_block_item(parser) {
if (parser.match('@')) {
@ -461,8 +463,8 @@ function read_block_item(parser) {
}
/**
* @param {import('../index.js').Parser} parser
* @returns {import('#compiler').Css.Declaration}
* @param {Parser} parser
* @returns {Css.Declaration}
*/
function read_declaration(parser) {
const start = parser.index;
@ -495,7 +497,7 @@ function read_declaration(parser) {
}
/**
* @param {import('../index.js').Parser} parser
* @param {Parser} parser
* @returns {string}
*/
function read_value(parser) {
@ -537,7 +539,7 @@ function read_value(parser) {
/**
* Read a property that may or may not be quoted, e.g.
* `foo` or `'foo bar'` or `"foo bar"`
* @param {import('../index.js').Parser} parser
* @param {Parser} parser
*/
function read_attribute_value(parser) {
let value = '';
@ -569,7 +571,7 @@ function read_attribute_value(parser) {
/**
* https://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
* @param {import('../index.js').Parser} parser
* @param {Parser} parser
*/
function read_identifier(parser) {
const start = parser.index;
@ -608,7 +610,7 @@ function read_identifier(parser) {
return identifier;
}
/** @param {import('../index.js').Parser} parser */
/** @param {Parser} parser */
function allow_comment_or_whitespace(parser) {
parser.allow_whitespace();
while (parser.match('/*') || parser.match('<!--')) {

@ -1,9 +1,11 @@
/** @import { Context, Visitors } from 'zimmerframe' */
/** @import { FunctionExpression, FunctionDeclaration } from 'estree' */
import { walk } from 'zimmerframe';
import * as b from '../../utils/builders.js';
/**
* @param {import('estree').FunctionExpression | import('estree').FunctionDeclaration} node
* @param {import('zimmerframe').Context<any, any>} context
* @param {FunctionExpression | FunctionDeclaration} node
* @param {Context<any, any>} context
*/
function remove_this_param(node, context) {
if (node.params[0]?.type === 'Identifier' && node.params[0].name === 'this') {
@ -12,7 +14,7 @@ function remove_this_param(node, context) {
return context.next();
}
/** @type {import('zimmerframe').Visitors<any, null>} */
/** @type {Visitors<any, null>} */
const visitors = {
ImportDeclaration(node) {
if (node.importKind === 'type') return b.empty;

@ -1,4 +1,6 @@
import { is_void } from '../utils/names.js';
/** @import { Parser } from '../index.js' */
/** @import * as Compiler from '#compiler' */
import { is_void } from '../../../../constants.js';
import read_expression from '../read/expression.js';
import { read_script } from '../read/script.js';
import read_style from '../read/style.js';
@ -14,7 +16,7 @@ const valid_tag_name = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
/** Invalid attribute characters if the attribute is not surrounded by quotes */
const regex_starts_with_invalid_attr_value = /^(\/>|[\s"'=<>`])/;
/** @type {Map<string, import('#compiler').ElementLike['type']>} */
/** @type {Map<string, Compiler.ElementLike['type']>} */
const root_only_meta_tags = new Map([
['svelte:head', 'SvelteHead'],
['svelte:options', 'SvelteOptions'],
@ -23,7 +25,7 @@ const root_only_meta_tags = new Map([
['svelte:body', 'SvelteBody']
]);
/** @type {Map<string, import('#compiler').ElementLike['type']>} */
/** @type {Map<string, Compiler.ElementLike['type']>} */
const meta_tags = new Map([
...root_only_meta_tags,
['svelte:element', 'SvelteElement'],
@ -39,7 +41,7 @@ const COMPONENT = /^svelte:component(?=[\s/>])/;
const SLOT = /^svelte:fragment(?=[\s/>])/;
const ELEMENT = /^svelte:element(?=[\s/>])/;
/** @param {import('#compiler').TemplateNode[]} stack */
/** @param {Compiler.TemplateNode[]} stack */
function parent_is_head(stack) {
let i = stack.length;
while (i--) {
@ -50,14 +52,14 @@ function parent_is_head(stack) {
return false;
}
/** @param {import('#compiler').TemplateNode[]} stack */
/** @param {Compiler.TemplateNode[]} stack */
function parent_is_shadowroot_template(stack) {
// https://developer.chrome.com/docs/css-ui/declarative-shadow-dom#building_a_declarative_shadow_root
let i = stack.length;
while (i--) {
if (
stack[i].type === 'RegularElement' &&
/** @type {import('#compiler').RegularElement} */ (stack[i]).attributes.some(
/** @type {Compiler.RegularElement} */ (stack[i]).attributes.some(
(a) => a.type === 'Attribute' && a.name === 'shadowrootmode'
)
) {
@ -71,7 +73,7 @@ const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
const regex_closing_comment = /-->/;
const regex_capital_letter = /[A-Z]/;
/** @param {import('../index.js').Parser} parser */
/** @param {Parser} parser */
export default function element(parser) {
const start = parser.index++;
@ -81,7 +83,7 @@ export default function element(parser) {
const data = parser.read_until(regex_closing_comment);
parser.eat('-->', true);
/** @type {ReturnType<typeof parser.append<import('#compiler').Comment>>} */
/** @type {ReturnType<typeof parser.append<Compiler.Comment>>} */
parser.append({
type: 'Comment',
start,
@ -100,10 +102,10 @@ export default function element(parser) {
if (is_closing_tag) {
if (
['svelte:options', 'svelte:window', 'svelte:body', 'svelte:document'].includes(name) &&
/** @type {import('#compiler').ElementLike} */ (parent).fragment.nodes.length
/** @type {Compiler.ElementLike} */ (parent).fragment.nodes.length
) {
e.svelte_meta_invalid_content(
/** @type {import('#compiler').ElementLike} */ (parent).fragment.nodes[0].start,
/** @type {Compiler.ElementLike} */ (parent).fragment.nodes[0].start,
name
);
}
@ -131,7 +133,7 @@ export default function element(parser) {
? 'SlotElement'
: 'RegularElement';
/** @type {import('#compiler').ElementLike} */
/** @type {Compiler.ElementLike} */
const element =
type === 'RegularElement'
? {
@ -149,7 +151,7 @@ export default function element(parser) {
},
parent: null
}
: /** @type {import('#compiler').ElementLike} */ ({
: /** @type {Compiler.ElementLike} */ ({
type,
start,
end: -1,
@ -172,7 +174,7 @@ export default function element(parser) {
parser.eat('>', true);
// close any elements that don't have their own closing tags, e.g. <div><p></div>
while (/** @type {import('#compiler').RegularElement} */ (parent).name !== name) {
while (/** @type {Compiler.RegularElement} */ (parent).name !== name) {
if (parent.type !== 'RegularElement') {
if (parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name) {
e.element_invalid_closing_tag_autoclosed(start, name, parser.last_auto_closed_tag.reason);
@ -238,9 +240,7 @@ export default function element(parser) {
e.svelte_component_missing_this(start);
}
const definition = /** @type {import('#compiler').Attribute} */ (
element.attributes.splice(index, 1)[0]
);
const definition = /** @type {Compiler.Attribute} */ (element.attributes.splice(index, 1)[0]);
if (
definition.value === true ||
definition.value.length !== 1 ||
@ -261,9 +261,7 @@ export default function element(parser) {
e.svelte_element_missing_this(start);
}
const definition = /** @type {import('#compiler').Attribute} */ (
element.attributes.splice(index, 1)[0]
);
const definition = /** @type {Compiler.Attribute} */ (element.attributes.splice(index, 1)[0]);
if (definition.value === true) {
e.svelte_element_missing_this(definition);
@ -296,7 +294,7 @@ export default function element(parser) {
if (is_top_level_script_or_style) {
parser.eat('>', true);
/** @type {import('#compiler').Comment | null} */
/** @type {Compiler.Comment | null} */
let prev_comment = null;
for (let i = current.fragment.nodes.length - 1; i >= 0; i--) {
const node = current.fragment.nodes[i];
@ -363,7 +361,7 @@ export default function element(parser) {
const data = parser.read_until(new RegExp(`</${name}>`));
const end = parser.index;
/** @type {import('#compiler').Text} */
/** @type {Compiler.Text} */
const node = {
start,
end,
@ -384,7 +382,7 @@ export default function element(parser) {
const regex_whitespace_or_slash_or_closing_tag = /(\s|\/|>)/;
/** @param {import('../index.js').Parser} parser */
/** @param {Parser} parser */
function read_tag_name(parser) {
const start = parser.index;
@ -441,8 +439,8 @@ const regex_starts_with_quote_characters = /^["']/;
const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]+))/;
/**
* @param {import('../index.js').Parser} parser
* @returns {import('#compiler').Attribute | null}
* @param {Parser} parser
* @returns {Compiler.Attribute | null}
*/
function read_static_attribute(parser) {
const start = parser.index;
@ -450,7 +448,7 @@ function read_static_attribute(parser) {
const name = parser.read_until(regex_token_ending_character);
if (!name) return null;
/** @type {true | Array<import('#compiler').Text | import('#compiler').ExpressionTag>} */
/** @type {true | Array<Compiler.Text | Compiler.ExpressionTag>} */
let value = true;
if (parser.eat('=')) {
@ -487,8 +485,8 @@ function read_static_attribute(parser) {
}
/**
* @param {import('../index.js').Parser} parser
* @returns {import('#compiler').Attribute | import('#compiler').SpreadAttribute | import('#compiler').Directive | null}
* @param {Parser} parser
* @returns {Compiler.Attribute | Compiler.SpreadAttribute | Compiler.Directive | null}
*/
function read_attribute(parser) {
const start = parser.index;
@ -502,7 +500,7 @@ function read_attribute(parser) {
parser.allow_whitespace();
parser.eat('}', true);
/** @type {import('#compiler').SpreadAttribute} */
/** @type {Compiler.SpreadAttribute} */
const spread = {
type: 'SpreadAttribute',
start,
@ -527,7 +525,7 @@ function read_attribute(parser) {
parser.allow_whitespace();
parser.eat('}', true);
/** @type {import('#compiler').ExpressionTag} */
/** @type {Compiler.ExpressionTag} */
const expression = {
type: 'ExpressionTag',
start: value_start,
@ -559,7 +557,7 @@ function read_attribute(parser) {
const colon_index = name.indexOf(':');
const type = colon_index !== -1 && get_directive_type(name.slice(0, colon_index));
/** @type {true | Array<import('#compiler').Text | import('#compiler').ExpressionTag>} */
/** @type {true | Array<Compiler.Text | Compiler.ExpressionTag>} */
let value = true;
if (parser.eat('=')) {
parser.allow_whitespace();
@ -604,7 +602,7 @@ function read_attribute(parser) {
}
}
/** @type {import('#compiler').Directive} */
/** @type {Compiler.Directive} */
// @ts-expect-error TODO can't figure out this error
const directive = {
start,
@ -663,7 +661,7 @@ function get_directive_type(name) {
}
/**
* @param {import('../index.js').Parser} parser
* @param {Parser} parser
*/
function read_attribute_value(parser) {
const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null;
@ -714,13 +712,13 @@ function read_attribute_value(parser) {
}
/**
* @param {import('../index.js').Parser} parser
* @param {Parser} parser
* @param {() => boolean} done
* @param {string} location
* @returns {any[]}
*/
function read_sequence(parser, done, location) {
/** @type {import('#compiler').Text} */
/** @type {Compiler.Text} */
let current_chunk = {
start: parser.index,
end: -1,
@ -730,7 +728,7 @@ function read_sequence(parser, done, location) {
parent: null
};
/** @type {Array<import('#compiler').Text | import('#compiler').ExpressionTag>} */
/** @type {Array<Compiler.Text | Compiler.ExpressionTag>} */
const chunks = [];
/** @param {number} end */
@ -768,7 +766,7 @@ function read_sequence(parser, done, location) {
parser.allow_whitespace();
parser.eat('}', true);
/** @type {import('#compiler').ExpressionTag} */
/** @type {Compiler.ExpressionTag} */
const chunk = {
type: 'ExpressionTag',
start: index,

@ -1,8 +1,9 @@
/** @import { Parser } from '../index.js' */
import element from './element.js';
import tag from './tag.js';
import text from './text.js';
/** @param {import('../index.js').Parser} parser */
/** @param {Parser} parser */
export default function fragment(parser) {
if (parser.match('<')) {
return element;

@ -39,7 +39,8 @@ export default function tag(parser) {
/** @param {import('../index.js').Parser} parser */
function open(parser) {
const start = parser.index - 2;
let start = parser.index - 2;
while (parser.template[start] !== '{') start -= 1;
if (parser.eat('if')) {
parser.require_whitespace();
@ -350,9 +351,12 @@ function next(parser) {
parser.allow_whitespace();
parser.eat('}', true);
let elseif_start = start - 1;
while (parser.template[elseif_start] !== '{') elseif_start -= 1;
/** @type {ReturnType<typeof parser.append<import('#compiler').IfBlock>>} */
const child = parser.append({
start: parser.index,
start: elseif_start,
end: -1,
type: 'IfBlock',
elseif: true,
@ -613,7 +617,10 @@ function special(parser) {
type: 'RenderTag',
start,
end: parser.index,
expression: expression
expression: expression,
metadata: {
dynamic: false
}
});
}
}

@ -1,6 +1,8 @@
/** @import { Text } from '#compiler' */
/** @import { Parser } from '../index.js' */
import { decode_character_references } from '../utils/html.js';
/** @param {import('../index.js').Parser} parser */
/** @param {Parser} parser */
export default function text(parser) {
const start = parser.index;
@ -10,7 +12,7 @@ export default function text(parser) {
data += parser.template[parser.index++];
}
/** @type {ReturnType<typeof parser.append<import('#compiler').Text>>} */
/** @type {ReturnType<typeof parser.append<Text>>} */
parser.append({
type: 'Text',
start,

@ -1,6 +1,7 @@
/** @import { Fragment } from '#compiler' */
/**
* @param {any} transparent
* @returns {import('#compiler').Fragment}
* @returns {Fragment}
*/
export function create_fragment(transparent = false) {
return {

@ -1,74 +0,0 @@
export const reserved = [
'arguments',
'await',
'break',
'case',
'catch',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'enum',
'eval',
'export',
'extends',
'false',
'finally',
'for',
'function',
'if',
'implements',
'import',
'in',
'instanceof',
'interface',
'let',
'new',
'null',
'package',
'private',
'protected',
'public',
'return',
'static',
'super',
'switch',
'this',
'throw',
'true',
'try',
'typeof',
'var',
'void',
'while',
'with',
'yield'
];
const void_element_names = [
'area',
'base',
'br',
'col',
'command',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'meta',
'param',
'source',
'track',
'wbr'
];
/** @param {string} name */
export function is_void(name) {
return void_element_names.includes(name) || name.toLowerCase() === '!doctype';
}

@ -1,3 +1,7 @@
/** @import { Visitors } from 'zimmerframe' */
/** @import { AnalysisState } from './types.js' */
/** @import { Attribute, SvelteNode, TemplateNode, RegularElement, SvelteElement } from '#compiler' */
/** @import { ARIARoleDefinitionKey, ARIARoleRelationConcept, ARIAProperty, ARIAPropertyDefinition, ARIARoleDefinition } from 'aria-query' */
import { roles as roles_map, aria, elementRoles } from 'aria-query';
// @ts-expect-error package doesn't provide typings
import { AXObjects, AXObjectRoles, elementAXObjects } from 'axobject-query';
@ -43,21 +47,21 @@ const interactive_roles = non_abstract_roles.filter(
);
/**
* @param {import('aria-query').ARIARoleDefinitionKey} role
* @param {ARIARoleDefinitionKey} role
*/
function is_non_interactive_roles(role) {
return non_interactive_roles.includes(role);
}
/**
* @param {import('aria-query').ARIARoleDefinitionKey} role
* @param {ARIARoleDefinitionKey} role
*/
function is_interactive_roles(role) {
return interactive_roles.includes(role);
}
/**
* @param {import('aria-query').ARIARoleDefinitionKey} role
* @param {ARIARoleDefinitionKey} role
*/
function is_abstract_role(role) {
return abstract_roles.includes(role);
@ -66,7 +70,7 @@ function is_abstract_role(role) {
const presentation_roles = ['presentation', 'none'];
/**
* @param {import('aria-query').ARIARoleDefinitionKey} role
* @param {ARIARoleDefinitionKey} role
*/
function is_presentation_role(role) {
return presentation_roles.includes(role);
@ -74,7 +78,7 @@ function is_presentation_role(role) {
/**
* @param {string} tag_name
* @param {Map<string, import('#compiler').Attribute>} attribute_map
* @param {Map<string, Attribute>} attribute_map
*/
function is_hidden_from_screen_reader(tag_name, attribute_map) {
if (tag_name === 'input') {
@ -92,7 +96,7 @@ function is_hidden_from_screen_reader(tag_name, attribute_map) {
}
/**
* @param {Map<string, import('#compiler').Attribute>} attribute_map
* @param {Map<string, Attribute>} attribute_map
*/
function has_disabled_attribute(attribute_map) {
const disabled_attr_value = get_static_value(attribute_map.get('disabled'));
@ -111,7 +115,7 @@ function has_disabled_attribute(attribute_map) {
}
/**
* @type {import('aria-query').ARIARoleRelationConcept[]}
* @type {ARIARoleRelationConcept[]}
*/
const non_interactive_element_role_schemas = [];
elementRoles.entries().forEach(([schema, roles]) => {
@ -121,7 +125,7 @@ elementRoles.entries().forEach(([schema, roles]) => {
});
/**
* @type {import('aria-query').ARIARoleRelationConcept[]}
* @type {ARIARoleRelationConcept[]}
*/
const interactive_element_role_schemas = [];
elementRoles.entries().forEach(([schema, roles]) => {
@ -137,7 +141,7 @@ const non_interactive_ax_objects = [...AXObjects.keys()].filter((name) =>
);
/**
* @type {import('aria-query').ARIARoleRelationConcept[]}
* @type {ARIARoleRelationConcept[]}
*/
const interactive_element_ax_object_schemas = [];
elementAXObjects.entries().forEach(
@ -152,7 +156,7 @@ elementAXObjects.entries().forEach(
);
/**
* @type {import('aria-query').ARIARoleRelationConcept[]}
* @type {ARIARoleRelationConcept[]}
*/
const non_interactive_element_ax_object_schemas = [];
elementAXObjects.entries().forEach(
@ -167,9 +171,9 @@ elementAXObjects.entries().forEach(
);
/**
* @param {import('aria-query').ARIARoleRelationConcept} schema
* @param {ARIARoleRelationConcept} schema
* @param {string} tag_name
* @param {Map<string, import('#compiler').Attribute>} attribute_map
* @param {Map<string, Attribute>} attribute_map
*/
function match_schema(schema, tag_name, attribute_map) {
if (schema.name !== tag_name) return false;
@ -192,7 +196,7 @@ const ElementInteractivity = /** @type {const} */ ({
/**
* @param {string} tag_name
* @param {Map<string, import('#compiler').Attribute>} attribute_map
* @param {Map<string, Attribute>} attribute_map
* @returns {ElementInteractivity[keyof ElementInteractivity]}
*/
function element_interactivity(tag_name, attribute_map) {
@ -228,7 +232,7 @@ function element_interactivity(tag_name, attribute_map) {
/**
* @param {string} tag_name
* @param {Map<string, import('#compiler').Attribute>} attribute_map
* @param {Map<string, Attribute>} attribute_map
* @returns {boolean}
*/
function is_interactive_element(tag_name, attribute_map) {
@ -237,7 +241,7 @@ function is_interactive_element(tag_name, attribute_map) {
/**
* @param {string} tag_name
* @param {Map<string, import('#compiler').Attribute>} attribute_map
* @param {Map<string, Attribute>} attribute_map
* @returns {boolean}
*/
function is_non_interactive_element(tag_name, attribute_map) {
@ -246,7 +250,7 @@ function is_non_interactive_element(tag_name, attribute_map) {
/**
* @param {string} tag_name
* @param {Map<string, import('#compiler').Attribute>} attribute_map
* @param {Map<string, Attribute>} attribute_map
* @returns {boolean}
*/
function is_static_element(tag_name, attribute_map) {
@ -254,9 +258,9 @@ function is_static_element(tag_name, attribute_map) {
}
/**
* @param {import('aria-query').ARIARoleDefinitionKey} role
* @param {ARIARoleDefinitionKey} role
* @param {string} tag_name
* @param {Map<string, import('#compiler').Attribute>} attribute_map
* @param {Map<string, Attribute>} attribute_map
*/
function is_semantic_role_element(role, tag_name, attribute_map) {
for (const [schema, ax_object] of elementAXObjects.entries()) {
@ -540,7 +544,7 @@ const a11y_non_interactive_element_to_interactive_role_exceptions = {
const combobox_if_list = ['email', 'search', 'tel', 'text', 'url'];
/** @param {Map<string, import('#compiler').Attribute>} attribute_map */
/** @param {Map<string, Attribute>} attribute_map */
function input_implicit_role(attribute_map) {
const type_attribute = attribute_map.get('type');
if (!type_attribute) return;
@ -553,7 +557,7 @@ function input_implicit_role(attribute_map) {
return input_type_to_implicit_role.get(type);
}
/** @param {Map<string, import('#compiler').Attribute>} attribute_map */
/** @param {Map<string, Attribute>} attribute_map */
function menuitem_implicit_role(attribute_map) {
const type_attribute = attribute_map.get('type');
if (!type_attribute) return;
@ -564,7 +568,7 @@ function menuitem_implicit_role(attribute_map) {
/**
* @param {string} name
* @param {Map<string, import('#compiler').Attribute>} attribute_map
* @param {Map<string, Attribute>} attribute_map
*/
function get_implicit_role(name, attribute_map) {
if (name === 'menuitem') {
@ -579,7 +583,7 @@ function get_implicit_role(name, attribute_map) {
const invisible_elements = ['meta', 'html', 'script', 'style'];
/**
* @param {import('#compiler').SvelteNode | null} parent
* @param {SvelteNode | null} parent
* @param {string[]} elements
*/
function is_parent(parent, elements) {
@ -588,15 +592,15 @@ function is_parent(parent, elements) {
if (parent.type === 'RegularElement') {
return elements.includes(parent.name);
}
parent = /** @type {import('#compiler').TemplateNode} */ (parent).parent;
parent = /** @type {TemplateNode} */ (parent).parent;
}
return false;
}
/**
* @param {import('#compiler').Attribute} attribute
* @param {import('aria-query').ARIAProperty} name
* @param {import('aria-query').ARIAPropertyDefinition} schema
* @param {Attribute} attribute
* @param {ARIAProperty} name
* @param {ARIAPropertyDefinition} schema
* @param {string | true | null} value
*/
function validate_aria_attribute_value(attribute, name, schema, value) {
@ -644,7 +648,7 @@ function validate_aria_attribute_value(attribute, name, schema, value) {
}
/**
* @param {import('#compiler').RegularElement |import('#compiler').SvelteElement} node
* @param {RegularElement |SvelteElement} node
* @param {string[]} attributes
* @param {string} name
*/
@ -660,7 +664,7 @@ function warn_missing_attribute(node, attributes, name = node.name) {
}
/**
* @param {import('#compiler').Attribute | undefined} attribute
* @param {Attribute | undefined} attribute
*/
function get_static_value(attribute) {
if (!attribute) return null;
@ -670,7 +674,7 @@ function get_static_value(attribute) {
}
/**
* @param {import('#compiler').Attribute | undefined} attribute
* @param {Attribute | undefined} attribute
*/
function get_static_text_value(attribute) {
const value = get_static_value(attribute);
@ -679,20 +683,20 @@ function get_static_text_value(attribute) {
}
/**
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
* @param {import('./types.js').AnalysisState} state
* @param {RegularElement | SvelteElement} node
* @param {AnalysisState} state
*/
function check_element(node, state) {
// foreign namespace means elements can have completely different meanings, therefore we don't check them
if (state.options.namespace === 'foreign') return;
/** @type {Map<string, import('#compiler').Attribute>} */
/** @type {Map<string, Attribute>} */
const attribute_map = new Map();
/** @type {Set<string>} */
const handlers = new Set();
/** @type {import('#compiler').Attribute[]} */
/** @type {Attribute[]} */
const attributes = [];
const is_dynamic_element = node.type === 'SvelteElement';
@ -748,14 +752,9 @@ function check_element(node, state) {
// aria-proptypes
let value = get_static_value(attribute);
const schema = aria.get(/** @type {import('aria-query').ARIAProperty} */ (name));
const schema = aria.get(/** @type {ARIAProperty} */ (name));
if (schema !== undefined) {
validate_aria_attribute_value(
attribute,
/** @type {import('aria-query').ARIAProperty} */ (name),
schema,
value
);
validate_aria_attribute_value(attribute, /** @type {ARIAProperty} */ (name), schema, value);
}
// aria-activedescendant-has-tabindex
@ -779,8 +778,7 @@ function check_element(node, state) {
const value = get_static_value(attribute);
if (typeof value === 'string') {
for (const c_r of value.split(regex_whitespaces)) {
const current_role =
/** @type {import('aria-query').ARIARoleDefinitionKey} current_role */ (c_r);
const current_role = /** @type {ARIARoleDefinitionKey} current_role */ (c_r);
if (current_role && is_abstract_role(current_role)) {
w.a11y_no_abstract_role(attribute, current_role);
@ -899,9 +897,7 @@ function check_element(node, state) {
}
const role = attribute_map.get('role');
const role_static_value = /** @type {import('aria-query').ARIARoleDefinitionKey} */ (
get_static_text_value(role)
);
const role_static_value = /** @type {ARIARoleDefinitionKey} */ (get_static_text_value(role));
// click-events-have-key-events
if (handlers.has('click')) {
@ -922,7 +918,7 @@ function check_element(node, state) {
}
}
const role_value = /** @type {import('aria-query').ARIARoleDefinitionKey} */ (
const role_value = /** @type {ARIARoleDefinitionKey} */ (
role ? role_static_value : get_implicit_role(node.name, attribute_map)
);
@ -941,15 +937,11 @@ function check_element(node, state) {
// role-supports-aria-props
if (typeof role_value === 'string' && roles_map.has(role_value)) {
const { props } = /** @type {import('aria-query').ARIARoleDefinition} */ (
roles_map.get(role_value)
);
const { props } = /** @type {ARIARoleDefinition} */ (roles_map.get(role_value));
const invalid_aria_props = aria.keys().filter((attribute) => !(attribute in props));
const is_implicit = role_value && role === undefined;
for (const attr of attributes) {
if (
invalid_aria_props.includes(/** @type {import('aria-query').ARIAProperty} */ (attr.name))
) {
if (invalid_aria_props.includes(/** @type {ARIAProperty} */ (attr.name))) {
if (is_implicit) {
w.a11y_role_supports_aria_props_implicit(attr, attr.name, role_value, node.name);
} else {
@ -1075,7 +1067,7 @@ function check_element(node, state) {
}
if (node.name === 'label') {
/** @param {import('#compiler').TemplateNode} node */
/** @param {TemplateNode} node */
const has_input_child = (node) => {
let has = false;
walk(
@ -1112,7 +1104,7 @@ function check_element(node, state) {
return;
}
let has_caption = false;
const track = /** @type {import('#compiler').RegularElement | undefined} */ (
const track = /** @type {RegularElement | undefined} */ (
node.fragment.nodes.find((i) => i.type === 'RegularElement' && i.name === 'track')
);
if (track) {
@ -1164,7 +1156,7 @@ function check_element(node, state) {
}
/**
* @type {import('zimmerframe').Visitors<import('#compiler').SvelteNode, import('./types.js').AnalysisState>}
* @type {Visitors<SvelteNode, AnalysisState>}
*/
export const a11y_validators = {
RegularElement(node, context) {

@ -1,19 +1,22 @@
/** @import { ComponentAnalysis } from '../../types.js' */
/** @import { Css } from '#compiler' */
/** @import { Visitors } from 'zimmerframe' */
import { walk } from 'zimmerframe';
import * as e from '../../../errors.js';
import { is_keyframes_node } from '../../css.js';
import { merge } from '../../visitors.js';
/**
* @typedef {import('zimmerframe').Visitors<
* import('#compiler').Css.Node,
* @typedef {Visitors<
* Css.Node,
* {
* keyframes: string[];
* rule: import('#compiler').Css.Rule | null;
* rule: Css.Rule | null;
* }
* >} Visitors
* >} CssVisitors
*/
/** @param {import('#compiler').Css.RelativeSelector} relative_selector */
/** @param {Css.RelativeSelector} relative_selector */
function is_global(relative_selector) {
const first = relative_selector.selectors[0];
@ -27,7 +30,7 @@ function is_global(relative_selector) {
);
}
/** @type {Visitors} */
/** @type {CssVisitors} */
const analysis_visitors = {
Atrule(node, context) {
if (is_keyframes_node(node)) {
@ -102,7 +105,7 @@ const analysis_visitors = {
}
};
/** @type {Visitors} */
/** @type {CssVisitors} */
const validation_visitors = {
Rule(node, context) {
if (node.metadata.is_global_block) {
@ -178,7 +181,7 @@ const validation_visitors = {
}
},
NestingSelector(node, context) {
const rule = /** @type {import('#compiler').Css.Rule} */ (context.state.rule);
const rule = /** @type {Css.Rule} */ (context.state.rule);
if (!rule.metadata.parent_rule) {
e.css_nesting_selector_invalid_placement(node);
}
@ -188,8 +191,8 @@ const validation_visitors = {
const css_visitors = merge(analysis_visitors, validation_visitors);
/**
* @param {import('#compiler').Css.StyleSheet} stylesheet
* @param {import('../../types.js').ComponentAnalysis} analysis
* @param {Css.StyleSheet} stylesheet
* @param {ComponentAnalysis} analysis
*/
export function analyze_css(stylesheet, analysis) {
walk(stylesheet, { keyframes: analysis.css.keyframes, rule: null }, css_visitors);

@ -1,11 +1,13 @@
/** @import { Visitors } from 'zimmerframe' */
/** @import * as Compiler from '#compiler' */
import { walk } from 'zimmerframe';
import { get_possible_values } from './utils.js';
import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js';
/**
* @typedef {{
* stylesheet: import('#compiler').Css.StyleSheet;
* element: import('#compiler').RegularElement | import('#compiler').SvelteElement;
* stylesheet: Compiler.Css.StyleSheet;
* element: Compiler.RegularElement | Compiler.SvelteElement;
* }} State
*/
/** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */
@ -18,7 +20,7 @@ const whitelist_attribute_selector = new Map([
['dialog', ['open']]
]);
/** @type {import('#compiler').Css.Combinator} */
/** @type {Compiler.Css.Combinator} */
const descendant_combinator = {
type: 'Combinator',
name: ' ',
@ -26,7 +28,7 @@ const descendant_combinator = {
end: -1
};
/** @type {import('#compiler').Css.RelativeSelector} */
/** @type {Compiler.Css.RelativeSelector} */
const nesting_selector = {
type: 'RelativeSelector',
start: -1,
@ -49,14 +51,14 @@ const nesting_selector = {
/**
*
* @param {import('#compiler').Css.StyleSheet} stylesheet
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element
* @param {Compiler.Css.StyleSheet} stylesheet
* @param {Compiler.RegularElement | Compiler.SvelteElement} element
*/
export function prune(stylesheet, element) {
walk(stylesheet, { stylesheet, element }, visitors);
}
/** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, State>} */
/** @type {Visitors<Compiler.Css.Node, State>} */
const visitors = {
Rule(node, context) {
if (node.metadata.is_global_block) {
@ -87,7 +89,7 @@ const visitors = {
if (
apply_selector(
selectors,
/** @type {import('#compiler').Css.Rule} */ (node.metadata.rule),
/** @type {Compiler.Css.Rule} */ (node.metadata.rule),
context.state.element,
context.state.stylesheet
)
@ -104,7 +106,7 @@ const visitors = {
/**
* Discard trailing `:global(...)` selectors, these are unused for scoping purposes
* @param {import('#compiler').Css.ComplexSelector} node
* @param {Compiler.Css.ComplexSelector} node
*/
function truncate(node) {
const i = node.children.findLastIndex(({ metadata }) => {
@ -115,10 +117,10 @@ function truncate(node) {
}
/**
* @param {import('#compiler').Css.RelativeSelector[]} relative_selectors
* @param {import('#compiler').Css.Rule} rule
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element
* @param {import('#compiler').Css.StyleSheet} stylesheet
* @param {Compiler.Css.RelativeSelector[]} relative_selectors
* @param {Compiler.Css.Rule} rule
* @param {Compiler.RegularElement | Compiler.SvelteElement} element
* @param {Compiler.Css.StyleSheet} stylesheet
* @returns {boolean}
*/
function apply_selector(relative_selectors, rule, element, stylesheet) {
@ -144,7 +146,7 @@ function apply_selector(relative_selectors, rule, element, stylesheet) {
switch (name) {
case ' ':
case '>': {
let parent = /** @type {import('#compiler').TemplateNode | null} */ (element.parent);
let parent = /** @type {Compiler.TemplateNode | null} */ (element.parent);
let parent_matched = false;
let crossed_component_boundary = false;
@ -167,7 +169,7 @@ function apply_selector(relative_selectors, rule, element, stylesheet) {
if (name === '>') return parent_matched;
}
parent = /** @type {import('#compiler').TemplateNode | null} */ (parent.parent);
parent = /** @type {Compiler.TemplateNode | null} */ (parent.parent);
}
return parent_matched || parent_selectors.every((selector) => is_global(selector, rule));
@ -218,8 +220,8 @@ function apply_selector(relative_selectors, rule, element, stylesheet) {
/**
* Mark both the compound selector and the node it selects as encapsulated,
* for transformation in a later step
* @param {import('#compiler').Css.RelativeSelector} relative_selector
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element
* @param {Compiler.Css.RelativeSelector} relative_selector
* @param {Compiler.RegularElement | Compiler.SvelteElement} element
*/
function mark(relative_selector, element) {
relative_selector.metadata.scoped = true;
@ -231,8 +233,8 @@ function mark(relative_selector, element) {
* it's a `:global(...)` or unscopeable selector, or
* is an `:is(...)` or `:where(...)` selector that contains
* a global selector
* @param {import('#compiler').Css.RelativeSelector} selector
* @param {import('#compiler').Css.Rule} rule
* @param {Compiler.Css.RelativeSelector} selector
* @param {Compiler.Css.Rule} rule
*/
function is_global(selector, rule) {
if (selector.metadata.is_global || selector.metadata.is_global_like) {
@ -240,7 +242,7 @@ function is_global(selector, rule) {
}
for (const s of selector.selectors) {
/** @type {import('#compiler').Css.SelectorList | null} */
/** @type {Compiler.Css.SelectorList | null} */
let selector_list = null;
let owner = rule;
@ -251,7 +253,7 @@ function is_global(selector, rule) {
}
if (s.type === 'NestingSelector') {
owner = /** @type {import('#compiler').Css.Rule} */ (rule.metadata.parent_rule);
owner = /** @type {Compiler.Css.Rule} */ (rule.metadata.parent_rule);
selector_list = owner.prelude;
}
@ -274,10 +276,10 @@ const regex_backslash_and_following_character = /\\(.)/g;
/**
* Ensure that `element` satisfies each simple selector in `relative_selector`
*
* @param {import('#compiler').Css.RelativeSelector} relative_selector
* @param {import('#compiler').Css.Rule} rule
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element
* @param {import('#compiler').Css.StyleSheet} stylesheet
* @param {Compiler.Css.RelativeSelector} relative_selector
* @param {Compiler.Css.Rule} rule
* @param {Compiler.RegularElement | Compiler.SvelteElement} element
* @param {Compiler.Css.StyleSheet} stylesheet
* @returns {boolean}
*/
function relative_selector_might_apply_to_node(relative_selector, rule, element, stylesheet) {
@ -293,7 +295,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
}
if (name === 'global' && relative_selector.selectors.length === 1) {
const args = /** @type {import('#compiler').Css.SelectorList} */ (selector.args);
const args = /** @type {Compiler.Css.SelectorList} */ (selector.args);
const complex_selector = args.children[0];
return apply_selector(complex_selector.children, rule, element, stylesheet);
}
@ -373,7 +375,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
case 'NestingSelector': {
let matched = false;
const parent = /** @type {import('#compiler').Css.Rule} */ (rule.metadata.parent_rule);
const parent = /** @type {Compiler.Css.Rule} */ (rule.metadata.parent_rule);
for (const complex_selector of parent.prelude.children) {
if (apply_selector(truncate(complex_selector), parent, element, stylesheet)) {
@ -425,7 +427,7 @@ function test_attribute(operator, expected_value, case_insensitive, value) {
}
/**
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
* @param {Compiler.RegularElement | Compiler.SvelteElement} node
* @param {string} name
* @param {string | null} expected_value
* @param {string | null} operator
@ -535,11 +537,11 @@ function unquote(str) {
}
/**
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
* @returns {import('#compiler').RegularElement | import('#compiler').SvelteElement | null}
* @param {Compiler.RegularElement | Compiler.SvelteElement} node
* @returns {Compiler.RegularElement | Compiler.SvelteElement | null}
*/
function get_element_parent(node) {
/** @type {import('#compiler').SvelteNode | null} */
/** @type {Compiler.SvelteNode | null} */
let parent = node;
while (
// @ts-expect-error TODO figure out a more elegant solution
@ -569,11 +571,11 @@ function get_element_parent(node) {
* <h1>Heading 1</h1>
* <h2>Heading 2</h2>
* ```
* @param {import('#compiler').SvelteNode} node
* @returns {import('#compiler').SvelteNode}
* @param {Compiler.SvelteNode} node
* @returns {Compiler.SvelteNode}
*/
function find_previous_sibling(node) {
/** @type {import('#compiler').SvelteNode} */
/** @type {Compiler.SvelteNode} */
let current_node = node;
while (
@ -602,15 +604,15 @@ function find_previous_sibling(node) {
}
/**
* @param {import('#compiler').SvelteNode} node
* @param {Compiler.SvelteNode} node
* @param {boolean} adjacent_only
* @returns {Map<import('#compiler').RegularElement | import('#compiler').SvelteElement | import('#compiler').SlotElement | import('#compiler').RenderTag, NodeExistsValue>}
* @returns {Map<Compiler.RegularElement | Compiler.SvelteElement | Compiler.SlotElement | Compiler.RenderTag, NodeExistsValue>}
*/
function get_possible_element_siblings(node, adjacent_only) {
/** @type {Map<import('#compiler').RegularElement | import('#compiler').SvelteElement | import('#compiler').SlotElement | import('#compiler').RenderTag, NodeExistsValue>} */
/** @type {Map<Compiler.RegularElement | Compiler.SvelteElement | Compiler.SlotElement | Compiler.RenderTag, NodeExistsValue>} */
const result = new Map();
/** @type {import('#compiler').SvelteNode} */
/** @type {Compiler.SvelteNode} */
let prev = node;
while ((prev = find_previous_sibling(prev))) {
if (prev.type === 'RegularElement') {
@ -642,7 +644,7 @@ function get_possible_element_siblings(node, adjacent_only) {
}
if (!prev || !adjacent_only) {
/** @type {import('#compiler').SvelteNode | null} */
/** @type {Compiler.SvelteNode | null} */
let parent = node;
while (
@ -669,12 +671,12 @@ function get_possible_element_siblings(node, adjacent_only) {
}
/**
* @param {import('#compiler').EachBlock | import('#compiler').IfBlock | import('#compiler').AwaitBlock} relative_selector
* @param {Compiler.EachBlock | Compiler.IfBlock | Compiler.AwaitBlock} relative_selector
* @param {boolean} adjacent_only
* @returns {Map<import('#compiler').RegularElement, NodeExistsValue>}
* @returns {Map<Compiler.RegularElement, NodeExistsValue>}
*/
function get_possible_last_child(relative_selector, adjacent_only) {
/** @typedef {Map<import('#compiler').RegularElement, NodeExistsValue>} NodeMap */
/** @typedef {Map<Compiler.RegularElement, NodeExistsValue>} NodeMap */
/** @type {NodeMap} */
const result = new Map();
@ -776,7 +778,7 @@ function higher_existence(exist1, exist2) {
return exist1 > exist2 ? exist1 : exist2;
}
/** @param {Map<import('#compiler').RegularElement, NodeExistsValue>} result */
/** @param {Map<Compiler.RegularElement, NodeExistsValue>} result */
function mark_as_probably(result) {
for (const key of result.keys()) {
result.set(key, NODE_PROBABLY_EXISTS);
@ -784,11 +786,11 @@ function mark_as_probably(result) {
}
/**
* @param {import('#compiler').SvelteNode[]} children
* @param {Compiler.SvelteNode[]} children
* @param {boolean} adjacent_only
*/
function loop_child(children, adjacent_only) {
/** @type {Map<import('#compiler').RegularElement, NodeExistsValue>} */
/** @type {Map<Compiler.RegularElement, NodeExistsValue>} */
const result = new Map();
for (let i = children.length - 1; i >= 0; i--) {
const child = children[i];

@ -1,15 +1,17 @@
/** @import { Visitors } from 'zimmerframe' */
/** @import { Css } from '#compiler' */
import { walk } from 'zimmerframe';
import * as w from '../../../warnings.js';
import { is_keyframes_node } from '../../css.js';
/**
* @param {import('#compiler').Css.StyleSheet} stylesheet
* @param {Css.StyleSheet} stylesheet
*/
export function warn_unused(stylesheet) {
walk(stylesheet, { stylesheet }, visitors);
}
/** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, { stylesheet: import('#compiler').Css.StyleSheet }>} */
/** @type {Visitors<Css.Node, { stylesheet: Css.StyleSheet }>} */
const visitors = {
Atrule(node, context) {
if (!is_keyframes_node(node)) {

@ -1,7 +1,9 @@
/** @import { Text, ExpressionTag } from '#compiler' */
/** @import { Node } from 'estree' */
const UNKNOWN = {};
/**
* @param {import('estree').Node} node
* @param {Node} node
* @param {Set<any>} set
*/
function gather_possible_values(node, set) {
@ -16,7 +18,7 @@ function gather_possible_values(node, set) {
}
/**
* @param {import('#compiler').Text | import('#compiler').ExpressionTag} chunk
* @param {Text | ExpressionTag} chunk
* @returns {Set<string> | null}
*/
export function get_possible_values(chunk) {

@ -31,6 +31,7 @@ import { hash } from './utils.js';
import { warn_unused } from './css/css-warn.js';
import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js';
import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.js';
import { equal } from '../../utils/assert.js';
/**
* @param {import('#compiler').Script | null} script
@ -165,25 +166,20 @@ function get_delegated_event(event_name, handler, context) {
}
// If we can't find a function, bail-out
if (target_function == null) {
return non_hoistable;
}
if (target_function == null) return non_hoistable;
// If the function is marked as non-hoistable, bail-out
if (target_function.metadata.hoistable === 'impossible') {
return non_hoistable;
}
if (target_function.metadata.hoistable === 'impossible') return non_hoistable;
// If the function has more than one arg, then bail-out
if (target_function.params.length > 1) {
return non_hoistable;
}
if (target_function.params.length > 1) return non_hoistable;
const visited_references = new Set();
const scope = target_function.metadata.scope;
for (const [reference] of scope.references) {
// Bail-out if the arguments keyword is used
if (reference === 'arguments') {
return non_hoistable;
}
if (reference === 'arguments') return non_hoistable;
// Bail-out if references a store subscription
if (scope.get(`$${reference}`)?.kind === 'store_sub') return non_hoistable;
const binding = scope.get(reference);
const local_binding = context.state.scope.get(reference);
@ -202,9 +198,7 @@ function get_delegated_event(event_name, handler, context) {
}
// If we reference the index within an each block, then bail-out.
if (binding !== null && binding.initial?.type === 'EachBlock') {
return non_hoistable;
}
if (binding !== null && binding.initial?.type === 'EachBlock') return non_hoistable;
if (
binding !== null &&
@ -969,34 +963,42 @@ const runes_scope_tweaker = {
if (rune === '$props') {
state.analysis.needs_props = true;
for (const property of /** @type {import('estree').ObjectPattern} */ (node.id).properties) {
if (property.type !== 'Property') continue;
const name =
property.value.type === 'AssignmentPattern'
? /** @type {import('estree').Identifier} */ (property.value.left).name
: /** @type {import('estree').Identifier} */ (property.value).name;
const alias =
property.key.type === 'Identifier'
? property.key.name
: String(/** @type {import('estree').Literal} */ (property.key).value);
let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null;
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name));
binding.prop_alias = alias;
// rewire initial from $props() to the actual initial value, stripping $bindable() if necessary
if (
initial?.type === 'CallExpression' &&
initial.callee.type === 'Identifier' &&
initial.callee.name === '$bindable'
) {
binding.initial = /** @type {import('estree').Expression | null} */ (
initial.arguments[0] ?? null
);
binding.kind = 'bindable_prop';
} else {
binding.initial = initial;
if (node.id.type === 'Identifier') {
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(node.id.name));
binding.initial = null; // else would be $props()
binding.kind = 'rest_prop';
} else {
equal(node.id.type, 'ObjectPattern');
for (const property of node.id.properties) {
if (property.type !== 'Property') continue;
const name =
property.value.type === 'AssignmentPattern'
? /** @type {import('estree').Identifier} */ (property.value.left).name
: /** @type {import('estree').Identifier} */ (property.value).name;
const alias =
property.key.type === 'Identifier'
? property.key.name
: String(/** @type {import('estree').Literal} */ (property.key).value);
let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null;
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name));
binding.prop_alias = alias;
// rewire initial from $props() to the actual initial value, stripping $bindable() if necessary
if (
initial?.type === 'CallExpression' &&
initial.callee.type === 'Identifier' &&
initial.callee.name === '$bindable'
) {
binding.initial = /** @type {import('estree').Expression | null} */ (
initial.arguments[0] ?? null
);
binding.kind = 'bindable_prop';
} else {
binding.initial = initial;
}
}
}
}
@ -1008,6 +1010,9 @@ const runes_scope_tweaker = {
name: node.local.name,
alias: node.exported.name
});
const binding = state.scope.get(node.local.name);
if (binding) binding.reassigned = true;
},
ExportNamedDeclaration(node, { next, state }) {
if (!node.declaration || state.ast_type !== 'instance') {
@ -1511,6 +1516,13 @@ const common_visitors = {
return;
}
}
},
Component(node, context) {
const binding = context.state.scope.get(
node.name.includes('.') ? node.name.slice(0, node.name.indexOf('.')) : node.name
);
node.metadata.dynamic = binding !== null && binding.kind !== 'normal';
}
};

@ -341,6 +341,14 @@ function validate_block_not_empty(node, context) {
* @type {import('zimmerframe').Visitors<import('#compiler').SvelteNode, import('./types.js').AnalysisState>}
*/
const validation = {
MemberExpression(node, context) {
if (node.object.type === 'Identifier' && node.property.type === 'Identifier') {
const binding = context.state.scope.get(node.object.name);
if (binding?.kind === 'rest_prop' && node.property.name.startsWith('$$')) {
e.props_illegal_name(node.property);
}
}
},
AssignmentExpression(node, context) {
validate_assignment(node, node.left, context.state);
},
@ -625,6 +633,11 @@ const validation = {
});
},
RenderTag(node, context) {
const callee = unwrap_optional(node.expression).callee;
node.metadata.dynamic =
callee.type !== 'Identifier' || context.state.scope.get(callee.name)?.kind !== 'normal';
context.state.analysis.uses_render_tags = true;
const raw_args = unwrap_optional(node.expression).arguments;
@ -634,7 +647,6 @@ const validation = {
}
}
const callee = unwrap_optional(node.expression).callee;
if (
callee.type === 'MemberExpression' &&
callee.property.type === 'Identifier' &&
@ -1078,6 +1090,20 @@ function validate_no_const_assignment(node, argument, scope, is_binding) {
}
}
/**
* Validates that the opening of a control flow block is `{` immediately followed by the expected character.
* In legacy mode whitespace is allowed inbetween. TODO remove once legacy mode is gone and move this into parser instead.
* @param {{start: number; end: number}} node
* @param {import('./types.js').AnalysisState} state
* @param {string} expected
*/
function validate_opening_tag(node, state, expected) {
if (state.analysis.source[node.start + 1] !== expected) {
// avoid a sea of red and only mark the first few characters
e.block_unexpected_character({ start: node.start, end: node.start + 5 }, expected);
}
}
/**
* @param {import('estree').AssignmentExpression | import('estree').UpdateExpression} node
* @param {import('estree').Pattern | import('estree').Expression} argument
@ -1205,6 +1231,8 @@ export const validation_runes = merge(validation, a11y_validators, {
validate_call_expression(node, state.scope, path);
},
EachBlock(node, { next, state }) {
validate_opening_tag(node, state, '#');
const context = node.context;
if (
context.type === 'Identifier' &&
@ -1214,6 +1242,51 @@ export const validation_runes = merge(validation, a11y_validators, {
}
next({ ...state });
},
IfBlock(node, { state, path }) {
const parent = path.at(-1);
const expected =
path.at(-2)?.type === 'IfBlock' && parent?.type === 'Fragment' && parent.nodes.length === 1
? ':'
: '#';
validate_opening_tag(node, state, expected);
},
AwaitBlock(node, { state }) {
validate_opening_tag(node, state, '#');
if (node.value) {
const start = /** @type {number} */ (node.value.start);
const match = state.analysis.source.substring(start - 10, start).match(/{(\s*):then\s+$/);
if (match && match[1] !== '') {
e.block_unexpected_character({ start: start - 10, end: start }, ':');
}
}
if (node.error) {
const start = /** @type {number} */ (node.error.start);
const match = state.analysis.source.substring(start - 10, start).match(/{(\s*):catch\s+$/);
if (match && match[1] !== '') {
e.block_unexpected_character({ start: start - 10, end: start }, ':');
}
}
},
KeyBlock(node, { state }) {
validate_opening_tag(node, state, '#');
},
SnippetBlock(node, { state }) {
validate_opening_tag(node, state, '#');
},
ConstTag(node, { state }) {
validate_opening_tag(node, state, '@');
},
HtmlTag(node, { state }) {
validate_opening_tag(node, state, '@');
},
DebugTag(node, { state }) {
validate_opening_tag(node, state, '@');
},
RenderTag(node, { state }) {
validate_opening_tag(node, state, '@');
},
VariableDeclarator(node, { state }) {
ensure_no_module_import_conflict(node, state);
@ -1240,7 +1313,7 @@ export const validation_runes = merge(validation, a11y_validators, {
e.rune_invalid_arguments(node, rune);
}
if (node.id.type !== 'ObjectPattern') {
if (node.id.type !== 'ObjectPattern' && node.id.type !== 'Identifier') {
e.props_invalid_identifier(node);
}
@ -1248,17 +1321,23 @@ export const validation_runes = merge(validation, a11y_validators, {
e.props_invalid_placement(node);
}
for (const property of node.id.properties) {
if (property.type === 'Property') {
if (property.computed) {
e.props_invalid_pattern(property);
}
if (node.id.type === 'ObjectPattern') {
for (const property of node.id.properties) {
if (property.type === 'Property') {
if (property.computed) {
e.props_invalid_pattern(property);
}
if (property.key.type === 'Identifier' && property.key.name.startsWith('$$')) {
e.props_illegal_name(property);
}
const value =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
const value =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
if (value.type !== 'Identifier') {
e.props_invalid_pattern(property);
if (value.type !== 'Identifier') {
e.props_invalid_pattern(property);
}
}
}
}

@ -1,3 +1,7 @@
/** @import * as ESTree from 'estree' */
/** @import { ValidatedCompileOptions, SvelteNode, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { ComponentAnalysis, Analysis } from '../../types' */
/** @import { Visitors, ComponentClientTransformState, ClientTransformState } from './types' */
import { walk } from 'zimmerframe';
import * as b from '../../../utils/builders.js';
import { set_scope } from '../../scope.js';
@ -12,8 +16,8 @@ import { filename } from '../../../state.js';
/**
* This function ensures visitor sets don't accidentally clobber each other
* @param {...import('./types').Visitors} array
* @returns {import('./types').Visitors}
* @param {...Visitors} array
* @returns {Visitors}
*/
function combine_visitors(...array) {
/** @type {Record<string, any>} */
@ -35,12 +39,12 @@ function combine_visitors(...array) {
/**
* @param {string} source
* @param {import('../../types').ComponentAnalysis} analysis
* @param {import('#compiler').ValidatedCompileOptions} options
* @returns {import('estree').Program}
* @param {ComponentAnalysis} analysis
* @param {ValidatedCompileOptions} options
* @returns {ESTree.Program}
*/
export function client_component(source, analysis, options) {
/** @type {import('./types').ComponentClientTransformState} */
/** @type {ComponentClientTransformState} */
const state = {
analysis,
options,
@ -72,9 +76,9 @@ export function client_component(source, analysis, options) {
locations: /** @type {any} */ (null)
};
const module = /** @type {import('estree').Program} */ (
const module = /** @type {ESTree.Program} */ (
walk(
/** @type {import('#compiler').SvelteNode} */ (analysis.module.ast),
/** @type {SvelteNode} */ (analysis.module.ast),
state,
combine_visitors(
set_scope(analysis.module.scopes),
@ -87,9 +91,9 @@ export function client_component(source, analysis, options) {
);
const instance_state = { ...state, scope: analysis.instance.scope };
const instance = /** @type {import('estree').Program} */ (
const instance = /** @type {ESTree.Program} */ (
walk(
/** @type {import('#compiler').SvelteNode} */ (analysis.instance.ast),
/** @type {SvelteNode} */ (analysis.instance.ast),
instance_state,
combine_visitors(
set_scope(analysis.instance.scopes),
@ -114,9 +118,9 @@ export function client_component(source, analysis, options) {
)
);
const template = /** @type {import('estree').Program} */ (
const template = /** @type {ESTree.Program} */ (
walk(
/** @type {import('#compiler').SvelteNode} */ (analysis.template.ast),
/** @type {SvelteNode} */ (analysis.template.ast),
{ ...state, scope: analysis.instance.scope },
combine_visitors(
set_scope(analysis.template.scopes),
@ -138,10 +142,10 @@ export function client_component(source, analysis, options) {
}
}
/** @type {import('estree').Statement[]} */
/** @type {ESTree.Statement[]} */
const store_setup = [];
/** @type {import('estree').VariableDeclaration[]} */
/** @type {ESTree.VariableDeclaration[]} */
const legacy_reactive_declarations = [];
for (const [name, binding] of analysis.instance.scope.declarations) {
@ -150,19 +154,12 @@ export function client_component(source, analysis, options) {
}
if (binding.kind === 'store_sub') {
if (store_setup.length === 0) {
store_setup.push(
b.const('$$subscriptions', b.object([])),
b.stmt(b.call('$.unsubscribe_on_destroy', b.id('$$subscriptions')))
);
store_setup.push(b.const('$$stores', b.call('$.setup_stores')));
}
// We're creating an arrow function that gets the store value which minifies better for two or more references
const store_reference = serialize_get_binding(b.id(name.slice(1)), instance_state);
const store_get = b.call(
'$.store_get',
store_reference,
b.literal(name),
b.id('$$subscriptions')
);
const store_get = b.call('$.store_get', store_reference, b.literal(name), b.id('$$stores'));
store_setup.push(
b.const(
binding.node,
@ -193,22 +190,46 @@ export function client_component(source, analysis, options) {
/**
* Used to store the group nodes
* @type {import('estree').VariableDeclaration[]}
* @type {ESTree.VariableDeclaration[]}
*/
const group_binding_declarations = [];
for (const group of analysis.binding_groups.values()) {
group_binding_declarations.push(b.const(group.name, b.array([])));
}
/** @type {Array<import('estree').Property | import('estree').SpreadElement>} */
const component_returned_object = analysis.exports.map(({ name, alias }) => {
/** @type {Array<ESTree.Property | ESTree.SpreadElement>} */
const component_returned_object = analysis.exports.flatMap(({ name, alias }) => {
const binding = instance_state.scope.get(name);
const expression = serialize_get_binding(b.id(name), instance_state);
const getter = b.get(alias ?? name, [b.return(expression)]);
if (expression.type === 'Identifier') {
if (binding?.declaration_kind === 'let' || binding?.declaration_kind === 'var') {
return [
getter,
b.set(alias ?? name, [b.stmt(b.assignment('=', expression, b.id('$$value')))])
];
} else if (!options.dev) {
return b.init(alias ?? name, expression);
}
}
if (expression.type === 'Identifier' && !options.dev) {
return b.init(alias ?? name, expression);
if (binding?.kind === 'state' || binding?.kind === 'frozen_state') {
return [
getter,
b.set(alias ?? name, [
b.stmt(
b.call(
'$.set',
b.id(name),
b.call(binding.kind === 'state' ? '$.proxy' : '$.freeze', b.id('$$value'))
)
)
])
];
}
return b.get(alias ?? name, [b.return(expression)]);
return getter;
});
const properties = [...analysis.instance.scope.declarations].filter(
@ -218,7 +239,7 @@ export function client_component(source, analysis, options) {
if (analysis.runes && options.dev) {
const exports = analysis.exports.map(({ name, alias }) => b.literal(alias ?? name));
/** @type {import('estree').Literal[]} */
/** @type {ESTree.Literal[]} */
const bindable = [];
for (const [name, binding] of properties) {
if (binding.kind === 'bindable_prop') {
@ -254,7 +275,7 @@ export function client_component(source, analysis, options) {
setter.value.params[0] = {
type: 'AssignmentPattern',
left: b.id('$$value'),
right: /** @type {import('estree').Expression} */ (binding.initial)
right: /** @type {ESTree.Expression} */ (binding.initial)
};
}
@ -290,9 +311,9 @@ export function client_component(source, analysis, options) {
...legacy_reactive_declarations,
...group_binding_declarations,
...analysis.top_level_snippets,
.../** @type {import('estree').Statement[]} */ (instance.body),
.../** @type {ESTree.Statement[]} */ (instance.body),
analysis.runes || !analysis.needs_context ? b.empty : b.stmt(b.call('$.init')),
.../** @type {import('estree').Statement[]} */ (template.body)
.../** @type {ESTree.Statement[]} */ (template.body)
]);
if (!analysis.runes) {
@ -480,7 +501,7 @@ export function client_component(source, analysis, options) {
if (analysis.custom_element) {
const ce = analysis.custom_element;
/** @type {import('estree').Property[]} */
/** @type {ESTree.Property[]} */
const props_str = [];
for (const [name, binding] of properties) {
@ -495,7 +516,7 @@ export function client_component(source, analysis, options) {
}
const value = b.object(
/** @type {import('estree').Property[]} */ (
/** @type {ESTree.Property[]} */ (
[
prop_def.attribute ? b.init('attribute', b.literal(prop_def.attribute)) : undefined,
prop_def.reflect ? b.init('reflect', b.literal(true)) : undefined,
@ -539,12 +560,12 @@ export function client_component(source, analysis, options) {
}
/**
* @param {import('../../types').Analysis} analysis
* @param {import('#compiler').ValidatedModuleCompileOptions} options
* @returns {import('estree').Program}
* @param {Analysis} analysis
* @param {ValidatedModuleCompileOptions} options
* @returns {ESTree.Program}
*/
export function client_module(analysis, options) {
/** @type {import('./types').ClientTransformState} */
/** @type {ClientTransformState} */
const state = {
analysis,
options,
@ -556,9 +577,9 @@ export function client_module(analysis, options) {
in_constructor: false
};
const module = /** @type {import('estree').Program} */ (
const module = /** @type {ESTree.Program} */ (
walk(
/** @type {import('#compiler').SvelteNode} */ (analysis.module.ast),
/** @type {SvelteNode} */ (analysis.module.ast),
state,
combine_visitors(
set_scope(analysis.module.scopes),

@ -8,7 +8,7 @@ import type {
import type { Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js';
import type { Location } from 'locate-character';
import type { SourceLocation } from '#shared';
export interface ClientTransformState extends TransformState {
readonly private_state: Map<string, StateField>;
@ -24,10 +24,6 @@ export interface ClientTransformState extends TransformState {
readonly legacy_reactive_statements: Map<LabeledStatement, Statement>;
}
export type SourceLocation =
| [line: number, column: number]
| [line: number, column: number, SourceLocation[]];
export interface ComponentClientTransformState extends ClientTransformState {
readonly analysis: ComponentAnalysis;
readonly options: ValidatedCompileOptions;

@ -345,7 +345,7 @@ export function serialize_set_binding(node, context, fallback, prefix, options)
}
if (state.scope.get(`$${left.name}`)?.kind === 'store_sub') {
return b.call('$.store_unsub', call, b.literal(`$${left.name}`), b.id('$$subscriptions'));
return b.call('$.store_unsub', call, b.literal(`$${left.name}`), b.id('$$stores'));
} else {
return call;
}
@ -454,7 +454,6 @@ export function serialize_proxy_reassignment(value, proxy_reference, state) {
? b.call(
'$.proxy',
value,
b.true,
b.null,
typeof proxy_reference === 'string'
? b.id(proxy_reference)
@ -547,6 +546,11 @@ function get_hoistable_params(node, context) {
} else {
// create a copy to remove start/end tags which would mess up source maps
push_unique(b.id(binding.node.name));
// rest props are often accessed through the $$props object for optimization reasons,
// but we can't know if the delegated event handler will use it, so we need to add both as params
if (binding.kind === 'rest_prop' && context.state.analysis.runes) {
push_unique(b.id('$$props'));
}
}
}
}

@ -9,6 +9,27 @@ export const global_visitors = {
if (node.name === '$$props') {
return b.id('$$sanitized_props');
}
// Optimize prop access: If it's a member read access, we can use the $$props object directly
const binding = state.scope.get(node.name);
if (
state.analysis.runes && // can't do this in legacy mode because the proxy does more than just read/write
binding !== null &&
node !== binding.node &&
binding.kind === 'rest_prop'
) {
const parent = path.at(-1);
const grand_parent = path.at(-2);
if (
parent?.type === 'MemberExpression' &&
!parent.computed &&
grand_parent?.type !== 'AssignmentExpression' &&
grand_parent?.type !== 'UpdateExpression'
) {
return b.id('$$props');
}
}
return serialize_get_binding(node, state);
}
},

@ -1,3 +1,7 @@
/** @import { Binding, ReactiveStatement } from '#compiler' */
/** @import { ComponentVisitors } from '../types.js' */
/** @import { Scope } from '../../../scope.js' */
/** @import { VariableDeclarator, Expression, Identifier, Statement } from 'estree' */
import { is_hoistable_function } from '../../utils.js';
import * as b from '../../../../utils/builders.js';
import { extract_paths } from '../../../../utils/ast.js';
@ -5,9 +9,9 @@ import { get_prop_source, serialize_get_binding } from '../utils.js';
/**
* Creates the output for a state declaration.
* @param {import('estree').VariableDeclarator} declarator
* @param {import('../../../scope.js').Scope} scope
* @param {import('estree').Expression} value
* @param {VariableDeclarator} declarator
* @param {Scope} scope
* @param {Expression} value
*/
function create_state_declarators(declarator, scope, value) {
if (declarator.id.type === 'Identifier') {
@ -20,7 +24,7 @@ function create_state_declarators(declarator, scope, value) {
b.declarator(b.id(tmp), value),
...paths.map((path) => {
const value = path.expression?.(b.id(tmp));
const binding = scope.get(/** @type {import('estree').Identifier} */ (path.node).name);
const binding = scope.get(/** @type {Identifier} */ (path.node).name);
return b.declarator(
path.node,
binding?.kind === 'state' ? b.call('$.mutable_source', value) : value
@ -29,16 +33,14 @@ function create_state_declarators(declarator, scope, value) {
];
}
/** @type {import('../types.js').ComponentVisitors} */
/** @type {ComponentVisitors} */
export const javascript_visitors_legacy = {
VariableDeclaration(node, { state, visit }) {
/** @type {import('estree').VariableDeclarator[]} */
/** @type {VariableDeclarator[]} */
const declarations = [];
for (const declarator of node.declarations) {
const bindings = /** @type {import('#compiler').Binding[]} */ (
state.scope.get_bindings(declarator)
);
const bindings = /** @type {Binding[]} */ (state.scope.get_bindings(declarator));
const has_state = bindings.some((binding) => binding.kind === 'state');
const has_props = bindings.some((binding) => binding.kind === 'bindable_prop');
@ -47,15 +49,11 @@ export const javascript_visitors_legacy = {
if (init != null && is_hoistable_function(init)) {
const hoistable_function = visit(init);
state.hoisted.push(
b.declaration(
'const',
declarator.id,
/** @type {import('estree').Expression} */ (hoistable_function)
)
b.declaration('const', declarator.id, /** @type {Expression} */ (hoistable_function))
);
continue;
}
declarations.push(/** @type {import('estree').VariableDeclarator} */ (visit(declarator)));
declarations.push(/** @type {VariableDeclarator} */ (visit(declarator)));
continue;
}
@ -68,14 +66,12 @@ export const javascript_visitors_legacy = {
declarations.push(
b.declarator(
b.id(tmp),
/** @type {import('estree').Expression} */ (
visit(/** @type {import('estree').Expression} */ (declarator.init))
)
/** @type {Expression} */ (visit(/** @type {Expression} */ (declarator.init)))
)
);
for (const path of paths) {
const name = /** @type {import('estree').Identifier} */ (path.node).name;
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name));
const name = /** @type {Identifier} */ (path.node).name;
const binding = /** @type {Binding} */ (state.scope.get(name));
const value = path.expression?.(b.id(tmp));
declarations.push(
b.declarator(
@ -89,9 +85,7 @@ export const javascript_visitors_legacy = {
continue;
}
const binding = /** @type {import('#compiler').Binding} */ (
state.scope.get(declarator.id.name)
);
const binding = /** @type {Binding} */ (state.scope.get(declarator.id.name));
declarations.push(
b.declarator(
@ -100,7 +94,7 @@ export const javascript_visitors_legacy = {
binding,
state,
binding.prop_alias ?? declarator.id.name,
declarator.init && /** @type {import('estree').Expression} */ (visit(declarator.init))
declarator.init && /** @type {Expression} */ (visit(declarator.init))
)
)
);
@ -112,7 +106,7 @@ export const javascript_visitors_legacy = {
...create_state_declarators(
declarator,
state.scope,
/** @type {import('estree').Expression} */ (declarator.init && visit(declarator.init))
/** @type {Expression} */ (declarator.init && visit(declarator.init))
)
);
}
@ -135,7 +129,7 @@ export const javascript_visitors_legacy = {
const state = context.state;
// To recreate Svelte 4 behaviour, we track the dependencies
// the compiler can 'see', but we untrack the effect itself
const reactive_stmt = /** @type {import('#compiler').ReactiveStatement} */ (
const reactive_stmt = /** @type {ReactiveStatement} */ (
state.analysis.reactive_statements.get(node)
);
@ -143,7 +137,7 @@ export const javascript_visitors_legacy = {
const { dependencies } = reactive_stmt;
let serialized_body = /** @type {import('estree').Statement} */ (context.visit(node.body));
let serialized_body = /** @type {Statement} */ (context.visit(node.body));
if (serialized_body.type !== 'BlockStatement') {
serialized_body = b.block([serialized_body]);
@ -151,7 +145,7 @@ export const javascript_visitors_legacy = {
const body = serialized_body.body;
/** @type {import('estree').Expression[]} */
/** @type {Expression[]} */
const sequence = [];
for (const binding of dependencies) {
if (binding.kind === 'normal') continue;

@ -238,8 +238,6 @@ export const javascript_visitors_runes = {
}
if (rune === '$props') {
assert.equal(declarator.id.type, 'ObjectPattern');
/** @type {string[]} */
const seen = ['$$slots', '$$events', '$$legacy'];
@ -247,44 +245,58 @@ export const javascript_visitors_runes = {
seen.push('$$host');
}
for (const property of declarator.id.properties) {
if (property.type === 'Property') {
const key = /** @type {import('estree').Identifier | import('estree').Literal} */ (
property.key
);
const name = key.type === 'Identifier' ? key.name : /** @type {string} */ (key.value);
seen.push(name);
let id =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
assert.equal(id.type, 'Identifier');
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name));
let initial =
binding.initial &&
/** @type {import('estree').Expression} */ (visit(binding.initial));
// We're adding proxy here on demand and not within the prop runtime function so that
// people not using proxied state anywhere in their code don't have to pay the additional bundle size cost
if (initial && binding.mutated && should_proxy_or_freeze(initial, state.scope)) {
initial = b.call('$.proxy', initial);
}
if (declarator.id.type === 'Identifier') {
/** @type {import('estree').Expression[]} */
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
if (is_prop_source(binding, state)) {
declarations.push(b.declarator(id, get_prop_source(binding, state, name, initial)));
}
} else {
// RestElement
/** @type {import('estree').Expression[]} */
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
if (state.options.dev) {
// include rest name, so we can provide informative error messages
args.push(
b.literal(/** @type {import('estree').Identifier} */ (property.argument).name)
if (state.options.dev) {
// include rest name, so we can provide informative error messages
args.push(b.literal(declarator.id.name));
}
declarations.push(b.declarator(declarator.id, b.call('$.rest_props', ...args)));
} else {
assert.equal(declarator.id.type, 'ObjectPattern');
for (const property of declarator.id.properties) {
if (property.type === 'Property') {
const key = /** @type {import('estree').Identifier | import('estree').Literal} */ (
property.key
);
const name = key.type === 'Identifier' ? key.name : /** @type {string} */ (key.value);
seen.push(name);
let id =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
assert.equal(id.type, 'Identifier');
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name));
let initial =
binding.initial &&
/** @type {import('estree').Expression} */ (visit(binding.initial));
// We're adding proxy here on demand and not within the prop runtime function so that
// people not using proxied state anywhere in their code don't have to pay the additional bundle size cost
if (initial && binding.mutated && should_proxy_or_freeze(initial, state.scope)) {
initial = b.call('$.proxy', initial);
}
if (is_prop_source(binding, state)) {
declarations.push(b.declarator(id, get_prop_source(binding, state, name, initial)));
}
} else {
// RestElement
/** @type {import('estree').Expression[]} */
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
if (state.options.dev) {
// include rest name, so we can provide informative error messages
args.push(
b.literal(/** @type {import('estree').Identifier} */ (property.argument).name)
);
}
declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args)));
}
declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args)));
}
}

@ -1,7 +1,9 @@
/** @import { FunctionDeclaration } from 'estree' */
/** @import { ComponentVisitors } from '../types.js' */
import * as b from '../../../../utils/builders.js';
import { function_visitor, serialize_hoistable_params } from '../utils.js';
/** @type {import('../types.js').ComponentVisitors} */
/** @type {ComponentVisitors} */
export const javascript_visitors = {
FunctionExpression: function_visitor,
ArrowFunctionExpression: function_visitor,
@ -14,7 +16,7 @@ export const javascript_visitors = {
const params = serialize_hoistable_params(node, context);
context.state.hoisted.push(
/** @type {import('estree').FunctionDeclaration} */ ({
/** @type {FunctionDeclaration} */ ({
...node,
id: node.id !== null ? context.visit(node.id, state) : null,
params,

@ -617,7 +617,16 @@ function serialize_element_special_value_attribute(element, node_id, attribute,
if (is_reactive) {
const id = state.scope.generate(`${node_id.name}_value`);
serialize_update_assignment(state, id, undefined, value, update);
serialize_update_assignment(
state,
id,
// `<option>` is a special case: The value property reflects to the DOM. If the value is set to undefined,
// that means the value should be set to the empty string. To be able to do that when the value is
// initially undefined, we need to set a value that is guaranteed to be different.
element === 'option' ? b.object([]) : undefined,
value,
update
);
return true;
} else {
state.init.push(update);
@ -652,9 +661,10 @@ function collect_parent_each_blocks(context) {
* @param {import('#compiler').Component | import('#compiler').SvelteComponent | import('#compiler').SvelteSelf} node
* @param {string} component_name
* @param {import('../types.js').ComponentContext} context
* @param {import('estree').Expression} anchor
* @returns {import('estree').Statement}
*/
function serialize_inline_component(node, component_name, context) {
function serialize_inline_component(node, component_name, context, anchor = context.state.node) {
/** @type {Array<import('estree').Property[] | import('estree').Expression>} */
const props_and_spreads = [];
@ -882,7 +892,7 @@ function serialize_inline_component(node, component_name, context) {
b.init(
'children',
context.state.options.dev
? b.call('$.wrap_snippet', slot_fn, b.id(context.state.analysis.name))
? b.call('$.wrap_snippet', b.id(context.state.analysis.name), slot_fn)
: slot_fn
)
);
@ -942,15 +952,16 @@ function serialize_inline_component(node, component_name, context) {
fn = (node_id) => {
return b.call(
'$.component',
node_id,
b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.expression))),
b.arrow(
[b.id(component_name)],
[b.id('$$anchor'), b.id(component_name)],
b.block([
...binding_initializers,
b.stmt(
context.state.options.dev
? b.call('$.validate_dynamic_component', b.thunk(prev(node_id)))
: prev(node_id)
? b.call('$.validate_dynamic_component', b.thunk(prev(b.id('$$anchor'))))
: prev(b.id('$$anchor'))
)
])
)
@ -968,12 +979,13 @@ function serialize_inline_component(node, component_name, context) {
);
statements.push(
b.stmt(b.call('$.css_props', context.state.node, b.thunk(b.object(custom_css_props)))),
b.stmt(fn(b.member(context.state.node, b.id('lastChild'))))
b.stmt(b.call('$.css_props', anchor, b.thunk(b.object(custom_css_props)))),
b.stmt(fn(b.member(anchor, b.id('lastChild')))),
b.stmt(b.call('$.reset', anchor))
);
} else {
context.state.template.push('<!>');
statements.push(b.stmt(fn(context.state.node)));
statements.push(b.stmt(fn(anchor)));
}
return statements.length > 1 ? b.block(statements) : statements[0];
@ -1049,7 +1061,7 @@ function serialize_bind_this(bind_this, context, node) {
}
/**
* @param {import('../types.js').SourceLocation[]} locations
* @param {import('#shared').SourceLocation[]} locations
*/
function serialize_locations(locations) {
return b.array(
@ -1096,13 +1108,12 @@ function serialize_update(statement) {
}
/**
*
* @param {import('../types.js').ComponentClientTransformState} state
* @param {import('estree').Statement[]} update
*/
function serialize_render_stmt(state) {
return state.update.length === 1
? serialize_update(state.update[0])
: b.stmt(b.call('$.template_effect', b.thunk(b.block(state.update))));
function serialize_render_stmt(update) {
return update.length === 1
? serialize_update(update[0])
: b.stmt(b.call('$.template_effect', b.thunk(b.block(update))));
}
/**
@ -1431,6 +1442,12 @@ function process_children(nodes, expression, is_element, { visit, state }) {
}
if (sequence.length > 0) {
// if the final item in a fragment is static text,
// we need to force `hydrate_node` to advance
if (sequence.length === 1 && sequence[0].type === 'Text' && nodes.length > 1) {
state.init.push(b.stmt(b.call('$.next')));
}
flush_sequence(sequence);
}
}
@ -1559,7 +1576,7 @@ export const template_visitors = {
const namespace = infer_namespace(context.state.metadata.namespace, parent, node.nodes);
const { hoisted, trimmed } = clean_nodes(
const { hoisted, trimmed, is_standalone, is_text_first } = clean_nodes(
parent,
node.nodes,
context.path,
@ -1609,6 +1626,11 @@ export const template_visitors = {
context.visit(node, state);
}
if (is_text_first) {
// skip over inserted comment
body.push(b.stmt(b.call('$.next')));
}
/**
* @param {import('estree').Identifier} template_name
* @param {import('estree').Expression[]} args
@ -1667,49 +1689,47 @@ export const template_visitors = {
state
});
body.push(
b.var(id, b.call('$.text', b.id('$$anchor'))),
...state.before_init,
...state.init
);
body.push(b.var(id, b.call('$.text')), ...state.before_init, ...state.init);
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else {
/** @type {(is_text: boolean) => import('estree').Expression} */
const expression = (is_text) =>
is_text ? b.call('$.first_child', id, b.true) : b.call('$.first_child', id);
process_children(trimmed, expression, false, { ...context, state });
if (is_standalone) {
// no need to create a template, we can just use the existing block's anchor
process_children(trimmed, () => b.id('$$anchor'), false, { ...context, state });
} else {
/** @type {(is_text: boolean) => import('estree').Expression} */
const expression = (is_text) => b.call('$.first_child', id, is_text && b.true);
const use_comment_template = state.template.length === 1 && state.template[0] === '<!>';
process_children(trimmed, expression, false, { ...context, state });
if (use_comment_template) {
// special case — we can use `$.comment` instead of creating a unique template
body.push(b.var(id, b.call('$.comment')));
} else {
let flags = TEMPLATE_FRAGMENT;
if (state.metadata.context.template_needs_import_node) {
flags |= TEMPLATE_USE_IMPORT_NODE;
}
add_template(template_name, [
b.template([b.quasi(state.template.join(''), true)], []),
b.literal(flags)
]);
if (state.template.length === 1 && state.template[0] === '<!>') {
// special case — we can use `$.comment` instead of creating a unique template
body.push(b.var(id, b.call('$.comment')));
} else {
add_template(template_name, [
b.template([b.quasi(state.template.join(''), true)], []),
b.literal(flags)
]);
body.push(b.var(id, b.call(template_name)));
body.push(b.var(id, b.call(template_name)));
}
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
}
body.push(...state.before_init, ...state.init);
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
}
} else {
body.push(...state.before_init, ...state.init);
}
if (state.update.length > 0) {
body.push(serialize_render_stmt(state));
body.push(serialize_render_stmt(state.update));
}
body.push(...state.after_update);
@ -1832,27 +1852,26 @@ export const template_visitors = {
context.state.template.push('<!>');
const callee = unwrap_optional(node.expression).callee;
const raw_args = unwrap_optional(node.expression).arguments;
const is_reactive =
callee.type !== 'Identifier' || context.state.scope.get(callee.name)?.kind !== 'normal';
/** @type {import('estree').Expression[]} */
const args = [context.state.node];
for (const arg of raw_args) {
args.push(b.thunk(/** @type {import('estree').Expression} */ (context.visit(arg))));
}
const args = raw_args.map((arg) =>
b.thunk(/** @type {import('estree').Expression} */ (context.visit(arg)))
);
let snippet_function = /** @type {import('estree').Expression} */ (context.visit(callee));
if (context.state.options.dev) {
snippet_function = b.call('$.validate_snippet', snippet_function);
}
if (is_reactive) {
context.state.init.push(b.stmt(b.call('$.snippet', b.thunk(snippet_function), ...args)));
if (node.metadata.dynamic) {
context.state.init.push(
b.stmt(b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args))
);
} else {
context.state.init.push(
b.stmt(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function,
context.state.node,
...args
)
)
@ -1902,7 +1921,7 @@ export const template_visitors = {
state.init.push(b.stmt(b.call('$.transition', ...args)));
},
RegularElement(node, context) {
/** @type {import('../types.js').SourceLocation} */
/** @type {import('#shared').SourceLocation} */
let location = [-1, -1];
if (context.state.options.dev) {
@ -1915,7 +1934,7 @@ export const template_visitors = {
}
if (node.name === 'noscript') {
context.state.template.push('<!>');
context.state.template.push('<noscript></noscript>');
return;
}
if (node.name === 'script') {
@ -2130,7 +2149,7 @@ export const template_visitors = {
context.state.template.push('>');
/** @type {import('../types.js').SourceLocation[]} */
/** @type {import('#shared').SourceLocation[]} */
const child_locations = [];
/** @type {import('../types').ComponentClientTransformState} */
@ -2157,22 +2176,51 @@ export const template_visitors = {
state.options.preserveComments
);
/** Whether or not we need to wrap the children in `{...}` to avoid declaration conflicts */
const has_declaration = node.fragment.nodes.some((node) => node.type === 'SnippetBlock');
const child_state = has_declaration
? { ...state, init: [], update: [], after_update: [] }
: state;
for (const node of hoisted) {
context.visit(node, state);
context.visit(node, child_state);
}
process_children(
trimmed,
() =>
b.call(
'$.child',
node.name === 'template'
? b.member(context.state.node, b.id('content'))
: context.state.node
),
true,
{ ...context, state }
);
/** @type {import('estree').Expression} */
let arg = context.state.node;
// If `hydrate_node` is set inside the element, we need to reset it
// after the element has been hydrated
let needs_reset = trimmed.some((node) => node.type !== 'Text');
// The same applies if it's a `<template>` element, since we need to
// set the value of `hydrate_node` to `node.content`
if (node.name === 'template') {
needs_reset = true;
arg = b.member(arg, b.id('content'));
child_state.init.push(b.stmt(b.call('$.reset', arg)));
}
process_children(trimmed, () => b.call('$.child', arg), true, {
...context,
state: child_state
});
if (needs_reset) {
child_state.init.push(b.stmt(b.call('$.reset', context.state.node)));
}
if (has_declaration) {
context.state.init.push(
b.block([
...child_state.init,
child_state.update.length > 0 ? serialize_render_stmt(child_state.update) : b.empty,
...child_state.after_update
])
);
}
if (has_direction_attribute) {
// This fixes an issue with Chromium where updates to text content within an element
@ -2270,7 +2318,7 @@ export const template_visitors = {
/** @type {import('estree').Statement[]} */
const inner = inner_context.state.init;
if (inner_context.state.update.length > 0) {
inner.push(serialize_render_stmt(inner_context.state));
inner.push(serialize_render_stmt(inner_context.state.update));
}
inner.push(...inner_context.state.after_update);
inner.push(
@ -2418,7 +2466,7 @@ export const template_visitors = {
b.thunk(b.sequence(indirect_dependencies))
);
const invalidate_store = store_to_invalidate
? b.call('$.invalidate_store', b.id('$$subscriptions'), b.literal(store_to_invalidate))
? b.call('$.invalidate_store', b.id('$$stores'), b.literal(store_to_invalidate))
: undefined;
const sequence = [];
@ -2738,10 +2786,10 @@ export const template_visitors = {
let snippet = b.arrow(args, body);
if (context.state.options.dev) {
snippet = b.call('$.wrap_snippet', snippet, b.id(context.state.analysis.name));
snippet = b.call('$.wrap_snippet', b.id(context.state.analysis.name), snippet);
}
const declaration = b.var(node.expression, snippet);
const declaration = b.const(node.expression, snippet);
// Top-level snippets are hoisted so they can be referenced in the `<script>`
if (context.path.length === 1 && context.path[0].type === 'Fragment') {
@ -2991,27 +3039,26 @@ export const template_visitors = {
}
},
Component(node, context) {
const binding = context.state.scope.get(
node.name.includes('.') ? node.name.slice(0, node.name.indexOf('.')) : node.name
);
if (binding !== null && binding.kind !== 'normal') {
if (node.metadata.dynamic) {
// Handle dynamic references to what seems like static inline components
const component = serialize_inline_component(node, '$$component', context);
const component = serialize_inline_component(node, '$$component', context, b.id('$$anchor'));
context.state.init.push(
b.stmt(
b.call(
'$.component',
context.state.node,
// TODO use untrack here to not update when binding changes?
// Would align with Svelte 4 behavior, but it's arguably nicer/expected to update this
b.thunk(
/** @type {import('estree').Expression} */ (context.visit(b.member_id(node.name)))
),
b.arrow([b.id('$$component')], b.block([component]))
b.arrow([b.id('$$anchor'), b.id('$$component')], b.block([component]))
)
)
);
return;
}
const component = serialize_inline_component(node, node.name, context);
context.state.init.push(component);
},

@ -1,3 +1,6 @@
/** @import { Visitors } from 'zimmerframe' */
/** @import { ValidatedCompileOptions, Css } from '#compiler' */
/** @import { ComponentAnalysis } from '../../types.js' */
import MagicString from 'magic-string';
import { walk } from 'zimmerframe';
import { is_keyframes_node, regex_css_name_boundary, remove_css_prefix } from '../../css.js';
@ -19,8 +22,8 @@ import { merge_with_preprocessor_map } from '../../../utils/mapped_code.js';
/**
*
* @param {string} source
* @param {import('../../types.js').ComponentAnalysis} analysis
* @param {import('#compiler').ValidatedCompileOptions} options
* @param {ComponentAnalysis} analysis
* @param {ValidatedCompileOptions} options
*/
export function render_stylesheet(source, analysis, options) {
const code = new MagicString(source);
@ -37,9 +40,9 @@ export function render_stylesheet(source, analysis, options) {
}
};
const ast = /** @type {import('#compiler').Css.StyleSheet} */ (analysis.css.ast);
const ast = /** @type {Css.StyleSheet} */ (analysis.css.ast);
walk(/** @type {import('#compiler').Css.Node} */ (ast), state, visitors);
walk(/** @type {Css.Node} */ (ast), state, visitors);
code.remove(0, ast.content.start);
code.remove(/** @type {number} */ (ast.content.end), source.length);
@ -64,7 +67,7 @@ export function render_stylesheet(source, analysis, options) {
return css;
}
/** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, State>} */
/** @type {Visitors<Css.Node, State>} */
const visitors = {
_: (node, context) => {
context.state.code.addSourcemapLocation(node.start);
@ -196,7 +199,7 @@ const visitors = {
if (parent?.type === 'Rule') {
specificity = { bumped: false };
/** @type {import('#compiler').Css.Rule | null} */
/** @type {Css.Rule | null} */
let rule = parent.metadata.parent_rule;
while (rule) {
@ -213,7 +216,7 @@ const visitors = {
ComplexSelector(node, context) {
const before_bumped = context.state.specificity.bumped;
/** @param {import('#compiler').Css.SimpleSelector} selector */
/** @param {Css.SimpleSelector} selector */
function remove_global_pseudo_class(selector) {
context.state.code
.remove(selector.start, selector.start + ':global('.length)
@ -293,7 +296,7 @@ const visitors = {
}
};
/** @param {import('#compiler').Css.Rule} rule */
/** @param {Css.Rule} rule */
function is_empty(rule) {
if (rule.metadata.is_global_block) {
return rule.block.children.length === 0;
@ -316,7 +319,7 @@ function is_empty(rule) {
return true;
}
/** @param {import('#compiler').Css.Rule} rule */
/** @param {Css.Rule} rule */
function is_used(rule) {
for (const selector of rule.prelude.children) {
if (selector.metadata.used) return true;
@ -335,7 +338,7 @@ function is_used(rule) {
/**
*
* @param {import('#compiler').Css.Rule} node
* @param {Css.Rule} node
* @param {MagicString} code
*/
function escape_comment_close(node, code) {

@ -1,3 +1,5 @@
/** @import { ValidatedCompileOptions, CompileResult, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { ComponentAnalysis, Analysis } from '../types' */
import { print } from 'esrap';
import { VERSION } from '../../../version.js';
import { server_component, server_module } from './server/transform-server.js';
@ -7,10 +9,10 @@ import { merge_with_preprocessor_map, get_source_name } from '../../utils/mapped
import * as state from '../../state.js';
/**
* @param {import('../types').ComponentAnalysis} analysis
* @param {ComponentAnalysis} analysis
* @param {string} source
* @param {import('#compiler').ValidatedCompileOptions} options
* @returns {import('#compiler').CompileResult}
* @param {ValidatedCompileOptions} options
* @returns {CompileResult}
*/
export function transform_component(analysis, source, options) {
if (options.generate === false) {
@ -55,10 +57,10 @@ export function transform_component(analysis, source, options) {
}
/**
* @param {import('../types').Analysis} analysis
* @param {Analysis} analysis
* @param {string} source
* @param {import('#compiler').ValidatedModuleCompileOptions} options
* @returns {import('#compiler').CompileResult}
* @param {ValidatedModuleCompileOptions} options
* @returns {CompileResult}
*/
export function transform_module(analysis, source, options) {
if (options.generate === false) {

@ -33,16 +33,21 @@ import {
import { escape_html } from '../../../../escaping.js';
import { sanitize_template_string } from '../../../utils/sanitize_template_string.js';
import {
BLOCK_ANCHOR,
EMPTY_COMMENT,
BLOCK_CLOSE,
BLOCK_CLOSE_ELSE,
BLOCK_OPEN
BLOCK_OPEN,
BLOCK_OPEN_ELSE
} from '../../../../internal/server/hydration.js';
import { filename, locator } from '../../../state.js';
export const block_open = b.literal(BLOCK_OPEN);
export const block_close = b.literal(BLOCK_CLOSE);
export const block_anchor = b.literal(BLOCK_ANCHOR);
/** Opens an if/each block, so that we can remove nodes in the case of a mismatch */
const block_open = b.literal(BLOCK_OPEN);
/** Closes an if/each block, so that we can remove nodes in the case of a mismatch. Also serves as an anchor for these blocks */
const block_close = b.literal(BLOCK_CLOSE);
/** Empty comment to keep text nodes separate, or provide an anchor node for blocks */
const empty_comment = b.literal(EMPTY_COMMENT);
/**
* @param {import('estree').Node} node
@ -411,6 +416,11 @@ const global_visitors = {
return b.literal(false);
}
if (rune === '$effect.root') {
// ignore $effect.root() calls, just return a noop which mimics the cleanup function
return b.arrow([], b.block([]));
}
if (rune === '$state.snapshot') {
return /** @type {import('estree').Expression} */ (context.visit(node.arguments[0]));
}
@ -571,7 +581,7 @@ const javascript_visitors_runes = {
for (const declarator of node.declarations) {
const init = declarator.init;
const rune = get_rune(init, state.scope);
if (!rune || rune === '$effect.tracking' || rune === '$inspect') {
if (!rune || rune === '$effect.tracking' || rune === '$inspect' || rune === '$effect.root') {
declarations.push(/** @type {import('estree').VariableDeclarator} */ (visit(declarator)));
continue;
}
@ -991,20 +1001,32 @@ function serialize_inline_component(node, expression, context) {
statement = b.block([...snippet_declarations, statement]);
}
const dynamic =
node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic);
if (custom_css_props.length > 0) {
statement = b.stmt(
b.call(
'$.css_props',
b.id('$$payload'),
b.literal(context.state.namespace === 'svg' ? false : true),
b.object(custom_css_props),
b.thunk(b.block([statement]))
context.state.template.push(
b.stmt(
b.call(
'$.css_props',
b.id('$$payload'),
b.literal(context.state.namespace === 'svg' ? false : true),
b.object(custom_css_props),
b.thunk(b.block([statement])),
dynamic && b.true
)
)
);
} else {
if (dynamic) {
context.state.template.push(empty_comment);
}
context.state.template.push(statement);
} else {
context.state.template.push(block_open, statement, block_close);
if (!context.state.skip_hydration_boundaries) {
context.state.template.push(empty_comment);
}
}
}
@ -1112,7 +1134,7 @@ const template_visitors = {
const parent = context.path.at(-1) ?? node;
const namespace = infer_namespace(context.state.namespace, parent, node.nodes);
const { hoisted, trimmed } = clean_nodes(
const { hoisted, trimmed, is_standalone, is_text_first } = clean_nodes(
parent,
node.nodes,
context.path,
@ -1127,20 +1149,26 @@ const template_visitors = {
...context.state,
init: [],
template: [],
namespace
namespace,
skip_hydration_boundaries: is_standalone
};
for (const node of hoisted) {
context.visit(node, state);
}
if (is_text_first) {
// insert `<!---->` to prevent this from being glued to the previous fragment
state.template.push(empty_comment);
}
process_children(trimmed, { ...context, state });
return b.block([...state.init, ...serialize_template(state.template)]);
},
HtmlTag(node, context) {
const expression = /** @type {import('estree').Expression} */ (context.visit(node.expression));
context.state.template.push(block_open, expression, block_close);
context.state.template.push(empty_comment, expression, empty_comment);
},
ConstTag(node, { state, visit }) {
const declaration = node.declaration.declarations[0];
@ -1181,16 +1209,18 @@ const template_visitors = {
});
context.state.template.push(
block_open,
b.stmt(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function,
b.id('$$payload'),
...snippet_args
)
),
block_close
)
);
if (!context.state.skip_hydration_boundaries) {
context.state.template.push(empty_comment);
}
},
ClassDirective() {
throw new Error('Node should have been handled elsewhere');
@ -1321,19 +1351,24 @@ const template_visitors = {
context.visit(node.fragment, state)
);
const body = b.stmt(
b.call('$.element', b.id('$$payload'), tag, b.thunk(attributes), b.thunk(children))
context.state.template.push(
b.stmt(
b.call(
'$.element',
b.id('$$payload'),
tag,
attributes.body.length > 0 && b.thunk(attributes),
children.body.length > 0 && b.thunk(children)
)
)
);
context.state.template.push(b.if(tag, body), block_anchor);
if (context.state.options.dev) {
context.state.template.push(b.stmt(b.call('$.pop_element')));
}
},
EachBlock(node, context) {
const state = context.state;
state.template.push(block_open);
const each_node_meta = node.metadata;
const collection = /** @type {import('estree').Expression} */ (context.visit(node.expression));
@ -1356,12 +1391,8 @@ const template_visitors = {
each.push(b.let(node.index, index));
}
each.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_OPEN))));
each.push(.../** @type {import('estree').BlockStatement} */ (context.visit(node.body)).body);
each.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE))));
const for_loop = b.for(
b.let(index, b.literal(0)),
b.binary('<', index, b.member(array_id, b.id('length'))),
@ -1369,26 +1400,27 @@ const template_visitors = {
b.block(each)
);
const close = b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE)));
if (node.fallback) {
const open = b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open));
const fallback = /** @type {import('estree').BlockStatement} */ (
context.visit(node.fallback)
);
fallback.body.push(
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE_ELSE)))
fallback.body.unshift(
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_OPEN_ELSE)))
);
state.template.push(
b.if(
b.binary('!==', b.member(array_id, b.id('length')), b.literal(0)),
b.block([for_loop, close]),
b.block([open, for_loop]),
fallback
)
),
block_close
);
} else {
state.template.push(for_loop, close);
state.template.push(block_open, for_loop, block_close);
}
},
IfBlock(node, context) {
@ -1402,16 +1434,17 @@ const template_visitors = {
? /** @type {import('estree').BlockStatement} */ (context.visit(node.alternate))
: b.block([]);
consequent.body.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE))));
alternate.body.push(
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE_ELSE)))
consequent.body.unshift(b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open)));
alternate.body.unshift(
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_OPEN_ELSE)))
);
context.state.template.push(block_open, b.if(test, consequent, alternate));
context.state.template.push(b.if(test, consequent, alternate), block_close);
},
AwaitBlock(node, context) {
context.state.template.push(
block_open,
empty_comment,
b.stmt(
b.call(
'$.await',
@ -1435,12 +1468,12 @@ const template_visitors = {
)
)
),
block_close
empty_comment
);
},
KeyBlock(node, context) {
const block = /** @type {import('estree').BlockStatement} */ (context.visit(node.fragment));
context.state.template.push(block_open, block, block_close);
context.state.template.push(empty_comment, block, empty_comment);
},
SnippetBlock(node, context) {
const fn = b.function_declaration(
@ -1574,7 +1607,7 @@ const template_visitors = {
const slot = b.call('$.slot', b.id('$$payload'), expression, props_expression, fallback);
context.state.template.push(block_open, b.stmt(slot), block_close);
context.state.template.push(empty_comment, b.stmt(slot), empty_comment);
},
SvelteHead(node, context) {
const block = /** @type {import('estree').BlockStatement} */ (context.visit(node.fragment));
@ -1925,7 +1958,8 @@ export function server_component(analysis, options) {
template: /** @type {any} */ (null),
namespace: options.namespace,
preserve_whitespace: options.preserveWhitespace,
private_derived: new Map()
private_derived: new Map(),
skip_hydration_boundaries: false
};
const module = /** @type {import('estree').Program} */ (

@ -22,6 +22,7 @@ export interface ComponentServerTransformState extends ServerTransformState {
readonly template: Array<Statement | Expression>;
readonly namespace: Namespace;
readonly preserve_whitespace: boolean;
readonly skip_hydration_boundaries: boolean;
}
export type Context = import('zimmerframe').Context<SvelteNode, ServerTransformState>;

@ -1,3 +1,7 @@
/** @import { Context } from 'zimmerframe' */
/** @import { TransformState } from './types.js' */
/** @import * as Compiler from '#compiler' */
/** @import { Node, Expression, CallExpression } from 'estree' */
import {
regex_ends_with_whitespaces,
regex_not_whitespace,
@ -12,7 +16,7 @@ import is_reference from 'is-reference';
import { set_scope } from '../scope.js';
/**
* @param {import('estree').Node} node
* @param {Node} node
* @returns {boolean}
*/
export function is_hoistable_function(node) {
@ -28,20 +32,20 @@ export function is_hoistable_function(node) {
/**
* Match Svelte 4 behaviour by sorting ConstTag nodes in topological order
* @param {import("#compiler").SvelteNode[]} nodes
* @param {import('./types.js').TransformState} state
* @param {Compiler.SvelteNode[]} nodes
* @param {TransformState} state
*/
function sort_const_tags(nodes, state) {
/**
* @typedef {{
* node: import('#compiler').ConstTag;
* deps: Set<import('#compiler').Binding>;
* node: Compiler.ConstTag;
* deps: Set<Compiler.Binding>;
* }} Tag
*/
const other = [];
/** @type {Map<import('#compiler').Binding, Tag>} */
/** @type {Map<Compiler.Binding, Tag>} */
const tags = new Map();
const { _ } = set_scope(state.scopes);
@ -51,16 +55,16 @@ function sort_const_tags(nodes, state) {
const declaration = node.declaration.declarations[0];
const bindings = extract_identifiers(declaration.id).map((id) => {
return /** @type {import('#compiler').Binding} */ (state.scope.get(id.name));
return /** @type {Compiler.Binding} */ (state.scope.get(id.name));
});
/** @type {Set<import('#compiler').Binding>} */
/** @type {Set<Compiler.Binding>} */
const deps = new Set();
walk(declaration.init, state, {
_,
Identifier(node, context) {
const parent = /** @type {import('estree').Expression} */ (context.path.at(-1));
const parent = /** @type {Expression} */ (context.path.at(-1));
if (is_reference(node, parent)) {
const binding = context.state.scope.get(node.name);
@ -81,7 +85,7 @@ function sort_const_tags(nodes, state) {
return nodes;
}
/** @type {Array<[import('#compiler').Binding, import('#compiler').Binding]>} */
/** @type {Array<[Compiler.Binding, Compiler.Binding]>} */
const edges = [];
for (const [id, tag] of tags) {
@ -98,7 +102,7 @@ function sort_const_tags(nodes, state) {
e.const_tag_cycle(tag.node, cycle.map((binding) => binding.node.name).join(' → '));
}
/** @type {import('#compiler').ConstTag[]} */
/** @type {Compiler.ConstTag[]} */
const sorted = [];
/** @param {Tag} tag */
@ -129,11 +133,11 @@ function sort_const_tags(nodes, state) {
* unless it's whitespace-only, in which case collapse to a single whitespace for all cases
* except when it's children of certain elements where we know ignore whitespace (like td/option/head),
* in which case we remove it entirely
* @param {import('#compiler').SvelteNode} parent
* @param {import('#compiler').SvelteNode[]} nodes
* @param {import('#compiler').SvelteNode[]} path
* @param {import('#compiler').Namespace} namespace
* @param {import('./types.js').TransformState} state
* @param {Compiler.SvelteNode} parent
* @param {Compiler.SvelteNode[]} nodes
* @param {Compiler.SvelteNode[]} path
* @param {Compiler.Namespace} namespace
* @param {TransformState & { options: Compiler.ValidatedCompileOptions }} state
* @param {boolean} preserve_whitespace
* @param {boolean} preserve_comments
*/
@ -153,10 +157,10 @@ export function clean_nodes(
nodes = sort_const_tags(nodes, state);
}
/** @type {import('#compiler').SvelteNode[]} */
/** @type {Compiler.SvelteNode[]} */
const hoisted = [];
/** @type {import('#compiler').SvelteNode[]} */
/** @type {Compiler.SvelteNode[]} */
const regular = [];
for (const node of nodes) {
@ -181,90 +185,120 @@ export function clean_nodes(
}
}
if (preserve_whitespace) {
return { hoisted, trimmed: regular };
}
let trimmed = regular;
let first, last;
if (!preserve_whitespace) {
trimmed = [];
while ((first = regular[0]) && first.type === 'Text' && !regex_not_whitespace.test(first.data)) {
regular.shift();
}
let first, last;
if (first?.type === 'Text') {
first.raw = first.raw.replace(regex_starts_with_whitespaces, '');
first.data = first.data.replace(regex_starts_with_whitespaces, '');
}
while (
(first = regular[0]) &&
first.type === 'Text' &&
!regex_not_whitespace.test(first.data)
) {
regular.shift();
}
while ((last = regular.at(-1)) && last.type === 'Text' && !regex_not_whitespace.test(last.data)) {
regular.pop();
}
if (first?.type === 'Text') {
first.raw = first.raw.replace(regex_starts_with_whitespaces, '');
first.data = first.data.replace(regex_starts_with_whitespaces, '');
}
if (last?.type === 'Text') {
last.raw = last.raw.replace(regex_ends_with_whitespaces, '');
last.data = last.data.replace(regex_ends_with_whitespaces, '');
}
while (
(last = regular.at(-1)) &&
last.type === 'Text' &&
!regex_not_whitespace.test(last.data)
) {
regular.pop();
}
const can_remove_entirely =
(namespace === 'svg' &&
(parent.type !== 'RegularElement' || parent.name !== 'text') &&
!path.some((n) => n.type === 'RegularElement' && n.name === 'text')) ||
(parent.type === 'RegularElement' &&
// TODO others?
(parent.name === 'select' ||
parent.name === 'tr' ||
parent.name === 'table' ||
parent.name === 'tbody' ||
parent.name === 'thead' ||
parent.name === 'tfoot' ||
parent.name === 'colgroup' ||
parent.name === 'datalist'));
/** @type {import('#compiler').SvelteNode[]} */
const trimmed = [];
// Replace any whitespace between a text and non-text node with a single spaceand keep whitespace
// as-is within text nodes, or between text nodes and expression tags (because in the end they count
// as one text). This way whitespace is mostly preserved when using CSS with `white-space: pre-line`
// and default slot content going into a pre tag (which we can't see).
for (let i = 0; i < regular.length; i++) {
const prev = regular[i - 1];
const node = regular[i];
const next = regular[i + 1];
if (node.type === 'Text') {
if (prev?.type !== 'ExpressionTag') {
const prev_is_text_ending_with_whitespace =
prev?.type === 'Text' && regex_ends_with_whitespaces.test(prev.data);
node.data = node.data.replace(
regex_starts_with_whitespaces,
prev_is_text_ending_with_whitespace ? '' : ' '
);
node.raw = node.raw.replace(
regex_starts_with_whitespaces,
prev_is_text_ending_with_whitespace ? '' : ' '
);
}
if (next?.type !== 'ExpressionTag') {
node.data = node.data.replace(regex_ends_with_whitespaces, ' ');
node.raw = node.raw.replace(regex_ends_with_whitespaces, ' ');
}
if (node.data && (node.data !== ' ' || !can_remove_entirely)) {
if (last?.type === 'Text') {
last.raw = last.raw.replace(regex_ends_with_whitespaces, '');
last.data = last.data.replace(regex_ends_with_whitespaces, '');
}
const can_remove_entirely =
(namespace === 'svg' &&
(parent.type !== 'RegularElement' || parent.name !== 'text') &&
!path.some((n) => n.type === 'RegularElement' && n.name === 'text')) ||
(parent.type === 'RegularElement' &&
// TODO others?
(parent.name === 'select' ||
parent.name === 'tr' ||
parent.name === 'table' ||
parent.name === 'tbody' ||
parent.name === 'thead' ||
parent.name === 'tfoot' ||
parent.name === 'colgroup' ||
parent.name === 'datalist'));
// Replace any whitespace between a text and non-text node with a single spaceand keep whitespace
// as-is within text nodes, or between text nodes and expression tags (because in the end they count
// as one text). This way whitespace is mostly preserved when using CSS with `white-space: pre-line`
// and default slot content going into a pre tag (which we can't see).
for (let i = 0; i < regular.length; i++) {
const prev = regular[i - 1];
const node = regular[i];
const next = regular[i + 1];
if (node.type === 'Text') {
if (prev?.type !== 'ExpressionTag') {
const prev_is_text_ending_with_whitespace =
prev?.type === 'Text' && regex_ends_with_whitespaces.test(prev.data);
node.data = node.data.replace(
regex_starts_with_whitespaces,
prev_is_text_ending_with_whitespace ? '' : ' '
);
node.raw = node.raw.replace(
regex_starts_with_whitespaces,
prev_is_text_ending_with_whitespace ? '' : ' '
);
}
if (next?.type !== 'ExpressionTag') {
node.data = node.data.replace(regex_ends_with_whitespaces, ' ');
node.raw = node.raw.replace(regex_ends_with_whitespaces, ' ');
}
if (node.data && (node.data !== ' ' || !can_remove_entirely)) {
trimmed.push(node);
}
} else {
trimmed.push(node);
}
} else {
trimmed.push(node);
}
}
return { hoisted, trimmed };
var first = trimmed[0];
return {
hoisted,
trimmed,
/**
* In a case like `{#if x}<Foo />{/if}`, we don't need to wrap the child in
* comments we can just use the parent block's anchor for the component.
* TODO extend this optimisation to other cases
*/
is_standalone:
trimmed.length === 1 &&
((first.type === 'RenderTag' && !first.metadata.dynamic) ||
(first.type === 'Component' &&
!state.options.hmr &&
!first.attributes.some(
(attribute) => attribute.type === 'Attribute' && attribute.name.startsWith('--')
))),
/** if a component or snippet starts with text, we need to add an anchor comment so that its text node doesn't get fused with its surroundings */
is_text_first:
(parent.type === 'Fragment' || parent.type === 'SnippetBlock') &&
first &&
(first?.type === 'Text' || first?.type === 'ExpressionTag')
};
}
/**
* Infers the namespace for the children of a node that should be used when creating the `$.template(...)`.
* @param {import('#compiler').Namespace} namespace
* @param {import('#compiler').SvelteNode} parent
* @param {import('#compiler').SvelteNode[]} nodes
* @param {Compiler.Namespace} namespace
* @param {Compiler.SvelteNode} parent
* @param {Compiler.SvelteNode[]} nodes
*/
export function infer_namespace(namespace, parent, nodes) {
if (namespace !== 'foreign') {
@ -303,12 +337,12 @@ export function infer_namespace(namespace, parent, nodes) {
* Heuristic: Keep current namespace, unless we find a regular element,
* in which case we always want html, or we only find svg nodes,
* in which case we assume svg.
* @param {import('#compiler').SvelteNode[]} nodes
* @param {import('#compiler').Namespace | 'keep' | 'maybe_html'} namespace
* @param {Compiler.SvelteNode[]} nodes
* @param {Compiler.Namespace | 'keep' | 'maybe_html'} namespace
*/
function check_nodes_for_namespace(nodes, namespace) {
/**
* @param {import('#compiler').SvelteElement | import('#compiler').RegularElement} node}
* @param {Compiler.SvelteElement | Compiler.RegularElement} node}
* @param {{stop: () => void}} context
*/
const RegularElement = (node, { stop }) => {
@ -357,9 +391,9 @@ function check_nodes_for_namespace(nodes, namespace) {
/**
* Determines the namespace the children of this node are in.
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
* @param {import('#compiler').Namespace} namespace
* @returns {import('#compiler').Namespace}
* @param {Compiler.RegularElement | Compiler.SvelteElement} node
* @param {Compiler.Namespace} namespace
* @returns {Compiler.Namespace}
*/
export function determine_namespace_for_children(node, namespace) {
if (namespace === 'foreign') {
@ -378,9 +412,9 @@ export function determine_namespace_for_children(node, namespace) {
}
/**
* @template {import('./types.js').TransformState} T
* @param {import('estree').CallExpression} node
* @param {import('zimmerframe').Context<any, T>} context
* @template {TransformState} T
* @param {CallExpression} node
* @param {Context<any, T>} context
*/
export function transform_inspect_rune(node, context) {
const { state, visit } = context;
@ -389,12 +423,11 @@ export function transform_inspect_rune(node, context) {
if (!state.options.dev) return b.unary('void', b.literal(0));
if (node.callee.type === 'MemberExpression') {
const raw_inspect_args = /** @type {import('estree').CallExpression} */ (node.callee.object)
.arguments;
const raw_inspect_args = /** @type {CallExpression} */ (node.callee.object).arguments;
const inspect_args =
/** @type {Array<import('estree').Expression>} */
/** @type {Array<Expression>} */
(raw_inspect_args.map((arg) => visit(arg)));
const with_arg = /** @type {import('estree').Expression} */ (visit(node.arguments[0]));
const with_arg = /** @type {Expression} */ (visit(node.arguments[0]));
return b.call(
'$.inspect',
@ -402,9 +435,7 @@ export function transform_inspect_rune(node, context) {
with_arg
);
} else {
const arg = node.arguments.map(
(arg) => /** @type {import('estree').Expression} */ (visit(arg))
);
const arg = node.arguments.map((arg) => /** @type {Expression} */ (visit(arg)));
return b.call('$.inspect', as_fn ? b.thunk(b.array(arg)) : b.array(arg));
}
}

@ -1,3 +1,4 @@
/** @import { Css } from '#compiler' */
const regex_css_browser_prefix = /^-((webkit)|(moz)|(o)|(ms))-/;
export const regex_css_name_boundary = /^[\s,;}]$/;
@ -9,5 +10,5 @@ export function remove_css_prefix(name) {
return name.replace(regex_css_browser_prefix, '');
}
/** @param {import('#compiler').Css.Atrule} node */
/** @param {Css.Atrule} node */
export const is_keyframes_node = (node) => remove_css_prefix(node.name) === 'keyframes';

@ -1,3 +1,4 @@
/** @import * as Compiler from '#compiler' */
/**
* All nodes that can appear elsewhere than the top level, have attributes and can contain children
*/
@ -13,15 +14,15 @@ const element_nodes = [
/**
* Returns true for all nodes that can appear elsewhere than the top level, have attributes and can contain children
* @param {import('#compiler').SvelteNode} node
* @returns {node is import('#compiler').Component | import('#compiler').RegularElement | import('#compiler').SlotElement | import('#compiler').SvelteComponent | import('#compiler').SvelteElement | import('#compiler').SvelteFragment | import('#compiler').SvelteSelf}
* @param {Compiler.SvelteNode} node
* @returns {node is Compiler.Component | Compiler.RegularElement | Compiler.SlotElement | Compiler.SvelteComponent | Compiler.SvelteElement | Compiler.SvelteFragment | Compiler.SvelteSelf}
*/
export function is_element_node(node) {
return element_nodes.includes(node.type);
}
/**
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
* @param {Compiler.RegularElement | Compiler.SvelteElement} node
* @returns {boolean}
*/
export function is_custom_element_node(node) {
@ -32,8 +33,8 @@ export function is_custom_element_node(node) {
* @param {string} name
* @param {number} start
* @param {number} end
* @param {true | Array<import('#compiler').Text | import('#compiler').ExpressionTag>} value
* @returns {import('#compiler').Attribute}
* @param {true | Array<Compiler.Text | Compiler.ExpressionTag>} value
* @returns {Compiler.Attribute}
*/
export function create_attribute(name, start, end, value) {
return {

@ -1,3 +1,4 @@
/** @import { Visitors, Context } from 'zimmerframe' */
const overrides = {
visit() {
throw new Error('Cannot call visit() during analysis');
@ -10,7 +11,7 @@ const overrides = {
/**
* @template {{ type: string }} T
* @template U
* @param {...import('zimmerframe').Visitors<T, U>} tasks
* @param {...Visitors<T, U>} tasks
* @returns
*/
export function merge(...tasks) {
@ -24,7 +25,7 @@ export function merge(...tasks) {
}
}
/** @type {import('zimmerframe').Visitors<T, U>} */
/** @type {Visitors<T, U>} */
// @ts-expect-error
const combined = {};
@ -33,7 +34,7 @@ export function merge(...tasks) {
/**
* @param {T} node
* @param {import('zimmerframe').Context<T, U>} context
* @param {Context<T, U>} context
*/
function visitor(node, context) {
/**

@ -1,3 +1,4 @@
/** @import { Processed } from './public.js' */
import { decode as decode_mappings } from '@jridgewell/sourcemap-codec';
/**
@ -80,7 +81,7 @@ function decoded_sourcemap_from_generator(generator) {
}
/**
* @param {import('./public.js').Processed} processed
* @param {Processed} processed
*/
export function decode_map(processed) {
let decoded_map = typeof processed.map === 'string' ? JSON.parse(processed.map) : processed.map;

@ -1,3 +1,6 @@
/** @import { Processed, Preprocessor, MarkupPreprocessor, PreprocessorGroup } from './public.js' */
/** @import { SourceUpdate, Source } from './private.js' */
/** @import { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping' */
import { getLocator } from 'locate-character';
import {
MappedCode,
@ -26,7 +29,7 @@ class PreprocessResult {
/**
* @default []
* @type {Array<import('@ampproject/remapping').DecodedSourceMap | import('@ampproject/remapping').RawSourceMap>}
* @type {Array<DecodedSourceMap | RawSourceMap>}
*/
sourcemap_list = [];
@ -59,7 +62,7 @@ class PreprocessResult {
}
/**
* @param {import('./private.js').SourceUpdate} opts
* @param {SourceUpdate} opts
*/
update_source({ string: source, map, dependencies }) {
if (source != null) {
@ -75,7 +78,7 @@ class PreprocessResult {
}
/**
* @returns {import('./public.js').Processed}
* @returns {Processed}
*/
to_processed() {
// Combine all the source maps for each preprocessor function into one
@ -96,7 +99,7 @@ class PreprocessResult {
}
/**
* Convert preprocessor output for the tag content into MappedCode
* @param {import('./public.js').Processed} processed
* @param {Processed} processed
* @param {{ line: number; column: number; }} location
* @param {string} file_basename
* @returns {MappedCode}
@ -105,7 +108,7 @@ function processed_content_to_code(processed, location, file_basename) {
// Convert the preprocessed code and its sourcemap to a MappedCode
/**
* @type {import('@ampproject/remapping').DecodedSourceMap | undefined}
* @type {DecodedSourceMap | undefined}
*/
let decoded_map = undefined;
if (processed.map) {
@ -124,11 +127,11 @@ function processed_content_to_code(processed, location, file_basename) {
/**
* Given the whole tag including content, return a `MappedCode`
* representing the tag content replaced with `processed`.
* @param {import('./public.js').Processed} processed
* @param {Processed} processed
* @param {'style' | 'script'} tag_name
* @param {string} original_attributes
* @param {string} generated_attributes
* @param {import('./private.js').Source} source
* @param {Source} source
* @returns {MappedCode}
*/
function processed_tag_to_code(
@ -157,7 +160,7 @@ function processed_tag_to_code(
if (original_tag_open.length !== tag_open.length) {
// Generate a source map for the open tag
/** @type {import('@ampproject/remapping').DecodedSourceMap['mappings']} */
/** @type {DecodedSourceMap['mappings']} */
const mappings = [
[
// start of tag
@ -183,7 +186,7 @@ function processed_tag_to_code(
original_tag_open.length - original_tag_open.lastIndexOf('\n') - 1
]);
/** @type {import('@ampproject/remapping').DecodedSourceMap} */
/** @type {DecodedSourceMap} */
const map = {
version: 3,
names: [],
@ -255,9 +258,9 @@ const regex_script_tags =
/**
* Calculate the updates required to process all instances of the specified tag.
* @param {'style' | 'script'} tag_name
* @param {import('./public.js').Preprocessor} preprocessor
* @param {import('./private.js').Source} source
* @returns {Promise<import('./private.js').SourceUpdate>}
* @param {Preprocessor} preprocessor
* @param {Source} source
* @returns {Promise<SourceUpdate>}
*/
async function process_tag(tag_name, preprocessor, source) {
const { filename, source: markup } = source;
@ -299,8 +302,8 @@ async function process_tag(tag_name, preprocessor, source) {
}
/**
* @param {import('./public.js').MarkupPreprocessor} process
* @param {import('./private.js').Source} source
* @param {MarkupPreprocessor} process
* @param {Source} source
*/
async function process_markup(process, source) {
const processed = await process({
@ -329,9 +332,9 @@ async function process_markup(process, source) {
*
* https://svelte.dev/docs/svelte-compiler#svelte-preprocess
* @param {string} source
* @param {import('./public.js').PreprocessorGroup | import('./public.js').PreprocessorGroup[]} preprocessor
* @param {PreprocessorGroup | PreprocessorGroup[]} preprocessor
* @param {{ filename?: string }} [options]
* @returns {Promise<import('./public.js').Processed>}
* @returns {Promise<Processed>}
*/
export default async function preprocess(source, preprocessor, options) {
/**

@ -1,10 +1,11 @@
/** @import { Source, Replacement } from './private.js' */
import { MappedCode } from '../utils/mapped_code.js';
/**
* @param {string} code_slice
* @param {number} offset
* @param {import('./private.js').Source} opts
* @returns {import('./private.js').Source}
* @param {Source} opts
* @returns {Source}
*/
export function slice_source(code_slice, offset, { file_basename, filename, get_location }) {
return {
@ -22,7 +23,7 @@ export function slice_source(code_slice, offset, { file_basename, filename, get_
*/
function calculate_replacements(re, get_replacement, source) {
/**
* @type {Array<Promise<import('./private.js').Replacement>>}
* @type {Array<Promise<Replacement>>}
*/
const replacements = [];
source.replace(re, (...match) => {
@ -39,8 +40,8 @@ function calculate_replacements(re, get_replacement, source) {
}
/**
* @param {import('./private.js').Replacement[]} replacements
* @param {import('./private.js').Source} source
* @param {Replacement[]} replacements
* @param {Source} source
* @returns {MappedCode}
*/
function perform_replacements(replacements, source) {
@ -62,7 +63,7 @@ function perform_replacements(replacements, source) {
/**
* @param {RegExp} regex
* @param {(...match: any[]) => Promise<MappedCode>} get_replacement
* @param {import('./private.js').Source} location
* @param {Source} location
* @returns {Promise<MappedCode>}
*/
export async function replace_in_code(regex, get_replacement, location) {

@ -5,4 +5,10 @@ export type {
PreprocessorGroup,
Processed
} from './preprocess/public';
export type { CompileOptions, ModuleCompileOptions, CompileResult, Warning } from './types/index';
export type {
CompileError,
CompileOptions,
ModuleCompileOptions,
CompileResult,
Warning
} from './types/index';

@ -1,8 +1,10 @@
/** @import { SvelteNode } from './types' */
/** @import { Warning } from '#compiler' */
import { getLocator } from 'locate-character';
/** @typedef {{ start?: number, end?: number }} NodeLike */
/** @type {import('#compiler').Warning[]} */
/** @type {Warning[]} */
export let warnings = [];
/**
@ -12,6 +14,12 @@ export let warnings = [];
*/
export let filename;
/**
* The original source code
* @type {string}
*/
export let source;
export let locator = getLocator('', { offsetLine: 1 });
/**
@ -24,7 +32,7 @@ export let ignore_stack = [];
* For each node the list of warnings that should be ignored for that node.
* Exists in addition to `ignore_stack` because not all warnings are emitted
* while the stack is being built.
* @type {Map<import("./types").SvelteNode | NodeLike, Set<string>[]>}
* @type {Map<SvelteNode | NodeLike, Set<string>[]>}
*/
export let ignore_map = new Map();
@ -41,10 +49,11 @@ export function pop_ignore() {
}
/**
* @param {string} source
* @param {string} _source
* @param {{ filename?: string, rootDir?: string }} options
*/
export function reset(source, options) {
export function reset(_source, options) {
source = _source;
const root_dir = options.rootDir?.replace(/\\/g, '/');
filename = options.filename?.replace(/\\/g, '/');

@ -6,12 +6,12 @@ import type {
Identifier,
ImportDeclaration
} from 'estree';
import type { Location } from 'locate-character';
import type { SourceMap } from 'magic-string';
import type { Context } from 'zimmerframe';
import type { Scope } from '../phases/scope.js';
import type { Css } from './css.js';
import type { EachBlock, Namespace, SvelteNode, SvelteOptions } from './template.js';
import type { ICompileDiagnostic } from '../utils/compile_diagnostic.js';
/** The return value of `compile` from `svelte/compiler` */
export interface CompileResult {
@ -50,22 +50,9 @@ export interface CompileResult {
ast: any;
}
export interface Warning {
start?: Location;
end?: Location;
// TODO there was pos: number in Svelte 4 - do we want to add it back?
code: string;
message: string;
filename?: string;
}
export interface Warning extends ICompileDiagnostic {}
export interface CompileError extends Error {
code: string;
filename?: string;
position?: [number, number];
start?: Location;
end?: Location;
}
export interface CompileError extends ICompileDiagnostic {}
export type CssHashGetter = (args: {
name: string;

@ -152,6 +152,9 @@ export interface DebugTag extends BaseNode {
export interface RenderTag extends BaseNode {
type: 'RenderTag';
expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression });
metadata: {
dynamic: boolean;
};
}
type Tag = ExpressionTag | HtmlTag | ConstTag | DebugTag | RenderTag;
@ -271,6 +274,9 @@ interface BaseElement extends BaseNode {
export interface Component extends BaseElement {
type: 'Component';
metadata: {
dynamic: boolean;
};
}
interface TitleElement extends BaseElement {

@ -1,16 +1,16 @@
/** @import { Attribute, Text, ExpressionTag, SvelteNode } from '#compiler' */
/** @import * as ESTree from 'estree' */
import { walk } from 'zimmerframe';
import * as b from '../utils/builders.js';
/**
* Gets the left-most identifier of a member expression or identifier.
* @param {import('estree').MemberExpression | import('estree').Identifier} expression
* @returns {import('estree').Identifier | null}
* @param {ESTree.MemberExpression | ESTree.Identifier} expression
* @returns {ESTree.Identifier | null}
*/
export function object(expression) {
while (expression.type === 'MemberExpression') {
expression = /** @type {import('estree').MemberExpression | import('estree').Identifier} */ (
expression.object
);
expression = /** @type {ESTree.MemberExpression | ESTree.Identifier} */ (expression.object);
}
if (expression.type !== 'Identifier') {
@ -22,8 +22,8 @@ export function object(expression) {
/**
* Returns true if the attribute contains a single static text node.
* @param {import('#compiler').Attribute} attribute
* @returns {attribute is import('#compiler').Attribute & { value: [import('#compiler').Text] }}
* @param {Attribute} attribute
* @returns {attribute is Attribute & { value: [Text] }}
*/
export function is_text_attribute(attribute) {
return (
@ -33,8 +33,8 @@ export function is_text_attribute(attribute) {
/**
* Returns true if the attribute contains a single expression node.
* @param {import('#compiler').Attribute} attribute
* @returns {attribute is import('#compiler').Attribute & { value: [import('#compiler').ExpressionTag] }}
* @param {Attribute} attribute
* @returns {attribute is Attribute & { value: [ExpressionTag] }}
*/
export function is_expression_attribute(attribute) {
return (
@ -46,8 +46,8 @@ export function is_expression_attribute(attribute) {
/**
* Returns true if the attribute starts with `on` and contains a single expression node.
* @param {import('#compiler').Attribute} attribute
* @returns {attribute is import('#compiler').Attribute & { value: [import('#compiler').ExpressionTag] }}
* @param {Attribute} attribute
* @returns {attribute is Attribute & { value: [ExpressionTag] }}
*/
export function is_event_attribute(attribute) {
return is_expression_attribute(attribute) && attribute.name.startsWith('on');
@ -55,9 +55,9 @@ export function is_event_attribute(attribute) {
/**
* Extracts all identifiers from a pattern.
* @param {import('estree').Pattern} param
* @param {import('estree').Identifier[]} [nodes]
* @returns {import('estree').Identifier[]}
* @param {ESTree.Pattern} param
* @param {ESTree.Identifier[]} [nodes]
* @returns {ESTree.Identifier[]}
*/
export function extract_identifiers(param, nodes = []) {
switch (param.type) {
@ -97,11 +97,11 @@ export function extract_identifiers(param, nodes = []) {
/**
* Extracts all identifiers and a stringified keypath from an expression.
* @param {import('estree').Expression} expr
* @returns {[keypath: string, ids: import('estree').Identifier[]]}
* @param {ESTree.Expression} expr
* @returns {[keypath: string, ids: ESTree.Identifier[]]}
*/
export function extract_all_identifiers_from_expression(expr) {
/** @type {import('estree').Identifier[]} */
/** @type {ESTree.Identifier[]} */
let nodes = [];
/** @type {string[]} */
let keypath = [];
@ -143,8 +143,8 @@ export function extract_all_identifiers_from_expression(expr) {
/**
* Extracts all leaf identifiers from a destructuring expression.
* @param {import('estree').Identifier | import('estree').ObjectExpression | import('estree').ArrayExpression} node
* @param {import('estree').Identifier[]} [nodes]
* @param {ESTree.Identifier | ESTree.ObjectExpression | ESTree.ArrayExpression} node
* @param {ESTree.Identifier[]} [nodes]
* @returns
*/
export function extract_identifiers_from_destructuring(node, nodes = []) {
@ -181,33 +181,33 @@ export function extract_identifiers_from_destructuring(node, nodes = []) {
* or assignment expression. For example, given `const { foo: { bar: baz } } = quux`,
* the path of `baz` is `foo.bar`
* @typedef {Object} DestructuredAssignment
* @property {import('estree').Identifier | import('estree').MemberExpression} node The node the destructuring path end in. Can be a member expression only for assignment expressions
* @property {ESTree.Identifier | ESTree.MemberExpression} node The node the destructuring path end in. Can be a member expression only for assignment expressions
* @property {boolean} is_rest `true` if this is a `...rest` destructuring
* @property {boolean} has_default_value `true` if this has a fallback value like `const { foo = 'bar } = ..`
* @property {(expression: import('estree').Expression) => import('estree').Identifier | import('estree').MemberExpression | import('estree').CallExpression | import('estree').AwaitExpression} expression Returns an expression which walks the path starting at the given expression.
* @property {(expression: ESTree.Expression) => ESTree.Identifier | ESTree.MemberExpression | ESTree.CallExpression | ESTree.AwaitExpression} expression Returns an expression which walks the path starting at the given expression.
* This will be a call expression if a rest element or default is involved e.g. `const { foo: { bar: baz = 42 }, ...rest } = quux` since we can't represent `baz` or `rest` purely as a path
* Will be an await expression in case of an async default value (`const { foo = await bar } = ...`)
* @property {(expression: import('estree').Expression) => import('estree').Identifier | import('estree').MemberExpression | import('estree').CallExpression | import('estree').AwaitExpression} update_expression Like `expression` but without default values.
* @property {(expression: ESTree.Expression) => ESTree.Identifier | ESTree.MemberExpression | ESTree.CallExpression | ESTree.AwaitExpression} update_expression Like `expression` but without default values.
*/
/**
* Extracts all destructured assignments from a pattern.
* @param {import('estree').Node} param
* @param {ESTree.Node} param
* @returns {DestructuredAssignment[]}
*/
export function extract_paths(param) {
return _extract_paths(
[],
param,
(node) => /** @type {import('estree').Identifier | import('estree').MemberExpression} */ (node),
(node) => /** @type {import('estree').Identifier | import('estree').MemberExpression} */ (node),
(node) => /** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node),
(node) => /** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node),
false
);
}
/**
* @param {DestructuredAssignment[]} assignments
* @param {import('estree').Node} param
* @param {ESTree.Node} param
* @param {DestructuredAssignment['expression']} expression
* @param {DestructuredAssignment['update_expression']} update_expression
* @param {boolean} has_default_value
@ -231,7 +231,7 @@ function _extract_paths(assignments = [], param, expression, update_expression,
if (prop.type === 'RestElement') {
/** @type {DestructuredAssignment['expression']} */
const rest_expression = (object) => {
/** @type {import('estree').Expression[]} */
/** @type {ESTree.Expression[]} */
const props = [];
for (const p of param.properties) {
if (p.type === 'Property' && p.key.type !== 'PrivateIdentifier') {
@ -351,7 +351,7 @@ function _extract_paths(assignments = [], param, expression, update_expression,
* Like `path.at(x)`, but skips over `TSNonNullExpression` and `TSAsExpression` nodes and eases assertions a bit
* by removing the `| undefined` from the resulting type.
*
* @template {import('#compiler').SvelteNode} T
* @template {SvelteNode} T
* @param {T[]} path
* @param {number} at
*/
@ -369,7 +369,7 @@ export function get_parent(path, at) {
* or a logical expression that only contains simple expressions. Used to determine whether
* something needs to be treated as though accessing it could have side-effects (i.e.
* reading signals prematurely)
* @param {import('estree').Expression} node
* @param {ESTree.Expression} node
* @returns {boolean}
*/
export function is_simple_expression(node) {
@ -398,8 +398,8 @@ export function is_simple_expression(node) {
}
/**
* @template {import('estree').SimpleCallExpression | import('estree').MemberExpression} T
* @param {import('estree').ChainExpression & { expression : T } | T} node
* @template {ESTree.SimpleCallExpression | ESTree.MemberExpression} T
* @param {ESTree.ChainExpression & { expression : T } | T} node
* @returns {T}
*/
export function unwrap_optional(node) {
@ -407,7 +407,7 @@ export function unwrap_optional(node) {
}
/**
* @param {import('estree').Expression | import('estree').Pattern} expression
* @param {ESTree.Expression | ESTree.Pattern} expression
* @returns {boolean}
*/
export function is_expression_async(expression) {

@ -1,35 +1,36 @@
/** @import * as ESTree from 'estree' */
import { regex_is_valid_identifier } from '../phases/patterns.js';
import { sanitize_template_string } from './sanitize_template_string.js';
/**
* @param {Array<import('estree').Expression | import('estree').SpreadElement | null>} elements
* @returns {import('estree').ArrayExpression}
* @param {Array<ESTree.Expression | ESTree.SpreadElement | null>} elements
* @returns {ESTree.ArrayExpression}
*/
export function array(elements = []) {
return { type: 'ArrayExpression', elements };
}
/**
* @param {Array<import('estree').Pattern | null>} elements
* @returns {import('estree').ArrayPattern}
* @param {Array<ESTree.Pattern | null>} elements
* @returns {ESTree.ArrayPattern}
*/
export function array_pattern(elements) {
return { type: 'ArrayPattern', elements };
}
/**
* @param {import('estree').Pattern} left
* @param {import('estree').Expression} right
* @returns {import('estree').AssignmentPattern}
* @param {ESTree.Pattern} left
* @param {ESTree.Expression} right
* @returns {ESTree.AssignmentPattern}
*/
export function assignment_pattern(left, right) {
return { type: 'AssignmentPattern', left, right };
}
/**
* @param {Array<import('estree').Pattern>} params
* @param {import('estree').BlockStatement | import('estree').Expression} body
* @returns {import('estree').ArrowFunctionExpression}
* @param {Array<ESTree.Pattern>} params
* @param {ESTree.BlockStatement | ESTree.Expression} body
* @returns {ESTree.ArrowFunctionExpression}
*/
export function arrow(params, body) {
return {
@ -44,10 +45,10 @@ export function arrow(params, body) {
}
/**
* @param {import('estree').AssignmentOperator} operator
* @param {import('estree').Pattern} left
* @param {import('estree').Expression} right
* @returns {import('estree').AssignmentExpression}
* @param {ESTree.AssignmentOperator} operator
* @param {ESTree.Pattern} left
* @param {ESTree.Expression} right
* @returns {ESTree.AssignmentExpression}
*/
export function assignment(operator, left, right) {
return { type: 'AssignmentExpression', operator, left, right };
@ -55,34 +56,34 @@ export function assignment(operator, left, right) {
/**
* @template T
* @param {T & import('estree').BaseFunction} func
* @returns {T & import('estree').BaseFunction}
* @param {T & ESTree.BaseFunction} func
* @returns {T & ESTree.BaseFunction}
*/
export function async(func) {
return { ...func, async: true };
}
/**
* @param {import('estree').Expression} argument
* @returns {import('estree').AwaitExpression}
* @param {ESTree.Expression} argument
* @returns {ESTree.AwaitExpression}
*/
function await_builder(argument) {
return { type: 'AwaitExpression', argument };
}
/**
* @param {import('estree').BinaryOperator} operator
* @param {import('estree').Expression} left
* @param {import('estree').Expression} right
* @returns {import('estree').BinaryExpression}
* @param {ESTree.BinaryOperator} operator
* @param {ESTree.Expression} left
* @param {ESTree.Expression} right
* @returns {ESTree.BinaryExpression}
*/
export function binary(operator, left, right) {
return { type: 'BinaryExpression', operator, left, right };
}
/**
* @param {import('estree').Statement[]} body
* @returns {import('estree').BlockStatement}
* @param {ESTree.Statement[]} body
* @returns {ESTree.BlockStatement}
*/
export function block(body) {
return { type: 'BlockStatement', body };
@ -90,17 +91,17 @@ export function block(body) {
/**
* @param {string} name
* @param {import('estree').Statement} body
* @returns {import('estree').LabeledStatement}
* @param {ESTree.Statement} body
* @returns {ESTree.LabeledStatement}
*/
export function labeled(name, body) {
return { type: 'LabeledStatement', label: id(name), body };
}
/**
* @param {string | import('estree').Expression} callee
* @param {...(import('estree').Expression | import('estree').SpreadElement | false | undefined)} args
* @returns {import('estree').CallExpression}
* @param {string | ESTree.Expression} callee
* @param {...(ESTree.Expression | ESTree.SpreadElement | false | undefined)} args
* @returns {ESTree.CallExpression}
*/
export function call(callee, ...args) {
if (typeof callee === 'string') callee = id(callee);
@ -124,20 +125,18 @@ export function call(callee, ...args) {
return {
type: 'CallExpression',
callee,
arguments: /** @type {Array<import('estree').Expression | import('estree').SpreadElement>} */ (
args
),
arguments: /** @type {Array<ESTree.Expression | ESTree.SpreadElement>} */ (args),
optional: false
};
}
/**
* @param {string | import('estree').Expression} callee
* @param {...import('estree').Expression} args
* @returns {import('estree').ChainExpression}
* @param {string | ESTree.Expression} callee
* @param {...ESTree.Expression} args
* @returns {ESTree.ChainExpression}
*/
export function maybe_call(callee, ...args) {
const expression = /** @type {import('estree').SimpleCallExpression} */ (call(callee, ...args));
const expression = /** @type {ESTree.SimpleCallExpression} */ (call(callee, ...args));
expression.optional = true;
return {
@ -147,29 +146,29 @@ export function maybe_call(callee, ...args) {
}
/**
* @param {import('estree').UnaryOperator} operator
* @param {import('estree').Expression} argument
* @returns {import('estree').UnaryExpression}
* @param {ESTree.UnaryOperator} operator
* @param {ESTree.Expression} argument
* @returns {ESTree.UnaryExpression}
*/
export function unary(operator, argument) {
return { type: 'UnaryExpression', argument, operator, prefix: true };
}
/**
* @param {import('estree').Expression} test
* @param {import('estree').Expression} consequent
* @param {import('estree').Expression} alternate
* @returns {import('estree').ConditionalExpression}
* @param {ESTree.Expression} test
* @param {ESTree.Expression} consequent
* @param {ESTree.Expression} alternate
* @returns {ESTree.ConditionalExpression}
*/
export function conditional(test, consequent, alternate) {
return { type: 'ConditionalExpression', test, consequent, alternate };
}
/**
* @param {import('estree').LogicalOperator} operator
* @param {import('estree').Expression} left
* @param {import('estree').Expression} right
* @returns {import('estree').LogicalExpression}
* @param {ESTree.LogicalOperator} operator
* @param {ESTree.Expression} left
* @param {ESTree.Expression} right
* @returns {ESTree.LogicalExpression}
*/
export function logical(operator, left, right) {
return { type: 'LogicalExpression', operator, left, right };
@ -177,9 +176,9 @@ export function logical(operator, left, right) {
/**
* @param {'const' | 'let' | 'var'} kind
* @param {string | import('estree').Pattern} pattern
* @param {import('estree').Expression} [init]
* @returns {import('estree').VariableDeclaration}
* @param {string | ESTree.Pattern} pattern
* @param {ESTree.Expression} [init]
* @returns {ESTree.VariableDeclaration}
*/
export function declaration(kind, pattern, init) {
if (typeof pattern === 'string') pattern = id(pattern);
@ -192,32 +191,32 @@ export function declaration(kind, pattern, init) {
}
/**
* @param {import('estree').Pattern} id
* @param {import('estree').Expression} [init]
* @returns {import('estree').VariableDeclarator}
* @param {ESTree.Pattern} id
* @param {ESTree.Expression} [init]
* @returns {ESTree.VariableDeclarator}
*/
export function declarator(id, init) {
return { type: 'VariableDeclarator', id, init };
}
/** @type {import('estree').EmptyStatement} */
/** @type {ESTree.EmptyStatement} */
export const empty = {
type: 'EmptyStatement'
};
/**
* @param {import('estree').Expression | import('estree').MaybeNamedClassDeclaration | import('estree').MaybeNamedFunctionDeclaration} declaration
* @returns {import('estree').ExportDefaultDeclaration}
* @param {ESTree.Expression | ESTree.MaybeNamedClassDeclaration | ESTree.MaybeNamedFunctionDeclaration} declaration
* @returns {ESTree.ExportDefaultDeclaration}
*/
export function export_default(declaration) {
return { type: 'ExportDefaultDeclaration', declaration };
}
/**
* @param {import('estree').Identifier} id
* @param {import('estree').Pattern[]} params
* @param {import('estree').BlockStatement} body
* @returns {import('estree').FunctionDeclaration}
* @param {ESTree.Identifier} id
* @param {ESTree.Pattern[]} params
* @param {ESTree.BlockStatement} body
* @returns {ESTree.FunctionDeclaration}
*/
export function function_declaration(id, params, body) {
return {
@ -233,8 +232,8 @@ export function function_declaration(id, params, body) {
/**
* @param {string} name
* @param {import('estree').Statement[]} body
* @returns {import('estree').Property & { value: import('estree').FunctionExpression}}}
* @param {ESTree.Statement[]} body
* @returns {ESTree.Property & { value: ESTree.FunctionExpression}}}
*/
export function get(name, body) {
return prop('get', key(name), function_builder(null, [], block(body)));
@ -242,7 +241,7 @@ export function get(name, body) {
/**
* @param {string} name
* @returns {import('estree').Identifier}
* @returns {ESTree.Identifier}
*/
export function id(name) {
return { type: 'Identifier', name };
@ -250,7 +249,7 @@ export function id(name) {
/**
* @param {string} name
* @returns {import('estree').PrivateIdentifier}
* @returns {ESTree.PrivateIdentifier}
*/
export function private_id(name) {
return { type: 'PrivateIdentifier', name };
@ -258,7 +257,7 @@ export function private_id(name) {
/**
* @param {string} local
* @returns {import('estree').ImportNamespaceSpecifier}
* @returns {ESTree.ImportNamespaceSpecifier}
*/
function import_namespace(local) {
return {
@ -269,8 +268,8 @@ function import_namespace(local) {
/**
* @param {string} name
* @param {import('estree').Expression} value
* @returns {import('estree').Property}
* @param {ESTree.Expression} value
* @returns {ESTree.Property}
*/
export function init(name, value) {
return prop('init', key(name), value);
@ -278,7 +277,7 @@ export function init(name, value) {
/**
* @param {string | boolean | null | number | RegExp} value
* @returns {import('estree').Literal}
* @returns {ESTree.Literal}
*/
export function literal(value) {
// @ts-expect-error we don't want to muck around with bigint here
@ -286,11 +285,11 @@ export function literal(value) {
}
/**
* @param {import('estree').Expression | import('estree').Super} object
* @param {import('estree').Expression | import('estree').PrivateIdentifier} property
* @param {ESTree.Expression | ESTree.Super} object
* @param {ESTree.Expression | ESTree.PrivateIdentifier} property
* @param {boolean} computed
* @param {boolean} optional
* @returns {import('estree').MemberExpression}
* @returns {ESTree.MemberExpression}
*/
export function member(object, property, computed = false, optional = false) {
return { type: 'MemberExpression', object, property, computed, optional };
@ -298,12 +297,12 @@ export function member(object, property, computed = false, optional = false) {
/**
* @param {string} path
* @returns {import('estree').Identifier | import('estree').MemberExpression}
* @returns {ESTree.Identifier | ESTree.MemberExpression}
*/
export function member_id(path) {
const parts = path.split('.');
/** @type {import('estree').Identifier | import('estree').MemberExpression} */
/** @type {ESTree.Identifier | ESTree.MemberExpression} */
let expression = id(parts[0]);
for (let i = 1; i < parts.length; i += 1) {
@ -313,39 +312,39 @@ export function member_id(path) {
}
/**
* @param {Array<import('estree').Property | import('estree').SpreadElement>} properties
* @returns {import('estree').ObjectExpression}
* @param {Array<ESTree.Property | ESTree.SpreadElement>} properties
* @returns {ESTree.ObjectExpression}
*/
export function object(properties) {
return { type: 'ObjectExpression', properties };
}
/**
* @param {Array<import('estree').RestElement | import('estree').AssignmentProperty>} properties
* @returns {import('estree').ObjectPattern}
* @param {Array<ESTree.RestElement | ESTree.AssignmentProperty>} properties
* @returns {ESTree.ObjectPattern}
*/
export function object_pattern(properties) {
return { type: 'ObjectPattern', properties };
}
/**
* @template {import('estree').Expression} Value
* @template {ESTree.Expression} Value
* @param {'init' | 'get' | 'set'} kind
* @param {import('estree').Expression} key
* @param {ESTree.Expression} key
* @param {Value} value
* @param {boolean} computed
* @returns {import('estree').Property & { value: Value }}
* @returns {ESTree.Property & { value: Value }}
*/
export function prop(kind, key, value, computed = false) {
return { type: 'Property', kind, key, value, method: false, shorthand: false, computed };
}
/**
* @param {import('estree').Expression | import('estree').PrivateIdentifier} key
* @param {import('estree').Expression | null | undefined} value
* @param {ESTree.Expression | ESTree.PrivateIdentifier} key
* @param {ESTree.Expression | null | undefined} value
* @param {boolean} computed
* @param {boolean} is_static
* @returns {import('estree').PropertyDefinition}
* @returns {ESTree.PropertyDefinition}
*/
export function prop_def(key, value, computed = false, is_static = false) {
return { type: 'PropertyDefinition', key, value, computed, static: is_static };
@ -354,7 +353,7 @@ export function prop_def(key, value, computed = false, is_static = false) {
/**
* @param {string} cooked
* @param {boolean} tail
* @returns {import('estree').TemplateElement}
* @returns {ESTree.TemplateElement}
*/
export function quasi(cooked, tail = false) {
const raw = sanitize_template_string(cooked);
@ -362,16 +361,16 @@ export function quasi(cooked, tail = false) {
}
/**
* @param {import('estree').Pattern} argument
* @returns {import('estree').RestElement}
* @param {ESTree.Pattern} argument
* @returns {ESTree.RestElement}
*/
export function rest(argument) {
return { type: 'RestElement', argument };
}
/**
* @param {import('estree').Expression[]} expressions
* @returns {import('estree').SequenceExpression}
* @param {ESTree.Expression[]} expressions
* @returns {ESTree.SequenceExpression}
*/
export function sequence(expressions) {
return { type: 'SequenceExpression', expressions };
@ -379,42 +378,42 @@ export function sequence(expressions) {
/**
* @param {string} name
* @param {import('estree').Statement[]} body
* @returns {import('estree').Property & { value: import('estree').FunctionExpression}}
* @param {ESTree.Statement[]} body
* @returns {ESTree.Property & { value: ESTree.FunctionExpression}}
*/
export function set(name, body) {
return prop('set', key(name), function_builder(null, [id('$$value')], block(body)));
}
/**
* @param {import('estree').Expression} argument
* @returns {import('estree').SpreadElement}
* @param {ESTree.Expression} argument
* @returns {ESTree.SpreadElement}
*/
export function spread(argument) {
return { type: 'SpreadElement', argument };
}
/**
* @param {import('estree').Expression} expression
* @returns {import('estree').ExpressionStatement}
* @param {ESTree.Expression} expression
* @returns {ESTree.ExpressionStatement}
*/
export function stmt(expression) {
return { type: 'ExpressionStatement', expression };
}
/**
* @param {import('estree').TemplateElement[]} elements
* @param {import('estree').Expression[]} expressions
* @returns {import('estree').TemplateLiteral}
* @param {ESTree.TemplateElement[]} elements
* @param {ESTree.Expression[]} expressions
* @returns {ESTree.TemplateLiteral}
*/
export function template(elements, expressions) {
return { type: 'TemplateLiteral', quasis: elements, expressions };
}
/**
* @param {import('estree').Expression | import('estree').BlockStatement} expression
* @param {ESTree.Expression | ESTree.BlockStatement} expression
* @param {boolean} [async]
* @returns {import('estree').Expression}
* @returns {ESTree.Expression}
*/
export function thunk(expression, async = false) {
if (
@ -434,9 +433,9 @@ export function thunk(expression, async = false) {
/**
*
* @param {string | import('estree').Expression} expression
* @param {...import('estree').Expression} args
* @returns {import('estree').NewExpression}
* @param {string | ESTree.Expression} expression
* @param {...ESTree.Expression} args
* @returns {ESTree.NewExpression}
*/
function new_builder(expression, ...args) {
if (typeof expression === 'string') expression = id(expression);
@ -449,19 +448,19 @@ function new_builder(expression, ...args) {
}
/**
* @param {import('estree').UpdateOperator} operator
* @param {import('estree').Expression} argument
* @param {ESTree.UpdateOperator} operator
* @param {ESTree.Expression} argument
* @param {boolean} prefix
* @returns {import('estree').UpdateExpression}
* @returns {ESTree.UpdateExpression}
*/
export function update(operator, argument, prefix = false) {
return { type: 'UpdateExpression', operator, argument, prefix };
}
/**
* @param {import('estree').Expression} test
* @param {import('estree').Statement} body
* @returns {import('estree').DoWhileStatement}
* @param {ESTree.Expression} test
* @param {ESTree.Statement} body
* @returns {ESTree.DoWhileStatement}
*/
export function do_while(test, body) {
return { type: 'DoWhileStatement', test, body };
@ -471,38 +470,38 @@ const true_instance = literal(true);
const false_instance = literal(false);
const null_instane = literal(null);
/** @type {import('estree').DebuggerStatement} */
/** @type {ESTree.DebuggerStatement} */
const debugger_builder = {
type: 'DebuggerStatement'
};
/** @type {import('estree').ThisExpression} */
/** @type {ESTree.ThisExpression} */
const this_instance = {
type: 'ThisExpression'
};
/**
* @param {string | import('estree').Pattern} pattern
* @param { import('estree').Expression} [init]
* @returns {import('estree').VariableDeclaration}
* @param {string | ESTree.Pattern} pattern
* @param { ESTree.Expression} [init]
* @returns {ESTree.VariableDeclaration}
*/
function let_builder(pattern, init) {
return declaration('let', pattern, init);
}
/**
* @param {string | import('estree').Pattern} pattern
* @param { import('estree').Expression} init
* @returns {import('estree').VariableDeclaration}
* @param {string | ESTree.Pattern} pattern
* @param { ESTree.Expression} init
* @returns {ESTree.VariableDeclaration}
*/
function const_builder(pattern, init) {
return declaration('const', pattern, init);
}
/**
* @param {string | import('estree').Pattern} pattern
* @param { import('estree').Expression} [init]
* @returns {import('estree').VariableDeclaration}
* @param {string | ESTree.Pattern} pattern
* @param { ESTree.Expression} [init]
* @returns {ESTree.VariableDeclaration}
*/
function var_builder(pattern, init) {
return declaration('var', pattern, init);
@ -510,11 +509,11 @@ function var_builder(pattern, init) {
/**
*
* @param {import('estree').VariableDeclaration | import('estree').Expression | null} init
* @param {import('estree').Expression} test
* @param {import('estree').Expression} update
* @param {import('estree').Statement} body
* @returns {import('estree').ForStatement}
* @param {ESTree.VariableDeclaration | ESTree.Expression | null} init
* @param {ESTree.Expression} test
* @param {ESTree.Expression} update
* @param {ESTree.Statement} body
* @returns {ESTree.ForStatement}
*/
function for_builder(init, test, update, body) {
return { type: 'ForStatement', init, test, update, body };
@ -523,12 +522,12 @@ function for_builder(init, test, update, body) {
/**
*
* @param {'constructor' | 'method' | 'get' | 'set'} kind
* @param {import('estree').Expression | import('estree').PrivateIdentifier} key
* @param {import('estree').Pattern[]} params
* @param {import('estree').Statement[]} body
* @param {ESTree.Expression | ESTree.PrivateIdentifier} key
* @param {ESTree.Pattern[]} params
* @param {ESTree.Statement[]} body
* @param {boolean} computed
* @param {boolean} is_static
* @returns {import('estree').MethodDefinition}
* @returns {ESTree.MethodDefinition}
*/
export function method(kind, key, params, body, computed = false, is_static = false) {
return {
@ -543,10 +542,10 @@ export function method(kind, key, params, body, computed = false, is_static = fa
/**
*
* @param {import('estree').Identifier | null} id
* @param {import('estree').Pattern[]} params
* @param {import('estree').BlockStatement} body
* @returns {import('estree').FunctionExpression}
* @param {ESTree.Identifier | null} id
* @param {ESTree.Pattern[]} params
* @param {ESTree.BlockStatement} body
* @returns {ESTree.FunctionExpression}
*/
function function_builder(id, params, body) {
return {
@ -561,10 +560,10 @@ function function_builder(id, params, body) {
}
/**
* @param {import('estree').Expression} test
* @param {import('estree').Statement} consequent
* @param {import('estree').Statement} [alternate]
* @returns {import('estree').IfStatement}
* @param {ESTree.Expression} test
* @param {ESTree.Statement} consequent
* @param {ESTree.Statement} [alternate]
* @returns {ESTree.IfStatement}
*/
function if_builder(test, consequent, alternate) {
return { type: 'IfStatement', test, consequent, alternate };
@ -573,7 +572,7 @@ function if_builder(test, consequent, alternate) {
/**
* @param {string} as
* @param {string} source
* @returns {import('estree').ImportDeclaration}
* @returns {ESTree.ImportDeclaration}
*/
export function import_all(as, source) {
return {
@ -586,7 +585,7 @@ export function import_all(as, source) {
/**
* @param {Array<[string, string]>} parts
* @param {string} source
* @returns {import('estree').ImportDeclaration}
* @returns {ESTree.ImportDeclaration}
*/
export function imports(parts, source) {
return {
@ -601,8 +600,8 @@ export function imports(parts, source) {
}
/**
* @param {import('estree').Expression | null} argument
* @returns {import('estree').ReturnStatement}
* @param {ESTree.Expression | null} argument
* @returns {ESTree.ReturnStatement}
*/
function return_builder(argument = null) {
return { type: 'ReturnStatement', argument };
@ -610,7 +609,7 @@ function return_builder(argument = null) {
/**
* @param {string} str
* @returns {import('estree').ThrowStatement}
* @returns {ESTree.ThrowStatement}
*/
export function throw_error(str) {
return {
@ -637,7 +636,7 @@ export {
/**
* @param {string} name
* @returns {import('estree').Expression}
* @returns {ESTree.Expression}
*/
export function key(name) {
return regex_is_valid_identifier.test(name) ? id(name) : literal(name);

@ -0,0 +1,105 @@
/** @import { Location } from 'locate-character' */
import * as state from '../state.js';
const regex_tabs = /^\t+/;
/**
* @param {string} str
*/
function tabs_to_spaces(str) {
return str.replace(regex_tabs, (match) => match.split('\t').join(' '));
}
/**
* @param {string} source
* @param {number} line
* @param {number} column
*/
function get_code_frame(source, line, column) {
const lines = source.split('\n');
const frame_start = Math.max(0, line - 2);
const frame_end = Math.min(line + 3, lines.length);
const digits = String(frame_end + 1).length;
return lines
.slice(frame_start, frame_end)
.map((str, i) => {
const is_error_line = frame_start + i === line;
const line_num = String(i + frame_start + 1).padStart(digits, ' ');
if (is_error_line) {
const indicator =
' '.repeat(digits + 2 + tabs_to_spaces(str.slice(0, column)).length) + '^';
return `${line_num}: ${tabs_to_spaces(str)}\n${indicator}`;
}
return `${line_num}: ${tabs_to_spaces(str)}`;
})
.join('\n');
}
/**
* @typedef {{
* code: string;
* message: string;
* filename?: string;
* start?: Location;
* end?: Location;
* position?: [number, number];
* frame?: string;
* }} ICompileDiagnostic */
/** @implements {ICompileDiagnostic} */
export class CompileDiagnostic extends Error {
name = 'CompileDiagnostic';
/**
* @param {string} code
* @param {string} message
* @param {[number, number] | undefined} position
*/
constructor(code, message, position) {
super(message);
this.code = code;
if (state.filename) {
this.filename = state.filename;
}
if (position) {
this.position = position;
this.start = state.locator(position[0]);
this.end = state.locator(position[1]);
if (this.start && this.end) {
this.frame = get_code_frame(state.source, this.start.line - 1, this.end.column);
}
}
}
toString() {
let out = `${this.code}: ${this.message}`;
if (this.filename) {
out += `\n${this.filename}`;
if (this.start) {
out += `:${this.start.line}:${this.start.column}`;
}
}
if (this.frame) {
out += `\n${this.frame}`;
}
return out;
}
toJSON() {
return {
code: this.code,
message: this.message,
filename: this.filename,
start: this.start,
end: this.end,
position: this.position,
frame: this.frame
};
}
}

@ -1,3 +1,8 @@
/** @import { ValidatedCompileOptions } from '#compiler' */
/** @import { Processed } from '../preprocess/public.js' */
/** @import { SourceMap } from 'magic-string' */
/** @import { Source } from '../preprocess/private.js' */
/** @import { DecodedSourceMap, SourceMapSegment, RawSourceMap } from '@ampproject/remapping' */
import remapping from '@ampproject/remapping';
import { push_array } from './push_array.js';
@ -10,7 +15,7 @@ function last_line_length(s) {
// mutate map in-place
/**
* @param {import('@ampproject/remapping').DecodedSourceMap} map
* @param {DecodedSourceMap} map
* @param {{ line: number; column: number; }} offset
* @param {number} source_index
*/
@ -74,13 +79,13 @@ export class MappedCode {
string = /** @type {any} */ (undefined);
/**
* @type {import('@ampproject/remapping').DecodedSourceMap}
* @type {DecodedSourceMap}
*/
map = /** @type {any} */ (undefined);
/**
* @param {string} string
* @param {import('@ampproject/remapping').DecodedSourceMap | null} map
* @param {DecodedSourceMap | null} map
*/
constructor(string = '', map = null) {
this.string = string;
@ -171,7 +176,7 @@ export class MappedCode {
// combine last line + first line
push_array(
m1.mappings[m1.mappings.length - 1],
/** @type {import('@ampproject/remapping').SourceMapSegment[]} */ (m2.mappings.shift())
/** @type {SourceMapSegment[]} */ (m2.mappings.shift())
);
// append other lines
push_array(m1.mappings, m2.mappings);
@ -181,7 +186,7 @@ export class MappedCode {
/**
* @static
* @param {string} string
* @param {import('@ampproject/remapping').DecodedSourceMap} [map]
* @param {DecodedSourceMap} [map]
* @returns {MappedCode}
*/
static from_processed(string, map) {
@ -205,7 +210,7 @@ export class MappedCode {
/**
* @static
* @param {import('../preprocess/private.js').Source} opts
* @param {Source} opts
* @returns {MappedCode}
*/
static from_source({ source, file_basename, get_location }) {
@ -216,7 +221,7 @@ export class MappedCode {
if (!offset) offset = { line: 0, column: 0 };
/**
* @type {import('@ampproject/remapping').DecodedSourceMap}
* @type {DecodedSourceMap}
*/
const map = { version: 3, names: [], sources: [file_basename], mappings: [] };
if (source == '') return new MappedCode(source, map);
@ -255,7 +260,7 @@ const b64dec =
/**
* @param {string} filename Basename of the input file
* @param {Array<import('@ampproject/remapping').DecodedSourceMap | import('@ampproject/remapping').RawSourceMap>} sourcemap_list
* @param {Array<DecodedSourceMap | RawSourceMap>} sourcemap_list
*/
export function combine_sourcemaps(filename, sourcemap_list) {
if (sourcemap_list.length == 0) return null;
@ -296,9 +301,9 @@ export function combine_sourcemaps(filename, sourcemap_list) {
/**
* @param {string} filename
* @param {import('magic-string').SourceMap} svelte_map
* @param {string | import('@ampproject/remapping').DecodedSourceMap | import('@ampproject/remapping').RawSourceMap} preprocessor_map_input
* @returns {import('magic-string').SourceMap}
* @param {SourceMap} svelte_map
* @param {string | DecodedSourceMap | RawSourceMap} preprocessor_map_input
* @returns {SourceMap}
*/
function apply_preprocessor_sourcemap(filename, svelte_map, preprocessor_map_input) {
if (!svelte_map || !preprocessor_map_input) return svelte_map;
@ -323,13 +328,13 @@ function apply_preprocessor_sourcemap(filename, svelte_map, preprocessor_map_inp
}
}
});
return /** @type {import('magic-string').SourceMap} */ (result_map);
return /** @type {SourceMap} */ (result_map);
}
const regex_data_uri = /data:(?:application|text)\/json;(?:charset[:=]\S+?;)?base64,(\S*)/;
// parse attached sourcemap in processed.code
/**
* @param {import('../preprocess/public.js').Processed} processed
* @param {Processed} processed
* @param {'script' | 'style'} tag_name
* @returns {void}
*/
@ -382,8 +387,8 @@ export function parse_attached_sourcemap(processed, tag_name) {
}
/**
* @param {{ code: string, map: import('magic-string').SourceMap}} result
* @param {import('#compiler').ValidatedCompileOptions} options
* @param {{ code: string, map: SourceMap}} result
* @param {ValidatedCompileOptions} options
* @param {string} source_name
*/
export function merge_with_preprocessor_map(result, options, source_name) {

@ -1,3 +1,4 @@
/** @import { ModuleCompileOptions, ValidatedModuleCompileOptions, CompileOptions, ValidatedCompileOptions } from '#compiler' */
import * as e from './errors.js';
import * as w from './warnings.js';
@ -32,14 +33,14 @@ const common = {
};
export const validate_module_options =
/** @type {Validator<import('#compiler').ModuleCompileOptions, import('#compiler').ValidatedModuleCompileOptions>} */ (
/** @type {Validator<ModuleCompileOptions, ValidatedModuleCompileOptions>} */ (
object({
...common
})
);
export const validate_component_options =
/** @type {Validator<import('#compiler').CompileOptions, import('#compiler').ValidatedCompileOptions>} */ (
/** @type {Validator<CompileOptions, ValidatedCompileOptions>} */ (
object({
...common,

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save