fix: vite

This commit is contained in:
2025-03-12 03:23:38 -04:00
parent 58b08839cc
commit c442635d6b
1630 changed files with 888315 additions and 0 deletions

69
node_modules/svelte/src/action/public.d.ts generated vendored Normal file
View File

@@ -0,0 +1,69 @@
/**
* Actions can return an object containing the two properties defined in this interface. Both are optional.
* - update: An action can have a parameter. This method will be called whenever that parameter changes,
* immediately after Svelte has applied updates to the markup. `ActionReturn` and `ActionReturn<undefined>` both
* mean that the action accepts no parameters.
* - destroy: Method that is called after the element is unmounted
*
* Additionally, you can specify which additional attributes and events the action enables on the applied element.
* This applies to TypeScript typings only and has no effect at runtime.
*
* Example usage:
* ```ts
* interface Attributes {
* newprop?: string;
* 'on:event': (e: CustomEvent<boolean>) => void;
* }
*
* export function myAction(node: HTMLElement, parameter: Parameter): ActionReturn<Parameter, Attributes> {
* // ...
* return {
* update: (updatedParameter) => {...},
* destroy: () => {...}
* };
* }
* ```
*/
export interface ActionReturn<
Parameter = undefined,
Attributes extends Record<string, any> = Record<never, any>
> {
update?: (parameter: Parameter) => void;
destroy?: () => void;
/**
* ### DO NOT USE THIS
* This exists solely for type-checking and has no effect at runtime.
* Set this through the `Attributes` generic instead.
*/
$$_attributes?: Attributes;
}
/**
* Actions are functions that are called when an element is created.
* You can use this interface to type such actions.
* The following example defines an action that only works on `<div>` elements
* and optionally accepts a parameter which it has a default value for:
* ```ts
* export const myAction: Action<HTMLDivElement, { someProperty: boolean } | undefined> = (node, param = { someProperty: true }) => {
* // ...
* }
* ```
* `Action<HTMLDivElement>` and `Action<HTMLDivElement, undefined>` both signal that the action accepts no parameters.
*
* You can return an object with methods `update` and `destroy` from the function and type which additional attributes and events it has.
* See interface `ActionReturn` for more details.
*/
export interface Action<
Element = HTMLElement,
Parameter = undefined,
Attributes extends Record<string, any> = Record<never, any>
> {
<Node extends Element>(
...args: undefined extends Parameter
? [node: Node, parameter?: Parameter]
: [node: Node, parameter: Parameter]
): void | ActionReturn<Parameter, Attributes>;
}
// Implementation notes:
// - undefined extends X instead of X extends undefined makes this work better with both strict and nonstrict mode

502
node_modules/svelte/src/ambient.d.ts generated vendored Normal file
View File

@@ -0,0 +1,502 @@
declare module '*.svelte' {
// use prettier-ignore for a while because of https://github.com/sveltejs/language-tools/commit/026111228b5814a9109cc4d779d37fb02955fb8b
// prettier-ignore
import { SvelteComponent } from 'svelte'
import { LegacyComponentType } from 'svelte/legacy';
const Comp: LegacyComponentType;
type Comp = SvelteComponent;
export default Comp;
}
/**
* Declares reactive state.
*
* Example:
* ```ts
* let count = $state(0);
* ```
*
* https://svelte.dev/docs/svelte/$state
*
* @param initial The initial value
*/
declare function $state<T>(initial: T): T;
declare function $state<T>(): T | undefined;
declare namespace $state {
type Primitive = string | number | boolean | null | undefined;
type TypedArray =
| Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Int32Array
| Uint32Array
| Float32Array
| Float64Array
| BigInt64Array
| BigUint64Array;
/** The things that `structuredClone` can handle — https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm */
export type Cloneable =
| ArrayBuffer
| DataView
| Date
| Error
| Map<any, any>
| RegExp
| Set<any>
| TypedArray
// web APIs
| Blob
| CryptoKey
| DOMException
| DOMMatrix
| DOMMatrixReadOnly
| DOMPoint
| DOMPointReadOnly
| DOMQuad
| DOMRect
| DOMRectReadOnly
| File
| FileList
| FileSystemDirectoryHandle
| FileSystemFileHandle
| FileSystemHandle
| ImageBitmap
| ImageData
| RTCCertificate
| VideoFrame;
/** Turn `SvelteDate`, `SvelteMap` and `SvelteSet` into their non-reactive counterparts. (`URL` is uncloneable.) */
type NonReactive<T> = T extends Date
? Date
: T extends Map<infer K, infer V>
? Map<K, V>
: T extends Set<infer K>
? Set<K>
: T;
type Snapshot<T> = T extends Primitive
? T
: T extends Cloneable
? NonReactive<T>
: T extends { toJSON(): infer R }
? R
: T extends Array<infer U>
? Array<Snapshot<U>>
: T extends object
? T extends { [key: string]: any }
? { [K in keyof T]: Snapshot<T[K]> }
: never
: never;
/**
* Declares state that is _not_ made deeply reactive — instead of mutating it,
* you must reassign it.
*
* Example:
* ```ts
* <script>
* let items = $state.raw([0]);
*
* const addItem = () => {
* items = [...items, items.length];
* };
* </script>
*
* <button on:click={addItem}>
* {items.join(', ')}
* </button>
* ```
*
* https://svelte.dev/docs/svelte/$state#$state.raw
*
* @param initial The initial value
*/
export function raw<T>(initial: T): T;
export function raw<T>(): T | undefined;
/**
* To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:
*
* Example:
* ```ts
* <script>
* let counter = $state({ count: 0 });
*
* function onclick() {
* // Will log `{ count: ... }` rather than `Proxy { ... }`
* console.log($state.snapshot(counter));
* };
* </script>
* ```
*
* https://svelte.dev/docs/svelte/$state#$state.snapshot
*
* @param state The value to snapshot
*/
export function snapshot<T>(state: T): Snapshot<T>;
// prevent intellisense from being unhelpful
/** @deprecated */
export const apply: never;
/** @deprecated */
// @ts-ignore
export const arguments: never;
/** @deprecated */
export const bind: never;
/** @deprecated */
export const call: never;
/** @deprecated */
export const caller: never;
/** @deprecated */
export const length: never;
/** @deprecated */
export const name: never;
/** @deprecated */
export const prototype: never;
/** @deprecated */
export const toString: never;
}
/**
* Declares derived state, i.e. one that depends on other state variables.
* The expression inside `$derived(...)` should be free of side-effects.
*
* Example:
* ```ts
* let double = $derived(count * 2);
* ```
*
* https://svelte.dev/docs/svelte/$derived
*
* @param expression The derived state expression
*/
declare function $derived<T>(expression: T): T;
declare namespace $derived {
/**
* Sometimes you need to create complex derivations that don't fit inside a short expression.
* In these cases, you can use `$derived.by` which accepts a function as its argument.
*
* Example:
* ```ts
* let total = $derived.by(() => {
* let result = 0;
* for (const n of numbers) {
* result += n;
* }
* return result;
* });
* ```
*
* https://svelte.dev/docs/svelte/$derived#$derived.by
*/
export function by<T>(fn: () => T): T;
// prevent intellisense from being unhelpful
/** @deprecated */
export const apply: never;
/** @deprecated */
// @ts-ignore
export const arguments: never;
/** @deprecated */
export const bind: never;
/** @deprecated */
export const call: never;
/** @deprecated */
export const caller: never;
/** @deprecated */
export const length: never;
/** @deprecated */
export const name: never;
/** @deprecated */
export const prototype: never;
/** @deprecated */
export const toString: never;
}
/**
* Runs code when a component is mounted to the DOM, and then whenever its dependencies change, i.e. `$state` or `$derived` values.
* The timing of the execution is after the DOM has been updated.
*
* Example:
* ```ts
* $effect(() => console.log('The count is now ' + count));
* ```
*
* If you return a function from the effect, it will be called right before the effect is run again, or when the component is unmounted.
*
* Does not run during server side rendering.
*
* https://svelte.dev/docs/svelte/$effect
* @param fn The function to execute
*/
declare function $effect(fn: () => void | (() => void)): void;
declare namespace $effect {
/**
* Runs code right before a component is mounted to the DOM, and then whenever its dependencies change, i.e. `$state` or `$derived` values.
* The timing of the execution is right before the DOM is updated.
*
* Example:
* ```ts
* $effect.pre(() => console.log('The count is now ' + count));
* ```
*
* If you return a function from the effect, it will be called right before the effect is run again, or when the component is unmounted.
*
* Does not run during server side rendering.
*
* https://svelte.dev/docs/svelte/$effect#$effect.pre
* @param fn The function to execute
*/
export function pre(fn: () => void | (() => void)): void;
/**
* The `$effect.tracking` rune is an advanced feature that tells you whether or not the code is running inside a tracking context, such as an effect or inside your template.
*
* Example:
* ```svelte
* <script>
* console.log('in component setup:', $effect.tracking()); // false
*
* $effect(() => {
* console.log('in effect:', $effect.tracking()); // true
* });
* </script>
*
* <p>in template: {$effect.tracking()}</p> <!-- true -->
* ```
*
* This allows you to (for example) add things like subscriptions without causing memory leaks, by putting them in child effects.
*
* https://svelte.dev/docs/svelte/$effect#$effect.tracking
*/
export function tracking(): boolean;
/**
* The `$effect.root` rune is an advanced feature that creates a non-tracked scope that doesn't auto-cleanup. This is useful for
* nested effects that you want to manually control. This rune also allows for creation of effects outside of the component
* initialisation phase.
*
* Example:
* ```svelte
* <script>
* let count = $state(0);
*
* const cleanup = $effect.root(() => {
* $effect(() => {
* console.log(count);
* })
*
* return () => {
* console.log('effect root cleanup');
* }
* });
* </script>
*
* <button onclick={() => cleanup()}>cleanup</button>
* ```
*
* https://svelte.dev/docs/svelte/$effect#$effect.root
*/
export function root(fn: () => void | (() => void)): () => void;
// prevent intellisense from being unhelpful
/** @deprecated */
export const apply: never;
/** @deprecated */
// @ts-ignore
export const arguments: never;
/** @deprecated */
export const bind: never;
/** @deprecated */
export const call: never;
/** @deprecated */
export const caller: never;
/** @deprecated */
export const length: never;
/** @deprecated */
export const name: never;
/** @deprecated */
export const prototype: never;
/** @deprecated */
export const toString: never;
}
/**
* Declares the props that a component accepts. Example:
*
* ```ts
* let { optionalProp = 42, requiredProp, bindableProp = $bindable() }: { optionalProp?: number; requiredProps: string; bindableProp: boolean } = $props();
* ```
*
* https://svelte.dev/docs/svelte/$props
*/
declare function $props(): any;
declare namespace $props {
// prevent intellisense from being unhelpful
/** @deprecated */
export const apply: never;
/** @deprecated */
// @ts-ignore
export const arguments: never;
/** @deprecated */
export const bind: never;
/** @deprecated */
export const call: never;
/** @deprecated */
export const caller: never;
/** @deprecated */
export const length: never;
/** @deprecated */
export const name: never;
/** @deprecated */
export const prototype: never;
/** @deprecated */
export const toString: never;
}
/**
* Declares a prop as bindable, meaning the parent component can use `bind:propName={value}` to bind to it.
*
* ```ts
* let { propName = $bindable() }: { propName: boolean } = $props();
* ```
*
* https://svelte.dev/docs/svelte/$bindable
*/
declare function $bindable<T>(fallback?: T): T;
declare namespace $bindable {
// prevent intellisense from being unhelpful
/** @deprecated */
export const apply: never;
/** @deprecated */
// @ts-ignore
export const arguments: never;
/** @deprecated */
export const bind: never;
/** @deprecated */
export const call: never;
/** @deprecated */
export const caller: never;
/** @deprecated */
export const length: never;
/** @deprecated */
export const name: never;
/** @deprecated */
export const prototype: never;
/** @deprecated */
export const toString: never;
}
/**
* Inspects one or more values whenever they, or the properties they contain, change. Example:
*
* ```ts
* $inspect(someValue, someOtherValue)
* ```
*
* `$inspect` returns a `with` function, which you can invoke with a callback function that
* will be called with the value and the event type (`'init'` or `'update'`) on every change.
* By default, the values will be logged to the console.
*
* ```ts
* $inspect(x).with(console.trace);
* $inspect(x, y).with(() => { debugger; });
* ```
*
* https://svelte.dev/docs/svelte/$inspect
*/
declare function $inspect<T extends any[]>(
...values: T
): { with: (fn: (type: 'init' | 'update', ...values: T) => void) => void };
declare namespace $inspect {
/**
* Tracks which reactive state changes caused an effect to re-run. Must be the first
* statement of a function body. Example:
*
* ```svelte
* <script>
* let count = $state(0);
*
* $effect(() => {
* $inspect.trace('my effect');
*
* count;
* });
* </script>
*/
export function trace(name?: string): void;
// prevent intellisense from being unhelpful
/** @deprecated */
export const apply: never;
/** @deprecated */
// @ts-ignore
export const arguments: never;
/** @deprecated */
export const bind: never;
/** @deprecated */
export const call: never;
/** @deprecated */
export const caller: never;
/** @deprecated */
export const length: never;
/** @deprecated */
export const name: never;
/** @deprecated */
export const prototype: never;
/** @deprecated */
export const toString: never;
}
/**
* Retrieves the `this` reference of the custom element that contains this component. Example:
*
* ```svelte
* <svelte:options customElement="my-element" />
*
* <script>
* function greet(greeting) {
* $host().dispatchEvent(new CustomEvent('greeting', { detail: greeting }))
* }
* </script>
*
* <button onclick={() => greet('hello')}>say hello</button>
* ```
*
* Only available inside custom element components, and only on the client-side.
*
* https://svelte.dev/docs/svelte/$host
*/
declare function $host<El extends HTMLElement = HTMLElement>(): El;
declare namespace $host {
// prevent intellisense from being unhelpful
/** @deprecated */
export const apply: never;
/** @deprecated */
// @ts-ignore
export const arguments: never;
/** @deprecated */
export const bind: never;
/** @deprecated */
export const call: never;
/** @deprecated */
export const caller: never;
/** @deprecated */
export const length: never;
/** @deprecated */
export const name: never;
/** @deprecated */
export const prototype: never;
/** @deprecated */
export const toString: never;
}

78
node_modules/svelte/src/animate/index.js generated vendored Normal file
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;
}

16
node_modules/svelte/src/animate/public.d.ts generated vendored Normal file
View File

@@ -0,0 +1,16 @@
// todo: same as Transition, should it be shared?
export interface AnimationConfig {
delay?: number;
duration?: number;
easing?: (t: number) => number;
css?: (t: number, u: number) => string;
tick?: (t: number, u: number) => void;
}
export interface FlipParams {
delay?: number;
duration?: number | ((len: number) => number);
easing?: (t: number) => number;
}
export * from './index.js';

1593
node_modules/svelte/src/compiler/errors.js generated vendored Normal file
View File

File diff suppressed because it is too large Load Diff

169
node_modules/svelte/src/compiler/index.js generated vendored Normal file
View File

