feat(frontend): scaffold SvelteKit with TS 7.0 (native-preview) and tsgo
This commit is contained in:
+78
@@ -0,0 +1,78 @@
|
||||
/** @import { FlipParams, AnimationConfig } from './public.js' */
|
||||
import { cubicOut } from '../easing/index.js';
|
||||
|
||||
/**
|
||||
* The flip function calculates the start and end position of an element and animates between them, translating the x and y values.
|
||||
* `flip` stands for [First, Last, Invert, Play](https://aerotwist.com/blog/flip-your-animations/).
|
||||
*
|
||||
* @param {Element} node
|
||||
* @param {{ from: DOMRect; to: DOMRect }} fromTo
|
||||
* @param {FlipParams} params
|
||||
* @returns {AnimationConfig}
|
||||
*/
|
||||
export function flip(node, { from, to }, params = {}) {
|
||||
var { delay = 0, duration = (d) => Math.sqrt(d) * 120, easing = cubicOut } = params;
|
||||
|
||||
var style = getComputedStyle(node);
|
||||
|
||||
// find the transform origin, expressed as a pair of values between 0 and 1
|
||||
var transform = style.transform === 'none' ? '' : style.transform;
|
||||
var [ox, oy] = style.transformOrigin.split(' ').map(parseFloat);
|
||||
ox /= node.clientWidth;
|
||||
oy /= node.clientHeight;
|
||||
|
||||
// calculate effect of parent transforms and zoom
|
||||
var zoom = get_zoom(node); // https://drafts.csswg.org/css-viewport/#effective-zoom
|
||||
var sx = node.clientWidth / to.width / zoom;
|
||||
var sy = node.clientHeight / to.height / zoom;
|
||||
|
||||
// find the starting position of the transform origin
|
||||
var fx = from.left + from.width * ox;
|
||||
var fy = from.top + from.height * oy;
|
||||
|
||||
// find the ending position of the transform origin
|
||||
var tx = to.left + to.width * ox;
|
||||
var ty = to.top + to.height * oy;
|
||||
|
||||
// find the translation at the start of the transform
|
||||
var dx = (fx - tx) * sx;
|
||||
var dy = (fy - ty) * sy;
|
||||
|
||||
// find the relative scale at the start of the transform
|
||||
var dsx = from.width / to.width;
|
||||
var dsy = from.height / to.height;
|
||||
|
||||
return {
|
||||
delay,
|
||||
duration: typeof duration === 'function' ? duration(Math.sqrt(dx * dx + dy * dy)) : duration,
|
||||
easing,
|
||||
css: (t, u) => {
|
||||
var x = u * dx;
|
||||
var y = u * dy;
|
||||
var sx = t + u * dsx;
|
||||
var sy = t + u * dsy;
|
||||
|
||||
return `transform: ${transform} translate(${x}px, ${y}px) scale(${sx}, ${sy});`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} element
|
||||
*/
|
||||
function get_zoom(element) {
|
||||
if ('currentCSSZoom' in element) {
|
||||
return /** @type {number} */ (element.currentCSSZoom);
|
||||
}
|
||||
|
||||
/** @type {Element | null} */
|
||||
var current = element;
|
||||
var zoom = 1;
|
||||
|
||||
while (current !== null) {
|
||||
zoom *= +getComputedStyle(current).zoom;
|
||||
current = /** @type {Element | null} */ (current.parentElement);
|
||||
}
|
||||
|
||||
return zoom;
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/** @import { Action, ActionReturn } from '../action/public' */
|
||||
/** @import { Attachment } from './public' */
|
||||
import { noop, render_effect } from 'svelte/internal/client';
|
||||
import { ATTACHMENT_KEY } from '../constants.js';
|
||||
import { untrack } from '../index-client.js';
|
||||
import { teardown } from '../internal/client/reactivity/effects.js';
|
||||
|
||||
/**
|
||||
* Creates an object key that will be recognised as an attachment when the object is spread onto an element,
|
||||
* as a programmatic alternative to using `{@attach ...}`. This can be useful for library authors, though
|
||||
* is generally not needed when building an app.
|
||||
*
|
||||
* ```svelte
|
||||
* <script>
|
||||
* import { createAttachmentKey } from 'svelte/attachments';
|
||||
*
|
||||
* const props = {
|
||||
* class: 'cool',
|
||||
* onclick: () => alert('clicked'),
|
||||
* [createAttachmentKey()]: (node) => {
|
||||
* node.textContent = 'attached!';
|
||||
* }
|
||||
* };
|
||||
* </script>
|
||||
*
|
||||
* <button {...props}>click me</button>
|
||||
* ```
|
||||
* @since 5.29
|
||||
*/
|
||||
export function createAttachmentKey() {
|
||||
return Symbol(ATTACHMENT_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior.
|
||||
* It's useful if you want to start using attachments on components but you have actions provided by a library.
|
||||
*
|
||||
* Note that the second argument, if provided, must be a function that _returns_ the argument to the
|
||||
* action function, not the argument itself.
|
||||
*
|
||||
* ```svelte
|
||||
* <!-- with an action -->
|
||||
* <div use:foo={bar}>...</div>
|
||||
*
|
||||
* <!-- with an attachment -->
|
||||
* <div {@attach fromAction(foo, () => bar)}>...</div>
|
||||
* ```
|
||||
* @template {EventTarget} E
|
||||
* @template {unknown} T
|
||||
* @overload
|
||||
* @param {Action<E, T> | ((element: E, arg: T) => void | ActionReturn<T>)} action The action function
|
||||
* @param {() => T} fn A function that returns the argument for the action
|
||||
* @returns {Attachment<E>}
|
||||
*/
|
||||
/**
|
||||
* Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior.
|
||||
* It's useful if you want to start using attachments on components but you have actions provided by a library.
|
||||
*
|
||||
* Note that the second argument, if provided, must be a function that _returns_ the argument to the
|
||||
* action function, not the argument itself.
|
||||
*
|
||||
* ```svelte
|
||||
* <!-- with an action -->
|
||||
* <div use:foo={bar}>...</div>
|
||||
*
|
||||
* <!-- with an attachment -->
|
||||
* <div {@attach fromAction(foo, () => bar)}>...</div>
|
||||
* ```
|
||||
* @template {EventTarget} E
|
||||
* @overload
|
||||
* @param {Action<E, void> | ((element: E) => void | ActionReturn<void>)} action The action function
|
||||
* @returns {Attachment<E>}
|
||||
*/
|
||||
/**
|
||||
* Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior.
|
||||
* It's useful if you want to start using attachments on components but you have actions provided by a library.
|
||||
*
|
||||
* Note that the second argument, if provided, must be a function that _returns_ the argument to the
|
||||
* action function, not the argument itself.
|
||||
*
|
||||
* ```svelte
|
||||
* <!-- with an action -->
|
||||
* <div use:foo={bar}>...</div>
|
||||
*
|
||||
* <!-- with an attachment -->
|
||||
* <div {@attach fromAction(foo, () => bar)}>...</div>
|
||||
* ```
|
||||
*
|
||||
* @template {EventTarget} E
|
||||
* @template {unknown} T
|
||||
* @param {Action<E, T> | ((element: E, arg: T) => void | ActionReturn<T>)} action The action function
|
||||
* @param {() => T} fn A function that returns the argument for the action
|
||||
* @returns {Attachment<E>}
|
||||
* @since 5.32
|
||||
*/
|
||||
export function fromAction(action, fn = /** @type {() => T} */ (noop)) {
|
||||
return (element) => {
|
||||
const { update, destroy } = untrack(() => action(element, fn()) ?? {});
|
||||
|
||||
if (update) {
|
||||
var ran = false;
|
||||
render_effect(() => {
|
||||
const arg = fn();
|
||||
if (ran) update(arg);
|
||||
});
|
||||
ran = true;
|
||||
}
|
||||
|
||||
if (destroy) {
|
||||
teardown(destroy);
|
||||
}
|
||||
};
|
||||
}
|
||||
+1719
File diff suppressed because it is too large
Load Diff
+201
@@ -0,0 +1,201 @@
|
||||
/** @import { LegacyRoot } from './types/legacy-nodes.js' */
|
||||
/** @import { CompileOptions, CompileResult, ValidatedCompileOptions, ModuleCompileOptions } from '#compiler' */
|
||||
/** @import { AST } from './public.js' */
|
||||
import { walk as zimmerframe_walk } from 'zimmerframe';
|
||||
import { convert } from './legacy.js';
|
||||
import { parse as _parse, Parser } from './phases/1-parse/index.js';
|
||||
import { remove_typescript_nodes } from './phases/1-parse/remove_typescript_nodes.js';
|
||||
import { parse_stylesheet } from './phases/1-parse/read/style.js';
|
||||
import { analyze_component, analyze_module } from './phases/2-analyze/index.js';
|
||||
import { transform_component, transform_module } from './phases/3-transform/index.js';
|
||||
import { validate_component_options, validate_module_options } from './validate-options.js';
|
||||
import * as state from './state.js';
|
||||
export { default as preprocess } from './preprocess/index.js';
|
||||
export { print } from './print/index.js';
|
||||
|
||||
/**
|
||||
* `compile` converts your `.svelte` source code into a JavaScript module that exports a component
|
||||
*
|
||||
* @param {string} source The component source code
|
||||
* @param {CompileOptions} options The compiler options
|
||||
* @returns {CompileResult}
|
||||
*/
|
||||
export function compile(source, options) {
|
||||
source = remove_bom(source);
|
||||
state.reset({ warning: options.warningFilter, filename: options.filename });
|
||||
|
||||
const validated = validate_component_options(options, '');
|
||||
|
||||
let parsed = _parse(source);
|
||||
|
||||
const { customElement: customElementOptions, ...parsed_options } = parsed.options || {};
|
||||
|
||||
/** @type {ValidatedCompileOptions} */
|
||||
const combined_options = {
|
||||
...validated,
|
||||
...parsed_options,
|
||||
customElementOptions,
|
||||
css: 'css' in parsed_options ? () => parsed_options.css ?? 'external' : validated.css,
|
||||
runes: 'runes' in parsed_options ? () => parsed_options.runes : validated.runes
|
||||
};
|
||||
|
||||
if (parsed.metadata.ts) {
|
||||
parsed = {
|
||||
...parsed,
|
||||
fragment: parsed.fragment && remove_typescript_nodes(parsed.fragment),
|
||||
instance: parsed.instance && remove_typescript_nodes(parsed.instance),
|
||||
module: parsed.module && remove_typescript_nodes(parsed.module)
|
||||
};
|
||||
if (combined_options.customElementOptions?.extend) {
|
||||
combined_options.customElementOptions.extend = remove_typescript_nodes(
|
||||
combined_options.customElementOptions?.extend
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const analysis = analyze_component(parsed, source, combined_options);
|
||||
const result = transform_component(analysis, source, combined_options);
|
||||
result.ast = to_public_ast(source, parsed, options.modernAst);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* `compileModule` takes your JavaScript source code containing runes, and turns it into a JavaScript module.
|
||||
*
|
||||
* @param {string} source The component source code
|
||||
* @param {ModuleCompileOptions} options
|
||||
* @returns {CompileResult}
|
||||
*/
|
||||
export function compileModule(source, options) {
|
||||
source = remove_bom(source);
|
||||
state.reset({ warning: options.warningFilter, filename: options.filename });
|
||||
const validated = validate_module_options(options, '');
|
||||
|
||||
const analysis = analyze_module(source, validated);
|
||||
return transform_module(analysis, source, validated);
|
||||
}
|
||||
|
||||
/**
|
||||
* The parse function parses a component, returning only its abstract syntax tree.
|
||||
*
|
||||
* The `modern` option (`false` by default in Svelte 5) makes the parser return a modern AST instead of the legacy AST.
|
||||
* `modern` will become `true` by default in Svelte 6, and the option will be removed in Svelte 7.
|
||||
*
|
||||
* @overload
|
||||
* @param {string} source
|
||||
* @param {{ filename?: string; modern: true; loose?: boolean }} options
|
||||
* @returns {AST.Root}
|
||||
*/
|
||||
|
||||
/**
|
||||
* The parse function parses a component, returning only its abstract syntax tree.
|
||||
*
|
||||
* The `modern` option (`false` by default in Svelte 5) makes the parser return a modern AST instead of the legacy AST.
|
||||
* `modern` will become `true` by default in Svelte 6, and the option will be removed in Svelte 7.
|
||||
*
|
||||
* @overload
|
||||
* @param {string} source
|
||||
* @param {{ filename?: string; modern?: false; loose?: boolean }} [options]
|
||||
* @returns {Record<string, any>}
|
||||
*/
|
||||
|
||||
// TODO 6.0 remove unused `filename`
|
||||
/**
|
||||
* The parse function parses a component, returning only its abstract syntax tree.
|
||||
*
|
||||
* The `modern` option (`false` by default in Svelte 5) makes the parser return a modern AST instead of the legacy AST.
|
||||
* `modern` will become `true` by default in Svelte 6, and the option will be removed in Svelte 7.
|
||||
*
|
||||
* The `loose` option, available since 5.13.0, tries to always return an AST even if the input will not successfully compile.
|
||||
*
|
||||
* The `filename` option is unused and will be removed in Svelte 6.0.
|
||||
*
|
||||
* @param {string} source
|
||||
* @param {{ filename?: string; rootDir?: string; modern?: boolean; loose?: boolean }} [options]
|
||||
* @returns {AST.Root | LegacyRoot}
|
||||
*/
|
||||
export function parse(source, { modern, loose } = {}) {
|
||||
source = remove_bom(source);
|
||||
state.reset({ warning: () => false, filename: undefined });
|
||||
|
||||
const ast = _parse(source, loose);
|
||||
return to_public_ast(source, ast, modern);
|
||||
}
|
||||
|
||||
/**
|
||||
* The parseCss function parses a CSS stylesheet, returning its abstract syntax tree.
|
||||
*
|
||||
* @param {string} source The CSS source code
|
||||
* @returns {AST.CSS.StyleSheetFile}
|
||||
*/
|
||||
export function parseCss(source) {
|
||||
source = remove_bom(source);
|
||||
state.reset({ warning: () => false, filename: undefined });
|
||||
|
||||
state.set_source(source);
|
||||
|
||||
const parser = Parser.forCss(source);
|
||||
const children = parse_stylesheet(parser);
|
||||
|
||||
return {
|
||||
type: 'StyleSheetFile',
|
||||
start: 0,
|
||||
end: source.length,
|
||||
children
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} source
|
||||
* @param {AST.Root} ast
|
||||
* @param {boolean | undefined} modern
|
||||
*/
|
||||
function to_public_ast(source, ast, modern) {
|
||||
if (modern) {
|
||||
const clean = (/** @type {any} */ node) => {
|
||||
delete node.metadata;
|
||||
};
|
||||
|
||||
ast.options?.attributes.forEach((attribute) => {
|
||||
clean(attribute);
|
||||
clean(attribute.value);
|
||||
if (Array.isArray(attribute.value)) {
|
||||
attribute.value.forEach(clean);
|
||||
}
|
||||
});
|
||||
|
||||
// remove things that we don't want to treat as public API
|
||||
return zimmerframe_walk(ast, null, {
|
||||
_(node, { next }) {
|
||||
clean(node);
|
||||
next();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return convert(source, ast);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the byte order mark from a string if it's present since it would mess with our template generation logic
|
||||
* @param {string} source
|
||||
*/
|
||||
function remove_bom(source) {
|
||||
if (source.charCodeAt(0) === 0xfeff) {
|
||||
return source.slice(1);
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Replace this with `import { walk } from 'estree-walker'`
|
||||
* @returns {never}
|
||||
*/
|
||||
export function walk() {
|
||||
throw new Error(
|
||||
`'svelte/compiler' no longer exports a \`walk\` utility — please import it directly from 'estree-walker' instead`
|
||||
);
|
||||
}
|
||||
|
||||
export { VERSION } from '../version.js';
|
||||
export { migrate } from './migrate/index.js';
|
||||
+637
@@ -0,0 +1,637 @@
|
||||
/** @import { Expression } from 'estree' */
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import * as Legacy from './types/legacy-nodes.js' */
|
||||
import { walk } from 'zimmerframe';
|
||||
import {
|
||||
regex_ends_with_whitespaces,
|
||||
regex_not_whitespace,
|
||||
regex_starts_with_whitespaces
|
||||
} from './phases/patterns.js';
|
||||
import { extract_svelte_ignore } from './utils/extract_svelte_ignore.js';
|
||||
|
||||
/**
|
||||
* Some of the legacy Svelte AST nodes remove whitespace from the start and end of their children.
|
||||
* @param {AST.TemplateNode[]} nodes
|
||||
*/
|
||||
function remove_surrounding_whitespace_nodes(nodes) {
|
||||
const first = nodes.at(0);
|
||||
const last = nodes.at(-1);
|
||||
|
||||
if (first?.type === 'Text') {
|
||||
if (!regex_not_whitespace.test(first.data)) {
|
||||
nodes.shift();
|
||||
} else {
|
||||
first.data = first.data.replace(regex_starts_with_whitespaces, '');
|
||||
}
|
||||
}
|
||||
if (last?.type === 'Text') {
|
||||
if (!regex_not_whitespace.test(last.data)) {
|
||||
nodes.pop();
|
||||
} else {
|
||||
last.data = last.data.replace(regex_ends_with_whitespaces, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform our nice modern AST into the monstrosity emitted by Svelte 4
|
||||
* @param {string} source
|
||||
* @param {AST.Root} ast
|
||||
* @returns {Legacy.LegacyRoot}
|
||||
*/
|
||||
export function convert(source, ast) {
|
||||
const root = /** @type {AST.SvelteNode | Legacy.LegacySvelteNode} */ (ast);
|
||||
|
||||
return /** @type {Legacy.LegacyRoot} */ (
|
||||
walk(root, null, {
|
||||
_(node, { next }) {
|
||||
// @ts-ignore
|
||||
delete node.metadata;
|
||||
next();
|
||||
},
|
||||
// @ts-ignore
|
||||
Root(node, { visit }) {
|
||||
const { instance, module, options } = node;
|
||||
|
||||
// Insert svelte:options back into the root nodes
|
||||
if (/** @type {any} */ (options)?.__raw__) {
|
||||
let idx = node.fragment.nodes.findIndex(
|
||||
(node) => /** @type {any} */ (options).end <= node.start
|
||||
);
|
||||
if (idx === -1) {
|
||||
idx = node.fragment.nodes.length;
|
||||
}
|
||||
|
||||
node.fragment.nodes.splice(idx, 0, /** @type {any} */ (options).__raw__);
|
||||
}
|
||||
|
||||
/** @type {number | null} */
|
||||
let start = null;
|
||||
|
||||
/** @type {number | null} */
|
||||
let end = null;
|
||||
|
||||
if (node.fragment.nodes.length > 0) {
|
||||
const first = /** @type {AST.BaseNode} */ (node.fragment.nodes.at(0));
|
||||
const last = /** @type {AST.BaseNode} */ (node.fragment.nodes.at(-1));
|
||||
|
||||
start = first.start;
|
||||
end = last.end;
|
||||
|
||||
while (/\s/.test(source[start])) start += 1;
|
||||
while (/\s/.test(source[end - 1])) end -= 1;
|
||||
}
|
||||
|
||||
if (instance) {
|
||||
// @ts-ignore
|
||||
delete instance.attributes;
|
||||
}
|
||||
|
||||
if (module) {
|
||||
// @ts-ignore
|
||||
delete module.attributes;
|
||||
}
|
||||
|
||||
return {
|
||||
html: {
|
||||
type: 'Fragment',
|
||||
start,
|
||||
end,
|
||||
children: node.fragment.nodes.map((child) => visit(child))
|
||||
},
|
||||
instance,
|
||||
module,
|
||||
css: ast.css ? visit(ast.css) : undefined,
|
||||
// put it on _comments not comments because the latter is checked by prettier and then fails
|
||||
// if we don't adjust stuff accordingly in our prettier plugin, and so it would be kind of an
|
||||
// indirect breaking change for people updating their Svelte version but not their prettier plugin version.
|
||||
// We can keep it as comments for the modern AST because the modern AST is not used in the plugin yet.
|
||||
_comments: ast.comments?.length > 0 ? ast.comments : undefined
|
||||
};
|
||||
},
|
||||
AnimateDirective(node) {
|
||||
return { ...node, type: 'Animation' };
|
||||
},
|
||||
// @ts-ignore
|
||||
AwaitBlock(node, { visit }) {
|
||||
let pendingblock = {
|
||||
type: 'PendingBlock',
|
||||
/** @type {number | null} */
|
||||
start: null,
|
||||
/** @type {number | null} */
|
||||
end: null,
|
||||
children: node.pending?.nodes.map((child) => visit(child)) ?? [],
|
||||
skip: true
|
||||
};
|
||||
|
||||
let thenblock = {
|
||||
type: 'ThenBlock',
|
||||
/** @type {number | null} */
|
||||
start: null,
|
||||
/** @type {number | null} */
|
||||
end: null,
|
||||
children: node.then?.nodes.map((child) => visit(child)) ?? [],
|
||||
skip: true
|
||||
};
|
||||
|
||||
let catchblock = {
|
||||
type: 'CatchBlock',
|
||||
/** @type {number | null} */
|
||||
start: null,
|
||||
/** @type {number | null} */
|
||||
end: null,
|
||||
children: node.catch?.nodes.map((child) => visit(child)) ?? [],
|
||||
skip: true
|
||||
};
|
||||
|
||||
if (node.pending) {
|
||||
const first = node.pending.nodes.at(0);
|
||||
const last = node.pending.nodes.at(-1);
|
||||
|
||||
pendingblock.start = first?.start ?? source.indexOf('}', node.expression.end) + 1;
|
||||
pendingblock.end = last?.end ?? pendingblock.start;
|
||||
pendingblock.skip = false;
|
||||
}
|
||||
|
||||
if (node.then) {
|
||||
const first = node.then.nodes.at(0);
|
||||
const last = node.then.nodes.at(-1);
|
||||
|
||||
thenblock.start =
|
||||
pendingblock.end ?? first?.start ?? source.indexOf('}', node.expression.end) + 1;
|
||||
thenblock.end =
|
||||
last?.end ?? source.lastIndexOf('}', pendingblock.end ?? node.expression.end) + 1;
|
||||
thenblock.skip = false;
|
||||
}
|
||||
|
||||
if (node.catch) {
|
||||
const first = node.catch.nodes.at(0);
|
||||
const last = node.catch.nodes.at(-1);
|
||||
|
||||
catchblock.start =
|
||||
thenblock.end ??
|
||||
pendingblock.end ??
|
||||
first?.start ??
|
||||
source.indexOf('}', node.expression.end) + 1;
|
||||
catchblock.end =
|
||||
last?.end ??
|
||||
source.lastIndexOf('}', thenblock.end ?? pendingblock.end ?? node.expression.end) + 1;
|
||||
catchblock.skip = false;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'AwaitBlock',
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
expression: node.expression,
|
||||
value: node.value,
|
||||
error: node.error,
|
||||
pending: pendingblock,
|
||||
then: thenblock,
|
||||
catch: catchblock
|
||||
};
|
||||
},
|
||||
BindDirective(node) {
|
||||
return { ...node, type: 'Binding' };
|
||||
},
|
||||
ClassDirective(node) {
|
||||
return { ...node, type: 'Class' };
|
||||
},
|
||||
Comment(node) {
|
||||
return {
|
||||
...node,
|
||||
ignores: extract_svelte_ignore(node.start, node.data, false)
|
||||
};
|
||||
},
|
||||
ComplexSelector(node, { next }) {
|
||||
next(); // delete inner metadata/parent properties
|
||||
|
||||
const children = [];
|
||||
|
||||
for (const child of node.children) {
|
||||
if (child.combinator) {
|
||||
children.push(child.combinator);
|
||||
}
|
||||
|
||||
children.push(...child.selectors);
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'Selector',
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
children
|
||||
};
|
||||
},
|
||||
Component(node, { visit }) {
|
||||
return {
|
||||
type: 'InlineComponent',
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
name: node.name,
|
||||
attributes: node.attributes.map(
|
||||
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
|
||||
),
|
||||
children: node.fragment.nodes.map(
|
||||
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
|
||||
)
|
||||
};
|
||||
},
|
||||
// @ts-ignore
|
||||
ConstTag(node) {
|
||||
if (/** @type {Legacy.LegacyConstTag} */ (node).expression !== undefined) {
|
||||
return node;
|
||||
}
|
||||
|
||||
const modern_node = /** @type {AST.ConstTag} */ (node);
|
||||
const { id: left } = { ...modern_node.declaration.declarations[0] };
|
||||
// @ts-ignore
|
||||
delete left.typeAnnotation;
|
||||
return {
|
||||
type: 'ConstTag',
|
||||
start: modern_node.start,
|
||||
end: node.end,
|
||||
expression: {
|
||||
type: 'AssignmentExpression',
|
||||
start: (modern_node.declaration.start ?? 0) + 'const '.length,
|
||||
end: modern_node.declaration.end ?? 0,
|
||||
operator: '=',
|
||||
left,
|
||||
right: modern_node.declaration.declarations[0].init
|
||||
}
|
||||
};
|
||||
},
|
||||
// @ts-ignore
|
||||
KeyBlock(node, { visit }) {
|
||||
remove_surrounding_whitespace_nodes(node.fragment.nodes);
|
||||
return {
|
||||
type: 'KeyBlock',
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
expression: node.expression,
|
||||
children: node.fragment.nodes.map(
|
||||
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
|
||||
)
|
||||
};
|
||||
},
|
||||
// @ts-ignore
|
||||
EachBlock(node, { visit }) {
|
||||
let elseblock = undefined;
|
||||
|
||||
if (node.fallback) {
|
||||
const first = node.fallback.nodes.at(0);
|
||||
const end = source.lastIndexOf('{', /** @type {number} */ (node.end) - 1);
|
||||
const start = first?.start ?? end;
|
||||
|
||||
remove_surrounding_whitespace_nodes(node.fallback.nodes);
|
||||
|
||||
elseblock = {
|
||||
type: 'ElseBlock',
|
||||
start,
|
||||
end,
|
||||
children: node.fallback.nodes.map((child) => visit(child))
|
||||
};
|
||||
}
|
||||
|
||||
remove_surrounding_whitespace_nodes(node.body.nodes);
|
||||
|
||||
return {
|
||||
type: 'EachBlock',
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
children: node.body.nodes.map((child) => visit(child)),
|
||||
context: node.context,
|
||||
expression: node.expression,
|
||||
index: node.index,
|
||||
key: node.key,
|
||||
else: elseblock
|
||||
};
|
||||
},
|
||||
ExpressionTag(node, { path }) {
|
||||
const parent = path.at(-1);
|
||||
if (parent?.type === 'Attribute') {
|
||||
if (source[parent.start] === '{') {
|
||||
return {
|
||||
type: 'AttributeShorthand',
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
expression: node.expression
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'MustacheTag',
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
expression: node.expression
|
||||
};
|
||||
},
|
||||
HtmlTag(node) {
|
||||
return { ...node, type: 'RawMustacheTag' };
|
||||
},
|
||||
// @ts-ignore
|
||||
IfBlock(node, { visit }) {
|
||||
let elseblock = undefined;
|
||||
if (node.alternate) {
|
||||
let nodes = node.alternate.nodes;
|
||||
if (nodes.length === 1 && nodes[0].type === 'IfBlock' && nodes[0].elseif) {
|
||||
nodes = nodes[0].consequent.nodes;
|
||||
}
|
||||
|
||||
const end = source.lastIndexOf('{', /** @type {number} */ (node.end) - 1);
|
||||
const start = nodes.at(0)?.start ?? end;
|
||||
|
||||
remove_surrounding_whitespace_nodes(node.alternate.nodes);
|
||||
|
||||
elseblock = {
|
||||
type: 'ElseBlock',
|
||||
start,
|
||||
end: end,
|
||||
children: node.alternate.nodes.map(
|
||||
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
const start = node.elseif
|
||||
? node.consequent.nodes[0]?.start ??
|
||||
source.lastIndexOf('{', /** @type {number} */ (node.end) - 1)
|
||||
: node.start;
|
||||
|
||||
remove_surrounding_whitespace_nodes(node.consequent.nodes);
|
||||
|
||||
return {
|
||||
type: 'IfBlock',
|
||||
start,
|
||||
end: node.end,
|
||||
expression: node.test,
|
||||
children: node.consequent.nodes.map(
|
||||
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
|
||||
),
|
||||
else: elseblock,
|
||||
elseif: node.elseif ? true : undefined
|
||||
};
|
||||
},
|
||||
OnDirective(node) {
|
||||
return { ...node, type: 'EventHandler' };
|
||||
},
|
||||
// @ts-expect-error
|
||||
SnippetBlock(node, { visit }) {
|
||||
remove_surrounding_whitespace_nodes(node.body.nodes);
|
||||
return {
|
||||
type: 'SnippetBlock',
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
expression: node.expression,
|
||||
parameters: node.parameters,
|
||||
children: node.body.nodes.map((child) => visit(child)),
|
||||
typeParams: node.typeParams
|
||||
};
|
||||
},
|
||||
// @ts-expect-error
|
||||
SvelteBoundary(node, { visit }) {
|
||||
remove_surrounding_whitespace_nodes(node.fragment.nodes);
|
||||
return {
|
||||
type: 'SvelteBoundary',
|
||||
name: 'svelte:boundary',
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
attributes: node.attributes.map(
|
||||
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
|
||||
),
|
||||
children: node.fragment.nodes.map((child) => visit(child))
|
||||
};
|
||||
},
|
||||
RegularElement(node, { visit }) {
|
||||
return {
|
||||
type: 'Element',
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
name: node.name,
|
||||
attributes: node.attributes.map((child) => visit(child)),
|
||||
children: node.fragment.nodes.map((child) => visit(child))
|
||||
};
|
||||
},
|
||||
SlotElement(node, { visit }) {
|
||||
return {
|
||||
type: 'Slot',
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
name: node.name,
|
||||
attributes: node.attributes.map(
|
||||
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
|
||||
),
|
||||
children: node.fragment.nodes.map(
|
||||
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
|
||||
)
|
||||
};
|
||||
},
|
||||
Attribute(node, { visit, next, path }) {
|
||||
if (node.value !== true && !Array.isArray(node.value)) {
|
||||
path.push(node);
|
||||
const value = /** @type {Legacy.LegacyAttribute['value']} */ ([visit(node.value)]);
|
||||
path.pop();
|
||||
|
||||
return {
|
||||
...node,
|
||||
value
|
||||
};
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
},
|
||||
StyleDirective(node, { visit, next, path }) {
|
||||
if (node.value !== true && !Array.isArray(node.value)) {
|
||||
path.push(node);
|
||||
const value = /** @type {Legacy.LegacyStyleDirective['value']} */ ([visit(node.value)]);
|
||||
path.pop();
|
||||
|
||||
return {
|
||||
...node,
|
||||
value
|
||||
};
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
},
|
||||
SpreadAttribute(node) {
|
||||
return { ...node, type: 'Spread' };
|
||||
},
|
||||
// @ts-ignore
|
||||
StyleSheet(node, context) {
|
||||
return {
|
||||
...node,
|
||||
...context.next(),
|
||||
type: 'Style'
|
||||
};
|
||||
},
|
||||
SvelteBody(node, { visit }) {
|
||||
return {
|
||||
type: 'Body',
|
||||
name: 'svelte:body',
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
attributes: node.attributes.map(
|
||||
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
|
||||
),
|
||||
children: node.fragment.nodes.map(
|
||||
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
|
||||
)
|
||||
};
|
||||
},
|
||||
SvelteComponent(node, { visit }) {
|
||||
return {
|
||||
type: 'InlineComponent',
|
||||
name: 'svelte:component',
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
expression: node.expression,
|
||||
attributes: node.attributes.map(
|
||||
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
|
||||
),
|
||||
children: node.fragment.nodes.map(
|
||||
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
|
||||
)
|
||||
};
|
||||
},
|
||||
SvelteDocument(node, { visit }) {
|
||||
return {
|
||||
type: 'Document',
|
||||
name: 'svelte:document',
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
attributes: node.attributes.map(
|
||||
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
|
||||
),
|
||||
children: node.fragment.nodes.map(
|
||||
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
|
||||
)
|
||||
};
|
||||
},
|
||||
SvelteElement(node, { visit }) {
|
||||
/** @type {Expression | string} */
|
||||
let tag = node.tag;
|
||||
if (
|
||||
tag.type === 'Literal' &&
|
||||
typeof tag.value === 'string' &&
|
||||
source[/** @type {number} */ (node.tag.start) - 1] !== '{'
|
||||
) {
|
||||
tag = tag.value;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'Element',
|
||||
name: 'svelte:element',
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
tag,
|
||||
attributes: node.attributes.map((child) => visit(child)),
|
||||
children: node.fragment.nodes.map((child) => visit(child))
|
||||
};
|
||||
},
|
||||
SvelteFragment(node, { visit }) {
|
||||
return {
|
||||
type: 'SlotTemplate',
|
||||
name: 'svelte:fragment',
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
attributes: node.attributes.map(
|
||||
(a) => /** @type {Legacy.LegacyAttributeLike} */ (visit(a))
|
||||
),
|
||||
children: node.fragment.nodes.map(
|
||||
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
|
||||
)
|
||||
};
|
||||
},
|
||||
SvelteHead(node, { visit }) {
|
||||
return {
|
||||
type: 'Head',
|
||||
name: 'svelte:head',
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
attributes: node.attributes.map(
|
||||
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
|
||||
),
|
||||
children: node.fragment.nodes.map(
|
||||
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
|
||||
)
|
||||
};
|
||||
},
|
||||
SvelteOptions(node, { visit }) {
|
||||
return {
|
||||
type: 'Options',
|
||||
name: 'svelte:options',
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
attributes: node.attributes.map(
|
||||
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
|
||||
)
|
||||
};
|
||||
},
|
||||
SvelteSelf(node, { visit }) {
|
||||
return {
|
||||
type: 'InlineComponent',
|
||||
name: 'svelte:self',
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
attributes: node.attributes.map(
|
||||
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
|
||||
),
|
||||
children: node.fragment.nodes.map(
|
||||
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
|
||||
)
|
||||
};
|
||||
},
|
||||
SvelteWindow(node, { visit }) {
|
||||
return {
|
||||
type: 'Window',
|
||||
name: 'svelte:window',
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
attributes: node.attributes.map(
|
||||
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
|
||||
),
|
||||
children: node.fragment.nodes.map(
|
||||
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
|
||||
)
|
||||
};
|
||||
},
|
||||
Text(node, { path }) {
|
||||
const parent = path.at(-1);
|
||||
if (parent?.type === 'RegularElement' && parent.name === 'style') {
|
||||
// these text nodes are missing `raw` for some dumb reason
|
||||
return /** @type {AST.Text} */ ({
|
||||
type: 'Text',
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
data: node.data
|
||||
});
|
||||
}
|
||||
},
|
||||
TitleElement(node, { visit }) {
|
||||
return {
|
||||
type: 'Title',
|
||||
name: 'title',
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
attributes: node.attributes.map(
|
||||
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
|
||||
),
|
||||
children: node.fragment.nodes.map(
|
||||
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
|
||||
)
|
||||
};
|
||||
},
|
||||
TransitionDirective(node) {
|
||||
return { ...node, type: 'Transition' };
|
||||
},
|
||||
UseDirective(node) {
|
||||
return { ...node, type: 'Action' };
|
||||
},
|
||||
LetDirective(node) {
|
||||
return { ...node, type: 'Let' };
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
+1998
File diff suppressed because it is too large
Load Diff
+225
@@ -0,0 +1,225 @@
|
||||
/** @import { Comment, Program } from 'estree' */
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Parser } from './index.js' */
|
||||
import * as acorn from 'acorn';
|
||||
import { walk } from 'zimmerframe';
|
||||
import { tsPlugin } from '@sveltejs/acorn-typescript';
|
||||
import * as e from '../../errors.js';
|
||||
|
||||
const JSParser = acorn.Parser;
|
||||
const TSParser = JSParser.extend(tsPlugin());
|
||||
|
||||
/**
|
||||
* @typedef {Comment & {
|
||||
* start: number;
|
||||
* end: number;
|
||||
* }} CommentWithLocation
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} source
|
||||
* @param {AST.JSComment[]} comments
|
||||
* @param {boolean} typescript
|
||||
* @param {boolean} [is_script]
|
||||
*/
|
||||
export function parse(source, comments, typescript, is_script) {
|
||||
const acorn = typescript ? TSParser : JSParser;
|
||||
|
||||
const { onComment, add_comments } = get_comment_handlers(
|
||||
source,
|
||||
/** @type {CommentWithLocation[]} */ (comments)
|
||||
);
|
||||
|
||||
// @ts-expect-error
|
||||
const parse_statement = acorn.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
|
||||
acorn.prototype.parseStatement = function (...args) {
|
||||
const v = parse_statement.call(this, ...args);
|
||||
// @ts-ignore
|
||||
this.undefinedExports = {};
|
||||
return v;
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const ast = acorn.parse(source, {
|
||||
onComment,
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 16,
|
||||
locations: true
|
||||
});
|
||||
|
||||
add_comments(ast);
|
||||
|
||||
return /** @type {Program} */ (ast);
|
||||
} catch (err) {
|
||||
// TODO the `return` in necessary for TS<7 due to a bug; otherwise
|
||||
// the `finally` block is regarded as unreachable
|
||||
return handle_parse_error(err);
|
||||
} finally {
|
||||
if (is_script) {
|
||||
// @ts-expect-error
|
||||
acorn.prototype.parseStatement = parse_statement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {string} source
|
||||
* @param {number} index
|
||||
* @returns {acorn.Expression & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }}
|
||||
*/
|
||||
export function parse_expression_at(parser, source, index) {
|
||||
const acorn = parser.ts ? TSParser : JSParser;
|
||||
|
||||
const { onComment, add_comments } = get_comment_handlers(source, parser.root.comments, index);
|
||||
|
||||
try {
|
||||
const ast = acorn.parseExpressionAt(source, index, {
|
||||
onComment,
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 16,
|
||||
locations: true,
|
||||
preserveParens: true
|
||||
});
|
||||
|
||||
add_comments(ast);
|
||||
|
||||
return ast;
|
||||
} catch (e) {
|
||||
handle_parse_error(e);
|
||||
}
|
||||
}
|
||||
|
||||
const regex_position_indicator = / \(\d+:\d+\)$/;
|
||||
|
||||
/**
|
||||
* @param {any} err
|
||||
* @returns {never}
|
||||
*/
|
||||
function handle_parse_error(err) {
|
||||
e.js_parse_error(err.pos, err.message.replace(regex_position_indicator, ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {acorn.Expression} node
|
||||
* @returns {acorn.Expression}
|
||||
*/
|
||||
export function remove_parens(node) {
|
||||
return walk(node, null, {
|
||||
ParenthesizedExpression(node, context) {
|
||||
return context.visit(node.expression);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Acorn doesn't add comments to the AST by itself. This factory returns the capabilities
|
||||
* to add them after the fact. They are needed in order to support `svelte-ignore` comments
|
||||
* in JS code and so that `prettier-plugin-svelte` doesn't remove all comments when formatting.
|
||||
* @param {string} source
|
||||
* @param {CommentWithLocation[]} comments
|
||||
* @param {number} index
|
||||
*/
|
||||
function get_comment_handlers(source, comments, index = 0) {
|
||||
return {
|
||||
/**
|
||||
* @param {boolean} block
|
||||
* @param {string} value
|
||||
* @param {number} start
|
||||
* @param {number} end
|
||||
* @param {import('acorn').Position} [start_loc]
|
||||
* @param {import('acorn').Position} [end_loc]
|
||||
*/
|
||||
onComment: (block, value, start, end, start_loc, end_loc) => {
|
||||
if (block && /\n/.test(value)) {
|
||||
let a = start;
|
||||
while (a > 0 && source[a - 1] !== '\n') a -= 1;
|
||||
|
||||
let b = a;
|
||||
while (/[ \t]/.test(source[b])) b += 1;
|
||||
|
||||
const indentation = source.slice(a, b);
|
||||
value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
|
||||
}
|
||||
|
||||
comments.push({
|
||||
type: block ? 'Block' : 'Line',
|
||||
value,
|
||||
start,
|
||||
end,
|
||||
loc: {
|
||||
start: /** @type {import('acorn').Position} */ (start_loc),
|
||||
end: /** @type {import('acorn').Position} */ (end_loc)
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/** @param {acorn.Node & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }} ast */
|
||||
add_comments(ast) {
|
||||
if (comments.length === 0) return;
|
||||
|
||||
comments = comments
|
||||
.filter((comment) => comment.start >= index)
|
||||
.map(({ type, value, start, end }) => ({ type, value, start, end }));
|
||||
|
||||
walk(ast, null, {
|
||||
_(node, { next, path }) {
|
||||
let comment;
|
||||
|
||||
while (comments[0] && comments[0].start < node.start) {
|
||||
comment = /** @type {CommentWithLocation} */ (comments.shift());
|
||||
(node.leadingComments ||= []).push(comment);
|
||||
}
|
||||
|
||||
next();
|
||||
|
||||
if (comments[0]) {
|
||||
const parent = /** @type {any} */ (path.at(-1));
|
||||
|
||||
if (parent === undefined || node.end !== parent.end) {
|
||||
const slice = source.slice(node.end, comments[0].start);
|
||||
const is_last_in_body =
|
||||
((parent?.type === 'BlockStatement' || parent?.type === 'Program') &&
|
||||
parent.body.indexOf(node) === parent.body.length - 1) ||
|
||||
(parent?.type === 'ArrayExpression' &&
|
||||
parent.elements.indexOf(node) === parent.elements.length - 1) ||
|
||||
(parent?.type === 'ObjectExpression' &&
|
||||
parent.properties.indexOf(node) === parent.properties.length - 1);
|
||||
|
||||
if (is_last_in_body) {
|
||||
// Special case: There can be multiple trailing comments after the last node in a block,
|
||||
// and they can be separated by newlines
|
||||
let end = node.end;
|
||||
|
||||
while (comments.length) {
|
||||
const comment = comments[0];
|
||||
if (parent && comment.start >= parent.end) break;
|
||||
|
||||
(node.trailingComments ||= []).push(comment);
|
||||
comments.shift();
|
||||
end = comment.end;
|
||||
}
|
||||
} else if (node.end <= comments[0].start && /^[,) \t]*$/.test(slice)) {
|
||||
node.trailingComments = [/** @type {CommentWithLocation} */ (comments.shift())];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Special case: Trailing comments after the root node (which can only happen for expression tags or for Program nodes).
|
||||
// Adding them ensures that we can later detect the end of the expression tag correctly.
|
||||
if (comments.length > 0 && (comments[0].start >= ast.end || ast.type === 'Program')) {
|
||||
(ast.trailingComments ||= []).push(...comments.splice(0));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Location } from 'locate-character' */
|
||||
/** @import * as ESTree from 'estree' */
|
||||
// @ts-expect-error acorn type definitions are borked in the release we use
|
||||
import { isIdentifierStart, isIdentifierChar } from 'acorn';
|
||||
import fragment from './state/fragment.js';
|
||||
import * as e from '../../errors.js';
|
||||
import { create_fragment } from './utils/create.js';
|
||||
import read_options from './read/options.js';
|
||||
import { is_reserved } from '../../../utils.js';
|
||||
import { disallow_children } from '../2-analyze/visitors/shared/special-element.js';
|
||||
import * as state from '../../state.js';
|
||||
|
||||
/** @param {number} cc */
|
||||
function is_whitespace(cc) {
|
||||
// fast path for common whitespace
|
||||
if (cc === 32 || (cc <= 13 && cc >= 9)) return true;
|
||||
// rare whitespace — \u00a0, \u1680, \u2000-\u200a, \u2028, \u2029, \u202f, \u205f, \u3000, \ufeff
|
||||
if (cc < 160) return false;
|
||||
return (
|
||||
cc === 160 ||
|
||||
cc === 5760 ||
|
||||
(cc >= 8192 && cc <= 8202) ||
|
||||
cc === 8232 ||
|
||||
cc === 8233 ||
|
||||
cc === 8239 ||
|
||||
cc === 8287 ||
|
||||
cc === 12288 ||
|
||||
cc === 65279
|
||||
);
|
||||
}
|
||||
|
||||
const regex_lang_attribute =
|
||||
/<!--[^]*?-->|<script\s+(?:[^>]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/g;
|
||||
|
||||
export class Parser {
|
||||
/**
|
||||
* @readonly
|
||||
* @type {string}
|
||||
*/
|
||||
template;
|
||||
|
||||
/**
|
||||
* Whether or not we're in loose parsing mode, in which
|
||||
* case we try to continue parsing as much as possible
|
||||
* @type {boolean}
|
||||
*/
|
||||
loose;
|
||||
|
||||
/** */
|
||||
index = 0;
|
||||
|
||||
/**
|
||||
* Creates a minimal parser instance for CSS-only parsing.
|
||||
* Skips Svelte component parsing setup.
|
||||
* @param {string} source
|
||||
* @returns {Parser}
|
||||
*/
|
||||
static forCss(source) {
|
||||
const parser = Object.create(Parser.prototype);
|
||||
parser.template = source;
|
||||
parser.index = 0;
|
||||
parser.loose = false;
|
||||
return parser;
|
||||
}
|
||||
|
||||
/** Whether we're parsing in TypeScript mode */
|
||||
ts = false;
|
||||
|
||||
/** @type {AST.TemplateNode[]} */
|
||||
stack = [];
|
||||
|
||||
/** @type {AST.Fragment[]} */
|
||||
fragments = [];
|
||||
|
||||
/** @type {AST.Root} */
|
||||
root;
|
||||
|
||||
/** @type {Record<string, boolean>} */
|
||||
meta_tags = {};
|
||||
|
||||
/** @type {LastAutoClosedTag | undefined} */
|
||||
last_auto_closed_tag;
|
||||
|
||||
/**
|
||||
* @param {string} template
|
||||
* @param {boolean} loose
|
||||
*/
|
||||
constructor(template, loose) {
|
||||
if (typeof template !== 'string') {
|
||||
throw new TypeError('Template must be a string');
|
||||
}
|
||||
|
||||
this.loose = loose;
|
||||
this.template = template.trimEnd();
|
||||
|
||||
let match_lang;
|
||||
|
||||
do match_lang = regex_lang_attribute.exec(template);
|
||||
while (match_lang && match_lang[0][1] !== 's'); // ensure it starts with '<s' to match script tags
|
||||
|
||||
regex_lang_attribute.lastIndex = 0; // reset matched index to pass tests - otherwise declare the regex inside the constructor
|
||||
|
||||
this.ts = match_lang?.[2] === 'ts';
|
||||
|
||||
this.root = {
|
||||
css: null,
|
||||
js: [],
|
||||
// @ts-ignore
|
||||
start: null,
|
||||
// @ts-ignore
|
||||
end: null,
|
||||
type: 'Root',
|
||||
fragment: create_fragment(),
|
||||
options: null,
|
||||
comments: [],
|
||||
metadata: {
|
||||
ts: this.ts
|
||||
}
|
||||
};
|
||||
|
||||
this.stack.push(this.root);
|
||||
this.fragments.push(this.root.fragment);
|
||||
|
||||
/** @type {ParserState} */
|
||||
let state = fragment;
|
||||
|
||||
while (this.index < this.template.length) {
|
||||
state = state(this) || fragment;
|
||||
}
|
||||
|
||||
if (this.stack.length > 1) {
|
||||
const current = this.current();
|
||||
|
||||
if (this.loose) {
|
||||
current.end = this.template.length;
|
||||
} else if (current.type === 'RegularElement') {
|
||||
current.end = current.start + 1;
|
||||
e.element_unclosed(current, current.name);
|
||||
} else {
|
||||
current.end = current.start + 1;
|
||||
e.block_unclosed(current);
|
||||
}
|
||||
}
|
||||
|
||||
if (state !== fragment) {
|
||||
e.unexpected_eof(this.index);
|
||||
}
|
||||
|
||||
this.root.start = 0;
|
||||
this.root.end = template.length;
|
||||
|
||||
const options_index = this.root.fragment.nodes.findIndex(
|
||||
/** @param {any} thing */
|
||||
(thing) => thing.type === 'SvelteOptions'
|
||||
);
|
||||
if (options_index !== -1) {
|
||||
const options = /** @type {AST.SvelteOptionsRaw} */ (this.root.fragment.nodes[options_index]);
|
||||
this.root.fragment.nodes.splice(options_index, 1);
|
||||
this.root.options = read_options(options);
|
||||
|
||||
disallow_children(options);
|
||||
|
||||
// We need this for the old AST format
|
||||
Object.defineProperty(this.root.options, '__raw__', {
|
||||
value: options,
|
||||
enumerable: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
current() {
|
||||
return this.stack[this.stack.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @param {boolean} required
|
||||
* @param {boolean} required_in_loose
|
||||
*/
|
||||
eat(str, required = false, required_in_loose = true) {
|
||||
if (this.match(str)) {
|
||||
this.index += str.length;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (required && (!this.loose || required_in_loose)) {
|
||||
e.expected_token(this.index, str);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @param {string} str */
|
||||
match(str) {
|
||||
const length = str.length;
|
||||
if (length === 1) {
|
||||
// more performant than slicing
|
||||
return this.template[this.index] === str;
|
||||
}
|
||||
|
||||
return this.template.startsWith(str, this.index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Match a regex at the current index
|
||||
* @param {RegExp} pattern Should have the sticky (`y`) flag so that it only matches at the current index
|
||||
*/
|
||||
match_regex(pattern) {
|
||||
pattern.lastIndex = this.index;
|
||||
const match = pattern.exec(this.template);
|
||||
if (!match || match.index !== this.index) return null;
|
||||
|
||||
return match[0];
|
||||
}
|
||||
|
||||
allow_whitespace() {
|
||||
while (
|
||||
this.index < this.template.length &&
|
||||
is_whitespace(this.template.charCodeAt(this.index))
|
||||
) {
|
||||
this.index++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for a regex starting at the current index and return the result if it matches
|
||||
* @param {RegExp} pattern Should have a ^ anchor at the start so the regex doesn't search past the beginning, resulting in worse performance
|
||||
*/
|
||||
read(pattern) {
|
||||
const result = this.match_regex(pattern);
|
||||
if (result) this.index += result.length;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ESTree.Identifier & { start: number, end: number, loc: { start: Location, end: Location } }}
|
||||
*/
|
||||
read_identifier() {
|
||||
const start = this.index;
|
||||
let end = start;
|
||||
let name = '';
|
||||
|
||||
const code = /** @type {number} */ (this.template.codePointAt(this.index));
|
||||
|
||||
if (isIdentifierStart(code, true)) {
|
||||
let i = this.index;
|
||||
end += code <= 0xffff ? 1 : 2;
|
||||
|
||||
while (end < this.template.length) {
|
||||
const code = /** @type {number} */ (this.template.codePointAt(end));
|
||||
|
||||
if (!isIdentifierChar(code, true)) break;
|
||||
end += code <= 0xffff ? 1 : 2;
|
||||
}
|
||||
|
||||
name = this.template.slice(start, end);
|
||||
this.index = end;
|
||||
|
||||
if (is_reserved(name)) {
|
||||
e.unexpected_reserved_word(start, name);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'Identifier',
|
||||
name,
|
||||
start,
|
||||
end,
|
||||
loc: {
|
||||
start: state.locator(start),
|
||||
end: state.locator(end)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {RegExp} pattern */
|
||||
read_until(pattern) {
|
||||
if (this.index >= this.template.length) {
|
||||
if (this.loose) return '';
|
||||
e.unexpected_eof(this.template.length);
|
||||
}
|
||||
|
||||
const start = this.index;
|
||||
const match = pattern.exec(this.template.slice(start));
|
||||
|
||||
if (match) {
|
||||
this.index = start + match.index;
|
||||
return this.template.slice(start, this.index);
|
||||
}
|
||||
|
||||
this.index = this.template.length;
|
||||
return this.template.slice(start);
|
||||
}
|
||||
|
||||
require_whitespace() {
|
||||
if (!is_whitespace(this.template.charCodeAt(this.index))) {
|
||||
e.expected_whitespace(this.index);
|
||||
}
|
||||
|
||||
this.allow_whitespace();
|
||||
}
|
||||
|
||||
pop() {
|
||||
this.fragments.pop();
|
||||
return this.stack.pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {AST.Fragment['nodes'][number]} T
|
||||
* @param {T} node
|
||||
* @returns {T}
|
||||
*/
|
||||
append(node) {
|
||||
this.fragments.at(-1)?.nodes.push(node);
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} template
|
||||
* @param {boolean} [loose]
|
||||
* @returns {AST.Root}
|
||||
*/
|
||||
export function parse(template, loose = false) {
|
||||
state.set_source(template);
|
||||
|
||||
const parser = new Parser(template, loose);
|
||||
return parser.root;
|
||||
}
|
||||
|
||||
/** @typedef {(parser: Parser) => ParserState | void} ParserState */
|
||||
|
||||
/** @typedef {Object} LastAutoClosedTag
|
||||
* @property {string} tag
|
||||
* @property {string} reason
|
||||
* @property {number} depth
|
||||
*/
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
/** @import { Pattern } from 'estree' */
|
||||
/** @import { Parser } from '../index.js' */
|
||||
import { match_bracket } from '../utils/bracket.js';
|
||||
import { parse_expression_at, remove_parens } from '../acorn.js';
|
||||
import { regex_not_newline_characters } from '../../patterns.js';
|
||||
import * as e from '../../../errors.js';
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export default function read_pattern(parser) {
|
||||
const start = parser.index;
|
||||
let i = parser.index;
|
||||
|
||||
const id = parser.read_identifier();
|
||||
|
||||
if (id.name !== '') {
|
||||
const annotation = read_type_annotation(parser);
|
||||
|
||||
return {
|
||||
...id,
|
||||
typeAnnotation: annotation
|
||||
};
|
||||
}
|
||||
|
||||
const char = parser.template[i];
|
||||
|
||||
if (char !== '{' && char !== '[') {
|
||||
e.expected_pattern(i);
|
||||
}
|
||||
|
||||
i = match_bracket(parser, start);
|
||||
parser.index = i;
|
||||
|
||||
const pattern_string = parser.template.slice(start, i);
|
||||
|
||||
// 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);
|
||||
|
||||
/** @type {any} */
|
||||
let expression = remove_parens(
|
||||
parse_expression_at(parser, `${space_with_newline}(${pattern_string} = 1)`, start - 1)
|
||||
);
|
||||
|
||||
expression = expression.left;
|
||||
|
||||
expression.typeAnnotation = read_type_annotation(parser);
|
||||
if (expression.typeAnnotation) {
|
||||
expression.end = expression.typeAnnotation.end;
|
||||
}
|
||||
|
||||
return expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 = remove_parens(parse_expression_at(parser, template, a));
|
||||
|
||||
// `foo: bar = baz` gets mangled — fix it
|
||||
if (expression.type === 'AssignmentExpression') {
|
||||
let b = expression.right.start;
|
||||
while (template[b] !== '=') b -= 1;
|
||||
expression = remove_parens(parse_expression_at(parser, template.slice(0, b), 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
|
||||
};
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/** @import { Expression } from 'estree' */
|
||||
/** @import { Parser } from '../index.js' */
|
||||
import { parse_expression_at, remove_parens } 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, parser.template, parser.index);
|
||||
|
||||
let index = /** @type {number} */ (node.end);
|
||||
|
||||
const last_comment = parser.root.comments.at(-1);
|
||||
if (last_comment && last_comment.end > index) index = last_comment.end;
|
||||
|
||||
parser.index = index;
|
||||
|
||||
return /** @type {Expression} */ (remove_parens(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;
|
||||
}
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
/** @import { ObjectExpression } from 'estree' */
|
||||
/** @import { AST } from '#compiler' */
|
||||
import { NAMESPACE_MATHML, NAMESPACE_SVG } from '../../../../constants.js';
|
||||
import * as e from '../../../errors.js';
|
||||
|
||||
/**
|
||||
* @param {AST.SvelteOptionsRaw} node
|
||||
* @returns {AST.Root['options']}
|
||||
*/
|
||||
export default function read_options(node) {
|
||||
/** @type {AST.SvelteOptions} */
|
||||
const component_options = {
|
||||
start: node.start,
|
||||
end: node.end,
|
||||
// @ts-ignore
|
||||
attributes: node.attributes
|
||||
};
|
||||
|
||||
if (!node) {
|
||||
return component_options;
|
||||
}
|
||||
|
||||
for (const attribute of node.attributes) {
|
||||
if (attribute.type !== 'Attribute') {
|
||||
e.svelte_options_invalid_attribute(attribute);
|
||||
}
|
||||
|
||||
const { name } = attribute;
|
||||
|
||||
switch (name) {
|
||||
case 'runes': {
|
||||
component_options.runes = get_boolean_value(attribute);
|
||||
break;
|
||||
}
|
||||
case 'tag': {
|
||||
e.svelte_options_deprecated_tag(attribute);
|
||||
break; // eslint doesn't know this is unnecessary
|
||||
}
|
||||
case 'customElement': {
|
||||
/** @type {AST.SvelteOptions['customElement']} */
|
||||
const ce = {};
|
||||
const { value: v } = attribute;
|
||||
const value = v === true || Array.isArray(v) ? v : [v];
|
||||
|
||||
if (value === true) {
|
||||
e.svelte_options_invalid_customelement(attribute);
|
||||
} else if (value[0].type === 'Text') {
|
||||
const tag = get_static_value(attribute);
|
||||
validate_tag(attribute, tag);
|
||||
ce.tag = tag;
|
||||
component_options.customElement = ce;
|
||||
break;
|
||||
} else if (value[0].expression.type !== 'ObjectExpression') {
|
||||
// Before Svelte 4 it was necessary to explicitly set customElement to null or else you'd get a warning.
|
||||
// This is no longer necessary, but for backwards compat just skip in this case now.
|
||||
if (value[0].expression.type === 'Literal' && value[0].expression.value === null) {
|
||||
break;
|
||||
}
|
||||
e.svelte_options_invalid_customelement(attribute);
|
||||
}
|
||||
|
||||
/** @type {Array<[string, any]>} */
|
||||
const properties = [];
|
||||
for (const property of value[0].expression.properties) {
|
||||
if (
|
||||
property.type !== 'Property' ||
|
||||
property.computed ||
|
||||
property.key.type !== 'Identifier'
|
||||
) {
|
||||
e.svelte_options_invalid_customelement(attribute);
|
||||
}
|
||||
properties.push([property.key.name, property.value]);
|
||||
}
|
||||
|
||||
const tag = properties.find(([name]) => name === 'tag');
|
||||
if (tag) {
|
||||
const tag_value = tag[1]?.value;
|
||||
validate_tag(tag, tag_value);
|
||||
ce.tag = tag_value;
|
||||
}
|
||||
|
||||
const props = properties.find(([name]) => name === 'props')?.[1];
|
||||
if (props) {
|
||||
if (props.type !== 'ObjectExpression') {
|
||||
e.svelte_options_invalid_customelement_props(attribute);
|
||||
}
|
||||
ce.props = {};
|
||||
for (const property of /** @type {ObjectExpression} */ (props).properties) {
|
||||
if (
|
||||
property.type !== 'Property' ||
|
||||
property.computed ||
|
||||
property.key.type !== 'Identifier' ||
|
||||
property.value.type !== 'ObjectExpression'
|
||||
) {
|
||||
e.svelte_options_invalid_customelement_props(attribute);
|
||||
}
|
||||
ce.props[property.key.name] = {};
|
||||
for (const prop of property.value.properties) {
|
||||
if (
|
||||
prop.type !== 'Property' ||
|
||||
prop.computed ||
|
||||
prop.key.type !== 'Identifier' ||
|
||||
prop.value.type !== 'Literal'
|
||||
) {
|
||||
e.svelte_options_invalid_customelement_props(attribute);
|
||||
}
|
||||
|
||||
if (prop.key.name === 'type') {
|
||||
if (
|
||||
['String', 'Number', 'Boolean', 'Array', 'Object'].indexOf(
|
||||
/** @type {string} */ (prop.value.value)
|
||||
) === -1
|
||||
) {
|
||||
e.svelte_options_invalid_customelement_props(attribute);
|
||||
}
|
||||
ce.props[property.key.name].type = /** @type {any} */ (prop.value.value);
|
||||
} else if (prop.key.name === 'reflect') {
|
||||
if (typeof prop.value.value !== 'boolean') {
|
||||
e.svelte_options_invalid_customelement_props(attribute);
|
||||
}
|
||||
ce.props[property.key.name].reflect = prop.value.value;
|
||||
} else if (prop.key.name === 'attribute') {
|
||||
if (typeof prop.value.value !== 'string') {
|
||||
e.svelte_options_invalid_customelement_props(attribute);
|
||||
}
|
||||
ce.props[property.key.name].attribute = prop.value.value;
|
||||
} else {
|
||||
e.svelte_options_invalid_customelement_props(attribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const shadow = properties.find(([name]) => name === 'shadow')?.[1];
|
||||
if (shadow) {
|
||||
if (shadow.type === 'Literal' && (shadow.value === 'open' || shadow.value === 'none')) {
|
||||
ce.shadow = shadow.value;
|
||||
} else if (shadow.type === 'ObjectExpression') {
|
||||
ce.shadow = shadow;
|
||||
} else {
|
||||
e.svelte_options_invalid_customelement_shadow(attribute);
|
||||
}
|
||||
}
|
||||
|
||||
const extend = properties.find(([name]) => name === 'extend')?.[1];
|
||||
if (extend) {
|
||||
ce.extend = extend;
|
||||
}
|
||||
|
||||
component_options.customElement = ce;
|
||||
break;
|
||||
}
|
||||
case 'namespace': {
|
||||
const value = get_static_value(attribute);
|
||||
|
||||
if (value === NAMESPACE_SVG) {
|
||||
component_options.namespace = 'svg';
|
||||
} else if (value === NAMESPACE_MATHML) {
|
||||
component_options.namespace = 'mathml';
|
||||
} else if (value === 'html' || value === 'mathml' || value === 'svg') {
|
||||
component_options.namespace = value;
|
||||
} else {
|
||||
e.svelte_options_invalid_attribute_value(attribute, `"html", "mathml" or "svg"`);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'css': {
|
||||
const value = get_static_value(attribute);
|
||||
|
||||
if (value === 'injected') {
|
||||
component_options.css = value;
|
||||
} else {
|
||||
e.svelte_options_invalid_attribute_value(attribute, `"injected"`);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'immutable': {
|
||||
component_options.immutable = get_boolean_value(attribute);
|
||||
break;
|
||||
}
|
||||
case 'preserveWhitespace': {
|
||||
component_options.preserveWhitespace = get_boolean_value(attribute);
|
||||
break;
|
||||
}
|
||||
case 'accessors': {
|
||||
component_options.accessors = get_boolean_value(attribute);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
e.svelte_options_unknown_attribute(attribute, name);
|
||||
}
|
||||
}
|
||||
|
||||
return component_options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} attribute
|
||||
*/
|
||||
function get_static_value(attribute) {
|
||||
const { value } = attribute;
|
||||
|
||||
if (value === true) return true;
|
||||
|
||||
const chunk = Array.isArray(value) ? value[0] : value;
|
||||
|
||||
if (!chunk) return true;
|
||||
if (value.length > 1) {
|
||||
return null;
|
||||
}
|
||||
if (chunk.type === 'Text') return chunk.data;
|
||||
if (chunk.expression.type !== 'Literal') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return chunk.expression.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} attribute
|
||||
*/
|
||||
function get_boolean_value(attribute) {
|
||||
const value = get_static_value(attribute);
|
||||
if (typeof value !== 'boolean') {
|
||||
e.svelte_options_invalid_attribute_value(attribute, 'true or false');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
|
||||
const tag_name_char =
|
||||
'[a-z0-9_.\xB7\xC0-\xD6\xD8-\xF6\xF8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u{10000}-\u{EFFFF}-]';
|
||||
const regex_valid_tag_name = new RegExp(`^[a-z]${tag_name_char}*-${tag_name_char}*$`, 'u');
|
||||
const reserved_tag_names = [
|
||||
'annotation-xml',
|
||||
'color-profile',
|
||||
'font-face',
|
||||
'font-face-src',
|
||||
'font-face-uri',
|
||||
'font-face-format',
|
||||
'font-face-name',
|
||||
'missing-glyph'
|
||||
];
|
||||
|
||||
/**
|
||||
* @param {any} attribute
|
||||
* @param {string | null} tag
|
||||
* @returns {asserts tag is string}
|
||||
*/
|
||||
function validate_tag(attribute, tag) {
|
||||
if (typeof tag !== 'string') {
|
||||
e.svelte_options_invalid_tagname(attribute);
|
||||
}
|
||||
if (tag) {
|
||||
if (!regex_valid_tag_name.test(tag)) {
|
||||
e.svelte_options_invalid_tagname(attribute);
|
||||
} else if (reserved_tag_names.includes(tag)) {
|
||||
e.svelte_options_reserved_tagname(attribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
+90
@@ -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';
|
||||
import { locator } from '../../../state.js';
|
||||
|
||||
const regex_closing_script_tag = /<\/script\s*>/;
|
||||
const regex_starts_with_closing_script_tag = /<\/script\s*>/y;
|
||||
|
||||
const RESERVED_ATTRIBUTES = ['server', 'client', 'worker', 'test', 'default'];
|
||||
const ALLOWED_ATTRIBUTES = ['context', 'generics', 'lang', 'module'];
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {number} start
|
||||
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag>} attributes
|
||||
* @returns {AST.Script}
|
||||
*/
|
||||
export function read_script(parser, start, attributes) {
|
||||
const script_start = parser.index;
|
||||
const data = parser.read_until(regex_closing_script_tag);
|
||||
if (parser.index >= parser.template.length) {
|
||||
e.element_unclosed(parser.template.length, 'script');
|
||||
}
|
||||
|
||||
const source =
|
||||
parser.template.slice(0, script_start).replace(regex_not_newline_characters, ' ') + data;
|
||||
parser.read(regex_starts_with_closing_script_tag);
|
||||
|
||||
const ast = acorn.parse(source, parser.root.comments, parser.ts, true);
|
||||
|
||||
ast.start = script_start;
|
||||
|
||||
if (ast.loc) {
|
||||
// Acorn always uses `0` as the start of a `Program`, but for sourcemap purposes
|
||||
// we need it to be the start of the `<script>` contents
|
||||
({ line: ast.loc.start.line, column: ast.loc.start.column } = locator(start));
|
||||
({ line: ast.loc.end.line, column: ast.loc.end.column } = locator(parser.index));
|
||||
}
|
||||
|
||||
/** @type {'default' | 'module'} */
|
||||
let context = 'default';
|
||||
|
||||
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
|
||||
if (RESERVED_ATTRIBUTES.includes(attribute.name)) {
|
||||
e.script_reserved_attribute(attribute, attribute.name);
|
||||
}
|
||||
|
||||
if (!ALLOWED_ATTRIBUTES.includes(attribute.name)) {
|
||||
w.script_unknown_attribute(attribute);
|
||||
}
|
||||
|
||||
if (attribute.name === 'module') {
|
||||
if (attribute.value !== true) {
|
||||
// Deliberately a generic code to future-proof for potential other attributes
|
||||
e.script_invalid_attribute_value(attribute, attribute.name);
|
||||
}
|
||||
|
||||
context = 'module';
|
||||
}
|
||||
|
||||
if (attribute.name === 'context') {
|
||||
if (attribute.value === true || !is_text_attribute(attribute)) {
|
||||
e.script_invalid_context(attribute);
|
||||
}
|
||||
|
||||
const value = attribute.value[0].data;
|
||||
|
||||
if (value !== 'module') {
|
||||
e.script_invalid_context(attribute);
|
||||
}
|
||||
|
||||
context = 'module';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'Script',
|
||||
start,
|
||||
end: parser.index,
|
||||
context,
|
||||
content: ast,
|
||||
// @ts-ignore
|
||||
attributes
|
||||
};
|
||||
}
|
||||
+637
@@ -0,0 +1,637 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Parser } from '../index.js' */
|
||||
import * as e from '../../../errors.js';
|
||||
|
||||
const REGEX_MATCHER = /[~^$*|]?=/y;
|
||||
const REGEX_CLOSING_BRACKET = /[\s\]]/;
|
||||
const REGEX_ATTRIBUTE_FLAGS = /[a-zA-Z]+/y; // only `i` and `s` are valid today, but make it future-proof
|
||||
const REGEX_COMBINATOR = /(\+|~|>|\|\|)/y;
|
||||
const REGEX_PERCENTAGE = /\d+(\.\d+)?%/y;
|
||||
const REGEX_NTH_OF =
|
||||
/(even|odd|\+?(\d+|\d*n(\s*[+-]\s*\d+)?)|-\d*n(\s*\+\s*\d+))((?=\s*[,)])|\s+of\s+)/y;
|
||||
const REGEX_WHITESPACE_OR_COLON = /[\s:]/;
|
||||
const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/y;
|
||||
const REGEX_VALID_IDENTIFIER_CHAR = /[a-zA-Z0-9_-]/;
|
||||
const REGEX_UNICODE_SEQUENCE = /\\[0-9a-fA-F]{1,6}(\r\n|\s)?/y;
|
||||
const REGEX_COMMENT_CLOSE = /\*\//;
|
||||
const REGEX_HTML_COMMENT_CLOSE = /-->/;
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {number} start
|
||||
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag>} attributes
|
||||
* @returns {AST.CSS.StyleSheet}
|
||||
*/
|
||||
export default function read_style(parser, start, attributes) {
|
||||
const content_start = parser.index;
|
||||
const children = read_body(parser, (p) => p.match('</style') || p.index >= p.template.length);
|
||||
const content_end = parser.index;
|
||||
|
||||
parser.eat('</style', true);
|
||||
parser.read(/\s*>/y);
|
||||
|
||||
return {
|
||||
type: 'StyleSheet',
|
||||
start,
|
||||
end: parser.index,
|
||||
attributes,
|
||||
children,
|
||||
content: {
|
||||
start: content_start,
|
||||
end: content_end,
|
||||
styles: parser.template.slice(content_start, content_end),
|
||||
comment: null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {(parser: Parser) => boolean} finished
|
||||
* @returns {Array<AST.CSS.Rule | AST.CSS.Atrule>}
|
||||
*/
|
||||
function read_body(parser, finished) {
|
||||
/** @type {Array<AST.CSS.Rule | AST.CSS.Atrule>} */
|
||||
const children = [];
|
||||
|
||||
while ((allow_comment_or_whitespace(parser), !finished(parser))) {
|
||||
if (parser.match('@')) {
|
||||
children.push(read_at_rule(parser));
|
||||
} else {
|
||||
children.push(read_rule(parser));
|
||||
}
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {AST.CSS.Atrule}
|
||||
*/
|
||||
function read_at_rule(parser) {
|
||||
const start = parser.index;
|
||||
parser.eat('@', true);
|
||||
|
||||
const name = read_identifier(parser);
|
||||
|
||||
const prelude = read_value(parser);
|
||||
|
||||
/** @type {AST.CSS.Block | null} */
|
||||
let block = null;
|
||||
|
||||
if (parser.match('{')) {
|
||||
// e.g. `@media (...) {...}`
|
||||
block = read_block(parser);
|
||||
} else {
|
||||
// e.g. `@import '...'`
|
||||
parser.eat(';', true);
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'Atrule',
|
||||
start,
|
||||
end: parser.index,
|
||||
name,
|
||||
prelude,
|
||||
block
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {AST.CSS.Rule}
|
||||
*/
|
||||
function read_rule(parser) {
|
||||
const start = parser.index;
|
||||
|
||||
return {
|
||||
type: 'Rule',
|
||||
prelude: read_selector_list(parser),
|
||||
block: read_block(parser),
|
||||
start,
|
||||
end: parser.index,
|
||||
metadata: {
|
||||
parent_rule: null,
|
||||
has_local_selectors: false,
|
||||
has_global_selectors: false,
|
||||
is_global_block: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {boolean} [inside_pseudo_class]
|
||||
* @returns {AST.CSS.SelectorList}
|
||||
*/
|
||||
function read_selector_list(parser, inside_pseudo_class = false) {
|
||||
/** @type {AST.CSS.ComplexSelector[]} */
|
||||
const children = [];
|
||||
|
||||
allow_comment_or_whitespace(parser);
|
||||
|
||||
const start = parser.index;
|
||||
|
||||
while (parser.index < parser.template.length) {
|
||||
children.push(read_selector(parser, inside_pseudo_class));
|
||||
|
||||
const end = parser.index;
|
||||
|
||||
allow_comment_or_whitespace(parser);
|
||||
|
||||
if (inside_pseudo_class ? parser.match(')') : parser.match('{')) {
|
||||
return {
|
||||
type: 'SelectorList',
|
||||
start,
|
||||
end,
|
||||
children
|
||||
};
|
||||
} else {
|
||||
parser.eat(',', true);
|
||||
allow_comment_or_whitespace(parser);
|
||||
}
|
||||
}
|
||||
|
||||
e.unexpected_eof(parser.template.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {boolean} [inside_pseudo_class]
|
||||
* @returns {AST.CSS.ComplexSelector}
|
||||
*/
|
||||
function read_selector(parser, inside_pseudo_class = false) {
|
||||
const list_start = parser.index;
|
||||
|
||||
/** @type {AST.CSS.RelativeSelector[]} */
|
||||
const children = [];
|
||||
|
||||
/**
|
||||
* @param {AST.CSS.Combinator | null} combinator
|
||||
* @param {number} start
|
||||
* @returns {AST.CSS.RelativeSelector}
|
||||
*/
|
||||
function create_selector(combinator, start) {
|
||||
return {
|
||||
type: 'RelativeSelector',
|
||||
combinator,
|
||||
selectors: [],
|
||||
start,
|
||||
end: -1,
|
||||
metadata: {
|
||||
is_global: false,
|
||||
is_global_like: false,
|
||||
scoped: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** @type {AST.CSS.RelativeSelector} */
|
||||
let relative_selector = create_selector(null, parser.index);
|
||||
|
||||
while (parser.index < parser.template.length) {
|
||||
let start = parser.index;
|
||||
|
||||
if (parser.eat('&')) {
|
||||
relative_selector.selectors.push({
|
||||
type: 'NestingSelector',
|
||||
name: '&',
|
||||
start,
|
||||
end: parser.index
|
||||
});
|
||||
} else if (parser.eat('*')) {
|
||||
let name = '*';
|
||||
|
||||
if (parser.eat('|')) {
|
||||
// * is the namespace (which we ignore)
|
||||
name = read_identifier(parser);
|
||||
}
|
||||
|
||||
relative_selector.selectors.push({
|
||||
type: 'TypeSelector',
|
||||
name,
|
||||
start,
|
||||
end: parser.index
|
||||
});
|
||||
} else if (parser.eat('#')) {
|
||||
relative_selector.selectors.push({
|
||||
type: 'IdSelector',
|
||||
name: read_identifier(parser),
|
||||
start,
|
||||
end: parser.index
|
||||
});
|
||||
} else if (parser.eat('.')) {
|
||||
relative_selector.selectors.push({
|
||||
type: 'ClassSelector',
|
||||
name: read_identifier(parser),
|
||||
start,
|
||||
end: parser.index
|
||||
});
|
||||
} else if (parser.eat('::')) {
|
||||
relative_selector.selectors.push({
|
||||
type: 'PseudoElementSelector',
|
||||
name: read_identifier(parser),
|
||||
start,
|
||||
end: parser.index
|
||||
});
|
||||
// We read the inner selectors of a pseudo element to ensure it parses correctly,
|
||||
// but we don't do anything with the result.
|
||||
if (parser.eat('(')) {
|
||||
read_selector_list(parser, true);
|
||||
parser.eat(')', true);
|
||||
}
|
||||
} else if (parser.eat(':')) {
|
||||
const name = read_identifier(parser);
|
||||
|
||||
/** @type {null | AST.CSS.SelectorList} */
|
||||
let args = null;
|
||||
|
||||
if (parser.eat('(')) {
|
||||
args = read_selector_list(parser, true);
|
||||
parser.eat(')', true);
|
||||
}
|
||||
|
||||
relative_selector.selectors.push({
|
||||
type: 'PseudoClassSelector',
|
||||
name,
|
||||
args,
|
||||
start,
|
||||
end: parser.index
|
||||
});
|
||||
} else if (parser.eat('[')) {
|
||||
parser.allow_whitespace();
|
||||
const name = read_identifier(parser);
|
||||
parser.allow_whitespace();
|
||||
|
||||
/** @type {string | null} */
|
||||
let value = null;
|
||||
|
||||
const matcher = parser.read(REGEX_MATCHER);
|
||||
|
||||
if (matcher) {
|
||||
parser.allow_whitespace();
|
||||
value = read_attribute_value(parser);
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
const flags = parser.read(REGEX_ATTRIBUTE_FLAGS);
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat(']', true);
|
||||
|
||||
relative_selector.selectors.push({
|
||||
type: 'AttributeSelector',
|
||||
start,
|
||||
end: parser.index,
|
||||
name,
|
||||
matcher,
|
||||
value,
|
||||
flags
|
||||
});
|
||||
} else if (inside_pseudo_class && parser.match_regex(REGEX_NTH_OF)) {
|
||||
// nth of matcher must come before combinator matcher to prevent collision else the '+' in '+2n-1' would be parsed as a combinator
|
||||
|
||||
relative_selector.selectors.push({
|
||||
type: 'Nth',
|
||||
value: /**@type {string} */ (parser.read(REGEX_NTH_OF)),
|
||||
start,
|
||||
end: parser.index
|
||||
});
|
||||
} else if (parser.match_regex(REGEX_PERCENTAGE)) {
|
||||
relative_selector.selectors.push({
|
||||
type: 'Percentage',
|
||||
value: /** @type {string} */ (parser.read(REGEX_PERCENTAGE)),
|
||||
start,
|
||||
end: parser.index
|
||||
});
|
||||
} else if (!parser.match_regex(REGEX_COMBINATOR)) {
|
||||
let name = read_identifier(parser);
|
||||
|
||||
if (parser.eat('|')) {
|
||||
// we ignore the namespace when trying to find matching element classes
|
||||
name = read_identifier(parser);
|
||||
}
|
||||
|
||||
relative_selector.selectors.push({
|
||||
type: 'TypeSelector',
|
||||
name,
|
||||
start,
|
||||
end: parser.index
|
||||
});
|
||||
}
|
||||
|
||||
const index = parser.index;
|
||||
allow_comment_or_whitespace(parser);
|
||||
|
||||
if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) {
|
||||
// rewind, so we know whether to continue building the selector list
|
||||
parser.index = index;
|
||||
|
||||
relative_selector.end = index;
|
||||
children.push(relative_selector);
|
||||
|
||||
return {
|
||||
type: 'ComplexSelector',
|
||||
start: list_start,
|
||||
end: index,
|
||||
children,
|
||||
metadata: {
|
||||
rule: null,
|
||||
is_global: false,
|
||||
used: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
parser.index = index;
|
||||
const combinator = read_combinator(parser);
|
||||
|
||||
if (combinator) {
|
||||
if (relative_selector.selectors.length > 0) {
|
||||
relative_selector.end = index;
|
||||
children.push(relative_selector);
|
||||
}
|
||||
|
||||
// ...and start a new one
|
||||
relative_selector = create_selector(combinator, combinator.start);
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) {
|
||||
e.css_selector_invalid(parser.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
e.unexpected_eof(parser.template.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {AST.CSS.Combinator | null}
|
||||
*/
|
||||
function read_combinator(parser) {
|
||||
const start = parser.index;
|
||||
parser.allow_whitespace();
|
||||
|
||||
const index = parser.index;
|
||||
const name = parser.read(REGEX_COMBINATOR);
|
||||
|
||||
if (name) {
|
||||
const end = parser.index;
|
||||
parser.allow_whitespace();
|
||||
|
||||
return {
|
||||
type: 'Combinator',
|
||||
name,
|
||||
start: index,
|
||||
end
|
||||
};
|
||||
}
|
||||
|
||||
if (parser.index !== start) {
|
||||
return {
|
||||
type: 'Combinator',
|
||||
name: ' ',
|
||||
start,
|
||||
end: parser.index
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {AST.CSS.Block}
|
||||
*/
|
||||
function read_block(parser) {
|
||||
const start = parser.index;
|
||||
|
||||
parser.eat('{', true);
|
||||
|
||||
/** @type {Array<AST.CSS.Declaration | AST.CSS.Rule | AST.CSS.Atrule>} */
|
||||
const children = [];
|
||||
|
||||
while (parser.index < parser.template.length) {
|
||||
allow_comment_or_whitespace(parser);
|
||||
|
||||
if (parser.match('}')) {
|
||||
break;
|
||||
} else {
|
||||
children.push(read_block_item(parser));
|
||||
}
|
||||
}
|
||||
|
||||
parser.eat('}', true);
|
||||
|
||||
return {
|
||||
type: 'Block',
|
||||
start,
|
||||
end: parser.index,
|
||||
children
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a declaration, rule or at-rule
|
||||
*
|
||||
* @param {Parser} parser
|
||||
* @returns {AST.CSS.Declaration | AST.CSS.Rule | AST.CSS.Atrule}
|
||||
*/
|
||||
function read_block_item(parser) {
|
||||
if (parser.match('@')) {
|
||||
return read_at_rule(parser);
|
||||
}
|
||||
|
||||
// read ahead to understand whether we're dealing with a declaration or a nested rule.
|
||||
// this involves some duplicated work, but avoids a try-catch that would disguise errors
|
||||
const start = parser.index;
|
||||
read_value(parser);
|
||||
const char = parser.template[parser.index];
|
||||
parser.index = start;
|
||||
|
||||
return char === '{' ? read_rule(parser) : read_declaration(parser);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {AST.CSS.Declaration}
|
||||
*/
|
||||
function read_declaration(parser) {
|
||||
const start = parser.index;
|
||||
|
||||
const property = parser.read_until(REGEX_WHITESPACE_OR_COLON);
|
||||
parser.allow_whitespace();
|
||||
parser.eat(':');
|
||||
let index = parser.index;
|
||||
parser.allow_whitespace();
|
||||
|
||||
const value = read_value(parser);
|
||||
|
||||
if (!value && !property.startsWith('--')) {
|
||||
e.css_empty_declaration({ start, end: index });
|
||||
}
|
||||
|
||||
const end = parser.index;
|
||||
|
||||
if (!parser.match('}')) {
|
||||
parser.eat(';', true);
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'Declaration',
|
||||
start,
|
||||
end,
|
||||
property,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {string}
|
||||
*/
|
||||
function read_value(parser) {
|
||||
let value = '';
|
||||
let escaped = false;
|
||||
let in_url = false;
|
||||
|
||||
/** @type {null | '"' | "'"} */
|
||||
let quote_mark = null;
|
||||
|
||||
while (parser.index < parser.template.length) {
|
||||
const char = parser.template[parser.index];
|
||||
|
||||
if (escaped) {
|
||||
value += '\\' + char;
|
||||
escaped = false;
|
||||
parser.index++;
|
||||
continue;
|
||||
} else if (char === '\\') {
|
||||
escaped = true;
|
||||
parser.index++;
|
||||
continue;
|
||||
} else if (char === quote_mark) {
|
||||
quote_mark = null;
|
||||
} else if (char === ')') {
|
||||
in_url = false;
|
||||
} else if (quote_mark === null && (char === '"' || char === "'")) {
|
||||
quote_mark = char;
|
||||
} else if (char === '(' && value.slice(-3) === 'url') {
|
||||
in_url = true;
|
||||
} else if ((char === ';' || char === '{' || char === '}') && !in_url && !quote_mark) {
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
value += char;
|
||||
|
||||
parser.index++;
|
||||
}
|
||||
|
||||
e.unexpected_eof(parser.template.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a property that may or may not be quoted, e.g.
|
||||
* `foo` or `'foo bar'` or `"foo bar"`
|
||||
* @param {Parser} parser
|
||||
*/
|
||||
function read_attribute_value(parser) {
|
||||
let value = '';
|
||||
let escaped = false;
|
||||
const quote_mark = parser.eat('"') ? '"' : parser.eat("'") ? "'" : null;
|
||||
|
||||
while (parser.index < parser.template.length) {
|
||||
const char = parser.template[parser.index];
|
||||
if (escaped) {
|
||||
value += '\\' + char;
|
||||
escaped = false;
|
||||
} else if (char === '\\') {
|
||||
escaped = true;
|
||||
} else if (quote_mark ? char === quote_mark : REGEX_CLOSING_BRACKET.test(char)) {
|
||||
if (quote_mark) {
|
||||
parser.eat(quote_mark, true);
|
||||
}
|
||||
|
||||
return value.trim();
|
||||
} else {
|
||||
value += char;
|
||||
}
|
||||
|
||||
parser.index++;
|
||||
}
|
||||
|
||||
e.unexpected_eof(parser.template.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see {@link https://www.w3.org/TR/css-syntax-3/#ident-token-diagram CSS Syntax Module Level 3}
|
||||
* @param {Parser} parser
|
||||
*/
|
||||
function read_identifier(parser) {
|
||||
const start = parser.index;
|
||||
|
||||
let identifier = '';
|
||||
|
||||
if (parser.match_regex(REGEX_LEADING_HYPHEN_OR_DIGIT)) {
|
||||
e.css_expected_identifier(start);
|
||||
}
|
||||
|
||||
while (parser.index < parser.template.length) {
|
||||
const char = parser.template[parser.index];
|
||||
if (char === '\\') {
|
||||
const sequence = parser.match_regex(REGEX_UNICODE_SEQUENCE);
|
||||
if (sequence) {
|
||||
identifier += String.fromCodePoint(parseInt(sequence.slice(1), 16));
|
||||
parser.index += sequence.length;
|
||||
} else {
|
||||
identifier += '\\' + parser.template[parser.index + 1];
|
||||
parser.index += 2;
|
||||
}
|
||||
} else if (
|
||||
/** @type {number} */ (char.codePointAt(0)) >= 160 ||
|
||||
REGEX_VALID_IDENTIFIER_CHAR.test(char)
|
||||
) {
|
||||
identifier += char;
|
||||
parser.index++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (identifier === '') {
|
||||
e.css_expected_identifier(start);
|
||||
}
|
||||
|
||||
return identifier;
|
||||
}
|
||||
|
||||
/** @param {Parser} parser */
|
||||
function allow_comment_or_whitespace(parser) {
|
||||
parser.allow_whitespace();
|
||||
while (parser.match('/*') || parser.match('<!--')) {
|
||||
if (parser.eat('/*')) {
|
||||
parser.read_until(REGEX_COMMENT_CLOSE);
|
||||
parser.eat('*/', true);
|
||||
}
|
||||
|
||||
if (parser.eat('<!--')) {
|
||||
parser.read_until(REGEX_HTML_COMMENT_CLOSE);
|
||||
parser.eat('-->', true);
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse standalone CSS content (not wrapped in `<style>`).
|
||||
* @param {Parser} parser
|
||||
* @returns {Array<AST.CSS.Rule | AST.CSS.Atrule>}
|
||||
*/
|
||||
export function parse_stylesheet(parser) {
|
||||
return read_body(parser, (p) => p.index >= p.template.length);
|
||||
}
|
||||
Generated
Vendored
+182
@@ -0,0 +1,182 @@
|
||||
/** @import { Context, Visitors } from 'zimmerframe' */
|
||||
/** @import { FunctionExpression, FunctionDeclaration } from 'estree' */
|
||||
import { walk } from 'zimmerframe';
|
||||
import * as b from '#compiler/builders';
|
||||
import * as e from '../../errors.js';
|
||||
|
||||
/**
|
||||
* @param {FunctionExpression | FunctionDeclaration} node
|
||||
* @param {Context<any, any>} context
|
||||
*/
|
||||
function remove_this_param(node, context) {
|
||||
if (node.params[0]?.type === 'Identifier' && node.params[0].name === 'this') {
|
||||
node.params.shift();
|
||||
}
|
||||
return context.next();
|
||||
}
|
||||
|
||||
/** @type {Visitors<any, null>} */
|
||||
const visitors = {
|
||||
_(node, context) {
|
||||
const n = context.next() ?? node;
|
||||
|
||||
// TODO there may come a time when we decide to preserve type annotations.
|
||||
// until that day comes, we just delete them so they don't confuse esrap
|
||||
delete n.typeAnnotation;
|
||||
delete n.typeParameters;
|
||||
delete n.typeArguments;
|
||||
delete n.returnType;
|
||||
delete n.accessibility;
|
||||
delete n.readonly;
|
||||
delete n.definite;
|
||||
delete n.override;
|
||||
},
|
||||
Decorator(node) {
|
||||
e.typescript_invalid_feature(node, 'decorators (related TSC proposal is not stage 4 yet)');
|
||||
},
|
||||
ImportDeclaration(node) {
|
||||
if (node.importKind === 'type') return b.empty;
|
||||
|
||||
if (node.specifiers?.length > 0) {
|
||||
const specifiers = node.specifiers.filter((/** @type {any} */ s) => s.importKind !== 'type');
|
||||
if (specifiers.length === 0) return b.empty;
|
||||
|
||||
return { ...node, specifiers };
|
||||
}
|
||||
|
||||
return node;
|
||||
},
|
||||
ExportNamedDeclaration(node, context) {
|
||||
if (node.exportKind === 'type') return b.empty;
|
||||
|
||||
if (node.declaration) {
|
||||
const result = context.next();
|
||||
if (result?.declaration?.type === 'EmptyStatement') {
|
||||
return b.empty;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (node.specifiers) {
|
||||
const specifiers = node.specifiers.filter((/** @type {any} */ s) => s.exportKind !== 'type');
|
||||
if (specifiers.length === 0) return b.empty;
|
||||
|
||||
return { ...node, specifiers };
|
||||
}
|
||||
|
||||
return node;
|
||||
},
|
||||
ExportDefaultDeclaration(node) {
|
||||
if (node.exportKind === 'type') return b.empty;
|
||||
return node;
|
||||
},
|
||||
ExportAllDeclaration(node) {
|
||||
if (node.exportKind === 'type') return b.empty;
|
||||
return node;
|
||||
},
|
||||
PropertyDefinition(node, { next }) {
|
||||
if (node.accessor) {
|
||||
e.typescript_invalid_feature(
|
||||
node,
|
||||
'accessor fields (related TSC proposal is not stage 4 yet)'
|
||||
);
|
||||
}
|
||||
return next();
|
||||
},
|
||||
TSAsExpression(node, context) {
|
||||
return context.visit(node.expression);
|
||||
},
|
||||
TSSatisfiesExpression(node, context) {
|
||||
return context.visit(node.expression);
|
||||
},
|
||||
TSNonNullExpression(node, context) {
|
||||
return context.visit(node.expression);
|
||||
},
|
||||
TSInterfaceDeclaration() {
|
||||
return b.empty;
|
||||
},
|
||||
TSTypeAliasDeclaration() {
|
||||
return b.empty;
|
||||
},
|
||||
TSTypeAssertion(node, context) {
|
||||
return context.visit(node.expression);
|
||||
},
|
||||
TSEnumDeclaration(node) {
|
||||
e.typescript_invalid_feature(node, 'enums');
|
||||
},
|
||||
TSParameterProperty(node, context) {
|
||||
if ((node.readonly || node.accessibility) && context.path.at(-2)?.kind === 'constructor') {
|
||||
e.typescript_invalid_feature(node, 'accessibility modifiers on constructor parameters');
|
||||
}
|
||||
return context.visit(node.parameter);
|
||||
},
|
||||
TSInstantiationExpression(node, context) {
|
||||
return context.visit(node.expression);
|
||||
},
|
||||
FunctionExpression: remove_this_param,
|
||||
FunctionDeclaration: remove_this_param,
|
||||
TSDeclareFunction() {
|
||||
return b.empty;
|
||||
},
|
||||
ClassBody(node, context) {
|
||||
const body = [];
|
||||
for (const _child of node.body) {
|
||||
const child = context.visit(_child);
|
||||
if (child.type !== 'PropertyDefinition' || !child.declare) {
|
||||
body.push(child);
|
||||
}
|
||||
}
|
||||
return {
|
||||
...node,
|
||||
body
|
||||
};
|
||||
},
|
||||
ClassDeclaration(node, context) {
|
||||
if (node.declare) {
|
||||
return b.empty;
|
||||
}
|
||||
delete node.abstract;
|
||||
delete node.implements;
|
||||
delete node.superTypeArguments;
|
||||
delete node.superTypeParameters;
|
||||
return context.next();
|
||||
},
|
||||
ClassExpression(node, context) {
|
||||
delete node.implements;
|
||||
delete node.superTypeArguments;
|
||||
delete node.superTypeParameters;
|
||||
return context.next();
|
||||
},
|
||||
MethodDefinition(node, context) {
|
||||
if (node.abstract) {
|
||||
return b.empty;
|
||||
}
|
||||
return context.next();
|
||||
},
|
||||
VariableDeclaration(node, context) {
|
||||
if (node.declare) {
|
||||
return b.empty;
|
||||
}
|
||||
return context.next();
|
||||
},
|
||||
TSModuleDeclaration(node, context) {
|
||||
if (!node.body) return b.empty;
|
||||
|
||||
// namespaces can contain non-type nodes
|
||||
const cleaned = /** @type {any[]} */ (node.body.body).map((entry) => context.visit(entry));
|
||||
if (cleaned.some((entry) => entry !== b.empty)) {
|
||||
e.typescript_invalid_feature(node, 'namespaces with non-type nodes');
|
||||
}
|
||||
|
||||
return b.empty;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {T} ast
|
||||
* @returns {T}
|
||||
*/
|
||||
export function remove_typescript_nodes(ast) {
|
||||
return walk(ast, null, visitors);
|
||||
}
|
||||
+950
@@ -0,0 +1,950 @@
|
||||
/** @import { Expression, Identifier, SourceLocation } from 'estree' */
|
||||
/** @import { Location } from 'locate-character' */
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Parser } from '../index.js' */
|
||||
import { is_void, REGEX_VALID_TAG_NAME } from '../../../../utils.js';
|
||||
import read_expression from '../read/expression.js';
|
||||
import { read_script } from '../read/script.js';
|
||||
import read_style from '../read/style.js';
|
||||
import { decode_character_references } from '../utils/html.js';
|
||||
import * as e from '../../../errors.js';
|
||||
import * as w from '../../../warnings.js';
|
||||
import { create_fragment } from '../utils/create.js';
|
||||
import { create_attribute, ExpressionMetadata, is_element_node } from '../../nodes.js';
|
||||
import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js';
|
||||
import { closing_tag_omitted } from '../../../../html-tree-validation.js';
|
||||
import { list } from '../../../utils/string.js';
|
||||
import { locator } from '../../../state.js';
|
||||
import * as b from '#compiler/builders';
|
||||
|
||||
const regex_invalid_unquoted_attribute_value = /(\/>|[\s"'=<>`])/y;
|
||||
const regex_closing_textarea_tag = /<\/textarea(\s[^>]*)?>/iy;
|
||||
const regex_closing_comment = /-->/;
|
||||
const regex_whitespace_or_slash_or_closing_tag = /(\s|\/|>)/;
|
||||
const regex_token_ending_character = /[\s=/>"']/;
|
||||
const regex_starts_with_quote_characters = /["']/y;
|
||||
const regex_attribute_value = /(?:"([^"]*)"|'([^'])*'|([^>\s]+))/y;
|
||||
const regex_doctype_name = /^![a-zA-Z]+$/;
|
||||
const regex_namespaced_name = /^[a-zA-Z][a-zA-Z0-9]*:[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9]$/;
|
||||
|
||||
/** @param {string} name */
|
||||
function is_valid_element_name(name) {
|
||||
// DOCTYPE (e.g. !DOCTYPE)
|
||||
if (regex_doctype_name.test(name)) return true;
|
||||
// svelte:* meta tags (e.g. svelte:element, svelte:head)
|
||||
if (regex_namespaced_name.test(name)) return true;
|
||||
// standard HTML/SVG/MathML elements and custom elements
|
||||
return REGEX_VALID_TAG_NAME.test(name);
|
||||
}
|
||||
export const regex_valid_component_name =
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers adjusted for our needs
|
||||
// (must start with uppercase letter if no dots, can contain dots)
|
||||
/^(?:\p{Lu}[$\u200c\u200d\p{ID_Continue}.]*|\p{ID_Start}[$\u200c\u200d\p{ID_Continue}]*(?:\.[$\u200c\u200d\p{ID_Continue}]+)+)$/u;
|
||||
|
||||
/** @type {Map<string, AST.ElementLike['type']>} */
|
||||
const root_only_meta_tags = new Map([
|
||||
['svelte:head', 'SvelteHead'],
|
||||
['svelte:options', 'SvelteOptions'],
|
||||
['svelte:window', 'SvelteWindow'],
|
||||
['svelte:document', 'SvelteDocument'],
|
||||
['svelte:body', 'SvelteBody']
|
||||
]);
|
||||
|
||||
/** @type {Map<string, AST.ElementLike['type']>} */
|
||||
const meta_tags = new Map([
|
||||
...root_only_meta_tags,
|
||||
['svelte:element', 'SvelteElement'],
|
||||
['svelte:component', 'SvelteComponent'],
|
||||
['svelte:self', 'SvelteSelf'],
|
||||
['svelte:fragment', 'SvelteFragment'],
|
||||
['svelte:boundary', 'SvelteBoundary']
|
||||
]);
|
||||
|
||||
/** @param {Parser} parser */
|
||||
export default function element(parser) {
|
||||
const start = parser.index++;
|
||||
|
||||
let parent = parser.current();
|
||||
|
||||
if (parser.eat('!--')) {
|
||||
const data = parser.read_until(regex_closing_comment);
|
||||
parser.eat('-->', true);
|
||||
|
||||
parser.append({
|
||||
type: 'Comment',
|
||||
start,
|
||||
end: parser.index,
|
||||
data
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.eat('/')) {
|
||||
const name = parser.read_until(regex_whitespace_or_slash_or_closing_tag);
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('>', true);
|
||||
|
||||
if (is_void(name)) {
|
||||
e.void_element_invalid_content(start);
|
||||
}
|
||||
|
||||
// close any elements that don't have their own closing tags, e.g. <div><p></div>
|
||||
while (/** @type {AST.RegularElement} */ (parent).name !== name) {
|
||||
if (parser.loose) {
|
||||
// If the previous element did interpret the next opening tag as an attribute, backtrack
|
||||
if (is_element_node(parent)) {
|
||||
const last = parent.attributes.at(-1);
|
||||
if (last?.type === 'Attribute' && last.name === `<${name}`) {
|
||||
parser.index = last.start;
|
||||
parent.attributes.pop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (parent.type === 'RegularElement') {
|
||||
if (!parser.last_auto_closed_tag || parser.last_auto_closed_tag.tag !== name) {
|
||||
const end = parent.fragment.nodes[0]?.start ?? start;
|
||||
w.element_implicitly_closed(
|
||||
{ start: parent.start, end },
|
||||
`</${name}>`,
|
||||
`</${parent.name}>`
|
||||
);
|
||||
}
|
||||
} else if (!parser.loose) {
|
||||
if (parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name) {
|
||||
e.element_invalid_closing_tag_autoclosed(start, name, parser.last_auto_closed_tag.reason);
|
||||
} else {
|
||||
e.element_invalid_closing_tag(start, name);
|
||||
}
|
||||
}
|
||||
|
||||
parent.end = start;
|
||||
parser.pop();
|
||||
|
||||
parent = parser.current();
|
||||
}
|
||||
|
||||
parent.end = parser.index;
|
||||
parser.pop();
|
||||
|
||||
if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) {
|
||||
parser.last_auto_closed_tag = undefined;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const tag = read_tag(parser, regex_whitespace_or_slash_or_closing_tag);
|
||||
|
||||
if (tag.name.startsWith('svelte:') && !meta_tags.has(tag.name)) {
|
||||
const bounds = { start: start + 1, end: start + 1 + tag.name.length };
|
||||
e.svelte_meta_invalid_tag(bounds, list(Array.from(meta_tags.keys())));
|
||||
}
|
||||
|
||||
if (!is_valid_element_name(tag.name) && !regex_valid_component_name.test(tag.name)) {
|
||||
// <div. -> in the middle of typing -> allow in loose mode
|
||||
if (!parser.loose || !tag.name.endsWith('.')) {
|
||||
const bounds = { start: start + 1, end: start + 1 + tag.name.length };
|
||||
e.tag_invalid_name(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
if (root_only_meta_tags.has(tag.name)) {
|
||||
if (tag.name in parser.meta_tags) {
|
||||
e.svelte_meta_duplicate(start, tag.name);
|
||||
}
|
||||
|
||||
if (parent.type !== 'Root') {
|
||||
e.svelte_meta_invalid_placement(start, tag.name);
|
||||
}
|
||||
|
||||
parser.meta_tags[tag.name] = true;
|
||||
}
|
||||
|
||||
const type = meta_tags.has(tag.name)
|
||||
? meta_tags.get(tag.name)
|
||||
: regex_valid_component_name.test(tag.name) || (parser.loose && tag.name.endsWith('.'))
|
||||
? 'Component'
|
||||
: tag.name === 'title' && parent_is_head(parser.stack)
|
||||
? 'TitleElement'
|
||||
: // TODO Svelte 6/7: once slots are removed in favor of snippets, always keep slot as a regular element
|
||||
tag.name === 'slot' && !parent_is_shadowroot_template(parser.stack)
|
||||
? 'SlotElement'
|
||||
: 'RegularElement';
|
||||
|
||||
/** @type {AST.ElementLike} */
|
||||
const element =
|
||||
type === 'RegularElement'
|
||||
? {
|
||||
type,
|
||||
start,
|
||||
end: -1,
|
||||
name: tag.name,
|
||||
name_loc: tag.loc,
|
||||
attributes: [],
|
||||
fragment: create_fragment(true),
|
||||
metadata: {
|
||||
svg: false,
|
||||
mathml: false,
|
||||
scoped: false,
|
||||
has_spread: false,
|
||||
path: [],
|
||||
synthetic_value_node: null
|
||||
}
|
||||
}
|
||||
: /** @type {AST.ElementLike} */ ({
|
||||
type,
|
||||
start,
|
||||
end: -1,
|
||||
name: tag.name,
|
||||
name_loc: tag.loc,
|
||||
attributes: [],
|
||||
fragment: create_fragment(true),
|
||||
metadata: {
|
||||
// unpopulated at first, differs between types
|
||||
}
|
||||
});
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, tag.name)) {
|
||||
const end = parent.fragment.nodes[0]?.start ?? start;
|
||||
w.element_implicitly_closed({ start: parent.start, end }, `<${tag.name}>`, `</${parent.name}>`);
|
||||
parent.end = start;
|
||||
parser.pop();
|
||||
parser.last_auto_closed_tag = {
|
||||
tag: parent.name,
|
||||
reason: tag.name,
|
||||
depth: parser.stack.length
|
||||
};
|
||||
}
|
||||
|
||||
/** @type {string[]} */
|
||||
const unique_names = [];
|
||||
|
||||
const current = parser.current();
|
||||
const is_top_level_script_or_style =
|
||||
(tag.name === 'script' || tag.name === 'style') && current.type === 'Root';
|
||||
|
||||
const read = is_top_level_script_or_style ? read_static_attribute : read_attribute;
|
||||
|
||||
let attribute;
|
||||
while ((attribute = read(parser))) {
|
||||
// animate and transition can only be specified once per element so no need
|
||||
// to check here, use can be used multiple times, same for the on directive
|
||||
// finally let already has error handling in case of duplicate variable names
|
||||
if (
|
||||
attribute.type === 'Attribute' ||
|
||||
attribute.type === 'BindDirective' ||
|
||||
attribute.type === 'StyleDirective' ||
|
||||
attribute.type === 'ClassDirective'
|
||||
) {
|
||||
// `bind:attribute` and `attribute` are just the same but `class:attribute`,
|
||||
// `style:attribute` and `attribute` are different and should be allowed together
|
||||
// so we concatenate the type while normalizing the type for BindDirective
|
||||
const type = attribute.type === 'BindDirective' ? 'Attribute' : attribute.type;
|
||||
if (unique_names.includes(type + attribute.name)) {
|
||||
e.attribute_duplicate(attribute);
|
||||
// <svelte:element bind:this this=..> is allowed
|
||||
} else if (attribute.name !== 'this') {
|
||||
unique_names.push(type + attribute.name);
|
||||
}
|
||||
}
|
||||
|
||||
element.attributes.push(attribute);
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
|
||||
if (element.type === 'Component') {
|
||||
element.metadata.expression = new ExpressionMetadata();
|
||||
}
|
||||
|
||||
if (element.type === 'SvelteComponent') {
|
||||
const index = element.attributes.findIndex(
|
||||
/** @param {any} attr */
|
||||
(attr) => attr.type === 'Attribute' && attr.name === 'this'
|
||||
);
|
||||
if (index === -1) {
|
||||
e.svelte_component_missing_this(start);
|
||||
}
|
||||
|
||||
const definition = /** @type {AST.Attribute} */ (element.attributes.splice(index, 1)[0]);
|
||||
if (!is_expression_attribute(definition)) {
|
||||
e.svelte_component_invalid_this(definition.start);
|
||||
}
|
||||
|
||||
element.expression = get_attribute_expression(definition);
|
||||
element.metadata.expression = new ExpressionMetadata();
|
||||
}
|
||||
|
||||
if (element.type === 'SvelteElement') {
|
||||
const index = element.attributes.findIndex(
|
||||
/** @param {any} attr */
|
||||
(attr) => attr.type === 'Attribute' && attr.name === 'this'
|
||||
);
|
||||
if (index === -1) {
|
||||
e.svelte_element_missing_this(start);
|
||||
}
|
||||
|
||||
const definition = /** @type {AST.Attribute} */ (element.attributes.splice(index, 1)[0]);
|
||||
|
||||
if (definition.value === true) {
|
||||
e.svelte_element_missing_this(definition);
|
||||
}
|
||||
|
||||
if (!is_expression_attribute(definition)) {
|
||||
w.svelte_element_invalid_this(definition);
|
||||
|
||||
// note that this is wrong, in the case of e.g. `this="h{n}"` — it will result in `<h>`.
|
||||
// it would be much better to just error here, but we are preserving the existing buggy
|
||||
// Svelte 4 behaviour out of an overabundance of caution regarding breaking changes.
|
||||
// TODO in 6.0, error
|
||||
const chunk = /** @type {Array<AST.ExpressionTag | AST.Text>} */ (definition.value)[0];
|
||||
element.tag =
|
||||
chunk.type === 'Text'
|
||||
? {
|
||||
type: 'Literal',
|
||||
value: chunk.data,
|
||||
raw: `'${chunk.raw}'`,
|
||||
start: chunk.start,
|
||||
end: chunk.end
|
||||
}
|
||||
: chunk.expression;
|
||||
} else {
|
||||
element.tag = get_attribute_expression(definition);
|
||||
}
|
||||
|
||||
element.metadata.expression = new ExpressionMetadata();
|
||||
}
|
||||
|
||||
if (is_top_level_script_or_style) {
|
||||
parser.eat('>', true);
|
||||
|
||||
/** @type {AST.Comment | null} */
|
||||
let prev_comment = null;
|
||||
for (let i = current.fragment.nodes.length - 1; i >= 0; i--) {
|
||||
const node = current.fragment.nodes[i];
|
||||
|
||||
if (i === current.fragment.nodes.length - 1 && node.end !== start) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (node.type === 'Comment') {
|
||||
prev_comment = node;
|
||||
break;
|
||||
} else if (node.type !== 'Text' || node.data.trim()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (tag.name === 'script') {
|
||||
const content = read_script(parser, start, element.attributes);
|
||||
if (prev_comment) {
|
||||
// We take advantage of the fact that the root will never have leadingComments set,
|
||||
// and set the previous comment to it so that the warning mechanism can later
|
||||
// inspect the root and see if there was a html comment before it silencing specific warnings.
|
||||
content.content.leadingComments = [{ type: 'Line', value: prev_comment.data }];
|
||||
}
|
||||
|
||||
if (content.context === 'module') {
|
||||
if (current.module) e.script_duplicate(start);
|
||||
current.module = content;
|
||||
} else {
|
||||
if (current.instance) e.script_duplicate(start);
|
||||
current.instance = content;
|
||||
}
|
||||
} else {
|
||||
const content = read_style(parser, start, element.attributes);
|
||||
content.content.comment = prev_comment;
|
||||
|
||||
if (current.css) e.style_duplicate(start);
|
||||
current.css = content;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
parser.append(element);
|
||||
|
||||
const self_closing = parser.eat('/') || is_void(tag.name);
|
||||
const closed = parser.eat('>', true, false);
|
||||
|
||||
// Loose parsing mode
|
||||
if (!closed) {
|
||||
// We may have eaten an opening `<` of the next element and treated it as an attribute...
|
||||
const last = element.attributes.at(-1);
|
||||
if (last?.type === 'Attribute' && last.name === '<') {
|
||||
parser.index = last.start;
|
||||
element.attributes.pop();
|
||||
} else {
|
||||
// ... or we may have eaten part of a following block ...
|
||||
const prev_1 = parser.template[parser.index - 1];
|
||||
const prev_2 = parser.template[parser.index - 2];
|
||||
const current = parser.template[parser.index];
|
||||
if (prev_2 === '{' && prev_1 === '/') {
|
||||
parser.index -= 2;
|
||||
} else if (prev_1 === '{' && (current === '#' || current === '@' || current === ':')) {
|
||||
parser.index -= 1;
|
||||
} else {
|
||||
// ... or we're followed by whitespace, for example near the end of the template,
|
||||
// which we want to take in so that language tools has more room to work with
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (self_closing || !closed) {
|
||||
// don't push self-closing elements onto the stack
|
||||
element.end = parser.index;
|
||||
} else if (tag.name === 'textarea') {
|
||||
// special case
|
||||
element.fragment.nodes = read_sequence(
|
||||
parser,
|
||||
() => {
|
||||
regex_closing_textarea_tag.lastIndex = parser.index;
|
||||
return regex_closing_textarea_tag.test(parser.template);
|
||||
},
|
||||
'inside <textarea>'
|
||||
);
|
||||
parser.read(regex_closing_textarea_tag);
|
||||
element.end = parser.index;
|
||||
} else if (tag.name === 'script' || tag.name === 'style') {
|
||||
// special case
|
||||
const start = parser.index;
|
||||
const close_tag = `</${tag.name}>`;
|
||||
const close_index = parser.template.indexOf(close_tag, parser.index);
|
||||
const data = parser.template.slice(
|
||||
parser.index,
|
||||
close_index === -1 ? parser.template.length : close_index
|
||||
);
|
||||
parser.index = close_index === -1 ? parser.template.length : close_index;
|
||||
const end = parser.index;
|
||||
|
||||
/** @type {AST.Text} */
|
||||
const node = {
|
||||
start,
|
||||
end,
|
||||
type: 'Text',
|
||||
data,
|
||||
raw: data
|
||||
};
|
||||
|
||||
element.fragment.nodes.push(node);
|
||||
parser.eat(`</${tag.name}>`, true);
|
||||
element.end = parser.index;
|
||||
} else {
|
||||
parser.stack.push(element);
|
||||
parser.fragments.push(element.fragment);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {AST.TemplateNode[]} stack */
|
||||
function parent_is_head(stack) {
|
||||
let i = stack.length;
|
||||
while (i--) {
|
||||
const { type } = stack[i];
|
||||
if (type === 'SvelteHead') return true;
|
||||
if (type === 'RegularElement' || type === 'Component') return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @param {AST.TemplateNode[]} stack */
|
||||
function parent_is_shadowroot_template(stack) {
|
||||
// https://developer.chrome.com/docs/css-ui/declarative-shadow-dom#building_a_declarative_shadow_root
|
||||
let i = stack.length;
|
||||
while (i--) {
|
||||
if (
|
||||
stack[i].type === 'RegularElement' &&
|
||||
/** @type {AST.RegularElement} */ (stack[i]).attributes.some(
|
||||
(a) => a.type === 'Attribute' && a.name === 'shadowrootmode'
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {AST.Attribute | null}
|
||||
*/
|
||||
function read_static_attribute(parser) {
|
||||
const start = parser.index;
|
||||
|
||||
const tag = read_tag(parser, regex_token_ending_character);
|
||||
if (!tag.name) return null;
|
||||
|
||||
/** @type {true | Array<AST.Text | AST.ExpressionTag>} */
|
||||
let value = true;
|
||||
|
||||
if (parser.eat('=')) {
|
||||
parser.allow_whitespace();
|
||||
let raw = parser.match_regex(regex_attribute_value);
|
||||
if (!raw) {
|
||||
e.expected_attribute_value(parser.index);
|
||||
}
|
||||
|
||||
parser.index += raw.length;
|
||||
|
||||
const quoted = raw[0] === '"' || raw[0] === "'";
|
||||
if (quoted) {
|
||||
raw = raw.slice(1, -1);
|
||||
}
|
||||
|
||||
value = [
|
||||
{
|
||||
start: parser.index - raw.length - (quoted ? 1 : 0),
|
||||
end: quoted ? parser.index - 1 : parser.index,
|
||||
type: 'Text',
|
||||
raw: raw,
|
||||
data: decode_character_references(raw, true)
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (parser.match_regex(regex_starts_with_quote_characters)) {
|
||||
e.expected_token(parser.index, '=');
|
||||
}
|
||||
|
||||
return create_attribute(tag.name, tag.loc, start, parser.index, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag | null}
|
||||
*/
|
||||
function read_attribute(parser) {
|
||||
/** @type {AST.JSComment | null} */
|
||||
// eslint-disable-next-line no-useless-assignment -- it is, in fact, eslint that is useless
|
||||
let comment = null;
|
||||
|
||||
while ((comment = read_comment(parser))) {
|
||||
parser.root.comments.push(comment);
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
|
||||
const start = parser.index;
|
||||
|
||||
if (parser.eat('{')) {
|
||||
parser.allow_whitespace();
|
||||
|
||||
if (parser.eat('@attach')) {
|
||||
parser.require_whitespace();
|
||||
|
||||
const expression = read_expression(parser);
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
/** @type {AST.AttachTag} */
|
||||
const attachment = {
|
||||
type: 'AttachTag',
|
||||
start,
|
||||
end: parser.index,
|
||||
expression,
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
};
|
||||
|
||||
return attachment;
|
||||
}
|
||||
|
||||
if (parser.eat('...')) {
|
||||
const expression = read_expression(parser);
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
/** @type {AST.SpreadAttribute} */
|
||||
const spread = {
|
||||
type: 'SpreadAttribute',
|
||||
start,
|
||||
end: parser.index,
|
||||
expression,
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
};
|
||||
|
||||
return spread;
|
||||
} else {
|
||||
const id = parser.read_identifier();
|
||||
|
||||
if (id.name === '') {
|
||||
if (
|
||||
parser.loose &&
|
||||
(parser.match('#') || parser.match('/') || parser.match('@') || parser.match(':'))
|
||||
) {
|
||||
// We're likely in an unclosed opening tag and did read part of a block.
|
||||
// Return null to not crash the parser so it can continue with closing the tag.
|
||||
return null;
|
||||
} else if (parser.loose && parser.match('}')) {
|
||||
// Likely in the middle of typing, just created the shorthand
|
||||
} else {
|
||||
e.attribute_empty_shorthand(start);
|
||||
}
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
/** @type {AST.ExpressionTag} */
|
||||
const expression = {
|
||||
type: 'ExpressionTag',
|
||||
start: id.start,
|
||||
end: id.end,
|
||||
expression: id,
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
};
|
||||
|
||||
return create_attribute(id.name, id.loc, start, parser.index, expression);
|
||||
}
|
||||
}
|
||||
|
||||
const tag = read_tag(parser, regex_token_ending_character);
|
||||
|
||||
if (!tag.name) return null;
|
||||
|
||||
let end = parser.index;
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
const colon_index = tag.name.indexOf(':');
|
||||
const type = colon_index !== -1 && get_directive_type(tag.name.slice(0, colon_index));
|
||||
|
||||
/** @type {true | AST.ExpressionTag | Array<AST.Text | AST.ExpressionTag>} */
|
||||
let value = true;
|
||||
if (parser.eat('=')) {
|
||||
parser.allow_whitespace();
|
||||
|
||||
if (parser.template[parser.index] === '/' && parser.template[parser.index + 1] === '>') {
|
||||
const char_start = parser.index;
|
||||
parser.index++; // consume '/'
|
||||
value = [
|
||||
{
|
||||
start: char_start,
|
||||
end: char_start + 1,
|
||||
type: 'Text',
|
||||
raw: '/',
|
||||
data: '/'
|
||||
}
|
||||
];
|
||||
end = parser.index;
|
||||
} else {
|
||||
value = read_attribute_value(parser);
|
||||
end = parser.index;
|
||||
}
|
||||
} else if (parser.match_regex(regex_starts_with_quote_characters)) {
|
||||
e.expected_token(parser.index, '=');
|
||||
}
|
||||
|
||||
if (type) {
|
||||
const [directive_name, ...modifiers] = tag.name.slice(colon_index + 1).split('|');
|
||||
|
||||
if (directive_name === '') {
|
||||
e.directive_missing_name({ start, end: start + colon_index + 1 }, tag.name);
|
||||
}
|
||||
|
||||
if (type === 'StyleDirective') {
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
type,
|
||||
name: directive_name,
|
||||
name_loc: tag.loc,
|
||||
modifiers: /** @type {Array<'important'>} */ (modifiers),
|
||||
value,
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const first_value = value === true ? undefined : Array.isArray(value) ? value[0] : value;
|
||||
|
||||
/** @type {Expression | null} */
|
||||
let expression = null;
|
||||
|
||||
if (first_value) {
|
||||
const attribute_contains_text =
|
||||
/** @type {any[]} */ (value).length > 1 || first_value.type === 'Text';
|
||||
if (attribute_contains_text) {
|
||||
e.directive_invalid_value(/** @type {number} */ (first_value.start));
|
||||
} else {
|
||||
// TODO throw a parser error in a future version here if this `[ExpressionTag]` instead of `ExpressionTag`,
|
||||
// which means stringified value, which isn't allowed for some directives?
|
||||
expression = first_value.expression;
|
||||
}
|
||||
}
|
||||
|
||||
const directive = /** @type {AST.Directive} */ ({
|
||||
start,
|
||||
end,
|
||||
type,
|
||||
name: directive_name,
|
||||
name_loc: tag.loc,
|
||||
expression,
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
});
|
||||
|
||||
// @ts-expect-error we do this separately from the declaration to avoid upsetting typescript
|
||||
directive.modifiers = modifiers;
|
||||
|
||||
if (directive.type === 'TransitionDirective') {
|
||||
const direction = tag.name.slice(0, colon_index);
|
||||
directive.intro = direction === 'in' || direction === 'transition';
|
||||
directive.outro = direction === 'out' || direction === 'transition';
|
||||
}
|
||||
|
||||
// Directive name is expression, e.g. <p class:isRed />
|
||||
if (
|
||||
(directive.type === 'BindDirective' || directive.type === 'ClassDirective') &&
|
||||
!directive.expression
|
||||
) {
|
||||
directive.expression = /** @type {any} */ ({
|
||||
start: start + colon_index + 1,
|
||||
end,
|
||||
type: 'Identifier',
|
||||
name: directive.name
|
||||
});
|
||||
}
|
||||
|
||||
return directive;
|
||||
}
|
||||
|
||||
return create_attribute(tag.name, tag.loc, start, end, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @returns {AST.JSComment | null}
|
||||
*/
|
||||
function read_comment(parser) {
|
||||
const start = parser.index;
|
||||
|
||||
if (parser.eat('//')) {
|
||||
const value = parser.read_until(/\n/);
|
||||
const end = parser.index;
|
||||
|
||||
return {
|
||||
type: 'Line',
|
||||
start,
|
||||
end,
|
||||
value,
|
||||
loc: {
|
||||
start: locator(start),
|
||||
end: locator(end)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (parser.eat('/*')) {
|
||||
const value = parser.read_until(/\*\//);
|
||||
|
||||
parser.eat('*/');
|
||||
const end = parser.index;
|
||||
|
||||
return {
|
||||
type: 'Block',
|
||||
start,
|
||||
end,
|
||||
value,
|
||||
loc: {
|
||||
start: locator(start),
|
||||
end: locator(end)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {any}
|
||||
*/
|
||||
function get_directive_type(name) {
|
||||
if (name === 'use') return 'UseDirective';
|
||||
if (name === 'animate') return 'AnimateDirective';
|
||||
if (name === 'bind') return 'BindDirective';
|
||||
if (name === 'class') return 'ClassDirective';
|
||||
if (name === 'style') return 'StyleDirective';
|
||||
if (name === 'on') return 'OnDirective';
|
||||
if (name === 'let') return 'LetDirective';
|
||||
if (name === 'in' || name === 'out' || name === 'transition') return 'TransitionDirective';
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @return {AST.ExpressionTag | Array<AST.ExpressionTag | AST.Text>}
|
||||
*/
|
||||
function read_attribute_value(parser) {
|
||||
const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null;
|
||||
if (quote_mark && parser.eat(quote_mark)) {
|
||||
return [
|
||||
{
|
||||
start: parser.index - 1,
|
||||
end: parser.index - 1,
|
||||
type: 'Text',
|
||||
raw: '',
|
||||
data: ''
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/** @type {Array<AST.ExpressionTag | AST.Text>} */
|
||||
let value;
|
||||
try {
|
||||
value = read_sequence(
|
||||
parser,
|
||||
() => {
|
||||
// handle common case of quote marks existing outside of regex for performance reasons
|
||||
if (quote_mark) return parser.match(quote_mark);
|
||||
return !!parser.match_regex(regex_invalid_unquoted_attribute_value);
|
||||
},
|
||||
'in attribute value'
|
||||
);
|
||||
} catch (/** @type {any} */ error) {
|
||||
if (error.code === 'js_parse_error') {
|
||||
// if the attribute value didn't close + self-closing tag
|
||||
// eg: `<Component test={{a:1} />`
|
||||
// acorn may throw a `Unterminated regular expression` because of `/>`
|
||||
const pos = error.position?.[0];
|
||||
if (pos !== undefined && parser.template.slice(pos - 1, pos + 1) === '/>') {
|
||||
parser.index = pos;
|
||||
e.expected_token(pos, quote_mark || '}');
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (value.length === 0 && !quote_mark) {
|
||||
e.expected_attribute_value(parser.index);
|
||||
}
|
||||
|
||||
if (quote_mark) parser.index += 1;
|
||||
|
||||
if (quote_mark || value.length > 1 || value[0].type === 'Text') {
|
||||
return value;
|
||||
} else {
|
||||
return value[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {() => boolean} done
|
||||
* @param {string} location
|
||||
* @returns {any[]}
|
||||
*/
|
||||
function read_sequence(parser, done, location) {
|
||||
/** @type {AST.Text} */
|
||||
let current_chunk = {
|
||||
start: parser.index,
|
||||
end: -1,
|
||||
type: 'Text',
|
||||
raw: '',
|
||||
data: ''
|
||||
};
|
||||
|
||||
/** @type {Array<AST.Text | AST.ExpressionTag>} */
|
||||
const chunks = [];
|
||||
|
||||
/** @param {number} end */
|
||||
function flush(end) {
|
||||
if (end > current_chunk.start) {
|
||||
current_chunk.raw = parser.template.slice(current_chunk.start, end);
|
||||
current_chunk.data = decode_character_references(current_chunk.raw, true);
|
||||
current_chunk.end = end;
|
||||
chunks.push(current_chunk);
|
||||
}
|
||||
}
|
||||
|
||||
while (parser.index < parser.template.length) {
|
||||
const index = parser.index;
|
||||
|
||||
if (done()) {
|
||||
flush(parser.index);
|
||||
return chunks;
|
||||
} else if (parser.eat('{')) {
|
||||
if (parser.match('#')) {
|
||||
const index = parser.index - 1;
|
||||
parser.eat('#');
|
||||
const name = parser.read_until(/[^a-z]/);
|
||||
e.block_invalid_placement(index, name, location);
|
||||
} else if (parser.match('@')) {
|
||||
const index = parser.index - 1;
|
||||
parser.eat('@');
|
||||
const name = parser.read_until(/[^a-z]/);
|
||||
e.tag_invalid_placement(index, name, location);
|
||||
}
|
||||
|
||||
flush(parser.index - 1);
|
||||
|
||||
parser.allow_whitespace();
|
||||
const expression = read_expression(parser);
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
/** @type {AST.ExpressionTag} */
|
||||
const chunk = {
|
||||
type: 'ExpressionTag',
|
||||
start: index,
|
||||
end: parser.index,
|
||||
expression,
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
};
|
||||
|
||||
chunks.push(chunk);
|
||||
|
||||
current_chunk = {
|
||||
start: parser.index,
|
||||
end: -1,
|
||||
type: 'Text',
|
||||
raw: '',
|
||||
data: ''
|
||||
};
|
||||
} else {
|
||||
parser.index++;
|
||||
}
|
||||
}
|
||||
|
||||
if (parser.loose) {
|
||||
return chunks;
|
||||
} else {
|
||||
e.unexpected_eof(parser.template.length);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {RegExp} regex
|
||||
* @returns {Identifier & { start: number, end: number, loc: SourceLocation }}
|
||||
*/
|
||||
function read_tag(parser, regex) {
|
||||
const start = parser.index;
|
||||
const name = parser.read_until(regex);
|
||||
const end = parser.index;
|
||||
|
||||
return {
|
||||
type: 'Identifier',
|
||||
name,
|
||||
start,
|
||||
end,
|
||||
loc: {
|
||||
start: locator(start),
|
||||
end: locator(end)
|
||||
}
|
||||
};
|
||||
}
|
||||
+17
@@ -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;
|
||||
}
|
||||
+746
@@ -0,0 +1,746 @@
|
||||
/** @import { ArrowFunctionExpression, Expression, Identifier, Pattern } from 'estree' */
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Parser } from '../index.js' */
|
||||
import { walk } from 'zimmerframe';
|
||||
import * as e from '../../../errors.js';
|
||||
import { ExpressionMetadata } from '../../nodes.js';
|
||||
import { parse_expression_at } from '../acorn.js';
|
||||
import read_pattern from '../read/context.js';
|
||||
import read_expression, { get_loose_identifier } from '../read/expression.js';
|
||||
import { create_fragment } from '../utils/create.js';
|
||||
import { match_bracket } from '../utils/bracket.js';
|
||||
|
||||
const regex_whitespace_with_closing_curly_brace = /\s*}/y;
|
||||
|
||||
const pointy_bois = { '<': '>' };
|
||||
|
||||
/** @param {Parser} parser */
|
||||
export default function tag(parser) {
|
||||
const start = parser.index;
|
||||
parser.index += 1;
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
if (parser.eat('#')) return open(parser);
|
||||
if (parser.eat(':')) return next(parser);
|
||||
if (parser.eat('@')) return special(parser);
|
||||
if (parser.match('/')) {
|
||||
if (!parser.match('/*') && !parser.match('//')) {
|
||||
parser.eat('/');
|
||||
return close(parser);
|
||||
}
|
||||
}
|
||||
|
||||
const expression = read_expression(parser);
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
parser.append({
|
||||
type: 'ExpressionTag',
|
||||
start,
|
||||
end: parser.index,
|
||||
expression,
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {Parser} parser */
|
||||
function open(parser) {
|
||||
let start = parser.index - 2;
|
||||
while (parser.template[start] !== '{') start -= 1;
|
||||
|
||||
if (parser.eat('if')) {
|
||||
parser.require_whitespace();
|
||||
|
||||
/** @type {AST.IfBlock} */
|
||||
const block = parser.append({
|
||||
type: 'IfBlock',
|
||||
elseif: false,
|
||||
start,
|
||||
end: -1,
|
||||
test: read_expression(parser),
|
||||
consequent: create_fragment(),
|
||||
alternate: null,
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
});
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
parser.stack.push(block);
|
||||
parser.fragments.push(block.consequent);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.eat('each')) {
|
||||
parser.require_whitespace();
|
||||
|
||||
const template = parser.template;
|
||||
let end = parser.template.length;
|
||||
|
||||
/** @type {Expression | undefined} */
|
||||
let expression;
|
||||
|
||||
// we have to do this loop because `{#each x as { y = z }}` fails to parse —
|
||||
// the `as { y = z }` is treated as an Expression but it's actually a Pattern.
|
||||
// the 'fix' is to backtrack and hide everything from the `as` onwards, until
|
||||
// we get a valid expression
|
||||
while (!expression) {
|
||||
try {
|
||||
expression = read_expression(parser, undefined, true);
|
||||
} catch (err) {
|
||||
end = /** @type {any} */ (err).position[0] - 2;
|
||||
|
||||
while (end > start && parser.template.slice(end, end + 2) !== 'as') {
|
||||
end -= 1;
|
||||
}
|
||||
|
||||
if (end <= start) {
|
||||
if (parser.loose) {
|
||||
expression = get_loose_identifier(parser);
|
||||
if (expression) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
// @ts-expect-error parser.template is meant to be readonly, this is a special case
|
||||
parser.template = template.slice(0, end);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
parser.template = template;
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
// {#each} blocks must declare a context – {#each list as item}
|
||||
if (!parser.match('as')) {
|
||||
// this could be a TypeScript assertion that was erroneously eaten.
|
||||
|
||||
if (expression.type === 'SequenceExpression') {
|
||||
expression = expression.expressions[0];
|
||||
}
|
||||
|
||||
let assertion = null;
|
||||
let end = expression.end;
|
||||
|
||||
expression = walk(expression, null, {
|
||||
// @ts-expect-error
|
||||
TSAsExpression(node, context) {
|
||||
if (node.end === /** @type {Expression} */ (expression).end) {
|
||||
assertion = node;
|
||||
end = node.expression.end;
|
||||
return node.expression;
|
||||
}
|
||||
|
||||
context.next();
|
||||
}
|
||||
});
|
||||
|
||||
expression.end = end;
|
||||
|
||||
if (assertion) {
|
||||
// we can't reset `parser.index` to `expression.expression.end` because
|
||||
// it will ignore any parentheses — we need to jump through this hoop
|
||||
let end = /** @type {any} */ (/** @type {any} */ (assertion).typeAnnotation).start - 2;
|
||||
while (parser.template.slice(end, end + 2) !== 'as') end -= 1;
|
||||
|
||||
parser.index = end;
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Pattern | null} */
|
||||
let context = null;
|
||||
let index;
|
||||
let key;
|
||||
|
||||
if (parser.eat('as')) {
|
||||
parser.require_whitespace();
|
||||
|
||||
context = read_pattern(parser);
|
||||
} else {
|
||||
// {#each Array.from({ length: 10 }), i} is read as a sequence expression,
|
||||
// which is set back above - we now gotta reset the index as a consequence
|
||||
// to properly read the , i part
|
||||
parser.index = /** @type {number} */ (expression.end);
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
if (parser.eat(',')) {
|
||||
parser.allow_whitespace();
|
||||
index = parser.read_identifier().name;
|
||||
if (!index) {
|
||||
e.expected_identifier(parser.index);
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
|
||||
if (parser.eat('(')) {
|
||||
parser.allow_whitespace();
|
||||
|
||||
key = read_expression(parser, '(');
|
||||
parser.allow_whitespace();
|
||||
parser.eat(')', true);
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
|
||||
const matches = parser.eat('}', true, false);
|
||||
|
||||
if (!matches) {
|
||||
// Parser may have read the `as` as part of the expression (e.g. in `{#each foo. as x}`)
|
||||
if (parser.template.slice(parser.index - 4, parser.index) === ' as ') {
|
||||
const prev_index = parser.index;
|
||||
context = read_pattern(parser);
|
||||
parser.eat('}', true);
|
||||
expression = {
|
||||
type: 'Identifier',
|
||||
name: '',
|
||||
start: expression.start,
|
||||
end: prev_index - 4
|
||||
};
|
||||
} else {
|
||||
parser.eat('}', true); // rerun to produce the parser error
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {AST.EachBlock} */
|
||||
const block = parser.append({
|
||||
type: 'EachBlock',
|
||||
start,
|
||||
end: -1,
|
||||
expression,
|
||||
body: create_fragment(),
|
||||
context,
|
||||
index,
|
||||
key,
|
||||
metadata: /** @type {any} */ (null) // filled in later
|
||||
});
|
||||
|
||||
parser.stack.push(block);
|
||||
parser.fragments.push(block.body);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.eat('await')) {
|
||||
parser.require_whitespace();
|
||||
const expression = read_expression(parser);
|
||||
parser.allow_whitespace();
|
||||
|
||||
/** @type {AST.AwaitBlock} */
|
||||
const block = parser.append({
|
||||
type: 'AwaitBlock',
|
||||
start,
|
||||
end: -1,
|
||||
expression,
|
||||
value: null,
|
||||
error: null,
|
||||
pending: null,
|
||||
then: null,
|
||||
catch: null,
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
});
|
||||
|
||||
if (parser.eat('then')) {
|
||||
if (parser.match_regex(regex_whitespace_with_closing_curly_brace)) {
|
||||
parser.allow_whitespace();
|
||||
} else {
|
||||
parser.require_whitespace();
|
||||
block.value = read_pattern(parser);
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
|
||||
block.then = create_fragment();
|
||||
parser.fragments.push(block.then);
|
||||
} else if (parser.eat('catch')) {
|
||||
if (parser.match_regex(regex_whitespace_with_closing_curly_brace)) {
|
||||
parser.allow_whitespace();
|
||||
} else {
|
||||
parser.require_whitespace();
|
||||
block.error = read_pattern(parser);
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
|
||||
block.catch = create_fragment();
|
||||
parser.fragments.push(block.catch);
|
||||
} else {
|
||||
block.pending = create_fragment();
|
||||
parser.fragments.push(block.pending);
|
||||
}
|
||||
|
||||
const matches = parser.eat('}', true, false);
|
||||
|
||||
// Parser may have read the `then/catch` as part of the expression (e.g. in `{#await foo. then x}`)
|
||||
if (!matches) {
|
||||
if (parser.template.slice(parser.index - 6, parser.index) === ' then ') {
|
||||
const prev_index = parser.index;
|
||||
block.value = read_pattern(parser);
|
||||
parser.eat('}', true);
|
||||
block.expression = {
|
||||
type: 'Identifier',
|
||||
name: '',
|
||||
start: expression.start,
|
||||
end: prev_index - 6
|
||||
};
|
||||
block.then = block.pending;
|
||||
block.pending = null;
|
||||
} else if (parser.template.slice(parser.index - 7, parser.index) === ' catch ') {
|
||||
const prev_index = parser.index;
|
||||
block.error = read_pattern(parser);
|
||||
parser.eat('}', true);
|
||||
block.expression = {
|
||||
type: 'Identifier',
|
||||
name: '',
|
||||
start: expression.start,
|
||||
end: prev_index - 7
|
||||
};
|
||||
block.catch = block.pending;
|
||||
block.pending = null;
|
||||
} else {
|
||||
parser.eat('}', true); // rerun to produce the parser error
|
||||
}
|
||||
}
|
||||
|
||||
parser.stack.push(block);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.eat('key')) {
|
||||
parser.require_whitespace();
|
||||
|
||||
const expression = read_expression(parser);
|
||||
parser.allow_whitespace();
|
||||
|
||||
parser.eat('}', true);
|
||||
|
||||
/** @type {AST.KeyBlock} */
|
||||
const block = parser.append({
|
||||
type: 'KeyBlock',
|
||||
start,
|
||||
end: -1,
|
||||
expression,
|
||||
fragment: create_fragment(),
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
});
|
||||
|
||||
parser.stack.push(block);
|
||||
parser.fragments.push(block.fragment);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.eat('snippet')) {
|
||||
parser.require_whitespace();
|
||||
|
||||
const id = parser.read_identifier();
|
||||
|
||||
if (id.name === '' && !parser.loose) {
|
||||
e.expected_identifier(parser.index);
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
const params_start = parser.index;
|
||||
|
||||
// snippets could have a generic signature, e.g. `#snippet foo<T>(...)`
|
||||
/** @type {string | undefined} */
|
||||
let type_params;
|
||||
|
||||
// if we match a generic opening
|
||||
if (parser.ts && parser.match('<')) {
|
||||
const start = parser.index;
|
||||
const end = match_bracket(parser, start, pointy_bois);
|
||||
|
||||
type_params = parser.template.slice(start + 1, end - 1);
|
||||
|
||||
parser.index = end;
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
const matched = parser.eat('(', true, false);
|
||||
|
||||
if (matched) {
|
||||
let parentheses = 1;
|
||||
|
||||
while (parser.index < parser.template.length && (!parser.match(')') || parentheses !== 1)) {
|
||||
if (parser.match('(')) parentheses++;
|
||||
if (parser.match(')')) parentheses--;
|
||||
parser.index += 1;
|
||||
}
|
||||
|
||||
parser.eat(')', true);
|
||||
}
|
||||
|
||||
const prelude = parser.template.slice(0, params_start).replace(/\S/g, ' ');
|
||||
const params = parser.template.slice(params_start, parser.index);
|
||||
|
||||
let function_expression = matched
|
||||
? /** @type {ArrowFunctionExpression} */ (
|
||||
parse_expression_at(parser, prelude + `${params} => {}`, params_start)
|
||||
)
|
||||
: { params: [] };
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
/** @type {AST.SnippetBlock} */
|
||||
const block = parser.append({
|
||||
type: 'SnippetBlock',
|
||||
start,
|
||||
end: -1,
|
||||
expression: id,
|
||||
typeParams: type_params,
|
||||
parameters: function_expression.params,
|
||||
body: create_fragment(),
|
||||
metadata: {
|
||||
can_hoist: false,
|
||||
sites: new Set()
|
||||
}
|
||||
});
|
||||
parser.stack.push(block);
|
||||
parser.fragments.push(block.body);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
e.expected_block_type(parser.index);
|
||||
}
|
||||
|
||||
/** @param {Parser} parser */
|
||||
function next(parser) {
|
||||
const start = parser.index - 1;
|
||||
|
||||
const block = parser.current(); // TODO type should not be TemplateNode, that's much too broad
|
||||
|
||||
if (block.type === 'IfBlock') {
|
||||
if (!parser.eat('else')) e.expected_token(start, '{:else} or {:else if}');
|
||||
if (parser.eat('if')) e.block_invalid_elseif(start);
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
parser.fragments.pop();
|
||||
|
||||
block.alternate = create_fragment();
|
||||
parser.fragments.push(block.alternate);
|
||||
|
||||
// :else if
|
||||
if (parser.eat('if')) {
|
||||
parser.require_whitespace();
|
||||
|
||||
const expression = read_expression(parser);
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
let elseif_start = start - 1;
|
||||
while (parser.template[elseif_start] !== '{') elseif_start -= 1;
|
||||
|
||||
/** @type {AST.IfBlock} */
|
||||
const child = parser.append({
|
||||
start: elseif_start,
|
||||
end: -1,
|
||||
type: 'IfBlock',
|
||||
elseif: true,
|
||||
test: expression,
|
||||
consequent: create_fragment(),
|
||||
alternate: null,
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
});
|
||||
|
||||
parser.stack.push(child);
|
||||
parser.fragments.pop();
|
||||
parser.fragments.push(child.consequent);
|
||||
} else {
|
||||
// :else
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (block.type === 'EachBlock') {
|
||||
if (!parser.eat('else')) e.expected_token(start, '{:else}');
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
block.fallback = create_fragment();
|
||||
|
||||
parser.fragments.pop();
|
||||
parser.fragments.push(block.fallback);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (block.type === 'AwaitBlock') {
|
||||
if (parser.eat('then')) {
|
||||
if (block.then) {
|
||||
e.block_duplicate_clause(start, '{:then}');
|
||||
}
|
||||
|
||||
if (!parser.eat('}')) {
|
||||
parser.require_whitespace();
|
||||
block.value = read_pattern(parser);
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
}
|
||||
|
||||
block.then = create_fragment();
|
||||
parser.fragments.pop();
|
||||
parser.fragments.push(block.then);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.eat('catch')) {
|
||||
if (block.catch) {
|
||||
e.block_duplicate_clause(start, '{:catch}');
|
||||
}
|
||||
|
||||
if (!parser.eat('}')) {
|
||||
parser.require_whitespace();
|
||||
block.error = read_pattern(parser);
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
}
|
||||
|
||||
block.catch = create_fragment();
|
||||
parser.fragments.pop();
|
||||
parser.fragments.push(block.catch);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
e.expected_token(start, '{:then ...} or {:catch ...}');
|
||||
}
|
||||
|
||||
e.block_invalid_continuation_placement(start);
|
||||
}
|
||||
|
||||
/** @param {Parser} parser */
|
||||
function close(parser) {
|
||||
const start = parser.index - 1;
|
||||
|
||||
let block = parser.current();
|
||||
/** Only relevant/reached for loose parsing mode */
|
||||
let matched;
|
||||
|
||||
switch (block.type) {
|
||||
case 'IfBlock':
|
||||
matched = parser.eat('if', true, false);
|
||||
|
||||
if (!matched) {
|
||||
block.end = start - 1;
|
||||
parser.pop();
|
||||
close(parser);
|
||||
return;
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
while (block.elseif) {
|
||||
block.end = parser.index;
|
||||
parser.stack.pop();
|
||||
block = /** @type {AST.IfBlock} */ (parser.current());
|
||||
}
|
||||
|
||||
block.end = parser.index;
|
||||
parser.pop();
|
||||
return;
|
||||
|
||||
case 'EachBlock':
|
||||
matched = parser.eat('each', true, false);
|
||||
break;
|
||||
case 'KeyBlock':
|
||||
matched = parser.eat('key', true, false);
|
||||
break;
|
||||
case 'AwaitBlock':
|
||||
matched = parser.eat('await', true, false);
|
||||
break;
|
||||
case 'SnippetBlock':
|
||||
matched = parser.eat('snippet', true, false);
|
||||
break;
|
||||
|
||||
case 'RegularElement':
|
||||
if (parser.loose) {
|
||||
matched = false;
|
||||
} else {
|
||||
// TODO handle implicitly closed elements
|
||||
e.block_unexpected_close(start);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
e.block_unexpected_close(start);
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
block.end = start - 1;
|
||||
parser.pop();
|
||||
close(parser);
|
||||
return;
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
block.end = parser.index;
|
||||
parser.pop();
|
||||
}
|
||||
|
||||
/** @param {Parser} parser */
|
||||
function special(parser) {
|
||||
let start = parser.index;
|
||||
while (parser.template[start] !== '{') start -= 1;
|
||||
|
||||
if (parser.eat('html')) {
|
||||
// {@html content} tag
|
||||
parser.require_whitespace();
|
||||
|
||||
const expression = read_expression(parser);
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
parser.append({
|
||||
type: 'HtmlTag',
|
||||
start,
|
||||
end: parser.index,
|
||||
expression,
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.eat('debug')) {
|
||||
/** @type {Identifier[]} */
|
||||
let identifiers;
|
||||
|
||||
// Implies {@debug} which indicates "debug all"
|
||||
if (parser.read(regex_whitespace_with_closing_curly_brace)) {
|
||||
identifiers = [];
|
||||
} else {
|
||||
const expression = read_expression(parser);
|
||||
|
||||
identifiers =
|
||||
expression.type === 'SequenceExpression'
|
||||
? /** @type {Identifier[]} */ (expression.expressions)
|
||||
: [/** @type {Identifier} */ (expression)];
|
||||
|
||||
identifiers.forEach(
|
||||
/** @param {any} node */ (node) => {
|
||||
if (node.type !== 'Identifier') {
|
||||
e.debug_tag_invalid_arguments(/** @type {number} */ (node.start));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
}
|
||||
|
||||
parser.append({
|
||||
type: 'DebugTag',
|
||||
start,
|
||||
end: parser.index,
|
||||
identifiers
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.eat('const')) {
|
||||
parser.require_whitespace();
|
||||
|
||||
const id = read_pattern(parser);
|
||||
parser.allow_whitespace();
|
||||
|
||||
parser.eat('=', true);
|
||||
parser.allow_whitespace();
|
||||
|
||||
const expression_start = parser.index;
|
||||
const init = read_expression(parser);
|
||||
if (
|
||||
init.type === 'SequenceExpression' &&
|
||||
!parser.template.substring(expression_start, init.start).includes('(')
|
||||
) {
|
||||
// const a = (b, c) is allowed but a = b, c = d is not;
|
||||
e.const_tag_invalid_expression(init);
|
||||
}
|
||||
parser.allow_whitespace();
|
||||
|
||||
parser.eat('}', true);
|
||||
|
||||
parser.append({
|
||||
type: 'ConstTag',
|
||||
start,
|
||||
end: parser.index,
|
||||
declaration: {
|
||||
type: 'VariableDeclaration',
|
||||
kind: 'const',
|
||||
declarations: [{ type: 'VariableDeclarator', id, init, start: id.start, end: init.end }],
|
||||
start: start + 2, // start at const, not at @const
|
||||
end: parser.index - 1
|
||||
},
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata()
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.eat('render')) {
|
||||
// {@render foo(...)}
|
||||
parser.require_whitespace();
|
||||
|
||||
const expression = read_expression(parser);
|
||||
|
||||
if (
|
||||
expression.type !== 'CallExpression' &&
|
||||
(expression.type !== 'ChainExpression' || expression.expression.type !== 'CallExpression')
|
||||
) {
|
||||
e.render_tag_invalid_expression(expression);
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
parser.append({
|
||||
type: 'RenderTag',
|
||||
start,
|
||||
end: parser.index,
|
||||
expression: /** @type {AST.RenderTag['expression']} */ (expression),
|
||||
metadata: {
|
||||
expression: new ExpressionMetadata(),
|
||||
dynamic: false,
|
||||
arguments: [],
|
||||
path: [],
|
||||
snippets: new Set()
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
e.expected_tag(parser.index);
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Parser } from '../index.js' */
|
||||
import { decode_character_references } from '../utils/html.js';
|
||||
|
||||
/** @param {Parser} parser */
|
||||
export default function text(parser) {
|
||||
const start = parser.index;
|
||||
|
||||
while (parser.index < parser.template.length && !parser.match('<') && !parser.match('{')) {
|
||||
parser.index++;
|
||||
}
|
||||
|
||||
const data = parser.template.slice(start, parser.index);
|
||||
|
||||
/** @type {AST.Text} */
|
||||
parser.append({
|
||||
type: 'Text',
|
||||
start,
|
||||
end: parser.index,
|
||||
raw: data,
|
||||
data: decode_character_references(data, false)
|
||||
});
|
||||
}
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
/** @import { Parser } from '../index.js' */
|
||||
import * as e from '../../../errors.js';
|
||||
|
||||
/**
|
||||
* @param {number} num
|
||||
* @returns {number} Infinity if {@link num} is negative, else {@link num}.
|
||||
*/
|
||||
function infinity_if_negative(num) {
|
||||
if (num < 0) {
|
||||
return Infinity;
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} string The string to search.
|
||||
* @param {number} search_start_index The index to start searching at.
|
||||
* @param {"'" | '"' | '`'} string_start_char The character that started this string.
|
||||
* @returns {number} The index of the end of this string expression, or `Infinity` if not found.
|
||||
*/
|
||||
function find_string_end(string, search_start_index, string_start_char) {
|
||||
let string_to_search;
|
||||
if (string_start_char === '`') {
|
||||
string_to_search = string;
|
||||
} else {
|
||||
// we could slice at the search start index, but this way the index remains valid
|
||||
string_to_search = string.slice(
|
||||
0,
|
||||
infinity_if_negative(string.indexOf('\n', search_start_index))
|
||||
);
|
||||
}
|
||||
|
||||
return find_unescaped_char(string_to_search, search_start_index, string_start_char);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} string The string to search.
|
||||
* @param {number} search_start_index The index to start searching at.
|
||||
* @returns {number} The index of the end of this regex expression, or `Infinity` if not found.
|
||||
*/
|
||||
function find_regex_end(string, search_start_index) {
|
||||
return find_unescaped_char(string, search_start_index, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} string The string to search.
|
||||
* @param {number} search_start_index The index to begin the search at.
|
||||
* @param {string} char The character to search for.
|
||||
* @returns {number} The index of the first unescaped instance of {@link char}, or `Infinity` if not found.
|
||||
*/
|
||||
function find_unescaped_char(string, search_start_index, char) {
|
||||
let i = search_start_index;
|
||||
while (true) {
|
||||
const found_index = string.indexOf(char, i);
|
||||
if (found_index === -1) {
|
||||
return Infinity;
|
||||
}
|
||||
if (count_leading_backslashes(string, found_index - 1) % 2 === 0) {
|
||||
return found_index;
|
||||
}
|
||||
i = found_index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count consecutive leading backslashes before {@link search_start_index}.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* count_leading_backslashes('\\\\\\foo', 2); // 3 (the backslashes have to be escaped in the string literal, there are three in reality)
|
||||
* ```
|
||||
*
|
||||
* @param {string} string The string to search.
|
||||
* @param {number} search_start_index The index to begin the search at.
|
||||
*/
|
||||
function count_leading_backslashes(string, search_start_index) {
|
||||
let i = search_start_index;
|
||||
let count = 0;
|
||||
while (string[i] === '\\') {
|
||||
count++;
|
||||
i--;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the corresponding closing bracket, ignoring brackets found inside comments, strings, or regex expressions.
|
||||
* @param {string} template The string to search.
|
||||
* @param {number} index The index to begin the search at.
|
||||
* @param {string} open The opening bracket (ex: `'{'` will search for `'}'`).
|
||||
* @returns {number | undefined} The index of the closing bracket, or undefined if not found.
|
||||
*/
|
||||
export function find_matching_bracket(template, index, open) {
|
||||
const close = default_brackets[open];
|
||||
let brackets = 1;
|
||||
let i = index;
|
||||
while (brackets > 0 && i < template.length) {
|
||||
const char = template[i];
|
||||
switch (char) {
|
||||
case "'":
|
||||
case '"':
|
||||
case '`':
|
||||
i = find_string_end(template, i + 1, char) + 1;
|
||||
continue;
|
||||
case '/': {
|
||||
const next_char = template[i + 1];
|
||||
if (!next_char) continue;
|
||||
if (next_char === '/') {
|
||||
i = infinity_if_negative(template.indexOf('\n', i + 1)) + '\n'.length;
|
||||
continue;
|
||||
}
|
||||
if (next_char === '*') {
|
||||
i = infinity_if_negative(template.indexOf('*/', i + 1)) + '*/'.length;
|
||||
continue;
|
||||
}
|
||||
i = find_regex_end(template, i + 1) + '/'.length;
|
||||
continue;
|
||||
}
|
||||
default: {
|
||||
const char = template[i];
|
||||
if (char === open) {
|
||||
brackets++;
|
||||
} else if (char === close) {
|
||||
brackets--;
|
||||
}
|
||||
if (brackets === 0) {
|
||||
return i;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** @type {Record<string, string>} */
|
||||
const default_brackets = {
|
||||
'{': '}',
|
||||
'(': ')',
|
||||
'[': ']'
|
||||
};
|
||||
|
||||
const default_close = new Set(Object.values(default_brackets));
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {number} start
|
||||
* @param {Record<string, string>} brackets
|
||||
*/
|
||||
export function match_bracket(parser, start, brackets = default_brackets) {
|
||||
const close = brackets === default_brackets ? default_close : new Set(Object.values(brackets));
|
||||
const bracket_stack = [];
|
||||
|
||||
let i = start;
|
||||
|
||||
while (i < parser.template.length) {
|
||||
let char = parser.template[i++];
|
||||
|
||||
if (char === "'" || char === '"' || char === '`') {
|
||||
i = match_quote(parser, i, char);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char in brackets) {
|
||||
bracket_stack.push(char);
|
||||
} else if (close.has(char)) {
|
||||
const popped = /** @type {string} */ (bracket_stack.pop());
|
||||
const expected = /** @type {string} */ (brackets[popped]);
|
||||
|
||||
if (char !== expected) {
|
||||
e.expected_token(i - 1, expected);
|
||||
}
|
||||
|
||||
if (bracket_stack.length === 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
e.unexpected_eof(parser.template.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Parser} parser
|
||||
* @param {number} start
|
||||
* @param {string} quote
|
||||
*/
|
||||
function match_quote(parser, start, quote) {
|
||||
let is_escaped = false;
|
||||
let i = start;
|
||||
|
||||
while (i < parser.template.length) {
|
||||
const char = parser.template[i++];
|
||||
|
||||
if (is_escaped) {
|
||||
is_escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === quote) {
|
||||
return i;
|
||||
}
|
||||
|
||||
if (char === '\\') {
|
||||
is_escaped = true;
|
||||
}
|
||||
|
||||
if (quote === '`' && char === '$' && parser.template[i] === '{') {
|
||||
i = match_bracket(parser, i);
|
||||
}
|
||||
}
|
||||
|
||||
e.unterminated_string_constant(start);
|
||||
}
|
||||
+16
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
+2234
File diff suppressed because it is too large
Load Diff
+281
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {string[]} names
|
||||
* @returns {string | null}
|
||||
*/
|
||||
export default function fuzzymatch(name, names) {
|
||||
if (names.length === 0) return null;
|
||||
|
||||
const set = new FuzzySet(names);
|
||||
const matches = set.get(name);
|
||||
|
||||
return matches && matches[0][0] > 0.7 ? matches[0][1] : null;
|
||||
}
|
||||
|
||||
// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js in 2016
|
||||
// BSD Licensed (see https://github.com/Glench/fuzzyset.js/issues/10)
|
||||
|
||||
const GRAM_SIZE_LOWER = 2;
|
||||
const GRAM_SIZE_UPPER = 3;
|
||||
|
||||
// return an edit distance from 0 to 1
|
||||
|
||||
/**
|
||||
* @param {string} str1
|
||||
* @param {string} str2
|
||||
*/
|
||||
function _distance(str1, str2) {
|
||||
if (str1 === null && str2 === null) {
|
||||
throw 'Trying to compare two null values';
|
||||
}
|
||||
if (str1 === null || str2 === null) return 0;
|
||||
str1 = String(str1);
|
||||
str2 = String(str2);
|
||||
|
||||
const distance = levenshtein(str1, str2);
|
||||
return 1 - distance / Math.max(str1.length, str2.length);
|
||||
}
|
||||
|
||||
// helper functions
|
||||
|
||||
/**
|
||||
* @param {string} str1
|
||||
* @param {string} str2
|
||||
*/
|
||||
function levenshtein(str1, str2) {
|
||||
/** @type {number[]} */
|
||||
const current = [];
|
||||
let prev = 0;
|
||||
|
||||
for (let i = 0; i <= str2.length; i++) {
|
||||
for (let j = 0; j <= str1.length; j++) {
|
||||
let value;
|
||||
|
||||
if (i && j) {
|
||||
if (str1.charAt(j - 1) === str2.charAt(i - 1)) {
|
||||
value = prev;
|
||||
} else {
|
||||
value = Math.min(current[j], current[j - 1], prev) + 1;
|
||||
}
|
||||
} else {
|
||||
value = i + j;
|
||||
}
|
||||
|
||||
prev = current[j];
|
||||
current[j] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return /** @type {number} */ (current.pop());
|
||||
}
|
||||
|
||||
const non_word_regex = /[^\w, ]+/;
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @param {any} gram_size
|
||||
*/
|
||||
function iterate_grams(value, gram_size = 2) {
|
||||
const simplified = '-' + value.toLowerCase().replace(non_word_regex, '') + '-';
|
||||
const len_diff = gram_size - simplified.length;
|
||||
const results = [];
|
||||
|
||||
if (len_diff > 0) {
|
||||
for (let i = 0; i < len_diff; ++i) {
|
||||
value += '-';
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < simplified.length - gram_size + 1; ++i) {
|
||||
results.push(simplified.slice(i, i + gram_size));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @param {any} gram_size
|
||||
*/
|
||||
function gram_counter(value, gram_size = 2) {
|
||||
// return an object where key=gram, value=number of occurrences
|
||||
|
||||
/** @type {Record<string, number>} */
|
||||
const result = {};
|
||||
const grams = iterate_grams(value, gram_size);
|
||||
let i = 0;
|
||||
|
||||
for (i; i < grams.length; ++i) {
|
||||
if (grams[i] in result) {
|
||||
result[grams[i]] += 1;
|
||||
} else {
|
||||
result[grams[i]] = 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MatchTuple} a
|
||||
* @param {MatchTuple} b
|
||||
*/
|
||||
function sort_descending(a, b) {
|
||||
return b[0] - a[0];
|
||||
}
|
||||
|
||||
class FuzzySet {
|
||||
/** @type {Record<string, string>} */
|
||||
exact_set = {};
|
||||
|
||||
/** @type {Record<string, [number, number][]>} */
|
||||
match_dict = {};
|
||||
|
||||
/** @type {Record<string, number[]>} */
|
||||
items = {};
|
||||
|
||||
/** @param {string[]} arr */
|
||||
constructor(arr) {
|
||||
// initialisation
|
||||
for (let i = GRAM_SIZE_LOWER; i < GRAM_SIZE_UPPER + 1; ++i) {
|
||||
this.items[i] = [];
|
||||
}
|
||||
|
||||
// add all the items to the set
|
||||
for (let i = 0; i < arr.length; ++i) {
|
||||
this.add(arr[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {string} value */
|
||||
add(value) {
|
||||
const normalized_value = value.toLowerCase();
|
||||
if (normalized_value in this.exact_set) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let i = GRAM_SIZE_LOWER;
|
||||
for (i; i < GRAM_SIZE_UPPER + 1; ++i) {
|
||||
this._add(value, i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @param {number} gram_size
|
||||
*/
|
||||
_add(value, gram_size) {
|
||||
const normalized_value = value.toLowerCase();
|
||||
const items = this.items[gram_size] || [];
|
||||
const index = items.length;
|
||||
|
||||
items.push(0);
|
||||
const gram_counts = gram_counter(normalized_value, gram_size);
|
||||
let sum_of_square_gram_counts = 0;
|
||||
let gram;
|
||||
let gram_count;
|
||||
|
||||
for (gram in gram_counts) {
|
||||
gram_count = gram_counts[gram];
|
||||
sum_of_square_gram_counts += Math.pow(gram_count, 2);
|
||||
if (gram in this.match_dict) {
|
||||
this.match_dict[gram].push([index, gram_count]);
|
||||
} else {
|
||||
this.match_dict[gram] = [[index, gram_count]];
|
||||
}
|
||||
}
|
||||
const vector_normal = Math.sqrt(sum_of_square_gram_counts);
|
||||
// @ts-ignore no idea what this code is doing
|
||||
items[index] = [vector_normal, normalized_value];
|
||||
this.items[gram_size] = items;
|
||||
this.exact_set[normalized_value] = value;
|
||||
}
|
||||
|
||||
/** @param {string} value */
|
||||
get(value) {
|
||||
const normalized_value = value.toLowerCase();
|
||||
const result = this.exact_set[normalized_value];
|
||||
|
||||
if (result) {
|
||||
return /** @type {MatchTuple[]} */ ([[1, result]]);
|
||||
}
|
||||
|
||||
// start with high gram size and if there are no results, go to lower gram sizes
|
||||
for (let gram_size = GRAM_SIZE_UPPER; gram_size >= GRAM_SIZE_LOWER; --gram_size) {
|
||||
const results = this.__get(value, gram_size);
|
||||
if (results.length > 0) return results;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @param {number} gram_size
|
||||
* @returns {MatchTuple[]}
|
||||
*/
|
||||
__get(value, gram_size) {
|
||||
const normalized_value = value.toLowerCase();
|
||||
|
||||
/** @type {Record<string, number>} */
|
||||
const matches = {};
|
||||
const gram_counts = gram_counter(normalized_value, gram_size);
|
||||
const items = this.items[gram_size];
|
||||
let sum_of_square_gram_counts = 0;
|
||||
let gram;
|
||||
let gram_count;
|
||||
let i;
|
||||
let index;
|
||||
let other_gram_count;
|
||||
|
||||
for (gram in gram_counts) {
|
||||
gram_count = gram_counts[gram];
|
||||
sum_of_square_gram_counts += Math.pow(gram_count, 2);
|
||||
if (gram in this.match_dict) {
|
||||
for (i = 0; i < this.match_dict[gram].length; ++i) {
|
||||
index = this.match_dict[gram][i][0];
|
||||
other_gram_count = this.match_dict[gram][i][1];
|
||||
if (index in matches) {
|
||||
matches[index] += gram_count * other_gram_count;
|
||||
} else {
|
||||
matches[index] = gram_count * other_gram_count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const vector_normal = Math.sqrt(sum_of_square_gram_counts);
|
||||
|
||||
/** @type {MatchTuple[]} */
|
||||
let results = [];
|
||||
let match_score;
|
||||
|
||||
// build a results list of [score, str]
|
||||
for (const match_index in matches) {
|
||||
match_score = matches[match_index];
|
||||
// @ts-ignore no idea what this code is doing
|
||||
results.push([match_score / (vector_normal * items[match_index][0]), items[match_index][1]]);
|
||||
}
|
||||
|
||||
results.sort(sort_descending);
|
||||
|
||||
/** @type {MatchTuple[]} */
|
||||
let new_results = [];
|
||||
const end_index = Math.min(50, results.length);
|
||||
// truncate somewhat arbitrarily to 50
|
||||
for (let i = 0; i < end_index; ++i) {
|
||||
// @ts-ignore no idea what this code is doing
|
||||
new_results.push([_distance(results[i][1], normalized_value), results[i][1]]);
|
||||
}
|
||||
results = new_results;
|
||||
results.sort(sort_descending);
|
||||
|
||||
new_results = [];
|
||||
for (let i = 0; i < results.length; ++i) {
|
||||
if (results[i][0] === results[0][0]) {
|
||||
// @ts-ignore no idea what this code is doing
|
||||
new_results.push([results[i][0], this.exact_set[results[i][1]]]);
|
||||
}
|
||||
}
|
||||
|
||||
return new_results;
|
||||
}
|
||||
}
|
||||
|
||||
/** @typedef {[score: number, match: string]} MatchTuple */
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
import entities from './entities.js';
|
||||
|
||||
const windows_1252 = [
|
||||
8364, 129, 8218, 402, 8222, 8230, 8224, 8225, 710, 8240, 352, 8249, 338, 141, 381, 143, 144, 8216,
|
||||
8217, 8220, 8221, 8226, 8211, 8212, 732, 8482, 353, 8250, 339, 157, 382, 376
|
||||
];
|
||||
|
||||
/**
|
||||
* @param {string} entity_name
|
||||
* @param {boolean} is_attribute_value
|
||||
*/
|
||||
function reg_exp_entity(entity_name, is_attribute_value) {
|
||||
// https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state
|
||||
// doesn't decode the html entity which not ends with ; and next character is =, number or alphabet in attribute value.
|
||||
if (is_attribute_value && !entity_name.endsWith(';')) {
|
||||
return `${entity_name}\\b(?!=)`;
|
||||
}
|
||||
return entity_name;
|
||||
}
|
||||
|
||||
/** @param {boolean} is_attribute_value */
|
||||
function get_entity_pattern(is_attribute_value) {
|
||||
const reg_exp_num = '#(?:x[a-fA-F\\d]+|\\d+)(?:;)?';
|
||||
const reg_exp_entities = Object.keys(entities).map(
|
||||
/** @param {any} entity_name */ (entity_name) => reg_exp_entity(entity_name, is_attribute_value)
|
||||
);
|
||||
|
||||
const entity_pattern = new RegExp(`&(${reg_exp_num}|${reg_exp_entities.join('|')})`, 'g');
|
||||
|
||||
return entity_pattern;
|
||||
}
|
||||
|
||||
const entity_pattern_content = get_entity_pattern(false);
|
||||
const entity_pattern_attr_value = get_entity_pattern(true);
|
||||
|
||||
/**
|
||||
* @param {string} html
|
||||
* @param {boolean} is_attribute_value
|
||||
*/
|
||||
export function decode_character_references(html, is_attribute_value) {
|
||||
const entity_pattern = is_attribute_value ? entity_pattern_attr_value : entity_pattern_content;
|
||||
return html.replace(
|
||||
entity_pattern,
|
||||
/**
|
||||
* @param {any} match
|
||||
* @param {keyof typeof entities} entity
|
||||
*/ (match, entity) => {
|
||||
let code;
|
||||
|
||||
// Handle named entities
|
||||
if (entity[0] !== '#') {
|
||||
code = entities[entity];
|
||||
} else if (entity[1] === 'x') {
|
||||
code = parseInt(entity.substring(2), 16);
|
||||
} else {
|
||||
code = parseInt(entity.substring(1), 10);
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
return match;
|
||||
}
|
||||
|
||||
return String.fromCodePoint(validate_code(code));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const NUL = 0;
|
||||
|
||||
// some code points are verboten. If we were inserting HTML, the browser would replace the illegal
|
||||
// code points with alternatives in some cases - since we're bypassing that mechanism, we need
|
||||
// to replace them ourselves
|
||||
//
|
||||
// Source: http://en.wikipedia.org/wiki/Character_encodings_in_HTML#Illegal_characters
|
||||
// Also see: https://en.wikipedia.org/wiki/Plane_(Unicode)
|
||||
// Also see: https://html.spec.whatwg.org/multipage/parsing.html#preprocessing-the-input-stream
|
||||
|
||||
/** @param {number} code */
|
||||
function validate_code(code) {
|
||||
// line feed becomes generic whitespace
|
||||
if (code === 10) {
|
||||
return 32;
|
||||
}
|
||||
|
||||
// ASCII range. (Why someone would use HTML entities for ASCII characters I don't know, but...)
|
||||
if (code < 128) {
|
||||
return code;
|
||||
}
|
||||
|
||||
// code points 128-159 are dealt with leniently by browsers, but they're incorrect. We need
|
||||
// to correct the mistake or we'll end up with missing € signs and so on
|
||||
if (code <= 159) {
|
||||
return windows_1252[code - 128];
|
||||
}
|
||||
|
||||
// basic multilingual plane
|
||||
if (code < 55296) {
|
||||
return code;
|
||||
}
|
||||
|
||||
// UTF-16 surrogate halves
|
||||
if (code <= 57343) {
|
||||
return NUL;
|
||||
}
|
||||
|
||||
// rest of the basic multilingual plane
|
||||
if (code <= 65535) {
|
||||
return code;
|
||||
}
|
||||
|
||||
// supplementary multilingual plane 0x10000 - 0x1ffff
|
||||
if (code >= 65536 && code <= 131071) {
|
||||
return code;
|
||||
}
|
||||
|
||||
// supplementary ideographic plane 0x20000 - 0x2ffff
|
||||
if (code >= 131072 && code <= 196607) {
|
||||
return code;
|
||||
}
|
||||
|
||||
// supplementary special-purpose plane 0xe0000 - 0xe07f and 0xe0100 - 0xe01ef
|
||||
if ((code >= 917504 && code <= 917631) || (code >= 917760 && code <= 917999)) {
|
||||
return code;
|
||||
}
|
||||
|
||||
return NUL;
|
||||
}
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
/** @import { ComponentAnalysis } from '../../types.js' */
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Visitors } from 'zimmerframe' */
|
||||
import { walk } from 'zimmerframe';
|
||||
import * as e from '../../../errors.js';
|
||||
import { is_keyframes_node } from '../../css.js';
|
||||
import { is_global, is_unscoped_pseudo_class } from './utils.js';
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* keyframes: string[];
|
||||
* rule: AST.CSS.Rule | null;
|
||||
* analysis: ComponentAnalysis;
|
||||
* }} CssState
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Visitors<AST.CSS.Node, CssState>} CssVisitors
|
||||
*/
|
||||
|
||||
/**
|
||||
* True if is `:global`
|
||||
* @param {AST.CSS.SimpleSelector} simple_selector
|
||||
*/
|
||||
function is_global_block_selector(simple_selector) {
|
||||
return (
|
||||
simple_selector.type === 'PseudoClassSelector' &&
|
||||
simple_selector.name === 'global' &&
|
||||
simple_selector.args === null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AST.SvelteNode[]} path
|
||||
*/
|
||||
function is_unscoped(path) {
|
||||
return path
|
||||
.filter((node) => node.type === 'Rule')
|
||||
.every((node) => node.metadata.has_global_selectors);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Array<AST.CSS.Node>} path
|
||||
*/
|
||||
function is_in_global_block(path) {
|
||||
return path.some((node) => node.type === 'Rule' && node.metadata.is_global_block);
|
||||
}
|
||||
|
||||
/** @type {CssVisitors} */
|
||||
const css_visitors = {
|
||||
Atrule(node, context) {
|
||||
if (is_keyframes_node(node)) {
|
||||
if (!node.prelude.startsWith('-global-') && !is_in_global_block(context.path)) {
|
||||
context.state.keyframes.push(node.prelude);
|
||||
} else if (node.prelude.startsWith('-global-')) {
|
||||
// we don't check if the block.children.length because the keyframe is still added even if empty
|
||||
context.state.analysis.css.has_global ||= is_unscoped(context.path);
|
||||
}
|
||||
}
|
||||
|
||||
context.next();
|
||||
},
|
||||
ComplexSelector(node, context) {
|
||||
context.next(); // analyse relevant selectors first
|
||||
|
||||
{
|
||||
const global = node.children.find(is_global);
|
||||
|
||||
if (global) {
|
||||
const is_nested = context.path.at(-2)?.type === 'PseudoClassSelector';
|
||||
if (is_nested && !global.selectors[0].args) {
|
||||
e.css_global_block_invalid_placement(global.selectors[0]);
|
||||
}
|
||||
|
||||
const idx = node.children.indexOf(global);
|
||||
if (global.selectors[0].args !== null && idx !== 0 && idx !== node.children.length - 1) {
|
||||
// ensure `:global(...)` is not used in the middle of a selector (but multiple `global(...)` in sequence are ok)
|
||||
for (let i = idx + 1; i < node.children.length; i++) {
|
||||
if (!is_global(node.children[i])) {
|
||||
e.css_global_invalid_placement(global.selectors[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ensure `:global(...)` do not lead to invalid css after `:global()` is removed
|
||||
for (const relative_selector of node.children) {
|
||||
for (let i = 0; i < relative_selector.selectors.length; i++) {
|
||||
const selector = relative_selector.selectors[i];
|
||||
|
||||
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
|
||||
const child = selector.args?.children[0].children[0];
|
||||
// ensure `:global(element)` to be at the first position in a compound selector
|
||||
if (child?.selectors[0].type === 'TypeSelector' && i !== 0) {
|
||||
e.css_global_invalid_selector_list(selector);
|
||||
}
|
||||
|
||||
// ensure `:global(.class)` is not followed by a type selector, eg: `:global(.class)element`
|
||||
if (relative_selector.selectors[i + 1]?.type === 'TypeSelector') {
|
||||
e.css_type_selector_invalid_placement(relative_selector.selectors[i + 1]);
|
||||
}
|
||||
|
||||
// ensure `:global(...)`contains a single selector
|
||||
// (standalone :global() with multiple selectors is OK)
|
||||
if (
|
||||
selector.args !== null &&
|
||||
selector.args.children.length > 1 &&
|
||||
(node.children.length > 1 || relative_selector.selectors.length > 1)
|
||||
) {
|
||||
e.css_global_invalid_selector(selector);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node.metadata.rule = context.state.rule;
|
||||
|
||||
node.metadata.is_global = node.children.every(
|
||||
({ metadata }) => metadata.is_global || metadata.is_global_like
|
||||
);
|
||||
|
||||
node.metadata.used ||= node.metadata.is_global;
|
||||
|
||||
if (
|
||||
node.metadata.rule?.metadata.parent_rule &&
|
||||
node.children[0]?.selectors[0]?.type === 'NestingSelector'
|
||||
) {
|
||||
const first = node.children[0]?.selectors[1];
|
||||
const no_nesting_scope =
|
||||
first?.type !== 'PseudoClassSelector' || is_unscoped_pseudo_class(first);
|
||||
const parent_is_global = node.metadata.rule.metadata.parent_rule.prelude.children.some(
|
||||
(child) => child.children.length === 1 && child.children[0].metadata.is_global
|
||||
);
|
||||
// mark `&:hover` in `:global(.foo) { &:hover { color: green }}` as used
|
||||
if (no_nesting_scope && parent_is_global) {
|
||||
node.metadata.used = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
RelativeSelector(node, context) {
|
||||
const parent = /** @type {AST.CSS.ComplexSelector} */ (context.path.at(-1));
|
||||
|
||||
if (
|
||||
node.combinator != null &&
|
||||
!context.state.rule?.metadata.parent_rule &&
|
||||
parent.children[0] === node &&
|
||||
context.path.at(-3)?.type !== 'PseudoClassSelector'
|
||||
) {
|
||||
e.css_selector_invalid(node.combinator);
|
||||
}
|
||||
|
||||
node.metadata.is_global = node.selectors.length >= 1 && is_global(node);
|
||||
|
||||
if (
|
||||
node.selectors.length >= 1 &&
|
||||
node.selectors.every(
|
||||
(selector) =>
|
||||
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
|
||||
)
|
||||
) {
|
||||
const first = node.selectors[0];
|
||||
node.metadata.is_global_like ||=
|
||||
(first.type === 'PseudoClassSelector' && first.name === 'host') ||
|
||||
(first.type === 'PseudoElementSelector' &&
|
||||
[
|
||||
'view-transition',
|
||||
'view-transition-group',
|
||||
'view-transition-old',
|
||||
'view-transition-new',
|
||||
'view-transition-image-pair'
|
||||
].includes(first.name));
|
||||
}
|
||||
|
||||
node.metadata.is_global_like ||=
|
||||
node.selectors.some(
|
||||
(child) => child.type === 'PseudoClassSelector' && child.name === 'root'
|
||||
) &&
|
||||
// :root.y:has(.x) is not a global selector because while .y is unscoped, .x inside `:has(...)` should be scoped
|
||||
!node.selectors.some((child) => child.type === 'PseudoClassSelector' && child.name === 'has');
|
||||
|
||||
if (node.metadata.is_global_like || node.metadata.is_global) {
|
||||
// So that nested selectors like `:root:not(.x)` are not marked as unused
|
||||
for (const child of node.selectors) {
|
||||
walk(/** @type {AST.CSS.Node} */ (child), null, {
|
||||
ComplexSelector(node, context) {
|
||||
node.metadata.used = true;
|
||||
context.next();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
context.next();
|
||||
},
|
||||
Rule(node, context) {
|
||||
node.metadata.parent_rule = context.state.rule;
|
||||
|
||||
// We gotta allow :global x, :global y because CSS preprocessors might generate that from :global { x, y {...} }
|
||||
for (const complex_selector of node.prelude.children) {
|
||||
let is_global_block = false;
|
||||
|
||||
for (let selector_idx = 0; selector_idx < complex_selector.children.length; selector_idx++) {
|
||||
const child = complex_selector.children[selector_idx];
|
||||
const idx = child.selectors.findIndex(is_global_block_selector);
|
||||
|
||||
if (is_global_block) {
|
||||
// All selectors after :global are unscoped
|
||||
child.metadata.is_global_like = true;
|
||||
}
|
||||
|
||||
if (idx === 0) {
|
||||
if (
|
||||
child.selectors.length > 1 &&
|
||||
selector_idx === 0 &&
|
||||
node.metadata.parent_rule === null
|
||||
) {
|
||||
e.css_global_block_invalid_modifier_start(child.selectors[1]);
|
||||
} else {
|
||||
// `child` starts with `:global`
|
||||
node.metadata.is_global_block = is_global_block = true;
|
||||
|
||||
for (let i = 1; i < child.selectors.length; i++) {
|
||||
walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, {
|
||||
ComplexSelector(node) {
|
||||
node.metadata.used = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (child.combinator && child.combinator.name !== ' ') {
|
||||
e.css_global_block_invalid_combinator(child, child.combinator.name);
|
||||
}
|
||||
|
||||
const declaration = node.block.children.find((child) => child.type === 'Declaration');
|
||||
const is_lone_global =
|
||||
complex_selector.children.length === 1 &&
|
||||
complex_selector.children[0].selectors.length === 1; // just `:global`, not e.g. `:global x`
|
||||
|
||||
if (is_lone_global && node.prelude.children.length > 1) {
|
||||
// `:global, :global x { z { ... } }` would become `x { z { ... } }` which means `z` is always
|
||||
// constrained by `x`, which is not what the user intended
|
||||
e.css_global_block_invalid_list(node.prelude);
|
||||
}
|
||||
|
||||
if (
|
||||
declaration &&
|
||||
// :global { color: red; } is invalid, but foo :global { color: red; } is valid
|
||||
node.prelude.children.length === 1 &&
|
||||
is_lone_global
|
||||
) {
|
||||
e.css_global_block_invalid_declaration(declaration);
|
||||
}
|
||||
}
|
||||
} else if (idx !== -1) {
|
||||
e.css_global_block_invalid_modifier(child.selectors[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.metadata.is_global_block && !is_global_block) {
|
||||
e.css_global_block_invalid_list(node.prelude);
|
||||
}
|
||||
}
|
||||
|
||||
const state = { ...context.state, rule: node };
|
||||
|
||||
// visit selector list first, to populate child selector metadata
|
||||
context.visit(node.prelude, state);
|
||||
|
||||
for (const selector of node.prelude.children) {
|
||||
node.metadata.has_global_selectors ||= selector.metadata.is_global;
|
||||
node.metadata.has_local_selectors ||= !selector.metadata.is_global;
|
||||
}
|
||||
|
||||
// if this rule has a ComplexSelector whose RelativeSelector children are all
|
||||
// `:global(...)`, and the rule contains declarations (rather than just
|
||||
// nested rules) then the component as a whole includes global CSS
|
||||
context.state.analysis.css.has_global ||=
|
||||
node.metadata.has_global_selectors &&
|
||||
node.block.children.filter((child) => child.type === 'Declaration').length > 0 &&
|
||||
is_unscoped(context.path);
|
||||
|
||||
// visit block list, so parent rule metadata is populated
|
||||
context.visit(node.block, state);
|
||||
},
|
||||
NestingSelector(node, context) {
|
||||
const rule = /** @type {AST.CSS.Rule} */ (context.state.rule);
|
||||
const parent_rule = rule.metadata.parent_rule;
|
||||
|
||||
if (!parent_rule) {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/Nesting_selector#using_outside_nested_rule
|
||||
const children = rule.prelude.children;
|
||||
const selectors = children[0].children[0].selectors;
|
||||
if (
|
||||
children.length > 1 ||
|
||||
selectors.length > 1 ||
|
||||
selectors[0].type !== 'PseudoClassSelector' ||
|
||||
selectors[0].name !== 'global' ||
|
||||
selectors[0].args?.children[0]?.children[0].selectors[0] !== node
|
||||
) {
|
||||
e.css_nesting_selector_invalid_placement(node);
|
||||
}
|
||||
} else if (
|
||||
// :global { &.foo { ... } } is invalid
|
||||
parent_rule.metadata.is_global_block &&
|
||||
!parent_rule.metadata.parent_rule &&
|
||||
parent_rule.prelude.children[0].children.length === 1 &&
|
||||
parent_rule.prelude.children[0].children[0].selectors.length === 1
|
||||
) {
|
||||
e.css_global_block_invalid_modifier_start(node);
|
||||
}
|
||||
|
||||
context.next();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {AST.CSS.StyleSheet} stylesheet
|
||||
* @param {ComponentAnalysis} analysis
|
||||
*/
|
||||
export function analyze_css(stylesheet, analysis) {
|
||||
/** @type {CssState} */
|
||||
const css_state = {
|
||||
keyframes: analysis.css.keyframes,
|
||||
rule: null,
|
||||
analysis
|
||||
};
|
||||
|
||||
walk(stylesheet, css_state, css_visitors);
|
||||
}
|
||||
+1247
File diff suppressed because it is too large
Load Diff
+47
@@ -0,0 +1,47 @@
|
||||
/** @import { Visitors } from 'zimmerframe' */
|
||||
/** @import { AST } from '#compiler' */
|
||||
import { walk } from 'zimmerframe';
|
||||
import * as w from '../../../warnings.js';
|
||||
import { is_keyframes_node } from '../../css.js';
|
||||
|
||||
/**
|
||||
* @param {AST.CSS.StyleSheet} stylesheet
|
||||
*/
|
||||
export function warn_unused(stylesheet) {
|
||||
walk(stylesheet, { stylesheet }, visitors);
|
||||
}
|
||||
|
||||
/** @type {Visitors<AST.CSS.Node, { stylesheet: AST.CSS.StyleSheet }>} */
|
||||
const visitors = {
|
||||
Atrule(node, context) {
|
||||
if (!is_keyframes_node(node)) {
|
||||
context.next();
|
||||
}
|
||||
},
|
||||
PseudoClassSelector(node, context) {
|
||||
if (node.name === 'is' || node.name === 'where') {
|
||||
context.next();
|
||||
}
|
||||
},
|
||||
ComplexSelector(node, context) {
|
||||
if (
|
||||
!node.metadata.used &&
|
||||
// prevent double-marking of `.unused:is(.unused)`
|
||||
(context.path.at(-2)?.type !== 'PseudoClassSelector' ||
|
||||
/** @type {AST.CSS.ComplexSelector} */ (context.path.at(-4))?.metadata.used)
|
||||
) {
|
||||
const content = context.state.stylesheet.content;
|
||||
const text = content.styles.substring(node.start - content.start, node.end - content.start);
|
||||
w.css_unused_selector(node, text);
|
||||
}
|
||||
|
||||
context.next();
|
||||
},
|
||||
Rule(node, context) {
|
||||
if (node.metadata.is_global_block) {
|
||||
context.visit(node.prelude);
|
||||
} else {
|
||||
context.next();
|
||||
}
|
||||
}
|
||||
};
|
||||
+177
@@ -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'
|
||||
))
|
||||
);
|
||||
}
|
||||
+1325
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @template T
|
||||
* @param {Array<[T, T]>} edges
|
||||
* @returns {Array<T>|undefined}
|
||||
*/
|
||||
export default function check_graph_for_cycles(edges) {
|
||||
/** @type {Map<T, T[]>} */
|
||||
const graph = edges.reduce((g, edge) => {
|
||||
const [u, v] = edge;
|
||||
if (!g.has(u)) g.set(u, []);
|
||||
if (!g.has(v)) g.set(v, []);
|
||||
g.get(u).push(v);
|
||||
return g;
|
||||
}, new Map());
|
||||
|
||||
const visited = new Set();
|
||||
/** @type {Set<T>} */
|
||||
const on_stack = new Set();
|
||||
/** @type {Array<Array<T>>} */
|
||||
const cycles = [];
|
||||
|
||||
/**
|
||||
* @param {T} v
|
||||
*/
|
||||
function visit(v) {
|
||||
visited.add(v);
|
||||
on_stack.add(v);
|
||||
|
||||
graph.get(v)?.forEach((w) => {
|
||||
if (!visited.has(w)) {
|
||||
visit(w);
|
||||
} else if (on_stack.has(w)) {
|
||||
cycles.push([...on_stack, w]);
|
||||
}
|
||||
});
|
||||
|
||||
on_stack.delete(v);
|
||||
}
|
||||
|
||||
graph.forEach((_, v) => {
|
||||
if (!visited.has(v)) {
|
||||
visit(v);
|
||||
}
|
||||
});
|
||||
|
||||
return cycles[0];
|
||||
}
|
||||
Generated
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
/** @import { Context } from '../types' */
|
||||
/** @import { AST } from '#compiler'; */
|
||||
import * as e from '../../../errors.js';
|
||||
|
||||
/**
|
||||
* @param {AST.AnimateDirective} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function AnimateDirective(node, context) {
|
||||
context.next({ ...context.state, expression: node.metadata.expression });
|
||||
|
||||
if (node.metadata.expression.has_await) {
|
||||
e.illegal_await_expression(node);
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+11
@@ -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);
|
||||
}
|
||||
Generated
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
/** @import { AssignmentExpression } from 'estree' */
|
||||
/** @import { Context } from '../types' */
|
||||
import { extract_identifiers, object } from '../../../utils/ast.js';
|
||||
import { validate_assignment } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* @param {AssignmentExpression} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function AssignmentExpression(node, context) {
|
||||
validate_assignment(node, node.left, context);
|
||||
|
||||
if (context.state.reactive_statement) {
|
||||
const id = node.left.type === 'MemberExpression' ? object(node.left) : node.left;
|
||||
if (id !== null) {
|
||||
for (const id of extract_identifiers(node.left)) {
|
||||
const binding = context.state.scope.get(id.name);
|
||||
|
||||
if (binding) {
|
||||
context.state.reactive_statement.assignments.add(binding);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (context.state.expression) {
|
||||
context.state.expression.has_assignment = true;
|
||||
}
|
||||
|
||||
context.next();
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import { mark_subtree_dynamic } from './shared/fragment.js';
|
||||
import * as e from '../../../errors.js';
|
||||
|
||||
/**
|
||||
* @param {AST.AttachTag} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function AttachTag(node, context) {
|
||||
mark_subtree_dynamic(context.path);
|
||||
context.next({ ...context.state, expression: node.metadata.expression });
|
||||
|
||||
if (node.metadata.expression.has_await) {
|
||||
e.illegal_await_expression(node);
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import { cannot_be_set_statically, can_delegate_event } from '../../../../utils.js';
|
||||
import { get_attribute_chunks, is_event_attribute } from '../../../utils/ast.js';
|
||||
import { mark_subtree_dynamic } from './shared/fragment.js';
|
||||
|
||||
/**
|
||||
* @param {AST.Attribute} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function Attribute(node, context) {
|
||||
context.next();
|
||||
|
||||
const parent = /** @type {AST.SvelteNode} */ (context.path.at(-1));
|
||||
|
||||
if (parent.type === 'RegularElement') {
|
||||
// special case <option value="" />
|
||||
if (node.name === 'value' && parent.name === 'option') {
|
||||
mark_subtree_dynamic(context.path);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_event_attribute(node)) {
|
||||
mark_subtree_dynamic(context.path);
|
||||
}
|
||||
|
||||
if (cannot_be_set_statically(node.name)) {
|
||||
mark_subtree_dynamic(context.path);
|
||||
}
|
||||
|
||||
// class={[...]} or class={{...}} or `class={x}` need clsx to resolve the classes
|
||||
if (
|
||||
node.name === 'class' &&
|
||||
!Array.isArray(node.value) &&
|
||||
node.value !== true &&
|
||||
node.value.expression.type !== 'Literal' &&
|
||||
node.value.expression.type !== 'TemplateLiteral' &&
|
||||
node.value.expression.type !== 'BinaryExpression'
|
||||
) {
|
||||
mark_subtree_dynamic(context.path);
|
||||
node.metadata.needs_clsx = true;
|
||||
}
|
||||
|
||||
if (node.value !== true) {
|
||||
for (const chunk of get_attribute_chunks(node.value)) {
|
||||
if (chunk.type !== 'ExpressionTag') continue;
|
||||
|
||||
if (
|
||||
chunk.expression.type === 'FunctionExpression' ||
|
||||
chunk.expression.type === 'ArrowFunctionExpression'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_event_attribute(node)) {
|
||||
const parent = context.path.at(-1);
|
||||
if (parent?.type === 'RegularElement' || parent?.type === 'SvelteElement') {
|
||||
context.state.analysis.uses_event_attributes = true;
|
||||
}
|
||||
|
||||
node.metadata.delegated =
|
||||
parent?.type === 'RegularElement' && can_delegate_event(node.name.slice(2));
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
|
||||
import * as e from '../../../errors.js';
|
||||
import { mark_subtree_dynamic } from './shared/fragment.js';
|
||||
|
||||
/**
|
||||
* @param {AST.AwaitBlock} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function AwaitBlock(node, context) {
|
||||
validate_block_not_empty(node.pending, context);
|
||||
validate_block_not_empty(node.then, context);
|
||||
validate_block_not_empty(node.catch, context);
|
||||
|
||||
if (context.state.analysis.runes) {
|
||||
validate_opening_tag(node, context.state, '#');
|
||||
|
||||
if (node.value) {
|
||||
const start = /** @type {number} */ (node.value.start);
|
||||
const match = context.state.analysis.source
|
||||
.substring(start - 10, start)
|
||||
.match(/{(\s*):then\s+$/);
|
||||
|
||||
if (match && match[1] !== '') {
|
||||
e.block_unexpected_character({ start: start - 10, end: start }, ':');
|
||||
}
|
||||
}
|
||||
|
||||
if (node.error) {
|
||||
const start = /** @type {number} */ (node.error.start);
|
||||
const match = context.state.analysis.source
|
||||
.substring(start - 10, start)
|
||||
.match(/{(\s*):catch\s+$/);
|
||||
|
||||
if (match && match[1] !== '') {
|
||||
e.block_unexpected_character({ start: start - 10, end: start }, ':');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mark_subtree_dynamic(context.path);
|
||||
|
||||
context.visit(node.expression, { ...context.state, expression: node.metadata.expression });
|
||||
if (node.pending) context.visit(node.pending);
|
||||
if (node.then) context.visit(node.then);
|
||||
if (node.catch) context.visit(node.catch);
|
||||
}
|
||||
Generated
Vendored
+150
@@ -0,0 +1,150 @@
|
||||
/** @import { AwaitExpression, Expression, SpreadElement, Property } from 'estree' */
|
||||
/** @import { Context } from '../types' */
|
||||
/** @import { AST } from '#compiler' */
|
||||
import * as e from '../../../errors.js';
|
||||
|
||||
/**
|
||||
* @param {AwaitExpression} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function AwaitExpression(node, context) {
|
||||
const tla = context.state.ast_type === 'instance' && context.state.function_depth === 1;
|
||||
|
||||
// preserve context for awaits that precede other expressions in template or `$derived(...)`
|
||||
if (
|
||||
is_reactive_expression(
|
||||
context.path,
|
||||
context.state.derived_function_depth === context.state.function_depth
|
||||
) &&
|
||||
!is_last_evaluated_expression(context.path, node)
|
||||
) {
|
||||
context.state.analysis.pickled_awaits.add(node);
|
||||
}
|
||||
|
||||
let suspend = tla;
|
||||
|
||||
if (context.state.expression) {
|
||||
context.state.expression.has_await = true;
|
||||
|
||||
suspend = true;
|
||||
}
|
||||
|
||||
// disallow top-level `await` or `await` in template expressions
|
||||
// unless a) in runes mode and b) opted into `experimental.async`
|
||||
if (suspend) {
|
||||
if (!context.state.options.experimental.async) {
|
||||
e.experimental_async(node);
|
||||
}
|
||||
|
||||
if (!context.state.analysis.runes) {
|
||||
e.legacy_await_invalid(node);
|
||||
}
|
||||
}
|
||||
|
||||
context.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AST.SvelteNode[]} path
|
||||
* @param {boolean} in_derived
|
||||
*/
|
||||
export function is_reactive_expression(path, in_derived) {
|
||||
if (in_derived) return true;
|
||||
|
||||
let i = path.length;
|
||||
|
||||
while (i--) {
|
||||
const parent = path[i];
|
||||
|
||||
if (
|
||||
parent.type === 'ArrowFunctionExpression' ||
|
||||
parent.type === 'FunctionExpression' ||
|
||||
parent.type === 'FunctionDeclaration'
|
||||
) {
|
||||
// No reactive expression found between function and await
|
||||
return false;
|
||||
}
|
||||
|
||||
// @ts-expect-error we could probably use a neater/more robust mechanism
|
||||
if (parent.metadata) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AST.SvelteNode[]} path
|
||||
* @param {Expression | SpreadElement | Property} node
|
||||
*/
|
||||
function is_last_evaluated_expression(path, node) {
|
||||
let i = path.length;
|
||||
|
||||
while (i--) {
|
||||
const parent = path[i];
|
||||
|
||||
if (parent.type === 'ConstTag') {
|
||||
// {@const ...} tags are treated as deriveds and its contents should all get the preserve-reactivity treatment
|
||||
return false;
|
||||
}
|
||||
|
||||
// @ts-expect-error we could probably use a neater/more robust mechanism
|
||||
if (parent.metadata) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (parent.type) {
|
||||
case 'ArrayExpression':
|
||||
if (node !== parent.elements.at(-1)) return false;
|
||||
break;
|
||||
|
||||
case 'AssignmentExpression':
|
||||
case 'BinaryExpression':
|
||||
case 'LogicalExpression':
|
||||
if (node === parent.left) return false;
|
||||
break;
|
||||
|
||||
case 'CallExpression':
|
||||
case 'NewExpression':
|
||||
if (node !== parent.arguments.at(-1)) return false;
|
||||
break;
|
||||
|
||||
case 'ConditionalExpression':
|
||||
if (node === parent.test) return false;
|
||||
break;
|
||||
|
||||
case 'MemberExpression':
|
||||
if (parent.computed && node === parent.object) return false;
|
||||
break;
|
||||
|
||||
case 'ObjectExpression':
|
||||
if (node !== parent.properties.at(-1)) return false;
|
||||
break;
|
||||
|
||||
case 'Property':
|
||||
if (node === parent.key) return false;
|
||||
break;
|
||||
|
||||
case 'SequenceExpression':
|
||||
if (node !== parent.expressions.at(-1)) return false;
|
||||
break;
|
||||
|
||||
case 'TaggedTemplateExpression':
|
||||
if (node !== parent.quasi.expressions.at(-1)) return false;
|
||||
break;
|
||||
|
||||
case 'TemplateLiteral':
|
||||
if (node !== parent.expressions.at(-1)) return false;
|
||||
break;
|
||||
|
||||
case 'VariableDeclarator':
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
node = parent;
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+280
@@ -0,0 +1,280 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import {
|
||||
extract_all_identifiers_from_expression,
|
||||
is_text_attribute,
|
||||
object
|
||||
} from '../../../utils/ast.js';
|
||||
import { validate_assignment } from './shared/utils.js';
|
||||
import * as e from '../../../errors.js';
|
||||
import * as w from '../../../warnings.js';
|
||||
import { binding_properties } from '../../bindings.js';
|
||||
import fuzzymatch from '../../1-parse/utils/fuzzymatch.js';
|
||||
import { is_content_editable_binding, is_svg } from '../../../../utils.js';
|
||||
import { mark_subtree_dynamic } from './shared/fragment.js';
|
||||
|
||||
/**
|
||||
* @param {AST.BindDirective} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function BindDirective(node, context) {
|
||||
const parent = context.path.at(-1);
|
||||
|
||||
if (
|
||||
parent?.type === 'RegularElement' ||
|
||||
parent?.type === 'SvelteElement' ||
|
||||
parent?.type === 'SvelteWindow' ||
|
||||
parent?.type === 'SvelteDocument' ||
|
||||
parent?.type === 'SvelteBody'
|
||||
) {
|
||||
if (node.name in binding_properties) {
|
||||
const property = binding_properties[node.name];
|
||||
if (property.valid_elements && !property.valid_elements.includes(parent.name)) {
|
||||
e.bind_invalid_target(
|
||||
node,
|
||||
node.name,
|
||||
property.valid_elements.map((valid_element) => `\`<${valid_element}>\``).join(', ')
|
||||
);
|
||||
}
|
||||
|
||||
if (property.invalid_elements && property.invalid_elements.includes(parent.name)) {
|
||||
const valid_bindings = Object.entries(binding_properties)
|
||||
.filter(([_, binding_property]) => {
|
||||
return (
|
||||
binding_property.valid_elements?.includes(parent.name) ||
|
||||
(!binding_property.valid_elements &&
|
||||
!binding_property.invalid_elements?.includes(parent.name))
|
||||
);
|
||||
})
|
||||
.map(([property_name]) => property_name)
|
||||
.sort();
|
||||
|
||||
e.bind_invalid_name(
|
||||
node,
|
||||
node.name,
|
||||
`Possible bindings for <${parent.name}> are ${valid_bindings.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
if (parent.name === 'input' && node.name !== 'this') {
|
||||
const type = /** @type {AST.Attribute | undefined} */ (
|
||||
parent.attributes.find((a) => a.type === 'Attribute' && a.name === 'type')
|
||||
);
|
||||
|
||||
if (type && !is_text_attribute(type)) {
|
||||
if (node.name !== 'value' || type.value === true) {
|
||||
e.attribute_invalid_type(type);
|
||||
}
|
||||
} else {
|
||||
if (node.name === 'checked' && type?.value[0].data !== 'checkbox') {
|
||||
e.bind_invalid_target(
|
||||
node,
|
||||
node.name,
|
||||
`\`<input type="checkbox">\`${type?.value[0].data === 'radio' ? ` — for \`<input type="radio">\`, use \`bind:group\`` : ''}`
|
||||
);
|
||||
}
|
||||
|
||||
if (node.name === 'files' && type?.value[0].data !== 'file') {
|
||||
e.bind_invalid_target(node, node.name, '`<input type="file">`');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (parent.name === 'select' && node.name !== 'this') {
|
||||
const multiple = parent.attributes.find(
|
||||
(a) =>
|
||||
a.type === 'Attribute' &&
|
||||
a.name === 'multiple' &&
|
||||
!is_text_attribute(a) &&
|
||||
a.value !== true
|
||||
);
|
||||
|
||||
if (multiple) {
|
||||
e.attribute_invalid_multiple(multiple);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.name === 'offsetWidth' && is_svg(parent.name)) {
|
||||
e.bind_invalid_target(
|
||||
node,
|
||||
node.name,
|
||||
`non-\`<svg>\` elements. Use \`bind:clientWidth\` for \`<svg>\` instead`
|
||||
);
|
||||
}
|
||||
|
||||
if (is_content_editable_binding(node.name)) {
|
||||
const contenteditable = /** @type {AST.Attribute} */ (
|
||||
parent.attributes.find((a) => a.type === 'Attribute' && a.name === 'contenteditable')
|
||||
);
|
||||
|
||||
if (!contenteditable) {
|
||||
e.attribute_contenteditable_missing(node);
|
||||
} else if (!is_text_attribute(contenteditable) && contenteditable.value !== true) {
|
||||
e.attribute_contenteditable_dynamic(contenteditable);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const match = fuzzymatch(node.name, Object.keys(binding_properties));
|
||||
|
||||
if (match) {
|
||||
const property = binding_properties[match];
|
||||
if (!property.valid_elements || property.valid_elements.includes(parent.name)) {
|
||||
e.bind_invalid_name(node, node.name, `Did you mean '${match}'?`);
|
||||
}
|
||||
}
|
||||
|
||||
e.bind_invalid_name(node, node.name);
|
||||
}
|
||||
}
|
||||
|
||||
// When dealing with bind getters/setters skip the specific binding validation
|
||||
// Group bindings aren't supported for getter/setters so we don't need to handle
|
||||
// the metadata
|
||||
if (node.expression.type === 'SequenceExpression') {
|
||||
if (node.name === 'group') {
|
||||
e.bind_group_invalid_expression(node);
|
||||
}
|
||||
|
||||
let i = /** @type {number} */ (node.expression.start);
|
||||
let leading_comments_start = /**@type {any}*/ (node.expression.leadingComments?.at(0))?.start;
|
||||
let leading_comments_end = /**@type {any}*/ (node.expression.leadingComments?.at(-1))?.end;
|
||||
while (context.state.analysis.source[--i] !== '{') {
|
||||
if (
|
||||
context.state.analysis.source[i] === '(' &&
|
||||
// if the parenthesis is in a leading comment we don't need to throw the error
|
||||
!(
|
||||
leading_comments_start &&
|
||||
leading_comments_end &&
|
||||
i <= leading_comments_end &&
|
||||
i >= leading_comments_start
|
||||
)
|
||||
) {
|
||||
e.bind_invalid_parens(node, node.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.expression.expressions.length !== 2) {
|
||||
e.bind_invalid_expression(node);
|
||||
}
|
||||
|
||||
mark_subtree_dynamic(context.path);
|
||||
|
||||
const [get, set] = node.expression.expressions;
|
||||
// We gotta jump across the getter/setter functions to avoid the expression metadata field being reset to null
|
||||
// as we want to collect the functions' blocker/async info
|
||||
context.visit(get.type === 'ArrowFunctionExpression' ? get.body : get, {
|
||||
...context.state,
|
||||
expression: node.metadata.expression
|
||||
});
|
||||
context.visit(set.type === 'ArrowFunctionExpression' ? set.body : set, {
|
||||
...context.state,
|
||||
expression: node.metadata.expression
|
||||
});
|
||||
|
||||
if (node.metadata.expression.has_await) {
|
||||
e.illegal_await_expression(node);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
validate_assignment(node, node.expression, context);
|
||||
|
||||
const assignee = node.expression;
|
||||
const left = object(assignee);
|
||||
|
||||
if (left === null) {
|
||||
e.bind_invalid_expression(node);
|
||||
}
|
||||
|
||||
const binding = context.state.scope.get(left.name);
|
||||
node.metadata.binding = binding;
|
||||
|
||||
if (assignee.type === 'Identifier') {
|
||||
// reassignment
|
||||
if (
|
||||
node.name !== 'this' && // bind:this also works for regular variables
|
||||
(!binding ||
|
||||
(binding.kind !== 'state' &&
|
||||
binding.kind !== 'raw_state' &&
|
||||
binding.kind !== 'prop' &&
|
||||
binding.kind !== 'bindable_prop' &&
|
||||
binding.kind !== 'each' &&
|
||||
binding.kind !== 'store_sub' &&
|
||||
!binding.updated)) // TODO wut?
|
||||
) {
|
||||
e.bind_invalid_value(node.expression);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.name === 'group') {
|
||||
if (!binding) {
|
||||
throw new Error('Cannot find declaration for bind:group');
|
||||
}
|
||||
|
||||
if (binding.kind === 'snippet') {
|
||||
e.bind_group_invalid_snippet_parameter(node);
|
||||
}
|
||||
|
||||
// Traverse the path upwards and find all EachBlocks who are (indirectly) contributing to bind:group,
|
||||
// i.e. one of their declarations is referenced in the binding. This allows group bindings to work
|
||||
// correctly when referencing a variable declared in an EachBlock by using the index of the each block
|
||||
// entries as keys.
|
||||
const each_blocks = [];
|
||||
const [keypath, expression_ids] = extract_all_identifiers_from_expression(node.expression);
|
||||
let ids = expression_ids;
|
||||
|
||||
let i = context.path.length;
|
||||
while (i--) {
|
||||
const parent = context.path[i];
|
||||
|
||||
if (parent.type === 'EachBlock') {
|
||||
const references = ids.filter((id) => parent.metadata.declarations.has(id.name));
|
||||
|
||||
if (references.length > 0) {
|
||||
parent.metadata.contains_group_binding = true;
|
||||
|
||||
each_blocks.push(parent);
|
||||
ids = ids.filter((id) => !references.includes(id));
|
||||
ids.push(...extract_all_identifiers_from_expression(parent.expression)[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The identifiers that make up the binding expression form they key for the binding group.
|
||||
// If the same identifiers in the same order are used in another bind:group, they will be in the same group.
|
||||
// (there's an edge case where `bind:group={a[i]}` will be in a different group than `bind:group={a[j]}` even when i == j,
|
||||
// but this is a limitation of the current static analysis we do; it also never worked in Svelte 4)
|
||||
const bindings = expression_ids.map((id) => context.state.scope.get(id.name));
|
||||
let group_name;
|
||||
|
||||
outer: for (const [[key, b], group] of context.state.analysis.binding_groups) {
|
||||
if (b.length !== bindings.length || key !== keypath) continue;
|
||||
for (let i = 0; i < bindings.length; i++) {
|
||||
if (bindings[i] !== b[i]) continue outer;
|
||||
}
|
||||
group_name = group;
|
||||
}
|
||||
|
||||
if (!group_name) {
|
||||
group_name = context.state.scope.root.unique('binding_group');
|
||||
context.state.analysis.binding_groups.set([keypath, bindings], group_name);
|
||||
}
|
||||
|
||||
node.metadata = {
|
||||
binding_group_name: group_name,
|
||||
parent_each_blocks: each_blocks,
|
||||
expression: node.metadata.expression
|
||||
};
|
||||
}
|
||||
|
||||
if (binding?.kind === 'each' && binding.metadata?.inside_rest) {
|
||||
w.bind_invalid_each_rest(binding.node, binding.node.name);
|
||||
}
|
||||
|
||||
context.next({ ...context.state, expression: node.metadata.expression });
|
||||
|
||||
if (node.metadata.expression.has_await) {
|
||||
e.illegal_await_expression(node);
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+339
@@ -0,0 +1,339 @@
|
||||
/** @import { ArrowFunctionExpression, CallExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, VariableDeclarator } from 'estree' */
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import { get_rune } from '../../scope.js';
|
||||
import * as e from '../../../errors.js';
|
||||
import { get_parent } from '../../../utils/ast.js';
|
||||
import { is_pure, is_safe_identifier } from './shared/utils.js';
|
||||
import { dev, locate_node, source } from '../../../state.js';
|
||||
import * as b from '#compiler/builders';
|
||||
import { ExpressionMetadata } from '../../nodes.js';
|
||||
|
||||
/**
|
||||
* @param {CallExpression} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function CallExpression(node, context) {
|
||||
const parent = /** @type {AST.SvelteNode} */ (get_parent(context.path, -1));
|
||||
|
||||
const rune = get_rune(node, context.state.scope);
|
||||
|
||||
if (rune && rune !== '$inspect') {
|
||||
for (const arg of node.arguments) {
|
||||
if (arg.type === 'SpreadElement') {
|
||||
e.rune_invalid_spread(node, rune);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (rune) {
|
||||
case null:
|
||||
if (!is_safe_identifier(node.callee, context.state.scope)) {
|
||||
context.state.analysis.needs_context = true;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case '$bindable':
|
||||
if (node.arguments.length > 1) {
|
||||
e.rune_invalid_arguments_length(node, '$bindable', 'zero or one arguments');
|
||||
}
|
||||
|
||||
if (
|
||||
parent.type !== 'AssignmentPattern' ||
|
||||
context.path.at(-3)?.type !== 'ObjectPattern' ||
|
||||
context.path.at(-4)?.type !== 'VariableDeclarator' ||
|
||||
get_rune(
|
||||
/** @type {VariableDeclarator} */ (context.path.at(-4)).init,
|
||||
context.state.scope
|
||||
) !== '$props'
|
||||
) {
|
||||
e.bindable_invalid_location(node);
|
||||
}
|
||||
|
||||
// We need context in case the bound prop is stale
|
||||
context.state.analysis.needs_context = true;
|
||||
|
||||
break;
|
||||
|
||||
case '$host':
|
||||
if (node.arguments.length > 0) {
|
||||
e.rune_invalid_arguments(node, '$host');
|
||||
} else if (context.state.ast_type === 'module' || !context.state.analysis.custom_element) {
|
||||
e.host_invalid_placement(node);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case '$props':
|
||||
if (context.state.has_props_rune) {
|
||||
e.props_duplicate(node, rune);
|
||||
}
|
||||
|
||||
context.state.has_props_rune = true;
|
||||
|
||||
if (
|
||||
parent.type !== 'VariableDeclarator' ||
|
||||
context.state.ast_type !== 'instance' ||
|
||||
context.state.scope !== context.state.analysis.instance.scope
|
||||
) {
|
||||
e.props_invalid_placement(node);
|
||||
}
|
||||
|
||||
if (node.arguments.length > 0) {
|
||||
e.rune_invalid_arguments(node, rune);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case '$props.id': {
|
||||
const grand_parent = get_parent(context.path, -2);
|
||||
|
||||
if (context.state.analysis.props_id) {
|
||||
e.props_duplicate(node, rune);
|
||||
}
|
||||
|
||||
if (
|
||||
parent.type !== 'VariableDeclarator' ||
|
||||
parent.id.type !== 'Identifier' ||
|
||||
context.state.ast_type !== 'instance' ||
|
||||
context.state.scope !== context.state.analysis.instance.scope ||
|
||||
grand_parent.type !== 'VariableDeclaration'
|
||||
) {
|
||||
e.props_id_invalid_placement(node);
|
||||
}
|
||||
|
||||
if (node.arguments.length > 0) {
|
||||
e.rune_invalid_arguments(node, rune);
|
||||
}
|
||||
|
||||
context.state.analysis.props_id = parent.id;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case '$state':
|
||||
case '$state.raw':
|
||||
case '$derived':
|
||||
case '$derived.by': {
|
||||
const valid =
|
||||
is_variable_declaration(parent, context) ||
|
||||
is_class_property_definition(parent) ||
|
||||
is_class_property_assignment_at_constructor_root(parent, context);
|
||||
|
||||
if (!valid) {
|
||||
e.state_invalid_placement(node, rune);
|
||||
}
|
||||
|
||||
if ((rune === '$derived' || rune === '$derived.by') && node.arguments.length !== 1) {
|
||||
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
|
||||
} else if (node.arguments.length > 1) {
|
||||
e.rune_invalid_arguments_length(node, rune, 'zero or one arguments');
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case '$effect':
|
||||
case '$effect.pre':
|
||||
if (parent.type !== 'ExpressionStatement') {
|
||||
e.effect_invalid_placement(node);
|
||||
}
|
||||
|
||||
if (node.arguments.length !== 1) {
|
||||
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
|
||||
}
|
||||
|
||||
// `$effect` needs context because Svelte needs to know whether it should re-run
|
||||
// effects that invalidate themselves, and that's determined by whether we're in runes mode
|
||||
context.state.analysis.needs_context = true;
|
||||
|
||||
break;
|
||||
|
||||
case '$effect.tracking':
|
||||
if (node.arguments.length !== 0) {
|
||||
e.rune_invalid_arguments(node, rune);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case '$effect.root':
|
||||
if (node.arguments.length !== 1) {
|
||||
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case '$effect.pending':
|
||||
if (context.state.expression) {
|
||||
context.state.expression.has_state = true;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case '$inspect':
|
||||
if (node.arguments.length < 1) {
|
||||
e.rune_invalid_arguments_length(node, rune, 'one or more arguments');
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case '$inspect().with':
|
||||
if (node.arguments.length !== 1) {
|
||||
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case '$inspect.trace': {
|
||||
if (node.arguments.length > 1) {
|
||||
e.rune_invalid_arguments_length(node, rune, 'zero or one arguments');
|
||||
}
|
||||
|
||||
const grand_parent = context.path.at(-2);
|
||||
const fn = context.path.at(-3);
|
||||
|
||||
if (
|
||||
parent.type !== 'ExpressionStatement' ||
|
||||
grand_parent?.type !== 'BlockStatement' ||
|
||||
!(
|
||||
fn?.type === 'FunctionDeclaration' ||
|
||||
fn?.type === 'FunctionExpression' ||
|
||||
fn?.type === 'ArrowFunctionExpression'
|
||||
) ||
|
||||
grand_parent.body[0] !== parent
|
||||
) {
|
||||
e.inspect_trace_invalid_placement(node);
|
||||
}
|
||||
|
||||
if (fn.generator) {
|
||||
e.inspect_trace_generator(node);
|
||||
}
|
||||
|
||||
if (dev) {
|
||||
if (node.arguments[0]) {
|
||||
context.state.scope.tracing = b.thunk(/** @type {Expression} */ (node.arguments[0]));
|
||||
} else {
|
||||
const label = get_function_label(context.path.slice(0, -2)) ?? 'trace';
|
||||
const loc = `(${locate_node(fn)})`;
|
||||
|
||||
context.state.scope.tracing = b.thunk(b.literal(label + ' ' + loc));
|
||||
}
|
||||
|
||||
context.state.analysis.tracing = true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case '$state.eager':
|
||||
if (node.arguments.length !== 1) {
|
||||
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case '$state.snapshot':
|
||||
if (node.arguments.length !== 1) {
|
||||
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// `$inspect(foo)` or `$derived(foo) should not trigger the `static-state-reference` warning
|
||||
if (rune === '$derived') {
|
||||
const expression = new ExpressionMetadata();
|
||||
|
||||
context.next({
|
||||
...context.state,
|
||||
function_depth: context.state.function_depth + 1,
|
||||
derived_function_depth: context.state.function_depth + 1,
|
||||
expression
|
||||
});
|
||||
|
||||
if (expression.has_await) {
|
||||
context.state.analysis.async_deriveds.add(node);
|
||||
}
|
||||
} else if (rune === '$inspect') {
|
||||
context.next({ ...context.state, function_depth: context.state.function_depth + 1 });
|
||||
} else {
|
||||
context.next();
|
||||
}
|
||||
|
||||
if (context.state.expression) {
|
||||
// TODO We assume that any dependencies are stateful, which isn't necessarily the case — see
|
||||
// https://github.com/sveltejs/svelte/issues/13266. This check also includes dependencies
|
||||
// outside the call expression itself (e.g. `{blah && pure()}`) resulting in additional
|
||||
// false positives, but for now we accept that trade-off
|
||||
if (!is_pure(node.callee, context) || context.state.expression.dependencies.size > 0) {
|
||||
context.state.expression.has_call = true;
|
||||
context.state.expression.has_state = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AST.SvelteNode[]} nodes
|
||||
*/
|
||||
function get_function_label(nodes) {
|
||||
const fn = /** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} */ (
|
||||
nodes.at(-1)
|
||||
);
|
||||
|
||||
if ((fn.type === 'FunctionDeclaration' || fn.type === 'FunctionExpression') && fn.id != null) {
|
||||
return fn.id.name;
|
||||
}
|
||||
|
||||
const parent = nodes.at(-2);
|
||||
if (!parent) return;
|
||||
|
||||
if (parent.type === 'CallExpression') {
|
||||
return source.slice(parent.callee.start, parent.callee.end) + '(...)';
|
||||
}
|
||||
|
||||
if (parent.type === 'Property' && !parent.computed) {
|
||||
return /** @type {Identifier} */ (parent.key).name;
|
||||
}
|
||||
|
||||
if (parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
|
||||
return parent.id.name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AST.SvelteNode} parent
|
||||
* @param {Context} context
|
||||
*/
|
||||
function is_variable_declaration(parent, context) {
|
||||
return parent.type === 'VariableDeclarator' && get_parent(context.path, -3).type !== 'ConstTag';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AST.SvelteNode} parent
|
||||
*/
|
||||
function is_class_property_definition(parent) {
|
||||
return parent.type === 'PropertyDefinition' && !parent.static && !parent.computed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AST.SvelteNode} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
function is_class_property_assignment_at_constructor_root(node, context) {
|
||||
if (
|
||||
node.type === 'AssignmentExpression' &&
|
||||
node.operator === '=' &&
|
||||
node.left.type === 'MemberExpression' &&
|
||||
node.left.object.type === 'ThisExpression' &&
|
||||
((node.left.property.type === 'Identifier' && !node.left.computed) ||
|
||||
node.left.property.type === 'PrivateIdentifier' ||
|
||||
node.left.property.type === 'Literal')
|
||||
) {
|
||||
// MethodDefinition (-5) -> FunctionExpression (-4) -> BlockStatement (-3) -> ExpressionStatement (-2) -> AssignmentExpression (-1)
|
||||
const parent = get_parent(context.path, -5);
|
||||
return parent?.type === 'MethodDefinition' && parent.kind === 'constructor';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
/** @import { AssignmentExpression, CallExpression, ClassBody, PropertyDefinition, Expression, PrivateIdentifier, MethodDefinition } from 'estree' */
|
||||
/** @import { StateField } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import * as b from '#compiler/builders';
|
||||
import { get_rune } from '../../scope.js';
|
||||
import * as e from '../../../errors.js';
|
||||
import { is_state_creation_rune } from '../../../../utils.js';
|
||||
import { get_name } from '../../nodes.js';
|
||||
import { regex_invalid_identifier_chars } from '../../patterns.js';
|
||||
|
||||
/**
|
||||
* @param {ClassBody} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function ClassBody(node, context) {
|
||||
if (!context.state.analysis.runes) {
|
||||
context.next();
|
||||
return;
|
||||
}
|
||||
|
||||
/** @type {string[]} */
|
||||
const private_ids = [];
|
||||
|
||||
for (const prop of node.body) {
|
||||
if (
|
||||
(prop.type === 'MethodDefinition' || prop.type === 'PropertyDefinition') &&
|
||||
prop.key.type === 'PrivateIdentifier'
|
||||
) {
|
||||
private_ids.push(prop.key.name);
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Map<string, StateField>} */
|
||||
const state_fields = new Map();
|
||||
|
||||
/** @type {Map<string, Array<MethodDefinition['kind'] | 'prop' | 'assigned_prop'>>} */
|
||||
const fields = new Map();
|
||||
|
||||
context.state.analysis.classes.set(node, state_fields);
|
||||
|
||||
/** @type {MethodDefinition | null} */
|
||||
let constructor = null;
|
||||
|
||||
/**
|
||||
* @param {PropertyDefinition | AssignmentExpression} node
|
||||
* @param {Expression | PrivateIdentifier} key
|
||||
* @param {Expression | null | undefined} value
|
||||
*/
|
||||
function handle(node, key, value) {
|
||||
const name = get_name(key);
|
||||
if (name === null) return;
|
||||
|
||||
const rune = get_rune(value, context.state.scope);
|
||||
|
||||
if (rune && is_state_creation_rune(rune)) {
|
||||
if (state_fields.has(name)) {
|
||||
e.state_field_duplicate(node, name);
|
||||
}
|
||||
|
||||
const _key = (node.type === 'AssignmentExpression' || !node.static ? '' : '@') + name;
|
||||
const field = fields.get(_key);
|
||||
|
||||
// if there's already a method or assigned field, error
|
||||
if (field && !(field.length === 1 && field[0] === 'prop')) {
|
||||
e.duplicate_class_field(node, _key);
|
||||
}
|
||||
|
||||
state_fields.set(name, {
|
||||
node,
|
||||
type: rune,
|
||||
// @ts-expect-error for public state this is filled out in a moment
|
||||
key: key.type === 'PrivateIdentifier' ? key : null,
|
||||
value: /** @type {CallExpression} */ (value)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const child of node.body) {
|
||||
if (child.type === 'PropertyDefinition' && !child.computed && !child.static) {
|
||||
handle(child, child.key, child.value);
|
||||
const key = /** @type {string} */ (get_name(child.key));
|
||||
const field = fields.get(key);
|
||||
if (!field) {
|
||||
fields.set(key, [child.value ? 'assigned_prop' : 'prop']);
|
||||
continue;
|
||||
}
|
||||
e.duplicate_class_field(child, key);
|
||||
}
|
||||
|
||||
if (child.type === 'MethodDefinition') {
|
||||
if (child.kind === 'constructor') {
|
||||
constructor = child;
|
||||
} else if (!child.computed) {
|
||||
const key = (child.static ? '@' : '') + get_name(child.key);
|
||||
const field = fields.get(key);
|
||||
if (!field) {
|
||||
fields.set(key, [child.kind]);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
field.includes(child.kind) ||
|
||||
field.includes('prop') ||
|
||||
field.includes('assigned_prop')
|
||||
) {
|
||||
e.duplicate_class_field(child, key);
|
||||
}
|
||||
if (child.kind === 'get') {
|
||||
if (field.length === 1 && field[0] === 'set') {
|
||||
field.push('get');
|
||||
continue;
|
||||
}
|
||||
} else if (child.kind === 'set') {
|
||||
if (field.length === 1 && field[0] === 'get') {
|
||||
field.push('set');
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
field.push(child.kind);
|
||||
continue;
|
||||
}
|
||||
e.duplicate_class_field(child, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (constructor) {
|
||||
for (const statement of constructor.value.body.body) {
|
||||
if (statement.type !== 'ExpressionStatement') continue;
|
||||
if (statement.expression.type !== 'AssignmentExpression') continue;
|
||||
|
||||
const { left, right } = statement.expression;
|
||||
|
||||
if (left.type !== 'MemberExpression') continue;
|
||||
if (left.object.type !== 'ThisExpression') continue;
|
||||
if (left.computed && left.property.type !== 'Literal') continue;
|
||||
|
||||
handle(statement.expression, left.property, right);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [name, field] of state_fields) {
|
||||
if (name[0] === '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let deconflicted = name.replace(regex_invalid_identifier_chars, '_');
|
||||
while (private_ids.includes(deconflicted)) {
|
||||
deconflicted = '_' + deconflicted;
|
||||
}
|
||||
|
||||
private_ids.push(deconflicted);
|
||||
field.key = b.private_id(deconflicted);
|
||||
}
|
||||
|
||||
context.next({ ...context.state, state_fields });
|
||||
}
|
||||
Generated
Vendored
+25
@@ -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();
|
||||
}
|
||||
Generated
Vendored
+13
@@ -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 });
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import { visit_component } from './shared/component.js';
|
||||
|
||||
/**
|
||||
* @param {AST.Component} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function Component(node, context) {
|
||||
const binding = context.state.scope.get(
|
||||
node.name.includes('.') ? node.name.slice(0, node.name.indexOf('.')) : node.name
|
||||
);
|
||||
|
||||
node.metadata.dynamic =
|
||||
context.state.analysis.runes && // Svelte 4 required you to use svelte:component to switch components
|
||||
binding !== null &&
|
||||
(binding.kind !== 'normal' || node.name.includes('.'));
|
||||
|
||||
if (binding) {
|
||||
node.metadata.expression.has_state = node.metadata.dynamic;
|
||||
node.metadata.expression.dependencies.add(binding);
|
||||
node.metadata.expression.references.add(binding);
|
||||
}
|
||||
|
||||
visit_component(node, context);
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import * as e from '../../../errors.js';
|
||||
import * as b from '#compiler/builders';
|
||||
import { validate_opening_tag } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* @param {AST.ConstTag} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function ConstTag(node, context) {
|
||||
if (context.state.analysis.runes) {
|
||||
validate_opening_tag(node, context.state, '@');
|
||||
}
|
||||
|
||||
const parent = context.path.at(-1);
|
||||
const grand_parent = context.path.at(-2);
|
||||
|
||||
if (
|
||||
parent?.type !== 'Fragment' ||
|
||||
(grand_parent?.type !== 'IfBlock' &&
|
||||
grand_parent?.type !== 'SvelteFragment' &&
|
||||
grand_parent?.type !== 'Component' &&
|
||||
grand_parent?.type !== 'SvelteComponent' &&
|
||||
grand_parent?.type !== 'EachBlock' &&
|
||||
grand_parent?.type !== 'AwaitBlock' &&
|
||||
grand_parent?.type !== 'SnippetBlock' &&
|
||||
grand_parent?.type !== 'SvelteBoundary' &&
|
||||
grand_parent?.type !== 'KeyBlock' &&
|
||||
((grand_parent?.type !== 'RegularElement' && grand_parent?.type !== 'SvelteElement') ||
|
||||
!grand_parent.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot')))
|
||||
) {
|
||||
e.const_tag_invalid_placement(node);
|
||||
}
|
||||
|
||||
const declaration = node.declaration.declarations[0];
|
||||
|
||||
context.visit(declaration.id);
|
||||
context.visit(declaration.init, {
|
||||
...context.state,
|
||||
expression: node.metadata.expression,
|
||||
// We're treating this like a $derived under the hood
|
||||
function_depth: context.state.function_depth + 1,
|
||||
derived_function_depth: context.state.function_depth + 1
|
||||
});
|
||||
|
||||
const has_await = node.metadata.expression.has_await;
|
||||
const blockers = [...node.metadata.expression.dependencies]
|
||||
.map((dep) => dep.blocker)
|
||||
.filter((b) => b !== null && b.object !== context.state.async_consts?.id);
|
||||
|
||||
if (has_await || context.state.async_consts || blockers.length > 0) {
|
||||
const run = (context.state.async_consts ??= {
|
||||
id: context.state.analysis.root.unique('promises'),
|
||||
declaration_count: 0
|
||||
});
|
||||
node.metadata.promises_id = run.id;
|
||||
|
||||
const bindings = context.state.scope.get_bindings(declaration);
|
||||
|
||||
// keep the counter in sync with the number of thunks pushed in ConstTag in transform
|
||||
// TODO 6.0 once non-async and non-runes mode is gone investigate making this more robust
|
||||
// via something like the approach in https://github.com/sveltejs/svelte/pull/18032
|
||||
const length = run.declaration_count + (blockers.length > 0 ? 1 : 0);
|
||||
run.declaration_count += blockers.length > 0 ? 2 : 1;
|
||||
const blocker = b.member(run.id, b.literal(length), true);
|
||||
for (const binding of bindings) {
|
||||
binding.blocker = blocker;
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -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();
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/** @import { Expression } from 'estree' */
|
||||
/** @import { AST, Binding } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
/** @import { Scope } from '../../scope' */
|
||||
import * as e from '../../../errors.js';
|
||||
import { extract_identifiers } from '../../../utils/ast.js';
|
||||
import { mark_subtree_dynamic } from './shared/fragment.js';
|
||||
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* @param {AST.EachBlock} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function EachBlock(node, context) {
|
||||
validate_opening_tag(node, context.state, '#');
|
||||
|
||||
validate_block_not_empty(node.body, context);
|
||||
validate_block_not_empty(node.fallback, context);
|
||||
|
||||
const id = node.context;
|
||||
if (id?.type === 'Identifier' && (id.name === '$state' || id.name === '$derived')) {
|
||||
// TODO weird that this is necessary
|
||||
e.state_invalid_placement(node, id.name);
|
||||
}
|
||||
|
||||
if (node.key) {
|
||||
// treat `{#each items as item, i (i)}` as a normal indexed block, everything else as keyed
|
||||
node.metadata.keyed =
|
||||
node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index;
|
||||
}
|
||||
|
||||
if (node.metadata.keyed && !node.context) {
|
||||
e.each_key_without_as(/** @type {Expression} */ (node.key));
|
||||
}
|
||||
|
||||
// evaluate expression in parent scope
|
||||
context.visit(node.expression, {
|
||||
...context.state,
|
||||
expression: node.metadata.expression,
|
||||
scope: /** @type {Scope} */ (context.state.scope.parent)
|
||||
});
|
||||
|
||||
context.visit(node.body);
|
||||
if (node.key) context.visit(node.key);
|
||||
if (node.fallback) context.visit(node.fallback);
|
||||
|
||||
if (!context.state.analysis.runes) {
|
||||
let mutated =
|
||||
!!node.context &&
|
||||
extract_identifiers(node.context).some((id) => {
|
||||
const binding = context.state.scope.get(id.name);
|
||||
return !!binding?.mutated;
|
||||
});
|
||||
|
||||
// collect transitive dependencies...
|
||||
for (const binding of node.metadata.expression.dependencies) {
|
||||
if (binding.declaration_kind !== 'function') {
|
||||
collect_transitive_dependencies(binding, node.metadata.transitive_deps);
|
||||
}
|
||||
}
|
||||
|
||||
// ...and ensure they are marked as state, so they can be turned
|
||||
// into mutable sources and invalidated
|
||||
if (mutated) {
|
||||
for (const binding of node.metadata.transitive_deps) {
|
||||
if (
|
||||
binding.kind === 'normal' &&
|
||||
(binding.declaration_kind === 'const' ||
|
||||
binding.declaration_kind === 'let' ||
|
||||
binding.declaration_kind === 'var')
|
||||
) {
|
||||
binding.kind = 'state';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mark_subtree_dynamic(context.path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Binding} binding
|
||||
* @param {Set<Binding>} bindings
|
||||
* @returns {void}
|
||||
*/
|
||||
function collect_transitive_dependencies(binding, bindings) {
|
||||
if (bindings.has(binding)) {
|
||||
return;
|
||||
}
|
||||
bindings.add(binding);
|
||||
|
||||
if (binding.kind === 'legacy_reactive') {
|
||||
for (const dep of binding.legacy_dependencies) {
|
||||
collect_transitive_dependencies(dep, bindings);
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+20
@@ -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();
|
||||
}
|
||||
Generated
Vendored
+70
@@ -0,0 +1,70 @@
|
||||
/** @import { ExportNamedDeclaration, Identifier } from 'estree' */
|
||||
/** @import { Context } from '../types' */
|
||||
import * as e from '../../../errors.js';
|
||||
import { extract_identifiers } from '../../../utils/ast.js';
|
||||
|
||||
/**
|
||||
* @param {ExportNamedDeclaration} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function ExportNamedDeclaration(node, context) {
|
||||
// visit children, so bindings are correctly initialised
|
||||
context.next();
|
||||
|
||||
if (
|
||||
context.state.ast_type &&
|
||||
node.specifiers.some((specifier) =>
|
||||
specifier.exported.type === 'Identifier'
|
||||
? specifier.exported.name === 'default'
|
||||
: specifier.exported.value === 'default'
|
||||
)
|
||||
) {
|
||||
e.module_illegal_default_export(node);
|
||||
}
|
||||
|
||||
if (node.declaration?.type === 'VariableDeclaration') {
|
||||
// in runes mode, forbid `export let`
|
||||
if (
|
||||
context.state.analysis.runes &&
|
||||
context.state.ast_type === 'instance' &&
|
||||
node.declaration.kind === 'let'
|
||||
) {
|
||||
e.legacy_export_invalid(node);
|
||||
}
|
||||
|
||||
for (const declarator of node.declaration.declarations) {
|
||||
for (const id of extract_identifiers(declarator.id)) {
|
||||
const binding = context.state.scope.get(id.name);
|
||||
if (!binding) continue;
|
||||
|
||||
if (binding.kind === 'derived') {
|
||||
e.derived_invalid_export(node);
|
||||
}
|
||||
|
||||
if ((binding.kind === 'state' || binding.kind === 'raw_state') && binding.reassigned) {
|
||||
e.state_invalid_export(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (context.state.analysis.runes) {
|
||||
if (node.declaration && context.state.ast_type === 'instance') {
|
||||
if (
|
||||
node.declaration.type === 'FunctionDeclaration' ||
|
||||
node.declaration.type === 'ClassDeclaration'
|
||||
) {
|
||||
context.state.analysis.exports.push({
|
||||
name: /** @type {Identifier} */ (node.declaration.id).name,
|
||||
alias: null
|
||||
});
|
||||
} else if (node.declaration.kind === 'const') {
|
||||
for (const declarator of node.declaration.declarations) {
|
||||
for (const node of extract_identifiers(declarator.id)) {
|
||||
context.state.analysis.exports.push({ name: node.name, alias: null });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
/** @import { ExportSpecifier } from 'estree' */
|
||||
/** @import { Context } from '../types' */
|
||||
import { validate_export } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* @param {ExportSpecifier} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function ExportSpecifier(node, context) {
|
||||
const local_name =
|
||||
node.local.type === 'Identifier' ? node.local.name : /** @type {string} */ (node.local.value);
|
||||
const exported_name =
|
||||
node.exported.type === 'Identifier'
|
||||
? node.exported.name
|
||||
: /** @type {string} */ (node.exported.value);
|
||||
|
||||
if (context.state.ast_type === 'instance') {
|
||||
if (context.state.analysis.runes) {
|
||||
context.state.analysis.exports.push({
|
||||
name: local_name,
|
||||
alias: exported_name
|
||||
});
|
||||
|
||||
const binding = context.state.scope.get(local_name);
|
||||
if (binding) binding.reassigned = true;
|
||||
}
|
||||
} else {
|
||||
validate_export(node, context.state.scope, local_name);
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+38
@@ -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();
|
||||
}
|
||||
Generated
Vendored
+26
@@ -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 });
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types.js' */
|
||||
|
||||
/**
|
||||
* @param {AST.Fragment} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function Fragment(node, context) {
|
||||
context.next({ ...context.state, fragment: node, async_consts: undefined });
|
||||
}
|
||||
Generated
Vendored
+16
@@ -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);
|
||||
}
|
||||
Generated
Vendored
+11
@@ -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);
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import { mark_subtree_dynamic } from './shared/fragment.js';
|
||||
import { validate_opening_tag } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* @param {AST.HtmlTag} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function HtmlTag(node, context) {
|
||||
if (context.state.analysis.runes) {
|
||||
validate_opening_tag(node, context.state, '@');
|
||||
}
|
||||
|
||||
// unfortunately this is necessary in order to fix invalid HTML
|
||||
mark_subtree_dynamic(context.path);
|
||||
|
||||
context.next({ ...context.state, expression: node.metadata.expression });
|
||||
}
|
||||
Generated
Vendored
+194
@@ -0,0 +1,194 @@
|
||||
/** @import { Expression, Identifier } from 'estree' */
|
||||
/** @import { Context } from '../types' */
|
||||
import is_reference from 'is-reference';
|
||||
import { should_proxy } from '../../3-transform/client/utils.js';
|
||||
import * as e from '../../../errors.js';
|
||||
import * as w from '../../../warnings.js';
|
||||
import { is_rune } from '../../../../utils.js';
|
||||
import { mark_subtree_dynamic } from './shared/fragment.js';
|
||||
import { get_rune } from '../../scope.js';
|
||||
import { is_component_node } from '../../nodes.js';
|
||||
|
||||
/**
|
||||
* @param {Identifier} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function Identifier(node, context) {
|
||||
let i = context.path.length;
|
||||
let parent = /** @type {Expression} */ (context.path[--i]);
|
||||
|
||||
if (!is_reference(node, parent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mark_subtree_dynamic(context.path);
|
||||
|
||||
// If we are using arguments outside of a function, then throw an error
|
||||
if (
|
||||
node.name === 'arguments' &&
|
||||
!context.path.some((n) => n.type === 'FunctionDeclaration' || n.type === 'FunctionExpression')
|
||||
) {
|
||||
e.invalid_arguments_usage(node);
|
||||
}
|
||||
|
||||
// `$$slots` exists even in runes mode
|
||||
if (node.name === '$$slots') {
|
||||
context.state.analysis.uses_slots = true;
|
||||
}
|
||||
|
||||
if (context.state.analysis.runes) {
|
||||
if (
|
||||
is_rune(node.name) &&
|
||||
context.state.scope.get(node.name) === null &&
|
||||
context.state.scope.get(node.name.slice(1))?.kind !== 'store_sub'
|
||||
) {
|
||||
/** @type {Expression} */
|
||||
let current = node;
|
||||
let name = node.name;
|
||||
|
||||
while (parent.type === 'MemberExpression') {
|
||||
if (parent.computed) e.rune_invalid_computed_property(parent);
|
||||
name += `.${/** @type {Identifier} */ (parent.property).name}`;
|
||||
|
||||
current = parent;
|
||||
parent = /** @type {Expression} */ (context.path[--i]);
|
||||
|
||||
if (!is_rune(name)) {
|
||||
if (name === '$effect.active') {
|
||||
e.rune_renamed(parent, '$effect.active', '$effect.tracking');
|
||||
}
|
||||
|
||||
if (name === '$state.frozen') {
|
||||
e.rune_renamed(parent, '$state.frozen', '$state.raw');
|
||||
}
|
||||
|
||||
if (name === '$state.is') {
|
||||
e.rune_removed(parent, '$state.is');
|
||||
}
|
||||
|
||||
e.rune_invalid_name(parent, name);
|
||||
}
|
||||
}
|
||||
|
||||
if (parent.type !== 'CallExpression') {
|
||||
e.rune_missing_parentheses(current);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let binding = context.state.scope.get(node.name);
|
||||
|
||||
if (!context.state.analysis.runes) {
|
||||
if (node.name === '$$props') {
|
||||
context.state.analysis.uses_props = true;
|
||||
}
|
||||
|
||||
if (node.name === '$$restProps') {
|
||||
context.state.analysis.uses_rest_props = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (binding) {
|
||||
if (context.state.expression) {
|
||||
context.state.expression.dependencies.add(binding);
|
||||
context.state.expression.references.add(binding);
|
||||
context.state.expression.has_state ||=
|
||||
binding.kind !== 'static' &&
|
||||
(binding.kind === 'prop' ||
|
||||
binding.kind === 'bindable_prop' ||
|
||||
binding.kind === 'rest_prop' ||
|
||||
!binding.is_function()) &&
|
||||
!context.state.scope.evaluate(node).is_known;
|
||||
}
|
||||
|
||||
if (
|
||||
context.state.analysis.runes &&
|
||||
node !== binding.node &&
|
||||
context.state.function_depth === binding.scope.function_depth &&
|
||||
// If we have $state that can be proxied or frozen and isn't re-assigned, then that means
|
||||
// it's likely not using a primitive value and thus this warning isn't that helpful.
|
||||
((binding.kind === 'state' &&
|
||||
(binding.reassigned ||
|
||||
(binding.initial?.type === 'CallExpression' &&
|
||||
binding.initial.arguments.length === 1 &&
|
||||
binding.initial.arguments[0].type !== 'SpreadElement' &&
|
||||
!should_proxy(binding.initial.arguments[0], context.state.scope)))) ||
|
||||
binding.kind === 'raw_state' ||
|
||||
binding.kind === 'derived' ||
|
||||
binding.kind === 'prop' ||
|
||||
binding.kind === 'rest_prop') &&
|
||||
// We're only concerned with reads here
|
||||
(parent.type !== 'AssignmentExpression' || parent.left !== node) &&
|
||||
parent.type !== 'UpdateExpression'
|
||||
) {
|
||||
let type = 'closure';
|
||||
|
||||
let i = context.path.length;
|
||||
while (i--) {
|
||||
const parent = context.path[i];
|
||||
|
||||
if (
|
||||
parent.type === 'ArrowFunctionExpression' ||
|
||||
parent.type === 'FunctionDeclaration' ||
|
||||
parent.type === 'FunctionExpression'
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
parent.type === 'CallExpression' &&
|
||||
parent.arguments.includes(/** @type {any} */ (context.path[i + 1]))
|
||||
) {
|
||||
const rune = get_rune(parent, context.state.scope);
|
||||
|
||||
if (rune === '$state' || rune === '$state.raw') {
|
||||
type = 'derived';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.state_referenced_locally(node, node.name, type);
|
||||
}
|
||||
|
||||
if (
|
||||
context.state.reactive_statement &&
|
||||
binding.scope === context.state.analysis.module.scope &&
|
||||
binding.reassigned
|
||||
) {
|
||||
w.reactive_declaration_module_script_dependency(node);
|
||||
}
|
||||
|
||||
if (binding.metadata?.is_template_declaration && context.state.options.experimental.async) {
|
||||
let snippet_name;
|
||||
|
||||
// Find out if this references a {@const ...} declaration of an implicit children snippet
|
||||
// when it is itself inside a snippet block at the same level. If so, error.
|
||||
for (let i = context.path.length - 1; i >= 0; i--) {
|
||||
const parent = context.path[i];
|
||||
const grand_parent = context.path[i - 1];
|
||||
|
||||
if (parent.type === 'SnippetBlock') {
|
||||
snippet_name = parent.expression.name;
|
||||
} else if (
|
||||
snippet_name &&
|
||||
grand_parent &&
|
||||
parent.type === 'Fragment' &&
|
||||
(is_component_node(grand_parent) ||
|
||||
(grand_parent.type === 'SvelteBoundary' &&
|
||||
(snippet_name === 'failed' || snippet_name === 'pending')))
|
||||
) {
|
||||
if (
|
||||
is_component_node(grand_parent)
|
||||
? grand_parent.metadata.scopes.default === binding.scope
|
||||
: context.state.scopes.get(parent) === binding.scope
|
||||
) {
|
||||
e.const_tag_invalid_reference(node, node.name);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import { mark_subtree_dynamic } from './shared/fragment.js';
|
||||
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* @param {AST.IfBlock} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function IfBlock(node, context) {
|
||||
validate_block_not_empty(node.consequent, context);
|
||||
validate_block_not_empty(node.alternate, context);
|
||||
|
||||
if (context.state.analysis.runes) {
|
||||
validate_opening_tag(node, context.state, node.elseif ? ':' : '#');
|
||||
}
|
||||
|
||||
mark_subtree_dynamic(context.path);
|
||||
|
||||
context.visit(node.test, {
|
||||
...context.state,
|
||||
expression: node.metadata.expression
|
||||
});
|
||||
|
||||
context.visit(node.consequent);
|
||||
if (node.alternate) context.visit(node.alternate);
|
||||
|
||||
// Check if we can flatten branches
|
||||
const alt = node.alternate;
|
||||
|
||||
if (alt && alt.nodes.length === 1 && alt.nodes[0].type === 'IfBlock' && alt.nodes[0].elseif) {
|
||||
const elseif = alt.nodes[0];
|
||||
|
||||
// Don't flatten if this else-if has an await expression or new blockers
|
||||
// TODO would be nice to check the await expression itself to see if it's awaiting the same thing as a previous if expression
|
||||
if (
|
||||
!elseif.metadata.expression.has_await &&
|
||||
!elseif.metadata.expression.has_more_blockers_than(node.metadata.expression)
|
||||
) {
|
||||
// Roll the existing flattened branches (if any) into this one, then delete those of the else-if block
|
||||
// to avoid processing them multiple times as we walk down the chain during code transformation.
|
||||
node.metadata.flattened = [elseif, ...(elseif.metadata.flattened ?? [])];
|
||||
elseif.metadata.flattened = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+31
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import { mark_subtree_dynamic } from './shared/fragment.js';
|
||||
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* @param {AST.KeyBlock} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function KeyBlock(node, context) {
|
||||
validate_block_not_empty(node.fragment, context);
|
||||
|
||||
if (context.state.analysis.runes) {
|
||||
validate_opening_tag(node, context.state, '#');
|
||||
}
|
||||
|
||||
mark_subtree_dynamic(context.path);
|
||||
|
||||
context.visit(node.expression, { ...context.state, expression: node.metadata.expression });
|
||||
context.visit(node.fragment);
|
||||
}
|
||||
Generated
Vendored
+95
@@ -0,0 +1,95 @@
|
||||
/** @import { Expression, LabeledStatement } from 'estree' */
|
||||
/** @import { AST, ReactiveStatement } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import * as e from '../../../errors.js';
|
||||
import { extract_identifiers, object } from '../../../utils/ast.js';
|
||||
import * as w from '../../../warnings.js';
|
||||
|
||||
/**
|
||||
* @param {LabeledStatement} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function LabeledStatement(node, context) {
|
||||
if (node.label.name === '$') {
|
||||
const parent = /** @type {AST.SvelteNode} */ (context.path.at(-1));
|
||||
|
||||
const is_reactive_statement =
|
||||
context.state.ast_type === 'instance' && parent.type === 'Program';
|
||||
|
||||
if (is_reactive_statement) {
|
||||
if (context.state.analysis.runes) {
|
||||
e.legacy_reactive_statement_invalid(node);
|
||||
}
|
||||
|
||||
// Find all dependencies of this `$: {...}` statement
|
||||
/** @type {ReactiveStatement} */
|
||||
const reactive_statement = {
|
||||
assignments: new Set(),
|
||||
dependencies: []
|
||||
};
|
||||
|
||||
context.next({
|
||||
...context.state,
|
||||
reactive_statement,
|
||||
function_depth: context.state.scope.function_depth + 1
|
||||
});
|
||||
|
||||
// Every referenced binding becomes a dependency, unless it's on
|
||||
// the left-hand side of an `=` assignment
|
||||
for (const [name, nodes] of context.state.scope.references) {
|
||||
const binding = context.state.scope.get(name);
|
||||
if (binding === null) continue;
|
||||
|
||||
for (const { node, path } of nodes) {
|
||||
/** @type {Expression} */
|
||||
let left = node;
|
||||
|
||||
let i = path.length - 1;
|
||||
let parent = /** @type {Expression} */ (path.at(i));
|
||||
while (parent.type === 'MemberExpression') {
|
||||
left = parent;
|
||||
parent = /** @type {Expression} */ (path.at(--i));
|
||||
}
|
||||
|
||||
if (
|
||||
parent.type === 'AssignmentExpression' &&
|
||||
parent.operator === '=' &&
|
||||
parent.left === left
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
reactive_statement.dependencies.push(binding);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
context.state.analysis.reactive_statements.set(node, reactive_statement);
|
||||
|
||||
if (
|
||||
node.body.type === 'ExpressionStatement' &&
|
||||
node.body.expression.type === 'AssignmentExpression'
|
||||
) {
|
||||
let ids = extract_identifiers(node.body.expression.left);
|
||||
if (node.body.expression.left.type === 'MemberExpression') {
|
||||
const id = object(node.body.expression.left);
|
||||
if (id !== null) {
|
||||
ids = [id];
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of ids) {
|
||||
const binding = context.state.scope.get(id.name);
|
||||
if (binding?.kind === 'legacy_reactive') {
|
||||
// TODO does this include `let double; $: double = x * 2`?
|
||||
binding.legacy_dependencies = Array.from(reactive_statement.dependencies);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!context.state.analysis.runes) {
|
||||
w.reactive_declaration_invalid_placement(node);
|
||||
}
|
||||
}
|
||||
|
||||
context.next();
|
||||
}
|
||||
Generated
Vendored
+24
@@ -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);
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/** @import { Literal } from 'estree' */
|
||||
import * as w from '../../../warnings.js';
|
||||
import { regex_bidirectional_control_characters } from '../../patterns.js';
|
||||
|
||||
/**
|
||||
* @param {Literal} node
|
||||
*/
|
||||
export function Literal(node) {
|
||||
if (typeof node.value === 'string') {
|
||||
if (regex_bidirectional_control_characters.test(node.value)) {
|
||||
w.bidirectional_control_characters(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
/** @import { MemberExpression } from 'estree' */
|
||||
/** @import { Context } from '../types' */
|
||||
import * as e from '../../../errors.js';
|
||||
import { is_pure, is_safe_identifier } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* @param {MemberExpression} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function MemberExpression(node, context) {
|
||||
if (node.object.type === 'Identifier' && node.property.type === 'Identifier') {
|
||||
const binding = context.state.scope.get(node.object.name);
|
||||
if (binding?.kind === 'rest_prop' && node.property.name.startsWith('$$')) {
|
||||
e.props_illegal_name(node.property);
|
||||
}
|
||||
}
|
||||
|
||||
if (context.state.expression) {
|
||||
context.state.expression.has_member_expression = true;
|
||||
context.state.expression.has_state ||= !is_pure(node, context);
|
||||
}
|
||||
|
||||
if (!is_safe_identifier(node, context.state.scope)) {
|
||||
context.state.analysis.needs_context = true;
|
||||
}
|
||||
|
||||
context.next();
|
||||
}
|
||||
Generated
Vendored
+17
@@ -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();
|
||||
}
|
||||
Generated
Vendored
+28
@@ -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 });
|
||||
}
|
||||
Generated
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
/** @import { PropertyDefinition } from 'estree' */
|
||||
/** @import { Context } from '../types' */
|
||||
import * as e from '../../../errors.js';
|
||||
import { get_name } from '../../nodes.js';
|
||||
|
||||
/**
|
||||
* @param {PropertyDefinition} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function PropertyDefinition(node, context) {
|
||||
const name = get_name(node.key);
|
||||
const field = name && context.state.state_fields.get(name);
|
||||
|
||||
if (field && node !== field.node && node.value) {
|
||||
if (/** @type {number} */ (node.start) < /** @type {number} */ (field.node.start)) {
|
||||
e.state_field_invalid_assignment(node);
|
||||
}
|
||||
}
|
||||
|
||||
context.next();
|
||||
}
|
||||
Generated
Vendored
+240
@@ -0,0 +1,240 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import { is_mathml, is_svg, is_void } from '../../../../utils.js';
|
||||
import {
|
||||
is_tag_valid_with_ancestor,
|
||||
is_tag_valid_with_parent
|
||||
} from '../../../../html-tree-validation.js';
|
||||
import * as e from '../../../errors.js';
|
||||
import * as w from '../../../warnings.js';
|
||||
import {
|
||||
create_attribute,
|
||||
is_custom_element_node,
|
||||
is_customizable_select_element
|
||||
} from '../../nodes.js';
|
||||
import { regex_starts_with_newline } from '../../patterns.js';
|
||||
import { check_element } from './shared/a11y/index.js';
|
||||
import { validate_element } from './shared/element.js';
|
||||
import { mark_subtree_dynamic } from './shared/fragment.js';
|
||||
import { object } from '../../../utils/ast.js';
|
||||
import { runes } from '../../../state.js';
|
||||
|
||||
/**
|
||||
* @param {AST.RegularElement} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function RegularElement(node, context) {
|
||||
validate_element(node, context);
|
||||
check_element(node, context);
|
||||
|
||||
node.metadata.path = [...context.path];
|
||||
context.state.analysis.elements.push(node);
|
||||
|
||||
// Special case: Move the children of <textarea> into a value attribute if they are dynamic
|
||||
if (node.name === 'textarea' && node.fragment.nodes.length > 0) {
|
||||
for (const attribute of node.attributes) {
|
||||
if (attribute.type === 'Attribute' && attribute.name === 'value') {
|
||||
e.textarea_invalid_content(node);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.fragment.nodes.length > 1 || node.fragment.nodes[0].type !== 'Text') {
|
||||
const first = node.fragment.nodes[0];
|
||||
if (first.type === 'Text') {
|
||||
// The leading newline character needs to be stripped because of a qirk:
|
||||
// It is ignored by browsers if the tag and its contents are set through
|
||||
// innerHTML, but we're now setting it through the value property at which
|
||||
// point it is _not_ ignored, so we need to strip it ourselves.
|
||||
// see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions
|
||||
// see https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
|
||||
first.data = first.data.replace(regex_starts_with_newline, '');
|
||||
first.raw = first.raw.replace(regex_starts_with_newline, '');
|
||||
}
|
||||
|
||||
node.attributes.push(
|
||||
create_attribute(
|
||||
'value',
|
||||
null,
|
||||
-1,
|
||||
-1,
|
||||
// @ts-ignore
|
||||
node.fragment.nodes
|
||||
)
|
||||
);
|
||||
|
||||
node.fragment.nodes = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Special case: `<select bind:value={foo}><option>{bar}</option>`
|
||||
// means we need to invalidate `bar` whenever `foo` is mutated
|
||||
if (node.name === 'select' && !runes) {
|
||||
for (const attribute of node.attributes) {
|
||||
if (
|
||||
attribute.type === 'BindDirective' &&
|
||||
attribute.name === 'value' &&
|
||||
attribute.expression.type !== 'SequenceExpression'
|
||||
) {
|
||||
const identifier = object(attribute.expression);
|
||||
const binding = identifier && context.state.scope.get(identifier.name);
|
||||
|
||||
if (binding) {
|
||||
for (const name of context.state.scope.references.keys()) {
|
||||
if (name === binding.node.name) continue;
|
||||
const indirect = context.state.scope.get(name);
|
||||
|
||||
if (indirect) {
|
||||
binding.legacy_indirect_bindings.add(indirect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Special case: single expression tag child of option element -> add "fake" attribute
|
||||
// to ensure that value types are the same (else for example numbers would be strings)
|
||||
if (
|
||||
node.name === 'option' &&
|
||||
node.fragment.nodes?.length === 1 &&
|
||||
node.fragment.nodes[0].type === 'ExpressionTag' &&
|
||||
!node.attributes.some(
|
||||
(attribute) => attribute.type === 'Attribute' && attribute.name === 'value'
|
||||
)
|
||||
) {
|
||||
const child = node.fragment.nodes[0];
|
||||
node.metadata.synthetic_value_node = child;
|
||||
}
|
||||
|
||||
// Special case: <select>, <option> or <optgroup> with rich content needs special hydration handling
|
||||
// We mark the subtree as dynamic so parent elements properly include the child init code
|
||||
if (is_customizable_select_element(node) || node.name === 'selectedcontent') {
|
||||
// Mark the element's own fragment as dynamic so it's not treated as static
|
||||
node.fragment.metadata.dynamic = true;
|
||||
// Also mark ancestor fragments so parents properly include the child init code
|
||||
mark_subtree_dynamic(context.path);
|
||||
}
|
||||
|
||||
const binding = context.state.scope.get(node.name);
|
||||
if (
|
||||
binding !== null &&
|
||||
binding.declaration_kind === 'import' &&
|
||||
binding.references.length === 0
|
||||
) {
|
||||
w.component_name_lowercase(node, node.name);
|
||||
}
|
||||
|
||||
node.metadata.has_spread = node.attributes.some(
|
||||
(attribute) => attribute.type === 'SpreadAttribute'
|
||||
);
|
||||
|
||||
const is_svg_element = () => {
|
||||
if (is_svg(node.name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (node.name === 'a' || node.name === 'title') {
|
||||
let i = context.path.length;
|
||||
|
||||
while (i--) {
|
||||
const ancestor = context.path[i];
|
||||
if (ancestor.type === 'RegularElement') {
|
||||
return ancestor.metadata.svg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
node.metadata.svg = is_svg_element();
|
||||
node.metadata.mathml = is_mathml(node.name);
|
||||
|
||||
if (is_custom_element_node(node) && node.attributes.length > 0) {
|
||||
// we're setting all attributes on custom elements through properties
|
||||
mark_subtree_dynamic(context.path);
|
||||
}
|
||||
|
||||
if (context.state.parent_element) {
|
||||
let past_parent = false;
|
||||
let only_warn = false;
|
||||
const ancestors = [context.state.parent_element];
|
||||
|
||||
for (let i = context.path.length - 1; i >= 0; i--) {
|
||||
const ancestor = context.path[i];
|
||||
|
||||
if (
|
||||
ancestor.type === 'IfBlock' ||
|
||||
ancestor.type === 'EachBlock' ||
|
||||
ancestor.type === 'AwaitBlock' ||
|
||||
ancestor.type === 'KeyBlock'
|
||||
) {
|
||||
// We're creating a separate template string inside blocks, which means client-side this would work
|
||||
only_warn = true;
|
||||
}
|
||||
|
||||
if (!past_parent) {
|
||||
if (ancestor.type === 'RegularElement' && ancestor.name === context.state.parent_element) {
|
||||
const message = is_tag_valid_with_parent(node.name, context.state.parent_element);
|
||||
if (message) {
|
||||
if (only_warn) {
|
||||
w.node_invalid_placement_ssr(node, message);
|
||||
} else {
|
||||
e.node_invalid_placement(node, message);
|
||||
}
|
||||
}
|
||||
|
||||
past_parent = true;
|
||||
}
|
||||
} else if (ancestor.type === 'RegularElement') {
|
||||
ancestors.push(ancestor.name);
|
||||
|
||||
const message = is_tag_valid_with_ancestor(node.name, ancestors);
|
||||
if (message) {
|
||||
if (only_warn) {
|
||||
w.node_invalid_placement_ssr(node, message);
|
||||
} else {
|
||||
e.node_invalid_placement(node, message);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
ancestor.type === 'Component' ||
|
||||
ancestor.type === 'SvelteComponent' ||
|
||||
ancestor.type === 'SvelteElement' ||
|
||||
ancestor.type === 'SvelteSelf' ||
|
||||
ancestor.type === 'SnippetBlock'
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strip off any namespace from the beginning of the node name.
|
||||
const node_name = node.name.replace(/[a-zA-Z-]*:/g, '');
|
||||
|
||||
if (
|
||||
context.state.analysis.source[node.end - 2] === '/' &&
|
||||
!is_void(node_name) &&
|
||||
!is_svg(node_name) &&
|
||||
!is_mathml(node_name)
|
||||
) {
|
||||
w.element_invalid_self_closing_tag(node, node.name);
|
||||
}
|
||||
|
||||
context.next({ ...context.state, parent_element: node.name });
|
||||
|
||||
// Special case: <a> tags are valid in both the SVG and HTML namespace.
|
||||
// If there's no parent, look downwards to see if it's the parent of a SVG or HTML element.
|
||||
if (node.name === 'a' && !context.state.parent_element) {
|
||||
for (const child of node.fragment.nodes) {
|
||||
if (child.type === 'RegularElement') {
|
||||
if (child.metadata.svg && child.name !== 'svg') {
|
||||
node.metadata.svg = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import { unwrap_optional } from '../../../utils/ast.js';
|
||||
import * as e from '../../../errors.js';
|
||||
import { validate_opening_tag } from './shared/utils.js';
|
||||
import { mark_subtree_dynamic } from './shared/fragment.js';
|
||||
import { is_resolved_snippet } from './shared/snippets.js';
|
||||
import { ExpressionMetadata } from '../../nodes.js';
|
||||
|
||||
/**
|
||||
* @param {AST.RenderTag} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function RenderTag(node, context) {
|
||||
validate_opening_tag(node, context.state, '@');
|
||||
|
||||
node.metadata.path = [...context.path];
|
||||
|
||||
const expression = unwrap_optional(node.expression);
|
||||
const callee = expression.callee;
|
||||
|
||||
const binding = callee.type === 'Identifier' ? context.state.scope.get(callee.name) : null;
|
||||
|
||||
node.metadata.dynamic = binding?.kind !== 'normal';
|
||||
|
||||
/**
|
||||
* If we can't unambiguously resolve this to a declaration, we
|
||||
* must assume the worst and link the render tag to every snippet
|
||||
*/
|
||||
let resolved = callee.type === 'Identifier' && is_resolved_snippet(binding);
|
||||
|
||||
if (binding?.initial?.type === 'SnippetBlock') {
|
||||
// if this render tag unambiguously references a local snippet, our job is easy
|
||||
node.metadata.snippets.add(binding.initial);
|
||||
}
|
||||
|
||||
context.state.analysis.snippet_renderers.set(node, resolved);
|
||||
context.state.analysis.uses_render_tags = true;
|
||||
|
||||
const raw_args = unwrap_optional(node.expression).arguments;
|
||||
for (const arg of raw_args) {
|
||||
if (arg.type === 'SpreadElement') {
|
||||
e.render_tag_invalid_spread_argument(arg);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
callee.type === 'MemberExpression' &&
|
||||
callee.property.type === 'Identifier' &&
|
||||
['bind', 'apply', 'call'].includes(callee.property.name)
|
||||
) {
|
||||
e.render_tag_invalid_call_expression(node);
|
||||
}
|
||||
|
||||
mark_subtree_dynamic(context.path);
|
||||
|
||||
context.visit(callee, { ...context.state, expression: node.metadata.expression });
|
||||
|
||||
for (const arg of expression.arguments) {
|
||||
const metadata = new ExpressionMetadata();
|
||||
node.metadata.arguments.push(metadata);
|
||||
|
||||
context.visit(arg, {
|
||||
...context.state,
|
||||
expression: metadata
|
||||
});
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+42
@@ -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();
|
||||
}
|
||||
Generated
Vendored
+113
@@ -0,0 +1,113 @@
|
||||
/** @import { AST, Binding } from '#compiler' */
|
||||
/** @import { Scope } from '../../scope' */
|
||||
/** @import { Context } from '../types' */
|
||||
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
|
||||
import * as e from '../../../errors.js';
|
||||
|
||||
/**
|
||||
* @param {AST.SnippetBlock} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function SnippetBlock(node, context) {
|
||||
context.state.analysis.snippets.add(node);
|
||||
|
||||
validate_block_not_empty(node.body, context);
|
||||
|
||||
if (context.state.analysis.runes) {
|
||||
validate_opening_tag(node, context.state, '#');
|
||||
}
|
||||
|
||||
for (const arg of node.parameters) {
|
||||
if (arg.type === 'RestElement') {
|
||||
e.snippet_invalid_rest_parameter(arg);
|
||||
}
|
||||
}
|
||||
|
||||
context.next({ ...context.state, parent_element: null });
|
||||
|
||||
const can_hoist =
|
||||
context.path.length === 1 &&
|
||||
context.path[0].type === 'Fragment' &&
|
||||
can_hoist_snippet(context.state.scope, context.state.scopes);
|
||||
|
||||
const name = node.expression.name;
|
||||
|
||||
if (can_hoist) {
|
||||
const binding = /** @type {Binding} */ (context.state.scope.get(name));
|
||||
context.state.analysis.module.scope.declarations.set(name, binding);
|
||||
}
|
||||
|
||||
node.metadata.can_hoist = can_hoist;
|
||||
|
||||
const { path } = context;
|
||||
const parent = path.at(-2);
|
||||
if (!parent) return;
|
||||
|
||||
if (
|
||||
parent.type === 'Component' &&
|
||||
parent.attributes.some(
|
||||
(attribute) =>
|
||||
(attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
|
||||
attribute.name === node.expression.name
|
||||
)
|
||||
) {
|
||||
e.snippet_shadowing_prop(node, node.expression.name);
|
||||
}
|
||||
|
||||
if (node.expression.name !== 'children') return;
|
||||
|
||||
if (
|
||||
parent.type === 'Component' ||
|
||||
parent.type === 'SvelteComponent' ||
|
||||
parent.type === 'SvelteSelf'
|
||||
) {
|
||||
if (
|
||||
parent.fragment.nodes.some(
|
||||
(node) =>
|
||||
node.type !== 'SnippetBlock' &&
|
||||
(node.type !== 'Text' || node.data.trim()) &&
|
||||
node.type !== 'Comment'
|
||||
)
|
||||
) {
|
||||
e.snippet_conflict(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Map<AST.SvelteNode, Scope>} scopes
|
||||
* @param {Scope} scope
|
||||
*/
|
||||
function can_hoist_snippet(scope, scopes, visited = new Set()) {
|
||||
for (const [reference] of scope.references) {
|
||||
const binding = scope.get(reference);
|
||||
if (!binding) continue;
|
||||
|
||||
if (binding.blocker) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (binding.scope.function_depth === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// ignore bindings declared inside the snippet (e.g. the snippet's own parameters)
|
||||
if (binding.scope.function_depth >= scope.function_depth) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (binding.initial?.type === 'SnippetBlock') {
|
||||
if (visited.has(binding)) continue;
|
||||
visited.add(binding);
|
||||
const snippet_scope = /** @type {Scope} */ (scopes.get(binding.initial));
|
||||
|
||||
if (can_hoist_snippet(snippet_scope, scopes, visited)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
Generated
Vendored
+13
@@ -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 });
|
||||
}
|
||||
Generated
Vendored
+16
@@ -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();
|
||||
}
|
||||
Generated
Vendored
+39
@@ -0,0 +1,39 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import * as e from '../../../errors.js';
|
||||
import { get_attribute_chunks } from '../../../utils/ast.js';
|
||||
import { mark_subtree_dynamic } from './shared/fragment.js';
|
||||
|
||||
/**
|
||||
* @param {AST.StyleDirective} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function StyleDirective(node, context) {
|
||||
if (node.modifiers.length > 1 || (node.modifiers.length && node.modifiers[0] !== 'important')) {
|
||||
e.style_directive_invalid_modifier(node);
|
||||
}
|
||||
|
||||
mark_subtree_dynamic(context.path);
|
||||
|
||||
if (node.value === true) {
|
||||
// get the binding for node.name and change the binding to state
|
||||
let binding = context.state.scope.get(node.name);
|
||||
|
||||
if (binding) {
|
||||
if (binding.kind !== 'normal') {
|
||||
node.metadata.expression.has_state = true;
|
||||
}
|
||||
if (binding.blocker) {
|
||||
node.metadata.expression.dependencies.add(binding);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
context.next();
|
||||
|
||||
for (const chunk of get_attribute_chunks(node.value)) {
|
||||
if (chunk.type !== 'ExpressionTag') continue;
|
||||
|
||||
node.metadata.expression.merge(chunk.metadata.expression);
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+22
@@ -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();
|
||||
}
|
||||
Generated
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import * as e from '../../../errors.js';
|
||||
import { mark_subtree_dynamic } from './shared/fragment.js';
|
||||
|
||||
const valid = ['onerror', 'failed', 'pending'];
|
||||
|
||||
/**
|
||||
* @param {AST.SvelteBoundary} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function SvelteBoundary(node, context) {
|
||||
for (const attribute of node.attributes) {
|
||||
if (attribute.type !== 'Attribute' || !valid.includes(attribute.name)) {
|
||||
e.svelte_boundary_invalid_attribute(attribute);
|
||||
}
|
||||
|
||||
if (
|
||||
attribute.value === true ||
|
||||
(Array.isArray(attribute.value) &&
|
||||
(attribute.value.length !== 1 || attribute.value[0].type !== 'ExpressionTag'))
|
||||
) {
|
||||
e.svelte_boundary_invalid_attribute_value(attribute);
|
||||
}
|
||||
}
|
||||
|
||||
mark_subtree_dynamic(context.path);
|
||||
|
||||
context.next();
|
||||
}
|
||||
Generated
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import * as w from '../../../warnings.js';
|
||||
import { visit_component } from './shared/component.js';
|
||||
|
||||
/**
|
||||
* @param {AST.SvelteComponent} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function SvelteComponent(node, context) {
|
||||
if (context.state.analysis.runes) {
|
||||
w.svelte_component_deprecated(node);
|
||||
}
|
||||
|
||||
context.visit(node.expression, { ...context.state, expression: node.metadata.expression });
|
||||
|
||||
visit_component(node, context);
|
||||
}
|
||||
Generated
Vendored
+24
@@ -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();
|
||||
}
|
||||
Generated
Vendored
+78
@@ -0,0 +1,78 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import { NAMESPACE_MATHML, NAMESPACE_SVG } from '../../../../constants.js';
|
||||
import { is_text_attribute } from '../../../utils/ast.js';
|
||||
import { check_element } from './shared/a11y/index.js';
|
||||
import { validate_element } from './shared/element.js';
|
||||
import { mark_subtree_dynamic } from './shared/fragment.js';
|
||||
|
||||
/**
|
||||
* @param {AST.SvelteElement} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function SvelteElement(node, context) {
|
||||
validate_element(node, context);
|
||||
check_element(node, context);
|
||||
|
||||
node.metadata.path = [...context.path];
|
||||
context.state.analysis.elements.push(node);
|
||||
|
||||
const xmlns = /** @type {AST.Attribute & { value: [AST.Text] } | undefined} */ (
|
||||
node.attributes.find(
|
||||
(a) => a.type === 'Attribute' && a.name === 'xmlns' && is_text_attribute(a)
|
||||
)
|
||||
);
|
||||
|
||||
if (xmlns) {
|
||||
node.metadata.svg = xmlns.value[0].data === NAMESPACE_SVG;
|
||||
node.metadata.mathml = xmlns.value[0].data === NAMESPACE_MATHML;
|
||||
} else {
|
||||
let i = context.path.length;
|
||||
while (i--) {
|
||||
const ancestor = context.path[i];
|
||||
|
||||
if (
|
||||
ancestor.type === 'Component' ||
|
||||
ancestor.type === 'SvelteComponent' ||
|
||||
ancestor.type === 'SvelteFragment' ||
|
||||
ancestor.type === 'SnippetBlock' ||
|
||||
i === 0
|
||||
) {
|
||||
// Root element, or inside a slot or a snippet -> this resets the namespace, so assume the component namespace
|
||||
node.metadata.svg = context.state.options.namespace === 'svg';
|
||||
node.metadata.mathml = context.state.options.namespace === 'mathml';
|
||||
break;
|
||||
}
|
||||
|
||||
if (ancestor.type === 'SvelteElement' || ancestor.type === 'RegularElement') {
|
||||
node.metadata.svg =
|
||||
ancestor.type === 'RegularElement' && ancestor.name === 'foreignObject'
|
||||
? false
|
||||
: ancestor.metadata.svg;
|
||||
|
||||
node.metadata.mathml =
|
||||
ancestor.type === 'RegularElement' && ancestor.name === 'foreignObject'
|
||||
? false
|
||||
: ancestor.metadata.mathml;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mark_subtree_dynamic(context.path);
|
||||
|
||||
context.visit(node.tag, {
|
||||
...context.state,
|
||||
expression: node.metadata.expression
|
||||
});
|
||||
|
||||
for (const attribute of node.attributes) {
|
||||
context.visit(attribute);
|
||||
}
|
||||
|
||||
context.visit(node.fragment, {
|
||||
...context.state,
|
||||
parent_element: null
|
||||
});
|
||||
}
|
||||
Generated
Vendored
+27
@@ -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 });
|
||||
}
|
||||
Generated
Vendored
+18
@@ -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();
|
||||
}
|
||||
Generated
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import { visit_component } from './shared/component.js';
|
||||
import * as e from '../../../errors.js';
|
||||
import * as w from '../../../warnings.js';
|
||||
import { filename, UNKNOWN_FILENAME } from '../../../state.js';
|
||||
|
||||
/**
|
||||
* @param {AST.SvelteSelf} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function SvelteSelf(node, context) {
|
||||
const valid = context.path.some(
|
||||
(node) =>
|
||||
node.type === 'IfBlock' ||
|
||||
node.type === 'EachBlock' ||
|
||||
node.type === 'Component' ||
|
||||
node.type === 'SnippetBlock'
|
||||
);
|
||||
|
||||
if (!valid) {
|
||||
e.svelte_self_invalid_placement(node);
|
||||
}
|
||||
|
||||
if (context.state.analysis.runes) {
|
||||
const name = filename === UNKNOWN_FILENAME ? 'Self' : context.state.analysis.name;
|
||||
const basename =
|
||||
filename === UNKNOWN_FILENAME
|
||||
? 'Self.svelte'
|
||||
: /** @type {string} */ (filename.split(/[/\\]/).pop());
|
||||
|
||||
w.svelte_self_deprecated(node, name, basename);
|
||||
}
|
||||
|
||||
visit_component(node, context);
|
||||
}
|
||||
Generated
Vendored
+24
@@ -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();
|
||||
}
|
||||
Generated
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
/** @import { TaggedTemplateExpression } from 'estree' */
|
||||
/** @import { Context } from '../types' */
|
||||
import { is_pure } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* @param {TaggedTemplateExpression} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function TaggedTemplateExpression(node, context) {
|
||||
if (context.state.expression && !is_pure(node.tag, context)) {
|
||||
context.state.expression.has_call = true;
|
||||
context.state.expression.has_state = true;
|
||||
}
|
||||
|
||||
context.next();
|
||||
}
|
||||
Generated
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
/** @import { TemplateElement } from 'estree' */
|
||||
import * as w from '../../../warnings.js';
|
||||
import { regex_bidirectional_control_characters } from '../../patterns.js';
|
||||
|
||||
/**
|
||||
* @param {TemplateElement} node
|
||||
*/
|
||||
export function TemplateElement(node) {
|
||||
if (regex_bidirectional_control_characters.test(node.value.cooked ?? '')) {
|
||||
w.bidirectional_control_characters(node);
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import { is_tag_valid_with_parent } from '../../../../html-tree-validation.js';
|
||||
import { regex_bidirectional_control_characters, regex_not_whitespace } from '../../patterns.js';
|
||||
import * as e from '../../../errors.js';
|
||||
import * as w from '../../../warnings.js';
|
||||
import { extract_svelte_ignore } from '../../../utils/extract_svelte_ignore.js';
|
||||
|
||||
/**
|
||||
* @param {AST.Text} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function Text(node, context) {
|
||||
const parent = /** @type {AST.SvelteNode} */ (context.path.at(-1));
|
||||
|
||||
if (
|
||||
parent.type === 'Fragment' &&
|
||||
context.state.parent_element &&
|
||||
regex_not_whitespace.test(node.data)
|
||||
) {
|
||||
const message = is_tag_valid_with_parent('#text', context.state.parent_element);
|
||||
if (message) {
|
||||
e.node_invalid_placement(node, message);
|
||||
}
|
||||
}
|
||||
|
||||
regex_bidirectional_control_characters.lastIndex = 0;
|
||||
for (const match of node.data.matchAll(regex_bidirectional_control_characters)) {
|
||||
let is_ignored = false;
|
||||
|
||||
// if we have a svelte-ignore comment earlier in the text, bail
|
||||
// (otherwise we can only use svelte-ignore on parent elements/blocks)
|
||||
if (parent.type === 'Fragment') {
|
||||
for (const child of parent.nodes) {
|
||||
if (child === node) break;
|
||||
|
||||
if (child.type === 'Comment') {
|
||||
is_ignored ||= extract_svelte_ignore(
|
||||
child.start + 4,
|
||||
child.data,
|
||||
context.state.analysis.runes
|
||||
).includes('bidirectional_control_characters');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_ignored) {
|
||||
let start = match.index + node.start;
|
||||
w.bidirectional_control_characters({ start, end: start + match[0].length });
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+21
@@ -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();
|
||||
}
|
||||
Generated
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import * as e from '../../../errors.js';
|
||||
|
||||
import { mark_subtree_dynamic } from './shared/fragment.js';
|
||||
|
||||
/**
|
||||
* @param {AST.TransitionDirective} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function TransitionDirective(node, context) {
|
||||
mark_subtree_dynamic(context.path);
|
||||
|
||||
context.next({ ...context.state, expression: node.metadata.expression });
|
||||
|
||||
if (node.metadata.expression.has_await) {
|
||||
e.illegal_await_expression(node);
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
/** @import { UpdateExpression } from 'estree' */
|
||||
/** @import { Context } from '../types' */
|
||||
import { object } from '../../../utils/ast.js';
|
||||
import { validate_assignment } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* @param {UpdateExpression} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function UpdateExpression(node, context) {
|
||||
validate_assignment(node, node.argument, context);
|
||||
|
||||
if (context.state.reactive_statement) {
|
||||
const id = node.argument.type === 'MemberExpression' ? object(node.argument) : node.argument;
|
||||
if (id?.type === 'Identifier') {
|
||||
const binding = context.state.scope.get(id.name);
|
||||
|
||||
if (binding) {
|
||||
context.state.reactive_statement.assignments.add(binding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (context.state.expression) {
|
||||
context.state.expression.has_assignment = true;
|
||||
}
|
||||
|
||||
context.next();
|
||||
}
|
||||
Generated
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import { mark_subtree_dynamic } from './shared/fragment.js';
|
||||
import * as e from '../../../errors.js';
|
||||
|
||||
/**
|
||||
* @param {AST.UseDirective} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function UseDirective(node, context) {
|
||||
mark_subtree_dynamic(context.path);
|
||||
|
||||
context.next({ ...context.state, expression: node.metadata.expression });
|
||||
|
||||
if (node.metadata.expression.has_await) {
|
||||
e.illegal_await_expression(node);
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+160
@@ -0,0 +1,160 @@
|
||||
/** @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 * as w from '../../../warnings.js';
|
||||
import { extract_paths } from '../../../utils/ast.js';
|
||||
import { equal } from '../../../utils/assert.js';
|
||||
import * as b from '#compiler/builders';
|
||||
|
||||
/**
|
||||
* @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, b.id('dummy'));
|
||||
|
||||
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' && binding.kind === 'rest_prop' && node.id.type === 'ObjectPattern') {
|
||||
const { properties } = node.id;
|
||||
/** @type {string[]} */
|
||||
const exclude_props = [];
|
||||
for (const property of properties) {
|
||||
if (property.type === 'RestElement') {
|
||||
continue;
|
||||
}
|
||||
const key = /** @type {Identifier | Literal & { value: string | number }} */ (
|
||||
property.key
|
||||
);
|
||||
exclude_props.push(key.type === 'Identifier' ? key.name : key.value.toString());
|
||||
}
|
||||
(binding.metadata ??= {}).exclude_props = exclude_props;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rune === '$props') {
|
||||
if (node.id.type !== 'ObjectPattern' && node.id.type !== 'Identifier') {
|
||||
e.props_invalid_identifier(node);
|
||||
}
|
||||
|
||||
if (
|
||||
context.state.analysis.custom_element &&
|
||||
context.state.options.customElementOptions?.props == null
|
||||
) {
|
||||
let warn_on;
|
||||
if (
|
||||
node.id.type === 'Identifier' ||
|
||||
(warn_on = node.id.properties.find((p) => p.type === 'RestElement')) != null
|
||||
) {
|
||||
w.custom_element_props_identifier(warn_on ?? node.id);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node.init && get_rune(node.init, context.state.scope) === '$props') {
|
||||
// prevent erroneous `state_referenced_locally` warnings on prop fallbacks
|
||||
context.visit(node.id, {
|
||||
...context.state,
|
||||
function_depth: context.state.function_depth + 1
|
||||
});
|
||||
|
||||
context.visit(node.init);
|
||||
} else {
|
||||
context.next();
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+334
@@ -0,0 +1,334 @@
|
||||
/** @import { ARIARoleRelationConcept } from 'aria-query' */
|
||||
import { roles as roles_map, elementRoles } from 'aria-query';
|
||||
// @ts-expect-error package doesn't provide typings
|
||||
import { AXObjects, elementAXObjects } from 'axobject-query';
|
||||
|
||||
export const aria_attributes =
|
||||
'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(
|
||||
' '
|
||||
);
|
||||
|
||||
/** @type {Record<string, string[]>} */
|
||||
export const a11y_required_attributes = {
|
||||
a: ['href'],
|
||||
area: ['alt', 'aria-label', 'aria-labelledby'],
|
||||
// html-has-lang
|
||||
html: ['lang'],
|
||||
// iframe-has-title
|
||||
iframe: ['title'],
|
||||
img: ['alt'],
|
||||
object: ['title', 'aria-label', 'aria-labelledby']
|
||||
};
|
||||
|
||||
export const a11y_distracting_elements = ['blink', 'marquee'];
|
||||
|
||||
// this excludes `<a>` and `<button>` because they are handled separately
|
||||
export const a11y_required_content = [
|
||||
// heading-has-content
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6'
|
||||
];
|
||||
|
||||
export const a11y_labelable = [
|
||||
'button',
|
||||
'input',
|
||||
'keygen',
|
||||
'meter',
|
||||
'output',
|
||||
'progress',
|
||||
'select',
|
||||
'textarea'
|
||||
];
|
||||
|
||||
export const a11y_interactive_handlers = [
|
||||
// Keyboard events
|
||||
'keypress',
|
||||
'keydown',
|
||||
'keyup',
|
||||
// Click events
|
||||
'click',
|
||||
'contextmenu',
|
||||
'dblclick',
|
||||
'drag',
|
||||
'dragend',
|
||||
'dragenter',
|
||||
'dragexit',
|
||||
'dragleave',
|
||||
'dragover',
|
||||
'dragstart',
|
||||
'drop',
|
||||
'mousedown',
|
||||
'mouseenter',
|
||||
'mouseleave',
|
||||
'mousemove',
|
||||
'mouseout',
|
||||
'mouseover',
|
||||
'mouseup',
|
||||
// Pointer events
|
||||
'pointerdown',
|
||||
'pointerup',
|
||||
'pointermove',
|
||||
'pointerenter',
|
||||
'pointerleave',
|
||||
'pointerover',
|
||||
'pointerout',
|
||||
'pointercancel',
|
||||
// Touch events
|
||||
'touchstart',
|
||||
'touchend',
|
||||
'touchmove',
|
||||
'touchcancel'
|
||||
];
|
||||
|
||||
export const a11y_recommended_interactive_handlers = [
|
||||
'click',
|
||||
'mousedown',
|
||||
'mouseup',
|
||||
'keypress',
|
||||
'keydown',
|
||||
'keyup'
|
||||
];
|
||||
|
||||
export const a11y_nested_implicit_semantics = new Map([
|
||||
['header', 'banner'],
|
||||
['footer', 'contentinfo']
|
||||
]);
|
||||
|
||||
export const a11y_implicit_semantics = new Map([
|
||||
['a', 'link'],
|
||||
['area', 'link'],
|
||||
['article', 'article'],
|
||||
['aside', 'complementary'],
|
||||
['body', 'document'],
|
||||
['button', 'button'],
|
||||
['datalist', 'listbox'],
|
||||
['dd', 'definition'],
|
||||
['dfn', 'term'],
|
||||
['dialog', 'dialog'],
|
||||
['details', 'group'],
|
||||
['dt', 'term'],
|
||||
['fieldset', 'group'],
|
||||
['figure', 'figure'],
|
||||
['form', 'form'],
|
||||
['h1', 'heading'],
|
||||
['h2', 'heading'],
|
||||
['h3', 'heading'],
|
||||
['h4', 'heading'],
|
||||
['h5', 'heading'],
|
||||
['h6', 'heading'],
|
||||
['hr', 'separator'],
|
||||
['img', 'img'],
|
||||
['li', 'listitem'],
|
||||
['link', 'link'],
|
||||
['main', 'main'],
|
||||
['menu', 'list'],
|
||||
['meter', 'progressbar'],
|
||||
['nav', 'navigation'],
|
||||
['ol', 'list'],
|
||||
['option', 'option'],
|
||||
['optgroup', 'group'],
|
||||
['output', 'status'],
|
||||
['progress', 'progressbar'],
|
||||
['section', 'region'],
|
||||
['summary', 'button'],
|
||||
['table', 'table'],
|
||||
['tbody', 'rowgroup'],
|
||||
['textarea', 'textbox'],
|
||||
['tfoot', 'rowgroup'],
|
||||
['thead', 'rowgroup'],
|
||||
['tr', 'row'],
|
||||
['ul', 'list']
|
||||
]);
|
||||
|
||||
export const menuitem_type_to_implicit_role = new Map([
|
||||
['command', 'menuitem'],
|
||||
['checkbox', 'menuitemcheckbox'],
|
||||
['radio', 'menuitemradio']
|
||||
]);
|
||||
|
||||
export const input_type_to_implicit_role = new Map([
|
||||
['button', 'button'],
|
||||
['image', 'button'],
|
||||
['reset', 'button'],
|
||||
['submit', 'button'],
|
||||
['checkbox', 'checkbox'],
|
||||
['radio', 'radio'],
|
||||
['range', 'slider'],
|
||||
['number', 'spinbutton'],
|
||||
['email', 'textbox'],
|
||||
['search', 'searchbox'],
|
||||
['tel', 'textbox'],
|
||||
['text', 'textbox'],
|
||||
['url', 'textbox']
|
||||
]);
|
||||
|
||||
/**
|
||||
* Exceptions to the rule which follows common A11y conventions
|
||||
* TODO make this configurable by the user
|
||||
* @type {Record<string, string[]>}
|
||||
*/
|
||||
export const a11y_non_interactive_element_to_interactive_role_exceptions = {
|
||||
ul: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
|
||||
ol: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
|
||||
menu: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
|
||||
li: ['menuitem', 'option', 'row', 'tab', 'treeitem'],
|
||||
table: ['grid'],
|
||||
td: ['gridcell'],
|
||||
fieldset: ['radiogroup', 'presentation']
|
||||
};
|
||||
|
||||
export const combobox_if_list = ['email', 'search', 'tel', 'text', 'url'];
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofilling-form-controls:-the-autocomplete-attribute
|
||||
export const address_type_tokens = ['shipping', 'billing'];
|
||||
|
||||
export const autofill_field_name_tokens = [
|
||||
'',
|
||||
'on',
|
||||
'off',
|
||||
'name',
|
||||
'honorific-prefix',
|
||||
'given-name',
|
||||
'additional-name',
|
||||
'family-name',
|
||||
'honorific-suffix',
|
||||
'nickname',
|
||||
'username',
|
||||
'new-password',
|
||||
'current-password',
|
||||
'one-time-code',
|
||||
'organization-title',
|
||||
'organization',
|
||||
'street-address',
|
||||
'address-line1',
|
||||
'address-line2',
|
||||
'address-line3',
|
||||
'address-level4',
|
||||
'address-level3',
|
||||
'address-level2',
|
||||
'address-level1',
|
||||
'country',
|
||||
'country-name',
|
||||
'postal-code',
|
||||
'cc-name',
|
||||
'cc-given-name',
|
||||
'cc-additional-name',
|
||||
'cc-family-name',
|
||||
'cc-number',
|
||||
'cc-exp',
|
||||
'cc-exp-month',
|
||||
'cc-exp-year',
|
||||
'cc-csc',
|
||||
'cc-type',
|
||||
'transaction-currency',
|
||||
'transaction-amount',
|
||||
'language',
|
||||
'bday',
|
||||
'bday-day',
|
||||
'bday-month',
|
||||
'bday-year',
|
||||
'sex',
|
||||
'url',
|
||||
'photo'
|
||||
];
|
||||
|
||||
export const contact_type_tokens = ['home', 'work', 'mobile', 'fax', 'pager'];
|
||||
|
||||
export const autofill_contact_field_name_tokens = [
|
||||
'tel',
|
||||
'tel-country-code',
|
||||
'tel-national',
|
||||
'tel-area-code',
|
||||
'tel-local',
|
||||
'tel-local-prefix',
|
||||
'tel-local-suffix',
|
||||
'tel-extension',
|
||||
'email',
|
||||
'impp'
|
||||
];
|
||||
|
||||
export const ElementInteractivity = /** @type {const} */ ({
|
||||
Interactive: 'interactive',
|
||||
NonInteractive: 'non-interactive',
|
||||
Static: 'static'
|
||||
});
|
||||
|
||||
export const invisible_elements = ['meta', 'html', 'script', 'style'];
|
||||
|
||||
export const aria_roles = roles_map.keys();
|
||||
|
||||
export const abstract_roles = aria_roles.filter((role) => roles_map.get(role)?.abstract);
|
||||
|
||||
const non_abstract_roles = aria_roles.filter((name) => !abstract_roles.includes(name));
|
||||
|
||||
export const non_interactive_roles = non_abstract_roles
|
||||
.filter((name) => {
|
||||
const role = roles_map.get(name);
|
||||
return (
|
||||
// 'toolbar' does not descend from widget, but it does support
|
||||
// aria-activedescendant, thus in practice we treat it as a widget.
|
||||
// focusable tabpanel elements are recommended if any panels in a set contain content where the first element in the panel is not focusable.
|
||||
// 'generic' is meant to have no semantic meaning.
|
||||
// 'cell' is treated as CellRole by the AXObject which is interactive, so we treat 'cell' it as interactive as well.
|
||||
!['toolbar', 'tabpanel', 'generic', 'cell'].includes(name) &&
|
||||
!role?.superClass.some((classes) => classes.includes('widget') || classes.includes('window'))
|
||||
);
|
||||
})
|
||||
.concat(
|
||||
// The `progressbar` is descended from `widget`, but in practice, its
|
||||
// value is always `readonly`, so we treat it as a non-interactive role.
|
||||
'progressbar'
|
||||
);
|
||||
|
||||
export const interactive_roles = non_abstract_roles.filter(
|
||||
(name) =>
|
||||
!non_interactive_roles.includes(name) &&
|
||||
// 'generic' is meant to have no semantic meaning.
|
||||
name !== 'generic'
|
||||
);
|
||||
|
||||
export const presentation_roles = ['presentation', 'none'];
|
||||
|
||||
/** @type {ARIARoleRelationConcept[]} */
|
||||
export const non_interactive_element_role_schemas = [];
|
||||
|
||||
/** @type {ARIARoleRelationConcept[]} */
|
||||
export const interactive_element_role_schemas = [];
|
||||
|
||||
for (const [schema, roles] of elementRoles.entries()) {
|
||||
if ([...roles].every((role) => role !== 'generic' && non_interactive_roles.includes(role))) {
|
||||
non_interactive_element_role_schemas.push(schema);
|
||||
}
|
||||
|
||||
if ([...roles].every((role) => interactive_roles.includes(role))) {
|
||||
interactive_element_role_schemas.push(schema);
|
||||
}
|
||||
}
|
||||
|
||||
const interactive_ax_objects = [...AXObjects.keys()].filter(
|
||||
(name) => AXObjects.get(name).type === 'widget'
|
||||
);
|
||||
|
||||
/** @type {ARIARoleRelationConcept[]} */
|
||||
export const interactive_element_ax_object_schemas = [];
|
||||
|
||||
/** @type {ARIARoleRelationConcept[]} */
|
||||
export const non_interactive_element_ax_object_schemas = [];
|
||||
|
||||
const non_interactive_ax_objects = [...AXObjects.keys()].filter((name) =>
|
||||
['windows', 'structure'].includes(AXObjects.get(name).type)
|
||||
);
|
||||
|
||||
for (const [schema, ax_object] of elementAXObjects.entries()) {
|
||||
if ([...ax_object].every((role) => interactive_ax_objects.includes(role))) {
|
||||
interactive_element_ax_object_schemas.push(schema);
|
||||
}
|
||||
|
||||
if ([...ax_object].every((role) => non_interactive_ax_objects.includes(role))) {
|
||||
non_interactive_element_ax_object_schemas.push(schema);
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+954
@@ -0,0 +1,954 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../../../types.js' */
|
||||
/** @import { ARIARoleDefinitionKey, ARIARoleRelationConcept, ARIAProperty, ARIAPropertyDefinition, ARIARoleDefinition } from 'aria-query' */
|
||||
import {
|
||||
a11y_distracting_elements,
|
||||
a11y_implicit_semantics,
|
||||
a11y_interactive_handlers,
|
||||
a11y_labelable,
|
||||
a11y_nested_implicit_semantics,
|
||||
a11y_non_interactive_element_to_interactive_role_exceptions,
|
||||
a11y_recommended_interactive_handlers,
|
||||
a11y_required_attributes,
|
||||
a11y_required_content,
|
||||
abstract_roles,
|
||||
address_type_tokens,
|
||||
aria_attributes,
|
||||
aria_roles,
|
||||
autofill_contact_field_name_tokens,
|
||||
autofill_field_name_tokens,
|
||||
combobox_if_list,
|
||||
contact_type_tokens,
|
||||
ElementInteractivity,
|
||||
input_type_to_implicit_role,
|
||||
interactive_element_ax_object_schemas,
|
||||
interactive_element_role_schemas,
|
||||
interactive_roles,
|
||||
invisible_elements,
|
||||
menuitem_type_to_implicit_role,
|
||||
non_interactive_element_ax_object_schemas,
|
||||
non_interactive_element_role_schemas,
|
||||
non_interactive_roles,
|
||||
presentation_roles
|
||||
} from './constants.js';
|
||||
import { roles as roles_map, aria } from 'aria-query';
|
||||
// @ts-expect-error package doesn't provide typings
|
||||
import { AXObjectRoles, elementAXObjects } from 'axobject-query';
|
||||
import {
|
||||
regex_heading_tags,
|
||||
regex_js_prefix,
|
||||
regex_not_whitespace,
|
||||
regex_redundant_img_alt,
|
||||
regex_starts_with_vowel,
|
||||
regex_whitespaces
|
||||
} from '../../../../patterns.js';
|
||||
import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
|
||||
import { list } from '../../../../../utils/string.js';
|
||||
import { walk } from 'zimmerframe';
|
||||
import fuzzymatch from '../../../../1-parse/utils/fuzzymatch.js';
|
||||
import { is_content_editable_binding } from '../../../../../../utils.js';
|
||||
import * as w from '../../../../../warnings.js';
|
||||
|
||||
/**
|
||||
* @param {AST.RegularElement | AST.SvelteElement} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function check_element(node, context) {
|
||||
/** @type {Map<string, AST.Attribute>} */
|
||||
const attribute_map = new Map();
|
||||
|
||||
/** @type {Set<string>} */
|
||||
const handlers = new Set();
|
||||
|
||||
/** @type {AST.Attribute[]} */
|
||||
const attributes = [];
|
||||
|
||||
const is_dynamic_element = node.type === 'SvelteElement';
|
||||
|
||||
let has_spread = false;
|
||||
let has_contenteditable_attr = false;
|
||||
let has_contenteditable_binding = false;
|
||||
|
||||
for (const attribute of node.attributes) {
|
||||
switch (attribute.type) {
|
||||
case 'Attribute': {
|
||||
if (is_event_attribute(attribute)) {
|
||||
handlers.add(attribute.name.slice(2));
|
||||
} else {
|
||||
attributes.push(attribute);
|
||||
attribute_map.set(attribute.name, attribute);
|
||||
if (attribute.name === 'contenteditable') {
|
||||
has_contenteditable_attr = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'SpreadAttribute': {
|
||||
has_spread = true;
|
||||
break;
|
||||
}
|
||||
case 'BindDirective': {
|
||||
if (is_content_editable_binding(attribute.name)) {
|
||||
has_contenteditable_binding = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'OnDirective': {
|
||||
handlers.add(attribute.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const interactivity = element_interactivity(node.name, attribute_map);
|
||||
const is_interactive = interactivity === ElementInteractivity.Interactive;
|
||||
const is_non_interactive = interactivity === ElementInteractivity.NonInteractive;
|
||||
const is_static = interactivity === ElementInteractivity.Static;
|
||||
|
||||
for (const attribute of node.attributes) {
|
||||
if (attribute.type !== 'Attribute') continue;
|
||||
|
||||
const name = attribute.name.toLowerCase();
|
||||
// aria-props
|
||||
if (name.startsWith('aria-')) {
|
||||
if (invisible_elements.includes(node.name)) {
|
||||
// aria-unsupported-elements
|
||||
w.a11y_aria_attributes(attribute, node.name);
|
||||
}
|
||||
|
||||
const type = name.slice(5);
|
||||
if (!aria_attributes.includes(type)) {
|
||||
const match = fuzzymatch(type, aria_attributes);
|
||||
w.a11y_unknown_aria_attribute(attribute, type, match);
|
||||
}
|
||||
|
||||
if (name === 'aria-hidden' && regex_heading_tags.test(node.name)) {
|
||||
w.a11y_hidden(attribute, node.name);
|
||||
}
|
||||
|
||||
// aria-proptypes
|
||||
let value = get_static_value(attribute);
|
||||
|
||||
const schema = aria.get(/** @type {ARIAProperty} */ (name));
|
||||
if (schema !== undefined) {
|
||||
validate_aria_attribute_value(attribute, /** @type {ARIAProperty} */ (name), schema, value);
|
||||
}
|
||||
|
||||
// aria-activedescendant-has-tabindex
|
||||
if (
|
||||
name === 'aria-activedescendant' &&
|
||||
!is_dynamic_element &&
|
||||
!is_interactive &&
|
||||
!attribute_map.has('tabindex') &&
|
||||
!has_spread
|
||||
) {
|
||||
w.a11y_aria_activedescendant_has_tabindex(attribute);
|
||||
}
|
||||
}
|
||||
|
||||
switch (name) {
|
||||
// aria-role
|
||||
case 'role': {
|
||||
if (invisible_elements.includes(node.name)) {
|
||||
// aria-unsupported-elements
|
||||
w.a11y_misplaced_role(attribute, node.name);
|
||||
}
|
||||
|
||||
const value = get_static_value(attribute);
|
||||
if (typeof value !== 'string') {
|
||||
break;
|
||||
}
|
||||
for (const c_r of value.split(regex_whitespaces)) {
|
||||
const current_role = /** @type {ARIARoleDefinitionKey} current_role */ (c_r);
|
||||
|
||||
if (current_role && is_abstract_role(current_role)) {
|
||||
w.a11y_no_abstract_role(attribute, current_role);
|
||||
} else if (current_role && !aria_roles.includes(current_role)) {
|
||||
const match = fuzzymatch(current_role, aria_roles);
|
||||
w.a11y_unknown_role(attribute, current_role, match);
|
||||
}
|
||||
|
||||
// no-redundant-roles
|
||||
if (
|
||||
current_role === get_implicit_role(node.name, attribute_map) &&
|
||||
// <ul role="list"> is ok because CSS list-style:none removes the semantics and this is a way to bring them back
|
||||
!['ul', 'ol', 'li', 'menu'].includes(node.name) &&
|
||||
// <a role="link" /> is ok because without href the a tag doesn't have a role of link
|
||||
!(node.name === 'a' && !attribute_map.has('href'))
|
||||
) {
|
||||
w.a11y_no_redundant_roles(attribute, current_role);
|
||||
}
|
||||
|
||||
// Footers and headers are special cases, and should not have redundant roles unless they are the children of sections or articles.
|
||||
const is_parent_section_or_article = is_parent(context.path, ['section', 'article']);
|
||||
if (!is_parent_section_or_article) {
|
||||
const has_nested_redundant_role =
|
||||
current_role === a11y_nested_implicit_semantics.get(node.name);
|
||||
if (has_nested_redundant_role) {
|
||||
w.a11y_no_redundant_roles(attribute, current_role);
|
||||
}
|
||||
}
|
||||
|
||||
// role-has-required-aria-props
|
||||
if (
|
||||
!is_dynamic_element &&
|
||||
!is_semantic_role_element(current_role, node.name, attribute_map)
|
||||
) {
|
||||
const role = roles_map.get(current_role);
|
||||
if (role) {
|
||||
const required_role_props = Object.keys(role.requiredProps);
|
||||
const has_missing_props =
|
||||
!has_spread &&
|
||||
required_role_props.some((prop) => !attributes.find((a) => a.name === prop));
|
||||
if (has_missing_props) {
|
||||
w.a11y_role_has_required_aria_props(
|
||||
attribute,
|
||||
current_role,
|
||||
list(
|
||||
required_role_props.map((v) => `"${v}"`),
|
||||
'and'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// interactive-supports-focus
|
||||
if (
|
||||
!has_spread &&
|
||||
!has_disabled_attribute(attribute_map) &&
|
||||
!is_hidden_from_screen_reader(node.name, attribute_map) &&
|
||||
!is_presentation_role(current_role) &&
|
||||
is_interactive_roles(current_role) &&
|
||||
is_static &&
|
||||
!attribute_map.get('tabindex')
|
||||
) {
|
||||
const has_interactive_handlers = [...handlers].some((handler) =>
|
||||
a11y_interactive_handlers.includes(handler)
|
||||
);
|
||||
if (has_interactive_handlers) {
|
||||
w.a11y_interactive_supports_focus(node, current_role);
|
||||
}
|
||||
}
|
||||
|
||||
// no-interactive-element-to-noninteractive-role
|
||||
if (
|
||||
!has_spread &&
|
||||
is_interactive &&
|
||||
(is_non_interactive_roles(current_role) || is_presentation_role(current_role))
|
||||
) {
|
||||
w.a11y_no_interactive_element_to_noninteractive_role(node, node.name, current_role);
|
||||
}
|
||||
|
||||
// no-noninteractive-element-to-interactive-role
|
||||
if (
|
||||
!has_spread &&
|
||||
is_non_interactive &&
|
||||
is_interactive_roles(current_role) &&
|
||||
!a11y_non_interactive_element_to_interactive_role_exceptions[node.name]?.includes(
|
||||
current_role
|
||||
)
|
||||
) {
|
||||
w.a11y_no_noninteractive_element_to_interactive_role(node, node.name, current_role);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
// no-access-key
|
||||
case 'accesskey': {
|
||||
w.a11y_accesskey(attribute);
|
||||
break;
|
||||
}
|
||||
// no-autofocus
|
||||
case 'autofocus': {
|
||||
if (node.name !== 'dialog' && !is_parent(context.path, ['dialog'])) {
|
||||
w.a11y_autofocus(attribute);
|
||||
}
|
||||
break;
|
||||
}
|
||||
// scope
|
||||
case 'scope': {
|
||||
if (!is_dynamic_element && node.name !== 'th') {
|
||||
w.a11y_misplaced_scope(attribute);
|
||||
}
|
||||
break;
|
||||
}
|
||||
// tabindex-no-positive
|
||||
case 'tabindex': {
|
||||
const value = get_static_value(attribute);
|
||||
// @ts-ignore todo is tabindex=true correct case?
|
||||
if (!isNaN(value) && +value > 0) {
|
||||
w.a11y_positive_tabindex(attribute);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const role = attribute_map.get('role');
|
||||
const role_static_value = /** @type {ARIARoleDefinitionKey} */ (get_static_text_value(role));
|
||||
|
||||
// click-events-have-key-events
|
||||
if (handlers.has('click')) {
|
||||
const is_non_presentation_role =
|
||||
role_static_value !== null && !is_presentation_role(role_static_value);
|
||||
if (
|
||||
!is_dynamic_element &&
|
||||
!is_hidden_from_screen_reader(node.name, attribute_map) &&
|
||||
(!role || is_non_presentation_role) &&
|
||||
!is_interactive &&
|
||||
!has_spread
|
||||
) {
|
||||
const has_key_event =
|
||||
handlers.has('keydown') || handlers.has('keyup') || handlers.has('keypress');
|
||||
if (!has_key_event) {
|
||||
w.a11y_click_events_have_key_events(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const role_value = /** @type {ARIARoleDefinitionKey} */ (
|
||||
role ? role_static_value : get_implicit_role(node.name, attribute_map)
|
||||
);
|
||||
|
||||
// no-noninteractive-tabindex
|
||||
if (!is_dynamic_element && !is_interactive && !is_interactive_roles(role_static_value)) {
|
||||
const tab_index = attribute_map.get('tabindex');
|
||||
const tab_index_value = get_static_text_value(tab_index);
|
||||
if (tab_index && (tab_index_value === null || Number(tab_index_value) >= 0)) {
|
||||
w.a11y_no_noninteractive_tabindex(node);
|
||||
}
|
||||
}
|
||||
|
||||
// role-supports-aria-props
|
||||
if (typeof role_value === 'string' && roles_map.has(role_value)) {
|
||||
const { props } = /** @type {ARIARoleDefinition} */ (roles_map.get(role_value));
|
||||
const invalid_aria_props = aria.keys().filter((attribute) => !(attribute in props));
|
||||
const is_implicit = role_value && role === undefined;
|
||||
for (const attr of attributes) {
|
||||
if (invalid_aria_props.includes(/** @type {ARIAProperty} */ (attr.name))) {
|
||||
if (is_implicit) {
|
||||
w.a11y_role_supports_aria_props_implicit(attr, attr.name, role_value, node.name);
|
||||
} else {
|
||||
w.a11y_role_supports_aria_props(attr, attr.name, role_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// no-noninteractive-element-interactions
|
||||
if (
|
||||
!has_spread &&
|
||||
!has_contenteditable_attr &&
|
||||
!is_hidden_from_screen_reader(node.name, attribute_map) &&
|
||||
!is_presentation_role(role_static_value) &&
|
||||
((!is_interactive && is_non_interactive_roles(role_static_value)) ||
|
||||
(is_non_interactive && !role))
|
||||
) {
|
||||
const has_interactive_handlers = [...handlers].some((handler) =>
|
||||
a11y_recommended_interactive_handlers.includes(handler)
|
||||
);
|
||||
if (has_interactive_handlers) {
|
||||
w.a11y_no_noninteractive_element_interactions(node, node.name);
|
||||
}
|
||||
}
|
||||
|
||||
// no-static-element-interactions
|
||||
if (
|
||||
!has_spread &&
|
||||
(!role || role_static_value !== null) &&
|
||||
!is_hidden_from_screen_reader(node.name, attribute_map) &&
|
||||
!is_presentation_role(role_static_value) &&
|
||||
!is_interactive &&
|
||||
!is_interactive_roles(role_static_value) &&
|
||||
!is_non_interactive &&
|
||||
!is_non_interactive_roles(role_static_value) &&
|
||||
!is_abstract_role(role_static_value)
|
||||
) {
|
||||
const interactive_handlers = [...handlers].filter((handler) =>
|
||||
a11y_interactive_handlers.includes(handler)
|
||||
);
|
||||
if (interactive_handlers.length > 0) {
|
||||
w.a11y_no_static_element_interactions(node, node.name, list(interactive_handlers));
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_spread && handlers.has('mouseover') && !handlers.has('focus')) {
|
||||
w.a11y_mouse_events_have_key_events(node, 'mouseover', 'focus');
|
||||
}
|
||||
|
||||
if (!has_spread && handlers.has('mouseout') && !handlers.has('blur')) {
|
||||
w.a11y_mouse_events_have_key_events(node, 'mouseout', 'blur');
|
||||
}
|
||||
|
||||
// element-specific checks
|
||||
const is_labelled =
|
||||
attribute_map.has('aria-label') ||
|
||||
attribute_map.has('aria-labelledby') ||
|
||||
attribute_map.has('title');
|
||||
|
||||
switch (node.name) {
|
||||
case 'a':
|
||||
case 'button': {
|
||||
const is_hidden =
|
||||
get_static_value(attribute_map.get('aria-hidden')) === 'true' ||
|
||||
get_static_value(attribute_map.get('inert')) !== null;
|
||||
|
||||
if (!has_spread && !is_hidden && !is_labelled && !has_content(node)) {
|
||||
w.a11y_consider_explicit_label(node);
|
||||
}
|
||||
if (node.name === 'button') {
|
||||
break;
|
||||
}
|
||||
const href = attribute_map.get('href') || attribute_map.get('xlink:href');
|
||||
if (href) {
|
||||
const href_value = get_static_text_value(href);
|
||||
if (href_value !== null) {
|
||||
if (href_value === '' || href_value === '#' || regex_js_prefix.test(href_value)) {
|
||||
w.a11y_invalid_attribute(href, href_value, href.name);
|
||||
}
|
||||
}
|
||||
} else if (!has_spread) {
|
||||
const id_attribute = get_static_value(attribute_map.get('id'));
|
||||
const name_attribute = get_static_value(attribute_map.get('name'));
|
||||
const aria_disabled_attribute = get_static_value(attribute_map.get('aria-disabled'));
|
||||
if (!id_attribute && !name_attribute && aria_disabled_attribute !== 'true') {
|
||||
warn_missing_attribute(node, ['href']);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'input': {
|
||||
const type = attribute_map.get('type');
|
||||
const type_value = get_static_text_value(type);
|
||||
if (type_value === 'image' && !has_spread) {
|
||||
const required_attributes = ['alt', 'aria-label', 'aria-labelledby'];
|
||||
const has_attribute = required_attributes.some((name) => attribute_map.has(name));
|
||||
if (!has_attribute) {
|
||||
warn_missing_attribute(node, required_attributes, 'input type="image"');
|
||||
}
|
||||
}
|
||||
// autocomplete-valid
|
||||
const autocomplete = attribute_map.get('autocomplete');
|
||||
if (type && autocomplete) {
|
||||
const autocomplete_value = get_static_value(autocomplete);
|
||||
if (!is_valid_autocomplete(autocomplete_value)) {
|
||||
w.a11y_autocomplete_valid(
|
||||
autocomplete,
|
||||
/** @type {string} */ (autocomplete_value),
|
||||
type_value ?? '...'
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'img': {
|
||||
const alt_attribute = get_static_text_value(attribute_map.get('alt'));
|
||||
const aria_hidden = get_static_value(attribute_map.get('aria-hidden'));
|
||||
if (alt_attribute && !aria_hidden && !has_spread) {
|
||||
if (regex_redundant_img_alt.test(alt_attribute)) {
|
||||
w.a11y_img_redundant_alt(node);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'label': {
|
||||
/** @param {AST.TemplateNode} node */
|
||||
const has_input_child = (node) => {
|
||||
let has = false;
|
||||
walk(
|
||||
node,
|
||||
{},
|
||||
{
|
||||
_(node, { next }) {
|
||||
if (
|
||||
node.type === 'SvelteElement' ||
|
||||
node.type === 'SlotElement' ||
|
||||
node.type === 'Component' ||
|
||||
node.type === 'RenderTag' ||
|
||||
(node.type === 'RegularElement' &&
|
||||
(a11y_labelable.includes(node.name) || node.name === 'slot'))
|
||||
) {
|
||||
has = true;
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
return has;
|
||||
};
|
||||
if (!has_spread && !attribute_map.has('for') && !has_input_child(node)) {
|
||||
w.a11y_label_has_associated_control(node);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'video': {
|
||||
const aria_hidden_attribute = attribute_map.get('aria-hidden');
|
||||
const aria_hidden_exist = aria_hidden_attribute && get_static_value(aria_hidden_attribute);
|
||||
|
||||
if (attribute_map.has('muted') || aria_hidden_exist === 'true' || has_spread) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!attribute_map.has('src')) {
|
||||
// don't warn about missing captions if `<video>` has no `src` —
|
||||
// could e.g. be playing a MediaStream
|
||||
return;
|
||||
}
|
||||
|
||||
let has_caption = false;
|
||||
const track = /** @type {AST.RegularElement | undefined} */ (
|
||||
node.fragment.nodes.find((i) => i.type === 'RegularElement' && i.name === 'track')
|
||||
);
|
||||
if (track) {
|
||||
has_caption = track.attributes.some(
|
||||
(a) =>
|
||||
a.type === 'SpreadAttribute' ||
|
||||
(a.type === 'Attribute' && a.name === 'kind' && get_static_value(a) === 'captions')
|
||||
);
|
||||
}
|
||||
if (!has_caption) {
|
||||
w.a11y_media_has_caption(node);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'figcaption': {
|
||||
if (!is_parent(context.path, ['figure'])) {
|
||||
w.a11y_figcaption_parent(node);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'figure': {
|
||||
const children = node.fragment.nodes.filter((node) => {
|
||||
if (node.type === 'Comment') return false;
|
||||
if (node.type === 'Text') return regex_not_whitespace.test(node.data);
|
||||
return true;
|
||||
});
|
||||
const index = children.findIndex(
|
||||
(child) => child.type === 'RegularElement' && child.name === 'figcaption'
|
||||
);
|
||||
if (index !== -1 && index !== 0 && index !== children.length - 1) {
|
||||
w.a11y_figcaption_index(children[index]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_spread && node.name !== 'a') {
|
||||
const required_attributes = a11y_required_attributes[node.name];
|
||||
if (required_attributes) {
|
||||
const has_attribute = required_attributes.some((name) => attribute_map.has(name));
|
||||
if (!has_attribute) {
|
||||
warn_missing_attribute(node, required_attributes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (a11y_distracting_elements.includes(node.name)) {
|
||||
// no-distracting-elements
|
||||
w.a11y_distracting_elements(node, node.name);
|
||||
}
|
||||
|
||||
// Check content
|
||||
if (
|
||||
!has_spread &&
|
||||
!is_labelled &&
|
||||
!has_contenteditable_binding &&
|
||||
a11y_required_content.includes(node.name) &&
|
||||
!has_content(node)
|
||||
) {
|
||||
w.a11y_missing_content(node, node.name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ARIARoleDefinitionKey} role
|
||||
*/
|
||||
function is_presentation_role(role) {
|
||||
return presentation_roles.includes(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} tag_name
|
||||
* @param {Map<string, AST.Attribute>} attribute_map
|
||||
*/
|
||||
function is_hidden_from_screen_reader(tag_name, attribute_map) {
|
||||
if (tag_name === 'input') {
|
||||
const type = get_static_value(attribute_map.get('type'));
|
||||
if (type === 'hidden') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const aria_hidden = attribute_map.get('aria-hidden');
|
||||
if (!aria_hidden) return false;
|
||||
const aria_hidden_value = get_static_value(aria_hidden);
|
||||
if (aria_hidden_value === null) return true;
|
||||
return aria_hidden_value === true || aria_hidden_value === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Map<string, AST.Attribute>} attribute_map
|
||||
*/
|
||||
function has_disabled_attribute(attribute_map) {
|
||||
const disabled_attr_value = get_static_value(attribute_map.get('disabled'));
|
||||
if (disabled_attr_value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const aria_disabled_attr = attribute_map.get('aria-disabled');
|
||||
if (aria_disabled_attr) {
|
||||
const aria_disabled_attr_value = get_static_value(aria_disabled_attr);
|
||||
if (aria_disabled_attr_value === 'true') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} tag_name
|
||||
* @param {Map<string, AST.Attribute>} attribute_map
|
||||
* @returns {typeof ElementInteractivity[keyof typeof ElementInteractivity]}
|
||||
*/
|
||||
function element_interactivity(tag_name, attribute_map) {
|
||||
if (
|
||||
interactive_element_role_schemas.some((schema) => match_schema(schema, tag_name, attribute_map))
|
||||
) {
|
||||
return ElementInteractivity.Interactive;
|
||||
}
|
||||
if (
|
||||
tag_name !== 'header' &&
|
||||
non_interactive_element_role_schemas.some((schema) =>
|
||||
match_schema(schema, tag_name, attribute_map)
|
||||
)
|
||||
) {
|
||||
return ElementInteractivity.NonInteractive;
|
||||
}
|
||||
if (
|
||||
interactive_element_ax_object_schemas.some((schema) =>
|
||||
match_schema(schema, tag_name, attribute_map)
|
||||
)
|
||||
) {
|
||||
return ElementInteractivity.Interactive;
|
||||
}
|
||||
if (
|
||||
non_interactive_element_ax_object_schemas.some((schema) =>
|
||||
match_schema(schema, tag_name, attribute_map)
|
||||
)
|
||||
) {
|
||||
return ElementInteractivity.NonInteractive;
|
||||
}
|
||||
return ElementInteractivity.Static;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ARIARoleDefinitionKey} role
|
||||
* @param {string} tag_name
|
||||
* @param {Map<string, AST.Attribute>} attribute_map
|
||||
*/
|
||||
function is_semantic_role_element(role, tag_name, attribute_map) {
|
||||
for (const [schema, ax_object] of elementAXObjects.entries()) {
|
||||
if (
|
||||
schema.name === tag_name &&
|
||||
(!schema.attributes ||
|
||||
schema.attributes.every(
|
||||
/** @param {any} attr */
|
||||
(attr) =>
|
||||
attribute_map.has(attr.name) &&
|
||||
get_static_value(attribute_map.get(attr.name)) === attr.value
|
||||
))
|
||||
) {
|
||||
for (const name of ax_object) {
|
||||
const roles = AXObjectRoles.get(name);
|
||||
if (roles) {
|
||||
for (const { name } of roles) {
|
||||
if (name === role) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {null | true | string} autocomplete
|
||||
*/
|
||||
function is_valid_autocomplete(autocomplete) {
|
||||
if (autocomplete === true) {
|
||||
return false;
|
||||
} else if (!autocomplete) {
|
||||
return true; // dynamic value
|
||||
}
|
||||
const tokens = autocomplete.trim().toLowerCase().split(regex_whitespaces);
|
||||
if (typeof tokens[0] === 'string' && tokens[0].startsWith('section-')) {
|
||||
tokens.shift();
|
||||
}
|
||||
if (address_type_tokens.includes(tokens[0])) {
|
||||
tokens.shift();
|
||||
}
|
||||
if (autofill_field_name_tokens.includes(tokens[0])) {
|
||||
tokens.shift();
|
||||
} else {
|
||||
if (contact_type_tokens.includes(tokens[0])) {
|
||||
tokens.shift();
|
||||
}
|
||||
if (autofill_contact_field_name_tokens.includes(tokens[0])) {
|
||||
tokens.shift();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (tokens[0] === 'webauthn') {
|
||||
tokens.shift();
|
||||
}
|
||||
return tokens.length === 0;
|
||||
}
|
||||
|
||||
/** @param {Map<string, AST.Attribute>} attribute_map */
|
||||
function input_implicit_role(attribute_map) {
|
||||
const type_attribute = attribute_map.get('type');
|
||||
if (!type_attribute) return;
|
||||
const type = get_static_text_value(type_attribute);
|
||||
if (!type) return;
|
||||
const list_attribute_exists = attribute_map.has('list');
|
||||
if (list_attribute_exists && combobox_if_list.includes(type)) {
|
||||
return 'combobox';
|
||||
}
|
||||
return input_type_to_implicit_role.get(type);
|
||||
}
|
||||
|
||||
/** @param {Map<string, AST.Attribute>} attribute_map */
|
||||
function menuitem_implicit_role(attribute_map) {
|
||||
const type_attribute = attribute_map.get('type');
|
||||
if (!type_attribute) return;
|
||||
const type = get_static_text_value(type_attribute);
|
||||
if (!type) return;
|
||||
return menuitem_type_to_implicit_role.get(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {Map<string, AST.Attribute>} attribute_map
|
||||
*/
|
||||
function get_implicit_role(name, attribute_map) {
|
||||
if (name === 'menuitem') {
|
||||
return menuitem_implicit_role(attribute_map);
|
||||
} else if (name === 'input') {
|
||||
return input_implicit_role(attribute_map);
|
||||
} else {
|
||||
return a11y_implicit_semantics.get(name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ARIARoleDefinitionKey} role
|
||||
*/
|
||||
function is_non_interactive_roles(role) {
|
||||
return non_interactive_roles.includes(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ARIARoleDefinitionKey} role
|
||||
*/
|
||||
function is_interactive_roles(role) {
|
||||
return interactive_roles.includes(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ARIARoleDefinitionKey} role
|
||||
*/
|
||||
function is_abstract_role(role) {
|
||||
return abstract_roles.includes(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AST.Attribute | undefined} attribute
|
||||
*/
|
||||
function get_static_text_value(attribute) {
|
||||
const value = get_static_value(attribute);
|
||||
if (value === true) return null;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AST.Attribute | undefined} attribute
|
||||
*/
|
||||
function get_static_value(attribute) {
|
||||
if (!attribute) return null;
|
||||
if (attribute.value === true) return true;
|
||||
if (is_text_attribute(attribute)) return attribute.value[0].data;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AST.RegularElement | AST.SvelteElement} element
|
||||
*/
|
||||
function has_content(element) {
|
||||
for (const node of element.fragment.nodes) {
|
||||
if (node.type === 'Text') {
|
||||
if (node.data.trim() === '') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type === 'RegularElement' || node.type === 'SvelteElement') {
|
||||
if (node.attributes.some((a) => a.type === 'Attribute' && a.name === 'popover')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
node.name === 'img' &&
|
||||
node.attributes.some((node) => node.type === 'Attribute' && node.name === 'alt')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (node.name === 'selectedcontent') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!has_content(node)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// assume everything else has content — this will result in false positives
|
||||
// (e.g. an empty `{#if ...}{/if}`) but that's probably fine
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ARIARoleRelationConcept} schema
|
||||
* @param {string} tag_name
|
||||
* @param {Map<string, AST.Attribute>} attribute_map
|
||||
*/
|
||||
function match_schema(schema, tag_name, attribute_map) {
|
||||
if (schema.name !== tag_name) return false;
|
||||
if (!schema.attributes) return true;
|
||||
return schema.attributes.every((schema_attribute) => {
|
||||
const attribute = attribute_map.get(schema_attribute.name);
|
||||
if (!attribute) return false;
|
||||
if (schema_attribute.value && schema_attribute.value !== get_static_text_value(attribute)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AST.SvelteNode[]} path
|
||||
* @param {string[]} elements
|
||||
*/
|
||||
function is_parent(path, elements) {
|
||||
let i = path.length;
|
||||
while (i--) {
|
||||
const parent = path[i];
|
||||
if (parent.type === 'SvelteElement') return true; // unknown, play it safe, so we don't warn
|
||||
if (parent.type === 'RegularElement') {
|
||||
return elements.includes(parent.name);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AST.Attribute} attribute
|
||||
* @param {ARIAProperty} name
|
||||
* @param {ARIAPropertyDefinition} schema
|
||||
* @param {string | true | null} value
|
||||
*/
|
||||
function validate_aria_attribute_value(attribute, name, schema, value) {
|
||||
const type = schema.type;
|
||||
|
||||
if (value === null) return;
|
||||
if (value === true) value = '';
|
||||
|
||||
switch (type) {
|
||||
case 'id':
|
||||
case 'string': {
|
||||
if (value === '') {
|
||||
w.a11y_incorrect_aria_attribute_type(attribute, name, 'non-empty string');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'number': {
|
||||
if (value === '' || isNaN(+value)) {
|
||||
w.a11y_incorrect_aria_attribute_type(attribute, name, 'number');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'boolean': {
|
||||
if (value !== 'true' && value !== 'false') {
|
||||
w.a11y_incorrect_aria_attribute_type_boolean(attribute, name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'idlist': {
|
||||
if (value === '') {
|
||||
w.a11y_incorrect_aria_attribute_type_idlist(attribute, name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'integer': {
|
||||
if (value === '' || !Number.isInteger(+value)) {
|
||||
w.a11y_incorrect_aria_attribute_type_integer(attribute, name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'token': {
|
||||
const values = (schema.values ?? []).map((value) => value.toString());
|
||||
if (!values.includes(value.toLowerCase())) {
|
||||
w.a11y_incorrect_aria_attribute_type_token(
|
||||
attribute,
|
||||
name,
|
||||
list(values.map((v) => `"${v}"`))
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'tokenlist': {
|
||||
const values = (schema.values ?? []).map((value) => value.toString());
|
||||
if (
|
||||
value
|
||||
.toLowerCase()
|
||||
.split(regex_whitespaces)
|
||||
.some((value) => !values.includes(value))
|
||||
) {
|
||||
w.a11y_incorrect_aria_attribute_type_tokenlist(
|
||||
attribute,
|
||||
name,
|
||||
list(values.map((v) => `"${v}"`))
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'tristate': {
|
||||
if (value !== 'true' && value !== 'false' && value !== 'mixed') {
|
||||
w.a11y_incorrect_aria_attribute_type_tristate(attribute, name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AST.RegularElement |AST.SvelteElement} node
|
||||
* @param {string[]} attributes
|
||||
* @param {string} name
|
||||
*/
|
||||
function warn_missing_attribute(node, attributes, name = node.name) {
|
||||
const article =
|
||||
regex_starts_with_vowel.test(attributes[0]) || attributes[0] === 'href' ? 'an' : 'a';
|
||||
const sequence =
|
||||
attributes.length > 1
|
||||
? attributes.slice(0, -1).join(', ') + ` or ${attributes[attributes.length - 1]}`
|
||||
: attributes[0];
|
||||
|
||||
w.a11y_missing_attribute(node, name, article, sequence);
|
||||
}
|
||||
Generated
Vendored
+125
@@ -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);
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+177
@@ -0,0 +1,177 @@
|
||||
/** @import { Expression } from 'estree' */
|
||||
/** @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' &&
|
||||
attribute.type !== 'AttachTag'
|
||||
) {
|
||||
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)) {
|
||||
disallow_unparenthesized_sequences(
|
||||
get_attribute_expression(attribute),
|
||||
context.state.analysis.source
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 (attribute.type === 'AttachTag') {
|
||||
disallow_unparenthesized_sequences(attribute.expression, context.state.analysis.source);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = [];
|
||||
}
|
||||
|
||||
/** @type {Set<string>} */
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Expression} expression
|
||||
* @param {string} source
|
||||
*/
|
||||
function disallow_unparenthesized_sequences(expression, source) {
|
||||
if (expression.type === 'SequenceExpression') {
|
||||
let i = /** @type {number} */ (expression.start);
|
||||
while (--i > 0) {
|
||||
const char = source[i];
|
||||
if (char === '(') break; // parenthesized sequence expressions are ok
|
||||
if (char === '{') e.attribute_invalid_sequence_expression(expression);
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+160
@@ -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']
|
||||
]);
|
||||
Generated
Vendored
+15
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
/** @import { ArrowFunctionExpression, FunctionDeclaration, FunctionExpression } from 'estree' */
|
||||
/** @import { Context } from '../../types' */
|
||||
|
||||
/**
|
||||
* @param {ArrowFunctionExpression | FunctionExpression | FunctionDeclaration} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function visit_function(node, context) {
|
||||
if (context.state.expression) {
|
||||
for (const [name] of context.state.scope.references) {
|
||||
const binding = context.state.scope.get(name);
|
||||
|
||||
if (binding && binding.scope !== context.state.scope) {
|
||||
context.state.expression.references.add(binding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.next({
|
||||
...context.state,
|
||||
function_depth: context.state.function_depth + 1,
|
||||
expression: null
|
||||
});
|
||||
}
|
||||
Generated
Vendored
+17
@@ -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'
|
||||
);
|
||||
}
|
||||
Generated
Vendored
+16
@@ -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);
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+300
@@ -0,0 +1,300 @@
|
||||
/** @import { AssignmentExpression, Expression, Literal, Node, Pattern, 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, get_parent } from '../../../../utils/ast.js';
|
||||
import * as w from '../../../../warnings.js';
|
||||
import * as b from '#compiler/builders';
|
||||
import { get_rune } from '../../../scope.js';
|
||||
import { get_name } from '../../../nodes.js';
|
||||
|
||||
/**
|
||||
* @param {AssignmentExpression | UpdateExpression | AST.BindDirective} node
|
||||
* @param {Pattern | Expression} argument
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function validate_assignment(node, argument, context) {
|
||||
validate_no_const_assignment(node, argument, context.state.scope, node.type === 'BindDirective');
|
||||
|
||||
if (argument.type === 'Identifier') {
|
||||
const binding = context.state.scope.get(argument.name);
|
||||
|
||||
if (context.state.analysis.runes) {
|
||||
if (
|
||||
context.state.analysis.props_id != null &&
|
||||
binding?.node === context.state.analysis.props_id
|
||||
) {
|
||||
e.constant_assignment(node, '$props.id()');
|
||||
}
|
||||
|
||||
if (binding?.kind === 'each') {
|
||||
e.each_item_invalid_assignment(node);
|
||||
}
|
||||
}
|
||||
|
||||
if (binding?.kind === 'snippet') {
|
||||
e.snippet_parameter_assignment(node);
|
||||
}
|
||||
}
|
||||
|
||||
if (argument.type === 'MemberExpression' && argument.object.type === 'ThisExpression') {
|
||||
const name =
|
||||
argument.computed && argument.property.type !== 'Literal'
|
||||
? null
|
||||
: get_name(argument.property);
|
||||
|
||||
const field = name !== null && context.state.state_fields?.get(name);
|
||||
|
||||
// check we're not assigning to a state field before its declaration in the constructor
|
||||
if (field && field.node.type === 'AssignmentExpression' && node !== field.node) {
|
||||
let i = context.path.length;
|
||||
while (i--) {
|
||||
const parent = context.path[i];
|
||||
|
||||
if (
|
||||
parent.type === 'FunctionDeclaration' ||
|
||||
parent.type === 'FunctionExpression' ||
|
||||
parent.type === 'ArrowFunctionExpression'
|
||||
) {
|
||||
const grandparent = get_parent(context.path, i - 1);
|
||||
|
||||
if (
|
||||
grandparent.type === 'MethodDefinition' &&
|
||||
grandparent.kind === 'constructor' &&
|
||||
/** @type {number} */ (node.start) < /** @type {number} */ (field.node.start)
|
||||
) {
|
||||
e.state_field_invalid_assignment(node);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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?.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' : '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'
|
||||
) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+719
@@ -0,0 +1,719 @@
|
||||
/** @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 '#compiler/builders';
|
||||
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 { AwaitExpression } from './visitors/AwaitExpression.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 { ForOfStatement } from './visitors/ForOfStatement.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 { 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 { AttachTag } from './visitors/AttachTag.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,
|
||||
AwaitExpression,
|
||||
BinaryExpression,
|
||||
BindDirective,
|
||||
BlockStatement,
|
||||
BreakStatement,
|
||||
CallExpression,
|
||||
ClassBody,
|
||||
Comment,
|
||||
Component,
|
||||
ConstTag,
|
||||
DebugTag,
|
||||
EachBlock,
|
||||
ExportNamedDeclaration,
|
||||
ExpressionStatement,
|
||||
ForOfStatement,
|
||||
Fragment,
|
||||
FunctionDeclaration,
|
||||
FunctionExpression,
|
||||
HtmlTag,
|
||||
Identifier,
|
||||
IfBlock,
|
||||
KeyBlock,
|
||||
LabeledStatement,
|
||||
LetDirective,
|
||||
MemberExpression,
|
||||
OnDirective,
|
||||
Program,
|
||||
RegularElement,
|
||||
RenderTag,
|
||||
SlotElement,
|
||||
SnippetBlock,
|
||||
SpreadAttribute,
|
||||
SvelteBody,
|
||||
SvelteComponent,
|
||||
SvelteDocument,
|
||||
SvelteElement,
|
||||
SvelteFragment,
|
||||
SvelteBoundary,
|
||||
SvelteHead,
|
||||
SvelteSelf,
|
||||
SvelteWindow,
|
||||
TitleElement,
|
||||
TransitionDirective,
|
||||
UpdateExpression,
|
||||
UseDirective,
|
||||
AttachTag,
|
||||
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'), ...analysis.instance_body.hoisted],
|
||||
node: /** @type {any} */ (null), // populated by the root node
|
||||
legacy_reactive_imports: [],
|
||||
legacy_reactive_statements: new Map(),
|
||||
metadata: {
|
||||
namespace: options.namespace,
|
||||
bound_contenteditable: false
|
||||
},
|
||||
events: new Set(),
|
||||
preserve_whitespace: options.preserveWhitespace,
|
||||
state_fields: new Map(),
|
||||
transform: {},
|
||||
in_constructor: false,
|
||||
instance_level_snippets: [],
|
||||
module_level_snippets: [],
|
||||
is_standalone: false,
|
||||
|
||||
// these are set inside the `Fragment` visitor, and cannot be used until then
|
||||
init: /** @type {any} */ (null),
|
||||
consts: /** @type {any} */ (null),
|
||||
snippets: /** @type {any} */ (null),
|
||||
let_directives: /** @type {any} */ (null),
|
||||
update: /** @type {any} */ (null),
|
||||
after_update: /** @type {any} */ (null),
|
||||
template: /** @type {any} */ (null),
|
||||
memoizer: /** @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.Statement} */
|
||||
let store_init = b.empty;
|
||||
/** @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_source', undefined, analysis.immutable ? b.true : undefined)
|
||||
)
|
||||
);
|
||||
}
|
||||
if (binding.kind === 'store_sub') {
|
||||
if (store_setup.length === 0) {
|
||||
needs_store_cleanup = true;
|
||||
store_init = 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'))
|
||||
]);
|
||||
|
||||
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.unshift(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));
|
||||
|
||||
let component_block = b.block([
|
||||
store_init,
|
||||
...legacy_reactive_declarations,
|
||||
...group_binding_declarations
|
||||
]);
|
||||
|
||||
const should_inject_context =
|
||||
dev ||
|
||||
analysis.needs_context ||
|
||||
analysis.reactive_statements.size > 0 ||
|
||||
component_returned_object.length > 0;
|
||||
|
||||
component_block.body.push(
|
||||
...state.instance_level_snippets,
|
||||
.../** @type {ESTree.Statement[]} */ (instance.body)
|
||||
);
|
||||
|
||||
if (should_inject_context && component_returned_object.length > 0) {
|
||||
component_block.body.push(b.var('$$exports', b.object(component_returned_object)));
|
||||
}
|
||||
component_block.body.unshift(...store_setup);
|
||||
|
||||
if (!analysis.runes && analysis.needs_context) {
|
||||
component_block.body.push(b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)));
|
||||
}
|
||||
|
||||
component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body));
|
||||
|
||||
if (analysis.needs_mutation_validation) {
|
||||
component_block.body.unshift(
|
||||
b.var('$$ownership_validator', b.call('$.create_ownership_validator', 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;
|
||||
|
||||
// trick esrap into including comments
|
||||
component_block.loc = instance.loc;
|
||||
|
||||
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')))
|
||||
);
|
||||
}
|
||||
|
||||
// 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.id('$$exports'));
|
||||
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'))));
|
||||
}
|
||||
|
||||
// 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 accept_fn_body = [
|
||||
b.stmt(b.call(b.member(b.member(id, b.id('$.HMR'), true), 'update'), b.id('module.default')))
|
||||
];
|
||||
|
||||
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.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))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (options.experimental.async) {
|
||||
body.unshift(b.imports([], 'svelte/internal/flags/async'));
|
||||
}
|
||||
|
||||
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 (analysis.props_id) {
|
||||
// need to be placed on first line of the component for hydration
|
||||
component_block.body.unshift(b.const(analysis.props_id, b.call('$.props_id')));
|
||||
}
|
||||
|
||||
if (state.events.size > 0) {
|
||||
body.push(
|
||||
b.stmt(b.call('$.delegate', b.array(Array.from(state.events).map((name) => b.literal(name)))))
|
||||
);
|
||||
}
|
||||
|
||||
const ce = analysis.custom_element;
|
||||
|
||||
if (ce) {
|
||||
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.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))
|
||||
);
|
||||
|
||||
/** @type {ESTree.ObjectExpression | undefined} */
|
||||
let shadow_root_init;
|
||||
if (typeof ce === 'boolean' || ce.shadow === 'open' || ce.shadow === undefined) {
|
||||
shadow_root_init = b.object([b.init('mode', b.literal('open'))]);
|
||||
} else if (ce.shadow === 'none') {
|
||||
shadow_root_init = undefined;
|
||||
} else {
|
||||
shadow_root_init = ce.shadow;
|
||||
}
|
||||
|
||||
const create_ce = b.call(
|
||||
'$.create_custom_element',
|
||||
b.id(analysis.name),
|
||||
b.object(props_str),
|
||||
slots_str,
|
||||
accessors_str,
|
||||
shadow_root_init,
|
||||
/** @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,
|
||||
state_fields: new Map(),
|
||||
transform: {},
|
||||
in_constructor: false,
|
||||
is_instance: 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]
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user