chore: fix build dependencies and frontend config

- Bump Docker SDK, downgrade otel deps for Go 1.24 compatibility
- Fix duplicate i18n import in InstanceCard
- Fix SvelteKit prerender/strict config for SPA build
- Update Dockerfile with GOTOOLCHAIN=auto
- Generate package-lock.json and go.sum

WIP: still resolving Go 1.24 vs otel transitive dep versions
This commit is contained in:
2026-03-28 00:38:18 +03:00
parent 1f81ca9eb0
commit f0b52c6ab7
1849 changed files with 994396 additions and 8 deletions
+7
View File
@@ -0,0 +1,7 @@
Copyright (c) 2016-2025 [Svelte Contributors](https://github.com/sveltejs/svelte/graphs/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+41
View File
@@ -0,0 +1,41 @@
<a href="https://svelte.dev">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="../../assets/banner_dark.png">
<img src="../../assets/banner.png" alt="Svelte - web development for the rest of us" />
</picture>
</a>
[![npm version](https://img.shields.io/npm/v/svelte.svg)](https://www.npmjs.com/package/svelte) [![license](https://img.shields.io/npm/l/svelte.svg)](LICENSE.md) [![Chat](https://img.shields.io/discord/457912077277855764?label=chat&logo=discord)](https://svelte.dev/chat)
## What is Svelte?
Svelte is a new way to build web applications. It's a compiler that takes your declarative components and converts them into efficient JavaScript that surgically updates the DOM.
Learn more at the [Svelte website](https://svelte.dev), or stop by the [Discord chatroom](https://svelte.dev/chat).
## Getting started
You can play around with Svelte in the [tutorial](https://svelte.dev/tutorial), [examples](https://svelte.dev/examples), and [REPL](https://svelte.dev/repl).
When you're ready to build a full-fledge application, we recommend using [SvelteKit](https://svelte.dev/docs/kit):
```sh
npx sv create my-app
cd my-app
npm install
npm run dev
```
See [the SvelteKit documentation](https://svelte.dev/docs/kit) to learn more.
## Changelog
[The Changelog for this package is available on GitHub](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md).
## Supporting Svelte
Svelte is an MIT-licensed open source project with its ongoing development made possible entirely by fantastic volunteers. If you'd like to support their efforts, please consider:
- [Becoming a backer on Open Collective](https://opencollective.com/svelte).
Funds donated via Open Collective will be used for compensating expenses related to Svelte's development.
+1
View File
@@ -0,0 +1 @@
import './types/index.js';
+1
View File
@@ -0,0 +1 @@
import './types/index.js';
+1
View File
@@ -0,0 +1 @@
import './types/index.js';
+1
View File
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
{
"type": "commonjs"
}
+1
View File
@@ -0,0 +1 @@
import './types/index.js';
+2078
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
import './types/index.js';
+1
View File
@@ -0,0 +1 @@
import './types/index.js';
+1
View File
@@ -0,0 +1 @@
import './types/index.js';
+186
View File
@@ -0,0 +1,186 @@
{
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.55.0",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
"node": ">=18"
},
"files": [
"*.d.ts",
"src",
"!src/**/*.test.*",
"!src/**/*.d.ts",
"types",
"compiler",
"README.md"
],
"module": "src/index-client.js",
"main": "src/index-client.js",
"exports": {
".": {
"types": "./types/index.d.ts",
"worker": "./src/index-server.js",
"browser": "./src/index-client.js",
"default": "./src/index-server.js"
},
"./package.json": "./package.json",
"./action": {
"types": "./types/index.d.ts"
},
"./animate": {
"types": "./types/index.d.ts",
"default": "./src/animate/index.js"
},
"./attachments": {
"types": "./types/index.d.ts",
"default": "./src/attachments/index.js"
},
"./compiler": {
"types": "./types/index.d.ts",
"require": "./compiler/index.js",
"default": "./src/compiler/index.js"
},
"./easing": {
"types": "./types/index.d.ts",
"default": "./src/easing/index.js"
},
"./elements": {
"types": "./elements.d.ts"
},
"./internal": {
"default": "./src/internal/index.js"
},
"./internal/client": {
"default": "./src/internal/client/index.js"
},
"./internal/disclose-version": {
"default": "./src/internal/disclose-version.js"
},
"./internal/flags/async": {
"default": "./src/internal/flags/async.js"
},
"./internal/flags/legacy": {
"default": "./src/internal/flags/legacy.js"
},
"./internal/flags/tracing": {
"default": "./src/internal/flags/tracing.js"
},
"./internal/server": {
"default": "./src/internal/server/index.js"
},
"./legacy": {
"types": "./types/index.d.ts",
"worker": "./src/legacy/legacy-server.js",
"browser": "./src/legacy/legacy-client.js",
"default": "./src/legacy/legacy-server.js"
},
"./motion": {
"types": "./types/index.d.ts",
"default": "./src/motion/index.js"
},
"./reactivity": {
"types": "./types/index.d.ts",
"worker": "./src/reactivity/index-server.js",
"browser": "./src/reactivity/index-client.js",
"default": "./src/reactivity/index-server.js"
},
"./reactivity/window": {
"types": "./types/index.d.ts",
"default": "./src/reactivity/window/index.js"
},
"./server": {
"types": "./types/index.d.ts",
"default": "./src/server/index.js"
},
"./store": {
"types": "./types/index.d.ts",
"worker": "./src/store/index-server.js",
"browser": "./src/store/index-client.js",
"default": "./src/store/index-server.js"
},
"./transition": {
"types": "./types/index.d.ts",
"default": "./src/transition/index.js"
},
"./events": {
"types": "./types/index.d.ts",
"default": "./src/events/index.js"
}
},
"imports": {
"#client": "./src/internal/client/types.d.ts",
"#client/constants": "./src/internal/client/constants.js",
"#compiler": {
"types": "./src/compiler/private.d.ts",
"default": "./src/compiler/index.js"
},
"#compiler/builders": "./src/compiler/utils/builders.js",
"#server": "./src/internal/server/types.d.ts",
"#shared": "./src/internal/shared/types.d.ts"
},
"repository": {
"type": "git",
"url": "git+https://github.com/sveltejs/svelte.git",
"directory": "packages/svelte"
},
"bugs": {
"url": "https://github.com/sveltejs/svelte/issues"
},
"homepage": "https://svelte.dev",
"keywords": [
"svelte",
"UI",
"framework",
"templates",
"templating"
],
"devDependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"@playwright/test": "^1.58.0",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-virtual": "^3.0.2",
"@types/aria-query": "^5.0.4",
"@types/node": "^20.11.5",
"dts-buddy": "^0.5.5",
"esbuild": "^0.25.10",
"rollup": "^4.59.0",
"source-map": "^0.7.4",
"tinyglobby": "^0.2.12",
"typescript": "^5.5.4",
"vitest": "^2.1.9"
},
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5",
"@types/trusted-types": "^2.0.7",
"acorn": "^8.12.1",
"aria-query": "5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"devalue": "^5.6.4",
"esm-env": "^1.2.1",
"esrap": "^2.2.2",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",
"zimmerframe": "^1.1.2"
},
"scripts": {
"build": "rollup -c && pnpm generate",
"dev": "node scripts/process-messages -w & rollup -cw",
"check": "tsc --project tsconfig.runtime.json && tsc && cd ./tests/types && tsc",
"check:tsgo": "tsgo --project tsconfig.runtime.json --skipLibCheck && tsgo --skipLibCheck",
"check:watch": "tsc --watch",
"generate": "node scripts/process-messages && node ./scripts/generate-types.js",
"generate:version": "node ./scripts/generate-version.js",
"generate:types": "node ./scripts/generate-types.js && tsc -p tsconfig.generated.json",
"knip": "pnpm dlx knip"
}
}
+78
View File
@@ -0,0 +1,78 @@
/** @import { FlipParams, AnimationConfig } from './public.js' */
import { cubicOut } from '../easing/index.js';
/**
* The flip function calculates the start and end position of an element and animates between them, translating the x and y values.
* `flip` stands for [First, Last, Invert, Play](https://aerotwist.com/blog/flip-your-animations/).
*
* @param {Element} node
* @param {{ from: DOMRect; to: DOMRect }} fromTo
* @param {FlipParams} params
* @returns {AnimationConfig}
*/
export function flip(node, { from, to }, params = {}) {
var { delay = 0, duration = (d) => Math.sqrt(d) * 120, easing = cubicOut } = params;
var style = getComputedStyle(node);
// find the transform origin, expressed as a pair of values between 0 and 1
var transform = style.transform === 'none' ? '' : style.transform;
var [ox, oy] = style.transformOrigin.split(' ').map(parseFloat);
ox /= node.clientWidth;
oy /= node.clientHeight;
// calculate effect of parent transforms and zoom
var zoom = get_zoom(node); // https://drafts.csswg.org/css-viewport/#effective-zoom
var sx = node.clientWidth / to.width / zoom;
var sy = node.clientHeight / to.height / zoom;
// find the starting position of the transform origin
var fx = from.left + from.width * ox;
var fy = from.top + from.height * oy;
// find the ending position of the transform origin
var tx = to.left + to.width * ox;
var ty = to.top + to.height * oy;
// find the translation at the start of the transform
var dx = (fx - tx) * sx;
var dy = (fy - ty) * sy;
// find the relative scale at the start of the transform
var dsx = from.width / to.width;
var dsy = from.height / to.height;
return {
delay,
duration: typeof duration === 'function' ? duration(Math.sqrt(dx * dx + dy * dy)) : duration,
easing,
css: (t, u) => {
var x = u * dx;
var y = u * dy;
var sx = t + u * dsx;
var sy = t + u * dsy;
return `transform: ${transform} translate(${x}px, ${y}px) scale(${sx}, ${sy});`;
}
};
}
/**
* @param {Element} element
*/
function get_zoom(element) {
if ('currentCSSZoom' in element) {
return /** @type {number} */ (element.currentCSSZoom);
}
/** @type {Element | null} */
var current = element;
var zoom = 1;
while (current !== null) {
zoom *= +getComputedStyle(current).zoom;
current = /** @type {Element | null} */ (current.parentElement);
}
return zoom;
}
+113
View File
@@ -0,0 +1,113 @@
/** @import { Action, ActionReturn } from '../action/public' */
/** @import { Attachment } from './public' */
import { noop, render_effect } from 'svelte/internal/client';
import { ATTACHMENT_KEY } from '../constants.js';
import { untrack } from '../index-client.js';
import { teardown } from '../internal/client/reactivity/effects.js';
/**
* Creates an object key that will be recognised as an attachment when the object is spread onto an element,
* as a programmatic alternative to using `{@attach ...}`. This can be useful for library authors, though
* is generally not needed when building an app.
*
* ```svelte
* <script>
* import { createAttachmentKey } from 'svelte/attachments';
*
* const props = {
* class: 'cool',
* onclick: () => alert('clicked'),
* [createAttachmentKey()]: (node) => {
* node.textContent = 'attached!';
* }
* };
* </script>
*
* <button {...props}>click me</button>
* ```
* @since 5.29
*/
export function createAttachmentKey() {
return Symbol(ATTACHMENT_KEY);
}
/**
* Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior.
* It's useful if you want to start using attachments on components but you have actions provided by a library.
*
* Note that the second argument, if provided, must be a function that _returns_ the argument to the
* action function, not the argument itself.
*
* ```svelte
* <!-- with an action -->
* <div use:foo={bar}>...</div>
*
* <!-- with an attachment -->
* <div {@attach fromAction(foo, () => bar)}>...</div>
* ```
* @template {EventTarget} E
* @template {unknown} T
* @overload
* @param {Action<E, T> | ((element: E, arg: T) => void | ActionReturn<T>)} action The action function
* @param {() => T} fn A function that returns the argument for the action
* @returns {Attachment<E>}
*/
/**
* Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior.
* It's useful if you want to start using attachments on components but you have actions provided by a library.
*
* Note that the second argument, if provided, must be a function that _returns_ the argument to the
* action function, not the argument itself.
*
* ```svelte
* <!-- with an action -->
* <div use:foo={bar}>...</div>
*
* <!-- with an attachment -->
* <div {@attach fromAction(foo, () => bar)}>...</div>
* ```
* @template {EventTarget} E
* @overload
* @param {Action<E, void> | ((element: E) => void | ActionReturn<void>)} action The action function
* @returns {Attachment<E>}
*/
/**
* Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior.
* It's useful if you want to start using attachments on components but you have actions provided by a library.
*
* Note that the second argument, if provided, must be a function that _returns_ the argument to the
* action function, not the argument itself.
*
* ```svelte
* <!-- with an action -->
* <div use:foo={bar}>...</div>
*
* <!-- with an attachment -->
* <div {@attach fromAction(foo, () => bar)}>...</div>
* ```
*
* @template {EventTarget} E
* @template {unknown} T
* @param {Action<E, T> | ((element: E, arg: T) => void | ActionReturn<T>)} action The action function
* @param {() => T} fn A function that returns the argument for the action
* @returns {Attachment<E>}
* @since 5.32
*/
export function fromAction(action, fn = /** @type {() => T} */ (noop)) {
return (element) => {
const { update, destroy } = untrack(() => action(element, fn()) ?? {});
if (update) {
var ran = false;
render_effect(() => {
const arg = fn();
if (ran) update(arg);
});
ran = true;
}
if (destroy) {
teardown(destroy);
}
};
}
+1719
View File
File diff suppressed because it is too large Load Diff
+201
View File
@@ -0,0 +1,201 @@
/** @import { LegacyRoot } from './types/legacy-nodes.js' */
/** @import { CompileOptions, CompileResult, ValidatedCompileOptions, ModuleCompileOptions } from '#compiler' */
/** @import { AST } from './public.js' */
import { walk as zimmerframe_walk } from 'zimmerframe';
import { convert } from './legacy.js';
import { parse as _parse, Parser } from './phases/1-parse/index.js';
import { remove_typescript_nodes } from './phases/1-parse/remove_typescript_nodes.js';
import { parse_stylesheet } from './phases/1-parse/read/style.js';
import { analyze_component, analyze_module } from './phases/2-analyze/index.js';
import { transform_component, transform_module } from './phases/3-transform/index.js';
import { validate_component_options, validate_module_options } from './validate-options.js';
import * as state from './state.js';
export { default as preprocess } from './preprocess/index.js';
export { print } from './print/index.js';
/**
* `compile` converts your `.svelte` source code into a JavaScript module that exports a component
*
* @param {string} source The component source code
* @param {CompileOptions} options The compiler options
* @returns {CompileResult}
*/
export function compile(source, options) {
source = remove_bom(source);
state.reset({ warning: options.warningFilter, filename: options.filename });
const validated = validate_component_options(options, '');
let parsed = _parse(source);
const { customElement: customElementOptions, ...parsed_options } = parsed.options || {};
/** @type {ValidatedCompileOptions} */
const combined_options = {
...validated,
...parsed_options,
customElementOptions,
css: 'css' in parsed_options ? () => parsed_options.css ?? 'external' : validated.css,
runes: 'runes' in parsed_options ? () => parsed_options.runes : validated.runes
};
if (parsed.metadata.ts) {
parsed = {
...parsed,
fragment: parsed.fragment && remove_typescript_nodes(parsed.fragment),
instance: parsed.instance && remove_typescript_nodes(parsed.instance),
module: parsed.module && remove_typescript_nodes(parsed.module)
};
if (combined_options.customElementOptions?.extend) {
combined_options.customElementOptions.extend = remove_typescript_nodes(
combined_options.customElementOptions?.extend
);
}
}
const analysis = analyze_component(parsed, source, combined_options);
const result = transform_component(analysis, source, combined_options);
result.ast = to_public_ast(source, parsed, options.modernAst);
return result;
}
/**
* `compileModule` takes your JavaScript source code containing runes, and turns it into a JavaScript module.
*
* @param {string} source The component source code
* @param {ModuleCompileOptions} options
* @returns {CompileResult}
*/
export function compileModule(source, options) {
source = remove_bom(source);
state.reset({ warning: options.warningFilter, filename: options.filename });
const validated = validate_module_options(options, '');
const analysis = analyze_module(source, validated);
return transform_module(analysis, source, validated);
}
/**
* The parse function parses a component, returning only its abstract syntax tree.
*
* The `modern` option (`false` by default in Svelte 5) makes the parser return a modern AST instead of the legacy AST.
* `modern` will become `true` by default in Svelte 6, and the option will be removed in Svelte 7.
*
* @overload
* @param {string} source
* @param {{ filename?: string; modern: true; loose?: boolean }} options
* @returns {AST.Root}
*/
/**
* The parse function parses a component, returning only its abstract syntax tree.
*
* The `modern` option (`false` by default in Svelte 5) makes the parser return a modern AST instead of the legacy AST.
* `modern` will become `true` by default in Svelte 6, and the option will be removed in Svelte 7.
*
* @overload
* @param {string} source
* @param {{ filename?: string; modern?: false; loose?: boolean }} [options]
* @returns {Record<string, any>}
*/
// TODO 6.0 remove unused `filename`
/**
* The parse function parses a component, returning only its abstract syntax tree.
*
* The `modern` option (`false` by default in Svelte 5) makes the parser return a modern AST instead of the legacy AST.
* `modern` will become `true` by default in Svelte 6, and the option will be removed in Svelte 7.
*
* The `loose` option, available since 5.13.0, tries to always return an AST even if the input will not successfully compile.
*
* The `filename` option is unused and will be removed in Svelte 6.0.
*
* @param {string} source
* @param {{ filename?: string; rootDir?: string; modern?: boolean; loose?: boolean }} [options]
* @returns {AST.Root | LegacyRoot}
*/
export function parse(source, { modern, loose } = {}) {
source = remove_bom(source);
state.reset({ warning: () => false, filename: undefined });
const ast = _parse(source, loose);
return to_public_ast(source, ast, modern);
}
/**
* The parseCss function parses a CSS stylesheet, returning its abstract syntax tree.
*
* @param {string} source The CSS source code
* @returns {AST.CSS.StyleSheetFile}
*/
export function parseCss(source) {
source = remove_bom(source);
state.reset({ warning: () => false, filename: undefined });
state.set_source(source);
const parser = Parser.forCss(source);
const children = parse_stylesheet(parser);
return {
type: 'StyleSheetFile',
start: 0,
end: source.length,
children
};
}
/**
* @param {string} source
* @param {AST.Root} ast
* @param {boolean | undefined} modern
*/
function to_public_ast(source, ast, modern) {
if (modern) {
const clean = (/** @type {any} */ node) => {
delete node.metadata;
};
ast.options?.attributes.forEach((attribute) => {
clean(attribute);
clean(attribute.value);
if (Array.isArray(attribute.value)) {
attribute.value.forEach(clean);
}
});
// remove things that we don't want to treat as public API
return zimmerframe_walk(ast, null, {
_(node, { next }) {
clean(node);
next();
}
});
}
return convert(source, ast);
}
/**
* Remove the byte order mark from a string if it's present since it would mess with our template generation logic
* @param {string} source
*/
function remove_bom(source) {
if (source.charCodeAt(0) === 0xfeff) {
return source.slice(1);
}
return source;
}
/**
* @deprecated Replace this with `import { walk } from 'estree-walker'`
* @returns {never}
*/
export function walk() {
throw new Error(
`'svelte/compiler' no longer exports a \`walk\` utility — please import it directly from 'estree-walker' instead`
);
}
export { VERSION } from '../version.js';
export { migrate } from './migrate/index.js';
+637
View File
@@ -0,0 +1,637 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import * as Legacy from './types/legacy-nodes.js' */
import { walk } from 'zimmerframe';
import {
regex_ends_with_whitespaces,
regex_not_whitespace,
regex_starts_with_whitespaces
} from './phases/patterns.js';
import { extract_svelte_ignore } from './utils/extract_svelte_ignore.js';
/**
* Some of the legacy Svelte AST nodes remove whitespace from the start and end of their children.
* @param {AST.TemplateNode[]} nodes
*/
function remove_surrounding_whitespace_nodes(nodes) {
const first = nodes.at(0);
const last = nodes.at(-1);
if (first?.type === 'Text') {
if (!regex_not_whitespace.test(first.data)) {
nodes.shift();
} else {
first.data = first.data.replace(regex_starts_with_whitespaces, '');
}
}
if (last?.type === 'Text') {
if (!regex_not_whitespace.test(last.data)) {
nodes.pop();
} else {
last.data = last.data.replace(regex_ends_with_whitespaces, '');
}
}
}
/**
* Transform our nice modern AST into the monstrosity emitted by Svelte 4
* @param {string} source
* @param {AST.Root} ast
* @returns {Legacy.LegacyRoot}
*/
export function convert(source, ast) {
const root = /** @type {AST.SvelteNode | Legacy.LegacySvelteNode} */ (ast);
return /** @type {Legacy.LegacyRoot} */ (
walk(root, null, {
_(node, { next }) {
// @ts-ignore
delete node.metadata;
next();
},
// @ts-ignore
Root(node, { visit }) {
const { instance, module, options } = node;
// Insert svelte:options back into the root nodes
if (/** @type {any} */ (options)?.__raw__) {
let idx = node.fragment.nodes.findIndex(
(node) => /** @type {any} */ (options).end <= node.start
);
if (idx === -1) {
idx = node.fragment.nodes.length;
}
node.fragment.nodes.splice(idx, 0, /** @type {any} */ (options).__raw__);
}
/** @type {number | null} */
let start = null;
/** @type {number | null} */
let end = null;
if (node.fragment.nodes.length > 0) {
const first = /** @type {AST.BaseNode} */ (node.fragment.nodes.at(0));
const last = /** @type {AST.BaseNode} */ (node.fragment.nodes.at(-1));
start = first.start;
end = last.end;
while (/\s/.test(source[start])) start += 1;
while (/\s/.test(source[end - 1])) end -= 1;
}
if (instance) {
// @ts-ignore
delete instance.attributes;
}
if (module) {
// @ts-ignore
delete module.attributes;
}
return {
html: {
type: 'Fragment',
start,
end,
children: node.fragment.nodes.map((child) => visit(child))
},
instance,
module,
css: ast.css ? visit(ast.css) : undefined,
// put it on _comments not comments because the latter is checked by prettier and then fails
// if we don't adjust stuff accordingly in our prettier plugin, and so it would be kind of an
// indirect breaking change for people updating their Svelte version but not their prettier plugin version.
// We can keep it as comments for the modern AST because the modern AST is not used in the plugin yet.
_comments: ast.comments?.length > 0 ? ast.comments : undefined
};
},
AnimateDirective(node) {
return { ...node, type: 'Animation' };
},
// @ts-ignore
AwaitBlock(node, { visit }) {
let pendingblock = {
type: 'PendingBlock',
/** @type {number | null} */
start: null,
/** @type {number | null} */
end: null,
children: node.pending?.nodes.map((child) => visit(child)) ?? [],
skip: true
};
let thenblock = {
type: 'ThenBlock',
/** @type {number | null} */
start: null,
/** @type {number | null} */
end: null,
children: node.then?.nodes.map((child) => visit(child)) ?? [],
skip: true
};
let catchblock = {
type: 'CatchBlock',
/** @type {number | null} */
start: null,
/** @type {number | null} */
end: null,
children: node.catch?.nodes.map((child) => visit(child)) ?? [],
skip: true
};
if (node.pending) {
const first = node.pending.nodes.at(0);
const last = node.pending.nodes.at(-1);
pendingblock.start = first?.start ?? source.indexOf('}', node.expression.end) + 1;
pendingblock.end = last?.end ?? pendingblock.start;
pendingblock.skip = false;
}
if (node.then) {
const first = node.then.nodes.at(0);
const last = node.then.nodes.at(-1);
thenblock.start =
pendingblock.end ?? first?.start ?? source.indexOf('}', node.expression.end) + 1;
thenblock.end =
last?.end ?? source.lastIndexOf('}', pendingblock.end ?? node.expression.end) + 1;
thenblock.skip = false;
}
if (node.catch) {
const first = node.catch.nodes.at(0);
const last = node.catch.nodes.at(-1);
catchblock.start =
thenblock.end ??
pendingblock.end ??
first?.start ??
source.indexOf('}', node.expression.end) + 1;
catchblock.end =
last?.end ??
source.lastIndexOf('}', thenblock.end ?? pendingblock.end ?? node.expression.end) + 1;
catchblock.skip = false;
}
return {
type: 'AwaitBlock',
start: node.start,
end: node.end,
expression: node.expression,
value: node.value,
error: node.error,
pending: pendingblock,
then: thenblock,
catch: catchblock
};
},
BindDirective(node) {
return { ...node, type: 'Binding' };
},
ClassDirective(node) {
return { ...node, type: 'Class' };
},
Comment(node) {
return {
...node,
ignores: extract_svelte_ignore(node.start, node.data, false)
};
},
ComplexSelector(node, { next }) {
next(); // delete inner metadata/parent properties
const children = [];
for (const child of node.children) {
if (child.combinator) {
children.push(child.combinator);
}
children.push(...child.selectors);
}
return {
type: 'Selector',
start: node.start,
end: node.end,
children
};
},
Component(node, { visit }) {
return {
type: 'InlineComponent',
start: node.start,
end: node.end,
name: node.name,
attributes: node.attributes.map(
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
),
children: node.fragment.nodes.map(
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
)
};
},
// @ts-ignore
ConstTag(node) {
if (/** @type {Legacy.LegacyConstTag} */ (node).expression !== undefined) {
return node;
}
const modern_node = /** @type {AST.ConstTag} */ (node);
const { id: left } = { ...modern_node.declaration.declarations[0] };
// @ts-ignore
delete left.typeAnnotation;
return {
type: 'ConstTag',
start: modern_node.start,
end: node.end,
expression: {
type: 'AssignmentExpression',
start: (modern_node.declaration.start ?? 0) + 'const '.length,
end: modern_node.declaration.end ?? 0,
operator: '=',
left,
right: modern_node.declaration.declarations[0].init
}
};
},
// @ts-ignore
KeyBlock(node, { visit }) {
remove_surrounding_whitespace_nodes(node.fragment.nodes);
return {
type: 'KeyBlock',
start: node.start,
end: node.end,
expression: node.expression,
children: node.fragment.nodes.map(
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
)
};
},
// @ts-ignore
EachBlock(node, { visit }) {
let elseblock = undefined;
if (node.fallback) {
const first = node.fallback.nodes.at(0);
const end = source.lastIndexOf('{', /** @type {number} */ (node.end) - 1);
const start = first?.start ?? end;
remove_surrounding_whitespace_nodes(node.fallback.nodes);
elseblock = {
type: 'ElseBlock',
start,
end,
children: node.fallback.nodes.map((child) => visit(child))
};
}
remove_surrounding_whitespace_nodes(node.body.nodes);
return {
type: 'EachBlock',
start: node.start,
end: node.end,
children: node.body.nodes.map((child) => visit(child)),
context: node.context,
expression: node.expression,
index: node.index,
key: node.key,
else: elseblock
};
},
ExpressionTag(node, { path }) {
const parent = path.at(-1);
if (parent?.type === 'Attribute') {
if (source[parent.start] === '{') {
return {
type: 'AttributeShorthand',
start: node.start,
end: node.end,
expression: node.expression
};
}
}
return {
type: 'MustacheTag',
start: node.start,
end: node.end,
expression: node.expression
};
},
HtmlTag(node) {
return { ...node, type: 'RawMustacheTag' };
},
// @ts-ignore
IfBlock(node, { visit }) {
let elseblock = undefined;
if (node.alternate) {
let nodes = node.alternate.nodes;
if (nodes.length === 1 && nodes[0].type === 'IfBlock' && nodes[0].elseif) {
nodes = nodes[0].consequent.nodes;
}
const end = source.lastIndexOf('{', /** @type {number} */ (node.end) - 1);
const start = nodes.at(0)?.start ?? end;
remove_surrounding_whitespace_nodes(node.alternate.nodes);
elseblock = {
type: 'ElseBlock',
start,
end: end,
children: node.alternate.nodes.map(
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
)
};
}
const start = node.elseif
? node.consequent.nodes[0]?.start ??
source.lastIndexOf('{', /** @type {number} */ (node.end) - 1)
: node.start;
remove_surrounding_whitespace_nodes(node.consequent.nodes);
return {
type: 'IfBlock',
start,
end: node.end,
expression: node.test,
children: node.consequent.nodes.map(
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
),
else: elseblock,
elseif: node.elseif ? true : undefined
};
},
OnDirective(node) {
return { ...node, type: 'EventHandler' };
},
// @ts-expect-error
SnippetBlock(node, { visit }) {
remove_surrounding_whitespace_nodes(node.body.nodes);
return {
type: 'SnippetBlock',
start: node.start,
end: node.end,
expression: node.expression,
parameters: node.parameters,
children: node.body.nodes.map((child) => visit(child)),
typeParams: node.typeParams
};
},
// @ts-expect-error
SvelteBoundary(node, { visit }) {
remove_surrounding_whitespace_nodes(node.fragment.nodes);
return {
type: 'SvelteBoundary',
name: 'svelte:boundary',
start: node.start,
end: node.end,
attributes: node.attributes.map(
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
),
children: node.fragment.nodes.map((child) => visit(child))
};
},
RegularElement(node, { visit }) {
return {
type: 'Element',
start: node.start,
end: node.end,
name: node.name,
attributes: node.attributes.map((child) => visit(child)),
children: node.fragment.nodes.map((child) => visit(child))
};
},
SlotElement(node, { visit }) {
return {
type: 'Slot',
start: node.start,
end: node.end,
name: node.name,
attributes: node.attributes.map(
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
),
children: node.fragment.nodes.map(
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
)
};
},
Attribute(node, { visit, next, path }) {
if (node.value !== true && !Array.isArray(node.value)) {
path.push(node);
const value = /** @type {Legacy.LegacyAttribute['value']} */ ([visit(node.value)]);
path.pop();
return {
...node,
value
};
} else {
return next();
}
},
StyleDirective(node, { visit, next, path }) {
if (node.value !== true && !Array.isArray(node.value)) {
path.push(node);
const value = /** @type {Legacy.LegacyStyleDirective['value']} */ ([visit(node.value)]);
path.pop();
return {
...node,
value
};
} else {
return next();
}
},
SpreadAttribute(node) {
return { ...node, type: 'Spread' };
},
// @ts-ignore
StyleSheet(node, context) {
return {
...node,
...context.next(),
type: 'Style'
};
},
SvelteBody(node, { visit }) {
return {
type: 'Body',
name: 'svelte:body',
start: node.start,
end: node.end,
attributes: node.attributes.map(
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
),
children: node.fragment.nodes.map(
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
)
};
},
SvelteComponent(node, { visit }) {
return {
type: 'InlineComponent',
name: 'svelte:component',
start: node.start,
end: node.end,
expression: node.expression,
attributes: node.attributes.map(
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
),
children: node.fragment.nodes.map(
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
)
};
},
SvelteDocument(node, { visit }) {
return {
type: 'Document',
name: 'svelte:document',
start: node.start,
end: node.end,
attributes: node.attributes.map(
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
),
children: node.fragment.nodes.map(
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
)
};
},
SvelteElement(node, { visit }) {
/** @type {Expression | string} */
let tag = node.tag;
if (
tag.type === 'Literal' &&
typeof tag.value === 'string' &&
source[/** @type {number} */ (node.tag.start) - 1] !== '{'
) {
tag = tag.value;
}
return {
type: 'Element',
name: 'svelte:element',
start: node.start,
end: node.end,
tag,
attributes: node.attributes.map((child) => visit(child)),
children: node.fragment.nodes.map((child) => visit(child))
};
},
SvelteFragment(node, { visit }) {
return {
type: 'SlotTemplate',
name: 'svelte:fragment',
start: node.start,
end: node.end,
attributes: node.attributes.map(
(a) => /** @type {Legacy.LegacyAttributeLike} */ (visit(a))
),
children: node.fragment.nodes.map(
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
)
};
},
SvelteHead(node, { visit }) {
return {
type: 'Head',
name: 'svelte:head',
start: node.start,
end: node.end,
attributes: node.attributes.map(
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
),
children: node.fragment.nodes.map(
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
)
};
},
SvelteOptions(node, { visit }) {
return {
type: 'Options',
name: 'svelte:options',
start: node.start,
end: node.end,
attributes: node.attributes.map(
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
)
};
},
SvelteSelf(node, { visit }) {
return {
type: 'InlineComponent',
name: 'svelte:self',
start: node.start,
end: node.end,
attributes: node.attributes.map(
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
),
children: node.fragment.nodes.map(
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
)
};
},
SvelteWindow(node, { visit }) {
return {
type: 'Window',
name: 'svelte:window',
start: node.start,
end: node.end,
attributes: node.attributes.map(
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
),
children: node.fragment.nodes.map(
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
)
};
},
Text(node, { path }) {
const parent = path.at(-1);
if (parent?.type === 'RegularElement' && parent.name === 'style') {
// these text nodes are missing `raw` for some dumb reason
return /** @type {AST.Text} */ ({
type: 'Text',
start: node.start,
end: node.end,
data: node.data
});
}
},
TitleElement(node, { visit }) {
return {
type: 'Title',
name: 'title',
start: node.start,
end: node.end,
attributes: node.attributes.map(
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
),
children: node.fragment.nodes.map(
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
)
};
},
TransitionDirective(node) {
return { ...node, type: 'Transition' };
},
UseDirective(node) {
return { ...node, type: 'Action' };
},
LetDirective(node) {
return { ...node, type: 'Let' };
}
})
);
}
File diff suppressed because it is too large Load Diff
+198
View File
@@ -0,0 +1,198 @@
/** @import { Comment, Program } from 'estree' */
/** @import { AST } from '#compiler' */
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import { tsPlugin } from '@sveltejs/acorn-typescript';
const ParserWithTS = acorn.Parser.extend(tsPlugin());
/**
* @typedef {Comment & {
* start: number;
* end: number;
* }} CommentWithLocation
*/
/**
* @param {string} source
* @param {AST.JSComment[]} comments
* @param {boolean} typescript
* @param {boolean} [is_script]
*/
export function parse(source, comments, typescript, is_script) {
const parser = typescript ? ParserWithTS : acorn.Parser;
const { onComment, add_comments } = get_comment_handlers(
source,
/** @type {CommentWithLocation[]} */ (comments)
);
// @ts-ignore
const parse_statement = parser.prototype.parseStatement;
// If we're dealing with a <script> then it might contain an export
// for something that doesn't exist directly inside but is inside the
// component instead, so we need to ensure that Acorn doesn't throw
// an error in these cases
if (is_script) {
// @ts-ignore
parser.prototype.parseStatement = function (...args) {
const v = parse_statement.call(this, ...args);
// @ts-ignore
this.undefinedExports = {};
return v;
};
}
let ast;
try {
ast = parser.parse(source, {
onComment,
sourceType: 'module',
ecmaVersion: 16,
locations: true
});
} finally {
if (is_script) {
// @ts-ignore
parser.prototype.parseStatement = parse_statement;
}
}
add_comments(ast);
return /** @type {Program} */ (ast);
}
/**
* @param {string} source
* @param {Comment[]} comments
* @param {boolean} typescript
* @param {number} index
* @returns {acorn.Expression & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }}
*/
export function parse_expression_at(source, comments, typescript, index) {
const parser = typescript ? ParserWithTS : acorn.Parser;
const { onComment, add_comments } = get_comment_handlers(
source,
/** @type {CommentWithLocation[]} */ (comments),
index
);
const ast = parser.parseExpressionAt(source, index, {
onComment,
sourceType: 'module',
ecmaVersion: 16,
locations: true
});
add_comments(ast);
return ast;
}
/**
* Acorn doesn't add comments to the AST by itself. This factory returns the capabilities
* to add them after the fact. They are needed in order to support `svelte-ignore` comments
* in JS code and so that `prettier-plugin-svelte` doesn't remove all comments when formatting.
* @param {string} source
* @param {CommentWithLocation[]} comments
* @param {number} index
*/
function get_comment_handlers(source, comments, index = 0) {
return {
/**
* @param {boolean} block
* @param {string} value
* @param {number} start
* @param {number} end
* @param {import('acorn').Position} [start_loc]
* @param {import('acorn').Position} [end_loc]
*/
onComment: (block, value, start, end, start_loc, end_loc) => {
if (block && /\n/.test(value)) {
let a = start;
while (a > 0 && source[a - 1] !== '\n') a -= 1;
let b = a;
while (/[ \t]/.test(source[b])) b += 1;
const indentation = source.slice(a, b);
value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
}
comments.push({
type: block ? 'Block' : 'Line',
value,
start,
end,
loc: {
start: /** @type {import('acorn').Position} */ (start_loc),
end: /** @type {import('acorn').Position} */ (end_loc)
}
});
},
/** @param {acorn.Node & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }} ast */
add_comments(ast) {
if (comments.length === 0) return;
comments = comments
.filter((comment) => comment.start >= index)
.map(({ type, value, start, end }) => ({ type, value, start, end }));
walk(ast, null, {
_(node, { next, path }) {
let comment;
while (comments[0] && comments[0].start < node.start) {
comment = /** @type {CommentWithLocation} */ (comments.shift());
(node.leadingComments ||= []).push(comment);
}
next();
if (comments[0]) {
const parent = /** @type {any} */ (path.at(-1));
if (parent === undefined || node.end !== parent.end) {
const slice = source.slice(node.end, comments[0].start);
const is_last_in_body =
((parent?.type === 'BlockStatement' || parent?.type === 'Program') &&
parent.body.indexOf(node) === parent.body.length - 1) ||
(parent?.type === 'ArrayExpression' &&
parent.elements.indexOf(node) === parent.elements.length - 1) ||
(parent?.type === 'ObjectExpression' &&
parent.properties.indexOf(node) === parent.properties.length - 1);
if (is_last_in_body) {
// Special case: There can be multiple trailing comments after the last node in a block,
// and they can be separated by newlines
let end = node.end;
while (comments.length) {
const comment = comments[0];
if (parent && comment.start >= parent.end) break;
(node.trailingComments ||= []).push(comment);
comments.shift();
end = comment.end;
}
} else if (node.end <= comments[0].start && /^[,) \t]*$/.test(slice)) {
node.trailingComments = [/** @type {CommentWithLocation} */ (comments.shift())];
}
}
}
}
});
// Special case: Trailing comments after the root node (which can only happen for expression tags or for Program nodes).
// Adding them ensures that we can later detect the end of the expression tag correctly.
if (comments.length > 0 && (comments[0].start >= ast.end || ast.type === 'Program')) {
(ast.trailingComments ||= []).push(...comments.splice(0));
}
}
};
}
+348
View File
@@ -0,0 +1,348 @@
/** @import { AST } from '#compiler' */
/** @import { Location } from 'locate-character' */
/** @import * as ESTree from 'estree' */
// @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 * as e from '../../errors.js';
import { create_fragment } from './utils/create.js';
import read_options from './read/options.js';
import { is_reserved } from '../../../utils.js';
import { disallow_children } from '../2-analyze/visitors/shared/special-element.js';
import * as state from '../../state.js';
const regex_position_indicator = / \(\d+:\d+\)$/;
/** @param {number} cc */
function is_whitespace(cc) {
// fast path for common whitespace
if (cc === 32 || (cc <= 13 && cc >= 9)) return true;
// rare whitespace — \u00a0, \u1680, \u2000-\u200a, \u2028, \u2029, \u202f, \u205f, \u3000, \ufeff
if (cc < 160) return false;
return (
cc === 160 ||
cc === 5760 ||
(cc >= 8192 && cc <= 8202) ||
cc === 8232 ||
cc === 8233 ||
cc === 8239 ||
cc === 8287 ||
cc === 12288 ||
cc === 65279
);
}
const regex_lang_attribute =
/<!--[^]*?-->|<script\s+(?:[^>]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/g;
export class Parser {
/**
* @readonly
* @type {string}
*/
template;
/**
* Whether or not we're in loose parsing mode, in which
* case we try to continue parsing as much as possible
* @type {boolean}
*/
loose;
/** */
index = 0;
/**
* Creates a minimal parser instance for CSS-only parsing.
* Skips Svelte component parsing setup.
* @param {string} source
* @returns {Parser}
*/
static forCss(source) {
const parser = Object.create(Parser.prototype);
parser.template = source;
parser.index = 0;
parser.loose = false;
return parser;
}
/** Whether we're parsing in TypeScript mode */
ts = false;
/** @type {AST.TemplateNode[]} */
stack = [];
/** @type {AST.Fragment[]} */
fragments = [];
/** @type {AST.Root} */
root;
/** @type {Record<string, boolean>} */
meta_tags = {};
/** @type {LastAutoClosedTag | undefined} */
last_auto_closed_tag;
/**
* @param {string} template
* @param {boolean} loose
*/
constructor(template, loose) {
if (typeof template !== 'string') {
throw new TypeError('Template must be a string');
}
this.loose = loose;
this.template = template.trimEnd();
let match_lang;
do match_lang = regex_lang_attribute.exec(template);
while (match_lang && match_lang[0][1] !== 's'); // ensure it starts with '<s' to match script tags
regex_lang_attribute.lastIndex = 0; // reset matched index to pass tests - otherwise declare the regex inside the constructor
this.ts = match_lang?.[2] === 'ts';
this.root = {
css: null,
js: [],
// @ts-ignore
start: null,
// @ts-ignore
end: null,
type: 'Root',
fragment: create_fragment(),
options: null,
comments: [],
metadata: {
ts: this.ts
}
};
this.stack.push(this.root);
this.fragments.push(this.root.fragment);
/** @type {ParserState} */
let state = fragment;
while (this.index < this.template.length) {
state = state(this) || fragment;
}
if (this.stack.length > 1) {
const current = this.current();
if (this.loose) {
current.end = this.template.length;
} else if (current.type === 'RegularElement') {
current.end = current.start + 1;
e.element_unclosed(current, current.name);
} else {
current.end = current.start + 1;
e.block_unclosed(current);
}
}
if (state !== fragment) {
e.unexpected_eof(this.index);
}
this.root.start = 0;
this.root.end = template.length;
const options_index = this.root.fragment.nodes.findIndex(
/** @param {any} thing */
(thing) => thing.type === 'SvelteOptions'
);
if (options_index !== -1) {
const options = /** @type {AST.SvelteOptionsRaw} */ (this.root.fragment.nodes[options_index]);
this.root.fragment.nodes.splice(options_index, 1);
this.root.options = read_options(options);
disallow_children(options);
// We need this for the old AST format
Object.defineProperty(this.root.options, '__raw__', {
value: options,
enumerable: false
});
}
}
current() {
return this.stack[this.stack.length - 1];
}
/**
* @param {any} err
* @returns {never}
*/
acorn_error(err) {
e.js_parse_error(err.pos, err.message.replace(regex_position_indicator, ''));
}
/**
* @param {string} str
* @param {boolean} required
* @param {boolean} required_in_loose
*/
eat(str, required = false, required_in_loose = true) {
if (this.match(str)) {
this.index += str.length;
return true;
}
if (required && (!this.loose || required_in_loose)) {
e.expected_token(this.index, str);
}
return false;
}
/** @param {string} str */
match(str) {
const length = str.length;
if (length === 1) {
// more performant than slicing
return this.template[this.index] === str;
}
return this.template.startsWith(str, this.index);
}
/**
* Match a regex at the current index
* @param {RegExp} pattern Should have the sticky (`y`) flag so that it only matches at the current index
*/
match_regex(pattern) {
pattern.lastIndex = this.index;
const match = pattern.exec(this.template);
if (!match || match.index !== this.index) return null;
return match[0];
}
allow_whitespace() {
while (
this.index < this.template.length &&
is_whitespace(this.template.charCodeAt(this.index))
) {
this.index++;
}
}
/**
* Search for a regex starting at the current index and return the result if it matches
* @param {RegExp} pattern Should have a ^ anchor at the start so the regex doesn't search past the beginning, resulting in worse performance
*/
read(pattern) {
const result = this.match_regex(pattern);
if (result) this.index += result.length;
return result;
}
/**
* @returns {ESTree.Identifier & { start: number, end: number, loc: { start: Location, end: Location } }}
*/
read_identifier() {
const start = this.index;
let end = start;
let name = '';
const code = /** @type {number} */ (this.template.codePointAt(this.index));
if (isIdentifierStart(code, true)) {
let i = this.index;
end += code <= 0xffff ? 1 : 2;
while (end < this.template.length) {
const code = /** @type {number} */ (this.template.codePointAt(end));
if (!isIdentifierChar(code, true)) break;
end += code <= 0xffff ? 1 : 2;
}
name = this.template.slice(start, end);
this.index = end;
if (is_reserved(name)) {
e.unexpected_reserved_word(start, name);
}
}
return {
type: 'Identifier',
name,
start,
end,
loc: {
start: state.locator(start),
end: state.locator(end)
}
};
}
/** @param {RegExp} pattern */
read_until(pattern) {
if (this.index >= this.template.length) {
if (this.loose) return '';
e.unexpected_eof(this.template.length);
}
const start = this.index;
const match = pattern.exec(this.template.slice(start));
if (match) {
this.index = start + match.index;
return this.template.slice(start, this.index);
}
this.index = this.template.length;
return this.template.slice(start);
}
require_whitespace() {
if (!is_whitespace(this.template.charCodeAt(this.index))) {
e.expected_whitespace(this.index);
}
this.allow_whitespace();
}
pop() {
this.fragments.pop();
return this.stack.pop();
}
/**
* @template {AST.Fragment['nodes'][number]} T
* @param {T} node
* @returns {T}
*/
append(node) {
this.fragments.at(-1)?.nodes.push(node);
return node;
}
}
/**
* @param {string} template
* @param {boolean} [loose]
* @returns {AST.Root}
*/
export function parse(template, loose = false) {
state.set_source(template);
const parser = new Parser(template, loose);
return parser.root;
}
/** @typedef {(parser: Parser) => ParserState | void} ParserState */
/** @typedef {Object} LastAutoClosedTag
* @property {string} tag
* @property {string} reason
* @property {number} depth
*/
+116
View File
@@ -0,0 +1,116 @@
/** @import { Pattern } from 'estree' */
/** @import { Parser } from '../index.js' */
import { match_bracket } from '../utils/bracket.js';
import { parse_expression_at } from '../acorn.js';
import { regex_not_newline_characters } from '../../patterns.js';
import * as e from '../../../errors.js';
/**
* @param {Parser} parser
* @returns {Pattern}
*/
export default function read_pattern(parser) {
const start = parser.index;
let i = parser.index;
const id = parser.read_identifier();
if (id.name !== '') {
const annotation = read_type_annotation(parser);
return {
...id,
typeAnnotation: annotation
};
}
const char = parser.template[i];
if (char !== '{' && char !== '[') {
e.expected_pattern(i);
}
i = match_bracket(parser, start);
parser.index = i;
const pattern_string = parser.template.slice(start, i);
try {
// the length of the `space_with_newline` has to be start - 1
// because we added a `(` in front of the pattern_string,
// which shifted the entire string to right by 1
// so we offset it by removing 1 character in the `space_with_newline`
// to achieve that, we remove the 1st space encountered,
// so it will not affect the `column` of the node
let space_with_newline = parser.template
.slice(0, start)
.replace(regex_not_newline_characters, ' ');
const first_space = space_with_newline.indexOf(' ');
space_with_newline =
space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
const expression = /** @type {any} */ (
parse_expression_at(
`${space_with_newline}(${pattern_string} = 1)`,
parser.root.comments,
parser.ts,
start - 1
)
).left;
expression.typeAnnotation = read_type_annotation(parser);
if (expression.typeAnnotation) {
expression.end = expression.typeAnnotation.end;
}
return expression;
} catch (error) {
parser.acorn_error(error);
}
}
/**
* @param {Parser} parser
* @returns {any}
*/
function read_type_annotation(parser) {
const start = parser.index;
parser.allow_whitespace();
if (!parser.eat(':')) {
parser.index = start;
return undefined;
}
// we need to trick Acorn into parsing the type annotation
const insert = '_ as ';
let a = parser.index - insert.length;
const template =
parser.template.slice(0, a).replace(/[^\n]/g, ' ') +
insert +
// If this is a type annotation for a function parameter, Acorn-TS will treat subsequent
// parameters as part of a sequence expression instead, and will then error on optional
// parameters (`?:`). Therefore replace that sequence with something that will not error.
parser.template.slice(parser.index).replace(/\?\s*:/g, ':');
let expression = parse_expression_at(template, parser.root.comments, parser.ts, a);
// `foo: bar = baz` gets mangled — fix it
if (expression.type === 'AssignmentExpression') {
let b = expression.right.start;
while (template[b] !== '=') b -= 1;
expression = parse_expression_at(template.slice(0, b), parser.root.comments, parser.ts, a);
}
// `array as item: string, index` becomes `string, index`, which is mistaken as a sequence expression - fix that
if (expression.type === 'SequenceExpression') {
expression = expression.expressions[0];
}
parser.index = /** @type {number} */ (expression.end);
return {
type: 'TSTypeAnnotation',
start,
end: parser.index,
typeAnnotation: /** @type {any} */ (expression).typeAnnotation
};
}
+93
View File
@@ -0,0 +1,93 @@
/** @import { Expression } from 'estree' */
/** @import { Parser } from '../index.js' */
import { parse_expression_at } from '../acorn.js';
import { regex_whitespace } from '../../patterns.js';
import * as e from '../../../errors.js';
import { find_matching_bracket } from '../utils/bracket.js';
/**
* @param {Parser} parser
* @param {string} [opening_token]
* @returns {Expression | undefined}
*/
export function get_loose_identifier(parser, opening_token) {
// Find the next } and treat it as the end of the expression
const end = find_matching_bracket(parser.template, parser.index, opening_token ?? '{');
if (end) {
const start = parser.index;
parser.index = end;
// We don't know what the expression is and signal this by returning an empty identifier
return {
type: 'Identifier',
start,
end,
name: ''
};
}
}
/**
* @param {Parser} parser
* @param {string} [opening_token]
* @param {boolean} [disallow_loose]
* @returns {Expression}
*/
export default function read_expression(parser, opening_token, disallow_loose) {
try {
let comment_index = parser.root.comments.length;
const node = parse_expression_at(
parser.template,
parser.root.comments,
parser.ts,
parser.index
);
let num_parens = 0;
let i = parser.root.comments.length;
while (i-- > comment_index) {
const comment = parser.root.comments[i];
if (comment.end < node.start) {
parser.index = comment.end;
break;
}
}
for (let i = parser.index; i < /** @type {number} */ (node.start); i += 1) {
if (parser.template[i] === '(') num_parens += 1;
}
let index = /** @type {number} */ (node.end);
const last_comment = parser.root.comments.at(-1);
if (last_comment && last_comment.end > index) index = last_comment.end;
while (num_parens > 0) {
const char = parser.template[index];
if (char === ')') {
num_parens -= 1;
} else if (!regex_whitespace.test(char)) {
e.expected_token(index, ')');
}
index += 1;
}
parser.index = index;
return /** @type {Expression} */ (node);
} catch (err) {
// If we are in an each loop we need the error to be thrown in cases like
// `as { y = z }` so we still throw and handle the error there
if (parser.loose && !disallow_loose) {
const expression = get_loose_identifier(parser, opening_token);
if (expression) {
return expression;
}
}
parser.acorn_error(err);
}
}
+263
View File
@@ -0,0 +1,263 @@
/** @import { ObjectExpression } from 'estree' */
/** @import { AST } from '#compiler' */
import { NAMESPACE_MATHML, NAMESPACE_SVG } from '../../../../constants.js';
import * as e from '../../../errors.js';
/**
* @param {AST.SvelteOptionsRaw} node
* @returns {AST.Root['options']}
*/
export default function read_options(node) {
/** @type {AST.SvelteOptions} */
const component_options = {
start: node.start,
end: node.end,
// @ts-ignore
attributes: node.attributes
};
if (!node) {
return component_options;
}
for (const attribute of node.attributes) {
if (attribute.type !== 'Attribute') {
e.svelte_options_invalid_attribute(attribute);
}
const { name } = attribute;
switch (name) {
case 'runes': {
component_options.runes = get_boolean_value(attribute);
break;
}
case 'tag': {
e.svelte_options_deprecated_tag(attribute);
break; // eslint doesn't know this is unnecessary
}
case 'customElement': {
/** @type {AST.SvelteOptions['customElement']} */
const ce = {};
const { value: v } = attribute;
const value = v === true || Array.isArray(v) ? v : [v];
if (value === true) {
e.svelte_options_invalid_customelement(attribute);
} else if (value[0].type === 'Text') {
const tag = get_static_value(attribute);
validate_tag(attribute, tag);
ce.tag = tag;
component_options.customElement = ce;
break;
} else if (value[0].expression.type !== 'ObjectExpression') {
// Before Svelte 4 it was necessary to explicitly set customElement to null or else you'd get a warning.
// This is no longer necessary, but for backwards compat just skip in this case now.
if (value[0].expression.type === 'Literal' && value[0].expression.value === null) {
break;
}
e.svelte_options_invalid_customelement(attribute);
}
/** @type {Array<[string, any]>} */
const properties = [];
for (const property of value[0].expression.properties) {
if (
property.type !== 'Property' ||
property.computed ||
property.key.type !== 'Identifier'
) {
e.svelte_options_invalid_customelement(attribute);
}
properties.push([property.key.name, property.value]);
}
const tag = properties.find(([name]) => name === 'tag');
if (tag) {
const tag_value = tag[1]?.value;
validate_tag(tag, tag_value);
ce.tag = tag_value;
}
const props = properties.find(([name]) => name === 'props')?.[1];
if (props) {
if (props.type !== 'ObjectExpression') {
e.svelte_options_invalid_customelement_props(attribute);
}
ce.props = {};
for (const property of /** @type {ObjectExpression} */ (props).properties) {
if (
property.type !== 'Property' ||
property.computed ||
property.key.type !== 'Identifier' ||
property.value.type !== 'ObjectExpression'
) {
e.svelte_options_invalid_customelement_props(attribute);
}
ce.props[property.key.name] = {};
for (const prop of property.value.properties) {
if (
prop.type !== 'Property' ||
prop.computed ||
prop.key.type !== 'Identifier' ||
prop.value.type !== 'Literal'
) {
e.svelte_options_invalid_customelement_props(attribute);
}
if (prop.key.name === 'type') {
if (
['String', 'Number', 'Boolean', 'Array', 'Object'].indexOf(
/** @type {string} */ (prop.value.value)
) === -1
) {
e.svelte_options_invalid_customelement_props(attribute);
}
ce.props[property.key.name].type = /** @type {any} */ (prop.value.value);
} else if (prop.key.name === 'reflect') {
if (typeof prop.value.value !== 'boolean') {
e.svelte_options_invalid_customelement_props(attribute);
}
ce.props[property.key.name].reflect = prop.value.value;
} else if (prop.key.name === 'attribute') {
if (typeof prop.value.value !== 'string') {
e.svelte_options_invalid_customelement_props(attribute);
}
ce.props[property.key.name].attribute = prop.value.value;
} else {
e.svelte_options_invalid_customelement_props(attribute);
}
}
}
}
const shadow = properties.find(([name]) => name === 'shadow')?.[1];
if (shadow) {
if (shadow.type === 'Literal' && (shadow.value === 'open' || shadow.value === 'none')) {
ce.shadow = shadow.value;
} else if (shadow.type === 'ObjectExpression') {
ce.shadow = shadow;
} else {
e.svelte_options_invalid_customelement_shadow(attribute);
}
}
const extend = properties.find(([name]) => name === 'extend')?.[1];
if (extend) {
ce.extend = extend;
}
component_options.customElement = ce;
break;
}
case 'namespace': {
const value = get_static_value(attribute);
if (value === NAMESPACE_SVG) {
component_options.namespace = 'svg';
} else if (value === NAMESPACE_MATHML) {
component_options.namespace = 'mathml';
} else if (value === 'html' || value === 'mathml' || value === 'svg') {
component_options.namespace = value;
} else {
e.svelte_options_invalid_attribute_value(attribute, `"html", "mathml" or "svg"`);
}
break;
}
case 'css': {
const value = get_static_value(attribute);
if (value === 'injected') {
component_options.css = value;
} else {
e.svelte_options_invalid_attribute_value(attribute, `"injected"`);
}
break;
}
case 'immutable': {
component_options.immutable = get_boolean_value(attribute);
break;
}
case 'preserveWhitespace': {
component_options.preserveWhitespace = get_boolean_value(attribute);
break;
}
case 'accessors': {
component_options.accessors = get_boolean_value(attribute);
break;
}
default:
e.svelte_options_unknown_attribute(attribute, name);
}
}
return component_options;
}
/**
* @param {any} attribute
*/
function get_static_value(attribute) {
const { value } = attribute;
if (value === true) return true;
const chunk = Array.isArray(value) ? value[0] : value;
if (!chunk) return true;
if (value.length > 1) {
return null;
}
if (chunk.type === 'Text') return chunk.data;
if (chunk.expression.type !== 'Literal') {
return null;
}
return chunk.expression.value;
}
/**
* @param {any} attribute
*/
function get_boolean_value(attribute) {
const value = get_static_value(attribute);
if (typeof value !== 'boolean') {
e.svelte_options_invalid_attribute_value(attribute, 'true or false');
}
return value;
}
// https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
const tag_name_char =
'[a-z0-9_.\xB7\xC0-\xD6\xD8-\xF6\xF8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u{10000}-\u{EFFFF}-]';
const regex_valid_tag_name = new RegExp(`^[a-z]${tag_name_char}*-${tag_name_char}*$`, 'u');
const reserved_tag_names = [
'annotation-xml',
'color-profile',
'font-face',
'font-face-src',
'font-face-uri',
'font-face-format',
'font-face-name',
'missing-glyph'
];
/**
* @param {any} attribute
* @param {string | null} tag
* @returns {asserts tag is string}
*/
function validate_tag(attribute, tag) {
if (typeof tag !== 'string') {
e.svelte_options_invalid_tagname(attribute);
}
if (tag) {
if (!regex_valid_tag_name.test(tag)) {
e.svelte_options_invalid_tagname(attribute);
} else if (reserved_tag_names.includes(tag)) {
e.svelte_options_reserved_tagname(attribute);
}
}
}
+97
View File
@@ -0,0 +1,97 @@
/** @import { Program } from 'estree' */
/** @import { AST } 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';
import * as w from '../../../warnings.js';
import { is_text_attribute } from '../../../utils/ast.js';
import { locator } from '../../../state.js';
const regex_closing_script_tag = /<\/script\s*>/;
const regex_starts_with_closing_script_tag = /<\/script\s*>/y;
const RESERVED_ATTRIBUTES = ['server', 'client', 'worker', 'test', 'default'];
const ALLOWED_ATTRIBUTES = ['context', 'generics', 'lang', 'module'];
/**
* @param {Parser} parser
* @param {number} start
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag>} attributes
* @returns {AST.Script}
*/
export function read_script(parser, start, attributes) {
const script_start = parser.index;
const data = parser.read_until(regex_closing_script_tag);
if (parser.index >= parser.template.length) {
e.element_unclosed(parser.template.length, 'script');
}
const source =
parser.template.slice(0, script_start).replace(regex_not_newline_characters, ' ') + data;
parser.read(regex_starts_with_closing_script_tag);
/** @type {Program} */
let ast;
try {
ast = acorn.parse(source, parser.root.comments, parser.ts, true);
} catch (err) {
parser.acorn_error(err);
}
ast.start = script_start;
if (ast.loc) {
// Acorn always uses `0` as the start of a `Program`, but for sourcemap purposes
// we need it to be the start of the `<script>` contents
({ line: ast.loc.start.line, column: ast.loc.start.column } = locator(start));
({ line: ast.loc.end.line, column: ast.loc.end.column } = locator(parser.index));
}
/** @type {'default' | 'module'} */
let context = 'default';
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
if (RESERVED_ATTRIBUTES.includes(attribute.name)) {
e.script_reserved_attribute(attribute, attribute.name);
}
if (!ALLOWED_ATTRIBUTES.includes(attribute.name)) {
w.script_unknown_attribute(attribute);
}
if (attribute.name === 'module') {
if (attribute.value !== true) {
// Deliberately a generic code to future-proof for potential other attributes
e.script_invalid_attribute_value(attribute, attribute.name);
}
context = 'module';
}
if (attribute.name === 'context') {
if (attribute.value === true || !is_text_attribute(attribute)) {
e.script_invalid_context(attribute);
}
const value = attribute.value[0].data;
if (value !== 'module') {
e.script_invalid_context(attribute);
}
context = 'module';
}
}
return {
type: 'Script',
start,
end: parser.index,
context,
content: ast,
// @ts-ignore
attributes
};
}
+637
View File
@@ -0,0 +1,637 @@
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import * as e from '../../../errors.js';
const REGEX_MATCHER = /[~^$*|]?=/y;
const REGEX_CLOSING_BRACKET = /[\s\]]/;
const REGEX_ATTRIBUTE_FLAGS = /[a-zA-Z]+/y; // only `i` and `s` are valid today, but make it future-proof
const REGEX_COMBINATOR = /(\+|~|>|\|\|)/y;
const REGEX_PERCENTAGE = /\d+(\.\d+)?%/y;
const REGEX_NTH_OF =
/(even|odd|\+?(\d+|\d*n(\s*[+-]\s*\d+)?)|-\d*n(\s*\+\s*\d+))((?=\s*[,)])|\s+of\s+)/y;
const REGEX_WHITESPACE_OR_COLON = /[\s:]/;
const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/y;
const REGEX_VALID_IDENTIFIER_CHAR = /[a-zA-Z0-9_-]/;
const REGEX_UNICODE_SEQUENCE = /\\[0-9a-fA-F]{1,6}(\r\n|\s)?/y;
const REGEX_COMMENT_CLOSE = /\*\//;
const REGEX_HTML_COMMENT_CLOSE = /-->/;
/**
* @param {Parser} parser
* @param {number} start
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag>} attributes
* @returns {AST.CSS.StyleSheet}
*/
export default function read_style(parser, start, attributes) {
const content_start = parser.index;
const children = read_body(parser, (p) => p.match('</style') || p.index >= p.template.length);
const content_end = parser.index;
parser.eat('</style', true);
parser.read(/\s*>/y);
return {
type: 'StyleSheet',
start,
end: parser.index,
attributes,
children,
content: {
start: content_start,
end: content_end,
styles: parser.template.slice(content_start, content_end),
comment: null
}
};
}
/**
* @param {Parser} parser
* @param {(parser: Parser) => boolean} finished
* @returns {Array<AST.CSS.Rule | AST.CSS.Atrule>}
*/
function read_body(parser, finished) {
/** @type {Array<AST.CSS.Rule | AST.CSS.Atrule>} */
const children = [];
while ((allow_comment_or_whitespace(parser), !finished(parser))) {
if (parser.match('@')) {
children.push(read_at_rule(parser));
} else {
children.push(read_rule(parser));
}
}
return children;
}
/**
* @param {Parser} parser
* @returns {AST.CSS.Atrule}
*/
function read_at_rule(parser) {
const start = parser.index;
parser.eat('@', true);
const name = read_identifier(parser);
const prelude = read_value(parser);
/** @type {AST.CSS.Block | null} */
let block = null;
if (parser.match('{')) {
// e.g. `@media (...) {...}`
block = read_block(parser);
} else {
// e.g. `@import '...'`
parser.eat(';', true);
}
return {
type: 'Atrule',
start,
end: parser.index,
name,
prelude,
block
};
}
/**
* @param {Parser} parser
* @returns {AST.CSS.Rule}
*/
function read_rule(parser) {
const start = parser.index;
return {
type: 'Rule',
prelude: read_selector_list(parser),
block: read_block(parser),
start,
end: parser.index,
metadata: {
parent_rule: null,
has_local_selectors: false,
has_global_selectors: false,
is_global_block: false
}
};
}
/**
* @param {Parser} parser
* @param {boolean} [inside_pseudo_class]
* @returns {AST.CSS.SelectorList}
*/
function read_selector_list(parser, inside_pseudo_class = false) {
/** @type {AST.CSS.ComplexSelector[]} */
const children = [];
allow_comment_or_whitespace(parser);
const start = parser.index;
while (parser.index < parser.template.length) {
children.push(read_selector(parser, inside_pseudo_class));
const end = parser.index;
allow_comment_or_whitespace(parser);
if (inside_pseudo_class ? parser.match(')') : parser.match('{')) {
return {
type: 'SelectorList',
start,
end,
children
};
} else {
parser.eat(',', true);
allow_comment_or_whitespace(parser);
}
}
e.unexpected_eof(parser.template.length);
}
/**
* @param {Parser} parser
* @param {boolean} [inside_pseudo_class]
* @returns {AST.CSS.ComplexSelector}
*/
function read_selector(parser, inside_pseudo_class = false) {
const list_start = parser.index;
/** @type {AST.CSS.RelativeSelector[]} */
const children = [];
/**
* @param {AST.CSS.Combinator | null} combinator
* @param {number} start
* @returns {AST.CSS.RelativeSelector}
*/
function create_selector(combinator, start) {
return {
type: 'RelativeSelector',
combinator,
selectors: [],
start,
end: -1,
metadata: {
is_global: false,
is_global_like: false,
scoped: false
}
};
}
/** @type {AST.CSS.RelativeSelector} */
let relative_selector = create_selector(null, parser.index);
while (parser.index < parser.template.length) {
let start = parser.index;
if (parser.eat('&')) {
relative_selector.selectors.push({
type: 'NestingSelector',
name: '&',
start,
end: parser.index
});
} else if (parser.eat('*')) {
let name = '*';
if (parser.eat('|')) {
// * is the namespace (which we ignore)
name = read_identifier(parser);
}
relative_selector.selectors.push({
type: 'TypeSelector',
name,
start,
end: parser.index
});
} else if (parser.eat('#')) {
relative_selector.selectors.push({
type: 'IdSelector',
name: read_identifier(parser),
start,
end: parser.index
});
} else if (parser.eat('.')) {
relative_selector.selectors.push({
type: 'ClassSelector',
name: read_identifier(parser),
start,
end: parser.index
});
} else if (parser.eat('::')) {
relative_selector.selectors.push({
type: 'PseudoElementSelector',
name: read_identifier(parser),
start,
end: parser.index
});
// We read the inner selectors of a pseudo element to ensure it parses correctly,
// but we don't do anything with the result.
if (parser.eat('(')) {
read_selector_list(parser, true);
parser.eat(')', true);
}
} else if (parser.eat(':')) {
const name = read_identifier(parser);
/** @type {null | AST.CSS.SelectorList} */
let args = null;
if (parser.eat('(')) {
args = read_selector_list(parser, true);
parser.eat(')', true);
}
relative_selector.selectors.push({
type: 'PseudoClassSelector',
name,
args,
start,
end: parser.index
});
} else if (parser.eat('[')) {
parser.allow_whitespace();
const name = read_identifier(parser);
parser.allow_whitespace();
/** @type {string | null} */
let value = null;
const matcher = parser.read(REGEX_MATCHER);
if (matcher) {
parser.allow_whitespace();
value = read_attribute_value(parser);
}
parser.allow_whitespace();
const flags = parser.read(REGEX_ATTRIBUTE_FLAGS);
parser.allow_whitespace();
parser.eat(']', true);
relative_selector.selectors.push({
type: 'AttributeSelector',
start,
end: parser.index,
name,
matcher,
value,
flags
});
} else if (inside_pseudo_class && parser.match_regex(REGEX_NTH_OF)) {
// nth of matcher must come before combinator matcher to prevent collision else the '+' in '+2n-1' would be parsed as a combinator
relative_selector.selectors.push({
type: 'Nth',
value: /**@type {string} */ (parser.read(REGEX_NTH_OF)),
start,
end: parser.index
});
} else if (parser.match_regex(REGEX_PERCENTAGE)) {
relative_selector.selectors.push({
type: 'Percentage',
value: /** @type {string} */ (parser.read(REGEX_PERCENTAGE)),
start,
end: parser.index
});
} else if (!parser.match_regex(REGEX_COMBINATOR)) {
let name = read_identifier(parser);
if (parser.eat('|')) {
// we ignore the namespace when trying to find matching element classes
name = read_identifier(parser);
}
relative_selector.selectors.push({
type: 'TypeSelector',
name,
start,
end: parser.index
});
}
const index = parser.index;
allow_comment_or_whitespace(parser);
if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) {
// rewind, so we know whether to continue building the selector list
parser.index = index;
relative_selector.end = index;
children.push(relative_selector);
return {
type: 'ComplexSelector',
start: list_start,
end: index,
children,
metadata: {
rule: null,
is_global: false,
used: false
}
};
}
parser.index = index;
const combinator = read_combinator(parser);
if (combinator) {
if (relative_selector.selectors.length > 0) {
relative_selector.end = index;
children.push(relative_selector);
}
// ...and start a new one
relative_selector = create_selector(combinator, combinator.start);
parser.allow_whitespace();
if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) {
e.css_selector_invalid(parser.index);
}
}
}
e.unexpected_eof(parser.template.length);
}
/**
* @param {Parser} parser
* @returns {AST.CSS.Combinator | null}
*/
function read_combinator(parser) {
const start = parser.index;
parser.allow_whitespace();
const index = parser.index;
const name = parser.read(REGEX_COMBINATOR);
if (name) {
const end = parser.index;
parser.allow_whitespace();
return {
type: 'Combinator',
name,
start: index,
end
};
}
if (parser.index !== start) {
return {
type: 'Combinator',
name: ' ',
start,
end: parser.index
};
}
return null;
}
/**
* @param {Parser} parser
* @returns {AST.CSS.Block}
*/
function read_block(parser) {
const start = parser.index;
parser.eat('{', true);
/** @type {Array<AST.CSS.Declaration | AST.CSS.Rule | AST.CSS.Atrule>} */
const children = [];
while (parser.index < parser.template.length) {
allow_comment_or_whitespace(parser);
if (parser.match('}')) {
break;
} else {
children.push(read_block_item(parser));
}
}
parser.eat('}', true);
return {
type: 'Block',
start,
end: parser.index,
children
};
}
/**
* Reads a declaration, rule or at-rule
*
* @param {Parser} parser
* @returns {AST.CSS.Declaration | AST.CSS.Rule | AST.CSS.Atrule}
*/
function read_block_item(parser) {
if (parser.match('@')) {
return read_at_rule(parser);
}
// read ahead to understand whether we're dealing with a declaration or a nested rule.
// this involves some duplicated work, but avoids a try-catch that would disguise errors
const start = parser.index;
read_value(parser);
const char = parser.template[parser.index];
parser.index = start;
return char === '{' ? read_rule(parser) : read_declaration(parser);
}
/**
* @param {Parser} parser
* @returns {AST.CSS.Declaration}
*/
function read_declaration(parser) {
const start = parser.index;
const property = parser.read_until(REGEX_WHITESPACE_OR_COLON);
parser.allow_whitespace();
parser.eat(':');
let index = parser.index;
parser.allow_whitespace();
const value = read_value(parser);
if (!value && !property.startsWith('--')) {
e.css_empty_declaration({ start, end: index });
}
const end = parser.index;
if (!parser.match('}')) {
parser.eat(';', true);
}
return {
type: 'Declaration',
start,
end,
property,
value
};
}
/**
* @param {Parser} parser
* @returns {string}
*/
function read_value(parser) {
let value = '';
let escaped = false;
let in_url = false;
/** @type {null | '"' | "'"} */
let quote_mark = null;
while (parser.index < parser.template.length) {
const char = parser.template[parser.index];
if (escaped) {
value += '\\' + char;
escaped = false;
parser.index++;
continue;
} else if (char === '\\') {
escaped = true;
parser.index++;
continue;
} else if (char === quote_mark) {
quote_mark = null;
} else if (char === ')') {
in_url = false;
} else if (quote_mark === null && (char === '"' || char === "'")) {
quote_mark = char;
} else if (char === '(' && value.slice(-3) === 'url') {
in_url = true;
} else if ((char === ';' || char === '{' || char === '}') && !in_url && !quote_mark) {
return value.trim();
}
value += char;
parser.index++;
}
e.unexpected_eof(parser.template.length);
}
/**
* Read a property that may or may not be quoted, e.g.
* `foo` or `'foo bar'` or `"foo bar"`
* @param {Parser} parser
*/
function read_attribute_value(parser) {
let value = '';
let escaped = false;
const quote_mark = parser.eat('"') ? '"' : parser.eat("'") ? "'" : null;
while (parser.index < parser.template.length) {
const char = parser.template[parser.index];
if (escaped) {
value += '\\' + char;
escaped = false;
} else if (char === '\\') {
escaped = true;
} else if (quote_mark ? char === quote_mark : REGEX_CLOSING_BRACKET.test(char)) {
if (quote_mark) {
parser.eat(quote_mark, true);
}
return value.trim();
} else {
value += char;
}
parser.index++;
}
e.unexpected_eof(parser.template.length);
}
/**
* @see {@link https://www.w3.org/TR/css-syntax-3/#ident-token-diagram CSS Syntax Module Level 3}
* @param {Parser} parser
*/
function read_identifier(parser) {
const start = parser.index;
let identifier = '';
if (parser.match_regex(REGEX_LEADING_HYPHEN_OR_DIGIT)) {
e.css_expected_identifier(start);
}
while (parser.index < parser.template.length) {
const char = parser.template[parser.index];
if (char === '\\') {
const sequence = parser.match_regex(REGEX_UNICODE_SEQUENCE);
if (sequence) {
identifier += String.fromCodePoint(parseInt(sequence.slice(1), 16));
parser.index += sequence.length;
} else {
identifier += '\\' + parser.template[parser.index + 1];
parser.index += 2;
}
} else if (
/** @type {number} */ (char.codePointAt(0)) >= 160 ||
REGEX_VALID_IDENTIFIER_CHAR.test(char)
) {
identifier += char;
parser.index++;
} else {
break;
}
}
if (identifier === '') {
e.css_expected_identifier(start);
}
return identifier;
}
/** @param {Parser} parser */
function allow_comment_or_whitespace(parser) {
parser.allow_whitespace();
while (parser.match('/*') || parser.match('<!--')) {
if (parser.eat('/*')) {
parser.read_until(REGEX_COMMENT_CLOSE);
parser.eat('*/', true);
}
if (parser.eat('<!--')) {
parser.read_until(REGEX_HTML_COMMENT_CLOSE);
parser.eat('-->', true);
}
parser.allow_whitespace();
}
}
/**
* Parse standalone CSS content (not wrapped in `<style>`).
* @param {Parser} parser
* @returns {Array<AST.CSS.Rule | AST.CSS.Atrule>}
*/
export function parse_stylesheet(parser) {
return read_body(parser, (p) => p.index >= p.template.length);
}
@@ -0,0 +1,180 @@
/** @import { Context, Visitors } from 'zimmerframe' */
/** @import { FunctionExpression, FunctionDeclaration } from 'estree' */
import { walk } from 'zimmerframe';
import * as b from '#compiler/builders';
import * as e from '../../errors.js';
/**
* @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') {
node.params.shift();
}
return context.next();
}
/** @type {Visitors<any, null>} */
const visitors = {
_(node, context) {
const n = context.next() ?? node;
// TODO there may come a time when we decide to preserve type annotations.
// until that day comes, we just delete them so they don't confuse esrap
delete n.typeAnnotation;
delete n.typeParameters;
delete n.typeArguments;
delete n.returnType;
delete n.accessibility;
delete n.readonly;
delete n.definite;
delete n.override;
},
Decorator(node) {
e.typescript_invalid_feature(node, 'decorators (related TSC proposal is not stage 4 yet)');
},
ImportDeclaration(node) {
if (node.importKind === 'type') return b.empty;
if (node.specifiers?.length > 0) {
const specifiers = node.specifiers.filter((/** @type {any} */ s) => s.importKind !== 'type');
if (specifiers.length === 0) return b.empty;
return { ...node, specifiers };
}
return node;
},
ExportNamedDeclaration(node, context) {
if (node.exportKind === 'type') return b.empty;
if (node.declaration) {
const result = context.next();
if (result?.declaration?.type === 'EmptyStatement') {
return b.empty;
}
return result;
}
if (node.specifiers) {
const specifiers = node.specifiers.filter((/** @type {any} */ s) => s.exportKind !== 'type');
if (specifiers.length === 0) return b.empty;
return { ...node, specifiers };
}
return node;
},
ExportDefaultDeclaration(node) {
if (node.exportKind === 'type') return b.empty;
return node;
},
ExportAllDeclaration(node) {
if (node.exportKind === 'type') return b.empty;
return node;
},
PropertyDefinition(node, { next }) {
if (node.accessor) {
e.typescript_invalid_feature(
node,
'accessor fields (related TSC proposal is not stage 4 yet)'
);
}
return next();
},
TSAsExpression(node, context) {
return context.visit(node.expression);
},
TSSatisfiesExpression(node, context) {
return context.visit(node.expression);
},
TSNonNullExpression(node, context) {
return context.visit(node.expression);
},
TSInterfaceDeclaration() {
return b.empty;
},
TSTypeAliasDeclaration() {
return b.empty;
},
TSTypeAssertion(node, context) {
return context.visit(node.expression);
},
TSEnumDeclaration(node) {
e.typescript_invalid_feature(node, 'enums');
},
TSParameterProperty(node, context) {
if ((node.readonly || node.accessibility) && context.path.at(-2)?.kind === 'constructor') {
e.typescript_invalid_feature(node, 'accessibility modifiers on constructor parameters');
}
return context.visit(node.parameter);
},
TSInstantiationExpression(node, context) {
return context.visit(node.expression);
},
FunctionExpression: remove_this_param,
FunctionDeclaration: remove_this_param,
TSDeclareFunction() {
return b.empty;
},
ClassBody(node, context) {
const body = [];
for (const _child of node.body) {
const child = context.visit(_child);
if (child.type !== 'PropertyDefinition' || !child.declare) {
body.push(child);
}
}
return {
...node,
body
};
},
ClassDeclaration(node, context) {
if (node.declare) {
return b.empty;
}
delete node.abstract;
delete node.implements;
delete node.superTypeArguments;
return context.next();
},
ClassExpression(node, context) {
delete node.implements;
delete node.superTypeArguments;
return context.next();
},
MethodDefinition(node, context) {
if (node.abstract) {
return b.empty;
}
return context.next();
},
VariableDeclaration(node, context) {
if (node.declare) {
return b.empty;
}
return context.next();
},
TSModuleDeclaration(node, context) {
if (!node.body) return b.empty;
// namespaces can contain non-type nodes
const cleaned = /** @type {any[]} */ (node.body.body).map((entry) => context.visit(entry));
if (cleaned.some((entry) => entry !== b.empty)) {
e.typescript_invalid_feature(node, 'namespaces with non-type nodes');
}
return b.empty;
}
};
/**
* @template T
* @param {T} ast
* @returns {T}
*/
export function remove_typescript_nodes(ast) {
return walk(ast, null, visitors);
}
+950
View File
@@ -0,0 +1,950 @@
/** @import { Expression, Identifier, SourceLocation } from 'estree' */
/** @import { Location } from 'locate-character' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import { is_void, REGEX_VALID_TAG_NAME } from '../../../../utils.js';
import read_expression from '../read/expression.js';
import { read_script } from '../read/script.js';
import read_style from '../read/style.js';
import { decode_character_references } from '../utils/html.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { create_fragment } from '../utils/create.js';
import { create_attribute, ExpressionMetadata, is_element_node } from '../../nodes.js';
import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js';
import { closing_tag_omitted } from '../../../../html-tree-validation.js';
import { list } from '../../../utils/string.js';
import { locator } from '../../../state.js';
import * as b from '#compiler/builders';
const regex_invalid_unquoted_attribute_value = /(\/>|[\s"'=<>`])/y;
const regex_closing_textarea_tag = /<\/textarea(\s[^>]*)?>/iy;
const regex_closing_comment = /-->/;
const regex_whitespace_or_slash_or_closing_tag = /(\s|\/|>)/;
const regex_token_ending_character = /[\s=/>"']/;
const regex_starts_with_quote_characters = /["']/y;
const regex_attribute_value = /(?:"([^"]*)"|'([^'])*'|([^>\s]+))/y;
const regex_doctype_name = /^![a-zA-Z]+$/;
const regex_namespaced_name = /^[a-zA-Z][a-zA-Z0-9]*:[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9]$/;
/** @param {string} name */
function is_valid_element_name(name) {
// DOCTYPE (e.g. !DOCTYPE)
if (regex_doctype_name.test(name)) return true;
// svelte:* meta tags (e.g. svelte:element, svelte:head)
if (regex_namespaced_name.test(name)) return true;
// standard HTML/SVG/MathML elements and custom elements
return REGEX_VALID_TAG_NAME.test(name);
}
export const regex_valid_component_name =
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers adjusted for our needs
// (must start with uppercase letter if no dots, can contain dots)
/^(?:\p{Lu}[$\u200c\u200d\p{ID_Continue}.]*|\p{ID_Start}[$\u200c\u200d\p{ID_Continue}]*(?:\.[$\u200c\u200d\p{ID_Continue}]+)+)$/u;
/** @type {Map<string, AST.ElementLike['type']>} */
const root_only_meta_tags = new Map([
['svelte:head', 'SvelteHead'],
['svelte:options', 'SvelteOptions'],
['svelte:window', 'SvelteWindow'],
['svelte:document', 'SvelteDocument'],
['svelte:body', 'SvelteBody']
]);
/** @type {Map<string, AST.ElementLike['type']>} */
const meta_tags = new Map([
...root_only_meta_tags,
['svelte:element', 'SvelteElement'],
['svelte:component', 'SvelteComponent'],
['svelte:self', 'SvelteSelf'],
['svelte:fragment', 'SvelteFragment'],
['svelte:boundary', 'SvelteBoundary']
]);
/** @param {Parser} parser */
export default function element(parser) {
const start = parser.index++;
let parent = parser.current();
if (parser.eat('!--')) {
const data = parser.read_until(regex_closing_comment);
parser.eat('-->', true);
parser.append({
type: 'Comment',
start,
end: parser.index,
data
});
return;
}
if (parser.eat('/')) {
const name = parser.read_until(regex_whitespace_or_slash_or_closing_tag);
parser.allow_whitespace();
parser.eat('>', true);
if (is_void(name)) {
e.void_element_invalid_content(start);
}
// close any elements that don't have their own closing tags, e.g. <div><p></div>
while (/** @type {AST.RegularElement} */ (parent).name !== name) {
if (parser.loose) {
// If the previous element did interpret the next opening tag as an attribute, backtrack
if (is_element_node(parent)) {
const last = parent.attributes.at(-1);
if (last?.type === 'Attribute' && last.name === `<${name}`) {
parser.index = last.start;
parent.attributes.pop();
break;
}
}
}
if (parent.type === 'RegularElement') {
if (!parser.last_auto_closed_tag || parser.last_auto_closed_tag.tag !== name) {
const end = parent.fragment.nodes[0]?.start ?? start;
w.element_implicitly_closed(
{ start: parent.start, end },
`</${name}>`,
`</${parent.name}>`
);
}
} else if (!parser.loose) {
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);
} else {
e.element_invalid_closing_tag(start, name);
}
}
parent.end = start;
parser.pop();
parent = parser.current();
}
parent.end = parser.index;
parser.pop();
if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) {
parser.last_auto_closed_tag = undefined;
}
return;
}
const tag = read_tag(parser, regex_whitespace_or_slash_or_closing_tag);
if (tag.name.startsWith('svelte:') && !meta_tags.has(tag.name)) {
const bounds = { start: start + 1, end: start + 1 + tag.name.length };
e.svelte_meta_invalid_tag(bounds, list(Array.from(meta_tags.keys())));
}
if (!is_valid_element_name(tag.name) && !regex_valid_component_name.test(tag.name)) {
// <div. -> in the middle of typing -> allow in loose mode
if (!parser.loose || !tag.name.endsWith('.')) {
const bounds = { start: start + 1, end: start + 1 + tag.name.length };
e.tag_invalid_name(bounds);
}
}
if (root_only_meta_tags.has(tag.name)) {
if (tag.name in parser.meta_tags) {
e.svelte_meta_duplicate(start, tag.name);
}
if (parent.type !== 'Root') {
e.svelte_meta_invalid_placement(start, tag.name);
}
parser.meta_tags[tag.name] = true;
}
const type = meta_tags.has(tag.name)
? meta_tags.get(tag.name)
: regex_valid_component_name.test(tag.name) || (parser.loose && tag.name.endsWith('.'))
? 'Component'
: tag.name === 'title' && parent_is_head(parser.stack)
? 'TitleElement'
: // TODO Svelte 6/7: once slots are removed in favor of snippets, always keep slot as a regular element
tag.name === 'slot' && !parent_is_shadowroot_template(parser.stack)
? 'SlotElement'
: 'RegularElement';
/** @type {AST.ElementLike} */
const element =
type === 'RegularElement'
? {
type,
start,
end: -1,
name: tag.name,
name_loc: tag.loc,
attributes: [],
fragment: create_fragment(true),
metadata: {
svg: false,
mathml: false,
scoped: false,
has_spread: false,
path: [],
synthetic_value_node: null
}
}
: /** @type {AST.ElementLike} */ ({
type,
start,
end: -1,
name: tag.name,
name_loc: tag.loc,
attributes: [],
fragment: create_fragment(true),
metadata: {
// unpopulated at first, differs between types
}
});
parser.allow_whitespace();
if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, tag.name)) {
const end = parent.fragment.nodes[0]?.start ?? start;
w.element_implicitly_closed({ start: parent.start, end }, `<${tag.name}>`, `</${parent.name}>`);
parent.end = start;
parser.pop();
parser.last_auto_closed_tag = {
tag: parent.name,
reason: tag.name,
depth: parser.stack.length
};
}
/** @type {string[]} */
const unique_names = [];
const current = parser.current();
const is_top_level_script_or_style =
(tag.name === 'script' || tag.name === 'style') && current.type === 'Root';
const read = is_top_level_script_or_style ? read_static_attribute : read_attribute;
let attribute;
while ((attribute = read(parser))) {
// animate and transition can only be specified once per element so no need
// to check here, use can be used multiple times, same for the on directive
// finally let already has error handling in case of duplicate variable names
if (
attribute.type === 'Attribute' ||
attribute.type === 'BindDirective' ||
attribute.type === 'StyleDirective' ||
attribute.type === 'ClassDirective'
) {
// `bind:attribute` and `attribute` are just the same but `class:attribute`,
// `style:attribute` and `attribute` are different and should be allowed together
// so we concatenate the type while normalizing the type for BindDirective
const type = attribute.type === 'BindDirective' ? 'Attribute' : attribute.type;
if (unique_names.includes(type + attribute.name)) {
e.attribute_duplicate(attribute);
// <svelte:element bind:this this=..> is allowed
} else if (attribute.name !== 'this') {
unique_names.push(type + attribute.name);
}
}
element.attributes.push(attribute);
parser.allow_whitespace();
}
if (element.type === 'Component') {
element.metadata.expression = new ExpressionMetadata();
}
if (element.type === 'SvelteComponent') {
const index = element.attributes.findIndex(
/** @param {any} attr */
(attr) => attr.type === 'Attribute' && attr.name === 'this'
);
if (index === -1) {
e.svelte_component_missing_this(start);
}
const definition = /** @type {AST.Attribute} */ (element.attributes.splice(index, 1)[0]);
if (!is_expression_attribute(definition)) {
e.svelte_component_invalid_this(definition.start);
}
element.expression = get_attribute_expression(definition);
element.metadata.expression = new ExpressionMetadata();
}
if (element.type === 'SvelteElement') {
const index = element.attributes.findIndex(
/** @param {any} attr */
(attr) => attr.type === 'Attribute' && attr.name === 'this'
);
if (index === -1) {
e.svelte_element_missing_this(start);
}
const definition = /** @type {AST.Attribute} */ (element.attributes.splice(index, 1)[0]);
if (definition.value === true) {
e.svelte_element_missing_this(definition);
}
if (!is_expression_attribute(definition)) {
w.svelte_element_invalid_this(definition);
// note that this is wrong, in the case of e.g. `this="h{n}"` — it will result in `<h>`.
// it would be much better to just error here, but we are preserving the existing buggy
// Svelte 4 behaviour out of an overabundance of caution regarding breaking changes.
// TODO in 6.0, error
const chunk = /** @type {Array<AST.ExpressionTag | AST.Text>} */ (definition.value)[0];
element.tag =
chunk.type === 'Text'
? {
type: 'Literal',
value: chunk.data,
raw: `'${chunk.raw}'`,
start: chunk.start,
end: chunk.end
}
: chunk.expression;
} else {
element.tag = get_attribute_expression(definition);
}
element.metadata.expression = new ExpressionMetadata();
}
if (is_top_level_script_or_style) {
parser.eat('>', true);
/** @type {AST.Comment | null} */
let prev_comment = null;
for (let i = current.fragment.nodes.length - 1; i >= 0; i--) {
const node = current.fragment.nodes[i];
if (i === current.fragment.nodes.length - 1 && node.end !== start) {
break;
}
if (node.type === 'Comment') {
prev_comment = node;
break;
} else if (node.type !== 'Text' || node.data.trim()) {
break;
}
}
if (tag.name === 'script') {
const content = read_script(parser, start, element.attributes);
if (prev_comment) {
// We take advantage of the fact that the root will never have leadingComments set,
// and set the previous comment to it so that the warning mechanism can later
// inspect the root and see if there was a html comment before it silencing specific warnings.
content.content.leadingComments = [{ type: 'Line', value: prev_comment.data }];
}
if (content.context === 'module') {
if (current.module) e.script_duplicate(start);
current.module = content;
} else {
if (current.instance) e.script_duplicate(start);
current.instance = content;
}
} else {
const content = read_style(parser, start, element.attributes);
content.content.comment = prev_comment;
if (current.css) e.style_duplicate(start);
current.css = content;
}
return;
}
parser.append(element);
const self_closing = parser.eat('/') || is_void(tag.name);
const closed = parser.eat('>', true, false);
// Loose parsing mode
if (!closed) {
// We may have eaten an opening `<` of the next element and treated it as an attribute...
const last = element.attributes.at(-1);
if (last?.type === 'Attribute' && last.name === '<') {
parser.index = last.start;
element.attributes.pop();
} else {
// ... or we may have eaten part of a following block ...
const prev_1 = parser.template[parser.index - 1];
const prev_2 = parser.template[parser.index - 2];
const current = parser.template[parser.index];
if (prev_2 === '{' && prev_1 === '/') {
parser.index -= 2;
} else if (prev_1 === '{' && (current === '#' || current === '@' || current === ':')) {
parser.index -= 1;
} else {
// ... or we're followed by whitespace, for example near the end of the template,
// which we want to take in so that language tools has more room to work with
parser.allow_whitespace();
}
}
}
if (self_closing || !closed) {
// don't push self-closing elements onto the stack
element.end = parser.index;
} else if (tag.name === 'textarea') {
// special case
element.fragment.nodes = read_sequence(
parser,
() => {
regex_closing_textarea_tag.lastIndex = parser.index;
return regex_closing_textarea_tag.test(parser.template);
},
'inside <textarea>'
);
parser.read(regex_closing_textarea_tag);
element.end = parser.index;
} else if (tag.name === 'script' || tag.name === 'style') {
// special case
const start = parser.index;
const close_tag = `</${tag.name}>`;
const close_index = parser.template.indexOf(close_tag, parser.index);
const data = parser.template.slice(
parser.index,
close_index === -1 ? parser.template.length : close_index
);
parser.index = close_index === -1 ? parser.template.length : close_index;
const end = parser.index;
/** @type {AST.Text} */
const node = {
start,
end,
type: 'Text',
data,
raw: data
};
element.fragment.nodes.push(node);
parser.eat(`</${tag.name}>`, true);
element.end = parser.index;
} else {
parser.stack.push(element);
parser.fragments.push(element.fragment);
}
}
/** @param {AST.TemplateNode[]} stack */
function parent_is_head(stack) {
let i = stack.length;
while (i--) {
const { type } = stack[i];
if (type === 'SvelteHead') return true;
if (type === 'RegularElement' || type === 'Component') return false;
}
return false;
}
/** @param {AST.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 {AST.RegularElement} */ (stack[i]).attributes.some(
(a) => a.type === 'Attribute' && a.name === 'shadowrootmode'
)
) {
return true;
}
}
return false;
}
/**
* @param {Parser} parser
* @returns {AST.Attribute | null}
*/
function read_static_attribute(parser) {
const start = parser.index;
const tag = read_tag(parser, regex_token_ending_character);
if (!tag.name) return null;
/** @type {true | Array<AST.Text | AST.ExpressionTag>} */
let value = true;
if (parser.eat('=')) {
parser.allow_whitespace();
let raw = parser.match_regex(regex_attribute_value);
if (!raw) {
e.expected_attribute_value(parser.index);
}
parser.index += raw.length;
const quoted = raw[0] === '"' || raw[0] === "'";
if (quoted) {
raw = raw.slice(1, -1);
}
value = [
{
start: parser.index - raw.length - (quoted ? 1 : 0),
end: quoted ? parser.index - 1 : parser.index,
type: 'Text',
raw: raw,
data: decode_character_references(raw, true)
}
];
}
if (parser.match_regex(regex_starts_with_quote_characters)) {
e.expected_token(parser.index, '=');
}
return create_attribute(tag.name, tag.loc, start, parser.index, value);
}
/**
* @param {Parser} parser
* @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag | null}
*/
function read_attribute(parser) {
/** @type {AST.JSComment | null} */
// eslint-disable-next-line no-useless-assignment -- it is, in fact, eslint that is useless
let comment = null;
while ((comment = read_comment(parser))) {
parser.root.comments.push(comment);
parser.allow_whitespace();
}
const start = parser.index;
if (parser.eat('{')) {
parser.allow_whitespace();
if (parser.eat('@attach')) {
parser.require_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
/** @type {AST.AttachTag} */
const attachment = {
type: 'AttachTag',
start,
end: parser.index,
expression,
metadata: {
expression: new ExpressionMetadata()
}
};
return attachment;
}
if (parser.eat('...')) {
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
/** @type {AST.SpreadAttribute} */
const spread = {
type: 'SpreadAttribute',
start,
end: parser.index,
expression,
metadata: {
expression: new ExpressionMetadata()
}
};
return spread;
} else {
const id = parser.read_identifier();
if (id.name === '') {
if (
parser.loose &&
(parser.match('#') || parser.match('/') || parser.match('@') || parser.match(':'))
) {
// We're likely in an unclosed opening tag and did read part of a block.
// Return null to not crash the parser so it can continue with closing the tag.
return null;
} else if (parser.loose && parser.match('}')) {
// Likely in the middle of typing, just created the shorthand
} else {
e.attribute_empty_shorthand(start);
}
}
parser.allow_whitespace();
parser.eat('}', true);
/** @type {AST.ExpressionTag} */
const expression = {
type: 'ExpressionTag',
start: id.start,
end: id.end,
expression: id,
metadata: {
expression: new ExpressionMetadata()
}
};
return create_attribute(id.name, id.loc, start, parser.index, expression);
}
}
const tag = read_tag(parser, regex_token_ending_character);
if (!tag.name) return null;
let end = parser.index;
parser.allow_whitespace();
const colon_index = tag.name.indexOf(':');
const type = colon_index !== -1 && get_directive_type(tag.name.slice(0, colon_index));
/** @type {true | AST.ExpressionTag | Array<AST.Text | AST.ExpressionTag>} */
let value = true;
if (parser.eat('=')) {
parser.allow_whitespace();
if (parser.template[parser.index] === '/' && parser.template[parser.index + 1] === '>') {
const char_start = parser.index;
parser.index++; // consume '/'
value = [
{
start: char_start,
end: char_start + 1,
type: 'Text',
raw: '/',
data: '/'
}
];
end = parser.index;
} else {
value = read_attribute_value(parser);
end = parser.index;
}
} else if (parser.match_regex(regex_starts_with_quote_characters)) {
e.expected_token(parser.index, '=');
}
if (type) {
const [directive_name, ...modifiers] = tag.name.slice(colon_index + 1).split('|');
if (directive_name === '') {
e.directive_missing_name({ start, end: start + colon_index + 1 }, tag.name);
}
if (type === 'StyleDirective') {
return {
start,
end,
type,
name: directive_name,
name_loc: tag.loc,
modifiers: /** @type {Array<'important'>} */ (modifiers),
value,
metadata: {
expression: new ExpressionMetadata()
}
};
}
const first_value = value === true ? undefined : Array.isArray(value) ? value[0] : value;
/** @type {Expression | null} */
let expression = null;
if (first_value) {
const attribute_contains_text =
/** @type {any[]} */ (value).length > 1 || first_value.type === 'Text';
if (attribute_contains_text) {
e.directive_invalid_value(/** @type {number} */ (first_value.start));
} else {
// TODO throw a parser error in a future version here if this `[ExpressionTag]` instead of `ExpressionTag`,
// which means stringified value, which isn't allowed for some directives?
expression = first_value.expression;
}
}
const directive = /** @type {AST.Directive} */ ({
start,
end,
type,
name: directive_name,
name_loc: tag.loc,
expression,
metadata: {
expression: new ExpressionMetadata()
}
});
// @ts-expect-error we do this separately from the declaration to avoid upsetting typescript
directive.modifiers = modifiers;
if (directive.type === 'TransitionDirective') {
const direction = tag.name.slice(0, colon_index);
directive.intro = direction === 'in' || direction === 'transition';
directive.outro = direction === 'out' || direction === 'transition';
}
// Directive name is expression, e.g. <p class:isRed />
if (
(directive.type === 'BindDirective' || directive.type === 'ClassDirective') &&
!directive.expression
) {
directive.expression = /** @type {any} */ ({
start: start + colon_index + 1,
end,
type: 'Identifier',
name: directive.name
});
}
return directive;
}
return create_attribute(tag.name, tag.loc, start, end, value);
}
/**
* @param {Parser} parser
* @returns {AST.JSComment | null}
*/
function read_comment(parser) {
const start = parser.index;
if (parser.eat('//')) {
const value = parser.read_until(/\n/);
const end = parser.index;
return {
type: 'Line',
start,
end,
value,
loc: {
start: locator(start),
end: locator(end)
}
};
}
if (parser.eat('/*')) {
const value = parser.read_until(/\*\//);
parser.eat('*/');
const end = parser.index;
return {
type: 'Block',
start,
end,
value,
loc: {
start: locator(start),
end: locator(end)
}
};
}
return null;
}
/**
* @param {string} name
* @returns {any}
*/
function get_directive_type(name) {
if (name === 'use') return 'UseDirective';
if (name === 'animate') return 'AnimateDirective';
if (name === 'bind') return 'BindDirective';
if (name === 'class') return 'ClassDirective';
if (name === 'style') return 'StyleDirective';
if (name === 'on') return 'OnDirective';
if (name === 'let') return 'LetDirective';
if (name === 'in' || name === 'out' || name === 'transition') return 'TransitionDirective';
return false;
}
/**
* @param {Parser} parser
* @return {AST.ExpressionTag | Array<AST.ExpressionTag | AST.Text>}
*/
function read_attribute_value(parser) {
const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null;
if (quote_mark && parser.eat(quote_mark)) {
return [
{
start: parser.index - 1,
end: parser.index - 1,
type: 'Text',
raw: '',
data: ''
}
];
}
/** @type {Array<AST.ExpressionTag | AST.Text>} */
let value;
try {
value = read_sequence(
parser,
() => {
// handle common case of quote marks existing outside of regex for performance reasons
if (quote_mark) return parser.match(quote_mark);
return !!parser.match_regex(regex_invalid_unquoted_attribute_value);
},
'in attribute value'
);
} catch (/** @type {any} */ error) {
if (error.code === 'js_parse_error') {
// if the attribute value didn't close + self-closing tag
// eg: `<Component test={{a:1} />`
// acorn may throw a `Unterminated regular expression` because of `/>`
const pos = error.position?.[0];
if (pos !== undefined && parser.template.slice(pos - 1, pos + 1) === '/>') {
parser.index = pos;
e.expected_token(pos, quote_mark || '}');
}
}
throw error;
}
if (value.length === 0 && !quote_mark) {
e.expected_attribute_value(parser.index);
}
if (quote_mark) parser.index += 1;
if (quote_mark || value.length > 1 || value[0].type === 'Text') {
return value;
} else {
return value[0];
}
}
/**
* @param {Parser} parser
* @param {() => boolean} done
* @param {string} location
* @returns {any[]}
*/
function read_sequence(parser, done, location) {
/** @type {AST.Text} */
let current_chunk = {
start: parser.index,
end: -1,
type: 'Text',
raw: '',
data: ''
};
/** @type {Array<AST.Text | AST.ExpressionTag>} */
const chunks = [];
/** @param {number} end */
function flush(end) {
if (end > current_chunk.start) {
current_chunk.raw = parser.template.slice(current_chunk.start, end);
current_chunk.data = decode_character_references(current_chunk.raw, true);
current_chunk.end = end;
chunks.push(current_chunk);
}
}
while (parser.index < parser.template.length) {
const index = parser.index;
if (done()) {
flush(parser.index);
return chunks;
} else if (parser.eat('{')) {
if (parser.match('#')) {
const index = parser.index - 1;
parser.eat('#');
const name = parser.read_until(/[^a-z]/);
e.block_invalid_placement(index, name, location);
} else if (parser.match('@')) {
const index = parser.index - 1;
parser.eat('@');
const name = parser.read_until(/[^a-z]/);
e.tag_invalid_placement(index, name, location);
}
flush(parser.index - 1);
parser.allow_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
/** @type {AST.ExpressionTag} */
const chunk = {
type: 'ExpressionTag',
start: index,
end: parser.index,
expression,
metadata: {
expression: new ExpressionMetadata()
}
};
chunks.push(chunk);
current_chunk = {
start: parser.index,
end: -1,
type: 'Text',
raw: '',
data: ''
};
} else {
parser.index++;
}
}
if (parser.loose) {
return chunks;
} else {
e.unexpected_eof(parser.template.length);
}
}
/**
* @param {Parser} parser
* @param {RegExp} regex
* @returns {Identifier & { start: number, end: number, loc: SourceLocation }}
*/
function read_tag(parser, regex) {
const start = parser.index;
const name = parser.read_until(regex);
const end = parser.index;
return {
type: 'Identifier',
name,
start,
end,
loc: {
start: locator(start),
end: locator(end)
}
};
}
+17
View File
@@ -0,0 +1,17 @@
/** @import { Parser } from '../index.js' */
import element from './element.js';
import tag from './tag.js';
import text from './text.js';
/** @param {Parser} parser */
export default function fragment(parser) {
if (parser.match('<')) {
return element;
}
if (parser.match('{')) {
return tag;
}
return text;
}
+751
View File
@@ -0,0 +1,751 @@
/** @import { ArrowFunctionExpression, Expression, Identifier, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import { walk } from 'zimmerframe';
import * as e from '../../../errors.js';
import { ExpressionMetadata } from '../../nodes.js';
import { parse_expression_at } from '../acorn.js';
import read_pattern from '../read/context.js';
import read_expression, { get_loose_identifier } from '../read/expression.js';
import { create_fragment } from '../utils/create.js';
import { match_bracket } from '../utils/bracket.js';
const regex_whitespace_with_closing_curly_brace = /\s*}/y;
const pointy_bois = { '<': '>' };
/** @param {Parser} parser */
export default function tag(parser) {
const start = parser.index;
parser.index += 1;
parser.allow_whitespace();
if (parser.eat('#')) return open(parser);
if (parser.eat(':')) return next(parser);
if (parser.eat('@')) return special(parser);
if (parser.match('/')) {
if (!parser.match('/*') && !parser.match('//')) {
parser.eat('/');
return close(parser);
}
}
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
parser.append({
type: 'ExpressionTag',
start,
end: parser.index,
expression,
metadata: {
expression: new ExpressionMetadata()
}
});
}
/** @param {Parser} parser */
function open(parser) {
let start = parser.index - 2;
while (parser.template[start] !== '{') start -= 1;
if (parser.eat('if')) {
parser.require_whitespace();
/** @type {AST.IfBlock} */
const block = parser.append({
type: 'IfBlock',
elseif: false,
start,
end: -1,
test: read_expression(parser),
consequent: create_fragment(),
alternate: null,
metadata: {
expression: new ExpressionMetadata()
}
});
parser.allow_whitespace();
parser.eat('}', true);
parser.stack.push(block);
parser.fragments.push(block.consequent);
return;
}
if (parser.eat('each')) {
parser.require_whitespace();
const template = parser.template;
let end = parser.template.length;
/** @type {Expression | undefined} */
let expression;
// we have to do this loop because `{#each x as { y = z }}` fails to parse —
// the `as { y = z }` is treated as an Expression but it's actually a Pattern.
// the 'fix' is to backtrack and hide everything from the `as` onwards, until
// we get a valid expression
while (!expression) {
try {
expression = read_expression(parser, undefined, true);
} catch (err) {
end = /** @type {any} */ (err).position[0] - 2;
while (end > start && parser.template.slice(end, end + 2) !== 'as') {
end -= 1;
}
if (end <= start) {
if (parser.loose) {
expression = get_loose_identifier(parser);
if (expression) {
break;
}
}
throw err;
}
// @ts-expect-error parser.template is meant to be readonly, this is a special case
parser.template = template.slice(0, end);
}
}
// @ts-expect-error
parser.template = template;
parser.allow_whitespace();
// {#each} blocks must declare a context {#each list as item}
if (!parser.match('as')) {
// this could be a TypeScript assertion that was erroneously eaten.
if (expression.type === 'SequenceExpression') {
expression = expression.expressions[0];
}
let assertion = null;
let end = expression.end;
expression = walk(expression, null, {
// @ts-expect-error
TSAsExpression(node, context) {
if (node.end === /** @type {Expression} */ (expression).end) {
assertion = node;
end = node.expression.end;
return node.expression;
}
context.next();
}
});
expression.end = end;
if (assertion) {
// we can't reset `parser.index` to `expression.expression.end` because
// it will ignore any parentheses — we need to jump through this hoop
let end = /** @type {any} */ (/** @type {any} */ (assertion).typeAnnotation).start - 2;
while (parser.template.slice(end, end + 2) !== 'as') end -= 1;
parser.index = end;
}
}
/** @type {Pattern | null} */
let context = null;
let index;
let key;
if (parser.eat('as')) {
parser.require_whitespace();
context = read_pattern(parser);
} else {
// {#each Array.from({ length: 10 }), i} is read as a sequence expression,
// which is set back above - we now gotta reset the index as a consequence
// to properly read the , i part
parser.index = /** @type {number} */ (expression.end);
}
parser.allow_whitespace();
if (parser.eat(',')) {
parser.allow_whitespace();
index = parser.read_identifier().name;
if (!index) {
e.expected_identifier(parser.index);
}
parser.allow_whitespace();
}
if (parser.eat('(')) {
parser.allow_whitespace();
key = read_expression(parser, '(');
parser.allow_whitespace();
parser.eat(')', true);
parser.allow_whitespace();
}
const matches = parser.eat('}', true, false);
if (!matches) {
// Parser may have read the `as` as part of the expression (e.g. in `{#each foo. as x}`)
if (parser.template.slice(parser.index - 4, parser.index) === ' as ') {
const prev_index = parser.index;
context = read_pattern(parser);
parser.eat('}', true);
expression = {
type: 'Identifier',
name: '',
start: expression.start,
end: prev_index - 4
};
} else {
parser.eat('}', true); // rerun to produce the parser error
}
}
/** @type {AST.EachBlock} */
const block = parser.append({
type: 'EachBlock',
start,
end: -1,
expression,
body: create_fragment(),
context,
index,
key,
metadata: /** @type {any} */ (null) // filled in later
});
parser.stack.push(block);
parser.fragments.push(block.body);
return;
}
if (parser.eat('await')) {
parser.require_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
/** @type {AST.AwaitBlock} */
const block = parser.append({
type: 'AwaitBlock',
start,
end: -1,
expression,
value: null,
error: null,
pending: null,
then: null,
catch: null,
metadata: {
expression: new ExpressionMetadata()
}
});
if (parser.eat('then')) {
if (parser.match_regex(regex_whitespace_with_closing_curly_brace)) {
parser.allow_whitespace();
} else {
parser.require_whitespace();
block.value = read_pattern(parser);
parser.allow_whitespace();
}
block.then = create_fragment();
parser.fragments.push(block.then);
} else if (parser.eat('catch')) {
if (parser.match_regex(regex_whitespace_with_closing_curly_brace)) {
parser.allow_whitespace();
} else {
parser.require_whitespace();
block.error = read_pattern(parser);
parser.allow_whitespace();
}
block.catch = create_fragment();
parser.fragments.push(block.catch);
} else {
block.pending = create_fragment();
parser.fragments.push(block.pending);
}
const matches = parser.eat('}', true, false);
// Parser may have read the `then/catch` as part of the expression (e.g. in `{#await foo. then x}`)
if (!matches) {
if (parser.template.slice(parser.index - 6, parser.index) === ' then ') {
const prev_index = parser.index;
block.value = read_pattern(parser);
parser.eat('}', true);
block.expression = {
type: 'Identifier',
name: '',
start: expression.start,
end: prev_index - 6
};
block.then = block.pending;
block.pending = null;
} else if (parser.template.slice(parser.index - 7, parser.index) === ' catch ') {
const prev_index = parser.index;
block.error = read_pattern(parser);
parser.eat('}', true);
block.expression = {
type: 'Identifier',
name: '',
start: expression.start,
end: prev_index - 7
};
block.catch = block.pending;
block.pending = null;
} else {
parser.eat('}', true); // rerun to produce the parser error
}
}
parser.stack.push(block);
return;
}
if (parser.eat('key')) {
parser.require_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
/** @type {AST.KeyBlock} */
const block = parser.append({
type: 'KeyBlock',
start,
end: -1,
expression,
fragment: create_fragment(),
metadata: {
expression: new ExpressionMetadata()
}
});
parser.stack.push(block);
parser.fragments.push(block.fragment);
return;
}
if (parser.eat('snippet')) {
parser.require_whitespace();
const id = parser.read_identifier();
if (id.name === '' && !parser.loose) {
e.expected_identifier(parser.index);
}
parser.allow_whitespace();
const params_start = parser.index;
// snippets could have a generic signature, e.g. `#snippet foo<T>(...)`
/** @type {string | undefined} */
let type_params;
// if we match a generic opening
if (parser.ts && parser.match('<')) {
const start = parser.index;
const end = match_bracket(parser, start, pointy_bois);
type_params = parser.template.slice(start + 1, end - 1);
parser.index = end;
}
parser.allow_whitespace();
const matched = parser.eat('(', true, false);
if (matched) {
let parentheses = 1;
while (parser.index < parser.template.length && (!parser.match(')') || parentheses !== 1)) {
if (parser.match('(')) parentheses++;
if (parser.match(')')) parentheses--;
parser.index += 1;
}
parser.eat(')', true);
}
const prelude = parser.template.slice(0, params_start).replace(/\S/g, ' ');
const params = parser.template.slice(params_start, parser.index);
let function_expression = matched
? /** @type {ArrowFunctionExpression} */ (
parse_expression_at(
prelude + `${params} => {}`,
parser.root.comments,
parser.ts,
params_start
)
)
: { params: [] };
parser.allow_whitespace();
parser.eat('}', true);
/** @type {AST.SnippetBlock} */
const block = parser.append({
type: 'SnippetBlock',
start,
end: -1,
expression: id,
typeParams: type_params,
parameters: function_expression.params,
body: create_fragment(),
metadata: {
can_hoist: false,
sites: new Set()
}
});
parser.stack.push(block);
parser.fragments.push(block.body);
return;
}
e.expected_block_type(parser.index);
}
/** @param {Parser} parser */
function next(parser) {
const start = parser.index - 1;
const block = parser.current(); // TODO type should not be TemplateNode, that's much too broad
if (block.type === 'IfBlock') {
if (!parser.eat('else')) e.expected_token(start, '{:else} or {:else if}');
if (parser.eat('if')) e.block_invalid_elseif(start);
parser.allow_whitespace();
parser.fragments.pop();
block.alternate = create_fragment();
parser.fragments.push(block.alternate);
// :else if
if (parser.eat('if')) {
parser.require_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
let elseif_start = start - 1;
while (parser.template[elseif_start] !== '{') elseif_start -= 1;
/** @type {AST.IfBlock} */
const child = parser.append({
start: elseif_start,
end: -1,
type: 'IfBlock',
elseif: true,
test: expression,
consequent: create_fragment(),
alternate: null,
metadata: {
expression: new ExpressionMetadata()
}
});
parser.stack.push(child);
parser.fragments.pop();
parser.fragments.push(child.consequent);
} else {
// :else
parser.allow_whitespace();
parser.eat('}', true);
}
return;
}
if (block.type === 'EachBlock') {
if (!parser.eat('else')) e.expected_token(start, '{:else}');
parser.allow_whitespace();
parser.eat('}', true);
block.fallback = create_fragment();
parser.fragments.pop();
parser.fragments.push(block.fallback);
return;
}
if (block.type === 'AwaitBlock') {
if (parser.eat('then')) {
if (block.then) {
e.block_duplicate_clause(start, '{:then}');
}
if (!parser.eat('}')) {
parser.require_whitespace();
block.value = read_pattern(parser);
parser.allow_whitespace();
parser.eat('}', true);
}
block.then = create_fragment();
parser.fragments.pop();
parser.fragments.push(block.then);
return;
}
if (parser.eat('catch')) {
if (block.catch) {
e.block_duplicate_clause(start, '{:catch}');
}
if (!parser.eat('}')) {
parser.require_whitespace();
block.error = read_pattern(parser);
parser.allow_whitespace();
parser.eat('}', true);
}
block.catch = create_fragment();
parser.fragments.pop();
parser.fragments.push(block.catch);
return;
}
e.expected_token(start, '{:then ...} or {:catch ...}');
}
e.block_invalid_continuation_placement(start);
}
/** @param {Parser} parser */
function close(parser) {
const start = parser.index - 1;
let block = parser.current();
/** Only relevant/reached for loose parsing mode */
let matched;
switch (block.type) {
case 'IfBlock':
matched = parser.eat('if', true, false);
if (!matched) {
block.end = start - 1;
parser.pop();
close(parser);
return;
}
parser.allow_whitespace();
parser.eat('}', true);
while (block.elseif) {
block.end = parser.index;
parser.stack.pop();
block = /** @type {AST.IfBlock} */ (parser.current());
}
block.end = parser.index;
parser.pop();
return;
case 'EachBlock':
matched = parser.eat('each', true, false);
break;
case 'KeyBlock':
matched = parser.eat('key', true, false);
break;
case 'AwaitBlock':
matched = parser.eat('await', true, false);
break;
case 'SnippetBlock':
matched = parser.eat('snippet', true, false);
break;
case 'RegularElement':
if (parser.loose) {
matched = false;
} else {
// TODO handle implicitly closed elements
e.block_unexpected_close(start);
}
break;
default:
e.block_unexpected_close(start);
}
if (!matched) {
block.end = start - 1;
parser.pop();
close(parser);
return;
}
parser.allow_whitespace();
parser.eat('}', true);
block.end = parser.index;
parser.pop();
}
/** @param {Parser} parser */
function special(parser) {
let start = parser.index;
while (parser.template[start] !== '{') start -= 1;
if (parser.eat('html')) {
// {@html content} tag
parser.require_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
parser.append({
type: 'HtmlTag',
start,
end: parser.index,
expression,
metadata: {
expression: new ExpressionMetadata()
}
});
return;
}
if (parser.eat('debug')) {
/** @type {Identifier[]} */
let identifiers;
// Implies {@debug} which indicates "debug all"
if (parser.read(regex_whitespace_with_closing_curly_brace)) {
identifiers = [];
} else {
const expression = read_expression(parser);
identifiers =
expression.type === 'SequenceExpression'
? /** @type {Identifier[]} */ (expression.expressions)
: [/** @type {Identifier} */ (expression)];
identifiers.forEach(
/** @param {any} node */ (node) => {
if (node.type !== 'Identifier') {
e.debug_tag_invalid_arguments(/** @type {number} */ (node.start));
}
}
);
parser.allow_whitespace();
parser.eat('}', true);
}
parser.append({
type: 'DebugTag',
start,
end: parser.index,
identifiers
});
return;
}
if (parser.eat('const')) {
parser.require_whitespace();
const id = read_pattern(parser);
parser.allow_whitespace();
parser.eat('=', true);
parser.allow_whitespace();
const expression_start = parser.index;
const init = read_expression(parser);
if (
init.type === 'SequenceExpression' &&
!parser.template.substring(expression_start, init.start).includes('(')
) {
// const a = (b, c) is allowed but a = b, c = d is not;
e.const_tag_invalid_expression(init);
}
parser.allow_whitespace();
parser.eat('}', true);
parser.append({
type: 'ConstTag',
start,
end: parser.index,
declaration: {
type: 'VariableDeclaration',
kind: 'const',
declarations: [{ type: 'VariableDeclarator', id, init, start: id.start, end: init.end }],
start: start + 2, // start at const, not at @const
end: parser.index - 1
},
metadata: {
expression: new ExpressionMetadata()
}
});
return;
}
if (parser.eat('render')) {
// {@render foo(...)}
parser.require_whitespace();
const expression = read_expression(parser);
if (
expression.type !== 'CallExpression' &&
(expression.type !== 'ChainExpression' || expression.expression.type !== 'CallExpression')
) {
e.render_tag_invalid_expression(expression);
}
parser.allow_whitespace();
parser.eat('}', true);
parser.append({
type: 'RenderTag',
start,
end: parser.index,
expression: /** @type {AST.RenderTag['expression']} */ (expression),
metadata: {
expression: new ExpressionMetadata(),
dynamic: false,
arguments: [],
path: [],
snippets: new Set()
}
});
return;
}
e.expected_tag(parser.index);
}
+23
View File
@@ -0,0 +1,23 @@
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import { decode_character_references } from '../utils/html.js';
/** @param {Parser} parser */
export default function text(parser) {
const start = parser.index;
while (parser.index < parser.template.length && !parser.match('<') && !parser.match('{')) {
parser.index++;
}
const data = parser.template.slice(start, parser.index);
/** @type {AST.Text} */
parser.append({
type: 'Text',
start,
end: parser.index,
raw: data,
data: decode_character_references(data, false)
});
}
+215
View File
@@ -0,0 +1,215 @@
/** @import { Parser } from '../index.js' */
import * as e from '../../../errors.js';
/**
* @param {number} num
* @returns {number} Infinity if {@link num} is negative, else {@link num}.
*/
function infinity_if_negative(num) {
if (num < 0) {
return Infinity;
}
return num;
}
/**
* @param {string} string The string to search.
* @param {number} search_start_index The index to start searching at.
* @param {"'" | '"' | '`'} string_start_char The character that started this string.
* @returns {number} The index of the end of this string expression, or `Infinity` if not found.
*/
function find_string_end(string, search_start_index, string_start_char) {
let string_to_search;
if (string_start_char === '`') {
string_to_search = string;
} else {
// we could slice at the search start index, but this way the index remains valid
string_to_search = string.slice(
0,
infinity_if_negative(string.indexOf('\n', search_start_index))
);
}
return find_unescaped_char(string_to_search, search_start_index, string_start_char);
}
/**
* @param {string} string The string to search.
* @param {number} search_start_index The index to start searching at.
* @returns {number} The index of the end of this regex expression, or `Infinity` if not found.
*/
function find_regex_end(string, search_start_index) {
return find_unescaped_char(string, search_start_index, '/');
}
/**
*
* @param {string} string The string to search.
* @param {number} search_start_index The index to begin the search at.
* @param {string} char The character to search for.
* @returns {number} The index of the first unescaped instance of {@link char}, or `Infinity` if not found.
*/
function find_unescaped_char(string, search_start_index, char) {
let i = search_start_index;
while (true) {
const found_index = string.indexOf(char, i);
if (found_index === -1) {
return Infinity;
}
if (count_leading_backslashes(string, found_index - 1) % 2 === 0) {
return found_index;
}
i = found_index + 1;
}
}
/**
* Count consecutive leading backslashes before {@link search_start_index}.
*
* @example
* ```js
* count_leading_backslashes('\\\\\\foo', 2); // 3 (the backslashes have to be escaped in the string literal, there are three in reality)
* ```
*
* @param {string} string The string to search.
* @param {number} search_start_index The index to begin the search at.
*/
function count_leading_backslashes(string, search_start_index) {
let i = search_start_index;
let count = 0;
while (string[i] === '\\') {
count++;
i--;
}
return count;
}
/**
* Finds the corresponding closing bracket, ignoring brackets found inside comments, strings, or regex expressions.
* @param {string} template The string to search.
* @param {number} index The index to begin the search at.
* @param {string} open The opening bracket (ex: `'{'` will search for `'}'`).
* @returns {number | undefined} The index of the closing bracket, or undefined if not found.
*/
export function find_matching_bracket(template, index, open) {
const close = default_brackets[open];
let brackets = 1;
let i = index;
while (brackets > 0 && i < template.length) {
const char = template[i];
switch (char) {
case "'":
case '"':
case '`':
i = find_string_end(template, i + 1, char) + 1;
continue;
case '/': {
const next_char = template[i + 1];
if (!next_char) continue;
if (next_char === '/') {
i = infinity_if_negative(template.indexOf('\n', i + 1)) + '\n'.length;
continue;
}
if (next_char === '*') {
i = infinity_if_negative(template.indexOf('*/', i + 1)) + '*/'.length;
continue;
}
i = find_regex_end(template, i + 1) + '/'.length;
continue;
}
default: {
const char = template[i];
if (char === open) {
brackets++;
} else if (char === close) {
brackets--;
}
if (brackets === 0) {
return i;
}
i++;
}
}
}
return undefined;
}
/** @type {Record<string, string>} */
const default_brackets = {
'{': '}',
'(': ')',
'[': ']'
};
const default_close = new Set(Object.values(default_brackets));
/**
* @param {Parser} parser
* @param {number} start
* @param {Record<string, string>} brackets
*/
export function match_bracket(parser, start, brackets = default_brackets) {
const close = brackets === default_brackets ? default_close : new Set(Object.values(brackets));
const bracket_stack = [];
let i = start;
while (i < parser.template.length) {
let char = parser.template[i++];
if (char === "'" || char === '"' || char === '`') {
i = match_quote(parser, i, char);
continue;
}
if (char in brackets) {
bracket_stack.push(char);
} else if (close.has(char)) {
const popped = /** @type {string} */ (bracket_stack.pop());
const expected = /** @type {string} */ (brackets[popped]);
if (char !== expected) {
e.expected_token(i - 1, expected);
}
if (bracket_stack.length === 0) {
return i;
}
}
}
e.unexpected_eof(parser.template.length);
}
/**
* @param {Parser} parser
* @param {number} start
* @param {string} quote
*/
function match_quote(parser, start, quote) {
let is_escaped = false;
let i = start;
while (i < parser.template.length) {
const char = parser.template[i++];
if (is_escaped) {
is_escaped = false;
continue;
}
if (char === quote) {
return i;
}
if (char === '\\') {
is_escaped = true;
}
if (quote === '`' && char === '$' && parser.template[i] === '{') {
i = match_bracket(parser, i);
}
}
e.unterminated_string_constant(start);
}
+16
View File
@@ -0,0 +1,16 @@
/** @import { AST } from '#compiler' */
/**
* @param {any} transparent
* @returns {AST.Fragment}
*/
export function create_fragment(transparent = false) {
return {
type: 'Fragment',
nodes: [],
metadata: {
transparent,
dynamic: false
}
};
}
File diff suppressed because it is too large Load Diff
+281
View File
@@ -0,0 +1,281 @@
/**
* @param {string} name
* @param {string[]} names
* @returns {string | null}
*/
export default function fuzzymatch(name, names) {
if (names.length === 0) return null;
const set = new FuzzySet(names);
const matches = set.get(name);
return matches && matches[0][0] > 0.7 ? matches[0][1] : null;
}
// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js in 2016
// BSD Licensed (see https://github.com/Glench/fuzzyset.js/issues/10)
const GRAM_SIZE_LOWER = 2;
const GRAM_SIZE_UPPER = 3;
// return an edit distance from 0 to 1
/**
* @param {string} str1
* @param {string} str2
*/
function _distance(str1, str2) {
if (str1 === null && str2 === null) {
throw 'Trying to compare two null values';
}
if (str1 === null || str2 === null) return 0;
str1 = String(str1);
str2 = String(str2);
const distance = levenshtein(str1, str2);
return 1 - distance / Math.max(str1.length, str2.length);
}
// helper functions
/**
* @param {string} str1
* @param {string} str2
*/
function levenshtein(str1, str2) {
/** @type {number[]} */
const current = [];
let prev = 0;
for (let i = 0; i <= str2.length; i++) {
for (let j = 0; j <= str1.length; j++) {
let value;
if (i && j) {
if (str1.charAt(j - 1) === str2.charAt(i - 1)) {
value = prev;
} else {
value = Math.min(current[j], current[j - 1], prev) + 1;
}
} else {
value = i + j;
}
prev = current[j];
current[j] = value;
}
}
return /** @type {number} */ (current.pop());
}
const non_word_regex = /[^\w, ]+/;
/**
* @param {string} value
* @param {any} gram_size
*/
function iterate_grams(value, gram_size = 2) {
const simplified = '-' + value.toLowerCase().replace(non_word_regex, '') + '-';
const len_diff = gram_size - simplified.length;
const results = [];
if (len_diff > 0) {
for (let i = 0; i < len_diff; ++i) {
value += '-';
}
}
for (let i = 0; i < simplified.length - gram_size + 1; ++i) {
results.push(simplified.slice(i, i + gram_size));
}
return results;
}
/**
* @param {string} value
* @param {any} gram_size
*/
function gram_counter(value, gram_size = 2) {
// return an object where key=gram, value=number of occurrences
/** @type {Record<string, number>} */
const result = {};
const grams = iterate_grams(value, gram_size);
let i = 0;
for (i; i < grams.length; ++i) {
if (grams[i] in result) {
result[grams[i]] += 1;
} else {
result[grams[i]] = 1;
}
}
return result;
}
/**
* @param {MatchTuple} a
* @param {MatchTuple} b
*/
function sort_descending(a, b) {
return b[0] - a[0];
}
class FuzzySet {
/** @type {Record<string, string>} */
exact_set = {};
/** @type {Record<string, [number, number][]>} */
match_dict = {};
/** @type {Record<string, number[]>} */
items = {};
/** @param {string[]} arr */
constructor(arr) {
// initialisation
for (let i = GRAM_SIZE_LOWER; i < GRAM_SIZE_UPPER + 1; ++i) {
this.items[i] = [];
}
// add all the items to the set
for (let i = 0; i < arr.length; ++i) {
this.add(arr[i]);
}
}
/** @param {string} value */
add(value) {
const normalized_value = value.toLowerCase();
if (normalized_value in this.exact_set) {
return false;
}
let i = GRAM_SIZE_LOWER;
for (i; i < GRAM_SIZE_UPPER + 1; ++i) {
this._add(value, i);
}
}
/**
* @param {string} value
* @param {number} gram_size
*/
_add(value, gram_size) {
const normalized_value = value.toLowerCase();
const items = this.items[gram_size] || [];
const index = items.length;
items.push(0);
const gram_counts = gram_counter(normalized_value, gram_size);
let sum_of_square_gram_counts = 0;
let gram;
let gram_count;
for (gram in gram_counts) {
gram_count = gram_counts[gram];
sum_of_square_gram_counts += Math.pow(gram_count, 2);
if (gram in this.match_dict) {
this.match_dict[gram].push([index, gram_count]);
} else {
this.match_dict[gram] = [[index, gram_count]];
}
}
const vector_normal = Math.sqrt(sum_of_square_gram_counts);
// @ts-ignore no idea what this code is doing
items[index] = [vector_normal, normalized_value];
this.items[gram_size] = items;
this.exact_set[normalized_value] = value;
}
/** @param {string} value */
get(value) {
const normalized_value = value.toLowerCase();
const result = this.exact_set[normalized_value];
if (result) {
return /** @type {MatchTuple[]} */ ([[1, result]]);
}
// start with high gram size and if there are no results, go to lower gram sizes
for (let gram_size = GRAM_SIZE_UPPER; gram_size >= GRAM_SIZE_LOWER; --gram_size) {
const results = this.__get(value, gram_size);
if (results.length > 0) return results;
}
return null;
}
/**
* @param {string} value
* @param {number} gram_size
* @returns {MatchTuple[]}
*/
__get(value, gram_size) {
const normalized_value = value.toLowerCase();
/** @type {Record<string, number>} */
const matches = {};
const gram_counts = gram_counter(normalized_value, gram_size);
const items = this.items[gram_size];
let sum_of_square_gram_counts = 0;
let gram;
let gram_count;
let i;
let index;
let other_gram_count;
for (gram in gram_counts) {
gram_count = gram_counts[gram];
sum_of_square_gram_counts += Math.pow(gram_count, 2);
if (gram in this.match_dict) {
for (i = 0; i < this.match_dict[gram].length; ++i) {
index = this.match_dict[gram][i][0];
other_gram_count = this.match_dict[gram][i][1];
if (index in matches) {
matches[index] += gram_count * other_gram_count;
} else {
matches[index] = gram_count * other_gram_count;
}
}
}
}
const vector_normal = Math.sqrt(sum_of_square_gram_counts);
/** @type {MatchTuple[]} */
let results = [];
let match_score;
// build a results list of [score, str]
for (const match_index in matches) {
match_score = matches[match_index];
// @ts-ignore no idea what this code is doing
results.push([match_score / (vector_normal * items[match_index][0]), items[match_index][1]]);
}
results.sort(sort_descending);
/** @type {MatchTuple[]} */
let new_results = [];
const end_index = Math.min(50, results.length);
// truncate somewhat arbitrarily to 50
for (let i = 0; i < end_index; ++i) {
// @ts-ignore no idea what this code is doing
new_results.push([_distance(results[i][1], normalized_value), results[i][1]]);
}
results = new_results;
results.sort(sort_descending);
new_results = [];
for (let i = 0; i < results.length; ++i) {
if (results[i][0] === results[0][0]) {
// @ts-ignore no idea what this code is doing
new_results.push([results[i][0], this.exact_set[results[i][1]]]);
}
}
return new_results;
}
}
/** @typedef {[score: number, match: string]} MatchTuple */
+127
View File
@@ -0,0 +1,127 @@
import entities from './entities.js';
const windows_1252 = [
8364, 129, 8218, 402, 8222, 8230, 8224, 8225, 710, 8240, 352, 8249, 338, 141, 381, 143, 144, 8216,
8217, 8220, 8221, 8226, 8211, 8212, 732, 8482, 353, 8250, 339, 157, 382, 376
];
/**
* @param {string} entity_name
* @param {boolean} is_attribute_value
*/
function reg_exp_entity(entity_name, is_attribute_value) {
// https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state
// doesn't decode the html entity which not ends with ; and next character is =, number or alphabet in attribute value.
if (is_attribute_value && !entity_name.endsWith(';')) {
return `${entity_name}\\b(?!=)`;
}
return entity_name;
}
/** @param {boolean} is_attribute_value */
function get_entity_pattern(is_attribute_value) {
const reg_exp_num = '#(?:x[a-fA-F\\d]+|\\d+)(?:;)?';
const reg_exp_entities = Object.keys(entities).map(
/** @param {any} entity_name */ (entity_name) => reg_exp_entity(entity_name, is_attribute_value)
);
const entity_pattern = new RegExp(`&(${reg_exp_num}|${reg_exp_entities.join('|')})`, 'g');
return entity_pattern;
}
const entity_pattern_content = get_entity_pattern(false);
const entity_pattern_attr_value = get_entity_pattern(true);
/**
* @param {string} html
* @param {boolean} is_attribute_value
*/
export function decode_character_references(html, is_attribute_value) {
const entity_pattern = is_attribute_value ? entity_pattern_attr_value : entity_pattern_content;
return html.replace(
entity_pattern,
/**
* @param {any} match
* @param {keyof typeof entities} entity
*/ (match, entity) => {
let code;
// Handle named entities
if (entity[0] !== '#') {
code = entities[entity];
} else if (entity[1] === 'x') {
code = parseInt(entity.substring(2), 16);
} else {
code = parseInt(entity.substring(1), 10);
}
if (!code) {
return match;
}
return String.fromCodePoint(validate_code(code));
}
);
}
const NUL = 0;
// some code points are verboten. If we were inserting HTML, the browser would replace the illegal
// code points with alternatives in some cases - since we're bypassing that mechanism, we need
// to replace them ourselves
//
// Source: http://en.wikipedia.org/wiki/Character_encodings_in_HTML#Illegal_characters
// Also see: https://en.wikipedia.org/wiki/Plane_(Unicode)
// Also see: https://html.spec.whatwg.org/multipage/parsing.html#preprocessing-the-input-stream
/** @param {number} code */
function validate_code(code) {
// line feed becomes generic whitespace
if (code === 10) {
return 32;
}
// ASCII range. (Why someone would use HTML entities for ASCII characters I don't know, but...)
if (code < 128) {
return code;
}
// code points 128-159 are dealt with leniently by browsers, but they're incorrect. We need
// to correct the mistake or we'll end up with missing € signs and so on
if (code <= 159) {
return windows_1252[code - 128];
}
// basic multilingual plane
if (code < 55296) {
return code;
}
// UTF-16 surrogate halves
if (code <= 57343) {
return NUL;
}
// rest of the basic multilingual plane
if (code <= 65535) {
return code;
}
// supplementary multilingual plane 0x10000 - 0x1ffff
if (code >= 65536 && code <= 131071) {
return code;
}
// supplementary ideographic plane 0x20000 - 0x2ffff
if (code >= 131072 && code <= 196607) {
return code;
}
// supplementary special-purpose plane 0xe0000 - 0xe07f and 0xe0100 - 0xe01ef
if ((code >= 917504 && code <= 917631) || (code >= 917760 && code <= 917999)) {
return code;
}
return NUL;
}
@@ -0,0 +1,331 @@
/** @import { ComponentAnalysis } from '../../types.js' */
/** @import { AST } from '#compiler' */
/** @import { Visitors } from 'zimmerframe' */
import { walk } from 'zimmerframe';
import * as e from '../../../errors.js';
import { is_keyframes_node } from '../../css.js';
import { is_global, is_unscoped_pseudo_class } from './utils.js';
/**
* @typedef {{
* keyframes: string[];
* rule: AST.CSS.Rule | null;
* analysis: ComponentAnalysis;
* }} CssState
*/
/**
* @typedef {Visitors<AST.CSS.Node, CssState>} CssVisitors
*/
/**
* True if is `:global`
* @param {AST.CSS.SimpleSelector} simple_selector
*/
function is_global_block_selector(simple_selector) {
return (
simple_selector.type === 'PseudoClassSelector' &&
simple_selector.name === 'global' &&
simple_selector.args === null
);
}
/**
* @param {AST.SvelteNode[]} path
*/
function is_unscoped(path) {
return path
.filter((node) => node.type === 'Rule')
.every((node) => node.metadata.has_global_selectors);
}
/**
*
* @param {Array<AST.CSS.Node>} path
*/
function is_in_global_block(path) {
return path.some((node) => node.type === 'Rule' && node.metadata.is_global_block);
}
/** @type {CssVisitors} */
const css_visitors = {
Atrule(node, context) {
if (is_keyframes_node(node)) {
if (!node.prelude.startsWith('-global-') && !is_in_global_block(context.path)) {
context.state.keyframes.push(node.prelude);
} else if (node.prelude.startsWith('-global-')) {
// we don't check if the block.children.length because the keyframe is still added even if empty
context.state.analysis.css.has_global ||= is_unscoped(context.path);
}
}
context.next();
},
ComplexSelector(node, context) {
context.next(); // analyse relevant selectors first
{
const global = node.children.find(is_global);
if (global) {
const is_nested = context.path.at(-2)?.type === 'PseudoClassSelector';
if (is_nested && !global.selectors[0].args) {
e.css_global_block_invalid_placement(global.selectors[0]);
}
const idx = node.children.indexOf(global);
if (global.selectors[0].args !== null && idx !== 0 && idx !== node.children.length - 1) {
// ensure `:global(...)` is not used in the middle of a selector (but multiple `global(...)` in sequence are ok)
for (let i = idx + 1; i < node.children.length; i++) {
if (!is_global(node.children[i])) {
e.css_global_invalid_placement(global.selectors[0]);
}
}
}
}
}
// ensure `:global(...)` do not lead to invalid css after `:global()` is removed
for (const relative_selector of node.children) {
for (let i = 0; i < relative_selector.selectors.length; i++) {
const selector = relative_selector.selectors[i];
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
const child = selector.args?.children[0].children[0];
// ensure `:global(element)` to be at the first position in a compound selector
if (child?.selectors[0].type === 'TypeSelector' && i !== 0) {
e.css_global_invalid_selector_list(selector);
}
// ensure `:global(.class)` is not followed by a type selector, eg: `:global(.class)element`
if (relative_selector.selectors[i + 1]?.type === 'TypeSelector') {
e.css_type_selector_invalid_placement(relative_selector.selectors[i + 1]);
}
// ensure `:global(...)`contains a single selector
// (standalone :global() with multiple selectors is OK)
if (
selector.args !== null &&
selector.args.children.length > 1 &&
(node.children.length > 1 || relative_selector.selectors.length > 1)
) {
e.css_global_invalid_selector(selector);
}
}
}
}
node.metadata.rule = context.state.rule;
node.metadata.is_global = node.children.every(
({ metadata }) => metadata.is_global || metadata.is_global_like
);
node.metadata.used ||= node.metadata.is_global;
if (
node.metadata.rule?.metadata.parent_rule &&
node.children[0]?.selectors[0]?.type === 'NestingSelector'
) {
const first = node.children[0]?.selectors[1];
const no_nesting_scope =
first?.type !== 'PseudoClassSelector' || is_unscoped_pseudo_class(first);
const parent_is_global = node.metadata.rule.metadata.parent_rule.prelude.children.some(
(child) => child.children.length === 1 && child.children[0].metadata.is_global
);
// mark `&:hover` in `:global(.foo) { &:hover { color: green }}` as used
if (no_nesting_scope && parent_is_global) {
node.metadata.used = true;
}
}
},
RelativeSelector(node, context) {
const parent = /** @type {AST.CSS.ComplexSelector} */ (context.path.at(-1));
if (
node.combinator != null &&
!context.state.rule?.metadata.parent_rule &&
parent.children[0] === node &&
context.path.at(-3)?.type !== 'PseudoClassSelector'
) {
e.css_selector_invalid(node.combinator);
}
node.metadata.is_global = node.selectors.length >= 1 && is_global(node);
if (
node.selectors.length >= 1 &&
node.selectors.every(
(selector) =>
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
)
) {
const first = node.selectors[0];
node.metadata.is_global_like ||=
(first.type === 'PseudoClassSelector' && first.name === 'host') ||
(first.type === 'PseudoElementSelector' &&
[
'view-transition',
'view-transition-group',
'view-transition-old',
'view-transition-new',
'view-transition-image-pair'
].includes(first.name));
}
node.metadata.is_global_like ||=
node.selectors.some(
(child) => child.type === 'PseudoClassSelector' && child.name === 'root'
) &&
// :root.y:has(.x) is not a global selector because while .y is unscoped, .x inside `:has(...)` should be scoped
!node.selectors.some((child) => child.type === 'PseudoClassSelector' && child.name === 'has');
if (node.metadata.is_global_like || node.metadata.is_global) {
// So that nested selectors like `:root:not(.x)` are not marked as unused
for (const child of node.selectors) {
walk(/** @type {AST.CSS.Node} */ (child), null, {
ComplexSelector(node, context) {
node.metadata.used = true;
context.next();
}
});
}
}
context.next();
},
Rule(node, context) {
node.metadata.parent_rule = context.state.rule;
// We gotta allow :global x, :global y because CSS preprocessors might generate that from :global { x, y {...} }
for (const complex_selector of node.prelude.children) {
let is_global_block = false;
for (let selector_idx = 0; selector_idx < complex_selector.children.length; selector_idx++) {
const child = complex_selector.children[selector_idx];
const idx = child.selectors.findIndex(is_global_block_selector);
if (is_global_block) {
// All selectors after :global are unscoped
child.metadata.is_global_like = true;
}
if (idx === 0) {
if (
child.selectors.length > 1 &&
selector_idx === 0 &&
node.metadata.parent_rule === null
) {
e.css_global_block_invalid_modifier_start(child.selectors[1]);
} else {
// `child` starts with `:global`
node.metadata.is_global_block = is_global_block = true;
for (let i = 1; i < child.selectors.length; i++) {
walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, {
ComplexSelector(node) {
node.metadata.used = true;
}
});
}
if (child.combinator && child.combinator.name !== ' ') {
e.css_global_block_invalid_combinator(child, child.combinator.name);
}
const declaration = node.block.children.find((child) => child.type === 'Declaration');
const is_lone_global =
complex_selector.children.length === 1 &&
complex_selector.children[0].selectors.length === 1; // just `:global`, not e.g. `:global x`
if (is_lone_global && node.prelude.children.length > 1) {
// `:global, :global x { z { ... } }` would become `x { z { ... } }` which means `z` is always
// constrained by `x`, which is not what the user intended
e.css_global_block_invalid_list(node.prelude);
}
if (
declaration &&
// :global { color: red; } is invalid, but foo :global { color: red; } is valid
node.prelude.children.length === 1 &&
is_lone_global
) {
e.css_global_block_invalid_declaration(declaration);
}
}
} else if (idx !== -1) {
e.css_global_block_invalid_modifier(child.selectors[idx]);
}
}
if (node.metadata.is_global_block && !is_global_block) {
e.css_global_block_invalid_list(node.prelude);
}
}
const state = { ...context.state, rule: node };
// visit selector list first, to populate child selector metadata
context.visit(node.prelude, state);
for (const selector of node.prelude.children) {
node.metadata.has_global_selectors ||= selector.metadata.is_global;
node.metadata.has_local_selectors ||= !selector.metadata.is_global;
}
// if this rule has a ComplexSelector whose RelativeSelector children are all
// `:global(...)`, and the rule contains declarations (rather than just
// nested rules) then the component as a whole includes global CSS
context.state.analysis.css.has_global ||=
node.metadata.has_global_selectors &&
node.block.children.filter((child) => child.type === 'Declaration').length > 0 &&
is_unscoped(context.path);
// visit block list, so parent rule metadata is populated
context.visit(node.block, state);
},
NestingSelector(node, context) {
const rule = /** @type {AST.CSS.Rule} */ (context.state.rule);
const parent_rule = rule.metadata.parent_rule;
if (!parent_rule) {
// https://developer.mozilla.org/en-US/docs/Web/CSS/Nesting_selector#using_outside_nested_rule
const children = rule.prelude.children;
const selectors = children[0].children[0].selectors;
if (
children.length > 1 ||
selectors.length > 1 ||
selectors[0].type !== 'PseudoClassSelector' ||
selectors[0].name !== 'global' ||
selectors[0].args?.children[0]?.children[0].selectors[0] !== node
) {
e.css_nesting_selector_invalid_placement(node);
}
} else if (
// :global { &.foo { ... } } is invalid
parent_rule.metadata.is_global_block &&
!parent_rule.metadata.parent_rule &&
parent_rule.prelude.children[0].children.length === 1 &&
parent_rule.prelude.children[0].children[0].selectors.length === 1
) {
e.css_global_block_invalid_modifier_start(node);
}
context.next();
}
};
/**
* @param {AST.CSS.StyleSheet} stylesheet
* @param {ComponentAnalysis} analysis
*/
export function analyze_css(stylesheet, analysis) {
/** @type {CssState} */
const css_state = {
keyframes: analysis.css.keyframes,
rule: null,
analysis
};
walk(stylesheet, css_state, css_visitors);
}
File diff suppressed because it is too large Load Diff
+47
View File
@@ -0,0 +1,47 @@
/** @import { Visitors } from 'zimmerframe' */
/** @import { AST } from '#compiler' */
import { walk } from 'zimmerframe';
import * as w from '../../../warnings.js';
import { is_keyframes_node } from '../../css.js';
/**
* @param {AST.CSS.StyleSheet} stylesheet
*/
export function warn_unused(stylesheet) {
walk(stylesheet, { stylesheet }, visitors);
}
/** @type {Visitors<AST.CSS.Node, { stylesheet: AST.CSS.StyleSheet }>} */
const visitors = {
Atrule(node, context) {
if (!is_keyframes_node(node)) {
context.next();
}
},
PseudoClassSelector(node, context) {
if (node.name === 'is' || node.name === 'where') {
context.next();
}
},
ComplexSelector(node, context) {
if (
!node.metadata.used &&
// prevent double-marking of `.unused:is(.unused)`
(context.path.at(-2)?.type !== 'PseudoClassSelector' ||
/** @type {AST.CSS.ComplexSelector} */ (context.path.at(-4))?.metadata.used)
) {
const content = context.state.stylesheet.content;
const text = content.styles.substring(node.start - content.start, node.end - content.start);
w.css_unused_selector(node, text);
}
context.next();
},
Rule(node, context) {
if (node.metadata.is_global_block) {
context.visit(node.prelude);
} else {
context.next();
}
}
};
+177
View File
@@ -0,0 +1,177 @@
/** @import { AST } from '#compiler' */
/** @import { Node } from 'estree' */
const UNKNOWN = {};
/**
* @param {Node} node
* @param {boolean} is_class
* @param {Set<any>} set
* @param {boolean} is_nested
*/
function gather_possible_values(node, is_class, set, is_nested = false) {
if (set.has(UNKNOWN)) {
// no point traversing any further
return;
}
if (node.type === 'Literal') {
set.add(String(node.value));
} else if (node.type === 'ConditionalExpression') {
gather_possible_values(node.consequent, is_class, set, is_nested);
gather_possible_values(node.alternate, is_class, set, is_nested);
} else if (node.type === 'LogicalExpression') {
if (node.operator === '&&') {
// && is a special case, because the only way the left
// hand value can be included is if it's falsy. this is
// a bit of extra work but it's worth it because
// `class={[condition && 'blah']}` is common,
// and we don't want to deopt on `condition`
const left = new Set();
gather_possible_values(node.left, is_class, left, is_nested);
if (left.has(UNKNOWN)) {
// add all non-nullish falsy values, unless this is a `class` attribute that
// will be processed by cslx, in which case falsy values are removed, unless
// they're not inside an array/object (TODO 6.0 remove that last part)
if (!is_class || !is_nested) {
set.add('');
set.add(false);
set.add(NaN);
set.add(0); // -0 and 0n are also falsy, but stringify to '0'
}
} else {
for (const value of left) {
if (!value && value != undefined && (!is_class || !is_nested)) {
set.add(value);
}
}
}
gather_possible_values(node.right, is_class, set, is_nested);
} else {
gather_possible_values(node.left, is_class, set, is_nested);
gather_possible_values(node.right, is_class, set, is_nested);
}
} else if (is_class && node.type === 'ArrayExpression') {
for (const entry of node.elements) {
if (entry) {
gather_possible_values(entry, is_class, set, true);
}
}
} else if (is_class && node.type === 'ObjectExpression') {
for (const property of node.properties) {
if (
property.type === 'Property' &&
!property.computed &&
(property.key.type === 'Identifier' || property.key.type === 'Literal')
) {
set.add(
property.key.type === 'Identifier' ? property.key.name : String(property.key.value)
);
} else {
set.add(UNKNOWN);
}
}
} else {
set.add(UNKNOWN);
}
}
/**
* @param {AST.Text | AST.ExpressionTag} chunk
* @param {boolean} is_class
* @returns {string[] | null}
*/
export function get_possible_values(chunk, is_class) {
const values = new Set();
if (chunk.type === 'Text') {
values.add(chunk.data);
} else {
gather_possible_values(chunk.expression, is_class, values);
}
if (values.has(UNKNOWN)) return null;
return [...values].map((value) => String(value));
}
/**
* Returns all parent rules; root is last
* @param {AST.CSS.Rule | null} rule
*/
export function get_parent_rules(rule) {
const rules = [];
while (rule) {
rules.push(rule);
rule = rule.metadata.parent_rule;
}
return rules;
}
/**
* True if is `:global(...)` or `:global` and no pseudo class that is scoped.
* @param {AST.CSS.RelativeSelector} relative_selector
* @returns {relative_selector is AST.CSS.RelativeSelector & { selectors: [AST.CSS.PseudoClassSelector, ...Array<AST.CSS.PseudoClassSelector | AST.CSS.PseudoElementSelector>] }}
*/
export function is_global(relative_selector) {
const first = relative_selector.selectors[0];
return (
first.type === 'PseudoClassSelector' &&
first.name === 'global' &&
(first.args === null ||
// Only these two selector types keep the whole selector global, because e.g.
// :global(button).x means that the selector is still scoped because of the .x
relative_selector.selectors.every(
(selector) =>
is_unscoped_pseudo_class(selector) || selector.type === 'PseudoElementSelector'
))
);
}
/**
* `true` if is a pseudo class that cannot be or is not scoped
* @param {AST.CSS.SimpleSelector} selector
*/
export function is_unscoped_pseudo_class(selector) {
return (
selector.type === 'PseudoClassSelector' &&
// These make the selector scoped
((selector.name !== 'has' &&
selector.name !== 'is' &&
selector.name !== 'where' &&
// Not is special because we want to scope as specific as possible, but because :not
// inverses the result, we want to leave the unscoped, too. The exception is more than
// one selector in the :not (.e.g :not(.x .y)), then .x and .y should be scoped
(selector.name !== 'not' ||
selector.args === null ||
selector.args.children.every((c) => c.children.length === 1))) ||
// selectors with has/is/where/not can also be global if all their children are global
selector.args === null ||
selector.args.children.every((c) => c.children.every((r) => is_global(r))))
);
}
/**
* True if is `:global(...)` or `:global`, irrespective of whether or not there are any pseudo classes that are scoped.
* Difference to `is_global`: `:global(x):has(y)` is `true` for `is_outer_global` but `false` for `is_global`.
* @param {AST.CSS.RelativeSelector} relative_selector
* @returns {relative_selector is AST.CSS.RelativeSelector & { selectors: [AST.CSS.PseudoClassSelector, ...Array<AST.CSS.PseudoClassSelector | AST.CSS.PseudoElementSelector>] }}
*/
export function is_outer_global(relative_selector) {
const first = relative_selector.selectors[0];
return (
first.type === 'PseudoClassSelector' &&
first.name === 'global' &&
(first.args === null ||
// Only these two selector types can keep the whole selector global, because e.g.
// :global(button).x means that the selector is still scoped because of the .x
relative_selector.selectors.every(
(selector) =>
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
))
);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,47 @@
/**
* @template T
* @param {Array<[T, T]>} edges
* @returns {Array<T>|undefined}
*/
export default function check_graph_for_cycles(edges) {
/** @type {Map<T, T[]>} */
const graph = edges.reduce((g, edge) => {
const [u, v] = edge;
if (!g.has(u)) g.set(u, []);
if (!g.has(v)) g.set(v, []);
g.get(u).push(v);
return g;
}, new Map());
const visited = new Set();
/** @type {Set<T>} */
const on_stack = new Set();
/** @type {Array<Array<T>>} */
const cycles = [];
/**
* @param {T} v
*/
function visit(v) {
visited.add(v);
on_stack.add(v);
graph.get(v)?.forEach((w) => {
if (!visited.has(w)) {
visit(w);
} else if (on_stack.has(w)) {
cycles.push([...on_stack, w]);
}
});
on_stack.delete(v);
}
graph.forEach((_, v) => {
if (!visited.has(v)) {
visit(v);
}
});
return cycles[0];
}
@@ -0,0 +1,15 @@
/** @import { Context } from '../types' */
/** @import { AST } from '#compiler'; */
import * as e from '../../../errors.js';
/**
* @param {AST.AnimateDirective} node
* @param {Context} context
*/
export function AnimateDirective(node, context) {
context.next({ ...context.state, expression: node.metadata.expression });
if (node.metadata.expression.has_await) {
e.illegal_await_expression(node);
}
}
@@ -0,0 +1,11 @@
/** @import { ArrowFunctionExpression } from 'estree' */
/** @import { Context } from '../types' */
import { visit_function } from './shared/function.js';
/**
* @param {ArrowFunctionExpression} node
* @param {Context} context
*/
export function ArrowFunctionExpression(node, context) {
visit_function(node, context);
}
@@ -0,0 +1,31 @@
/** @import { AssignmentExpression } from 'estree' */
/** @import { Context } from '../types' */
import { extract_identifiers, object } from '../../../utils/ast.js';
import { validate_assignment } from './shared/utils.js';
/**
* @param {AssignmentExpression} node
* @param {Context} context
*/
export function AssignmentExpression(node, context) {
validate_assignment(node, node.left, context);
if (context.state.reactive_statement) {
const id = node.left.type === 'MemberExpression' ? object(node.left) : node.left;
if (id !== null) {
for (const id of extract_identifiers(node.left)) {
const binding = context.state.scope.get(id.name);
if (binding) {
context.state.reactive_statement.assignments.add(binding);
}
}
}
}
if (context.state.expression) {
context.state.expression.has_assignment = true;
}
context.next();
}
@@ -0,0 +1,17 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { mark_subtree_dynamic } from './shared/fragment.js';
import * as e from '../../../errors.js';
/**
* @param {AST.AttachTag} node
* @param {Context} context
*/
export function AttachTag(node, context) {
mark_subtree_dynamic(context.path);
context.next({ ...context.state, expression: node.metadata.expression });
if (node.metadata.expression.has_await) {
e.illegal_await_expression(node);
}
}
@@ -0,0 +1,66 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { cannot_be_set_statically, can_delegate_event } from '../../../../utils.js';
import { get_attribute_chunks, is_event_attribute } from '../../../utils/ast.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {AST.Attribute} node
* @param {Context} context
*/
export function Attribute(node, context) {
context.next();
const parent = /** @type {AST.SvelteNode} */ (context.path.at(-1));
if (parent.type === 'RegularElement') {
// special case <option value="" />
if (node.name === 'value' && parent.name === 'option') {
mark_subtree_dynamic(context.path);
}
}
if (is_event_attribute(node)) {
mark_subtree_dynamic(context.path);
}
if (cannot_be_set_statically(node.name)) {
mark_subtree_dynamic(context.path);
}
// class={[...]} or class={{...}} or `class={x}` need clsx to resolve the classes
if (
node.name === 'class' &&
!Array.isArray(node.value) &&
node.value !== true &&
node.value.expression.type !== 'Literal' &&
node.value.expression.type !== 'TemplateLiteral' &&
node.value.expression.type !== 'BinaryExpression'
) {
mark_subtree_dynamic(context.path);
node.metadata.needs_clsx = true;
}
if (node.value !== true) {
for (const chunk of get_attribute_chunks(node.value)) {
if (chunk.type !== 'ExpressionTag') continue;
if (
chunk.expression.type === 'FunctionExpression' ||
chunk.expression.type === 'ArrowFunctionExpression'
) {
continue;
}
}
if (is_event_attribute(node)) {
const parent = context.path.at(-1);
if (parent?.type === 'RegularElement' || parent?.type === 'SvelteElement') {
context.state.analysis.uses_event_attributes = true;
}
node.metadata.delegated =
parent?.type === 'RegularElement' && can_delegate_event(node.name.slice(2));
}
}
}
@@ -0,0 +1,48 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
import * as e from '../../../errors.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {AST.AwaitBlock} node
* @param {Context} context
*/
export function AwaitBlock(node, context) {
validate_block_not_empty(node.pending, context);
validate_block_not_empty(node.then, context);
validate_block_not_empty(node.catch, context);
if (context.state.analysis.runes) {
validate_opening_tag(node, context.state, '#');
if (node.value) {
const start = /** @type {number} */ (node.value.start);
const match = context.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 = context.state.analysis.source
.substring(start - 10, start)
.match(/{(\s*):catch\s+$/);
if (match && match[1] !== '') {
e.block_unexpected_character({ start: start - 10, end: start }, ':');
}
}
}
mark_subtree_dynamic(context.path);
context.visit(node.expression, { ...context.state, expression: node.metadata.expression });
if (node.pending) context.visit(node.pending);
if (node.then) context.visit(node.then);
if (node.catch) context.visit(node.catch);
}
@@ -0,0 +1,150 @@
/** @import { AwaitExpression, Expression, SpreadElement, Property } from 'estree' */
/** @import { Context } from '../types' */
/** @import { AST } from '#compiler' */
import * as e from '../../../errors.js';
/**
* @param {AwaitExpression} node
* @param {Context} context
*/
export function AwaitExpression(node, context) {
const tla = context.state.ast_type === 'instance' && context.state.function_depth === 1;
// preserve context for awaits that precede other expressions in template or `$derived(...)`
if (
is_reactive_expression(
context.path,
context.state.derived_function_depth === context.state.function_depth
) &&
!is_last_evaluated_expression(context.path, node)
) {
context.state.analysis.pickled_awaits.add(node);
}
let suspend = tla;
if (context.state.expression) {
context.state.expression.has_await = true;
suspend = true;
}
// disallow top-level `await` or `await` in template expressions
// unless a) in runes mode and b) opted into `experimental.async`
if (suspend) {
if (!context.state.options.experimental.async) {
e.experimental_async(node);
}
if (!context.state.analysis.runes) {
e.legacy_await_invalid(node);
}
}
context.next();
}
/**
* @param {AST.SvelteNode[]} path
* @param {boolean} in_derived
*/
export function is_reactive_expression(path, in_derived) {
if (in_derived) return true;
let i = path.length;
while (i--) {
const parent = path[i];
if (
parent.type === 'ArrowFunctionExpression' ||
parent.type === 'FunctionExpression' ||
parent.type === 'FunctionDeclaration'
) {
// No reactive expression found between function and await
return false;
}
// @ts-expect-error we could probably use a neater/more robust mechanism
if (parent.metadata) {
return true;
}
}
return false;
}
/**
* @param {AST.SvelteNode[]} path
* @param {Expression | SpreadElement | Property} node
*/
function is_last_evaluated_expression(path, node) {
let i = path.length;
while (i--) {
const parent = path[i];
if (parent.type === 'ConstTag') {
// {@const ...} tags are treated as deriveds and its contents should all get the preserve-reactivity treatment
return false;
}
// @ts-expect-error we could probably use a neater/more robust mechanism
if (parent.metadata) {
return true;
}
switch (parent.type) {
case 'ArrayExpression':
if (node !== parent.elements.at(-1)) return false;
break;
case 'AssignmentExpression':
case 'BinaryExpression':
case 'LogicalExpression':
if (node === parent.left) return false;
break;
case 'CallExpression':
case 'NewExpression':
if (node !== parent.arguments.at(-1)) return false;
break;
case 'ConditionalExpression':
if (node === parent.test) return false;
break;
case 'MemberExpression':
if (parent.computed && node === parent.object) return false;
break;
case 'ObjectExpression':
if (node !== parent.properties.at(-1)) return false;
break;
case 'Property':
if (node === parent.key) return false;
break;
case 'SequenceExpression':
if (node !== parent.expressions.at(-1)) return false;
break;
case 'TaggedTemplateExpression':
if (node !== parent.quasi.expressions.at(-1)) return false;
break;
case 'TemplateLiteral':
if (node !== parent.expressions.at(-1)) return false;
break;
case 'VariableDeclarator':
return true;
default:
return false;
}
node = parent;
}
}
@@ -0,0 +1,280 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import {
extract_all_identifiers_from_expression,
is_text_attribute,
object
} from '../../../utils/ast.js';
import { validate_assignment } from './shared/utils.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { binding_properties } from '../../bindings.js';
import fuzzymatch from '../../1-parse/utils/fuzzymatch.js';
import { is_content_editable_binding, is_svg } from '../../../../utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {AST.BindDirective} node
* @param {Context} context
*/
export function BindDirective(node, context) {
const parent = context.path.at(-1);
if (
parent?.type === 'RegularElement' ||
parent?.type === 'SvelteElement' ||
parent?.type === 'SvelteWindow' ||
parent?.type === 'SvelteDocument' ||
parent?.type === 'SvelteBody'
) {
if (node.name in binding_properties) {
const property = binding_properties[node.name];
if (property.valid_elements && !property.valid_elements.includes(parent.name)) {
e.bind_invalid_target(
node,
node.name,
property.valid_elements.map((valid_element) => `\`<${valid_element}>\``).join(', ')
);
}
if (property.invalid_elements && property.invalid_elements.includes(parent.name)) {
const valid_bindings = Object.entries(binding_properties)
.filter(([_, binding_property]) => {
return (
binding_property.valid_elements?.includes(parent.name) ||
(!binding_property.valid_elements &&
!binding_property.invalid_elements?.includes(parent.name))
);
})
.map(([property_name]) => property_name)
.sort();
e.bind_invalid_name(
node,
node.name,
`Possible bindings for <${parent.name}> are ${valid_bindings.join(', ')}`
);
}
if (parent.name === 'input' && node.name !== 'this') {
const type = /** @type {AST.Attribute | undefined} */ (
parent.attributes.find((a) => a.type === 'Attribute' && a.name === 'type')
);
if (type && !is_text_attribute(type)) {
if (node.name !== 'value' || type.value === true) {
e.attribute_invalid_type(type);
}
} else {
if (node.name === 'checked' && type?.value[0].data !== 'checkbox') {
e.bind_invalid_target(
node,
node.name,
`\`<input type="checkbox">\`${type?.value[0].data === 'radio' ? ` — for \`<input type="radio">\`, use \`bind:group\`` : ''}`
);
}
if (node.name === 'files' && type?.value[0].data !== 'file') {
e.bind_invalid_target(node, node.name, '`<input type="file">`');
}
}
}
if (parent.name === 'select' && node.name !== 'this') {
const multiple = parent.attributes.find(
(a) =>
a.type === 'Attribute' &&
a.name === 'multiple' &&
!is_text_attribute(a) &&
a.value !== true
);
if (multiple) {
e.attribute_invalid_multiple(multiple);
}
}
if (node.name === 'offsetWidth' && is_svg(parent.name)) {
e.bind_invalid_target(
node,
node.name,
`non-\`<svg>\` elements. Use \`bind:clientWidth\` for \`<svg>\` instead`
);
}
if (is_content_editable_binding(node.name)) {
const contenteditable = /** @type {AST.Attribute} */ (
parent.attributes.find((a) => a.type === 'Attribute' && a.name === 'contenteditable')
);
if (!contenteditable) {
e.attribute_contenteditable_missing(node);
} else if (!is_text_attribute(contenteditable) && contenteditable.value !== true) {
e.attribute_contenteditable_dynamic(contenteditable);
}
}
} else {
const match = fuzzymatch(node.name, Object.keys(binding_properties));
if (match) {
const property = binding_properties[match];
if (!property.valid_elements || property.valid_elements.includes(parent.name)) {
e.bind_invalid_name(node, node.name, `Did you mean '${match}'?`);
}
}
e.bind_invalid_name(node, node.name);
}
}
// When dealing with bind getters/setters skip the specific binding validation
// Group bindings aren't supported for getter/setters so we don't need to handle
// the metadata
if (node.expression.type === 'SequenceExpression') {
if (node.name === 'group') {
e.bind_group_invalid_expression(node);
}
let i = /** @type {number} */ (node.expression.start);
let leading_comments_start = /**@type {any}*/ (node.expression.leadingComments?.at(0))?.start;
let leading_comments_end = /**@type {any}*/ (node.expression.leadingComments?.at(-1))?.end;
while (context.state.analysis.source[--i] !== '{') {
if (
context.state.analysis.source[i] === '(' &&
// if the parenthesis is in a leading comment we don't need to throw the error
!(
leading_comments_start &&
leading_comments_end &&
i <= leading_comments_end &&
i >= leading_comments_start
)
) {
e.bind_invalid_parens(node, node.name);
}
}
if (node.expression.expressions.length !== 2) {
e.bind_invalid_expression(node);
}
mark_subtree_dynamic(context.path);
const [get, set] = node.expression.expressions;
// We gotta jump across the getter/setter functions to avoid the expression metadata field being reset to null
// as we want to collect the functions' blocker/async info
context.visit(get.type === 'ArrowFunctionExpression' ? get.body : get, {
...context.state,
expression: node.metadata.expression
});
context.visit(set.type === 'ArrowFunctionExpression' ? set.body : set, {
...context.state,
expression: node.metadata.expression
});
if (node.metadata.expression.has_await) {
e.illegal_await_expression(node);
}
return;
}
validate_assignment(node, node.expression, context);
const assignee = node.expression;
const left = object(assignee);
if (left === null) {
e.bind_invalid_expression(node);
}
const binding = context.state.scope.get(left.name);
node.metadata.binding = binding;
if (assignee.type === 'Identifier') {
// reassignment
if (
node.name !== 'this' && // bind:this also works for regular variables
(!binding ||
(binding.kind !== 'state' &&
binding.kind !== 'raw_state' &&
binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
binding.kind !== 'each' &&
binding.kind !== 'store_sub' &&
!binding.updated)) // TODO wut?
) {
e.bind_invalid_value(node.expression);
}
}
if (node.name === 'group') {
if (!binding) {
throw new Error('Cannot find declaration for bind:group');
}
if (binding.kind === 'snippet') {
e.bind_group_invalid_snippet_parameter(node);
}
// Traverse the path upwards and find all EachBlocks who are (indirectly) contributing to bind:group,
// i.e. one of their declarations is referenced in the binding. This allows group bindings to work
// correctly when referencing a variable declared in an EachBlock by using the index of the each block
// entries as keys.
const each_blocks = [];
const [keypath, expression_ids] = extract_all_identifiers_from_expression(node.expression);
let ids = expression_ids;
let i = context.path.length;
while (i--) {
const parent = context.path[i];
if (parent.type === 'EachBlock') {
const references = ids.filter((id) => parent.metadata.declarations.has(id.name));
if (references.length > 0) {
parent.metadata.contains_group_binding = true;
each_blocks.push(parent);
ids = ids.filter((id) => !references.includes(id));
ids.push(...extract_all_identifiers_from_expression(parent.expression)[1]);
}
}
}
// The identifiers that make up the binding expression form they key for the binding group.
// If the same identifiers in the same order are used in another bind:group, they will be in the same group.
// (there's an edge case where `bind:group={a[i]}` will be in a different group than `bind:group={a[j]}` even when i == j,
// but this is a limitation of the current static analysis we do; it also never worked in Svelte 4)
const bindings = expression_ids.map((id) => context.state.scope.get(id.name));
let group_name;
outer: for (const [[key, b], group] of context.state.analysis.binding_groups) {
if (b.length !== bindings.length || key !== keypath) continue;
for (let i = 0; i < bindings.length; i++) {
if (bindings[i] !== b[i]) continue outer;
}
group_name = group;
}
if (!group_name) {
group_name = context.state.scope.root.unique('binding_group');
context.state.analysis.binding_groups.set([keypath, bindings], group_name);
}
node.metadata = {
binding_group_name: group_name,
parent_each_blocks: each_blocks,
expression: node.metadata.expression
};
}
if (binding?.kind === 'each' && binding.metadata?.inside_rest) {
w.bind_invalid_each_rest(binding.node, binding.node.name);
}
context.next({ ...context.state, expression: node.metadata.expression });
if (node.metadata.expression.has_await) {
e.illegal_await_expression(node);
}
}
@@ -0,0 +1,339 @@
/** @import { ArrowFunctionExpression, CallExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, VariableDeclarator } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { get_rune } from '../../scope.js';
import * as e from '../../../errors.js';
import { get_parent } from '../../../utils/ast.js';
import { is_pure, is_safe_identifier } from './shared/utils.js';
import { dev, locate_node, source } from '../../../state.js';
import * as b from '#compiler/builders';
import { ExpressionMetadata } from '../../nodes.js';
/**
* @param {CallExpression} node
* @param {Context} context
*/
export function CallExpression(node, context) {
const parent = /** @type {AST.SvelteNode} */ (get_parent(context.path, -1));
const rune = get_rune(node, context.state.scope);
if (rune && rune !== '$inspect') {
for (const arg of node.arguments) {
if (arg.type === 'SpreadElement') {
e.rune_invalid_spread(node, rune);
}
}
}
switch (rune) {
case null:
if (!is_safe_identifier(node.callee, context.state.scope)) {
context.state.analysis.needs_context = true;
}
break;
case '$bindable':
if (node.arguments.length > 1) {
e.rune_invalid_arguments_length(node, '$bindable', 'zero or one arguments');
}
if (
parent.type !== 'AssignmentPattern' ||
context.path.at(-3)?.type !== 'ObjectPattern' ||
context.path.at(-4)?.type !== 'VariableDeclarator' ||
get_rune(
/** @type {VariableDeclarator} */ (context.path.at(-4)).init,
context.state.scope
) !== '$props'
) {
e.bindable_invalid_location(node);
}
// We need context in case the bound prop is stale
context.state.analysis.needs_context = true;
break;
case '$host':
if (node.arguments.length > 0) {
e.rune_invalid_arguments(node, '$host');
} else if (context.state.ast_type === 'module' || !context.state.analysis.custom_element) {
e.host_invalid_placement(node);
}
break;
case '$props':
if (context.state.has_props_rune) {
e.props_duplicate(node, rune);
}
context.state.has_props_rune = true;
if (
parent.type !== 'VariableDeclarator' ||
context.state.ast_type !== 'instance' ||
context.state.scope !== context.state.analysis.instance.scope
) {
e.props_invalid_placement(node);
}
if (node.arguments.length > 0) {
e.rune_invalid_arguments(node, rune);
}
break;
case '$props.id': {
const grand_parent = get_parent(context.path, -2);
if (context.state.analysis.props_id) {
e.props_duplicate(node, rune);
}
if (
parent.type !== 'VariableDeclarator' ||
parent.id.type !== 'Identifier' ||
context.state.ast_type !== 'instance' ||
context.state.scope !== context.state.analysis.instance.scope ||
grand_parent.type !== 'VariableDeclaration'
) {
e.props_id_invalid_placement(node);
}
if (node.arguments.length > 0) {
e.rune_invalid_arguments(node, rune);
}
context.state.analysis.props_id = parent.id;
break;
}
case '$state':
case '$state.raw':
case '$derived':
case '$derived.by': {
const valid =
is_variable_declaration(parent, context) ||
is_class_property_definition(parent) ||
is_class_property_assignment_at_constructor_root(parent, context);
if (!valid) {
e.state_invalid_placement(node, rune);
}
if ((rune === '$derived' || rune === '$derived.by') && node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
} else if (node.arguments.length > 1) {
e.rune_invalid_arguments_length(node, rune, 'zero or one arguments');
}
break;
}
case '$effect':
case '$effect.pre':
if (parent.type !== 'ExpressionStatement') {
e.effect_invalid_placement(node);
}
if (node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
}
// `$effect` needs context because Svelte needs to know whether it should re-run
// effects that invalidate themselves, and that's determined by whether we're in runes mode
context.state.analysis.needs_context = true;
break;
case '$effect.tracking':
if (node.arguments.length !== 0) {
e.rune_invalid_arguments(node, rune);
}
break;
case '$effect.root':
if (node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
}
break;
case '$effect.pending':
if (context.state.expression) {
context.state.expression.has_state = true;
}
break;
case '$inspect':
if (node.arguments.length < 1) {
e.rune_invalid_arguments_length(node, rune, 'one or more arguments');
}
break;
case '$inspect().with':
if (node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
}
break;
case '$inspect.trace': {
if (node.arguments.length > 1) {
e.rune_invalid_arguments_length(node, rune, 'zero or one arguments');
}
const grand_parent = context.path.at(-2);
const fn = context.path.at(-3);
if (
parent.type !== 'ExpressionStatement' ||
grand_parent?.type !== 'BlockStatement' ||
!(
fn?.type === 'FunctionDeclaration' ||
fn?.type === 'FunctionExpression' ||
fn?.type === 'ArrowFunctionExpression'
) ||
grand_parent.body[0] !== parent
) {
e.inspect_trace_invalid_placement(node);
}
if (fn.generator) {
e.inspect_trace_generator(node);
}
if (dev) {
if (node.arguments[0]) {
context.state.scope.tracing = b.thunk(/** @type {Expression} */ (node.arguments[0]));
} else {
const label = get_function_label(context.path.slice(0, -2)) ?? 'trace';
const loc = `(${locate_node(fn)})`;
context.state.scope.tracing = b.thunk(b.literal(label + ' ' + loc));
}
context.state.analysis.tracing = true;
}
break;
}
case '$state.eager':
if (node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
}
break;
case '$state.snapshot':
if (node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
}
break;
}
// `$inspect(foo)` or `$derived(foo) should not trigger the `static-state-reference` warning
if (rune === '$derived') {
const expression = new ExpressionMetadata();
context.next({
...context.state,
function_depth: context.state.function_depth + 1,
derived_function_depth: context.state.function_depth + 1,
expression
});
if (expression.has_await) {
context.state.analysis.async_deriveds.add(node);
}
} else if (rune === '$inspect') {
context.next({ ...context.state, function_depth: context.state.function_depth + 1 });
} else {
context.next();
}
if (context.state.expression) {
// TODO We assume that any dependencies are stateful, which isn't necessarily the case — see
// https://github.com/sveltejs/svelte/issues/13266. This check also includes dependencies
// outside the call expression itself (e.g. `{blah && pure()}`) resulting in additional
// false positives, but for now we accept that trade-off
if (!is_pure(node.callee, context) || context.state.expression.dependencies.size > 0) {
context.state.expression.has_call = true;
context.state.expression.has_state = true;
}
}
}
/**
* @param {AST.SvelteNode[]} nodes
*/
function get_function_label(nodes) {
const fn = /** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} */ (
nodes.at(-1)
);
if ((fn.type === 'FunctionDeclaration' || fn.type === 'FunctionExpression') && fn.id != null) {
return fn.id.name;
}
const parent = nodes.at(-2);
if (!parent) return;
if (parent.type === 'CallExpression') {
return source.slice(parent.callee.start, parent.callee.end) + '(...)';
}
if (parent.type === 'Property' && !parent.computed) {
return /** @type {Identifier} */ (parent.key).name;
}
if (parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
return parent.id.name;
}
}
/**
* @param {AST.SvelteNode} parent
* @param {Context} context
*/
function is_variable_declaration(parent, context) {
return parent.type === 'VariableDeclarator' && get_parent(context.path, -3).type !== 'ConstTag';
}
/**
* @param {AST.SvelteNode} parent
*/
function is_class_property_definition(parent) {
return parent.type === 'PropertyDefinition' && !parent.static && !parent.computed;
}
/**
* @param {AST.SvelteNode} node
* @param {Context} context
*/
function is_class_property_assignment_at_constructor_root(node, context) {
if (
node.type === 'AssignmentExpression' &&
node.operator === '=' &&
node.left.type === 'MemberExpression' &&
node.left.object.type === 'ThisExpression' &&
((node.left.property.type === 'Identifier' && !node.left.computed) ||
node.left.property.type === 'PrivateIdentifier' ||
node.left.property.type === 'Literal')
) {
// MethodDefinition (-5) -> FunctionExpression (-4) -> BlockStatement (-3) -> ExpressionStatement (-2) -> AssignmentExpression (-1)
const parent = get_parent(context.path, -5);
return parent?.type === 'MethodDefinition' && parent.kind === 'constructor';
}
return false;
}
@@ -0,0 +1,156 @@
/** @import { AssignmentExpression, CallExpression, ClassBody, PropertyDefinition, Expression, PrivateIdentifier, MethodDefinition } from 'estree' */
/** @import { StateField } from '#compiler' */
/** @import { Context } from '../types' */
import * as b from '#compiler/builders';
import { get_rune } from '../../scope.js';
import * as e from '../../../errors.js';
import { is_state_creation_rune } from '../../../../utils.js';
import { get_name } from '../../nodes.js';
import { regex_invalid_identifier_chars } from '../../patterns.js';
/**
* @param {ClassBody} node
* @param {Context} context
*/
export function ClassBody(node, context) {
if (!context.state.analysis.runes) {
context.next();
return;
}
/** @type {string[]} */
const private_ids = [];
for (const prop of node.body) {
if (
(prop.type === 'MethodDefinition' || prop.type === 'PropertyDefinition') &&
prop.key.type === 'PrivateIdentifier'
) {
private_ids.push(prop.key.name);
}
}
/** @type {Map<string, StateField>} */
const state_fields = new Map();
/** @type {Map<string, Array<MethodDefinition['kind'] | 'prop' | 'assigned_prop'>>} */
const fields = new Map();
context.state.analysis.classes.set(node, state_fields);
/** @type {MethodDefinition | null} */
let constructor = null;
/**
* @param {PropertyDefinition | AssignmentExpression} node
* @param {Expression | PrivateIdentifier} key
* @param {Expression | null | undefined} value
*/
function handle(node, key, value) {
const name = get_name(key);
if (name === null) return;
const rune = get_rune(value, context.state.scope);
if (rune && is_state_creation_rune(rune)) {
if (state_fields.has(name)) {
e.state_field_duplicate(node, name);
}
const _key = (node.type === 'AssignmentExpression' || !node.static ? '' : '@') + name;
const field = fields.get(_key);
// if there's already a method or assigned field, error
if (field && !(field.length === 1 && field[0] === 'prop')) {
e.duplicate_class_field(node, _key);
}
state_fields.set(name, {
node,
type: rune,
// @ts-expect-error for public state this is filled out in a moment
key: key.type === 'PrivateIdentifier' ? key : null,
value: /** @type {CallExpression} */ (value)
});
}
}
for (const child of node.body) {
if (child.type === 'PropertyDefinition' && !child.computed && !child.static) {
handle(child, child.key, child.value);
const key = /** @type {string} */ (get_name(child.key));
const field = fields.get(key);
if (!field) {
fields.set(key, [child.value ? 'assigned_prop' : 'prop']);
continue;
}
e.duplicate_class_field(child, key);
}
if (child.type === 'MethodDefinition') {
if (child.kind === 'constructor') {
constructor = child;
} else if (!child.computed) {
const key = (child.static ? '@' : '') + get_name(child.key);
const field = fields.get(key);
if (!field) {
fields.set(key, [child.kind]);
continue;
}
if (
field.includes(child.kind) ||
field.includes('prop') ||
field.includes('assigned_prop')
) {
e.duplicate_class_field(child, key);
}
if (child.kind === 'get') {
if (field.length === 1 && field[0] === 'set') {
field.push('get');
continue;
}
} else if (child.kind === 'set') {
if (field.length === 1 && field[0] === 'get') {
field.push('set');
continue;
}
} else {
field.push(child.kind);
continue;
}
e.duplicate_class_field(child, key);
}
}
}
if (constructor) {
for (const statement of constructor.value.body.body) {
if (statement.type !== 'ExpressionStatement') continue;
if (statement.expression.type !== 'AssignmentExpression') continue;
const { left, right } = statement.expression;
if (left.type !== 'MemberExpression') continue;
if (left.object.type !== 'ThisExpression') continue;
if (left.computed && left.property.type !== 'Literal') continue;
handle(statement.expression, left.property, right);
}
}
for (const [name, field] of state_fields) {
if (name[0] === '#') {
continue;
}
let deconflicted = name.replace(regex_invalid_identifier_chars, '_');
while (private_ids.includes(deconflicted)) {
deconflicted = '_' + deconflicted;
}
private_ids.push(deconflicted);
field.key = b.private_id(deconflicted);
}
context.next({ ...context.state, state_fields });
}
@@ -0,0 +1,25 @@
/** @import { ClassDeclaration } from 'estree' */
/** @import { Context } from '../types' */
import * as w from '../../../warnings.js';
import { validate_identifier_name } from './shared/utils.js';
/**
* @param {ClassDeclaration} node
* @param {Context} context
*/
export function ClassDeclaration(node, context) {
if (context.state.analysis.runes && node.id !== null) {
validate_identifier_name(context.state.scope.get(node.id.name));
}
// In modules, we allow top-level module scope only, in components, we allow the component scope,
// which is function_depth of 1. With the exception of `new class` which is also not allowed at
// component scope level either.
const allowed_depth = context.state.ast_type === 'module' ? 0 : 1;
if (context.state.scope.function_depth > allowed_depth) {
w.perf_avoid_nested_class(node);
}
context.next();
}
@@ -0,0 +1,13 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {AST.ClassDirective} node
* @param {Context} context
*/
export function ClassDirective(node, context) {
mark_subtree_dynamic(context.path);
context.next({ ...context.state, expression: node.metadata.expression });
}
@@ -0,0 +1,26 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { visit_component } from './shared/component.js';
/**
* @param {AST.Component} node
* @param {Context} context
*/
export function Component(node, context) {
const binding = context.state.scope.get(
node.name.includes('.') ? node.name.slice(0, node.name.indexOf('.')) : node.name
);
node.metadata.dynamic =
context.state.analysis.runes && // Svelte 4 required you to use svelte:component to switch components
binding !== null &&
(binding.kind !== 'normal' || node.name.includes('.'));
if (binding) {
node.metadata.expression.has_state = node.metadata.dynamic;
node.metadata.expression.dependencies.add(binding);
node.metadata.expression.references.add(binding);
}
visit_component(node, context);
}
@@ -0,0 +1,45 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { validate_opening_tag } from './shared/utils.js';
/**
* @param {AST.ConstTag} node
* @param {Context} context
*/
export function ConstTag(node, context) {
if (context.state.analysis.runes) {
validate_opening_tag(node, context.state, '@');
}
const parent = context.path.at(-1);
const grand_parent = context.path.at(-2);
if (
parent?.type !== 'Fragment' ||
(grand_parent?.type !== 'IfBlock' &&
grand_parent?.type !== 'SvelteFragment' &&
grand_parent?.type !== 'Component' &&
grand_parent?.type !== 'SvelteComponent' &&
grand_parent?.type !== 'EachBlock' &&
grand_parent?.type !== 'AwaitBlock' &&
grand_parent?.type !== 'SnippetBlock' &&
grand_parent?.type !== 'SvelteBoundary' &&
grand_parent?.type !== 'KeyBlock' &&
((grand_parent?.type !== 'RegularElement' && grand_parent?.type !== 'SvelteElement') ||
!grand_parent.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot')))
) {
e.const_tag_invalid_placement(node);
}
const declaration = node.declaration.declarations[0];
context.visit(declaration.id);
context.visit(declaration.init, {
...context.state,
expression: node.metadata.expression,
// We're treating this like a $derived under the hood
function_depth: context.state.function_depth + 1,
derived_function_depth: context.state.function_depth + 1
});
}
@@ -0,0 +1,15 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { validate_opening_tag } from './shared/utils.js';
/**
* @param {AST.DebugTag} node
* @param {Context} context
*/
export function DebugTag(node, context) {
if (context.state.analysis.runes) {
validate_opening_tag(node, context.state, '@');
}
context.next();
}
@@ -0,0 +1,97 @@
/** @import { Expression } from 'estree' */
/** @import { AST, Binding } from '#compiler' */
/** @import { Context } from '../types' */
/** @import { Scope } from '../../scope' */
import * as e from '../../../errors.js';
import { extract_identifiers } from '../../../utils/ast.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
/**
* @param {AST.EachBlock} node
* @param {Context} context
*/
export function EachBlock(node, context) {
validate_opening_tag(node, context.state, '#');
validate_block_not_empty(node.body, context);
validate_block_not_empty(node.fallback, context);
const id = node.context;
if (id?.type === 'Identifier' && (id.name === '$state' || id.name === '$derived')) {
// TODO weird that this is necessary
e.state_invalid_placement(node, id.name);
}
if (node.key) {
// treat `{#each items as item, i (i)}` as a normal indexed block, everything else as keyed
node.metadata.keyed =
node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index;
}
if (node.metadata.keyed && !node.context) {
e.each_key_without_as(/** @type {Expression} */ (node.key));
}
// evaluate expression in parent scope
context.visit(node.expression, {
...context.state,
expression: node.metadata.expression,
scope: /** @type {Scope} */ (context.state.scope.parent)
});
context.visit(node.body);
if (node.key) context.visit(node.key);
if (node.fallback) context.visit(node.fallback);
if (!context.state.analysis.runes) {
let mutated =
!!node.context &&
extract_identifiers(node.context).some((id) => {
const binding = context.state.scope.get(id.name);
return !!binding?.mutated;
});
// collect transitive dependencies...
for (const binding of node.metadata.expression.dependencies) {
if (binding.declaration_kind !== 'function') {
collect_transitive_dependencies(binding, node.metadata.transitive_deps);
}
}
// ...and ensure they are marked as state, so they can be turned
// into mutable sources and invalidated
if (mutated) {
for (const binding of node.metadata.transitive_deps) {
if (
binding.kind === 'normal' &&
(binding.declaration_kind === 'const' ||
binding.declaration_kind === 'let' ||
binding.declaration_kind === 'var')
) {
binding.kind = 'state';
}
}
}
}
mark_subtree_dynamic(context.path);
}
/**
* @param {Binding} binding
* @param {Set<Binding>} bindings
* @returns {void}
*/
function collect_transitive_dependencies(binding, bindings) {
if (bindings.has(binding)) {
return;
}
bindings.add(binding);
if (binding.kind === 'legacy_reactive') {
for (const dep of binding.legacy_dependencies) {
collect_transitive_dependencies(dep, bindings);
}
}
}
@@ -0,0 +1,20 @@
/** @import { ExportDefaultDeclaration } from 'estree' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { validate_export } from './shared/utils.js';
/**
* @param {ExportDefaultDeclaration} node
* @param {Context} context
*/
export function ExportDefaultDeclaration(node, context) {
if (!context.state.ast_type /* .svelte.js module */) {
if (node.declaration.type === 'Identifier') {
validate_export(node, context.state.scope, node.declaration.name);
}
} else {
e.module_illegal_default_export(node);
}
context.next();
}
@@ -0,0 +1,70 @@
/** @import { ExportNamedDeclaration, Identifier } from 'estree' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { extract_identifiers } from '../../../utils/ast.js';
/**
* @param {ExportNamedDeclaration} node
* @param {Context} context
*/
export function ExportNamedDeclaration(node, context) {
// visit children, so bindings are correctly initialised
context.next();
if (
context.state.ast_type &&
node.specifiers.some((specifier) =>
specifier.exported.type === 'Identifier'
? specifier.exported.name === 'default'
: specifier.exported.value === 'default'
)
) {
e.module_illegal_default_export(node);
}
if (node.declaration?.type === 'VariableDeclaration') {
// in runes mode, forbid `export let`
if (
context.state.analysis.runes &&
context.state.ast_type === 'instance' &&
node.declaration.kind === 'let'
) {
e.legacy_export_invalid(node);
}
for (const declarator of node.declaration.declarations) {
for (const id of extract_identifiers(declarator.id)) {
const binding = context.state.scope.get(id.name);
if (!binding) continue;
if (binding.kind === 'derived') {
e.derived_invalid_export(node);
}
if ((binding.kind === 'state' || binding.kind === 'raw_state') && binding.reassigned) {
e.state_invalid_export(node);
}
}
}
}
if (context.state.analysis.runes) {
if (node.declaration && context.state.ast_type === 'instance') {
if (
node.declaration.type === 'FunctionDeclaration' ||
node.declaration.type === 'ClassDeclaration'
) {
context.state.analysis.exports.push({
name: /** @type {Identifier} */ (node.declaration.id).name,
alias: null
});
} else if (node.declaration.kind === 'const') {
for (const declarator of node.declaration.declarations) {
for (const node of extract_identifiers(declarator.id)) {
context.state.analysis.exports.push({ name: node.name, alias: null });
}
}
}
}
}
}
@@ -0,0 +1,30 @@
/** @import { ExportSpecifier } from 'estree' */
/** @import { Context } from '../types' */
import { validate_export } from './shared/utils.js';
/**
* @param {ExportSpecifier} node
* @param {Context} context
*/
export function ExportSpecifier(node, context) {
const local_name =
node.local.type === 'Identifier' ? node.local.name : /** @type {string} */ (node.local.value);
const exported_name =
node.exported.type === 'Identifier'
? node.exported.name
: /** @type {string} */ (node.exported.value);
if (context.state.ast_type === 'instance') {
if (context.state.analysis.runes) {
context.state.analysis.exports.push({
name: local_name,
alias: exported_name
});
const binding = context.state.scope.get(local_name);
if (binding) binding.reassigned = true;
}
} else {
validate_export(node, context.state.scope, local_name);
}
}
@@ -0,0 +1,38 @@
/** @import { ExpressionStatement, ImportDeclaration } from 'estree' */
/** @import { Context } from '../types' */
import * as w from '../../../warnings.js';
/**
* @param {ExpressionStatement} node
* @param {Context} context
*/
export function ExpressionStatement(node, context) {
// warn on `new Component({ target: ... })` if imported from a `.svelte` file
if (
node.expression.type === 'NewExpression' &&
node.expression.callee.type === 'Identifier' &&
node.expression.arguments.length === 1 &&
node.expression.arguments[0].type === 'ObjectExpression' &&
node.expression.arguments[0].properties.some(
(p) => p.type === 'Property' && p.key.type === 'Identifier' && p.key.name === 'target'
)
) {
const binding = context.state.scope.get(node.expression.callee.name);
if (binding?.kind === 'normal' && binding.declaration_kind === 'import') {
const declaration = /** @type {ImportDeclaration} */ (binding.initial);
// Theoretically someone could import a class from a `.svelte.js` module, but that's too rare to worry about
if (
/** @type {string} */ (declaration.source.value).endsWith('.svelte') &&
declaration.specifiers.find(
(s) => s.local.name === binding.node.name && s.type === 'ImportDefaultSpecifier'
)
) {
w.legacy_component_creation(node.expression);
}
}
}
context.next();
}
@@ -0,0 +1,26 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { is_tag_valid_with_parent } from '../../../../html-tree-validation.js';
import * as e from '../../../errors.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {AST.ExpressionTag} node
* @param {Context} context
*/
export function ExpressionTag(node, context) {
const in_template = context.path.at(-1)?.type === 'Fragment';
if (in_template && context.state.parent_element) {
const message = is_tag_valid_with_parent('#text', context.state.parent_element);
if (message) {
e.node_invalid_placement(node, message);
}
}
// TODO ideally we wouldn't do this here, we'd just do it on encountering
// an `Identifier` within the tag. But we currently need to handle `{42}` etc
mark_subtree_dynamic(context.path);
context.next({ ...context.state, expression: node.metadata.expression });
}
@@ -0,0 +1,10 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types.js' */
/**
* @param {AST.Fragment} node
* @param {Context} context
*/
export function Fragment(node, context) {
context.next({ ...context.state, fragment: node });
}
@@ -0,0 +1,16 @@
/** @import { FunctionDeclaration } from 'estree' */
/** @import { Context } from '../types' */
import { visit_function } from './shared/function.js';
import { validate_identifier_name } from './shared/utils.js';
/**
* @param {FunctionDeclaration} node
* @param {Context} context
*/
export function FunctionDeclaration(node, context) {
if (context.state.analysis.runes && node.id !== null) {
validate_identifier_name(context.state.scope.get(node.id.name));
}
visit_function(node, context);
}
@@ -0,0 +1,11 @@
/** @import { FunctionExpression } from 'estree' */
/** @import { Context } from '../types' */
import { visit_function } from './shared/function.js';
/**
* @param {FunctionExpression} node
* @param {Context} context
*/
export function FunctionExpression(node, context) {
visit_function(node, context);
}
@@ -0,0 +1,19 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { mark_subtree_dynamic } from './shared/fragment.js';
import { validate_opening_tag } from './shared/utils.js';
/**
* @param {AST.HtmlTag} node
* @param {Context} context
*/
export function HtmlTag(node, context) {
if (context.state.analysis.runes) {
validate_opening_tag(node, context.state, '@');
}
// unfortunately this is necessary in order to fix invalid HTML
mark_subtree_dynamic(context.path);
context.next({ ...context.state, expression: node.metadata.expression });
}
@@ -0,0 +1,194 @@
/** @import { Expression, Identifier } from 'estree' */
/** @import { Context } from '../types' */
import is_reference from 'is-reference';
import { should_proxy } from '../../3-transform/client/utils.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { is_rune } from '../../../../utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
import { get_rune } from '../../scope.js';
import { is_component_node } from '../../nodes.js';
/**
* @param {Identifier} node
* @param {Context} context
*/
export function Identifier(node, context) {
let i = context.path.length;
let parent = /** @type {Expression} */ (context.path[--i]);
if (!is_reference(node, parent)) {
return;
}
mark_subtree_dynamic(context.path);
// If we are using arguments outside of a function, then throw an error
if (
node.name === 'arguments' &&
!context.path.some((n) => n.type === 'FunctionDeclaration' || n.type === 'FunctionExpression')
) {
e.invalid_arguments_usage(node);
}
// `$$slots` exists even in runes mode
if (node.name === '$$slots') {
context.state.analysis.uses_slots = true;
}
if (context.state.analysis.runes) {
if (
is_rune(node.name) &&
context.state.scope.get(node.name) === null &&
context.state.scope.get(node.name.slice(1))?.kind !== 'store_sub'
) {
/** @type {Expression} */
let current = node;
let name = node.name;
while (parent.type === 'MemberExpression') {
if (parent.computed) e.rune_invalid_computed_property(parent);
name += `.${/** @type {Identifier} */ (parent.property).name}`;
current = parent;
parent = /** @type {Expression} */ (context.path[--i]);
if (!is_rune(name)) {
if (name === '$effect.active') {
e.rune_renamed(parent, '$effect.active', '$effect.tracking');
}
if (name === '$state.frozen') {
e.rune_renamed(parent, '$state.frozen', '$state.raw');
}
if (name === '$state.is') {
e.rune_removed(parent, '$state.is');
}
e.rune_invalid_name(parent, name);
}
}
if (parent.type !== 'CallExpression') {
e.rune_missing_parentheses(current);
}
}
}
let binding = context.state.scope.get(node.name);
if (!context.state.analysis.runes) {
if (node.name === '$$props') {
context.state.analysis.uses_props = true;
}
if (node.name === '$$restProps') {
context.state.analysis.uses_rest_props = true;
}
}
if (binding) {
if (context.state.expression) {
context.state.expression.dependencies.add(binding);
context.state.expression.references.add(binding);
context.state.expression.has_state ||=
binding.kind !== 'static' &&
(binding.kind === 'prop' ||
binding.kind === 'bindable_prop' ||
binding.kind === 'rest_prop' ||
!binding.is_function()) &&
!context.state.scope.evaluate(node).is_known;
}
if (
context.state.analysis.runes &&
node !== binding.node &&
context.state.function_depth === binding.scope.function_depth &&
// If we have $state that can be proxied or frozen and isn't re-assigned, then that means
// it's likely not using a primitive value and thus this warning isn't that helpful.
((binding.kind === 'state' &&
(binding.reassigned ||
(binding.initial?.type === 'CallExpression' &&
binding.initial.arguments.length === 1 &&
binding.initial.arguments[0].type !== 'SpreadElement' &&
!should_proxy(binding.initial.arguments[0], context.state.scope)))) ||
binding.kind === 'raw_state' ||
binding.kind === 'derived' ||
binding.kind === 'prop' ||
binding.kind === 'rest_prop') &&
// We're only concerned with reads here
(parent.type !== 'AssignmentExpression' || parent.left !== node) &&
parent.type !== 'UpdateExpression'
) {
let type = 'closure';
let i = context.path.length;
while (i--) {
const parent = context.path[i];
if (
parent.type === 'ArrowFunctionExpression' ||
parent.type === 'FunctionDeclaration' ||
parent.type === 'FunctionExpression'
) {
break;
}
if (
parent.type === 'CallExpression' &&
parent.arguments.includes(/** @type {any} */ (context.path[i + 1]))
) {
const rune = get_rune(parent, context.state.scope);
if (rune === '$state' || rune === '$state.raw') {
type = 'derived';
break;
}
}
}
w.state_referenced_locally(node, node.name, type);
}
if (
context.state.reactive_statement &&
binding.scope === context.state.analysis.module.scope &&
binding.reassigned
) {
w.reactive_declaration_module_script_dependency(node);
}
if (binding.metadata?.is_template_declaration && context.state.options.experimental.async) {
let snippet_name;
// Find out if this references a {@const ...} declaration of an implicit children snippet
// when it is itself inside a snippet block at the same level. If so, error.
for (let i = context.path.length - 1; i >= 0; i--) {
const parent = context.path[i];
const grand_parent = context.path[i - 1];
if (parent.type === 'SnippetBlock') {
snippet_name = parent.expression.name;
} else if (
snippet_name &&
grand_parent &&
parent.type === 'Fragment' &&
(is_component_node(grand_parent) ||
(grand_parent.type === 'SvelteBoundary' &&
(snippet_name === 'failed' || snippet_name === 'pending')))
) {
if (
is_component_node(grand_parent)
? grand_parent.metadata.scopes.default === binding.scope
: context.state.scopes.get(parent) === binding.scope
) {
e.const_tag_invalid_reference(node, node.name);
} else {
break;
}
}
}
}
}
}
@@ -0,0 +1,46 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { mark_subtree_dynamic } from './shared/fragment.js';
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
/**
* @param {AST.IfBlock} node
* @param {Context} context
*/
export function IfBlock(node, context) {
validate_block_not_empty(node.consequent, context);
validate_block_not_empty(node.alternate, context);
if (context.state.analysis.runes) {
validate_opening_tag(node, context.state, node.elseif ? ':' : '#');
}
mark_subtree_dynamic(context.path);
context.visit(node.test, {
...context.state,
expression: node.metadata.expression
});
context.visit(node.consequent);
if (node.alternate) context.visit(node.alternate);
// Check if we can flatten branches
const alt = node.alternate;
if (alt && alt.nodes.length === 1 && alt.nodes[0].type === 'IfBlock' && alt.nodes[0].elseif) {
const elseif = alt.nodes[0];
// Don't flatten if this else-if has an await expression or new blockers
// TODO would be nice to check the await expression itself to see if it's awaiting the same thing as a previous if expression
if (
!elseif.metadata.expression.has_await &&
!elseif.metadata.expression.has_more_blockers_than(node.metadata.expression)
) {
// Roll the existing flattened branches (if any) into this one, then delete those of the else-if block
// to avoid processing them multiple times as we walk down the chain during code transformation.
node.metadata.flattened = [elseif, ...(elseif.metadata.flattened ?? [])];
elseif.metadata.flattened = undefined;
}
}
}
@@ -0,0 +1,31 @@
/** @import { ImportDeclaration } from 'estree' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
/**
* @param {ImportDeclaration} node
* @param {Context} context
*/
export function ImportDeclaration(node, context) {
if (context.state.analysis.runes) {
const source = /** @type {string} */ (node.source.value);
if (source.startsWith('svelte/internal')) {
e.import_svelte_internal_forbidden(node);
}
if (source === 'svelte') {
for (const specifier of node.specifiers) {
if (specifier.type === 'ImportSpecifier') {
if (
specifier.imported.type === 'Identifier' &&
(specifier.imported.name === 'beforeUpdate' ||
specifier.imported.name === 'afterUpdate')
) {
e.runes_mode_invalid_import(specifier, specifier.imported.name);
}
}
}
}
}
}
@@ -0,0 +1,21 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { mark_subtree_dynamic } from './shared/fragment.js';
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
/**
* @param {AST.KeyBlock} node
* @param {Context} context
*/
export function KeyBlock(node, context) {
validate_block_not_empty(node.fragment, context);
if (context.state.analysis.runes) {
validate_opening_tag(node, context.state, '#');
}
mark_subtree_dynamic(context.path);
context.visit(node.expression, { ...context.state, expression: node.metadata.expression });
context.visit(node.fragment);
}
@@ -0,0 +1,95 @@
/** @import { Expression, LabeledStatement } from 'estree' */
/** @import { AST, ReactiveStatement } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { extract_identifiers, object } from '../../../utils/ast.js';
import * as w from '../../../warnings.js';
/**
* @param {LabeledStatement} node
* @param {Context} context
*/
export function LabeledStatement(node, context) {
if (node.label.name === '$') {
const parent = /** @type {AST.SvelteNode} */ (context.path.at(-1));
const is_reactive_statement =
context.state.ast_type === 'instance' && parent.type === 'Program';
if (is_reactive_statement) {
if (context.state.analysis.runes) {
e.legacy_reactive_statement_invalid(node);
}
// Find all dependencies of this `$: {...}` statement
/** @type {ReactiveStatement} */
const reactive_statement = {
assignments: new Set(),
dependencies: []
};
context.next({
...context.state,
reactive_statement,
function_depth: context.state.scope.function_depth + 1
});
// Every referenced binding becomes a dependency, unless it's on
// the left-hand side of an `=` assignment
for (const [name, nodes] of context.state.scope.references) {
const binding = context.state.scope.get(name);
if (binding === null) continue;
for (const { node, path } of nodes) {
/** @type {Expression} */
let left = node;
let i = path.length - 1;
let parent = /** @type {Expression} */ (path.at(i));
while (parent.type === 'MemberExpression') {
left = parent;
parent = /** @type {Expression} */ (path.at(--i));
}
if (
parent.type === 'AssignmentExpression' &&
parent.operator === '=' &&
parent.left === left
) {
continue;
}
reactive_statement.dependencies.push(binding);
break;
}
}
context.state.analysis.reactive_statements.set(node, reactive_statement);
if (
node.body.type === 'ExpressionStatement' &&
node.body.expression.type === 'AssignmentExpression'
) {
let ids = extract_identifiers(node.body.expression.left);
if (node.body.expression.left.type === 'MemberExpression') {
const id = object(node.body.expression.left);
if (id !== null) {
ids = [id];
}
}
for (const id of ids) {
const binding = context.state.scope.get(id.name);
if (binding?.kind === 'legacy_reactive') {
// TODO does this include `let double; $: double = x * 2`?
binding.legacy_dependencies = Array.from(reactive_statement.dependencies);
}
}
}
} else if (!context.state.analysis.runes) {
w.reactive_declaration_invalid_placement(node);
}
}
context.next();
}
@@ -0,0 +1,24 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
/**
* @param {AST.LetDirective} node
* @param {Context} context
*/
export function LetDirective(node, context) {
const parent = context.path.at(-1);
if (
parent === undefined ||
(parent.type !== 'Component' &&
parent.type !== 'RegularElement' &&
parent.type !== 'SlotElement' &&
parent.type !== 'SvelteElement' &&
parent.type !== 'SvelteComponent' &&
parent.type !== 'SvelteSelf' &&
parent.type !== 'SvelteFragment')
) {
e.let_directive_invalid_placement(node);
}
}
@@ -0,0 +1,14 @@
/** @import { Literal } from 'estree' */
import * as w from '../../../warnings.js';
import { regex_bidirectional_control_characters } from '../../patterns.js';
/**
* @param {Literal} node
*/
export function Literal(node) {
if (typeof node.value === 'string') {
if (regex_bidirectional_control_characters.test(node.value)) {
w.bidirectional_control_characters(node);
}
}
}
@@ -0,0 +1,28 @@
/** @import { MemberExpression } from 'estree' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { is_pure, is_safe_identifier } from './shared/utils.js';
/**
* @param {MemberExpression} node
* @param {Context} context
*/
export function 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);
}
}
if (context.state.expression) {
context.state.expression.has_member_expression = true;
context.state.expression.has_state ||= !is_pure(node, context);
}
if (!is_safe_identifier(node, context.state.scope)) {
context.state.analysis.needs_context = true;
}
context.next();
}
@@ -0,0 +1,17 @@
/** @import { NewExpression } from 'estree' */
/** @import { Context } from '../types' */
import * as w from '../../../warnings.js';
/**
* @param {NewExpression} node
* @param {Context} context
*/
export function NewExpression(node, context) {
if (node.callee.type === 'ClassExpression' && context.state.scope.function_depth > 0) {
w.perf_avoid_inline_class(node);
}
context.state.analysis.needs_context = true;
context.next();
}
@@ -0,0 +1,28 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as w from '../../../warnings.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {AST.OnDirective} node
* @param {Context} context
*/
export function OnDirective(node, context) {
if (context.state.analysis.runes) {
const parent_type = context.path.at(-1)?.type;
// Don't warn on component events; these might not be under the author's control so the warning would be unactionable
if (parent_type === 'RegularElement' || parent_type === 'SvelteElement') {
w.event_directive_deprecated(node, node.name);
}
}
const parent = context.path.at(-1);
if (parent?.type === 'SvelteElement' || parent?.type === 'RegularElement') {
context.state.analysis.event_directive_node ??= node;
}
mark_subtree_dynamic(context.path);
context.next({ ...context.state, expression: node.metadata.expression });
}
@@ -0,0 +1,21 @@
/** @import { PropertyDefinition } from 'estree' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { get_name } from '../../nodes.js';
/**
* @param {PropertyDefinition} node
* @param {Context} context
*/
export function PropertyDefinition(node, context) {
const name = get_name(node.key);
const field = name && context.state.state_fields.get(name);
if (field && node !== field.node && node.value) {
if (/** @type {number} */ (node.start) < /** @type {number} */ (field.node.start)) {
e.state_field_invalid_assignment(node);
}
}
context.next();
}
@@ -0,0 +1,240 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { is_mathml, is_svg, is_void } from '../../../../utils.js';
import {
is_tag_valid_with_ancestor,
is_tag_valid_with_parent
} from '../../../../html-tree-validation.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import {
create_attribute,
is_custom_element_node,
is_customizable_select_element
} from '../../nodes.js';
import { regex_starts_with_newline } from '../../patterns.js';
import { check_element } from './shared/a11y/index.js';
import { validate_element } from './shared/element.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
import { object } from '../../../utils/ast.js';
import { runes } from '../../../state.js';
/**
* @param {AST.RegularElement} node
* @param {Context} context
*/
export function RegularElement(node, context) {
validate_element(node, context);
check_element(node, context);
node.metadata.path = [...context.path];
context.state.analysis.elements.push(node);
// Special case: Move the children of <textarea> into a value attribute if they are dynamic
if (node.name === 'textarea' && node.fragment.nodes.length > 0) {
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute' && attribute.name === 'value') {
e.textarea_invalid_content(node);
}
}
if (node.fragment.nodes.length > 1 || node.fragment.nodes[0].type !== 'Text') {
const first = node.fragment.nodes[0];
if (first.type === 'Text') {
// The leading newline character needs to be stripped because of a qirk:
// It is ignored by browsers if the tag and its contents are set through
// innerHTML, but we're now setting it through the value property at which
// point it is _not_ ignored, so we need to strip it ourselves.
// see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions
// see https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
first.data = first.data.replace(regex_starts_with_newline, '');
first.raw = first.raw.replace(regex_starts_with_newline, '');
}
node.attributes.push(
create_attribute(
'value',
null,
-1,
-1,
// @ts-ignore
node.fragment.nodes
)
);
node.fragment.nodes = [];
}
}
// Special case: `<select bind:value={foo}><option>{bar}</option>`
// means we need to invalidate `bar` whenever `foo` is mutated
if (node.name === 'select' && !runes) {
for (const attribute of node.attributes) {
if (
attribute.type === 'BindDirective' &&
attribute.name === 'value' &&
attribute.expression.type !== 'SequenceExpression'
) {
const identifier = object(attribute.expression);
const binding = identifier && context.state.scope.get(identifier.name);
if (binding) {
for (const name of context.state.scope.references.keys()) {
if (name === binding.node.name) continue;
const indirect = context.state.scope.get(name);
if (indirect) {
binding.legacy_indirect_bindings.add(indirect);
}
}
}
break;
}
}
}
// Special case: single expression tag child of option element -> add "fake" attribute
// to ensure that value types are the same (else for example numbers would be strings)
if (
node.name === 'option' &&
node.fragment.nodes?.length === 1 &&
node.fragment.nodes[0].type === 'ExpressionTag' &&
!node.attributes.some(
(attribute) => attribute.type === 'Attribute' && attribute.name === 'value'
)
) {
const child = node.fragment.nodes[0];
node.metadata.synthetic_value_node = child;
}
// Special case: <select>, <option> or <optgroup> with rich content needs special hydration handling
// We mark the subtree as dynamic so parent elements properly include the child init code
if (is_customizable_select_element(node) || node.name === 'selectedcontent') {
// Mark the element's own fragment as dynamic so it's not treated as static
node.fragment.metadata.dynamic = true;
// Also mark ancestor fragments so parents properly include the child init code
mark_subtree_dynamic(context.path);
}
const binding = context.state.scope.get(node.name);
if (
binding !== null &&
binding.declaration_kind === 'import' &&
binding.references.length === 0
) {
w.component_name_lowercase(node, node.name);
}
node.metadata.has_spread = node.attributes.some(
(attribute) => attribute.type === 'SpreadAttribute'
);
const is_svg_element = () => {
if (is_svg(node.name)) {
return true;
}
if (node.name === 'a' || node.name === 'title') {
let i = context.path.length;
while (i--) {
const ancestor = context.path[i];
if (ancestor.type === 'RegularElement') {
return ancestor.metadata.svg;
}
}
}
return false;
};
node.metadata.svg = is_svg_element();
node.metadata.mathml = is_mathml(node.name);
if (is_custom_element_node(node) && node.attributes.length > 0) {
// we're setting all attributes on custom elements through properties
mark_subtree_dynamic(context.path);
}
if (context.state.parent_element) {
let past_parent = false;
let only_warn = false;
const ancestors = [context.state.parent_element];
for (let i = context.path.length - 1; i >= 0; i--) {
const ancestor = context.path[i];
if (
ancestor.type === 'IfBlock' ||
ancestor.type === 'EachBlock' ||
ancestor.type === 'AwaitBlock' ||
ancestor.type === 'KeyBlock'
) {
// We're creating a separate template string inside blocks, which means client-side this would work
only_warn = true;
}
if (!past_parent) {
if (ancestor.type === 'RegularElement' && ancestor.name === context.state.parent_element) {
const message = is_tag_valid_with_parent(node.name, context.state.parent_element);
if (message) {
if (only_warn) {
w.node_invalid_placement_ssr(node, message);
} else {
e.node_invalid_placement(node, message);
}
}
past_parent = true;
}
} else if (ancestor.type === 'RegularElement') {
ancestors.push(ancestor.name);
const message = is_tag_valid_with_ancestor(node.name, ancestors);
if (message) {
if (only_warn) {
w.node_invalid_placement_ssr(node, message);
} else {
e.node_invalid_placement(node, message);
}
}
} else if (
ancestor.type === 'Component' ||
ancestor.type === 'SvelteComponent' ||
ancestor.type === 'SvelteElement' ||
ancestor.type === 'SvelteSelf' ||
ancestor.type === 'SnippetBlock'
) {
break;
}
}
}
// Strip off any namespace from the beginning of the node name.
const node_name = node.name.replace(/[a-zA-Z-]*:/g, '');
if (
context.state.analysis.source[node.end - 2] === '/' &&
!is_void(node_name) &&
!is_svg(node_name) &&
!is_mathml(node_name)
) {
w.element_invalid_self_closing_tag(node, node.name);
}
context.next({ ...context.state, parent_element: node.name });
// Special case: <a> tags are valid in both the SVG and HTML namespace.
// If there's no parent, look downwards to see if it's the parent of a SVG or HTML element.
if (node.name === 'a' && !context.state.parent_element) {
for (const child of node.fragment.nodes) {
if (child.type === 'RegularElement') {
if (child.metadata.svg && child.name !== 'svg') {
node.metadata.svg = true;
break;
}
}
}
}
}
@@ -0,0 +1,68 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { unwrap_optional } from '../../../utils/ast.js';
import * as e from '../../../errors.js';
import { validate_opening_tag } from './shared/utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
import { is_resolved_snippet } from './shared/snippets.js';
import { ExpressionMetadata } from '../../nodes.js';
/**
* @param {AST.RenderTag} node
* @param {Context} context
*/
export function RenderTag(node, context) {
validate_opening_tag(node, context.state, '@');
node.metadata.path = [...context.path];
const expression = unwrap_optional(node.expression);
const callee = expression.callee;
const binding = callee.type === 'Identifier' ? context.state.scope.get(callee.name) : null;
node.metadata.dynamic = binding?.kind !== 'normal';
/**
* If we can't unambiguously resolve this to a declaration, we
* must assume the worst and link the render tag to every snippet
*/
let resolved = callee.type === 'Identifier' && is_resolved_snippet(binding);
if (binding?.initial?.type === 'SnippetBlock') {
// if this render tag unambiguously references a local snippet, our job is easy
node.metadata.snippets.add(binding.initial);
}
context.state.analysis.snippet_renderers.set(node, resolved);
context.state.analysis.uses_render_tags = true;
const raw_args = unwrap_optional(node.expression).arguments;
for (const arg of raw_args) {
if (arg.type === 'SpreadElement') {
e.render_tag_invalid_spread_argument(arg);
}
}
if (
callee.type === 'MemberExpression' &&
callee.property.type === 'Identifier' &&
['bind', 'apply', 'call'].includes(callee.property.name)
) {
e.render_tag_invalid_call_expression(node);
}
mark_subtree_dynamic(context.path);
context.visit(callee, { ...context.state, expression: node.metadata.expression });
for (const arg of expression.arguments) {
const metadata = new ExpressionMetadata();
node.metadata.arguments.push(metadata);
context.visit(arg, {
...context.state,
expression: metadata
});
}
}
@@ -0,0 +1,42 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { is_text_attribute } from '../../../utils/ast.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {AST.SlotElement} node
* @param {Context} context
*/
export function SlotElement(node, context) {
if (context.state.analysis.runes && !context.state.analysis.custom_element) {
w.slot_element_deprecated(node);
}
mark_subtree_dynamic(context.path);
/** @type {string} */
let name = 'default';
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute') {
if (attribute.name === 'name') {
if (!is_text_attribute(attribute)) {
e.slot_element_invalid_name(attribute);
}
name = attribute.value[0].data;
if (name === 'default') {
e.slot_element_invalid_name_default(attribute);
}
}
} else if (attribute.type !== 'SpreadAttribute' && attribute.type !== 'LetDirective') {
e.slot_element_invalid_attribute(attribute);
}
}
context.state.analysis.slot_names.set(name, node);
context.next();
}
@@ -0,0 +1,113 @@
/** @import { AST, Binding } from '#compiler' */
/** @import { Scope } from '../../scope' */
/** @import { Context } from '../types' */
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
import * as e from '../../../errors.js';
/**
* @param {AST.SnippetBlock} node
* @param {Context} context
*/
export function SnippetBlock(node, context) {
context.state.analysis.snippets.add(node);
validate_block_not_empty(node.body, context);
if (context.state.analysis.runes) {
validate_opening_tag(node, context.state, '#');
}
for (const arg of node.parameters) {
if (arg.type === 'RestElement') {
e.snippet_invalid_rest_parameter(arg);
}
}
context.next({ ...context.state, parent_element: null });
const can_hoist =
context.path.length === 1 &&
context.path[0].type === 'Fragment' &&
can_hoist_snippet(context.state.scope, context.state.scopes);
const name = node.expression.name;
if (can_hoist) {
const binding = /** @type {Binding} */ (context.state.scope.get(name));
context.state.analysis.module.scope.declarations.set(name, binding);
}
node.metadata.can_hoist = can_hoist;
const { path } = context;
const parent = path.at(-2);
if (!parent) return;
if (
parent.type === 'Component' &&
parent.attributes.some(
(attribute) =>
(attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
attribute.name === node.expression.name
)
) {
e.snippet_shadowing_prop(node, node.expression.name);
}
if (node.expression.name !== 'children') return;
if (
parent.type === 'Component' ||
parent.type === 'SvelteComponent' ||
parent.type === 'SvelteSelf'
) {
if (
parent.fragment.nodes.some(
(node) =>
node.type !== 'SnippetBlock' &&
(node.type !== 'Text' || node.data.trim()) &&
node.type !== 'Comment'
)
) {
e.snippet_conflict(node);
}
}
}
/**
* @param {Map<AST.SvelteNode, Scope>} scopes
* @param {Scope} scope
*/
function can_hoist_snippet(scope, scopes, visited = new Set()) {
for (const [reference] of scope.references) {
const binding = scope.get(reference);
if (!binding) continue;
if (binding.blocker) {
return false;
}
if (binding.scope.function_depth === 0) {
continue;
}
// ignore bindings declared inside the snippet (e.g. the snippet's own parameters)
if (binding.scope.function_depth >= scope.function_depth) {
continue;
}
if (binding.initial?.type === 'SnippetBlock') {
if (visited.has(binding)) continue;
visited.add(binding);
const snippet_scope = /** @type {Scope} */ (scopes.get(binding.initial));
if (can_hoist_snippet(snippet_scope, scopes, visited)) {
continue;
}
}
return false;
}
return true;
}
@@ -0,0 +1,13 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {AST.SpreadAttribute} node
* @param {Context} context
*/
export function SpreadAttribute(node, context) {
mark_subtree_dynamic(context.path);
context.next({ ...context.state, expression: node.metadata.expression });
}
@@ -0,0 +1,16 @@
/** @import { SpreadElement } from 'estree' */
/** @import { Context } from '../types' */
/**
* @param {SpreadElement} node
* @param {Context} context
*/
export function SpreadElement(node, context) {
if (context.state.expression) {
// treat e.g. `[...x]` the same as `[...x.values()]`
context.state.expression.has_call = true;
context.state.expression.has_state = true;
}
context.next();
}
@@ -0,0 +1,39 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { get_attribute_chunks } from '../../../utils/ast.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {AST.StyleDirective} node
* @param {Context} context
*/
export function StyleDirective(node, context) {
if (node.modifiers.length > 1 || (node.modifiers.length && node.modifiers[0] !== 'important')) {
e.style_directive_invalid_modifier(node);
}
mark_subtree_dynamic(context.path);
if (node.value === true) {
// get the binding for node.name and change the binding to state
let binding = context.state.scope.get(node.name);
if (binding) {
if (binding.kind !== 'normal') {
node.metadata.expression.has_state = true;
}
if (binding.blocker) {
node.metadata.expression.dependencies.add(binding);
}
}
} else {
context.next();
for (const chunk of get_attribute_chunks(node.value)) {
if (chunk.type !== 'ExpressionTag') continue;
node.metadata.expression.merge(chunk.metadata.expression);
}
}
}
@@ -0,0 +1,22 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { is_event_attribute } from '../../../utils/ast.js';
import { disallow_children } from './shared/special-element.js';
/**
* @param {AST.SvelteBody} node
* @param {Context} context
*/
export function SvelteBody(node, context) {
disallow_children(node);
for (const attribute of node.attributes) {
if (
attribute.type === 'SpreadAttribute' ||
(attribute.type === 'Attribute' && !is_event_attribute(attribute))
) {
e.svelte_body_illegal_attribute(attribute);
}
}
context.next();
}
@@ -0,0 +1,30 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
const valid = ['onerror', 'failed', 'pending'];
/**
* @param {AST.SvelteBoundary} node
* @param {Context} context
*/
export function SvelteBoundary(node, context) {
for (const attribute of node.attributes) {
if (attribute.type !== 'Attribute' || !valid.includes(attribute.name)) {
e.svelte_boundary_invalid_attribute(attribute);
}
if (
attribute.value === true ||
(Array.isArray(attribute.value) &&
(attribute.value.length !== 1 || attribute.value[0].type !== 'ExpressionTag'))
) {
e.svelte_boundary_invalid_attribute_value(attribute);
}
}
mark_subtree_dynamic(context.path);
context.next();
}
@@ -0,0 +1,18 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as w from '../../../warnings.js';
import { visit_component } from './shared/component.js';
/**
* @param {AST.SvelteComponent} node
* @param {Context} context
*/
export function SvelteComponent(node, context) {
if (context.state.analysis.runes) {
w.svelte_component_deprecated(node);
}
context.visit(node.expression, { ...context.state, expression: node.metadata.expression });
visit_component(node, context);
}
@@ -0,0 +1,24 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { disallow_children } from './shared/special-element.js';
import * as e from '../../../errors.js';
import { is_event_attribute } from '../../../utils/ast.js';
/**
* @param {AST.SvelteDocument} node
* @param {Context} context
*/
export function SvelteDocument(node, context) {
disallow_children(node);
for (const attribute of node.attributes) {
if (
attribute.type === 'SpreadAttribute' ||
(attribute.type === 'Attribute' && !is_event_attribute(attribute))
) {
e.illegal_element_attribute(attribute, 'svelte:document');
}
}
context.next();
}
@@ -0,0 +1,78 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { NAMESPACE_MATHML, NAMESPACE_SVG } from '../../../../constants.js';
import { is_text_attribute } from '../../../utils/ast.js';
import { check_element } from './shared/a11y/index.js';
import { validate_element } from './shared/element.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {AST.SvelteElement} node
* @param {Context} context
*/
export function SvelteElement(node, context) {
validate_element(node, context);
check_element(node, context);
node.metadata.path = [...context.path];
context.state.analysis.elements.push(node);
const xmlns = /** @type {AST.Attribute & { value: [AST.Text] } | undefined} */ (
node.attributes.find(
(a) => a.type === 'Attribute' && a.name === 'xmlns' && is_text_attribute(a)
)
);
if (xmlns) {
node.metadata.svg = xmlns.value[0].data === NAMESPACE_SVG;
node.metadata.mathml = xmlns.value[0].data === NAMESPACE_MATHML;
} else {
let i = context.path.length;
while (i--) {
const ancestor = context.path[i];
if (
ancestor.type === 'Component' ||
ancestor.type === 'SvelteComponent' ||
ancestor.type === 'SvelteFragment' ||
ancestor.type === 'SnippetBlock' ||
i === 0
) {
// Root element, or inside a slot or a snippet -> this resets the namespace, so assume the component namespace
node.metadata.svg = context.state.options.namespace === 'svg';
node.metadata.mathml = context.state.options.namespace === 'mathml';
break;
}
if (ancestor.type === 'SvelteElement' || ancestor.type === 'RegularElement') {
node.metadata.svg =
ancestor.type === 'RegularElement' && ancestor.name === 'foreignObject'
? false
: ancestor.metadata.svg;
node.metadata.mathml =
ancestor.type === 'RegularElement' && ancestor.name === 'foreignObject'
? false
: ancestor.metadata.mathml;
break;
}
}
}
mark_subtree_dynamic(context.path);
context.visit(node.tag, {
...context.state,
expression: node.metadata.expression
});
for (const attribute of node.attributes) {
context.visit(attribute);
}
context.visit(node.fragment, {
...context.state,
parent_element: null
});
}
@@ -0,0 +1,27 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { validate_slot_attribute } from './shared/attribute.js';
/**
* @param {AST.SvelteFragment} node
* @param {Context} context
*/
export function SvelteFragment(node, context) {
const parent = context.path.at(-2);
if (parent?.type !== 'Component' && parent?.type !== 'SvelteComponent') {
e.svelte_fragment_invalid_placement(node);
}
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute') {
if (attribute.name === 'slot') {
validate_slot_attribute(context, attribute);
}
} else if (attribute.type !== 'LetDirective') {
e.svelte_fragment_invalid_attribute(attribute);
}
}
context.next({ ...context.state, parent_element: null });
}
@@ -0,0 +1,18 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {AST.SvelteHead} node
* @param {Context} context
*/
export function SvelteHead(node, context) {
for (const attribute of node.attributes) {
e.svelte_head_illegal_attribute(attribute);
}
mark_subtree_dynamic(context.path);
context.next();
}
@@ -0,0 +1,36 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { visit_component } from './shared/component.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { filename, UNKNOWN_FILENAME } from '../../../state.js';
/**
* @param {AST.SvelteSelf} node
* @param {Context} context
*/
export function SvelteSelf(node, context) {
const valid = context.path.some(
(node) =>
node.type === 'IfBlock' ||
node.type === 'EachBlock' ||
node.type === 'Component' ||
node.type === 'SnippetBlock'
);
if (!valid) {
e.svelte_self_invalid_placement(node);
}
if (context.state.analysis.runes) {
const name = filename === UNKNOWN_FILENAME ? 'Self' : context.state.analysis.name;
const basename =
filename === UNKNOWN_FILENAME
? 'Self.svelte'
: /** @type {string} */ (filename.split(/[/\\]/).pop());
w.svelte_self_deprecated(node, name, basename);
}
visit_component(node, context);
}
@@ -0,0 +1,24 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { disallow_children } from './shared/special-element.js';
import * as e from '../../../errors.js';
import { is_event_attribute } from '../../../utils/ast.js';
/**
* @param {AST.SvelteWindow} node
* @param {Context} context
*/
export function SvelteWindow(node, context) {
disallow_children(node);
for (const attribute of node.attributes) {
if (
attribute.type === 'SpreadAttribute' ||
(attribute.type === 'Attribute' && !is_event_attribute(attribute))
) {
e.illegal_element_attribute(attribute, 'svelte:window');
}
}
context.next();
}
@@ -0,0 +1,16 @@
/** @import { TaggedTemplateExpression } from 'estree' */
/** @import { Context } from '../types' */
import { is_pure } from './shared/utils.js';
/**
* @param {TaggedTemplateExpression} node
* @param {Context} context
*/
export function TaggedTemplateExpression(node, context) {
if (context.state.expression && !is_pure(node.tag, context)) {
context.state.expression.has_call = true;
context.state.expression.has_state = true;
}
context.next();
}
@@ -0,0 +1,12 @@
/** @import { TemplateElement } from 'estree' */
import * as w from '../../../warnings.js';
import { regex_bidirectional_control_characters } from '../../patterns.js';
/**
* @param {TemplateElement} node
*/
export function TemplateElement(node) {
if (regex_bidirectional_control_characters.test(node.value.cooked ?? '')) {
w.bidirectional_control_characters(node);
}
}
+52
View File
@@ -0,0 +1,52 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { is_tag_valid_with_parent } from '../../../../html-tree-validation.js';
import { regex_bidirectional_control_characters, regex_not_whitespace } from '../../patterns.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { extract_svelte_ignore } from '../../../utils/extract_svelte_ignore.js';
/**
* @param {AST.Text} node
* @param {Context} context
*/
export function Text(node, context) {
const parent = /** @type {AST.SvelteNode} */ (context.path.at(-1));
if (
parent.type === 'Fragment' &&
context.state.parent_element &&
regex_not_whitespace.test(node.data)
) {
const message = is_tag_valid_with_parent('#text', context.state.parent_element);
if (message) {
e.node_invalid_placement(node, message);
}
}
regex_bidirectional_control_characters.lastIndex = 0;
for (const match of node.data.matchAll(regex_bidirectional_control_characters)) {
let is_ignored = false;
// if we have a svelte-ignore comment earlier in the text, bail
// (otherwise we can only use svelte-ignore on parent elements/blocks)
if (parent.type === 'Fragment') {
for (const child of parent.nodes) {
if (child === node) break;
if (child.type === 'Comment') {
is_ignored ||= extract_svelte_ignore(
child.start + 4,
child.data,
context.state.analysis.runes
).includes('bidirectional_control_characters');
}
}
}
if (!is_ignored) {
let start = match.index + node.start;
w.bidirectional_control_characters({ start, end: start + match[0].length });
}
}
}
@@ -0,0 +1,21 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
/**
* @param {AST.TitleElement} node
* @param {Context} context
*/
export function TitleElement(node, context) {
for (const attribute of node.attributes) {
e.title_illegal_attribute(attribute);
}
for (const child of node.fragment.nodes) {
if (child.type !== 'Text' && child.type !== 'ExpressionTag') {
e.title_invalid_content(child);
}
}
context.next();
}
@@ -0,0 +1,19 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {AST.TransitionDirective} node
* @param {Context} context
*/
export function TransitionDirective(node, context) {
mark_subtree_dynamic(context.path);
context.next({ ...context.state, expression: node.metadata.expression });
if (node.metadata.expression.has_await) {
e.illegal_await_expression(node);
}
}
@@ -0,0 +1,29 @@
/** @import { UpdateExpression } from 'estree' */
/** @import { Context } from '../types' */
import { object } from '../../../utils/ast.js';
import { validate_assignment } from './shared/utils.js';
/**
* @param {UpdateExpression} node
* @param {Context} context
*/
export function UpdateExpression(node, context) {
validate_assignment(node, node.argument, context);
if (context.state.reactive_statement) {
const id = node.argument.type === 'MemberExpression' ? object(node.argument) : node.argument;
if (id?.type === 'Identifier') {
const binding = context.state.scope.get(id.name);
if (binding) {
context.state.reactive_statement.assignments.add(binding);
}
}
}
if (context.state.expression) {
context.state.expression.has_assignment = true;
}
context.next();
}

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