@@ -0,0 +1,169 @@
/** @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_acorn } from './phases/1-parse/acorn.js';
import { parse as _parse } from './phases/1-parse/index.js';
import { remove_typescript_nodes } from './phases/1-parse/remove_typescript_nodes.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';
/**
* `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_filter(options.warningFilter);
const validated = validate_component_options(options, '');
state.reset(source, validated);
let parsed = _parse(source);
const { customElement: customElementOptions, ...parsed_options } = parsed.options || {};
/** @type {ValidatedCompileOptions} */
const combined_options = {
...validated,
...parsed_options,
customElementOptions
};
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)
};
}
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_filter(options.warningFilter);
const validated = validate_module_options(options, '');
state.reset(source, validated);
const analysis = analyze_module(parse_acorn(source, false), 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>}
*/
/**
* 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.
*
* @param {string} source
* @param {{ filename?: string; rootDir?: string; modern?: boolean; loose?: boolean }} [options]
* @returns {AST.Root | LegacyRoot}
*/
export function parse(source, { filename, rootDir, modern, loose } = {}) {
source = remove_bom(source);
state.reset_warning_filter(() => false);
state.reset(source, { filename: filename ?? '(unknown)', rootDir });
const ast = _parse(source, loose);
return to_public_ast(source, ast, modern);
}
/**
* @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';

628
node_modules/svelte/src/compiler/legacy.js generated vendored Normal file
View File

@@ -0,0 +1,628 @@
/** @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) => 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
};
},
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))
};
},
// @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' };
},
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' };
}
})
);
}

1989
node_modules/svelte/src/compiler/migrate/index.js generated vendored Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,214 @@
/** @import { Comment, Program } from 'estree' */
/** @import { Node } from 'acorn' */
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import { tsPlugin } from 'acorn-typescript';
import { locator } from '../../state.js';
const ParserWithTS = acorn.Parser.extend(tsPlugin({ allowSatisfies: true }));
/**
* @param {string} source
* @param {boolean} typescript
* @param {boolean} [is_script]
*/
export function parse(source, typescript, is_script) {
const parser = typescript ? ParserWithTS : acorn.Parser;
const { onComment, add_comments } = get_comment_handlers(source);
// @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: 13,
locations: true
});
} finally {
if (is_script) {
// @ts-ignore
parser.prototype.parseStatement = parse_statement;
}
}
if (typescript) amend(source, ast);
add_comments(ast);
return /** @type {Program} */ (ast);
}
/**
* @param {string} source
* @param {boolean} typescript
* @param {number} index
* @returns {acorn.Expression & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }}
*/
export function parse_expression_at(source, typescript, index) {
const parser = typescript ? ParserWithTS : acorn.Parser;
const { onComment, add_comments } = get_comment_handlers(source);
const ast = parser.parseExpressionAt(source, index, {
onComment,
sourceType: 'module',
ecmaVersion: 13,
locations: true
});
if (typescript) amend(source, ast);
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
*/
function get_comment_handlers(source) {
/**
* @typedef {Comment & {
* start: number;
* end: number;
* }} CommentWithLocation
*/
/** @type {CommentWithLocation[]} */
const comments = [];
return {
/**
* @param {boolean} block
* @param {string} value
* @param {number} start
* @param {number} end
*/
onComment: (block, value, start, end) => {
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 });
},
/** @param {acorn.Node & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }} ast */
add_comments(ast) {
if (comments.length === 0) return;
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));
}
}
};
}
/**
* Tidy up some stuff left behind by acorn-typescript
* @param {string} source
* @param {Node} node
*/
function amend(source, node) {
return walk(node, null, {
_(node, context) {
// @ts-expect-error
delete node.loc.start.index;
// @ts-expect-error
delete node.loc.end.index;
if (typeof node.loc?.end === 'number') {
const loc = locator(node.loc.end);
if (loc) {
node.loc.end = {
line: loc.line,
column: loc.column
};
}
}
if (
/** @type {any} */ (node).typeAnnotation &&
(node.end === undefined || node.end < node.start)
) {
// i think there might be a bug in acorn-typescript that prevents
// `end` from being assigned when there's a type annotation
let end = /** @type {any} */ (node).typeAnnotation.start;
while (/\s/.test(source[end - 1])) end -= 1;
node.end = end;
}
context.next();
}
});
}

View File

@@ -0,0 +1,3 @@
// Silence the acorn typescript errors through this ambient type definition + tsconfig.json path alias
// That way we can omit `"skipLibCheck": true` and catch other errors in our d.ts files
declare module 'acorn-typescript';

View File

@@ -0,0 +1,312 @@
/** @import { AST } from '#compiler' */
// @ts-expect-error acorn type definitions are borked in the release we use
import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment.js';
import { regex_whitespace } from '../patterns.js';
import * 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';
const regex_position_indicator = / \(\d+:\d+\)$/;
const regex_lang_attribute =
/<!--[^]*?-->|<script\s+(?:[^>]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/g;
export class Parser {
/**
* @readonly
* @type {string}
*/
template;
/**
* @readonly
* @type {string}
*/
template_untrimmed;
/**
* 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;
/** 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_untrimmed = template;
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,
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);
}
if (this.root.fragment.nodes.length) {
let start = /** @type {number} */ (this.root.fragment.nodes[0].start);
while (regex_whitespace.test(template[start])) start += 1;
let end = /** @type {number} */ (
this.root.fragment.nodes[this.root.fragment.nodes.length - 1].end
);
while (regex_whitespace.test(template[end - 1])) end -= 1;
this.root.start = start;
this.root.end = end;
} else {
// @ts-ignore
this.root.start = this.root.end = null;
}
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.slice(this.index, this.index + length) === str;
}
/**
* Match a regex at the current index
* @param {RegExp} pattern Should have a ^ anchor at the start so the regex doesn't search past the beginning, resulting in worse performance
*/
match_regex(pattern) {
const match = pattern.exec(this.template.slice(this.index));
if (!match || match.index !== 0) return null;
return match[0];
}
allow_whitespace() {
while (this.index < this.template.length && regex_whitespace.test(this.template[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;
}
/** @param {any} allow_reserved */
read_identifier(allow_reserved = false) {
const start = this.index;
let i = this.index;
const code = /** @type {number} */ (this.template.codePointAt(i));
if (!isIdentifierStart(code, true)) return null;
i += code <= 0xffff ? 1 : 2;
while (i < this.template.length) {
const code = /** @type {number} */ (this.template.codePointAt(i));
if (!isIdentifierChar(code, true)) break;
i += code <= 0xffff ? 1 : 2;
}
const identifier = this.template.slice(this.index, (this.index = i));
if (!allow_reserved && is_reserved(identifier)) {
e.unexpected_reserved_word(start, identifier);
}
return identifier;
}
/** @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 (!regex_whitespace.test(this.template[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) {
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
*/

View File

@@ -0,0 +1,187 @@
/** @import { Location } from 'locate-character' */
/** @import { Pattern } from 'estree' */
/** @import { Parser } from '../index.js' */
import { is_bracket_open, is_bracket_close, get_bracket_close } 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';
import { locator } from '../../../state.js';
/**
* @param {Parser} parser
* @returns {Pattern}
*/
export default function read_pattern(parser) {
const start = parser.index;
let i = parser.index;
const name = parser.read_identifier();
if (name !== null) {
const annotation = read_type_annotation(parser);
return {
type: 'Identifier',
name,
start,
loc: {
start: /** @type {Location} */ (locator(start)),
end: /** @type {Location} */ (locator(parser.index))
},
end: parser.index,
typeAnnotation: annotation
};
}
if (!is_bracket_open(parser.template[i])) {
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.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
* @param {number} start
*/
function match_bracket(parser, start) {
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 (is_bracket_open(char)) {
bracket_stack.push(char);
} else if (is_bracket_close(char)) {
const popped = /** @type {string} */ (bracket_stack.pop());
const expected = /** @type {string} */ (get_bracket_close(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);
}
/**
* @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.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.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
};
}

View File

@@ -0,0 +1,81 @@
/** @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 {
const node = parse_expression_at(parser.template, parser.ts, parser.index);
let num_parens = 0;
if (node.leadingComments !== undefined && node.leadingComments.length > 0) {
parser.index = node.leadingComments.at(-1).end;
}
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);
if (node.trailingComments !== undefined && node.trailingComments.length > 0) {
index = node.trailingComments.at(-1).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);
}
}

View File

@@ -0,0 +1,261 @@
/** @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) {
const shadowdom = shadow?.value;
if (shadowdom !== 'open' && shadowdom !== 'none') {
e.svelte_options_invalid_customelement_shadow(shadow);
}
ce.shadow = shadowdom;
}
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);
}
}
}

View File

@@ -0,0 +1,90 @@
/** @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';
const regex_closing_script_tag = /<\/script\s*>/;
const regex_starts_with_closing_script_tag = /^<\/script\s*>/;
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>} 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.ts, true);
} catch (err) {
parser.acorn_error(err);
}
// TODO is this necessary?
ast.start = script_start;
/** @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
};
}

View File

@@ -0,0 +1,625 @@
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import * as e from '../../../errors.js';
const REGEX_MATCHER = /^[~^$*|]?=/;
const REGEX_CLOSING_BRACKET = /[\s\]]/;
const REGEX_ATTRIBUTE_FLAGS = /^[a-zA-Z]+/; // only `i` and `s` are valid today, but make it future-proof
const REGEX_COMBINATOR = /^(\+|~|>|\|\|)/;
const REGEX_PERCENTAGE = /^\d+(\.\d+)?%/;
const REGEX_NTH_OF =
/^(even|odd|\+?(\d+|\d*n(\s*[+-]\s*\d+)?)|-\d*n(\s*\+\s*\d+))((?=\s*[,)])|\s+of\s+)/;
const REGEX_WHITESPACE_OR_COLON = /[\s:]/;
const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/;
const REGEX_VALID_IDENTIFIER_CHAR = /[a-zA-Z0-9_-]/;
const REGEX_COMMENT_CLOSE = /\*\//;
const REGEX_HTML_COMMENT_CLOSE = /-->/;
/**
* @param {Parser} parser
* @param {number} start
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive>} attributes
* @returns {AST.CSS.StyleSheet}
*/
export default function read_style(parser, start, attributes) {
const content_start = parser.index;
const children = read_body(parser, '</style');
const content_end = parser.index;
parser.read(/^<\/style\s*>/);
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 {string} close
* @returns {any[]}
*/
function read_body(parser, close) {
/** @type {Array<AST.CSS.Rule | AST.CSS.Atrule>} */
const children = [];
while (parser.index < parser.template.length) {
allow_comment_or_whitespace(parser);
if (parser.match(close)) {
return children;
}
if (parser.match('@')) {
children.push(read_at_rule(parser));
} else {
children.push(read_rule(parser));
}
}
e.expected_token(parser.template.length, close);
}
/**
* @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,
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,
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;
} else if (char === '\\') {
escaped = true;
} 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);
}
/**
* https://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
* @param {Parser} parser
*/
function read_identifier(parser) {
const start = parser.index;
let identifier = '';
if (parser.match('--') || parser.match_regex(REGEX_LEADING_HYPHEN_OR_DIGIT)) {
e.css_expected_identifier(start);
}
let escaped = false;
while (parser.index < parser.template.length) {
const char = parser.template[parser.index];
if (escaped) {
identifier += '\\' + char;
escaped = false;
} else if (char === '\\') {
escaped = true;
} else if (
/** @type {number} */ (char.codePointAt(0)) >= 160 ||
REGEX_VALID_IDENTIFIER_CHAR.test(char)
) {
identifier += char;
} else {
break;
}
parser.index++;
}
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();
}
}

View File

@@ -0,0 +1,147 @@
/** @import { Context, Visitors } from 'zimmerframe' */
/** @import { FunctionExpression, FunctionDeclaration } from 'estree' */
import { walk } from 'zimmerframe';
import * as b from '../../utils/builders.js';
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.returnType;
delete n.accessibility;
},
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;
},
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;
},
ClassDeclaration(node, context) {
if (node.declare) {
return b.empty;
}
delete node.implements;
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);
}

View File

@@ -0,0 +1,823 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import { is_void } 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, create_expression_metadata, 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 { regex_whitespace } from '../../patterns.js';
const regex_invalid_unquoted_attribute_value = /^(\/>|[\s"'=<>`])/;
const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
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 = /^["']/;
const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]+))/;
const regex_valid_element_name =
/^(?:![a-zA-Z]+|[a-zA-Z](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?|[a-zA-Z][a-zA-Z0-9]*:[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9])$/;
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;
}
const is_closing_tag = parser.eat('/');
const name = parser.read_until(regex_whitespace_or_slash_or_closing_tag);
if (is_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' && !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;
}
if (name.startsWith('svelte:') && !meta_tags.has(name)) {
const bounds = { start: start + 1, end: start + 1 + name.length };
e.svelte_meta_invalid_tag(bounds, list(Array.from(meta_tags.keys())));
}
if (!regex_valid_element_name.test(name) && !regex_valid_component_name.test(name)) {
// <div. -> in the middle of typing -> allow in loose mode
if (!parser.loose || !name.endsWith('.')) {
const bounds = { start: start + 1, end: start + 1 + name.length };
e.tag_invalid_name(bounds);
}
}
if (root_only_meta_tags.has(name)) {
if (name in parser.meta_tags) {
e.svelte_meta_duplicate(start, name);
}
if (parent.type !== 'Root') {
e.svelte_meta_invalid_placement(start, name);
}
parser.meta_tags[name] = true;
}
const type = meta_tags.has(name)
? meta_tags.get(name)
: regex_valid_component_name.test(name) || (parser.loose && name.endsWith('.'))
? 'Component'
: 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
name === 'slot' && !parent_is_shadowroot_template(parser.stack)
? 'SlotElement'
: 'RegularElement';
/** @type {AST.ElementLike} */
const element =
type === 'RegularElement'
? {
type,
start,
end: -1,
name,
attributes: [],
fragment: create_fragment(true),
metadata: {
svg: false,
mathml: false,
scoped: false,
has_spread: false,
path: []
}
}
: /** @type {AST.ElementLike} */ ({
type,
start,
end: -1,
name,
attributes: [],
fragment: create_fragment(true),
metadata: {
// unpopulated at first, differs between types
}
});
parser.allow_whitespace();
if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, name)) {
parent.end = start;
parser.pop();
parser.last_auto_closed_tag = {
tag: parent.name,
reason: name,
depth: parser.stack.length
};
}
/** @type {string[]} */
const unique_names = [];
const current = parser.current();
const is_top_level_script_or_style =
(name === 'script' || 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 === '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);
}
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);
}
}
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 (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(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 (parser.index === parser.template.length) {
while (
parser.index < parser.template_untrimmed.length &&
regex_whitespace.test(parser.template_untrimmed[parser.index])
) {
parser.index++;
}
}
}
}
}
if (self_closing || !closed) {
// don't push self-closing elements onto the stack
element.end = parser.index;
} else if (name === 'textarea') {
// special case
element.fragment.nodes = read_sequence(
parser,
() => regex_closing_textarea_tag.test(parser.template.slice(parser.index)),
'inside <textarea>'
);
parser.read(regex_closing_textarea_tag);
element.end = parser.index;
} else if (name === 'script' || name === 'style') {
// special case
const start = parser.index;
const data = parser.read_until(new RegExp(`</${name}>`));
const end = parser.index;
/** @type {AST.Text} */
const node = {
start,
end,
type: 'Text',
data,
raw: data
};
element.fragment.nodes.push(node);
parser.eat(`</${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 name = parser.read_until(regex_token_ending_character);
if (!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(name, start, parser.index, value);
}
/**
* @param {Parser} parser
* @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | null}
*/
function read_attribute(parser) {
const start = parser.index;
if (parser.eat('{')) {
parser.allow_whitespace();
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: create_expression_metadata()
}
};
return spread;
} else {
const value_start = parser.index;
let name = parser.read_identifier();
if (name === null) {
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
name = '';
} else {
e.attribute_empty_shorthand(start);
}
}
parser.allow_whitespace();
parser.eat('}', true);
/** @type {AST.ExpressionTag} */
const expression = {
type: 'ExpressionTag',
start: value_start,
end: value_start + name.length,
expression: {
start: value_start,
end: value_start + name.length,
type: 'Identifier',
name
},
metadata: {
expression: create_expression_metadata()
}
};
return create_attribute(name, start, parser.index, expression);
}
}
const name = parser.read_until(regex_token_ending_character);
if (!name) return null;
let end = parser.index;
parser.allow_whitespace();
const colon_index = name.indexOf(':');
const type = colon_index !== -1 && get_directive_type(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] = name.slice(colon_index + 1).split('|');
if (directive_name === '') {
e.directive_missing_name({ start, end: start + colon_index + 1 }, name);
}
if (type === 'StyleDirective') {
return {
start,
end,
type,
name: directive_name,
modifiers: /** @type {Array<'important'>} */ (modifiers),
value,
metadata: {
expression: create_expression_metadata()
}
};
}
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;
}
}
/** @type {AST.Directive} */
const directive = {
start,
end,
type,
name: directive_name,
expression,
metadata: {
expression: create_expression_metadata()
}
};
// @ts-expect-error we do this separately from the declaration to avoid upsetting typescript
directive.modifiers = modifiers;
if (directive.type === 'TransitionDirective') {
const direction = 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(name, start, end, value);
}
/**
* @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 (current_chunk.raw) {
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: create_expression_metadata()
}
};
chunks.push(chunk);
current_chunk = {
start: parser.index,
end: -1,
type: 'Text',
raw: '',
data: ''
};
} else {
current_chunk.raw += parser.template[parser.index++];
}
}
if (parser.loose) {
return chunks;
} else {
e.unexpected_eof(parser.template.length);
}
}

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;
}

View File

@@ -0,0 +1,715 @@
/** @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 { create_expression_metadata } 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';
const regex_whitespace_with_closing_curly_brace = /^\s*}/;
/** @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: create_expression_metadata()
}
});
}
/** @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
});
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();
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
});
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()
});
parser.stack.push(block);
parser.fragments.push(block.fragment);
return;
}
if (parser.eat('snippet')) {
parser.require_whitespace();
const name_start = parser.index;
let name = parser.read_identifier();
const name_end = parser.index;
if (name === null) {
if (parser.loose) {
name = '';
} else {
e.expected_identifier(parser.index);
}
}
parser.allow_whitespace();
const params_start = parser.index;
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.ts, params_start)
)
: { params: [] };
parser.allow_whitespace();
parser.eat('}', true);
/** @type {AST.SnippetBlock} */
const block = parser.append({
type: 'SnippetBlock',
start,
end: -1,
expression: {
type: 'Identifier',
start: name_start,
end: name_end,
name
},
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
});
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
});
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
}
});
}
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: {
dynamic: false,
arguments: [],
path: [],
snippets: new Set()
}
});
}
}

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;
let data = '';
while (parser.index < parser.template.length && !parser.match('<') && !parser.match('{')) {
data += parser.template[parser.index++];
}
/** @type {AST.Text} */
parser.append({
type: 'Text',
start,
end: parser.index,
raw: data,
data: decode_character_references(data, false)
});
}

View File

@@ -0,0 +1,164 @@
const SQUARE_BRACKET_OPEN = '[';
const SQUARE_BRACKET_CLOSE = ']';
const CURLY_BRACKET_OPEN = '{';
const CURLY_BRACKET_CLOSE = '}';
const PARENTHESES_OPEN = '(';
const PARENTHESES_CLOSE = ')';
/** @param {string} char */
export function is_bracket_open(char) {
return char === SQUARE_BRACKET_OPEN || char === CURLY_BRACKET_OPEN;
}
/** @param {string} char */
export function is_bracket_close(char) {
return char === SQUARE_BRACKET_CLOSE || char === CURLY_BRACKET_CLOSE;
}
/** @param {string} open */
export function get_bracket_close(open) {
if (open === SQUARE_BRACKET_OPEN) {
return SQUARE_BRACKET_CLOSE;
}
if (open === CURLY_BRACKET_OPEN) {
return CURLY_BRACKET_CLOSE;
}
if (open === PARENTHESES_OPEN) {
return PARENTHESES_CLOSE;
}
}
/**
* @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 = get_bracket_close(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;
}

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
}
};
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,280 @@
/**
* @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
// BSD Licensed
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;
let value = 0;
for (let i = 0; i <= str2.length; i++) {
for (let j = 0; j <= str1.length; j++) {
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 */

View File

@@ -0,0 +1,120 @@
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
/** @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;
}
return NUL;
}

View File

@@ -0,0 +1,287 @@
/** @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 {Visitors<
* AST.CSS.Node,
* {
* keyframes: string[];
* rule: AST.CSS.Rule | null;
* }
* >} 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 {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);
}
}
context.next();
},
ComplexSelector(node, context) {
context.next(); // analyse relevant selectors first
{
const global = node.children.find(is_global);
if (global) {
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.used ||= node.children.every(
({ metadata }) => metadata.is_global || metadata.is_global_like
);
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) {
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;
node.metadata.is_global_block = node.prelude.children.some((selector) => {
let is_global_block = false;
for (const child of selector.children) {
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 !== -1) {
is_global_block = true;
for (let i = idx + 1; i < child.selectors.length; i++) {
walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, {
ComplexSelector(node) {
node.metadata.used = true;
}
});
}
}
}
return is_global_block;
});
if (node.metadata.is_global_block) {
if (node.prelude.children.length > 1) {
e.css_global_block_invalid_list(node.prelude);
}
const complex_selector = node.prelude.children[0];
const global_selector = complex_selector.children.find((r, selector_idx) => {
const idx = r.selectors.findIndex(is_global_block_selector);
if (idx === 0) {
if (r.selectors.length > 1 && selector_idx === 0 && node.metadata.parent_rule === null) {
e.css_global_block_invalid_modifier_start(r.selectors[1]);
}
return true;
} else if (idx !== -1) {
e.css_global_block_invalid_modifier(r.selectors[idx]);
}
});
if (!global_selector) {
throw new Error('Internal error: global block without :global selector');
}
if (global_selector.combinator && global_selector.combinator.name !== ' ') {
e.css_global_block_invalid_combinator(global_selector, global_selector.combinator.name);
}
const declaration = node.block.children.find((child) => child.type === 'Declaration');
if (
declaration &&
// :global { color: red; } is invalid, but foo :global { color: red; } is valid
node.prelude.children.length === 1 &&
node.prelude.children[0].children.length === 1 &&
node.prelude.children[0].children[0].selectors.length === 1
) {
e.css_global_block_invalid_declaration(declaration);
}
}
context.next({
...context.state,
rule: node
});
node.metadata.has_local_selectors = node.prelude.children.some((selector) => {
return selector.children.some(
({ metadata }) => !metadata.is_global && !metadata.is_global_like
);
});
},
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) {
walk(stylesheet, { keyframes: analysis.css.keyframes, rule: null }, css_visitors);
}

View File

File diff suppressed because it is too large Load Diff

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();
}
}
};

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'
))
);
}

View File

@@ -0,0 +1,890 @@
/** @import { Expression, Node, Program } from 'estree' */
/** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { AnalysisState, Visitors } from './types' */
/** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */
import { walk } from 'zimmerframe';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import { extract_identifiers, is_text_attribute } from '../../utils/ast.js';
import * as b from '../../utils/builders.js';
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
import check_graph_for_cycles from './utils/check_graph_for_cycles.js';
import { create_attribute, is_custom_element_node } from '../nodes.js';
import { analyze_css } from './css/css-analyze.js';
import { prune } from './css/css-prune.js';
import { hash, is_rune } from '../../../utils.js';
import { warn_unused } from './css/css-warn.js';
import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js';
import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.js';
import { ArrowFunctionExpression } from './visitors/ArrowFunctionExpression.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.js';
import { Attribute } from './visitors/Attribute.js';
import { AwaitBlock } from './visitors/AwaitBlock.js';
import { BindDirective } from './visitors/BindDirective.js';
import { CallExpression } from './visitors/CallExpression.js';
import { ClassBody } from './visitors/ClassBody.js';
import { ClassDeclaration } from './visitors/ClassDeclaration.js';
import { ClassDirective } from './visitors/ClassDirective.js';
import { Component } from './visitors/Component.js';
import { ConstTag } from './visitors/ConstTag.js';
import { DebugTag } from './visitors/DebugTag.js';
import { EachBlock } from './visitors/EachBlock.js';
import { ExportDefaultDeclaration } from './visitors/ExportDefaultDeclaration.js';
import { ExportNamedDeclaration } from './visitors/ExportNamedDeclaration.js';
import { ExportSpecifier } from './visitors/ExportSpecifier.js';
import { ExpressionStatement } from './visitors/ExpressionStatement.js';
import { ExpressionTag } from './visitors/ExpressionTag.js';
import { FunctionDeclaration } from './visitors/FunctionDeclaration.js';
import { FunctionExpression } from './visitors/FunctionExpression.js';
import { HtmlTag } from './visitors/HtmlTag.js';
import { Identifier } from './visitors/Identifier.js';
import { IfBlock } from './visitors/IfBlock.js';
import { ImportDeclaration } from './visitors/ImportDeclaration.js';
import { KeyBlock } from './visitors/KeyBlock.js';
import { LabeledStatement } from './visitors/LabeledStatement.js';
import { LetDirective } from './visitors/LetDirective.js';
import { MemberExpression } from './visitors/MemberExpression.js';
import { NewExpression } from './visitors/NewExpression.js';
import { OnDirective } from './visitors/OnDirective.js';
import { RegularElement } from './visitors/RegularElement.js';
import { RenderTag } from './visitors/RenderTag.js';
import { SlotElement } from './visitors/SlotElement.js';
import { SnippetBlock } from './visitors/SnippetBlock.js';
import { SpreadAttribute } from './visitors/SpreadAttribute.js';
import { SpreadElement } from './visitors/SpreadElement.js';
import { StyleDirective } from './visitors/StyleDirective.js';
import { SvelteBody } from './visitors/SvelteBody.js';
import { SvelteComponent } from './visitors/SvelteComponent.js';
import { SvelteDocument } from './visitors/SvelteDocument.js';
import { SvelteElement } from './visitors/SvelteElement.js';
import { SvelteFragment } from './visitors/SvelteFragment.js';
import { SvelteHead } from './visitors/SvelteHead.js';
import { SvelteSelf } from './visitors/SvelteSelf.js';
import { SvelteWindow } from './visitors/SvelteWindow.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
import { TaggedTemplateExpression } from './visitors/TaggedTemplateExpression.js';
import { Text } from './visitors/Text.js';
import { TitleElement } from './visitors/TitleElement.js';
import { TransitionDirective } from './visitors/TransitionDirective.js';
import { UpdateExpression } from './visitors/UpdateExpression.js';
import { UseDirective } from './visitors/UseDirective.js';
import { VariableDeclarator } from './visitors/VariableDeclarator.js';
import is_reference from 'is-reference';
import { mark_subtree_dynamic } from './visitors/shared/fragment.js';
/**
* @type {Visitors}
*/
const visitors = {
_(node, { state, next, path }) {
const parent = path.at(-1);
/** @type {string[]} */
const ignores = [];
if (parent?.type === 'Fragment' && node.type !== 'Comment' && node.type !== 'Text') {
const idx = parent.nodes.indexOf(/** @type {any} */ (node));
for (let i = idx - 1; i >= 0; i--) {
const prev = parent.nodes[i];
if (prev.type === 'Comment') {
ignores.push(
...extract_svelte_ignore(
prev.start + 4 /* '<!--'.length */,
prev.data,
state.analysis.runes
)
);
} else if (prev.type !== 'Text') {
break;
}
}
} else {
const comments = /** @type {any} */ (node).leadingComments;
if (comments) {
for (const comment of comments) {
ignores.push(
...extract_svelte_ignore(
comment.start + 2 /* '//'.length */,
comment.value,
state.analysis.runes
)
);
}
}
}
if (ignores.length > 0) {
push_ignore(ignores);
}
ignore_map.set(node, structuredClone(ignore_stack));
const scope = state.scopes.get(node);
next(scope !== undefined && scope !== state.scope ? { ...state, scope } : state);
if (ignores.length > 0) {
pop_ignore();
}
},
ArrowFunctionExpression,
AssignmentExpression,
Attribute,
AwaitBlock,
BindDirective,
CallExpression,
ClassBody,
ClassDeclaration,
ClassDirective,
Component,
ConstTag,
DebugTag,
EachBlock,
ExportDefaultDeclaration,
ExportNamedDeclaration,
ExportSpecifier,
ExpressionStatement,
ExpressionTag,
FunctionDeclaration,
FunctionExpression,
HtmlTag,
Identifier,
IfBlock,
ImportDeclaration,
KeyBlock,
LabeledStatement,
LetDirective,
MemberExpression,
NewExpression,
OnDirective,
RegularElement,
RenderTag,
SlotElement,
SnippetBlock,
SpreadAttribute,
SpreadElement,
StyleDirective,
SvelteBody,
SvelteComponent,
SvelteDocument,
SvelteElement,
SvelteFragment,
SvelteHead,
SvelteSelf,
SvelteWindow,
SvelteBoundary,
TaggedTemplateExpression,
Text,
TransitionDirective,
TitleElement,
UpdateExpression,
UseDirective,
VariableDeclarator
};
/**
* @param {AST.Script | null} script
* @param {ScopeRoot} root
* @param {boolean} allow_reactive_declarations
* @param {Scope | null} parent
* @returns {Js}
*/
function js(script, root, allow_reactive_declarations, parent) {
/** @type {Program} */
const ast = script?.content ?? {
type: 'Program',
sourceType: 'module',
start: -1,
end: -1,
body: []
};
const { scope, scopes } = create_scopes(ast, root, allow_reactive_declarations, parent);
return { ast, scope, scopes };
}
/**
* @param {string} filename
*/
function get_component_name(filename) {
const parts = filename.split(/[/\\]/);
const basename = /** @type {string} */ (parts.pop());
const last_dir = /** @type {string} */ (parts.at(-1));
let name = basename.replace('.svelte', '');
if (name === 'index' && last_dir && last_dir !== 'src') {
name = last_dir;
}
return name[0].toUpperCase() + name.slice(1);
}
const RESERVED = ['$$props', '$$restProps', '$$slots'];
/**
* @param {Program} ast
* @param {ValidatedModuleCompileOptions} options
* @returns {Analysis}
*/
export function analyze_module(ast, options) {
const { scope, scopes } = create_scopes(ast, new ScopeRoot(), false, null);
for (const [name, references] of scope.references) {
if (name[0] !== '$' || RESERVED.includes(name)) continue;
if (name === '$' || name[1] === '$') {
e.global_reference_invalid(references[0].node, name);
}
const binding = scope.get(name.slice(1));
if (binding !== null && !is_rune(name)) {
e.store_invalid_subscription_module(references[0].node);
}
}
/** @type {Analysis} */
const analysis = {
module: { ast, scope, scopes },
name: options.filename,
accessors: false,
runes: true,
immutable: true,
tracing: false
};
walk(
/** @type {Node} */ (ast),
{
scope,
scopes,
// @ts-expect-error TODO
analysis
},
visitors
);
return analysis;
}
/**
* @param {AST.Root} root
* @param {string} source
* @param {ValidatedCompileOptions} options
* @returns {ComponentAnalysis}
*/
export function analyze_component(root, source, options) {
const scope_root = new ScopeRoot();
const module = js(root.module, scope_root, false, null);
const instance = js(root.instance, scope_root, true, module.scope);
const { scope, scopes } = create_scopes(root.fragment, scope_root, false, instance.scope);
/** @type {Template} */
const template = { ast: root.fragment, scope, scopes };
let synthetic_stores_legacy_check = [];
// create synthetic bindings for store subscriptions
for (const [name, references] of module.scope.references) {
if (name[0] !== '$' || RESERVED.includes(name)) continue;
if (name === '$' || name[1] === '$') {
e.global_reference_invalid(references[0].node, name);
}
const store_name = name.slice(1);
const declaration = instance.scope.get(store_name);
const init = /** @type {Node | undefined} */ (declaration?.initial);
// If we're not in legacy mode through the compiler option, assume the user
// is referencing a rune and not a global store.
if (
options.runes === false ||
!is_rune(name) ||
(declaration !== null &&
// const state = $state(0) is valid
(get_rune(init, instance.scope) === null ||
// rune-line names received as props are valid too (but we have to protect against $props as store)
(store_name !== 'props' && get_rune(init, instance.scope) === '$props')) &&
// allow `import { derived } from 'svelte/store'` in the same file as `const x = $derived(..)` because one is not a subscription to the other
!(
name === '$derived' &&
declaration.initial?.type === 'ImportDeclaration' &&
declaration.initial.source.value === 'svelte/store'
))
) {
let is_nested_store_subscription_node = undefined;
search: for (const reference of references) {
for (let i = reference.path.length - 1; i >= 0; i--) {
const scope =
scopes.get(reference.path[i]) ||
module.scopes.get(reference.path[i]) ||
instance.scopes.get(reference.path[i]);
if (scope) {
const owner = scope?.owner(store_name);
if (!!owner && owner !== module.scope && owner !== instance.scope) {
is_nested_store_subscription_node = reference.node;
break search;
}
break;
}
}
}
if (is_nested_store_subscription_node) {
e.store_invalid_scoped_subscription(is_nested_store_subscription_node);
}
if (options.runes !== false) {
if (declaration === null && /[a-z]/.test(store_name[0])) {
e.global_reference_invalid(references[0].node, name);
} else if (declaration !== null && is_rune(name)) {
for (const { node, path } of references) {
if (path.at(-1)?.type === 'CallExpression') {
w.store_rune_conflict(node, store_name);
}
}
}
}
if (module.ast) {
for (const { node, path } of references) {
// if the reference is inside module, error. this is a bit hacky but it works
if (
/** @type {number} */ (node.start) > /** @type {number} */ (module.ast.start) &&
/** @type {number} */ (node.end) < /** @type {number} */ (module.ast.end) &&
// const state = $state(0) is valid
get_rune(/** @type {Node} */ (path.at(-1)), module.scope) === null
) {
e.store_invalid_subscription(node);
}
}
}
// we push to the array because at this moment in time we can't be sure if we are in legacy
// mode yet because we are still changing the module scope
synthetic_stores_legacy_check.push(() => {
// if we are creating a synthetic binding for a let declaration we should also declare
// the declaration as state in case it's reassigned and we are not in runes mode (the function will
// not be called if we are not in runes mode, that's why there's no !runes check here)
if (
declaration !== null &&
declaration.kind === 'normal' &&
declaration.declaration_kind === 'let' &&
declaration.reassigned
) {
declaration.kind = 'state';
}
});
const binding = instance.scope.declare(b.id(name), 'store_sub', 'synthetic');
binding.references = references;
instance.scope.references.set(name, references);
module.scope.references.delete(name);
}
}
const component_name = get_component_name(options.filename);
const runes = options.runes ?? Array.from(module.scope.references.keys()).some(is_rune);
if (!runes) {
for (let check of synthetic_stores_legacy_check) {
check();
}
}
if (runes && root.module) {
const context = root.module.attributes.find((attribute) => attribute.name === 'context');
if (context) {
w.script_context_deprecated(context);
}
}
// TODO remove all the ?? stuff, we don't need it now that we're validating the config
/** @type {ComponentAnalysis} */
const analysis = {
name: module.scope.generate(options.name ?? component_name),
root: scope_root,
module,
instance,
template,
elements: [],
runes,
tracing: false,
immutable: runes || options.immutable,
exports: [],
uses_props: false,
uses_rest_props: false,
uses_slots: false,
uses_component_bindings: false,
uses_render_tags: false,
needs_context: false,
needs_props: false,
event_directive_node: null,
uses_event_attributes: false,
custom_element: options.customElementOptions ?? options.customElement,
inject_styles: options.css === 'injected' || options.customElement,
accessors: options.customElement
? true
: (runes ? false : !!options.accessors) ||
// because $set method needs accessors
options.compatibility?.componentApi === 4,
reactive_statements: new Map(),
binding_groups: new Map(),
slot_names: new Map(),
css: {
ast: root.css,
hash: root.css
? options.cssHash({
css: root.css.content.styles,
filename: options.filename,
name: component_name,
hash
})
: '',
keyframes: []
},
source,
undefined_exports: new Map(),
snippet_renderers: new Map(),
snippets: new Set()
};
if (!runes) {
// every exported `let` or `var` declaration becomes a prop, everything else becomes an export
for (const node of instance.ast.body) {
if (node.type !== 'ExportNamedDeclaration') continue;
analysis.needs_props = true;
if (node.declaration) {
if (
node.declaration.type === 'FunctionDeclaration' ||
node.declaration.type === 'ClassDeclaration'
) {
analysis.exports.push({
name: /** @type {import('estree').Identifier} */ (node.declaration.id).name,
alias: null
});
} else if (node.declaration.type === 'VariableDeclaration') {
if (node.declaration.kind === 'const') {
for (const declarator of node.declaration.declarations) {
for (const node of extract_identifiers(declarator.id)) {
analysis.exports.push({ name: node.name, alias: null });
}
}
} else {
for (const declarator of node.declaration.declarations) {
for (const id of extract_identifiers(declarator.id)) {
const binding = /** @type {Binding} */ (instance.scope.get(id.name));
binding.kind = 'bindable_prop';
}
}
}
}
} else {
for (const specifier of node.specifiers) {
if (specifier.local.type !== 'Identifier' || specifier.exported.type !== 'Identifier') {
continue;
}
const binding = instance.scope.get(specifier.local.name);
if (
binding &&
(binding.declaration_kind === 'var' || binding.declaration_kind === 'let')
) {
binding.kind = 'bindable_prop';
if (specifier.exported.name !== specifier.local.name) {
binding.prop_alias = specifier.exported.name;
}
} else {
analysis.exports.push({ name: specifier.local.name, alias: specifier.exported.name });
}
}
}
}
// if reassigned/mutated bindings are referenced in `$:` blocks
// or the template, turn them into state
for (const binding of instance.scope.declarations.values()) {
if (binding.kind !== 'normal') continue;
for (const { node, path } of binding.references) {
if (node === binding.node) continue;
if (binding.updated) {
if (
path[path.length - 1].type === 'StyleDirective' ||
path.some((node) => node.type === 'Fragment') ||
(path[1].type === 'LabeledStatement' && path[1].label.name === '$')
) {
binding.kind = 'state';
}
}
}
}
// more legacy nonsense: if an `each` binding is reassigned/mutated,
// treat the expression as being mutated as well
walk(/** @type {AST.SvelteNode} */ (template.ast), null, {
EachBlock(node) {
const scope = /** @type {Scope} */ (template.scopes.get(node));
for (const binding of scope.declarations.values()) {
if (binding.updated) {
const state = { scope: /** @type {Scope} */ (scope.parent), scopes: template.scopes };
walk(node.expression, state, {
// @ts-expect-error
_: set_scope,
Identifier(node, context) {
const parent = /** @type {Expression} */ (context.path.at(-1));
if (is_reference(node, parent)) {
const binding = context.state.scope.get(node.name);
if (
binding &&
binding.kind === 'normal' &&
binding.declaration_kind !== 'import'
) {
binding.kind = 'state';
binding.mutated = binding.updated = true;
}
}
}
});
break;
}
}
}
});
}
if (root.options) {
for (const attribute of root.options.attributes) {
if (attribute.name === 'accessors' && analysis.runes) {
w.options_deprecated_accessors(attribute);
}
if (attribute.name === 'customElement' && !options.customElement) {
w.options_missing_custom_element(attribute);
}
if (attribute.name === 'immutable' && analysis.runes) {
w.options_deprecated_immutable(attribute);
}
}
}
if (analysis.runes) {
const props_refs = module.scope.references.get('$$props');
if (props_refs) {
e.legacy_props_invalid(props_refs[0].node);
}
const rest_props_refs = module.scope.references.get('$$restProps');
if (rest_props_refs) {
e.legacy_rest_props_invalid(rest_props_refs[0].node);
}
for (const { ast, scope, scopes } of [module, instance, template]) {
/** @type {AnalysisState} */
const state = {
scope,
scopes,
analysis,
options,
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
parent_element: null,
has_props_rune: false,
component_slots: new Set(),
expression: null,
private_derived_state: [],
function_depth: scope.function_depth,
instance_scope: instance.scope,
reactive_statement: null,
reactive_statements: new Map()
};
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);
}
// warn on any nonstate declarations that are a) reassigned and b) referenced in the template
for (const scope of [module.scope, instance.scope]) {
outer: for (const [name, binding] of scope.declarations) {
if (binding.kind === 'normal' && binding.reassigned) {
inner: for (const { path } of binding.references) {
if (path[0].type !== 'Fragment') continue;
for (let i = 1; i < path.length; i += 1) {
const type = path[i].type;
if (
type === 'FunctionDeclaration' ||
type === 'FunctionExpression' ||
type === 'ArrowFunctionExpression'
) {
continue inner;
}
// bind:this doesn't need to be a state reference if it will never change
if (
type === 'BindDirective' &&
/** @type {AST.BindDirective} */ (path[i]).name === 'this'
) {
for (let j = i - 1; j >= 0; j -= 1) {
const type = path[j].type;
if (
type === 'IfBlock' ||
type === 'EachBlock' ||
type === 'AwaitBlock' ||
type === 'KeyBlock'
) {
w.non_reactive_update(binding.node, name);
continue outer;
}
}
continue inner;
}
}
w.non_reactive_update(binding.node, name);
continue outer;
}
}
}
}
} else {
instance.scope.declare(b.id('$$props'), 'rest_prop', 'synthetic');
instance.scope.declare(b.id('$$restProps'), 'rest_prop', 'synthetic');
for (const { ast, scope, scopes } of [module, instance, template]) {
/** @type {AnalysisState} */
const state = {
scope,
scopes,
analysis,
options,
parent_element: null,
has_props_rune: false,
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
instance_scope: instance.scope,
reactive_statement: null,
reactive_statements: analysis.reactive_statements,
component_slots: new Set(),
expression: null,
private_derived_state: [],
function_depth: scope.function_depth
};
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);
}
for (const [name, binding] of instance.scope.declarations) {
if (
(binding.kind === 'prop' || binding.kind === 'bindable_prop') &&
binding.node.name !== '$$props'
) {
const references = binding.references.filter(
(r) => r.node !== binding.node && r.path.at(-1)?.type !== 'ExportSpecifier'
);
if (!references.length && !instance.scope.declarations.has(`$${name}`)) {
w.export_let_unused(binding.node, name);
}
}
}
analysis.reactive_statements = order_reactive_statements(analysis.reactive_statements);
}
for (const node of analysis.module.ast.body) {
if (node.type === 'ExportNamedDeclaration' && node.specifiers !== null && node.source == null) {
for (const specifier of node.specifiers) {
if (specifier.local.type !== 'Identifier') continue;
const binding = analysis.module.scope.get(specifier.local.name);
if (!binding) e.export_undefined(specifier, specifier.local.name);
}
}
}
if (analysis.event_directive_node && analysis.uses_event_attributes) {
e.mixed_event_handler_syntaxes(
analysis.event_directive_node,
analysis.event_directive_node.name
);
}
for (const [node, resolved] of analysis.snippet_renderers) {
if (!resolved) {
node.metadata.snippets = analysis.snippets;
}
for (const snippet of node.metadata.snippets) {
snippet.metadata.sites.add(node);
}
}
if (
analysis.uses_render_tags &&
(analysis.uses_slots || (!analysis.custom_element && analysis.slot_names.size > 0))
) {
const pos = analysis.slot_names.values().next().value ?? analysis.source.indexOf('$$slot');
e.slot_snippet_conflict(pos);
}
if (analysis.css.ast) {
analyze_css(analysis.css.ast, analysis);
// mark nodes as scoped/unused/empty etc
for (const node of analysis.elements) {
prune(analysis.css.ast, node);
}
const { comment } = analysis.css.ast.content;
const should_ignore_unused =
comment &&
extract_svelte_ignore(comment.start, comment.data, analysis.runes).includes(
'css_unused_selector'
);
if (!should_ignore_unused) {
warn_unused(analysis.css.ast);
}
outer: for (const node of analysis.elements) {
if (node.metadata.scoped) {
// Dynamic elements in dom mode always use spread for attributes and therefore shouldn't have a class attribute added to them
// TODO this happens during the analysis phase, which shouldn't know anything about client vs server
if (node.type === 'SvelteElement' && options.generate === 'client') continue;
/** @type {AST.Attribute | undefined} */
let class_attribute = undefined;
for (const attribute of node.attributes) {
if (attribute.type === 'SpreadAttribute') {
// The spread method appends the hash to the end of the class attribute on its own
continue outer;
}
if (attribute.type !== 'Attribute') continue;
if (attribute.name.toLowerCase() !== 'class') continue;
// The dynamic class method appends the hash to the end of the class attribute on its own
if (attribute.metadata.needs_clsx) continue outer;
class_attribute = attribute;
}
if (class_attribute && class_attribute.value !== true) {
if (is_text_attribute(class_attribute)) {
class_attribute.value[0].data += ` ${analysis.css.hash}`;
} else {
/** @type {AST.Text} */
const css_text = {
type: 'Text',
data: ` ${analysis.css.hash}`,
raw: ` ${analysis.css.hash}`,
start: -1,
end: -1
};
if (Array.isArray(class_attribute.value)) {
class_attribute.value.push(css_text);
} else {
class_attribute.value = [class_attribute.value, css_text];
}
}
} else {
node.attributes.push(
create_attribute('class', -1, -1, [
{
type: 'Text',
data: analysis.css.hash,
raw: analysis.css.hash,
start: -1,
end: -1
}
])
);
if (is_custom_element_node(node) && node.attributes.length === 1) {
mark_subtree_dynamic(node.metadata.path);
}
}
}
}
}
// TODO
// analysis.stylesheet.warn_on_unused_selectors(analysis);
return analysis;
}
/**
* @param {Map<import('estree').LabeledStatement, ReactiveStatement>} unsorted_reactive_declarations
*/
function order_reactive_statements(unsorted_reactive_declarations) {
/** @typedef {[import('estree').LabeledStatement, ReactiveStatement]} Tuple */
/** @type {Map<string, Array<Tuple>>} */
const lookup = new Map();
for (const [node, declaration] of unsorted_reactive_declarations) {
for (const binding of declaration.assignments) {
const statements = lookup.get(binding.node.name) ?? [];
statements.push([node, declaration]);
lookup.set(binding.node.name, statements);
}
}
/** @type {Array<[string, string]>} */
const edges = [];
for (const [, { assignments, dependencies }] of unsorted_reactive_declarations) {
for (const assignment of assignments) {
for (const dependency of dependencies) {
if (!assignments.has(dependency)) {
edges.push([assignment.node.name, dependency.node.name]);
}
}
}
}
const cycle = check_graph_for_cycles(edges);
if (cycle?.length) {
const declaration = /** @type {Tuple[]} */ (lookup.get(cycle[0]))[0];
e.reactive_declaration_cycle(declaration[0], cycle.join(' → '));
}
// We use a map and take advantage of the fact that the spec says insertion order is preserved when iterating
/** @type {Map<import('estree').LabeledStatement, ReactiveStatement>} */
const reactive_declarations = new Map();
/**
*
* @param {import('estree').LabeledStatement} node
* @param {ReactiveStatement} declaration
* @returns
*/
const add_declaration = (node, declaration) => {
if ([...reactive_declarations.values()].includes(declaration)) return;
for (const binding of declaration.dependencies) {
if (declaration.assignments.has(binding)) continue;
for (const [node, earlier] of lookup.get(binding.node.name) ?? []) {
add_declaration(node, earlier);
}
}
reactive_declarations.set(node, declaration);
};
for (const [node, declaration] of unsorted_reactive_declarations) {
add_declaration(node, declaration);
}
return reactive_declarations;
}

View File

@@ -0,0 +1,39 @@
import type { Scope } from '../scope.js';
import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
import type { AST, ExpressionMetadata, ValidatedCompileOptions } from '#compiler';
import type { LabeledStatement } from 'estree';
export interface AnalysisState {
scope: Scope;
scopes: Map<AST.SvelteNode, Scope>;
analysis: ComponentAnalysis;
options: ValidatedCompileOptions;
ast_type: 'instance' | 'template' | 'module';
/**
* Tag name of the parent element. `null` if the parent is `svelte:element`, `#snippet`, a component or the root.
* Parent doesn't necessarily mean direct path predecessor because there could be `#each`, `#if` etc in-between.
*/
parent_element: string | null;
has_props_rune: boolean;
/** Which slots the current parent component has */
component_slots: Set<string>;
/** Information about the current expression/directive/block value */
expression: ExpressionMetadata | null;
private_derived_state: string[];
function_depth: number;
// legacy stuff
instance_scope: Scope;
reactive_statement: null | ReactiveStatement;
reactive_statements: Map<LabeledStatement, ReactiveStatement>;
}
export type Context<State extends AnalysisState = AnalysisState> = import('zimmerframe').Context<
AST.SvelteNode,
State
>;
export type Visitors<State extends AnalysisState = AnalysisState> = import('zimmerframe').Visitors<
AST.SvelteNode,
State
>;

View File

@@ -0,0 +1,46 @@
/**
* @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();
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];
}

View File

@@ -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);
}

View File

@@ -0,0 +1,27 @@
/** @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.state);
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);
}
}
}
}
context.next();
}

View File

@@ -0,0 +1,243 @@
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression } from 'estree' */
/** @import { AST, DelegatedEvent } from '#compiler' */
/** @import { Context } from '../types' */
import { cannot_be_set_statically, is_capture_event, is_delegated } from '../../../../utils.js';
import {
get_attribute_chunks,
get_attribute_expression,
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);
}
// special case <img loading="lazy" />
if (node.name === 'loading' && parent.name === 'img') {
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;
}
const expression = get_attribute_expression(node);
const delegated_event = get_delegated_event(node.name.slice(2), expression, context);
if (delegated_event !== null) {
if (delegated_event.hoisted) {
delegated_event.function.metadata.hoisted = true;
}
node.metadata.delegated = delegated_event;
}
}
}
}
/** @type {DelegatedEvent} */
const unhoisted = { hoisted: false };
/**
* Checks if given event attribute can be delegated/hoisted and returns the corresponding info if so
* @param {string} event_name
* @param {Expression | null} handler
* @param {Context} context
* @returns {null | DelegatedEvent}
*/
function get_delegated_event(event_name, handler, context) {
// Handle delegated event handlers. Bail out if not a delegated event.
if (!handler || !is_delegated(event_name)) {
return null;
}
// If we are not working with a RegularElement, then bail out.
const element = context.path.at(-1);
if (element?.type !== 'RegularElement') {
return null;
}
/** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | null} */
let target_function = null;
let binding = null;
if (element.metadata.has_spread) {
// event attribute becomes part of the dynamic spread array
return unhoisted;
}
if (handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') {
target_function = handler;
} else if (handler.type === 'Identifier') {
binding = context.state.scope.get(handler.name);
if (context.state.analysis.module.scope.references.has(handler.name)) {
// If a binding with the same name is referenced in the module scope (even if not declared there), bail out
return unhoisted;
}
if (binding != null) {
for (const { path } of binding.references) {
const parent = path.at(-1);
if (parent === undefined) return unhoisted;
const grandparent = path.at(-2);
/** @type {AST.RegularElement | null} */
let element = null;
/** @type {string | null} */
let event_name = null;
if (parent.type === 'OnDirective') {
element = /** @type {AST.RegularElement} */ (grandparent);
event_name = parent.name;
} else if (
parent.type === 'ExpressionTag' &&
grandparent?.type === 'Attribute' &&
is_event_attribute(grandparent)
) {
element = /** @type {AST.RegularElement} */ (path.at(-3));
const attribute = /** @type {AST.Attribute} */ (grandparent);
event_name = get_attribute_event_name(attribute.name);
}
if (element && event_name) {
if (
element.type !== 'RegularElement' ||
element.metadata.has_spread ||
!is_delegated(event_name)
) {
return unhoisted;
}
} else if (parent.type !== 'FunctionDeclaration' && parent.type !== 'VariableDeclarator') {
return unhoisted;
}
}
}
// If the binding is exported, bail out
if (context.state.analysis.exports.find((node) => node.name === handler.name)) {
return unhoisted;
}
if (binding !== null && binding.initial !== null && !binding.updated && !binding.is_called) {
const binding_type = binding.initial.type;
if (
binding_type === 'ArrowFunctionExpression' ||
binding_type === 'FunctionDeclaration' ||
binding_type === 'FunctionExpression'
) {
target_function = binding.initial;
}
}
}
// If we can't find a function, or the function has multiple parameters, bail out
if (target_function == null || target_function.params.length > 1) {
return unhoisted;
}
const visited_references = new Set();
const scope = target_function.metadata.scope;
for (const [reference] of scope.references) {
// Bail out if the arguments keyword is used or $host is referenced
if (reference === 'arguments' || reference === '$host') return unhoisted;
// Bail out if references a store subscription
if (scope.get(`$${reference}`)?.kind === 'store_sub') return unhoisted;
const binding = scope.get(reference);
const local_binding = context.state.scope.get(reference);
// If we are referencing a binding that is shadowed in another scope then bail out.
if (local_binding !== null && binding !== null && local_binding.node !== binding.node) {
return unhoisted;
}
// If we have multiple references to the same store using $ prefix, bail out.
if (
binding !== null &&
binding.kind === 'store_sub' &&
visited_references.has(reference.slice(1))
) {
return unhoisted;
}
// If we reference the index within an each block, then bail out.
if (binding !== null && binding.initial?.type === 'EachBlock') return unhoisted;
if (
binding !== null &&
// Bail out if the the binding is a rest param
(binding.declaration_kind === 'rest_param' ||
// Bail out if we reference anything from the EachBlock (for now) that mutates in non-runes mode,
(((!context.state.analysis.runes && binding.kind === 'each') ||
// or any normal not reactive bindings that are mutated.
binding.kind === 'normal') &&
binding.updated))
) {
return unhoisted;
}
visited_references.add(reference);
}
return { hoisted: true, function: target_function };
}
/**
* @param {string} event_name
*/
function get_attribute_event_name(event_name) {
event_name = event_name.slice(2);
if (is_capture_event(event_name)) {
event_name = event_name.slice(0, -7);
}
return event_name;
}

View File

@@ -0,0 +1,45 @@
/** @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.next();
}

View File

@@ -0,0 +1,258 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import {
extract_all_identifiers_from_expression,
is_text_attribute,
object
} from '../../../utils/ast.js';
import { validate_no_const_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">');
}
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 '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);
return;
}
validate_no_const_assignment(node, node.expression, context.state.scope, true);
const assignee = node.expression;
const left = object(assignee);
if (left === null) {
e.bind_invalid_expression(node);
}
const binding = context.state.scope.get(left.name);
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 (context.state.analysis.runes && binding?.kind === 'each') {
e.each_item_invalid_assignment(node);
}
if (binding?.kind === 'snippet') {
e.snippet_parameter_assignment(node);
}
}
if (node.name === 'group') {
if (!binding) {
throw new Error('Cannot find declaration for bind:group');
}
// 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
};
}
if (binding?.kind === 'each' && binding.metadata?.inside_rest) {
w.bind_invalid_each_rest(binding.node, binding.node.name);
}
context.next();
}

View File

@@ -0,0 +1,243 @@
/** @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, unwrap_optional } 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 '../../../utils/builders.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);
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);
}
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);
}
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 '$state':
case '$state.raw':
case '$derived':
case '$derived.by':
if (
(parent.type !== 'VariableDeclarator' ||
get_parent(context.path, -3).type === 'ConstTag') &&
!(parent.type === 'PropertyDefinition' && !parent.static && !parent.computed)
) {
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 (rune === '$state' && 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 '$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.snapshot':
if (node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
}
break;
}
if (node.callee.type === 'Identifier') {
const binding = context.state.scope.get(node.callee.name);
if (binding !== null) {
binding.is_called = true;
}
}
// `$inspect(foo)` or `$derived(foo) should not trigger the `static-state-reference` warning
if (rune === '$inspect' || rune === '$derived') {
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;
}
}

View File

@@ -0,0 +1,27 @@
/** @import { ClassBody } from 'estree' */
/** @import { Context } from '../types' */
import { get_rune } from '../../scope.js';
/**
* @param {ClassBody} node
* @param {Context} context
*/
export function ClassBody(node, context) {
/** @type {string[]} */
const private_derived_state = [];
for (const definition of node.body) {
if (
definition.type === 'PropertyDefinition' &&
definition.key.type === 'PrivateIdentifier' &&
definition.value?.type === 'CallExpression'
) {
const rune = get_rune(definition.value, context.state.scope);
if (rune === '$derived' || rune === '$derived.by') {
private_derived_state.push(definition.key.name);
}
}
}
context.next({ ...context.state, private_derived_state });
}

View File

@@ -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();
}

View File

@@ -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 });
}

View File

@@ -0,0 +1,20 @@
/** @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('.'));
visit_component(node, context);
}

View File

@@ -0,0 +1,35 @@
/** @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 !== 'RegularElement' && grand_parent?.type !== 'SvelteElement') ||
!grand_parent.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot')))
) {
e.const_tag_invalid_placement(node);
}
context.next();
}

View File

@@ -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();
}

View File

@@ -0,0 +1,42 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
/** @import { Scope } from '../../scope' */
import * as e from '../../../errors.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;
}
// 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);
mark_subtree_dynamic(context.path);
}

View File

@@ -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();
}

View File

@@ -0,0 +1,61 @@
/** @import { ExportNamedDeclaration, Identifier, Node } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { Context } from '../types' */
/** @import { Scope } from '../../scope' */
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 (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 });
}
}
}
}
}
}

View File

@@ -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 = binding.updated = true;
}
} else {
validate_export(node, context.state.scope, local_name);
}
}

View File

@@ -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();
}

View File

@@ -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 });
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -0,0 +1,125 @@
/** @import { Expression, Identifier } from 'estree' */
/** @import { EachBlock } from '#compiler' */
/** @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';
/**
* @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)) === null
) {
/** @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.has_state ||= binding.kind !== 'normal';
}
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') &&
// We're only concerned with reads here
(parent.type !== 'AssignmentExpression' || parent.left !== node) &&
parent.type !== 'UpdateExpression'
) {
w.state_referenced_locally(node);
}
if (
context.state.reactive_statement &&
binding.scope === context.state.analysis.module.scope &&
binding.reassigned
) {
w.reactive_declaration_module_script_dependency(node);
}
}
}

View File

@@ -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.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.next();
}

View File

@@ -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);
}
}
}
}
}
}

View File

@@ -0,0 +1,20 @@
/** @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.next();
}

View File

@@ -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.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();
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,30 @@
/** @import { MemberExpression, Node } from 'estree' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { object } from '../../../utils/ast.js';
import { is_pure, is_safe_identifier } from './shared/utils.js';
import { mark_subtree_dynamic } from './shared/fragment.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 && !is_pure(node, context)) {
context.state.expression.has_state = true;
}
if (!is_safe_identifier(node, context.state.scope)) {
context.state.analysis.needs_context = true;
}
context.next();
}

View File

@@ -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();
}

View File

@@ -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 });
}

View File

@@ -0,0 +1,195 @@
/** @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 } from '../../nodes.js';
import { regex_starts_with_newline } from '../../patterns.js';
import { check_element } from './shared/a11y.js';
import { validate_element } from './shared/element.js';
import { mark_subtree_dynamic } from './shared/fragment.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',
/** @type {AST.Text} */ (node.fragment.nodes.at(0)).start,
/** @type {AST.Text} */ (node.fragment.nodes.at(-1)).end,
// @ts-ignore
node.fragment.nodes
)
);
node.fragment.nodes = [];
}
}
// 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.attributes.push(create_attribute('value', child.start, child.end, [child]));
}
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)
) {
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;
}
}
}
}
}

View File

@@ -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 { create_expression_metadata } 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);
for (const arg of expression.arguments) {
const metadata = create_expression_metadata();
node.metadata.arguments.push(metadata);
context.visit(arg, {
...context.state,
expression: metadata
});
}
}

View File

@@ -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();
}

View File

@@ -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);
} else {
const undefined_export = context.state.analysis.undefined_exports.get(name);
if (undefined_export) {
e.snippet_invalid_export(undefined_export);
}
}
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 || 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;
}

View File

@@ -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 });
}

View File

@@ -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();
}

View File

@@ -0,0 +1,37 @@
/** @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;
}
}
} else {
context.next();
for (const chunk of get_attribute_chunks(node.value)) {
if (chunk.type !== 'ExpressionTag') continue;
node.metadata.expression.has_state ||= chunk.metadata.expression.has_state;
node.metadata.expression.has_call ||= chunk.metadata.expression.has_call;
}
}
}

View File

@@ -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();
}

View File

@@ -0,0 +1,27 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
const valid = ['onerror', 'failed'];
/**
* @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);
}
}
context.next();
}

View File

@@ -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);
visit_component(node, context);
}

View File

@@ -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();
}

View File

@@ -0,0 +1,66 @@
/** @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.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.next({ ...context.state, parent_element: null });
}

View File

@@ -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 });
}

View File

@@ -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();
}

View File

@@ -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 } 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)' ? 'Self' : context.state.analysis.name;
const basename =
filename === '(unknown)'
? 'Self.svelte'
: /** @type {string} */ (filename.split(/[/\\]/).pop());
w.svelte_self_deprecated(node, name, basename);
}
visit_component(node, context);
}

View File

@@ -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();
}

View File

@@ -0,0 +1,23 @@
/** @import { TaggedTemplateExpression, VariableDeclarator } 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;
}
if (node.tag.type === 'Identifier') {
const binding = context.state.scope.get(node.tag.name);
if (binding !== null) {
binding.is_called = true;
}
}
context.next();
}

View File

@@ -0,0 +1,20 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { is_tag_valid_with_parent } from '../../../../html-tree-validation.js';
import { regex_not_whitespace } from '../../patterns.js';
import * as e from '../../../errors.js';
/**
* @param {AST.Text} node
* @param {Context} context
*/
export function Text(node, context) {
const in_template = context.path.at(-1)?.type === 'Fragment';
if (in_template && 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);
}
}
}

View File

@@ -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();
}

View File

@@ -0,0 +1,14 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
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();
}

View File

@@ -0,0 +1,25 @@
/** @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.state);
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);
}
}
}
context.next();
}

View File

@@ -0,0 +1,12 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {AST.UseDirective} node
* @param {Context} context
*/
export function UseDirective(node, context) {
mark_subtree_dynamic(context.path);
context.next();
}

View File

@@ -0,0 +1,120 @@
/** @import { Expression, Identifier, Literal, VariableDeclarator } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { Context } from '../types' */
import { get_rune } from '../../scope.js';
import { ensure_no_module_import_conflict, validate_identifier_name } from './shared/utils.js';
import * as e from '../../../errors.js';
import { extract_paths } from '../../../utils/ast.js';
import { equal } from '../../../utils/assert.js';
/**
* @param {VariableDeclarator} node
* @param {Context} context
*/
export function VariableDeclarator(node, context) {
ensure_no_module_import_conflict(node, context.state);
if (context.state.analysis.runes) {
const init = node.init;
const rune = get_rune(init, context.state.scope);
const paths = extract_paths(node.id);
for (const path of paths) {
validate_identifier_name(context.state.scope.get(/** @type {Identifier} */ (path.node).name));
}
// TODO feels like this should happen during scope creation?
if (
rune === '$state' ||
rune === '$state.raw' ||
rune === '$derived' ||
rune === '$derived.by' ||
rune === '$props'
) {
for (const path of paths) {
// @ts-ignore this fails in CI for some insane reason
const binding = /** @type {Binding} */ (context.state.scope.get(path.node.name));
binding.kind =
rune === '$state'
? 'state'
: rune === '$state.raw'
? 'raw_state'
: rune === '$derived' || rune === '$derived.by'
? 'derived'
: path.is_rest
? 'rest_prop'
: 'prop';
}
}
if (rune === '$props') {
if (node.id.type !== 'ObjectPattern' && node.id.type !== 'Identifier') {
e.props_invalid_identifier(node);
}
context.state.analysis.needs_props = true;
if (node.id.type === 'Identifier') {
const binding = /** @type {Binding} */ (context.state.scope.get(node.id.name));
binding.initial = null; // else would be $props()
binding.kind = 'rest_prop';
} else {
equal(node.id.type, 'ObjectPattern');
for (const property of node.id.properties) {
if (property.type !== 'Property') continue;
if (property.computed) {
e.props_invalid_pattern(property);
}
if (property.key.type === 'Identifier' && property.key.name.startsWith('$$')) {
e.props_illegal_name(property);
}
const value =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
if (value.type !== 'Identifier') {
e.props_invalid_pattern(property);
}
const alias =
property.key.type === 'Identifier'
? property.key.name
: String(/** @type {Literal} */ (property.key).value);
let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null;
const binding = /** @type {Binding} */ (context.state.scope.get(value.name));
binding.prop_alias = alias;
// rewire initial from $props() to the actual initial value, stripping $bindable() if necessary
if (
initial?.type === 'CallExpression' &&
initial.callee.type === 'Identifier' &&
initial.callee.name === '$bindable'
) {
binding.initial = /** @type {Expression | null} */ (initial.arguments[0] ?? null);
binding.kind = 'bindable_prop';
} else {
binding.initial = initial;
}
}
}
}
} else {
if (node.init?.type === 'CallExpression') {
const callee = node.init.callee;
if (
callee.type === 'Identifier' &&
(callee.name === '$state' || callee.name === '$derived' || callee.name === '$props') &&
context.state.scope.get(callee.name)?.kind !== 'store_sub'
) {
e.rune_invalid_usage(node.init, callee.name);
}
}
}
context.next();
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,125 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../../types' */
import * as e from '../../../../errors.js';
import { is_text_attribute } from '../../../../utils/ast.js';
import * as w from '../../../../warnings.js';
import { is_custom_element_node } from '../../../nodes.js';
import { regex_only_whitespaces } from '../../../patterns.js';
/**
* @param {AST.Attribute} attribute
*/
export function validate_attribute_name(attribute) {
if (
attribute.name.includes(':') &&
!attribute.name.startsWith('xmlns:') &&
!attribute.name.startsWith('xlink:') &&
!attribute.name.startsWith('xml:')
) {
w.attribute_illegal_colon(attribute);
}
}
/**
* @param {AST.Attribute} attribute
* @param {AST.ElementLike} parent
*/
export function validate_attribute(attribute, parent) {
if (
Array.isArray(attribute.value) &&
attribute.value.length === 1 &&
attribute.value[0].type === 'ExpressionTag' &&
(parent.type === 'Component' ||
parent.type === 'SvelteComponent' ||
parent.type === 'SvelteSelf' ||
(parent.type === 'RegularElement' && is_custom_element_node(parent)))
) {
w.attribute_quoted(attribute);
}
if (attribute.value === true || !Array.isArray(attribute.value) || attribute.value.length === 1) {
return;
}
const is_quoted = attribute.value.at(-1)?.end !== attribute.end;
if (!is_quoted) {
e.attribute_unquoted_sequence(attribute);
}
}
/**
* @param {Context} context
* @param {AST.Attribute} attribute
* @param {boolean} is_component
*/
export function validate_slot_attribute(context, attribute, is_component = false) {
const parent = context.path.at(-2);
let owner = undefined;
if (parent?.type === 'SnippetBlock') {
if (!is_text_attribute(attribute)) {
e.slot_attribute_invalid(attribute);
}
return;
}
let i = context.path.length;
while (i--) {
const ancestor = context.path[i];
if (
!owner &&
(ancestor.type === 'Component' ||
ancestor.type === 'SvelteComponent' ||
ancestor.type === 'SvelteSelf' ||
ancestor.type === 'SvelteElement' ||
(ancestor.type === 'RegularElement' && is_custom_element_node(ancestor)))
) {
owner = ancestor;
}
}
if (owner) {
if (
owner.type === 'Component' ||
owner.type === 'SvelteComponent' ||
owner.type === 'SvelteSelf'
) {
if (owner !== parent) {
if (!is_component) {
e.slot_attribute_invalid_placement(attribute);
}
} else {
if (!is_text_attribute(attribute)) {
e.slot_attribute_invalid(attribute);
}
const name = attribute.value[0].data;
if (context.state.component_slots.has(name)) {
e.slot_attribute_duplicate(attribute, name, owner.name);
}
context.state.component_slots.add(name);
if (name === 'default') {
for (const node of owner.fragment.nodes) {
if (node.type === 'Text' && regex_only_whitespaces.test(node.data)) {
continue;
}
if (node.type === 'RegularElement' || node.type === 'SvelteFragment') {
if (node.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot')) {
continue;
}
}
e.slot_default_duplicate(node);
}
}
}
}
} else if (!is_component) {
e.slot_attribute_invalid_placement(attribute);
}
}

View File

@@ -0,0 +1,160 @@
/** @import { AST } from '#compiler' */
/** @import { AnalysisState, Context } from '../../types' */
import * as e from '../../../../errors.js';
import { get_attribute_expression, is_expression_attribute } from '../../../../utils/ast.js';
import { determine_slot } from '../../../../utils/slot.js';
import {
validate_attribute,
validate_attribute_name,
validate_slot_attribute
} from './attribute.js';
import { mark_subtree_dynamic } from './fragment.js';
import { is_resolved_snippet } from './snippets.js';
/**
* @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node
* @param {Context} context
*/
export function visit_component(node, context) {
node.metadata.path = [...context.path];
// link this node to all the snippets that it could render, so that we can prune CSS correctly
node.metadata.snippets = new Set();
// 'resolved' means we know which snippets this component might render. if it is `false`,
// then `node.metadata.snippets` is populated with every locally defined snippet
// once analysis is complete
let resolved = true;
for (const attribute of node.attributes) {
if (attribute.type === 'SpreadAttribute' || attribute.type === 'BindDirective') {
resolved = false;
continue;
}
if (attribute.type !== 'Attribute' || !is_expression_attribute(attribute)) {
continue;
}
const expression = get_attribute_expression(attribute);
// given an attribute like `foo={bar}`, if `bar` resolves to an import or a prop
// then we know it doesn't reference a locally defined snippet. if it resolves
// to a `{#snippet bar()}` then we know _which_ snippet it resolves to. in all
// other cases, we can't know (without much more complex static analysis) which
// snippets the component might render, so we treat the component as unresolved
if (expression.type === 'Identifier') {
const binding = context.state.scope.get(expression.name);
resolved &&= is_resolved_snippet(binding);
if (binding?.initial?.type === 'SnippetBlock') {
node.metadata.snippets.add(binding.initial);
}
} else if (expression.type !== 'Literal') {
resolved = false;
}
}
if (resolved) {
for (const child of node.fragment.nodes) {
if (child.type === 'SnippetBlock') {
node.metadata.snippets.add(child);
}
}
}
context.state.analysis.snippet_renderers.set(node, resolved);
mark_subtree_dynamic(context.path);
for (const attribute of node.attributes) {
if (
attribute.type !== 'Attribute' &&
attribute.type !== 'SpreadAttribute' &&
attribute.type !== 'LetDirective' &&
attribute.type !== 'OnDirective' &&
attribute.type !== 'BindDirective'
) {
e.component_invalid_directive(attribute);
}
if (
attribute.type === 'OnDirective' &&
(attribute.modifiers.length > 1 || attribute.modifiers.some((m) => m !== 'once'))
) {
e.event_handler_invalid_component_modifier(attribute);
}
if (attribute.type === 'Attribute') {
if (context.state.analysis.runes) {
validate_attribute(attribute, node);
if (is_expression_attribute(attribute)) {
const expression = get_attribute_expression(attribute);
if (expression.type === 'SequenceExpression') {
let i = /** @type {number} */ (expression.start);
while (--i > 0) {
const char = context.state.analysis.source[i];
if (char === '(') break; // parenthesized sequence expressions are ok
if (char === '{') e.attribute_invalid_sequence_expression(expression);
}
}
}
}
validate_attribute_name(attribute);
if (attribute.name === 'slot') {
validate_slot_attribute(context, attribute, true);
}
}
if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
context.state.analysis.uses_component_bindings = true;
}
}
// If the component has a slot attribute — `<Foo slot="whatever" .../>` —
// then `let:` directives apply to other attributes, instead of just the
// top-level contents of the component. Yes, this is very weird.
const default_state = determine_slot(node)
? context.state
: { ...context.state, scope: node.metadata.scopes.default };
for (const attribute of node.attributes) {
context.visit(attribute, attribute.type === 'LetDirective' ? default_state : context.state);
}
/** @type {AST.Comment[]} */
let comments = [];
/** @type {Record<string, AST.Fragment['nodes']>} */
const nodes = { default: [] };
for (const child of node.fragment.nodes) {
if (child.type === 'Comment') {
comments.push(child);
continue;
}
const slot_name = determine_slot(child) ?? 'default';
(nodes[slot_name] ??= []).push(...comments, child);
if (slot_name !== 'default') comments = [];
}
const component_slots = new Set();
for (const slot_name in nodes) {
/** @type {AnalysisState} */
const state = {
...context.state,
scope: node.metadata.scopes[slot_name],
parent_element: null,
component_slots
};
context.visit({ ...node.fragment, nodes: nodes[slot_name] }, state);
}
}

View File

@@ -0,0 +1,160 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../../types' */
import { get_attribute_expression, is_expression_attribute } from '../../../../utils/ast.js';
import { regex_illegal_attribute_character } from '../../../patterns.js';
import * as e from '../../../../errors.js';
import * as w from '../../../../warnings.js';
import {
validate_attribute,
validate_attribute_name,
validate_slot_attribute
} from './attribute.js';
const EVENT_MODIFIERS = [
'preventDefault',
'stopPropagation',
'stopImmediatePropagation',
'capture',
'once',
'passive',
'nonpassive',
'self',
'trusted'
];
/**
* @param {AST.RegularElement | AST.SvelteElement} node
* @param {Context} context
*/
export function validate_element(node, context) {
let has_animate_directive = false;
/** @type {AST.TransitionDirective | null} */
let in_transition = null;
/** @type {AST.TransitionDirective | null} */
let out_transition = null;
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute') {
const is_expression = is_expression_attribute(attribute);
if (context.state.analysis.runes) {
validate_attribute(attribute, node);
if (is_expression) {
const expression = get_attribute_expression(attribute);
if (expression.type === 'SequenceExpression') {
let i = /** @type {number} */ (expression.start);
while (--i > 0) {
const char = context.state.analysis.source[i];
if (char === '(') break; // parenthesized sequence expressions are ok
if (char === '{') e.attribute_invalid_sequence_expression(expression);
}
}
}
}
if (regex_illegal_attribute_character.test(attribute.name)) {
e.attribute_invalid_name(attribute, attribute.name);
}
if (attribute.name.startsWith('on') && attribute.name.length > 2) {
if (!is_expression) {
e.attribute_invalid_event_handler(attribute);
}
const value = get_attribute_expression(attribute);
if (
value.type === 'Identifier' &&
value.name === attribute.name &&
!context.state.scope.get(value.name)
) {
w.attribute_global_event_reference(attribute, attribute.name);
}
}
if (attribute.name === 'slot') {
/** @type {AST.RegularElement | AST.SvelteElement | AST.Component | AST.SvelteComponent | AST.SvelteSelf | undefined} */
validate_slot_attribute(context, attribute);
}
if (attribute.name === 'is') {
w.attribute_avoid_is(attribute);
}
const correct_name = react_attributes.get(attribute.name);
if (correct_name) {
w.attribute_invalid_property_name(attribute, attribute.name, correct_name);
}
validate_attribute_name(attribute);
} else if (attribute.type === 'AnimateDirective') {
const parent = context.path.at(-2);
if (parent?.type !== 'EachBlock') {
e.animation_invalid_placement(attribute);
} else if (!parent.key) {
e.animation_missing_key(attribute);
} else if (
parent.body.nodes.filter(
(n) =>
n.type !== 'Comment' &&
n.type !== 'ConstTag' &&
(n.type !== 'Text' || n.data.trim() !== '')
).length > 1
) {
e.animation_invalid_placement(attribute);
}
if (has_animate_directive) {
e.animation_duplicate(attribute);
} else {
has_animate_directive = true;
}
} else if (attribute.type === 'TransitionDirective') {
const existing = /** @type {AST.TransitionDirective | null} */ (
(attribute.intro && in_transition) || (attribute.outro && out_transition)
);
if (existing) {
const a = existing.intro ? (existing.outro ? 'transition' : 'in') : 'out';
const b = attribute.intro ? (attribute.outro ? 'transition' : 'in') : 'out';
if (a === b) {
e.transition_duplicate(attribute, a);
} else {
e.transition_conflict(attribute, a, b);
}
}
if (attribute.intro) in_transition = attribute;
if (attribute.outro) out_transition = attribute;
} else if (attribute.type === 'OnDirective') {
let has_passive_modifier = false;
let conflicting_passive_modifier = '';
for (const modifier of attribute.modifiers) {
if (!EVENT_MODIFIERS.includes(modifier)) {
const list = `${EVENT_MODIFIERS.slice(0, -1).join(', ')} or ${EVENT_MODIFIERS.at(-1)}`;
e.event_handler_invalid_modifier(attribute, list);
}
if (modifier === 'passive') {
has_passive_modifier = true;
} else if (modifier === 'nonpassive' || modifier === 'preventDefault') {
conflicting_passive_modifier = modifier;
}
if (has_passive_modifier && conflicting_passive_modifier) {
e.event_handler_invalid_modifier_combination(
attribute,
'passive',
conflicting_passive_modifier
);
}
}
}
}
}
const react_attributes = new Map([
['className', 'class'],
['htmlFor', 'for']
]);

View File

@@ -0,0 +1,15 @@
/** @import { AST } from '#compiler' */
/**
* @param {AST.SvelteNode[]} path
*/
export function mark_subtree_dynamic(path) {
let i = path.length;
while (i--) {
const node = path[i];
if (node.type === 'Fragment') {
if (node.metadata.dynamic) return;
node.metadata.dynamic = true;
}
}
}

View File

@@ -0,0 +1,21 @@
/** @import { ArrowFunctionExpression, FunctionDeclaration, FunctionExpression } from 'estree' */
/** @import { Context } from '../../types' */
/**
* @param {ArrowFunctionExpression | FunctionExpression | FunctionDeclaration} node
* @param {Context} context
*/
export function visit_function(node, context) {
// TODO retire this in favour of a more general solution based on bindings
node.metadata = {
hoisted: false,
hoisted_params: [],
scope: context.state.scope
};
context.next({
...context.state,
function_depth: context.state.function_depth + 1,
expression: null
});
}

View File

@@ -0,0 +1,17 @@
/** @import { Binding } from '#compiler' */
/**
* Returns `true` if a binding unambiguously resolves to a specific
* snippet declaration, or is external to the current component
* @param {Binding | null} binding
*/
export function is_resolved_snippet(binding) {
return (
!binding ||
binding.declaration_kind === 'import' ||
binding.kind === 'prop' ||
binding.kind === 'rest_prop' ||
binding.kind === 'bindable_prop' ||
binding?.initial?.type === 'SnippetBlock'
);
}

View File

@@ -0,0 +1,16 @@
/** @import { AST } from '#compiler' */
import * as e from '../../../../errors.js';
/**
* @param {AST.SvelteBody | AST.SvelteDocument | AST.SvelteOptionsRaw | AST.SvelteWindow} node
*/
export function disallow_children(node) {
const { nodes } = node.fragment;
if (nodes.length > 0) {
const first = nodes[0];
const last = nodes[nodes.length - 1];
e.svelte_meta_invalid_content({ start: first.start, end: last.end }, node.name);
}
}

View File

@@ -0,0 +1,284 @@
/** @import { AssignmentExpression, Expression, Literal, Node, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */
/** @import { AST, Binding } from '#compiler' */
/** @import { AnalysisState, Context } from '../../types' */
/** @import { Scope } from '../../../scope' */
/** @import { NodeLike } from '../../../../errors.js' */
import * as e from '../../../../errors.js';
import { extract_identifiers } from '../../../../utils/ast.js';
import * as w from '../../../../warnings.js';
import * as b from '../../../../utils/builders.js';
import { get_rune } from '../../../scope.js';
/**
* @param {AssignmentExpression | UpdateExpression} node
* @param {Pattern | Expression} argument
* @param {AnalysisState} state
*/
export function validate_assignment(node, argument, state) {
validate_no_const_assignment(node, argument, state.scope, false);
if (argument.type === 'Identifier') {
const binding = state.scope.get(argument.name);
if (state.analysis.runes) {
if (binding?.kind === 'derived') {
e.constant_assignment(node, 'derived state');
}
if (binding?.kind === 'each') {
e.each_item_invalid_assignment(node);
}
}
if (binding?.kind === 'snippet') {
e.snippet_parameter_assignment(node);
}
}
let object = /** @type {Expression | Super} */ (argument);
/** @type {Expression | PrivateIdentifier | null} */
let property = null;
while (object.type === 'MemberExpression') {
property = object.property;
object = object.object;
}
if (object.type === 'ThisExpression' && property?.type === 'PrivateIdentifier') {
if (state.private_derived_state.includes(property.name)) {
e.constant_assignment(node, 'derived state');
}
}
}
/**
* @param {NodeLike} node
* @param {Pattern | Expression} argument
* @param {Scope} scope
* @param {boolean} is_binding
*/
export function validate_no_const_assignment(node, argument, scope, is_binding) {
if (argument.type === 'ArrayPattern') {
for (const element of argument.elements) {
if (element) {
validate_no_const_assignment(node, element, scope, is_binding);
}
}
} else if (argument.type === 'ObjectPattern') {
for (const element of argument.properties) {
if (element.type === 'Property') {
validate_no_const_assignment(node, element.value, scope, is_binding);
}
}
} else if (argument.type === 'Identifier') {
const binding = scope.get(argument.name);
if (
binding?.kind === 'derived' ||
binding?.declaration_kind === 'import' ||
(binding?.declaration_kind === 'const' && binding.kind !== 'each')
) {
// e.invalid_const_assignment(
// node,
// is_binding,
// // This takes advantage of the fact that we don't assign initial for let directives and then/catch variables.
// // If we start doing that, we need another property on the binding to differentiate, or give up on the more precise error message.
// binding.kind !== 'state' &&
// binding.kind !== 'raw_state' &&
// (binding.kind !== 'normal' || !binding.initial)
// );
// TODO have a more specific error message for assignments to things like `{:then foo}`
const thing =
binding.declaration_kind === 'import'
? 'import'
: binding.kind === 'derived'
? 'derived state'
: 'constant';
if (is_binding) {
e.constant_binding(node, thing);
} else {
e.constant_assignment(node, thing);
}
}
}
}
/**
* Validates that the opening of a control flow block is `{` immediately followed by the expected character.
* In legacy mode whitespace is allowed inbetween. TODO remove once legacy mode is gone and move this into parser instead.
* @param {{start: number; end: number}} node
* @param {AnalysisState} state
* @param {string} expected
*/
export function validate_opening_tag(node, state, expected) {
if (state.analysis.source[node.start + 1] !== expected) {
// avoid a sea of red and only mark the first few characters
e.block_unexpected_character({ start: node.start, end: node.start + 5 }, expected);
}
}
/**
* @param {AST.Fragment | null | undefined} node
* @param {Context} context
*/
export function validate_block_not_empty(node, context) {
if (!node) return;
// Assumption: If the block has zero elements, someone's in the middle of typing it out,
// so don't warn in that case because it would be distracting.
if (node.nodes.length === 1 && node.nodes[0].type === 'Text' && !node.nodes[0].raw.trim()) {
w.block_empty(node.nodes[0]);
}
}
/**
* @param {VariableDeclarator} node
* @param {AnalysisState} state
*/
export function ensure_no_module_import_conflict(node, state) {
const ids = extract_identifiers(node.id);
for (const id of ids) {
if (
state.ast_type === 'instance' &&
state.scope === state.analysis.instance.scope &&
state.analysis.module.scope.get(id.name)?.declaration_kind === 'import'
) {
// TODO fix the message here
e.declaration_duplicate_module_import(node.id);
}
}
}
/**
* A 'safe' identifier means that the `foo` in `foo.bar` or `foo()` will not
* call functions that require component context to exist
* @param {Expression | Super} expression
* @param {Scope} scope
*/
export function is_safe_identifier(expression, scope) {
let node = expression;
while (node.type === 'MemberExpression') node = node.object;
if (node.type !== 'Identifier') return false;
const binding = scope.get(node.name);
if (!binding) return true;
if (binding.kind === 'store_sub') {
return is_safe_identifier({ name: node.name.slice(1), type: 'Identifier' }, scope);
}
return (
binding.declaration_kind !== 'import' &&
binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
binding.kind !== 'rest_prop'
);
}
/**
* @param {Expression | Literal | Super} node
* @param {Context} context
* @returns {boolean}
*/
export function is_pure(node, context) {
if (node.type === 'Literal') {
return true;
}
if (node.type === 'CallExpression') {
if (!is_pure(node.callee, context)) {
return false;
}
for (let arg of node.arguments) {
if (!is_pure(arg.type === 'SpreadElement' ? arg.argument : arg, context)) {
return false;
}
}
return true;
}
if (node.type !== 'Identifier' && node.type !== 'MemberExpression') {
return false;
}
if (get_rune(b.call(node), context.state.scope) === '$effect.tracking') {
return false;
}
/** @type {Expression | Super | null} */
let left = node;
while (left.type === 'MemberExpression') {
left = left.object;
}
if (!left) return false;
if (left.type === 'Identifier') {
const binding = context.state.scope.get(left.name);
if (binding === null) return true; // globals are assumed to be safe
} else if (is_pure(left, context)) {
return true;
}
// TODO add more cases (safe Svelte imports, etc)
return false;
}
/**
* Checks if the name is valid, which it is when it's not starting with (or is) a dollar sign or if it's a function parameter.
* The second argument is the depth of the scope, which is there for backwards compatibility reasons: In Svelte 4, you
* were allowed to define `$`-prefixed variables anywhere below the top level of components. Once legacy mode is gone, this
* argument can be removed / the call sites adjusted accordingly.
* @param {Binding | null} binding
* @param {number | undefined} [function_depth]
*/
export function validate_identifier_name(binding, function_depth) {
if (!binding) return;
const declaration_kind = binding.declaration_kind;
if (
declaration_kind !== 'synthetic' &&
declaration_kind !== 'param' &&
declaration_kind !== 'rest_param' &&
(!function_depth || function_depth <= 1)
) {
const node = binding.node;
if (node.name === '$') {
e.dollar_binding_invalid(node);
} else if (
node.name.startsWith('$') &&
// import type { $Type } from "" - these are normally already filtered out,
// but for the migration they aren't, and throwing here is preventing the migration to complete
// TODO -> once migration script is gone we can remove this check
!(
binding.initial?.type === 'ImportDeclaration' &&
/** @type {any} */ (binding.initial).importKind === 'type'
)
) {
e.dollar_prefix_invalid(node);
}
}
}
/**
* Checks that the exported name is not a derived or reassigned state variable.
* @param {Node} node
* @param {Scope} scope
* @param {string} name
*/
export function validate_export(node, scope, name) {
const binding = scope.get(name);
if (!binding) return;
if (binding.kind === 'derived') {
e.derived_invalid_export(node);
}
if ((binding.kind === 'state' || binding.kind === 'raw_state') && binding.reassigned) {
e.state_invalid_export(node);
}
}

View File

@@ -0,0 +1,682 @@
/** @import * as ESTree from 'estree' */
/** @import { AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { ComponentAnalysis, Analysis } from '../../types' */
/** @import { Visitors, ComponentClientTransformState, ClientTransformState } from './types' */
import { walk } from 'zimmerframe';
import * as b from '../../../utils/builders.js';
import { build_getter, is_state_source } from './utils.js';
import { render_stylesheet } from '../css/index.js';
import { dev, filename } from '../../../state.js';
import { AnimateDirective } from './visitors/AnimateDirective.js';
import { ArrowFunctionExpression } from './visitors/ArrowFunctionExpression.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.js';
import { Attribute } from './visitors/Attribute.js';
import { AwaitBlock } from './visitors/AwaitBlock.js';
import { BinaryExpression } from './visitors/BinaryExpression.js';
import { BindDirective } from './visitors/BindDirective.js';
import { BlockStatement } from './visitors/BlockStatement.js';
import { BreakStatement } from './visitors/BreakStatement.js';
import { CallExpression } from './visitors/CallExpression.js';
import { ClassBody } from './visitors/ClassBody.js';
import { Comment } from './visitors/Comment.js';
import { Component } from './visitors/Component.js';
import { ConstTag } from './visitors/ConstTag.js';
import { DebugTag } from './visitors/DebugTag.js';
import { EachBlock } from './visitors/EachBlock.js';
import { ExportNamedDeclaration } from './visitors/ExportNamedDeclaration.js';
import { ExpressionStatement } from './visitors/ExpressionStatement.js';
import { Fragment } from './visitors/Fragment.js';
import { FunctionDeclaration } from './visitors/FunctionDeclaration.js';
import { FunctionExpression } from './visitors/FunctionExpression.js';
import { HtmlTag } from './visitors/HtmlTag.js';
import { Identifier } from './visitors/Identifier.js';
import { IfBlock } from './visitors/IfBlock.js';
import { ImportDeclaration } from './visitors/ImportDeclaration.js';
import { KeyBlock } from './visitors/KeyBlock.js';
import { LabeledStatement } from './visitors/LabeledStatement.js';
import { LetDirective } from './visitors/LetDirective.js';
import { MemberExpression } from './visitors/MemberExpression.js';
import { OnDirective } from './visitors/OnDirective.js';
import { Program } from './visitors/Program.js';
import { RegularElement } from './visitors/RegularElement.js';
import { RenderTag } from './visitors/RenderTag.js';
import { SlotElement } from './visitors/SlotElement.js';
import { SnippetBlock } from './visitors/SnippetBlock.js';
import { SpreadAttribute } from './visitors/SpreadAttribute.js';
import { SvelteBody } from './visitors/SvelteBody.js';
import { SvelteComponent } from './visitors/SvelteComponent.js';
import { SvelteDocument } from './visitors/SvelteDocument.js';
import { SvelteElement } from './visitors/SvelteElement.js';
import { SvelteFragment } from './visitors/SvelteFragment.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
import { SvelteHead } from './visitors/SvelteHead.js';
import { SvelteSelf } from './visitors/SvelteSelf.js';
import { SvelteWindow } from './visitors/SvelteWindow.js';
import { TitleElement } from './visitors/TitleElement.js';
import { TransitionDirective } from './visitors/TransitionDirective.js';
import { UpdateExpression } from './visitors/UpdateExpression.js';
import { UseDirective } from './visitors/UseDirective.js';
import { VariableDeclaration } from './visitors/VariableDeclaration.js';
/** @type {Visitors} */
const visitors = {
_: function set_scope(node, { next, state }) {
const scope = state.scopes.get(node);
if (scope && scope !== state.scope) {
const transform = { ...state.transform };
for (const [name, binding] of scope.declarations) {
if (
binding.kind === 'normal' ||
// Reads of `$state(...)` declarations are not
// transformed if they are never reassigned
(binding.kind === 'state' && !is_state_source(binding, state.analysis))
) {
delete transform[name];
}
}
next({ ...state, transform, scope });
} else {
next();
}
},
AnimateDirective,
ArrowFunctionExpression,
AssignmentExpression,
Attribute,
AwaitBlock,
BinaryExpression,
BindDirective,
BlockStatement,
BreakStatement,
CallExpression,
ClassBody,
Comment,
Component,
ConstTag,
DebugTag,
EachBlock,
ExportNamedDeclaration,
ExpressionStatement,
Fragment,
FunctionDeclaration,
FunctionExpression,
HtmlTag,
Identifier,
IfBlock,
ImportDeclaration,
KeyBlock,
LabeledStatement,
LetDirective,
MemberExpression,
OnDirective,
Program,
RegularElement,
RenderTag,
SlotElement,
SnippetBlock,
SpreadAttribute,
SvelteBody,
SvelteComponent,
SvelteDocument,
SvelteElement,
SvelteFragment,
SvelteBoundary,
SvelteHead,
SvelteSelf,
SvelteWindow,
TitleElement,
TransitionDirective,
UpdateExpression,
UseDirective,
VariableDeclaration
};
/**
* @param {ComponentAnalysis} analysis
* @param {ValidatedCompileOptions} options
* @returns {ESTree.Program}
*/
export function client_component(analysis, options) {
/** @type {ComponentClientTransformState} */
const state = {
analysis,
options,
scope: analysis.module.scope,
scopes: analysis.module.scopes,
is_instance: false,
hoisted: [b.import_all('$', 'svelte/internal/client')],
node: /** @type {any} */ (null), // populated by the root node
legacy_reactive_imports: [],
legacy_reactive_statements: new Map(),
metadata: {
context: {
template_needs_import_node: false,
template_contains_script_tag: false
},
namespace: options.namespace,
bound_contenteditable: false
},
events: new Set(),
preserve_whitespace: options.preserveWhitespace,
public_state: new Map(),
private_state: new Map(),
transform: {},
in_constructor: false,
instance_level_snippets: [],
module_level_snippets: [],
// these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null),
update: /** @type {any} */ (null),
expressions: /** @type {any} */ (null),
after_update: /** @type {any} */ (null),
template: /** @type {any} */ (null),
locations: /** @type {any} */ (null)
};
const module = /** @type {ESTree.Program} */ (
walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, visitors)
);
const instance_state = {
...state,
transform: { ...state.transform },
scope: analysis.instance.scope,
scopes: analysis.instance.scopes,
is_instance: true
};
const instance = /** @type {ESTree.Program} */ (
walk(/** @type {AST.SvelteNode} */ (analysis.instance.ast), instance_state, visitors)
);
const template = /** @type {ESTree.Program} */ (
walk(
/** @type {AST.SvelteNode} */ (analysis.template.ast),
{
...state,
transform: instance_state.transform,
scope: analysis.instance.scope,
scopes: analysis.template.scopes
},
visitors
)
);
module.body.unshift(...state.legacy_reactive_imports);
/** @type {ESTree.Statement[]} */
const store_setup = [];
/** @type {ESTree.VariableDeclaration[]} */
const legacy_reactive_declarations = [];
let needs_store_cleanup = false;
for (const [name, binding] of analysis.instance.scope.declarations) {
if (binding.kind === 'legacy_reactive') {
legacy_reactive_declarations.push(
b.const(name, b.call('$.mutable_state', undefined, analysis.immutable ? b.true : undefined))
);
}
if (binding.kind === 'store_sub') {
if (store_setup.length === 0) {
needs_store_cleanup = true;
store_setup.push(
b.const(b.array_pattern([b.id('$$stores'), b.id('$$cleanup')]), b.call('$.setup_stores'))
);
}
// We're creating an arrow function that gets the store value which minifies better for two or more references
const store_reference = build_getter(b.id(name.slice(1)), instance_state);
const store_get = b.call('$.store_get', store_reference, b.literal(name), b.id('$$stores'));
store_setup.push(
b.const(
binding.node,
dev
? b.thunk(
b.sequence([
b.call('$.validate_store', store_reference, b.literal(name.slice(1))),
store_get
])
)
: b.thunk(store_get)
)
);
}
}
for (const [node] of analysis.reactive_statements) {
const statement = [...state.legacy_reactive_statements].find(([n]) => n === node);
if (statement === undefined) {
throw new Error('Could not find reactive statement');
}
instance.body.push(statement[1]);
}
if (analysis.reactive_statements.size > 0) {
instance.body.push(b.stmt(b.call('$.legacy_pre_effect_reset')));
}
/**
* Used to store the group nodes
* @type {ESTree.VariableDeclaration[]}
*/
const group_binding_declarations = [];
for (const group of analysis.binding_groups.values()) {
group_binding_declarations.push(b.const(group.name, b.array([])));
}
/** @type {Array<ESTree.Property | ESTree.SpreadElement>} */
const component_returned_object = analysis.exports.flatMap(({ name, alias }) => {
const binding = instance_state.scope.get(name);
const expression = build_getter(b.id(name), instance_state);
const getter = b.get(alias ?? name, [b.return(expression)]);
if (expression.type === 'Identifier') {
if (binding?.declaration_kind === 'let' || binding?.declaration_kind === 'var') {
return [
getter,
b.set(alias ?? name, [b.stmt(b.assignment('=', expression, b.id('$$value')))])
];
} else if (!dev) {
return b.init(alias ?? name, expression);
}
}
if (binding?.kind === 'prop' || binding?.kind === 'bindable_prop') {
return [getter, b.set(alias ?? name, [b.stmt(b.call(name, b.id('$$value')))])];
}
if (binding?.kind === 'state' || binding?.kind === 'raw_state') {
const value = binding.kind === 'state' ? b.call('$.proxy', b.id('$$value')) : b.id('$$value');
return [getter, b.set(alias ?? name, [b.stmt(b.call('$.set', b.id(name), value))])];
}
return getter;
});
const properties = [...analysis.instance.scope.declarations].filter(
([name, binding]) =>
(binding.kind === 'prop' || binding.kind === 'bindable_prop') && !name.startsWith('$$')
);
if (analysis.accessors) {
for (const [name, binding] of properties) {
const key = binding.prop_alias ?? name;
const getter = b.get(key, [b.return(b.call(b.id(name)))]);
const setter = b.set(key, [
b.stmt(b.call(b.id(name), b.id('$$value'))),
b.stmt(b.call('$.flush_sync'))
]);
if (analysis.runes && binding.initial) {
// turn `set foo($$value)` into `set foo($$value = expression)`
setter.value.params[0] = {
type: 'AssignmentPattern',
left: b.id('$$value'),
right: /** @type {ESTree.Expression} */ (binding.initial)
};
}
component_returned_object.push(getter, setter);
}
}
if (options.compatibility.componentApi === 4) {
component_returned_object.push(
b.init('$set', b.id('$.update_legacy_props')),
b.init(
'$on',
b.arrow(
[b.id('$$event_name'), b.id('$$event_cb')],
b.call(
'$.add_legacy_event_listener',
b.id('$$props'),
b.id('$$event_name'),
b.id('$$event_cb')
)
)
)
);
} else if (dev) {
component_returned_object.push(b.spread(b.call(b.id('$.legacy_api'))));
}
const push_args = [b.id('$$props'), b.literal(analysis.runes)];
if (dev) push_args.push(b.id(analysis.name));
const component_block = b.block([
...store_setup,
...legacy_reactive_declarations,
...group_binding_declarations,
...state.instance_level_snippets,
.../** @type {ESTree.Statement[]} */ (instance.body),
analysis.runes || !analysis.needs_context
? b.empty
: b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)),
.../** @type {ESTree.Statement[]} */ (template.body)
]);
if (!analysis.runes) {
// Bind static exports to props so that people can access them with bind:x
for (const { name, alias } of analysis.exports) {
component_block.body.push(
b.stmt(
b.call(
'$.bind_prop',
b.id('$$props'),
b.literal(alias ?? name),
build_getter(b.id(name), instance_state)
)
)
);
}
}
if (analysis.css.ast !== null && analysis.inject_styles) {
const hash = b.literal(analysis.css.hash);
const code = b.literal(render_stylesheet(analysis.source, analysis, options).code);
state.hoisted.push(b.const('$$css', b.object([b.init('hash', hash), b.init('code', code)])));
component_block.body.unshift(
b.stmt(b.call('$.append_styles', b.id('$$anchor'), b.id('$$css')))
);
}
const should_inject_context =
dev ||
analysis.needs_context ||
analysis.reactive_statements.size > 0 ||
component_returned_object.length > 0;
// we want the cleanup function for the stores to run as the very last thing
// so that it can effectively clean up the store subscription even after the user effects runs
if (should_inject_context) {
component_block.body.unshift(b.stmt(b.call('$.push', ...push_args)));
let to_push;
if (component_returned_object.length > 0) {
let pop_call = b.call('$.pop', b.object(component_returned_object));
to_push = needs_store_cleanup ? b.var('$$pop', pop_call) : b.return(pop_call);
} else {
to_push = b.stmt(b.call('$.pop'));
}
component_block.body.push(to_push);
}
if (needs_store_cleanup) {
component_block.body.push(b.stmt(b.call('$$cleanup')));
if (component_returned_object.length > 0) {
component_block.body.push(b.return(b.id('$$pop')));
}
}
if (analysis.uses_rest_props) {
const named_props = analysis.exports.map(({ name, alias }) => alias ?? name);
for (const [name, binding] of analysis.instance.scope.declarations) {
if (binding.kind === 'bindable_prop') named_props.push(binding.prop_alias ?? name);
}
component_block.body.unshift(
b.const(
'$$restProps',
b.call(
'$.legacy_rest_props',
b.id('$$sanitized_props'),
b.array(named_props.map((name) => b.literal(name)))
)
)
);
}
if (analysis.uses_props || analysis.uses_rest_props) {
const to_remove = [
b.literal('children'),
b.literal('$$slots'),
b.literal('$$events'),
b.literal('$$legacy')
];
if (analysis.custom_element) {
to_remove.push(b.literal('$$host'));
}
component_block.body.unshift(
b.const(
'$$sanitized_props',
b.call('$.legacy_rest_props', b.id('$$props'), b.array(to_remove))
)
);
}
if (analysis.uses_slots) {
component_block.body.unshift(b.const('$$slots', b.call('$.sanitize_slots', b.id('$$props'))));
}
let should_inject_props =
should_inject_context ||
analysis.needs_props ||
analysis.uses_props ||
analysis.uses_rest_props ||
analysis.uses_slots ||
analysis.slot_names.size > 0;
// Merge hoisted statements into module body.
// Ensure imports are on top, with the order preserved, then module body, then hoisted statements
/** @type {ESTree.ImportDeclaration[]} */
const imports = [];
/** @type {ESTree.Program['body']} */
let body = [];
for (const entry of [...module.body, ...state.hoisted]) {
if (entry.type === 'ImportDeclaration') {
imports.push(entry);
} else {
body.push(entry);
}
}
body = [...imports, ...state.module_level_snippets, ...body];
const component = b.function_declaration(
b.id(analysis.name),
should_inject_props ? [b.id('$$anchor'), b.id('$$props')] : [b.id('$$anchor')],
component_block
);
if (options.hmr) {
const id = b.id(analysis.name);
const HMR = b.id('$.HMR');
const existing = b.member(id, HMR, true);
const incoming = b.member(b.id('module.default'), HMR, true);
const accept_fn_body = [
b.stmt(b.assignment('=', b.member(incoming, 'source'), b.member(existing, 'source'))),
b.stmt(b.call('$.set', b.member(existing, 'source'), b.member(incoming, 'original')))
];
if (analysis.css.hash) {
// remove existing `<style>` element, in case CSS changed
accept_fn_body.unshift(b.stmt(b.call('$.cleanup_styles', b.literal(analysis.css.hash))));
}
const hmr = b.block([
b.stmt(b.assignment('=', id, b.call('$.hmr', id, b.thunk(b.member(existing, 'source'))))),
b.stmt(b.call('import.meta.hot.accept', b.arrow([b.id('module')], b.block(accept_fn_body))))
]);
body.push(component, b.if(b.id('import.meta.hot'), hmr), b.export_default(b.id(analysis.name)));
} else {
body.push(b.export_default(component));
}
if (dev) {
// add `App[$.FILENAME] = 'App.svelte'` so that we can print useful messages later
body.unshift(
b.stmt(
b.assignment('=', b.member(b.id(analysis.name), '$.FILENAME', true), b.literal(filename))
)
);
body.unshift(b.stmt(b.call(b.id('$.mark_module_start'))));
body.push(b.stmt(b.call(b.id('$.mark_module_end'), b.id(analysis.name))));
}
if (!analysis.runes) {
body.unshift(b.imports([], 'svelte/internal/flags/legacy'));
}
if (analysis.tracing) {
body.unshift(b.imports([], 'svelte/internal/flags/tracing'));
}
if (options.discloseVersion) {
body.unshift(b.imports([], 'svelte/internal/disclose-version'));
}
if (options.compatibility.componentApi === 4) {
body.unshift(b.imports([['createClassComponent', '$$_createClassComponent']], 'svelte/legacy'));
component_block.body.unshift(
b.if(
b.id('new.target'),
b.return(
b.call(
'$$_createClassComponent',
// When called with new, the first argument is the constructor options
b.object([b.init('component', b.id(analysis.name)), b.spread(b.id('$$anchor'))])
)
)
)
);
} else if (dev) {
component_block.body.unshift(b.stmt(b.call('$.check_target', b.id('new.target'))));
}
if (state.events.size > 0) {
body.push(
b.stmt(b.call('$.delegate', b.array(Array.from(state.events).map((name) => b.literal(name)))))
);
}
if (analysis.custom_element) {
const ce = analysis.custom_element;
const ce_props = typeof ce === 'boolean' ? {} : ce.props || {};
/** @type {ESTree.Property[]} */
const props_str = [];
for (const [name, prop_def] of Object.entries(ce_props)) {
const binding = analysis.instance.scope.get(name);
const key = binding?.prop_alias ?? name;
if (
!prop_def.type &&
binding?.initial?.type === 'Literal' &&
typeof binding?.initial.value === 'boolean'
) {
prop_def.type = 'Boolean';
}
const value = b.object(
/** @type {ESTree.Property[]} */ (
[
prop_def.attribute ? b.init('attribute', b.literal(prop_def.attribute)) : undefined,
prop_def.reflect ? b.init('reflect', b.literal(true)) : undefined,
prop_def.type ? b.init('type', b.literal(prop_def.type)) : undefined
].filter(Boolean)
)
);
props_str.push(b.init(key, value));
}
for (const [name, binding] of properties) {
const key = binding.prop_alias ?? name;
if (ce_props[key]) continue;
props_str.push(b.init(key, b.object([])));
}
const slots_str = b.array([...analysis.slot_names.keys()].map((name) => b.literal(name)));
const accessors_str = b.array(
analysis.exports.map(({ name, alias }) => b.literal(alias ?? name))
);
const use_shadow_dom = typeof ce === 'boolean' || ce.shadow !== 'none' ? true : false;
const create_ce = b.call(
'$.create_custom_element',
b.id(analysis.name),
b.object(props_str),
slots_str,
accessors_str,
b.literal(use_shadow_dom),
/** @type {any} */ (typeof ce !== 'boolean' ? ce.extend : undefined)
);
// If a tag name is provided, call `customElements.define`, otherwise leave to the user
if (typeof ce !== 'boolean' && typeof ce.tag === 'string') {
const define = b.stmt(b.call('customElements.define', b.literal(ce.tag), create_ce));
if (options.hmr) {
body.push(
b.if(b.binary('==', b.call('customElements.get', b.literal(ce.tag)), b.null), define)
);
} else {
body.push(define);
}
} else {
body.push(b.stmt(create_ce));
}
}
return {
type: 'Program',
sourceType: 'module',
body
};
}
/**
* @param {Analysis} analysis
* @param {ValidatedModuleCompileOptions} options
* @returns {ESTree.Program}
*/
export function client_module(analysis, options) {
/** @type {ClientTransformState} */
const state = {
analysis,
options,
scope: analysis.module.scope,
scopes: analysis.module.scopes,
public_state: new Map(),
private_state: new Map(),
transform: {},
in_constructor: false
};
const module = /** @type {ESTree.Program} */ (
walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, visitors)
);
const body = [b.import_all('$', 'svelte/internal/client')];
if (analysis.tracing) {
body.push(b.imports([], 'svelte/internal/flags/tracing'));
}
return {
type: 'Program',
sourceType: 'module',
body: [...body, ...module.body]
};
}

View File

@@ -0,0 +1,111 @@
import type {
ModuleDeclaration,
Statement,
LabeledStatement,
Identifier,
PrivateIdentifier,
Expression,
AssignmentExpression,
UpdateExpression,
VariableDeclaration
} from 'estree';
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js';
import type { SourceLocation } from '#shared';
export interface ClientTransformState extends TransformState {
readonly private_state: Map<string, StateField>;
readonly public_state: Map<string, StateField>;
/**
* `true` if the current lexical scope belongs to a class constructor. this allows
* us to rewrite `this.foo` as `this.#foo.value`
*/
readonly in_constructor: boolean;
readonly transform: Record<
string,
{
/** turn `foo` into e.g. `$.get(foo)` */
read: (id: Identifier) => Expression;
/** turn `foo = bar` into e.g. `$.set(foo, bar)` */
assign?: (node: Identifier, value: Expression) => Expression;
/** turn `foo.bar = baz` into e.g. `$.mutate(foo, $.get(foo).bar = baz);` */
mutate?: (node: Identifier, mutation: AssignmentExpression | UpdateExpression) => Expression;
/** turn `foo++` into e.g. `$.update(foo)` */
update?: (node: UpdateExpression) => Expression;
}
>;
}
export interface ComponentClientTransformState extends ClientTransformState {
readonly analysis: ComponentAnalysis;
readonly options: ValidatedCompileOptions;
readonly hoisted: Array<Statement | ModuleDeclaration>;
readonly events: Set<string>;
readonly is_instance: boolean;
/** Stuff that happens before the render effect(s) */
readonly init: Statement[];
/** Stuff that happens inside the render effect */
readonly update: Statement[];
/** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */
readonly after_update: Statement[];
/** Expressions used inside the render effect */
readonly expressions: Expression[];
/** The HTML template string */
readonly template: Array<string | Expression>;
readonly locations: SourceLocation[];
readonly metadata: {
namespace: Namespace;
bound_contenteditable: boolean;
/**
* Stuff that is set within the children of one `Fragment` visitor that is relevant
* to said fragment. Shouldn't be destructured or otherwise spread unless inside the
* `Fragment` visitor to keep the object reference intact (it's also nested
* within `metadata` for this reason).
*/
context: {
/** `true` if the HTML template needs to be instantiated with `importNode` */
template_needs_import_node: boolean;
/**
* `true` if HTML template contains a `<script>` tag. In this case we need to invoke a special
* template instantiation function (see `create_fragment_with_script_from_html` for more info)
*/
template_contains_script_tag: boolean;
};
};
readonly preserve_whitespace: boolean;
/** The anchor node for the current context */
readonly node: Identifier;
/** Imports that should be re-evaluated in legacy mode following a mutation */
readonly legacy_reactive_imports: Statement[];
/** The $: calls, which will be ordered in the end */
readonly legacy_reactive_statements: Map<LabeledStatement, Statement>;
/** Snippets hoisted to the instance */
readonly instance_level_snippets: VariableDeclaration[];
/** Snippets hoisted to the module */
readonly module_level_snippets: VariableDeclaration[];
}
export interface StateField {
kind: 'state' | 'raw_state' | 'derived' | 'derived_by';
id: PrivateIdentifier;
}
export type Context = import('zimmerframe').Context<AST.SvelteNode, ClientTransformState>;
export type Visitors = import('zimmerframe').Visitors<AST.SvelteNode, any>;
export type ComponentContext = import('zimmerframe').Context<
AST.SvelteNode,
ComponentClientTransformState
>;
export type ComponentVisitors = import('zimmerframe').Visitors<
AST.SvelteNode,
ComponentClientTransformState
>;

View File

@@ -0,0 +1,279 @@
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement } from 'estree' */
/** @import { AST, Binding } from '#compiler' */
/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */
/** @import { Analysis } from '../../types.js' */
/** @import { Scope } from '../../scope.js' */
import * as b from '../../../utils/builders.js';
import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js';
import {
PROPS_IS_LAZY_INITIAL,
PROPS_IS_IMMUTABLE,
PROPS_IS_RUNES,
PROPS_IS_UPDATED,
PROPS_IS_BINDABLE
} from '../../../../constants.js';
import { dev } from '../../../state.js';
import { get_value } from './visitors/shared/declarations.js';
/**
* @param {Binding} binding
* @param {Analysis} analysis
* @returns {boolean}
*/
export function is_state_source(binding, analysis) {
return (
(binding.kind === 'state' || binding.kind === 'raw_state') &&
(!analysis.immutable || binding.reassigned || analysis.accessors)
);
}
/**
* @param {Identifier} node
* @param {ClientTransformState} state
* @returns {Expression}
*/
export function build_getter(node, state) {
if (Object.hasOwn(state.transform, node.name)) {
const binding = state.scope.get(node.name);
// don't transform the declaration itself
if (node !== binding?.node) {
return state.transform[node.name].read(node);
}
}
return node;
}
/**
* @param {Expression} value
* @param {Expression} previous
*/
export function build_proxy_reassignment(value, previous) {
return dev ? b.call('$.proxy', value, b.null, previous) : b.call('$.proxy', value);
}
/**
* @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node
* @param {ComponentContext} context
* @returns {Pattern[]}
*/
function get_hoisted_params(node, context) {
const scope = context.state.scope;
/** @type {Identifier[]} */
const params = [];
/**
* We only want to push if it's not already present to avoid name clashing
* @param {Identifier} id
*/
function push_unique(id) {
if (!params.find((param) => param.name === id.name)) {
params.push(id);
}
}
for (const [reference] of scope.references) {
let binding = scope.get(reference);
if (binding !== null && !scope.declarations.has(reference) && binding.initial !== node) {
if (binding.kind === 'store_sub') {
// We need both the subscription for getting the value and the store for updating
push_unique(b.id(binding.node.name));
binding = /** @type {Binding} */ (scope.get(binding.node.name.slice(1)));
}
let expression = context.state.transform[reference]?.read(b.id(binding.node.name));
if (
// If it's a destructured derived binding, then we can extract the derived signal reference and use that.
// TODO this code is bad, we need to kill it
expression != null &&
typeof expression !== 'function' &&
expression.type === 'MemberExpression' &&
expression.object.type === 'CallExpression' &&
expression.object.callee.type === 'Identifier' &&
expression.object.callee.name === '$.get' &&
expression.object.arguments[0].type === 'Identifier'
) {
push_unique(b.id(expression.object.arguments[0].name));
} else if (
// If we are referencing a simple $$props value, then we need to reference the object property instead
(binding.kind === 'prop' || binding.kind === 'bindable_prop') &&
!is_prop_source(binding, context.state)
) {
push_unique(b.id('$$props'));
} else if (
// imports don't need to be hoisted
binding.declaration_kind !== 'import'
) {
// create a copy to remove start/end tags which would mess up source maps
push_unique(b.id(binding.node.name));
// rest props are often accessed through the $$props object for optimization reasons,
// but we can't know if the delegated event handler will use it, so we need to add both as params
if (binding.kind === 'rest_prop' && context.state.analysis.runes) {
push_unique(b.id('$$props'));
}
}
}
}
return params;
}
/**
* @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node
* @param {ComponentContext} context
* @returns {Pattern[]}
*/
export function build_hoisted_params(node, context) {
const hoisted_params = get_hoisted_params(node, context);
node.metadata.hoisted_params = hoisted_params;
/** @type {Pattern[]} */
const params = [];
if (node.params.length === 0) {
if (hoisted_params.length > 0) {
// For the event object
params.push(b.id(context.state.scope.generate('_')));
}
} else {
for (const param of node.params) {
params.push(/** @type {Pattern} */ (context.visit(param)));
}
}
params.push(...hoisted_params);
return params;
}
/**
* @param {Binding} binding
* @param {ComponentClientTransformState} state
* @param {string} name
* @param {Expression | null} [initial]
* @returns
*/
export function get_prop_source(binding, state, name, initial) {
/** @type {Expression[]} */
const args = [b.id('$$props'), b.literal(name)];
let flags = 0;
if (binding.kind === 'bindable_prop') {
flags |= PROPS_IS_BINDABLE;
}
if (state.analysis.immutable) {
flags |= PROPS_IS_IMMUTABLE;
}
if (state.analysis.runes) {
flags |= PROPS_IS_RUNES;
}
if (
state.analysis.accessors ||
(state.analysis.immutable
? binding.reassigned || (state.analysis.runes && binding.mutated)
: binding.updated)
) {
flags |= PROPS_IS_UPDATED;
}
/** @type {Expression | undefined} */
let arg;
if (initial) {
// To avoid eagerly evaluating the right-hand-side, we wrap it in a thunk if necessary
if (is_simple_expression(initial)) {
arg = initial;
} else {
if (
initial.type === 'CallExpression' &&
initial.callee.type === 'Identifier' &&
initial.arguments.length === 0
) {
arg = initial.callee;
} else {
arg = b.thunk(initial);
}
flags |= PROPS_IS_LAZY_INITIAL;
}
}
if (flags || arg) {
args.push(b.literal(flags));
if (arg) args.push(arg);
}
return b.call('$.prop', ...args);
}
/**
*
* @param {Binding} binding
* @param {ClientTransformState} state
* @returns
*/
export function is_prop_source(binding, state) {
return (
(binding.kind === 'prop' || binding.kind === 'bindable_prop') &&
(!state.analysis.runes ||
state.analysis.accessors ||
binding.reassigned ||
binding.initial ||
// Until legacy mode is gone, we also need to use the prop source when only mutated is true,
// because the parent could be a legacy component which needs coarse-grained reactivity
binding.updated)
);
}
/**
* @param {Expression} node
* @param {Scope | null} scope
*/
export function should_proxy(node, scope) {
if (
!node ||
node.type === 'Literal' ||
node.type === 'TemplateLiteral' ||
node.type === 'ArrowFunctionExpression' ||
node.type === 'FunctionExpression' ||
node.type === 'UnaryExpression' ||
node.type === 'BinaryExpression' ||
(node.type === 'Identifier' && node.name === 'undefined')
) {
return false;
}
if (node.type === 'Identifier' && scope !== null) {
const binding = scope.get(node.name);
// Let's see if the reference is something that can be proxied
if (
binding !== null &&
!binding.reassigned &&
binding.initial !== null &&
binding.initial.type !== 'FunctionDeclaration' &&
binding.initial.type !== 'ClassDeclaration' &&
binding.initial.type !== 'ImportDeclaration' &&
binding.initial.type !== 'EachBlock' &&
binding.initial.type !== 'SnippetBlock'
) {
return should_proxy(binding.initial, null);
}
}
return true;
}
/**
* Svelte legacy mode should use safe equals in most places, runes mode shouldn't
* @param {ComponentClientTransformState} state
* @param {Expression} arg
*/
export function create_derived(state, arg) {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg);
}

View File

@@ -0,0 +1,28 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
import { parse_directive_name } from './shared/utils.js';
/**
* @param {AST.AnimateDirective} node
* @param {ComponentContext} context
*/
export function AnimateDirective(node, context) {
const expression =
node.expression === null
? b.literal(null)
: b.thunk(/** @type {Expression} */ (context.visit(node.expression)));
// in after_update to ensure it always happens after bind:this
context.state.after_update.push(
b.stmt(
b.call(
'$.animation',
context.state.node,
b.thunk(/** @type {Expression} */ (context.visit(parse_directive_name(node.name)))),
expression
)
)
);
}

View File

@@ -0,0 +1,11 @@
/** @import { ArrowFunctionExpression } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { visit_function } from './shared/function.js';
/**
* @param {ArrowFunctionExpression} node
* @param {ComponentContext} context
*/
export function ArrowFunctionExpression(node, context) {
return visit_function(node, context);
}

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