chore: TS to JSDoc Conversion (#8569)

---------

Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
pull/8608/head
S. Elliott Johnson 3 years ago committed by GitHub
parent 783bd9899e
commit fd9d61a7b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -4,7 +4,7 @@ _output
test/*/samples/*/output.js
# automatically generated
internal_exports.ts
internal_exports.js
# output files
animate/*.js

2
.gitignore vendored

@ -3,7 +3,7 @@
.vscode
node_modules
*.map
/src/compiler/compile/internal_exports.ts
/src/compiler/compile/internal_exports.js
/compiler.d.ts
/compiler.*js
/index.*js

@ -4,7 +4,7 @@
# TODO: after launch new site, format site dir.
/site
!/src
src/compiler/compile/internal_exports.ts
src/compiler/compile/internal_exports.js
!/test
/test/**/*.svelte
/test/**/_expected*

@ -0,0 +1,162 @@
// This script generates the TypeScript definitions
import { execSync } from 'child_process';
import { readFileSync, writeFileSync, readdirSync, existsSync, copyFileSync, statSync } from 'fs';
execSync('tsc -p src/compiler --emitDeclarationOnly && tsc -p src/runtime --emitDeclarationOnly', { stdio: 'inherit' });
function modify(path, modifyFn) {
const content = readFileSync(path, 'utf8');
writeFileSync(path, modifyFn(content));
}
function adjust(input) {
// Remove typedef jsdoc (duplicated in the type definition)
input = input.replace(/\/\*\*\n(\r)? \* @typedef .+?\*\//gs, '');
input = input.replace(/\/\*\* @typedef .+?\*\//gs, '');
// Extract the import paths and types
const import_regex = /import\(("|')(.+?)("|')\)\.(\w+)/g;
let import_match;
const import_map = new Map();
while ((import_match = import_regex.exec(input)) !== null) {
const imports = import_map.get(import_match[2]) || new Map();
let name = import_match[4];
if ([...imports.keys()].includes(name)) continue;
let i = 1;
if (name === 'default') {
name = import_match[2].split('/').pop().split('.').shift().replace(/[^a-z0-9]/gi, '_');
}
while ([...import_map].some(([path, names]) => path !== import_match[2] && names.has(name))) {
name = `${name}${i++}`;
}
imports.set(import_match[4], name);
import_map.set(import_match[2], imports);
}
// Replace inline imports with their type names
const transformed = input.replace(import_regex, (_match, _quote, path, _quote2, name) => {
return import_map.get(path).get(name);
});
// Remove/adjust @template, @param and @returns lines
// TODO rethink if we really need to do this for @param and @returns, doesn't show up in hover so unnecessary
const lines = transformed.split("\n");
let filtered_lines = [];
let removing = null;
let openCount = 1;
let closedCount = 0;
for (let line of lines) {
let start_removing = false;
if (line.trim().startsWith("* @template")) {
removing = "template";
start_removing = true;
}
if (line.trim().startsWith("* @param {")) {
openCount = 1;
closedCount = 0;
removing = "param";
start_removing = true;
}
if (line.trim().startsWith("* @returns {")) {
openCount = 1;
closedCount = 0;
removing = "returns";
start_removing = true;
}
if (removing === "returns" || removing === "param") {
let i = start_removing ? line.indexOf('{') + 1 : 0;
for (; i < line.length; i++) {
if (line[i] === "{") openCount++;
if (line[i] === "}") closedCount++;
if (openCount === closedCount) break;
}
if (openCount === closedCount) {
line = start_removing ? (line.slice(0, line.indexOf('{')) + line.slice(i + 1)) : (` * @${removing} ` + line.slice(i + 1));
removing = null;
}
}
if (removing && !start_removing && (line.trim().startsWith("* @") || line.trim().startsWith("*/"))) {
removing = null;
}
if (!removing) {
filtered_lines.push(line);
}
}
// Replace generic type names with their plain versions
const renamed_generics = filtered_lines.map(line => {
return line.replace(/(\W|\s)([A-Z][\w\d$]*)_\d+(\W|\s)/g, "$1$2$3");
});
// Generate the import statement for the types used
const import_statements = Array.from(import_map.entries())
.map(([path, names]) => {
const default_name = names.get('default');
names.delete('default');
const default_import = default_name ? (default_name + (names.size ? ', ' : ' ')) : '';
const named_imports = names.size ? `{ ${[...names.values()].join(', ')} } ` : '';
return `import ${default_import}${named_imports}from '${path}';`
})
.join("\n");
return [import_statements, ...renamed_generics].join("\n");
}
let did_replace = false;
function walk(dir) {
const files = readdirSync(dir);
const _dir = dir.slice('types/'.length)
for (const file of files) {
const path = `${dir}/${file}`;
if (file.endsWith('.d.ts')) {
modify(path, content => {
content = adjust(content);
if (file === 'index.d.ts' && existsSync(`src/${_dir}/public.d.ts`)) {
copyFileSync(`src/${_dir}/public.d.ts`, `${dir}/public.d.ts`);
content = "export * from './public.js';\n" + content;
}
if (file === 'Component.d.ts' && dir.includes('runtime')) {
if (!content.includes('$set(props: Partial<Props>): void;\n}')) {
throw new Error('Component.js was modified in a way that automatic patching of d.ts file no longer works. Please adjust it');
} else {
content = content.replace('$set(props: Partial<Props>): void;\n}', '$set(props: Partial<Props>): void;\n [accessor:string]: any;\n}');
did_replace = true;
}
}
return content;
});
} else if (statSync(path).isDirectory()) {
if (existsSync(`src/${_dir}/${file}/private.d.ts`)) {
copyFileSync(`src/${_dir}/${file}/private.d.ts`, `${path}/private.d.ts`);
}
if (existsSync(`src/${_dir}/${file}/interfaces.d.ts`)) {
copyFileSync(`src/${_dir}/${file}/interfaces.d.ts`, `${path}/interfaces.d.ts`);
}
walk(path);
}
}
}
walk('types');
if (!did_replace) {
throw new Error('Component.js file in runtime does no longer exist so that automatic patching of the d.ts file no longer works. Please adjust it');
}
copyFileSync(`src/runtime/ambient.d.ts`, `types/runtime/ambient.d.ts`);

@ -2,6 +2,7 @@
"name": "svelte",
"version": "4.0.0-next.0",
"description": "Cybernetically enhanced web apps",
"type": "module",
"module": "index.mjs",
"main": "index",
"files": [
@ -89,7 +90,7 @@
"dev": "rollup -cw",
"posttest": "agadoo internal/index.mjs",
"prepublishOnly": "node check_publish_env.js && npm run lint && npm run build && npm test",
"tsd": "tsc -p src/compiler --emitDeclarationOnly && tsc -p src/runtime --emitDeclarationOnly",
"tsd": "node ./generate-types.mjs",
"lint": "eslint \"{src,test}/**/*.{ts,js}\" --cache"
},
"repository": {
@ -147,6 +148,7 @@
"prettier": "^2.8.8",
"prettier-plugin-svelte": "^2.10.0",
"rollup": "^3.20.2",
"rollup-plugin-dts": "^5.3.0",
"source-map": "^0.7.4",
"source-map-support": "^0.5.21",
"tiny-glob": "^0.2.9",

@ -115,6 +115,9 @@ devDependencies:
rollup:
specifier: ^3.20.2
version: 3.20.2
rollup-plugin-dts:
specifier: ^5.3.0
version: 5.3.0(rollup@3.20.2)(typescript@5.0.4)
source-map:
specifier: ^0.7.4
version: 0.7.4
@ -147,6 +150,31 @@ packages:
'@jridgewell/trace-mapping': 0.3.18
dev: true
/@babel/code-frame@7.21.4:
resolution: {integrity: sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==}
engines: {node: '>=6.9.0'}
requiresBuild: true
dependencies:
'@babel/highlight': 7.18.6
dev: true
optional: true
/@babel/helper-validator-identifier@7.19.1:
resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==}
engines: {node: '>=6.9.0'}
dev: true
optional: true
/@babel/highlight@7.18.6:
resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/helper-validator-identifier': 7.19.1
chalk: 2.4.2
js-tokens: 4.0.0
dev: true
optional: true
/@esbuild/android-arm64@0.17.19:
resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==}
engines: {node: '>=12'}
@ -870,6 +898,14 @@ packages:
engines: {node: '>=8'}
dev: true
/ansi-styles@3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
engines: {node: '>=4'}
dependencies:
color-convert: 1.9.3
dev: true
optional: true
/ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
@ -1025,6 +1061,16 @@ packages:
type-detect: 4.0.8
dev: true
/chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
engines: {node: '>=4'}
dependencies:
ansi-styles: 3.2.1
escape-string-regexp: 1.0.5
supports-color: 5.5.0
dev: true
optional: true
/chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
@ -1047,6 +1093,13 @@ packages:
periscopic: 3.1.0
dev: true
/color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
dependencies:
color-name: 1.1.3
dev: true
optional: true
/color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@ -1054,6 +1107,11 @@ packages:
color-name: 1.1.4
dev: true
/color-name@1.1.3:
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
dev: true
optional: true
/color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
dev: true
@ -1355,6 +1413,12 @@ packages:
'@esbuild/win32-x64': 0.17.19
dev: true
/escape-string-regexp@1.0.5:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'}
dev: true
optional: true
/escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
@ -1879,6 +1943,12 @@ packages:
resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==}
dev: true
/has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'}
dev: true
optional: true
/has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
@ -2184,6 +2254,11 @@ packages:
engines: {node: '>= 0.8'}
dev: true
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
dev: true
optional: true
/js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true
@ -2705,6 +2780,20 @@ packages:
glob: 7.2.3
dev: true
/rollup-plugin-dts@5.3.0(rollup@3.20.2)(typescript@5.0.4):
resolution: {integrity: sha512-8FXp0ZkyZj1iU5klkIJYLjIq/YZSwBoERu33QBDxm/1yw5UU4txrEtcmMkrq+ZiKu3Q4qvPCNqc3ovX6rjqzbQ==}
engines: {node: '>=v14'}
peerDependencies:
rollup: ^3.0.0
typescript: ^4.1 || ^5.0
dependencies:
magic-string: 0.30.0
rollup: 3.20.2
typescript: 5.0.4
optionalDependencies:
'@babel/code-frame': 7.21.4
dev: true
/rollup@3.20.2:
resolution: {integrity: sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg==}
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
@ -2891,6 +2980,14 @@ packages:
ts-interface-checker: 0.1.13
dev: true
/supports-color@5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
dependencies:
has-flag: 3.0.0
dev: true
optional: true
/supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}

@ -32,7 +32,7 @@ const runtime_entrypoints = Object.fromEntries(
fs
.readdirSync('src/runtime', { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => [dirent.name, `src/runtime/${dirent.name}/index.ts`])
.map((dirent) => [dirent.name, `src/runtime/${dirent.name}/index.js`])
);
/**
@ -42,8 +42,8 @@ export default [
{
input: {
...runtime_entrypoints,
index: 'src/runtime/index.ts',
ssr: 'src/runtime/ssr.ts'
index: 'src/runtime/index.js',
ssr: 'src/runtime/ssr.js'
},
output: ['es', 'cjs'].map(
/** @returns {import('rollup').OutputOptions} */
@ -85,7 +85,7 @@ export default [
const mod = bundle[`internal/index.mjs`];
if (mod) {
fs.writeFileSync(
'src/compiler/compile/internal_exports.ts',
'src/compiler/compile/internal_exports.js',
`// This file is automatically generated\n` +
`export default new Set(${JSON.stringify(mod.exports)});`
);
@ -103,7 +103,7 @@ export default [
},
/* compiler.js */
{
input: 'src/compiler/index.ts',
input: 'src/compiler/index.js',
plugins: [
replace({
preventAssignment: true,

@ -9,7 +9,7 @@ see: https://github.com/microsoft/TypeScript/tree/main/lib
import http from 'https';
import fs from 'fs';
const GLOBAL_TS_PATH = './src/compiler/utils/globals.ts';
const GLOBAL_TS_PATH = './src/compiler/utils/globals.js';
// MEMO: add additional objects/functions which existed in `src/compiler/utils/names.ts`
// before this script was introduced but could not be retrieved by this process.

@ -6,39 +6,52 @@ const now =
}
: () => self.performance.now();
interface Timing {
label: string;
start: number;
end: number;
children: Timing[];
}
/** @param {any} timings */
function collapse_timings(timings) {
const result = {};
timings.forEach((timing) => {
result[timing.label] = Object.assign(
{
total: timing.end - timing.start
},
timing.children && collapse_timings(timing.children)
);
});
timings.forEach(
/** @param {any} timing */ (timing) => {
result[timing.label] = Object.assign(
{
total: timing.end - timing.start
},
timing.children && collapse_timings(timing.children)
);
}
);
return result;
}
export default class Stats {
start_time: number;
current_timing: Timing;
current_children: Timing[];
timings: Timing[];
stack: Timing[];
/**
* @typedef {Object} Timing
* @property {string} label
* @property {number} start
* @property {number} end
* @property {Timing[]} children
*/
/** @type {number} */
start_time;
/** @type {Timing} */
current_timing;
/** @type {Timing[]} */
current_children;
/** @type {Timing[]} */
timings;
/** @type {Timing[]} */
stack;
constructor() {
this.start_time = now();
this.stack = [];
this.current_children = this.timings = [];
}
/** @param {any} label */
start(label) {
const timing = {
label,
@ -46,27 +59,24 @@ export default class Stats {
end: null,
children: []
};
this.current_children.push(timing);
this.stack.push(timing);
this.current_timing = timing;
this.current_children = timing.children;
}
/** @param {any} label */
stop(label) {
if (label !== this.current_timing.label) {
throw new Error(
`Mismatched timing labels (expected ${this.current_timing.label}, got ${label})`
);
}
this.current_timing.end = now();
this.stack.pop();
this.current_timing = this.stack[this.stack.length - 1];
this.current_children = this.current_timing ? this.current_timing.children : this.timings;
}
render() {
const timings = Object.assign(
{
@ -74,7 +84,6 @@ export default class Stats {
},
collapse_timings(this.timings)
);
return {
timings
};

File diff suppressed because it is too large Load Diff

@ -1,36 +1,48 @@
// All compiler errors should be listed and accessed from here
/**
* @internal
*/
export default {
invalid_binding_elements: (element: string, binding: string) => ({
invalid_binding_elements: /**
* @param {string} element
* @param {string} binding
*/ (element, binding) => ({
code: 'invalid-binding',
message: `'${binding}' is not a valid binding on <${element}> elements`
}),
invalid_binding_element_with: (elements: string, binding: string) => ({
invalid_binding_element_with: /**
* @param {string} elements
* @param {string} binding
*/ (elements, binding) => ({
code: 'invalid-binding',
message: `'${binding}' binding can only be used with ${elements}`
}),
invalid_binding_on: (binding: string, element: string, post?: string) => ({
invalid_binding_on: /**
* @param {string} binding
* @param {string} element
* @param {string} [post]
*/ (binding, element, post) => ({
code: 'invalid-binding',
message: `'${binding}' is not a valid binding on ${element}` + (post || '')
}),
invalid_binding_foreign: (binding: string) => ({
invalid_binding_foreign: /** @param {string} binding */ (binding) => ({
code: 'invalid-binding',
message: `'${binding}' is not a valid binding. Foreign elements only support bind:this`
}),
invalid_binding_no_checkbox: (binding: string, is_radio: boolean) => ({
invalid_binding_no_checkbox: /**
* @param {string} binding
* @param {boolean} is_radio
*/ (binding, is_radio) => ({
code: 'invalid-binding',
message:
`'${binding}' binding can only be used with <input type="checkbox">` +
(is_radio ? ' — for <input type="radio">, use \'group\' binding' : '')
}),
invalid_binding: (binding: string) => ({
invalid_binding: /** @param {string} binding */ (binding) => ({
code: 'invalid-binding',
message: `'${binding}' is not a valid binding`
}),
invalid_binding_window: (parts: string[]) => ({
invalid_binding_window: /** @param {string[]} parts */ (parts) => ({
code: 'invalid-binding',
message: `Bindings on <svelte:window> must be to top-level properties, e.g. '${
parts[parts.length - 1]
@ -52,7 +64,7 @@ export default {
code: 'invalid-binding',
message: 'Cannot bind to a variable which is not writable'
},
binding_undeclared: (name: string) => ({
binding_undeclared: /** @param {string} name */ (name) => ({
code: 'binding-undeclared',
message: `${name} is not declared`
}),
@ -77,15 +89,18 @@ export default {
code: 'dynamic-contenteditable-attribute',
message: "'contenteditable' attribute cannot be dynamic if element uses two-way binding"
},
invalid_event_modifier_combination: (modifier1: string, modifier2: string) => ({
invalid_event_modifier_combination: /**
* @param {string} modifier1
* @param {string} modifier2
*/ (modifier1, modifier2) => ({
code: 'invalid-event-modifier',
message: `The '${modifier1}' and '${modifier2}' modifiers cannot be used together`
}),
invalid_event_modifier_legacy: (modifier: string) => ({
invalid_event_modifier_legacy: /** @param {string} modifier */ (modifier) => ({
code: 'invalid-event-modifier',
message: `The '${modifier}' modifier cannot be used in legacy mode`
}),
invalid_event_modifier: (valid: string) => ({
invalid_event_modifier: /** @param {string} valid */ (valid) => ({
code: 'invalid-event-modifier',
message: `Valid event modifiers are ${valid}`
}),
@ -98,7 +113,7 @@ export default {
message:
'A <textarea> can have either a value attribute or (equivalently) child content, but not both'
},
illegal_attribute: (name: string) => ({
illegal_attribute: /** @param {string} name */ (name) => ({
code: 'illegal-attribute',
message: `'${name}' is not a valid attribute name`
}),
@ -106,7 +121,7 @@ export default {
code: 'invalid-slot-attribute',
message: 'slot attribute cannot have a dynamic value'
},
duplicate_slot_attribute: (name: string) => ({
duplicate_slot_attribute: /** @param {string} name */ (name) => ({
code: 'duplicate-slot-attribute',
message: `Duplicate '${name}' slot`
}),
@ -163,8 +178,12 @@ export default {
code: 'illegal-structure',
message: '<title> can only contain text and {tags}'
},
duplicate_transition: (directive: string, parent_directive: string) => {
function describe(_directive: string) {
duplicate_transition: /**
* @param {string} directive
* @param {string} parent_directive
*/ (directive, parent_directive) => {
/** @param {string} _directive */
function describe(_directive) {
return _directive === 'transition' ? "a 'transition'" : `an '${_directive}'`;
}
const message =
@ -195,7 +214,7 @@ export default {
code: 'illegal-subscription',
message: 'Cannot reference store value inside <script context="module">'
},
illegal_global: (name: string) => ({
illegal_global: /** @param {string} name */ (name) => ({
code: 'illegal-global',
message: `${name} is an illegal variable name`
}),
@ -203,7 +222,7 @@ export default {
code: 'illegal-variable-declaration',
message: 'Cannot declare same variable name which is imported inside <script context="module">'
},
cyclical_reactive_declaration: (cycle: string[]) => ({
cyclical_reactive_declaration: /** @param {string[]} cycle */ (cycle) => ({
code: 'cyclical-reactive-declaration',
message: `Cyclical dependency detected: ${cycle.join(' → ')}`
}),
@ -231,7 +250,10 @@ export default {
"'props' must be a statically analyzable object literal of the form " +
"'{ [key: string]: { attribute?: string; reflect?: boolean; type?: 'String' | 'Boolean' | 'Number' | 'Array' | 'Object' }'"
},
invalid_namespace_property: (namespace: string, suggestion?: string) => ({
invalid_namespace_property: /**
* @param {string} namespace
* @param {string} [suggestion]
*/ (namespace, suggestion) => ({
code: 'invalid-namespace-property',
message:
`Invalid namespace '${namespace}'` + (suggestion ? ` (did you mean '${suggestion}'?)` : '')
@ -240,11 +262,11 @@ export default {
code: 'invalid-namespace-attribute',
message: "The 'namespace' attribute must be a string literal representing a valid namespace"
},
invalid_attribute_value: (name: string) => ({
invalid_attribute_value: /** @param {string} name */ (name) => ({
code: `invalid-${name}-value`,
message: `${name} attribute must be true or false`
}),
invalid_options_attribute_unknown: (name: string) => ({
invalid_options_attribute_unknown: /** @param {string} name */ (name) => ({
code: 'invalid-options-attribute',
message: `<svelte:options> unknown attribute '${name}'`
}),
@ -266,7 +288,7 @@ export default {
message:
':global(...) not at the start of a selector sequence should not contain type or universal selectors'
},
css_invalid_selector: (selector: string) => ({
css_invalid_selector: /** @param {string} selector */ (selector) => ({
code: 'css-invalid-selector',
message: `Invalid selector "${selector}"`
}),
@ -303,15 +325,15 @@ export default {
message:
'{@const} must be the immediate child of {#if}, {:else if}, {:else}, {#each}, {:then}, {:catch}, <svelte:fragment> or <Component>'
},
invalid_const_declaration: (name: string) => ({
invalid_const_declaration: /** @param {string} name */ (name) => ({
code: 'invalid-const-declaration',
message: `'${name}' has already been declared`
}),
invalid_const_update: (name: string) => ({
invalid_const_update: /** @param {string} name */ (name) => ({
code: 'invalid-const-update',
message: `'${name}' is declared using {@const ...} and is read-only`
}),
cyclical_const_tags: (cycle: string[]) => ({
cyclical_const_tags: /** @param {string[]} cycle */ (cycle) => ({
code: 'cyclical-const-tags',
message: `Cyclical dependency detected: ${cycle.join(' → ')}`
}),
@ -323,7 +345,7 @@ export default {
code: 'invalid_var_declaration',
message: '"var" scope should not extend outside the reactive block'
},
invalid_style_directive_modifier: (valid: string) => ({
invalid_style_directive_modifier: /** @param {string} valid */ (valid) => ({
code: 'invalid-style-directive-modifier',
message: `Valid modifiers for style directives are: ${valid}`
})

@ -1,7 +1,3 @@
// All compiler warnings should be listed and accessed from here
import { ARIAPropertyDefinition } from 'aria-query';
/**
* @internal
*/
@ -10,7 +6,10 @@ export default {
code: 'tag-option-deprecated',
message: "'tag' option is deprecated — use 'customElement' instead"
},
unused_export_let: (component: string, property: string) => ({
unused_export_let: /**
* @param {string} component
* @param {string} property
*/ (component, property) => ({
code: 'unused-export-let',
message: `${component} has unused export property '${property}'. If it is for external reference only, please consider using \`export const ${property}\``
}),
@ -22,13 +21,16 @@ export default {
code: 'non-top-level-reactive-declaration',
message: '$: has no effect outside of the top-level'
},
module_script_variable_reactive_declaration: (names: string[]) => ({
module_script_variable_reactive_declaration: /** @param {string[]} names */ (names) => ({
code: 'module-script-reactive-declaration',
message: `${names.map((name) => `"${name}"`).join(', ')} ${
message: `${names.map(/** @param {any} name */ (name) => `"${name}"`).join(', ')} ${
names.length > 1 ? 'are' : 'is'
} declared in a module script and will not be reactive`
}),
missing_declaration: (name: string, has_script: boolean) => ({
missing_declaration: /**
* @param {string} name
* @param {boolean} has_script
*/ (name, has_script) => ({
code: 'missing-declaration',
message:
`'${name}' is not defined` +
@ -41,7 +43,7 @@ export default {
message:
"The 'customElement' option is used when generating a custom element. Did you forget the 'customElement: true' compile option?"
},
css_unused_selector: (selector: string) => ({
css_unused_selector: /** @param {string} selector */ (selector) => ({
code: 'css-unused-selector',
message: `Unused CSS selector "${selector}"`
}),
@ -49,11 +51,11 @@ export default {
code: 'empty-block',
message: 'Empty block'
},
reactive_component: (name: string) => ({
reactive_component: /** @param {string} name */ (name) => ({
code: 'reactive-component',
message: `<${name}/> will not be reactive if ${name} changes. Use <svelte:component this={${name}}/> if you want this reactivity.`
}),
component_name_lowercase: (name: string) => ({
component_name_lowercase: /** @param {string} name */ (name) => ({
code: 'component-name-lowercase',
message: `<${name}> will be treated as an HTML element unless it begins with a capital letter`
}),
@ -61,15 +63,21 @@ export default {
code: 'avoid-is',
message: "The 'is' attribute is not supported cross-browser and should be avoided"
},
invalid_html_attribute: (name: string, suggestion: string) => ({
invalid_html_attribute: /**
* @param {string} name
* @param {string} suggestion
*/ (name, suggestion) => ({
code: 'invalid-html-attribute',
message: `'${name}' is not a valid HTML attribute. Did you mean '${suggestion}'?`
}),
a11y_aria_attributes: (name: string) => ({
a11y_aria_attributes: /** @param {string} name */ (name) => ({
code: 'a11y-aria-attributes',
message: `A11y: <${name}> should not have aria-* attributes`
}),
a11y_incorrect_attribute_type: (schema: ARIAPropertyDefinition, attribute: string) => {
a11y_incorrect_attribute_type: /**
* @param {import('aria-query').ARIAPropertyDefinition} schema
* @param {string} attribute
*/ (schema, attribute) => {
let message;
switch (schema.type) {
case 'boolean':
@ -102,73 +110,84 @@ export default {
message: `A11y: ${message}`
};
},
a11y_unknown_aria_attribute: (attribute: string, suggestion?: string) => ({
a11y_unknown_aria_attribute: /**
* @param {string} attribute
* @param {string} [suggestion]
*/ (attribute, suggestion) => ({
code: 'a11y-unknown-aria-attribute',
message:
`A11y: Unknown aria attribute 'aria-${attribute}'` +
(suggestion ? ` (did you mean '${suggestion}'?)` : '')
}),
a11y_hidden: (name: string) => ({
a11y_hidden: /** @param {string} name */ (name) => ({
code: 'a11y-hidden',
message: `A11y: <${name}> element should not be hidden`
}),
a11y_misplaced_role: (name: string) => ({
a11y_misplaced_role: /** @param {string} name */ (name) => ({
code: 'a11y-misplaced-role',
message: `A11y: <${name}> should not have role attribute`
}),
a11y_unknown_role: (role: string | boolean, suggestion?: string) => ({
a11y_unknown_role: /**
* @param {string | boolean} role
* @param {string} [suggestion]
*/ (role, suggestion) => ({
code: 'a11y-unknown-role',
message: `A11y: Unknown role '${role}'` + (suggestion ? ` (did you mean '${suggestion}'?)` : '')
}),
a11y_no_abstract_role: (role: string | boolean) => ({
a11y_no_abstract_role: /** @param {string | boolean} role */ (role) => ({
code: 'a11y-no-abstract-role',
message: `A11y: Abstract role '${role}' is forbidden`
}),
a11y_no_redundant_roles: (role: string | boolean) => ({
a11y_no_redundant_roles: /** @param {string | boolean} role */ (role) => ({
code: 'a11y-no-redundant-roles',
message: `A11y: Redundant role '${role}'`
}),
a11y_no_static_element_interactions: (element: string, handlers: string[]) => ({
a11y_no_static_element_interactions: /**
* @param {string} element
* @param {string[]} handlers
*/ (element, handlers) => ({
code: 'a11y-no-static-element-interactions',
message: `A11y: <${element}> with ${handlers.join(', ')} ${
handlers.length === 1 ? 'handler' : 'handlers'
} must have an ARIA role`
}),
a11y_no_interactive_element_to_noninteractive_role: (
role: string | boolean,
element: string
) => ({
a11y_no_interactive_element_to_noninteractive_role: /**
* @param {string | boolean} role
* @param {string} element
*/ (role, element) => ({
code: 'a11y-no-interactive-element-to-noninteractive-role',
message: `A11y: <${element}> cannot have role '${role}'`
}),
a11y_no_noninteractive_element_interactions: (element: string) => ({
a11y_no_noninteractive_element_interactions: /** @param {string} element */ (element) => ({
code: 'a11y-no-noninteractive-element-interactions',
message: `A11y: Non-interactive element <${element}> should not be assigned mouse or keyboard event listeners.`
}),
a11y_no_noninteractive_element_to_interactive_role: (
role: string | boolean,
element: string
) => ({
a11y_no_noninteractive_element_to_interactive_role: /**
* @param {string | boolean} role
* @param {string} element
*/ (role, element) => ({
code: 'a11y-no-noninteractive-element-to-interactive-role',
message: `A11y: Non-interactive element <${element}> cannot have interactive role '${role}'`
}),
a11y_role_has_required_aria_props: (role: string, props: string[]) => ({
a11y_role_has_required_aria_props: /**
* @param {string} role
* @param {string[]} props
*/ (role, props) => ({
code: 'a11y-role-has-required-aria-props',
message: `A11y: Elements with the ARIA role "${role}" must have the following attributes defined: ${props
.map((name) => `"${name}"`)
.map(/** @param {any} name */ (name) => `"${name}"`)
.join(', ')}`
}),
a11y_role_supports_aria_props: (
attribute: string,
role: string,
is_implicit: boolean,
name: string
) => {
a11y_role_supports_aria_props: /**
* @param {string} attribute
* @param {string} role
* @param {boolean} is_implicit
* @param {string} name
*/ (attribute, role, is_implicit, name) => {
let message = `The attribute '${attribute}' is not supported by the role '${role}'.`;
if (is_implicit) {
message += ` This role is implicit on the element <${name}>.`;
}
return {
code: 'a11y-role-supports-aria-props',
message: `A11y: ${message}`
@ -190,15 +209,25 @@ export default {
code: 'a11y-positive-tabindex',
message: 'A11y: avoid tabindex values above zero'
},
a11y_invalid_attribute: (href_attribute: string, href_value: string) => ({
a11y_invalid_attribute: /**
* @param {string} href_attribute
* @param {string} href_value
*/ (href_attribute, href_value) => ({
code: 'a11y-invalid-attribute',
message: `A11y: '${href_value}' is not a valid ${href_attribute} attribute`
}),
a11y_missing_attribute: (name: string, article: string, sequence: string) => ({
a11y_missing_attribute: /**
* @param {string} name
* @param {string} article
* @param {string} sequence
*/ (name, article, sequence) => ({
code: 'a11y-missing-attribute',
message: `A11y: <${name}> element should have ${article} ${sequence} attribute`
}),
a11y_autocomplete_valid: (type: null | true | string, value: null | true | string) => ({
a11y_autocomplete_valid: /**
* @param {null | true | string} type
* @param {null | true | string} value
*/ (type, value) => ({
code: 'a11y-autocomplete-valid',
message: `A11y: The value '${value}' is not supported by the attribute 'autocomplete' on element <input type="${
type || '...'
@ -208,7 +237,7 @@ export default {
code: 'a11y-img-redundant-alt',
message: 'A11y: Screenreaders already announce <img> elements as an image.'
},
a11y_interactive_supports_focus: (role: string) => ({
a11y_interactive_supports_focus: /** @param {string} role */ (role) => ({
code: 'a11y-interactive-supports-focus',
message: `A11y: Elements with the '${role}' interactive role must have a tabindex value.`
}),
@ -220,7 +249,7 @@ export default {
code: 'a11y-media-has-caption',
message: 'A11y: <video> elements must have a <track kind="captions">'
},
a11y_distracting_elements: (name: string) => ({
a11y_distracting_elements: /** @param {string} name */ (name) => ({
code: 'a11y-distracting-elements',
message: `A11y: Avoid <${name}> elements`
}),
@ -232,7 +261,10 @@ export default {
code: 'a11y-structure',
message: 'A11y: <figcaption> must be first or last child of <figure>'
},
a11y_mouse_events_have_key_events: (event: string, accompanied_by: string) => ({
a11y_mouse_events_have_key_events: /**
* @param {string} event
* @param {string} accompanied_by
*/ (event, accompanied_by) => ({
code: 'a11y-mouse-events-have-key-events',
message: `A11y: on:${event} must be accompanied by on:${accompanied_by}`
}),
@ -241,7 +273,7 @@ export default {
message:
'A11y: visible, non-interactive elements with an on:click event must be accompanied by an on:keydown, on:keyup, or on:keypress event.'
},
a11y_missing_content: (name: string) => ({
a11y_missing_content: /** @param {string} name */ (name) => ({
code: 'a11y-missing-content',
message: `A11y: <${name}> element should have child content`
}),
@ -261,7 +293,7 @@ export default {
code: 'redundant-event-modifier',
message: 'The passive modifier only works with wheel and touch events'
},
invalid_rest_eachblock_binding: (rest_element_name: string) => ({
invalid_rest_eachblock_binding: /** @param {string} rest_element_name */ (rest_element_name) => ({
code: 'invalid-rest-eachblock-binding',
message: `The rest operator (...) will create a new object and binding '${rest_element_name}' with the original object will not work`
}),

@ -1,38 +1,48 @@
import list from '../utils/list';
import { ModuleFormat } from '../interfaces';
import list from '../utils/list.js';
import { b, x } from 'code-red';
import { Identifier, ImportDeclaration, ExportNamedDeclaration } from 'estree';
const wrappers = { esm, cjs };
interface Export {
name: string;
as: string;
}
/**
* @param {any} program
* @param {import('../interfaces.js').ModuleFormat} format
* @param {import('estree').Identifier} name
* @param {string} banner
* @param {any} sveltePath
* @param {Array<{ name: string; alias: import('estree').Identifier }>} helpers
* @param {Array<{ name: string; alias: import('estree').Identifier }>} globals
* @param {import('estree').ImportDeclaration[]} imports
* @param {Export[]} module_exports
* @param {import('estree').ExportNamedDeclaration[]} exports_from
*/
export default function create_module(
program: any,
format: ModuleFormat,
name: Identifier,
banner: string,
program,
format,
name,
banner,
sveltePath = 'svelte',
helpers: Array<{ name: string; alias: Identifier }>,
globals: Array<{ name: string; alias: Identifier }>,
imports: ImportDeclaration[],
module_exports: Export[],
exports_from: ExportNamedDeclaration[]
helpers,
globals,
imports,
module_exports,
exports_from
) {
const internal_path = `${sveltePath}/internal`;
helpers.sort((a, b) => (a.name < b.name ? -1 : 1));
globals.sort((a, b) => (a.name < b.name ? -1 : 1));
helpers.sort(
/**
* @param {any} a
* @param {any} b
*/ (a, b) => (a.name < b.name ? -1 : 1)
);
globals.sort(
/**
* @param {any} a
* @param {any} b
*/ (a, b) => (a.name < b.name ? -1 : 1)
);
const formatter = wrappers[format];
if (!formatter) {
throw new Error(`options.format is invalid (must be ${list(Object.keys(wrappers))})`);
}
return formatter(
program,
name,
@ -47,16 +57,21 @@ export default function create_module(
);
}
/**
* @param {any} source
* @param {any} sveltePath
*/
function edit_source(source, sveltePath) {
return source === 'svelte' || source.startsWith('svelte/')
? source.replace('svelte', sveltePath)
: source;
}
function get_internal_globals(
globals: Array<{ name: string; alias: Identifier }>,
helpers: Array<{ name: string; alias: Identifier }>
) {
/**
* @param {Array<{ name: string; alias: import('estree').Identifier }>} globals
* @param {Array<{ name: string; alias: import('estree').Identifier }>} helpers
*/
function get_internal_globals(globals, helpers) {
return (
globals.length > 0 && {
type: 'VariableDeclaration',
@ -66,48 +81,64 @@ function get_internal_globals(
type: 'VariableDeclarator',
id: {
type: 'ObjectPattern',
properties: globals.map((g) => ({
type: 'Property',
method: false,
shorthand: false,
computed: false,
key: { type: 'Identifier', name: g.name },
value: g.alias,
kind: 'init'
}))
properties: globals.map(
/** @param {any} g */ (g) => ({
type: 'Property',
method: false,
shorthand: false,
computed: false,
key: { type: 'Identifier', name: g.name },
value: g.alias,
kind: 'init'
})
)
},
init: helpers.find(({ name }) => name === 'globals').alias
init: helpers.find(/** @param {any}params_0 */ ({ name }) => name === 'globals').alias
}
]
}
);
}
/**
* @param {any} program
* @param {import('estree').Identifier} name
* @param {string} banner
* @param {string} sveltePath
* @param {string} internal_path
* @param {Array<{ name: string; alias: import('estree').Identifier }>} helpers
* @param {Array<{ name: string; alias: import('estree').Identifier }>} globals
* @param {import('estree').ImportDeclaration[]} imports
* @param {Export[]} module_exports
* @param {import('estree').ExportNamedDeclaration[]} exports_from
*/
function esm(
program: any,
name: Identifier,
banner: string,
sveltePath: string,
internal_path: string,
helpers: Array<{ name: string; alias: Identifier }>,
globals: Array<{ name: string; alias: Identifier }>,
imports: ImportDeclaration[],
module_exports: Export[],
exports_from: ExportNamedDeclaration[]
program,
name,
banner,
sveltePath,
internal_path,
helpers,
globals,
imports,
module_exports,
exports_from
) {
const import_declaration = {
type: 'ImportDeclaration',
specifiers: helpers.map((h) => ({
type: 'ImportSpecifier',
local: h.alias,
imported: { type: 'Identifier', name: h.name }
})),
specifiers: helpers.map(
/** @param {any} h */ (h) => ({
type: 'ImportSpecifier',
local: h.alias,
imported: { type: 'Identifier', name: h.name }
})
),
source: { type: 'Literal', value: internal_path }
};
const internal_globals = get_internal_globals(globals, helpers);
// edit user imports
/** @param {any} node */
function rewrite_import(node) {
const value = edit_source(node.source.value, sveltePath);
if (node.source.value !== value) {
@ -117,16 +148,16 @@ function esm(
}
imports.forEach(rewrite_import);
exports_from.forEach(rewrite_import);
const exports = module_exports.length > 0 && {
type: 'ExportNamedDeclaration',
specifiers: module_exports.map((x) => ({
type: 'Specifier',
local: { type: 'Identifier', name: x.name },
exported: { type: 'Identifier', name: x.as }
}))
specifiers: module_exports.map(
/** @param {any} x */ (x) => ({
type: 'Specifier',
local: { type: 'Identifier', name: x.name },
exported: { type: 'Identifier', name: x.as }
})
)
};
program.body = b`
/* ${banner} */
@ -142,17 +173,29 @@ function esm(
`;
}
/**
* @param {any} program
* @param {import('estree').Identifier} name
* @param {string} banner
* @param {string} sveltePath
* @param {string} internal_path
* @param {Array<{ name: string; alias: import('estree').Identifier }>} helpers
* @param {Array<{ name: string; alias: import('estree').Identifier }>} globals
* @param {import('estree').ImportDeclaration[]} imports
* @param {Export[]} module_exports
* @param {import('estree').ExportNamedDeclaration[]} exports_from
*/
function cjs(
program: any,
name: Identifier,
banner: string,
sveltePath: string,
internal_path: string,
helpers: Array<{ name: string; alias: Identifier }>,
globals: Array<{ name: string; alias: Identifier }>,
imports: ImportDeclaration[],
module_exports: Export[],
exports_from: ExportNamedDeclaration[]
program,
name,
banner,
sveltePath,
internal_path,
helpers,
globals,
imports,
module_exports,
exports_from
) {
const internal_requires = {
type: 'VariableDeclaration',
@ -162,70 +205,76 @@ function cjs(
type: 'VariableDeclarator',
id: {
type: 'ObjectPattern',
properties: helpers.map((h) => ({
type: 'Property',
method: false,
shorthand: false,
computed: false,
key: { type: 'Identifier', name: h.name },
value: h.alias,
kind: 'init'
}))
properties: helpers.map(
/** @param {any} h */ (h) => ({
type: 'Property',
method: false,
shorthand: false,
computed: false,
key: { type: 'Identifier', name: h.name },
value: h.alias,
kind: 'init'
})
)
},
init: x`require("${internal_path}")`
}
]
};
const internal_globals = get_internal_globals(globals, helpers);
const user_requires = imports.map((node) => {
const init = x`require("${edit_source(node.source.value, sveltePath)}")`;
if (node.specifiers.length === 0) {
return b`${init};`;
const user_requires = imports.map(
/** @param {any} node */ (node) => {
const init = x`require("${edit_source(node.source.value, sveltePath)}")`;
if (node.specifiers.length === 0) {
return b`${init};`;
}
return {
type: 'VariableDeclaration',
kind: 'const',
declarations: [
{
type: 'VariableDeclarator',
id:
node.specifiers[0].type === 'ImportNamespaceSpecifier'
? { type: 'Identifier', name: node.specifiers[0].local.name }
: {
type: 'ObjectPattern',
properties: node.specifiers.map(
/** @param {any} s */ (s) => ({
type: 'Property',
method: false,
shorthand: false,
computed: false,
key:
s.type === 'ImportSpecifier'
? s.imported
: { type: 'Identifier', name: 'default' },
value: s.local,
kind: 'init'
})
)
},
init
}
]
};
}
return {
type: 'VariableDeclaration',
kind: 'const',
declarations: [
{
type: 'VariableDeclarator',
id:
node.specifiers[0].type === 'ImportNamespaceSpecifier'
? { type: 'Identifier', name: node.specifiers[0].local.name }
: {
type: 'ObjectPattern',
properties: node.specifiers.map((s) => ({
type: 'Property',
method: false,
shorthand: false,
computed: false,
key:
s.type === 'ImportSpecifier'
? s.imported
: { type: 'Identifier', name: 'default' },
value: s.local,
kind: 'init'
}))
},
init
}
]
};
});
);
const exports = module_exports.map(
/** @param {any} x */
(x) =>
b`exports.${{ type: 'Identifier', name: x.as }} = ${{ type: 'Identifier', name: x.name }};`
);
const user_exports_from = exports_from.map((node) => {
const init = x`require("${edit_source(node.source.value, sveltePath)}")`;
return node.specifiers.map((specifier) => {
return b`exports.${specifier.exported} = ${init}.${specifier.local};`;
});
});
const user_exports_from = exports_from.map(
/** @param {any} node */ (node) => {
const init = x`require("${edit_source(node.source.value, sveltePath)}")`;
return node.specifiers.map(
/** @param {any} specifier */ (specifier) => {
return b`exports.${specifier.exported} = ${init}.${specifier.local};`;
}
);
}
);
program.body = b`
/* ${banner} */
@ -241,3 +290,8 @@ function cjs(
${exports}
`;
}
/** @typedef {Object} Export
* @property {string} name
* @property {string} as
*/

@ -1,109 +1,118 @@
import MagicString from 'magic-string';
import Stylesheet from './Stylesheet';
import { gather_possible_values, UNKNOWN } from './gather_possible_values';
import { CssNode } from './interfaces';
import Component from '../Component';
import Element from '../nodes/Element';
import { INode } from '../nodes/interfaces';
import EachBlock from '../nodes/EachBlock';
import IfBlock from '../nodes/IfBlock';
import AwaitBlock from '../nodes/AwaitBlock';
import compiler_errors from '../compiler_errors';
import { regex_starts_with_whitespace, regex_ends_with_whitespace } from '../../utils/patterns';
enum BlockAppliesToNode {
NotPossible,
Possible,
UnknownSelectorType
}
enum NodeExist {
Probably = 1,
Definitely = 2
}
import { gather_possible_values, UNKNOWN } from './gather_possible_values.js';
import compiler_errors from '../compiler_errors.js';
import { regex_starts_with_whitespace, regex_ends_with_whitespace } from '../../utils/patterns.js';
const BlockAppliesToNode = /** @type {const} */ ({
NotPossible: 0,
Possible: 1,
UnknownSelectorType: 2
});
const NodeExist = /** @type {const} */ ({
Probably: 0,
Definitely: 1
});
/** @typedef {typeof NodeExist[keyof typeof NodeExist]} NodeExistsValue */
const whitelist_attribute_selector = new Map([
['details', new Set(['open'])],
['dialog', new Set(['open'])]
]);
const regex_is_single_css_selector = /[^\\],(?!([^([]+[^\\]|[^([\\])[)\]])/;
export default class Selector {
node: CssNode;
stylesheet: Stylesheet;
blocks: Block[];
local_blocks: Block[];
used: boolean;
/** @type {import('./private.js').CssNode} */
node;
/** @type {import('./Stylesheet.js').default} */
stylesheet;
/** @type {Block[]} */
blocks;
constructor(node: CssNode, stylesheet: Stylesheet) {
/** @type {Block[]} */
local_blocks;
/** @type {boolean} */
used;
/**
* @param {import('./private.js').CssNode} node
* @param {import('./Stylesheet.js').default} stylesheet
*/
constructor(node, stylesheet) {
this.node = node;
this.stylesheet = stylesheet;
this.blocks = group_selectors(node);
// take trailing :global(...) selectors out of consideration
let i = this.blocks.length;
while (i > 0) {
if (!this.blocks[i - 1].global) break;
i -= 1;
}
this.local_blocks = this.blocks.slice(0, i);
const host_only = this.blocks.length === 1 && this.blocks[0].host;
const root_only = this.blocks.length === 1 && this.blocks[0].root;
this.used = this.local_blocks.length === 0 || host_only || root_only;
}
apply(node: Element) {
const to_encapsulate: Array<{ node: Element; block: Block }> = [];
/** @param {import('../nodes/Element.js').default} node */
apply(node) {
/** @type {Array<{ node: import('../nodes/Element.js').default; block: Block }>} */
const to_encapsulate = [];
apply_selector(this.local_blocks.slice(), node, to_encapsulate);
if (to_encapsulate.length > 0) {
to_encapsulate.forEach(({ node, block }) => {
this.stylesheet.nodes_with_css_class.add(node);
block.should_encapsulate = true;
});
this.used = true;
}
}
minify(code: MagicString) {
let c: number = null;
/** @param {import('magic-string').default} code */
minify(code) {
/** @type {number} */
let c = null;
this.blocks.forEach((block, i) => {
if (i > 0) {
if (block.start - c > 1) {
code.update(c, block.start, block.combinator.name || ' ');
}
}
c = block.end;
});
}
transform(code: MagicString, attr: string, max_amount_class_specificity_increased: number) {
/**
* @param {import('magic-string').default} code
* @param {string} attr
* @param {number} max_amount_class_specificity_increased
*/
transform(code, attr, max_amount_class_specificity_increased) {
const amount_class_specificity_to_increase =
max_amount_class_specificity_increased -
this.blocks.filter((block) => block.should_encapsulate).length;
function remove_global_pseudo_class(selector: CssNode) {
/** @param {import('./private.js').CssNode} selector */
function remove_global_pseudo_class(selector) {
const first = selector.children[0];
const last = selector.children[selector.children.length - 1];
code.remove(selector.start, first.start).remove(last.end, selector.end);
}
function encapsulate_block(block: Block, attr: string) {
/**
* @param {Block} block
* @param {string} attr
*/
function encapsulate_block(block, attr) {
for (const selector of block.selectors) {
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
remove_global_pseudo_class(selector);
}
}
let i = block.selectors.length;
while (i--) {
const selector = block.selectors[i];
if (selector.type === 'PseudoElementSelector' || selector.type === 'PseudoClassSelector') {
@ -112,17 +121,14 @@ export default class Selector {
}
continue;
}
if (selector.type === 'TypeSelector' && selector.name === '*') {
code.update(selector.start, selector.end, attr);
} else {
code.appendLeft(selector.end, attr);
}
break;
}
}
this.blocks.forEach((block, index) => {
if (block.global) {
remove_global_pseudo_class(block.selectors[0]);
@ -137,35 +143,32 @@ export default class Selector {
});
}
validate(component: Component) {
/** @param {import('../Component.js').default} component */
validate(component) {
let start = 0;
let end = this.blocks.length;
for (; start < end; start += 1) {
if (!this.blocks[start].global) break;
}
for (; end > start; end -= 1) {
if (!this.blocks[end - 1].global) break;
}
for (let i = start; i < end; i += 1) {
if (this.blocks[i].global) {
return component.error(this.blocks[i].selectors[0], compiler_errors.css_invalid_global);
}
}
this.validate_global_with_multiple_selectors(component);
this.validate_global_compound_selector(component);
this.validate_invalid_combinator_without_selector(component);
}
validate_global_with_multiple_selectors(component: Component) {
/** @param {import('../Component.js').default} component */
validate_global_with_multiple_selectors(component) {
if (this.blocks.length === 1 && this.blocks[0].selectors.length === 1) {
// standalone :global() with multiple selectors is OK
return;
}
for (const block of this.blocks) {
for (const selector of block.selectors) {
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
@ -176,7 +179,9 @@ export default class Selector {
}
}
}
validate_invalid_combinator_without_selector(component: Component) {
/** @param {import('../Component.js').default} component */
validate_invalid_combinator_without_selector(component) {
for (let i = 0; i < this.blocks.length; i++) {
const block = this.blocks[i];
if (block.combinator && block.selectors.length === 0) {
@ -198,7 +203,8 @@ export default class Selector {
}
}
validate_global_compound_selector(component: Component) {
/** @param {import('../Component.js').default} component */
validate_global_compound_selector(component) {
for (const block of this.blocks) {
for (let index = 0; index < block.selectors.length; index++) {
const selector = block.selectors[index];
@ -227,42 +233,38 @@ export default class Selector {
}
}
function apply_selector(
blocks: Block[],
node: Element,
to_encapsulate: Array<{ node: Element; block: Block }>
): boolean {
/**
* @param {Block[]} blocks
* @param {import('../nodes/Element.js').default} node
* @param {Array<{ node: import('../nodes/Element.js').default; block: Block }>} to_encapsulate
* @returns {boolean}
*/
function apply_selector(blocks, node, to_encapsulate) {
const block = blocks.pop();
if (!block) return false;
if (!node) {
return (
(block.global && blocks.every((block) => block.global)) || (block.host && blocks.length === 0)
);
}
switch (block_might_apply_to_node(block, node)) {
case BlockAppliesToNode.NotPossible:
return false;
case BlockAppliesToNode.UnknownSelectorType:
// bail. TODO figure out what these could be
to_encapsulate.push({ node, block });
return true;
}
if (block.combinator) {
if (block.combinator.type === 'Combinator' && block.combinator.name === ' ') {
for (const ancestor_block of blocks) {
if (ancestor_block.global) {
continue;
}
if (ancestor_block.host) {
to_encapsulate.push({ node, block });
return true;
}
let parent = node;
while ((parent = get_element_parent(parent))) {
if (
@ -271,18 +273,15 @@ function apply_selector(
to_encapsulate.push({ node: parent, block: ancestor_block });
}
}
if (to_encapsulate.length) {
to_encapsulate.push({ node, block });
return true;
}
}
if (blocks.every((block) => block.global)) {
to_encapsulate.push({ node, block });
return true;
}
return false;
} else if (block.combinator.name === '>') {
const has_global_parent = blocks.every((block) => block.global);
@ -290,12 +289,10 @@ function apply_selector(
to_encapsulate.push({ node, block });
return true;
}
return false;
} else if (block.combinator.name === '+' || block.combinator.name === '~') {
const siblings = get_possible_element_siblings(node, block.combinator.name === '+');
let has_match = false;
// NOTE: if we have :global(), we couldn't figure out what is selected within `:global` due to the
// css-tree limitation that does not parse the inner selector of :global
// so unless we are sure there will be no sibling to match, we will consider it as matched
@ -307,7 +304,6 @@ function apply_selector(
to_encapsulate.push({ node, block });
return true;
}
for (const possible_sibling of siblings.keys()) {
if (apply_selector(blocks.slice(), possible_sibling, to_encapsulate)) {
to_encapsulate.push({ node, block });
@ -316,31 +312,31 @@ function apply_selector(
}
return has_match;
}
// TODO other combinators
to_encapsulate.push({ node, block });
return true;
}
to_encapsulate.push({ node, block });
return true;
}
const regex_backslash_and_following_character = /\\(.)/g;
function block_might_apply_to_node(block: Block, node: Element): BlockAppliesToNode {
/**
* @param {Block} block
* @param {import('../nodes/Element.js').default} node
* @returns {typeof BlockAppliesToNode[keyof typeof BlockAppliesToNode]}
*/
function block_might_apply_to_node(block, node) {
let i = block.selectors.length;
while (i--) {
const selector = block.selectors[i];
const name =
typeof selector.name === 'string' &&
selector.name.replace(regex_backslash_and_following_character, '$1');
if (selector.type === 'PseudoClassSelector' && (name === 'host' || name === 'root')) {
return BlockAppliesToNode.NotPossible;
}
if (
block.selectors.length === 1 &&
selector.type === 'PseudoClassSelector' &&
@ -348,11 +344,9 @@ function block_might_apply_to_node(block: Block, node: Element): BlockAppliesToN
) {
return BlockAppliesToNode.NotPossible;
}
if (selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector') {
continue;
}
if (selector.type === 'ClassSelector') {
if (
!attribute_matches(node, 'class', name, '~=', false) &&
@ -390,10 +384,15 @@ function block_might_apply_to_node(block: Block, node: Element): BlockAppliesToN
return BlockAppliesToNode.UnknownSelectorType;
}
}
return BlockAppliesToNode.Possible;
}
/**
* @param {any} operator
* @param {any} expected_value
* @param {any} case_insensitive
* @param {any} value
*/
function test_attribute(operator, expected_value, case_insensitive, value) {
if (case_insensitive) {
expected_value = expected_value.toLowerCase();
@ -417,32 +416,28 @@ function test_attribute(operator, expected_value, case_insensitive, value) {
}
}
function attribute_matches(
node: CssNode,
name: string,
expected_value: string,
operator: string,
case_insensitive: boolean
) {
/**
* @param {import('./private.js').CssNode} node
* @param {string} name
* @param {string} expected_value
* @param {string} operator
* @param {boolean} case_insensitive
*/
function attribute_matches(node, name, expected_value, operator, case_insensitive) {
const spread = node.attributes.find((attr) => attr.type === 'Spread');
if (spread) return true;
if (node.bindings.some((binding: CssNode) => binding.name === name)) return true;
const attr = node.attributes.find((attr: CssNode) => attr.name === name);
if (node.bindings.some((binding) => binding.name === name)) return true;
const attr = node.attributes.find((attr) => attr.name === name);
if (!attr) return false;
if (attr.is_true) return operator === null;
if (expected_value == null) return true;
if (attr.chunks.length === 1) {
const value = attr.chunks[0];
if (!value) return false;
if (value.type === 'Text')
return test_attribute(operator, expected_value, case_insensitive, value.data);
}
const possible_values = new Set();
let prev_values = [];
for (const chunk of attr.chunks) {
const current_possible_values = new Set();
@ -451,35 +446,30 @@ function attribute_matches(
} else {
gather_possible_values(chunk.node, current_possible_values);
}
// impossible to find out all combinations
if (current_possible_values.has(UNKNOWN)) return true;
if (prev_values.length > 0) {
const start_with_space = [];
const remaining = [];
current_possible_values.forEach((current_possible_value: string) => {
current_possible_values.forEach((current_possible_value) => {
if (regex_starts_with_whitespace.test(current_possible_value)) {
start_with_space.push(current_possible_value);
} else {
remaining.push(current_possible_value);
}
});
if (remaining.length > 0) {
if (start_with_space.length > 0) {
prev_values.forEach((prev_value) => possible_values.add(prev_value));
}
const combined = [];
prev_values.forEach((prev_value: string) => {
remaining.forEach((value: string) => {
prev_values.forEach((prev_value) => {
remaining.forEach((value) => {
combined.push(prev_value + value);
});
});
prev_values = combined;
start_with_space.forEach((value: string) => {
start_with_space.forEach((value) => {
if (regex_ends_with_whitespace.test(value)) {
possible_values.add(value);
} else {
@ -492,8 +482,7 @@ function attribute_matches(
prev_values = [];
}
}
current_possible_values.forEach((current_possible_value: string) => {
current_possible_values.forEach((current_possible_value) => {
if (regex_ends_with_whitespace.test(current_possible_value)) {
possible_values.add(current_possible_value);
} else {
@ -503,24 +492,21 @@ function attribute_matches(
if (prev_values.length < current_possible_values.size) {
prev_values.push(' ');
}
if (prev_values.length > 20) {
// might grow exponentially, bail out
return true;
}
}
prev_values.forEach((prev_value) => possible_values.add(prev_value));
if (possible_values.has(UNKNOWN)) return true;
for (const value of possible_values) {
if (test_attribute(operator, expected_value, case_insensitive, value)) return true;
}
return false;
}
function unquote(value: CssNode) {
/** @param {import('./private.js').CssNode} value */
function unquote(value) {
if (value.type === 'Identifier') return value.name;
const str = value.value;
if ((str[0] === str[str.length - 1] && str[0] === "'") || str[0] === '"') {
@ -529,10 +515,15 @@ function unquote(value: CssNode) {
return str;
}
function get_element_parent(node: Element): Element | null {
let parent: INode = node;
/**
* @param {import('../nodes/Element.js').default} node
* @returns {any}
*/
function get_element_parent(node) {
/** @type {import('../nodes/interfaces.js').INode} */
let parent = node;
while ((parent = parent.parent) && parent.type !== 'Element');
return parent as Element | null;
return /** @type {import('../nodes/Element.js').default | null} */ (parent);
}
/**
@ -550,9 +541,12 @@ function get_element_parent(node: Element): Element | null {
* is considered to look like:
* <h1>Heading 1</h1>
* <h2>Heading 2</h2>
* @param {import('../nodes/interfaces.js').INode} node
* @returns {import('../nodes/interfaces.js').INode}
*/
function find_previous_sibling(node: INode): INode {
let current_node: INode = node;
function find_previous_sibling(node) {
/** @type {import('../nodes/interfaces.js').INode} */
let current_node = node;
do {
if (current_node.type === 'Slot') {
const slot_children = current_node.children;
@ -561,22 +555,25 @@ function find_previous_sibling(node: INode): INode {
continue;
}
}
while (!current_node.prev && current_node.parent && current_node.parent.type === 'Slot') {
current_node = current_node.parent;
}
current_node = current_node.prev;
} while (current_node && current_node.type === 'Slot');
return current_node;
}
function get_possible_element_siblings(
node: INode,
adjacent_only: boolean
): Map<Element, NodeExist> {
const result: Map<Element, NodeExist> = new Map();
let prev: INode = node;
/**
* @param {import('../nodes/interfaces.js').INode} node
* @param {boolean} adjacent_only
* @returns {Map<import('../nodes/Element.js').default, NodeExistsValue>}
*/
function get_possible_element_siblings(node, adjacent_only) {
/** @type {Map<import('../nodes/Element.js').default, NodeExistsValue>} */
const result = new Map();
/** @type {import('../nodes/interfaces.js').INode} */
let prev = node;
while ((prev = find_previous_sibling(prev))) {
if (prev.type === 'Element') {
if (
@ -586,22 +583,20 @@ function get_possible_element_siblings(
) {
result.set(prev, NodeExist.Definitely);
}
if (adjacent_only) {
break;
}
} else if (prev.type === 'EachBlock' || prev.type === 'IfBlock' || prev.type === 'AwaitBlock') {
const possible_last_child = get_possible_last_child(prev, adjacent_only);
add_to_map(possible_last_child, result);
if (adjacent_only && has_definite_elements(possible_last_child)) {
return result;
}
}
}
if (!prev || !adjacent_only) {
let parent: INode = node;
/** @type {import('../nodes/interfaces.js').INode} */
let parent = node;
let skip_each_for_last_child = node.type === 'ElseBlock';
while (
(parent = parent.parent) &&
@ -612,7 +607,6 @@ function get_possible_element_siblings(
) {
const possible_siblings = get_possible_element_siblings(parent, adjacent_only);
add_to_map(possible_siblings, result);
if (parent.type === 'EachBlock') {
// first child of each block can select the last child of each block as previous sibling
if (skip_each_for_last_child) {
@ -624,30 +618,31 @@ function get_possible_element_siblings(
skip_each_for_last_child = true;
parent = parent.parent;
}
if (adjacent_only && has_definite_elements(possible_siblings)) {
break;
}
}
}
return result;
}
function get_possible_last_child(
block: EachBlock | IfBlock | AwaitBlock,
adjacent_only: boolean
): Map<Element, NodeExist> {
const result: Map<Element, NodeExist> = new Map();
/**
* @param {import('../nodes/EachBlock.js').default | import('../nodes/IfBlock.js').default | import('../nodes/AwaitBlock.js').default} block
* @param {boolean} adjacent_only
* @returns {Map<import('../nodes/Element.js').default, NodeExistsValue>}
*/
function get_possible_last_child(block, adjacent_only) {
/** @typedef {Map<import('../nodes/Element.js').default, NodeExistsValue>} NodeMap */
/** @type {NodeMap} */
const result = new Map();
if (block.type === 'EachBlock') {
const each_result: Map<Element, NodeExist> = loop_child(block.children, adjacent_only);
const else_result: Map<Element, NodeExist> = block.else
? loop_child(block.else.children, adjacent_only)
: new Map();
/** @type {NodeMap} */
const each_result = loop_child(block.children, adjacent_only);
/** @type {NodeMap} */
const else_result = block.else ? loop_child(block.else.children, adjacent_only) : new Map();
const not_exhaustive = !has_definite_elements(else_result);
if (not_exhaustive) {
mark_as_probably(each_result);
mark_as_probably(else_result);
@ -655,51 +650,50 @@ function get_possible_last_child(
add_to_map(each_result, result);
add_to_map(else_result, result);
} else if (block.type === 'IfBlock') {
const if_result: Map<Element, NodeExist> = loop_child(block.children, adjacent_only);
const else_result: Map<Element, NodeExist> = block.else
? loop_child(block.else.children, adjacent_only)
: new Map();
/** @type {NodeMap} */
const if_result = loop_child(block.children, adjacent_only);
/** @type {NodeMap} */
const else_result = block.else ? loop_child(block.else.children, adjacent_only) : new Map();
const not_exhaustive = !has_definite_elements(if_result) || !has_definite_elements(else_result);
if (not_exhaustive) {
mark_as_probably(if_result);
mark_as_probably(else_result);
}
add_to_map(if_result, result);
add_to_map(else_result, result);
} else if (block.type === 'AwaitBlock') {
const pending_result: Map<Element, NodeExist> = block.pending
/** @type {NodeMap} */
const pending_result = block.pending
? loop_child(block.pending.children, adjacent_only)
: new Map();
const then_result: Map<Element, NodeExist> = block.then
? loop_child(block.then.children, adjacent_only)
: new Map();
const catch_result: Map<Element, NodeExist> = block.catch
? loop_child(block.catch.children, adjacent_only)
: new Map();
/** @type {NodeMap} */
const then_result = block.then ? loop_child(block.then.children, adjacent_only) : new Map();
/** @type {NodeMap} */
const catch_result = block.catch ? loop_child(block.catch.children, adjacent_only) : new Map();
const not_exhaustive =
!has_definite_elements(pending_result) ||
!has_definite_elements(then_result) ||
!has_definite_elements(catch_result);
if (not_exhaustive) {
mark_as_probably(pending_result);
mark_as_probably(then_result);
mark_as_probably(catch_result);
}
add_to_map(pending_result, result);
add_to_map(then_result, result);
add_to_map(catch_result, result);
}
return result;
}
function has_definite_elements(result: Map<Element, NodeExist>): boolean {
/**
* @param {Map<import('../nodes/Element.js').default, NodeExistsValue>} result
* @returns {boolean}
*/
function has_definite_elements(result) {
if (result.size === 0) return false;
for (const exist of result.values()) {
if (exist === NodeExist.Definitely) {
@ -709,25 +703,41 @@ function has_definite_elements(result: Map<Element, NodeExist>): boolean {
return false;
}
function add_to_map(from: Map<Element, NodeExist>, to: Map<Element, NodeExist>) {
/**
* @param {Map<import('../nodes/Element.js').default, NodeExistsValue>} from
* @param {Map<import('../nodes/Element.js').default, NodeExistsValue>} to
* @returns {void}
*/
function add_to_map(from, to) {
from.forEach((exist, element) => {
to.set(element, higher_existence(exist, to.get(element)));
});
}
function higher_existence(exist1: NodeExist | null, exist2: NodeExist | null): NodeExist {
/**
* @param {NodeExistsValue | null} exist1
* @param {NodeExistsValue | null} exist2
* @returns {NodeExistsValue}
*/
function higher_existence(exist1, exist2) {
if (exist1 === undefined || exist2 === undefined) return exist1 || exist2;
return exist1 > exist2 ? exist1 : exist2;
}
function mark_as_probably(result: Map<Element, NodeExist>) {
/** @param {Map<import('../nodes/Element.js').default, NodeExistsValue>} result */
function mark_as_probably(result) {
for (const key of result.keys()) {
result.set(key, NodeExist.Probably);
}
}
function loop_child(children: INode[], adjacent_only: boolean) {
const result: Map<Element, NodeExist> = new Map();
/**
* @param {import('../nodes/interfaces.js').INode[]} children
* @param {boolean} adjacent_only
*/
function loop_child(children, adjacent_only) {
/** @type {Map<import('../nodes/Element.js').default, NodeExistsValue>} */
const result = new Map();
for (let i = children.length - 1; i >= 0; i--) {
const child = children[i];
if (child.type === 'Element') {
@ -751,37 +761,48 @@ function loop_child(children: INode[], adjacent_only: boolean) {
}
class Block {
host: boolean;
root: boolean;
combinator: CssNode;
selectors: CssNode[];
start: number;
end: number;
should_encapsulate: boolean;
constructor(combinator: CssNode) {
/** @type {boolean} */
host;
/** @type {boolean} */
root;
/** @type {import('./private.js').CssNode} */
combinator;
/** @type {import('./private.js').CssNode[]} */
selectors;
/** @type {number} */
start;
/** @type {number} */
end;
/** @type {boolean} */
should_encapsulate;
/** @param {import('./private.js').CssNode} combinator */
constructor(combinator) {
this.combinator = combinator;
this.host = false;
this.root = false;
this.selectors = [];
this.start = null;
this.end = null;
this.should_encapsulate = false;
}
add(selector: CssNode) {
/** @param {import('./private.js').CssNode} selector */
add(selector) {
if (this.selectors.length === 0) {
this.start = selector.start;
this.host = selector.type === 'PseudoClassSelector' && selector.name === 'host';
}
this.root = this.root || (selector.type === 'PseudoClassSelector' && selector.name === 'root');
this.selectors.push(selector);
this.end = selector.end;
}
get global() {
return (
this.selectors.length >= 1 &&
@ -795,12 +816,12 @@ class Block {
}
}
function group_selectors(selector: CssNode) {
let block: Block = new Block(null);
/** @param {import('./private.js').CssNode} selector */
function group_selectors(selector) {
/** @type {Block} */
let block = new Block(null);
const blocks = [block];
selector.children.forEach((child: CssNode) => {
selector.children.forEach((child) => {
if (child.type === 'WhiteSpace' || child.type === 'Combinator') {
block = new Block(child);
blocks.push(block);
@ -808,6 +829,5 @@ function group_selectors(selector: CssNode) {
block.add(child);
}
});
return blocks;
}

@ -1,34 +1,40 @@
import MagicString from 'magic-string';
import { walk } from 'estree-walker';
import Selector from './Selector';
import Element from '../nodes/Element';
import { Ast, CssHashGetter } from '../../interfaces';
import Component from '../Component';
import { CssNode } from './interfaces';
import hash from '../utils/hash';
import compiler_warnings from '../compiler_warnings';
import { extract_ignores_above_position } from '../../utils/extract_svelte_ignore';
import { push_array } from '../../utils/push_array';
import { regex_only_whitespaces, regex_whitespace } from '../../utils/patterns';
import Selector from './Selector.js';
import hash from '../utils/hash.js';
import compiler_warnings from '../compiler_warnings.js';
import { extract_ignores_above_position } from '../../utils/extract_svelte_ignore.js';
import { push_array } from '../../utils/push_array.js';
import { regex_only_whitespaces, regex_whitespace } from '../../utils/patterns.js';
const regex_css_browser_prefix = /^-((webkit)|(moz)|(o)|(ms))-/;
function remove_css_prefix(name: string): string {
/**
* @param {string} name
* @returns {string}
*/
function remove_css_prefix(name) {
return name.replace(regex_css_browser_prefix, '');
}
const is_keyframes_node = (node: CssNode) => remove_css_prefix(node.name) === 'keyframes';
const at_rule_has_declaration = ({ block }: CssNode): true =>
block && block.children && block.children.find((node: CssNode) => node.type === 'Declaration');
function minify_declarations(
code: MagicString,
start: number,
declarations: Declaration[]
): number {
/** @param {import('./private.js').CssNode} node */
const is_keyframes_node = (node) => remove_css_prefix(node.name) === 'keyframes';
/**
* @param {import('./private.js').CssNode} param
* @returns {true}
*/
const at_rule_has_declaration = ({ block }) =>
block && block.children && block.children.find((node) => node.type === 'Declaration');
/**
* @param {import('magic-string').default} code
* @param {number} start
* @param {Declaration[]} declarations
* @returns {number}
*/
function minify_declarations(code, start, declarations) {
let c = start;
declarations.forEach((declaration, i) => {
const separator = i > 0 ? ';' : '';
if (declaration.node.start - c > separator.length) {
@ -37,107 +43,122 @@ function minify_declarations(
declaration.minify(code);
c = declaration.node.end;
});
return c;
}
class Rule {
selectors: Selector[];
declarations: Declaration[];
node: CssNode;
parent: Atrule;
/** @type {import('./Selector.js').default[]} */
selectors;
/** @type {Declaration[]} */
declarations;
/** @type {import('./private.js').CssNode} */
node;
/** @type {Atrule} */
parent;
constructor(node: CssNode, stylesheet, parent?: Atrule) {
/**
* @param {import('./private.js').CssNode} node
* @param {any} stylesheet
* @param {Atrule} [parent]
*/
constructor(node, stylesheet, parent) {
this.node = node;
this.parent = parent;
this.selectors = node.prelude.children.map((node: CssNode) => new Selector(node, stylesheet));
this.declarations = node.block.children.map((node: CssNode) => new Declaration(node));
this.selectors = node.prelude.children.map((node) => new Selector(node, stylesheet));
this.declarations = node.block.children.map((node) => new Declaration(node));
}
apply(node: Element) {
/** @param {import('../nodes/Element.js').default} node */
apply(node) {
this.selectors.forEach((selector) => selector.apply(node)); // TODO move the logic in here?
}
is_used(dev: boolean) {
/** @param {boolean} dev */
is_used(dev) {
if (this.parent && this.parent.node.type === 'Atrule' && is_keyframes_node(this.parent.node))
return true;
if (this.declarations.length === 0) return dev;
return this.selectors.some((s) => s.used);
}
minify(code: MagicString, _dev: boolean) {
/**
* @param {import('magic-string').default} code
* @param {boolean} _dev
*/
minify(code, _dev) {
let c = this.node.start;
let started = false;
this.selectors.forEach((selector) => {
if (selector.used) {
const separator = started ? ',' : '';
if (selector.node.start - c > separator.length) {
code.update(c, selector.node.start, separator);
}
selector.minify(code);
c = selector.node.end;
started = true;
}
});
code.remove(c, this.node.block.start);
c = this.node.block.start + 1;
c = minify_declarations(code, c, this.declarations);
code.remove(c, this.node.block.end - 1);
}
transform(
code: MagicString,
id: string,
keyframes: Map<string, string>,
max_amount_class_specificity_increased: number
) {
/**
* @param {import('magic-string').default} code
* @param {string} id
* @param {Map<string, string>} keyframes
* @param {number} max_amount_class_specificity_increased
*/
transform(code, id, keyframes, max_amount_class_specificity_increased) {
if (this.parent && this.parent.node.type === 'Atrule' && is_keyframes_node(this.parent.node))
return true;
const attr = `.${id}`;
this.selectors.forEach((selector) =>
selector.transform(code, attr, max_amount_class_specificity_increased)
);
this.declarations.forEach((declaration) => declaration.transform(code, keyframes));
}
validate(component: Component) {
/** @param {import('../Component.js').default} component */
validate(component) {
this.selectors.forEach((selector) => {
selector.validate(component);
});
}
warn_on_unused_selector(handler: (selector: Selector) => void) {
/** @param {(selector: import('./Selector.js').default) => void} handler */
warn_on_unused_selector(handler) {
this.selectors.forEach((selector) => {
if (!selector.used) handler(selector);
});
}
get_max_amount_class_specificity_increased() {
return Math.max(
...this.selectors.map((selector) => selector.get_amount_class_specificity_increased())
);
}
}
class Declaration {
node: CssNode;
/** @type {import('./private.js').CssNode} */
node;
constructor(node: CssNode) {
/** @param {import('./private.js').CssNode} node */
constructor(node) {
this.node = node;
}
transform(code: MagicString, keyframes: Map<string, string>) {
/**
* @param {import('magic-string').default} code
* @param {Map<string, string>} keyframes
*/
transform(code, keyframes) {
const property = this.node.property && remove_css_prefix(this.node.property.toLowerCase());
if (property === 'animation' || property === 'animation-name') {
this.node.value.children.forEach((block: CssNode) => {
this.node.value.children.forEach((block) => {
if (block.type === 'Identifier') {
const name = block.name;
if (keyframes.has(name)) {
@ -148,37 +169,40 @@ class Declaration {
}
}
minify(code: MagicString) {
/** @param {import('magic-string').default} code */
minify(code) {
if (!this.node.property) return; // @apply, and possibly other weird cases?
const c = this.node.start + this.node.property.length;
const first = this.node.value.children ? this.node.value.children[0] : this.node.value;
// Don't minify whitespace in custom properties, since some browsers (Chromium < 99)
// treat --foo: ; and --foo:; differently
if (first.type === 'Raw' && regex_only_whitespaces.test(first.value)) return;
let start = first.start;
while (regex_whitespace.test(code.original[start])) start += 1;
if (start - c > 1) {
code.update(c, start, ':');
}
}
}
class Atrule {
node: CssNode;
children: Array<Atrule | Rule>;
declarations: Declaration[];
/** @type {import('./private.js').CssNode} */
node;
/** @type {Array<Atrule | Rule>} */
children;
constructor(node: CssNode) {
/** @type {Declaration[]} */
declarations;
/** @param {import('./private.js').CssNode} node */
constructor(node) {
this.node = node;
this.children = [];
this.declarations = [];
}
apply(node: Element) {
/** @param {import('../nodes/Element.js').default} node */
apply(node) {
if (
this.node.name === 'container' ||
this.node.name === 'media' ||
@ -189,7 +213,7 @@ class Atrule {
child.apply(node);
});
} else if (is_keyframes_node(this.node)) {
this.children.forEach((rule: Rule) => {
this.children.forEach((/** @type {Rule} */ rule) => {
rule.selectors.forEach((selector) => {
selector.used = true;
});
@ -197,26 +221,29 @@ class Atrule {
}
}
is_used(_dev: boolean) {
/** @param {boolean} _dev */
is_used(_dev) {
return true; // TODO
}
minify(code: MagicString, dev: boolean) {
/**
* @param {import('magic-string').default} code
* @param {boolean} dev
*/
minify(code, dev) {
if (this.node.name === 'media') {
const expression_char = code.original[this.node.prelude.start];
let c = this.node.start + (expression_char === '(' ? 6 : 7);
if (this.node.prelude.start > c) code.remove(c, this.node.prelude.start);
this.node.prelude.children.forEach((query: CssNode) => {
this.node.prelude.children.forEach((query) => {
// TODO minify queries
c = query.end;
});
code.remove(c, this.node.block.start);
} else if (this.node.name === 'supports') {
let c = this.node.start + 9;
if (this.node.prelude.start - c > 1) code.update(c, this.node.prelude.start, ' ');
this.node.prelude.children.forEach((query: CssNode) => {
this.node.prelude.children.forEach((query) => {
// TODO minify queries
c = query.end;
});
@ -231,9 +258,7 @@ class Atrule {
code.remove(c, this.node.block.start);
}
}
// TODO other atrules
if (this.node.block) {
let c = this.node.block.start + 1;
if (this.declarations.length) {
@ -241,7 +266,6 @@ class Atrule {
// if the atrule has children, leave the last declaration semicolon alone
if (this.children.length) c++;
}
this.children.forEach((child) => {
if (child.is_used(dev)) {
code.remove(c, child.node.start);
@ -249,23 +273,23 @@ class Atrule {
c = child.node.end;
}
});
code.remove(c, this.node.block.end - 1);
}
}
transform(
code: MagicString,
id: string,
keyframes: Map<string, string>,
max_amount_class_specificity_increased: number
) {
/**
* @param {import('magic-string').default} code
* @param {string} id
* @param {Map<string, string>} keyframes
* @param {number} max_amount_class_specificity_increased
*/
transform(code, id, keyframes, max_amount_class_specificity_increased) {
if (is_keyframes_node(this.node)) {
this.node.prelude.children.forEach(({ type, name, start, end }: CssNode) => {
this.node.prelude.children.forEach(({ type, name, start, end }) => {
if (type === 'Identifier') {
if (name.startsWith('-global-')) {
code.remove(start, start + 8);
this.children.forEach((rule: Rule) => {
this.children.forEach((/** @type {Rule} */ rule) => {
rule.selectors.forEach((selector) => {
selector.used = true;
});
@ -276,26 +300,25 @@ class Atrule {
}
});
}
this.children.forEach((child) => {
child.transform(code, id, keyframes, max_amount_class_specificity_increased);
});
}
validate(component: Component) {
/** @param {import('../Component.js').default} component */
validate(component) {
this.children.forEach((child) => {
child.validate(component);
});
}
warn_on_unused_selector(handler: (selector: Selector) => void) {
/** @param {(selector: import('./Selector.js').default) => void} handler */
warn_on_unused_selector(handler) {
if (this.node.name !== 'media') return;
this.children.forEach((child) => {
child.warn_on_unused_selector(handler);
});
}
get_max_amount_class_specificity_increased() {
return Math.max(
...this.children.map((rule) => rule.get_max_amount_class_specificity_increased())
@ -303,44 +326,53 @@ class Atrule {
}
}
const get_default_css_hash: CssHashGetter = ({ css, hash }) => {
/** @param {any} params */
const get_default_css_hash = ({ css, hash }) => {
return `svelte-${hash(css)}`;
};
export default class Stylesheet {
source: string;
ast: Ast;
filename: string;
dev: boolean;
has_styles: boolean;
id: string;
children: Array<Rule | Atrule> = [];
keyframes: Map<string, string> = new Map();
nodes_with_css_class: Set<CssNode> = new Set();
constructor({
source,
ast,
component_name,
filename,
dev,
get_css_hash = get_default_css_hash
}: {
source: string;
ast: Ast;
filename: string | undefined;
component_name: string | undefined;
dev: boolean;
get_css_hash: CssHashGetter;
}) {
/** @type {string} */
source;
/** @type {import('../../interfaces.js').Ast} */
ast;
/** @type {string} */
filename;
/** @type {boolean} */
dev;
/** @type {boolean} */
has_styles;
/** @type {string} */
id;
/** @type {Array<Rule | Atrule>} */
children = [];
/** @type {Map<string, string>} */
keyframes = new Map();
/** @type {Set<import('./private.js').CssNode>} */
nodes_with_css_class = new Set();
/**
* @param {{
* source: string;
* ast: import('../../interfaces.js').Ast;
* filename: string | undefined;
* component_name: string | undefined;
* dev: boolean;
* get_css_hash: import('../../interfaces.js').CssHashGetter;
* }} params
*/
constructor({ source, ast, component_name, filename, dev, get_css_hash = get_default_css_hash }) {
this.source = source;
this.ast = ast;
this.filename = filename;
this.dev = dev;
if (ast.css && ast.css.children.length) {
this.id = get_css_hash({
filename,
@ -348,27 +380,26 @@ export default class Stylesheet {
css: ast.css.content.styles,
hash
});
this.has_styles = true;
const stack: Atrule[] = [];
/** @type {Atrule[]} */
const stack = [];
let depth = 0;
let current_atrule: Atrule = null;
walk(ast.css as any, {
enter: (node: any) => {
/** @type {Atrule} */
let current_atrule = null;
walk(/** @type {any} */ (ast.css), {
enter: (/** @type {any} */ node) => {
if (node.type === 'Atrule') {
const atrule = new Atrule(node);
stack.push(atrule);
if (current_atrule) {
current_atrule.children.push(atrule);
} else if (depth <= 1) {
this.children.push(atrule);
}
if (is_keyframes_node(node)) {
node.prelude.children.forEach((expression: CssNode) => {
node.prelude.children.forEach((expression) => {
if (expression.type === 'Identifier' && !expression.name.startsWith('-global-')) {
this.keyframes.set(expression.name, `${this.id}-${expression.name}`);
}
@ -379,29 +410,23 @@ export default class Stylesheet {
.map((node) => new Declaration(node));
push_array(atrule.declarations, at_rule_declarations);
}
current_atrule = atrule;
}
if (node.type === 'Rule') {
const rule = new Rule(node, this, current_atrule);
if (current_atrule) {
current_atrule.children.push(rule);
} else if (depth <= 1) {
this.children.push(rule);
}
}
depth += 1;
},
leave: (node: any) => {
leave: (/** @type {any} */ node) => {
if (node.type === 'Atrule') {
stack.pop();
current_atrule = stack[stack.length - 1];
}
depth -= 1;
}
});
@ -410,42 +435,38 @@ export default class Stylesheet {
}
}
apply(node: Element) {
/** @param {import('../nodes/Element.js').default} node */
apply(node) {
if (!this.has_styles) return;
for (let i = 0; i < this.children.length; i += 1) {
const child = this.children[i];
child.apply(node);
}
}
reify() {
this.nodes_with_css_class.forEach((node: Element) => {
this.nodes_with_css_class.forEach((node) => {
node.add_css_class();
});
}
render(file: string) {
/** @param {string} file */
render(file) {
if (!this.has_styles) {
return { code: null, map: null };
}
const code = new MagicString(this.source);
walk(this.ast.css as any, {
enter: (node: any) => {
walk(/** @type {any} */ (this.ast.css), {
enter: (/** @type {any} */ node) => {
code.addSourcemapLocation(node.start);
code.addSourcemapLocation(node.end);
}
});
const max = Math.max(
...this.children.map((rule) => rule.get_max_amount_class_specificity_increased())
);
this.children.forEach((child: Atrule | Rule) => {
this.children.forEach((child) => {
child.transform(code, this.id, this.keyframes, max);
});
let c = 0;
this.children.forEach((child) => {
if (child.is_used(this.dev)) {
@ -454,9 +475,7 @@ export default class Stylesheet {
c = child.node.end;
}
});
code.remove(c, this.source.length);
return {
code: code.toString(),
map: code.generateMap({
@ -467,19 +486,21 @@ export default class Stylesheet {
};
}
validate(component: Component) {
/** @param {import('../Component.js').default} component */
validate(component) {
this.children.forEach((child) => {
child.validate(component);
});
}
warn_on_unused_selectors(component: Component) {
/** @param {import('../Component.js').default} component */
warn_on_unused_selectors(component) {
const ignores = !this.ast.css
? []
: extract_ignores_above_position(this.ast.css.start, this.ast.html.children);
component.push_ignores(ignores);
this.children.forEach((child) => {
child.warn_on_unused_selector((selector: Selector) => {
child.warn_on_unused_selector((selector) => {
component.warn(
selector.node,
compiler_warnings.css_unused_selector(

@ -1,8 +1,10 @@
import { Node } from 'estree';
export const UNKNOWN = {};
export function gather_possible_values(node: Node, set: Set<string | {}>) {
/**
* @param {import("estree").Node} node
* @param {Set<string | {}>} set
*/
export function gather_possible_values(node, set) {
if (node.type === 'Literal') {
set.add(node.value);
} else if (node.type === 'ConditionalExpression') {

@ -1,12 +1,11 @@
import Stats from '../Stats';
import parse from '../parse/index';
import render_dom from './render_dom/index';
import render_ssr from './render_ssr/index';
import { CompileOptions, Warning } from '../interfaces';
import Component from './Component';
import fuzzymatch from '../utils/fuzzymatch';
import get_name_from_filename from './utils/get_name_from_filename';
import { valid_namespaces } from '../utils/namespaces';
import Stats from '../Stats.js';
import parse from '../parse/index.js';
import render_dom from './render_dom/index.js';
import render_ssr from './render_ssr/index.js';
import Component from './Component.js';
import fuzzymatch from '../utils/fuzzymatch.js';
import get_name_from_filename from './utils/get_name_from_filename.js';
import { valid_namespaces } from '../utils/namespaces.js';
const valid_options = [
'format',
@ -34,29 +33,29 @@ const valid_options = [
'preserveWhitespace',
'cssHash'
];
const valid_css_values = [true, false, 'injected', 'external', 'none'];
const regex_valid_identifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/;
const regex_starts_with_lowercase_character = /^[a-z]/;
function validate_options(options: CompileOptions, warnings: Warning[]) {
/**
* @param {import('../interfaces.js').CompileOptions} options
* @param {import('../interfaces.js').Warning[]} warnings
*/
function validate_options(options, warnings) {
const { name, filename, loopGuardTimeout, dev, namespace, css } = options;
Object.keys(options).forEach((key) => {
if (!valid_options.includes(key)) {
const match = fuzzymatch(key, valid_options);
let message = `Unrecognized option '${key}'`;
if (match) message += ` (did you mean '${match}'?)`;
throw new Error(message);
Object.keys(options).forEach(
/** @param {any} key */ (key) => {
if (!valid_options.includes(key)) {
const match = fuzzymatch(key, valid_options);
let message = `Unrecognized option '${key}'`;
if (match) message += ` (did you mean '${match}'?)`;
throw new Error(message);
}
}
});
);
if (name && !regex_valid_identifier.test(name)) {
throw new Error(`options.name must be a valid identifier (got '${name}')`);
}
if (name && regex_starts_with_lowercase_character.test(name)) {
const message = 'options.name should be capitalised';
warnings.push({
@ -66,7 +65,6 @@ function validate_options(options: CompileOptions, warnings: Warning[]) {
toString: () => message
});
}
if (loopGuardTimeout && !dev) {
const message = 'options.loopGuardTimeout is for options.dev = true only';
warnings.push({
@ -76,13 +74,11 @@ function validate_options(options: CompileOptions, warnings: Warning[]) {
toString: () => message
});
}
if (valid_css_values.indexOf(css) === -1) {
throw new Error(
`options.css must be true, false, 'injected', 'external', or 'none' (got '${css}')`
);
}
if (css === true || css === false) {
options.css = css === true ? 'injected' : 'external';
// possibly show this warning once we decided how Svelte 4 looks like
@ -95,7 +91,6 @@ function validate_options(options: CompileOptions, warnings: Warning[]) {
// });
// }
}
if (namespace && valid_namespaces.indexOf(namespace) === -1) {
const match = fuzzymatch(namespace, valid_namespaces);
if (match) {
@ -106,21 +101,21 @@ function validate_options(options: CompileOptions, warnings: Warning[]) {
}
}
export default function compile(source: string, options: CompileOptions = {}) {
/**
* @param {string} source
* @param {import('../interfaces.js').CompileOptions} options
*/
export default function compile(source, options = {}) {
options = Object.assign(
{ generate: 'dom', dev: false, enableSourcemap: true, css: 'injected' },
options
);
const stats = new Stats();
const warnings = [];
validate_options(options, warnings);
stats.start('parse');
const ast = parse(source, options);
stats.stop('parse');
stats.start('create component');
const component = new Component(
ast,
@ -131,13 +126,11 @@ export default function compile(source: string, options: CompileOptions = {}) {
warnings
);
stats.stop('create component');
const result =
options.generate === false
? null
: options.generate === 'ssr'
? render_ssr(component, options)
: render_dom(component, options);
return component.generate(result);
}

@ -0,0 +1,2 @@
// This file is automatically generated
export default new Set(["HtmlTag","HtmlTagHydration","ResizeObserverSingleton","SvelteComponent","SvelteComponentDev","SvelteComponentTyped","SvelteElement","action_destroyer","add_attribute","add_classes","add_flush_callback","add_iframe_resize_listener","add_location","add_render_callback","add_styles","add_transform","afterUpdate","append","append_dev","append_empty_stylesheet","append_hydration","append_hydration_dev","append_styles","assign","attr","attr_dev","attribute_to_object","beforeUpdate","bind","binding_callbacks","blank_object","bubble","check_outros","children","claim_comment","claim_component","claim_element","claim_html_tag","claim_space","claim_svg_element","claim_text","clear_loops","comment","component_subscribe","compute_rest_props","compute_slots","construct_svelte_component","construct_svelte_component_dev","contenteditable_truthy_values","createEventDispatcher","create_animation","create_bidirectional_transition","create_component","create_custom_element","create_in_transition","create_out_transition","create_slot","create_ssr_component","current_component","custom_event","dataset_dev","debug","destroy_block","destroy_component","destroy_each","detach","detach_after_dev","detach_before_dev","detach_between_dev","detach_dev","dirty_components","dispatch_dev","each","element","element_is","empty","end_hydrating","escape","escape_attribute_value","escape_object","exclude_internal_props","fix_and_destroy_block","fix_and_outro_and_destroy_block","fix_position","flush","flush_render_callbacks","getAllContexts","getContext","get_all_dirty_from_scope","get_binding_group_value","get_current_component","get_custom_elements_slots","get_root_for_style","get_slot_changes","get_spread_object","get_spread_update","get_store_value","get_svelte_dataset","globals","group_outros","handle_promise","hasContext","has_prop","head_selector","identity","init","init_binding_group","init_binding_group_dynamic","insert","insert_dev","insert_hydration","insert_hydration_dev","intros","invalid_attribute_name_character","is_client","is_crossorigin","is_empty","is_function","is_promise","is_void","listen","listen_dev","loop","loop_guard","merge_ssr_styles","missing_component","mount_component","noop","not_equal","now","null_to_empty","object_without_properties","onDestroy","onMount","once","outro_and_destroy_block","prevent_default","prop_dev","query_selector_all","raf","resize_observer_border_box","resize_observer_content_box","resize_observer_device_pixel_content_box","run","run_all","safe_not_equal","schedule_update","select_multiple_value","select_option","select_options","select_value","self","setContext","set_attributes","set_current_component","set_custom_element_data","set_custom_element_data_map","set_data","set_data_contenteditable","set_data_contenteditable_dev","set_data_dev","set_data_maybe_contenteditable","set_data_maybe_contenteditable_dev","set_dynamic_element_data","set_input_type","set_input_value","set_now","set_raf","set_store_value","set_style","set_svg_attributes","space","split_css_unit","spread","src_url_equal","start_hydrating","stop_immediate_propagation","stop_propagation","subscribe","svg_element","text","tick","time_ranges_to_array","to_number","toggle_class","transition_in","transition_out","trusted","update_await_block_branch","update_keyed_each","update_slot","update_slot_base","validate_component","validate_dynamic_element","validate_each_argument","validate_each_keys","validate_slots","validate_store","validate_void_dynamic_element","xlink_attr"]);

@ -1,31 +1,36 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { Directive } from '../../interfaces';
import Node from './shared/Node.js';
import Expression from './shared/Expression.js';
/** @extends Node<'Action'> */
export default class Action extends Node {
type: 'Action';
name: string;
expression: Expression;
uses_context: boolean;
template_scope: TemplateScope;
/** @type {string} */
name;
constructor(component: Component, parent: Node, scope: TemplateScope, info: Directive) {
super(component, parent, scope, info);
/** @type {import('./shared/Expression.js').default} */
expression;
/** @type {boolean} */
uses_context;
/** @type {import('./shared/TemplateScope.js').default} */
template_scope;
/**
* @param {import('../Component.js').default} component *
* @param {import('./shared/Node.js').default} parent *
* @param {import('./shared/TemplateScope.js').default} scope *
* @param {import('../../interfaces.js').Directive} info undefined
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
const object = info.name.split('.')[0];
component.warn_if_undefined(object, info, scope);
this.name = info.name;
component.add_reference(this as any, object);
component.add_reference(/** @type {any} */ (this), object);
this.expression = info.expression
? new Expression(component, this, scope, info.expression)
: null;
this.template_scope = scope;
this.uses_context = this.expression && this.expression.uses_context;
}
}

@ -1,44 +1,41 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import Element from './Element';
import EachBlock from './EachBlock';
import compiler_errors from '../compiler_errors';
import Node from './shared/Node.js';
import Expression from './shared/Expression.js';
import compiler_errors from '../compiler_errors.js';
/** @extends Node<'Animation'> */
export default class Animation extends Node {
type: 'Animation';
name: string;
expression: Expression;
constructor(component: Component, parent: Element, scope: TemplateScope, info: TemplateNode) {
/** @type {string} */
name;
/** @type {import('./shared/Expression.js').default} */
expression;
/**
* @param {import('../Component.js').default} component *
* @param {import('./Element.js').default} parent *
* @param {import('./shared/TemplateScope.js').default} scope *
* @param {import('../../interfaces.js').TemplateNode} info undefined
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
component.warn_if_undefined(info.name, info, scope);
this.name = info.name;
component.add_reference(this as any, info.name.split('.')[0]);
component.add_reference(/** @type {any} */ (this), info.name.split('.')[0]);
if (parent.animation) {
component.error(this, compiler_errors.duplicate_animation);
return;
}
const block = parent.parent;
if (!block || block.type !== 'EachBlock') {
// TODO can we relax the 'immediate child' rule?
component.error(this, compiler_errors.invalid_animation_immediate);
return;
}
if (!block.key) {
component.error(this, compiler_errors.invalid_animation_key);
return;
}
(block as EachBlock).has_animation = true;
/** @type {import('./EachBlock.js').default} */ (block).has_animation = true;
this.expression = info.expression
? new Expression(component, this, scope, info.expression, true)
: null;

@ -1,118 +1,125 @@
import { string_literal } from '../utils/stringify';
import add_to_set from '../utils/add_to_set';
import Component from '../Component';
import Node from './shared/Node';
import Element from './Element';
import Text from './Text';
import Expression from './shared/Expression';
import TemplateScope from './shared/TemplateScope';
import { string_literal } from '../utils/stringify.js';
import add_to_set from '../utils/add_to_set.js';
import Node from './shared/Node.js';
import Expression from './shared/Expression.js';
import { x } from 'code-red';
import { TemplateNode } from '../../interfaces';
/** @extends Node<'Attribute' | 'Spread', import('./Element.js').default> */
export default class Attribute extends Node {
type: 'Attribute' | 'Spread';
start: number;
end: number;
scope: TemplateScope;
/** @type {import('./shared/TemplateScope.js').default} */
scope;
component: Component;
parent: Element;
name: string;
is_spread: boolean;
is_true: boolean;
is_static: boolean;
expression?: Expression;
chunks: Array<Text | Expression>;
dependencies: Set<string>;
/** @type {string} */
name;
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
/** @type {boolean} */
is_spread;
/** @type {boolean} */
is_true;
/** @type {boolean} */
is_static;
/** @type {import('./shared/Expression.js').default} */
expression;
/** @type {Array<import('./Text.js').default | import('./shared/Expression.js').default>} */
chunks;
/** @type {Set<string>} */
dependencies;
/**
* @param {import('../Component.js').default} component
* @param {import('./shared/Node.js').default} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').TemplateNode} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.scope = scope;
if (info.type === 'Spread') {
this.name = null;
this.is_spread = true;
this.is_true = false;
this.expression = new Expression(component, this, scope, info.expression);
this.dependencies = this.expression.dependencies;
this.chunks = null;
this.is_static = false;
} else {
this.name = info.name;
this.is_true = info.value === true;
this.is_static = true;
this.dependencies = new Set();
this.chunks = this.is_true
? []
: info.value.map((node) => {
if (node.type === 'Text') return node;
this.is_static = false;
const expression = new Expression(component, this, scope, node.expression);
add_to_set(this.dependencies, expression.dependencies);
return expression;
});
: info.value.map(
/** @param {any} node */ (node) => {
if (node.type === 'Text') return node;
this.is_static = false;
const expression = new Expression(component, this, scope, node.expression);
add_to_set(this.dependencies, expression.dependencies);
return expression;
}
);
}
if (this.dependencies.size > 0) {
parent.cannot_use_innerhtml();
parent.not_static_content();
}
}
get_dependencies() {
if (this.is_spread) return this.expression.dynamic_dependencies();
const dependencies: Set<string> = new Set();
this.chunks.forEach((chunk) => {
if (chunk.type === 'Expression') {
add_to_set(dependencies, chunk.dynamic_dependencies());
/** @type {Set<string>} */
const dependencies = new Set();
this.chunks.forEach(
/** @param {any} chunk */ (chunk) => {
if (chunk.type === 'Expression') {
add_to_set(dependencies, chunk.dynamic_dependencies());
}
}
});
);
return Array.from(dependencies);
}
/** @param {any} block */
get_value(block) {
if (this.is_true) return x`true`;
if (this.chunks.length === 0) return x`""`;
if (this.chunks.length === 1) {
return this.chunks[0].type === 'Text'
? string_literal((this.chunks[0] as Text).data)
: (this.chunks[0] as Expression).manipulate(block);
? string_literal(/** @type {import('./Text.js').default} */ (this.chunks[0]).data)
: /** @type {import('./shared/Expression.js').default} */ (this.chunks[0]).manipulate(
block
);
}
let expression = this.chunks
.map((chunk) =>
chunk.type === 'Text' ? string_literal(chunk.data) : chunk.manipulate(block)
.map(
/** @param {any} chunk */ (chunk) =>
chunk.type === 'Text' ? string_literal(chunk.data) : chunk.manipulate(block)
)
.reduce((lhs, rhs) => x`${lhs} + ${rhs}`);
.reduce(
/**
* @param {any} lhs
* @param {any} rhs
*/ (lhs, rhs) => x`${lhs} + ${rhs}`
);
if (this.chunks[0].type !== 'Text') {
expression = x`"" + ${expression}`;
}
return expression;
}
get_static_value() {
if (!this.is_static) return null;
return this.is_true
? true
: this.chunks[0]
? // method should be called only when `is_static = true`
(this.chunks[0] as Text).data
/** @type {import('./Text.js').default} */ (this.chunks[0]).data
: '';
}
should_cache() {
return this.is_static
? false

@ -1,40 +1,52 @@
import Node from './shared/Node';
import PendingBlock from './PendingBlock';
import ThenBlock from './ThenBlock';
import CatchBlock from './CatchBlock';
import Expression from './shared/Expression';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import { Context, unpack_destructuring } from './shared/Context';
import { Node as ESTreeNode } from 'estree';
import Node from './shared/Node.js';
import PendingBlock from './PendingBlock.js';
import ThenBlock from './ThenBlock.js';
import CatchBlock from './CatchBlock.js';
import Expression from './shared/Expression.js';
import { unpack_destructuring } from './shared/Context.js';
/** @extends Node<'AwaitBlock'> */
export default class AwaitBlock extends Node {
type: 'AwaitBlock';
expression: Expression;
/** @type {import('./shared/Expression.js').default} */
expression;
then_contexts: Context[];
catch_contexts: Context[];
/** @type {import('./shared/Context.js').Context[]} */
then_contexts;
then_node: ESTreeNode | null;
catch_node: ESTreeNode | null;
/** @type {import('./shared/Context.js').Context[]} */
catch_contexts;
pending: PendingBlock;
then: ThenBlock;
catch: CatchBlock;
/** @type {import('estree').Node | null} */
then_node;
context_rest_properties: Map<string, ESTreeNode> = new Map();
/** @type {import('estree').Node | null} */
catch_node;
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
/** @type {import('./PendingBlock.js').default} */
pending;
/** @type {import('./ThenBlock.js').default} */
then;
/** @type {import('./CatchBlock.js').default} */
catch;
/** @type {Map<string, import('estree').Node>} */
context_rest_properties = new Map();
/**
* @param {import('../Component.js').default} component
* @param {import('./shared/Node.js').default} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').TemplateNode} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();
this.expression = new Expression(component, this, scope, info.expression);
this.then_node = info.value;
this.catch_node = info.error;
if (this.then_node) {
this.then_contexts = [];
unpack_destructuring({
@ -45,7 +57,6 @@ export default class AwaitBlock extends Node {
context_rest_properties: this.context_rest_properties
});
}
if (this.catch_node) {
this.catch_contexts = [];
unpack_destructuring({
@ -56,7 +67,6 @@ export default class AwaitBlock extends Node {
context_rest_properties: this.context_rest_properties
});
}
this.pending = new PendingBlock(component, this, scope, info.pending);
this.then = new ThenBlock(component, this, scope, info.then);
this.catch = new CatchBlock(component, this, scope, info.catch);

@ -1,18 +1,10 @@
import Node from './shared/Node';
import get_object from '../utils/get_object';
import Expression from './shared/Expression';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { regex_dimensions, regex_box_size } from '../../utils/patterns';
import { Node as ESTreeNode } from 'estree';
import { TemplateNode } from '../../interfaces';
import Element from './Element';
import InlineComponent from './InlineComponent';
import Window from './Window';
import Document from './Document';
import { clone } from '../../utils/clone';
import compiler_errors from '../compiler_errors';
import compiler_warnings from '../compiler_warnings';
import Node from './shared/Node.js';
import get_object from '../utils/get_object.js';
import Expression from './shared/Expression.js';
import { regex_dimensions, regex_box_size } from '../../utils/patterns.js';
import { clone } from '../../utils/clone.js';
import compiler_errors from '../compiler_errors.js';
import compiler_warnings from '../compiler_warnings.js';
// TODO this should live in a specific binding
const read_only_media_attributes = new Set([
@ -29,38 +21,43 @@ const read_only_media_attributes = new Set([
'readyState'
]);
/** @extends Node<'Binding'> */
export default class Binding extends Node {
type: 'Binding';
name: string;
expression: Expression;
raw_expression: ESTreeNode; // TODO exists only for bind:this — is there a more elegant solution?
is_contextual: boolean;
is_readonly: boolean;
/** @type {string} */
name;
constructor(
component: Component,
parent: Element | InlineComponent | Window | Document,
scope: TemplateScope,
info: TemplateNode
) {
super(component, parent, scope, info);
/** @type {import('./shared/Expression.js').default} */
expression;
/** @type {import('estree').Node} */
raw_expression; // TODO exists only for bind:this — is there a more elegant solution?
/** @type {boolean} */
is_contextual;
/** @type {boolean} */
is_readonly;
/**
* @param {import('../Component.js').default} component
* @param {import('./Element.js').default | import('./InlineComponent.js').default | import('./Window.js').default | import('./Document.js').default} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').TemplateNode} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
if (info.expression.type !== 'Identifier' && info.expression.type !== 'MemberExpression') {
component.error(info, compiler_errors.invalid_directive_value);
return;
}
this.name = info.name;
this.expression = new Expression(component, this, scope, info.expression);
this.raw_expression = clone(info.expression);
const { name } = get_object(this.expression.node);
this.is_contextual = Array.from(this.expression.references).some((name) =>
scope.names.has(name)
this.is_contextual = Array.from(this.expression.references).some(
/** @param {any} name */ (name) => scope.names.has(name)
);
if (this.is_contextual) this.validate_binding_rest_properties(scope);
// make sure we track this as a mutable ref
if (scope.is_let(name)) {
component.error(this, compiler_errors.invalid_binding_let);
@ -73,31 +70,33 @@ export default class Binding extends Node {
if (scope.is_const(name)) {
component.error(this, compiler_errors.invalid_binding_const);
}
scope.dependencies_for_name.get(name).forEach((name) => {
const variable = component.var_lookup.get(name);
if (variable) {
variable.mutated = true;
scope.dependencies_for_name.get(name).forEach(
/** @param {any} name */ (name) => {
const variable = component.var_lookup.get(name);
if (variable) {
variable.mutated = true;
}
}
});
);
} else {
const variable = component.var_lookup.get(name);
if (!variable || variable.global) {
component.error(this.expression.node as any, compiler_errors.binding_undeclared(name));
component.error(
/** @type {any} */ (this.expression.node),
compiler_errors.binding_undeclared(name)
);
return;
}
variable[this.expression.node.type === 'MemberExpression' ? 'mutated' : 'reassigned'] = true;
if (info.expression.type === 'Identifier' && !variable.writable) {
component.error(this.expression.node as any, compiler_errors.invalid_binding_writable);
component.error(
/** @type {any} */ (this.expression.node),
compiler_errors.invalid_binding_writable
);
return;
}
}
const type = parent.get_static_attribute_value('type');
this.is_readonly =
regex_dimensions.test(this.name) ||
regex_box_size.test(this.name) ||
@ -105,27 +104,33 @@ export default class Binding extends Node {
((parent.is_media_node() && read_only_media_attributes.has(this.name)) ||
(parent.name === 'input' && type === 'file'))) /* TODO others? */;
}
is_readonly_media_attribute() {
return read_only_media_attributes.has(this.name);
}
validate_binding_rest_properties(scope: TemplateScope) {
this.expression.references.forEach((name) => {
const each_block = scope.get_owner(name);
if (each_block && each_block.type === 'EachBlock') {
const rest_node = each_block.context_rest_properties.get(name);
if (rest_node) {
this.component.warn(
rest_node as any,
compiler_warnings.invalid_rest_eachblock_binding(name)
);
/** @param {import('./shared/TemplateScope.js').default} scope */
validate_binding_rest_properties(scope) {
this.expression.references.forEach(
/** @param {any} name */ (name) => {
const each_block = scope.get_owner(name);
if (each_block && each_block.type === 'EachBlock') {
const rest_node = each_block.context_rest_properties.get(name);
if (rest_node) {
this.component.warn(
/** @type {any} */ (rest_node),
compiler_warnings.invalid_rest_eachblock_binding(name)
);
}
}
}
});
);
}
}
function isElement(node: Node): node is Element {
return !!(node as any).is_media_node;
/**
* @param {import('./shared/Node.js').default} node
* @returns {node is import('./Element.js').default}
*/
function isElement(node) {
return !!(/** @type {any} */ (node).is_media_node);
}

@ -1,26 +1,33 @@
import Node from './shared/Node';
import EventHandler from './EventHandler';
import Action from './Action';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { Element } from '../../interfaces';
import Node from './shared/Node.js';
import EventHandler from './EventHandler.js';
import Action from './Action.js';
/** @extends Node<'Body'> */
export default class Body extends Node {
type: 'Body';
handlers: EventHandler[] = [];
actions: Action[] = [];
/** @type {import('./EventHandler.js').default[]} */
handlers = [];
constructor(component: Component, parent: Node, scope: TemplateScope, info: Element) {
super(component, parent, scope, info);
/** @type {import('./Action.js').default[]} */
actions = [];
info.attributes.forEach((node) => {
if (node.type === 'EventHandler') {
this.handlers.push(new EventHandler(component, this, scope, node));
} else if (node.type === 'Action') {
this.actions.push(new Action(component, this, scope, node));
} else {
// TODO there shouldn't be anything else here...
/**
* @param {import('../Component.js').default} component
* @param {import('./shared/Node.js').default} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').Element} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
info.attributes.forEach(
/** @param {any} node */ (node) => {
if (node.type === 'EventHandler') {
this.handlers.push(new EventHandler(component, this, scope, node));
} else if (node.type === 'Action') {
this.actions.push(new Action(component, this, scope, node));
} else {
// TODO there shouldn't be anything else here...
}
}
});
);
}
}

@ -1,29 +1,32 @@
import TemplateScope from './shared/TemplateScope';
import AbstractBlock from './shared/AbstractBlock';
import AwaitBlock from './AwaitBlock';
import Component from '../Component';
import { TemplateNode } from '../../interfaces';
import get_const_tags from './shared/get_const_tags';
import ConstTag from './ConstTag';
import AbstractBlock from './shared/AbstractBlock.js';
import get_const_tags from './shared/get_const_tags.js';
/** @extends AbstractBlock<'CatchBlock'> */
export default class CatchBlock extends AbstractBlock {
type: 'CatchBlock';
scope: TemplateScope;
const_tags: ConstTag[];
/** @type {import('./shared/TemplateScope.js').default} */
scope;
constructor(component: Component, parent: AwaitBlock, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
/** @type {import('./ConstTag.js').default[]} */
const_tags;
/**
* @param {import('../Component.js').default} component
* @param {import('./AwaitBlock.js').default} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').TemplateNode} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.scope = scope.child();
if (parent.catch_node) {
parent.catch_contexts.forEach((context) => {
if (context.type !== 'DestructuredVariable') return;
this.scope.add(context.key.name, parent.expression.dependencies, this);
});
parent.catch_contexts.forEach(
/** @param {any} context */ (context) => {
if (context.type !== 'DestructuredVariable') return;
this.scope.add(context.key.name, parent.expression.dependencies, this);
}
);
}
[this.const_tags, this.children] = get_const_tags(info.children, component, this, parent);
if (!info.skip) {
this.warn_if_empty_block();
}

@ -1,19 +1,23 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
import { TemplateNode } from '../../interfaces';
import TemplateScope from './shared/TemplateScope';
import Component from '../Component';
import Node from './shared/Node.js';
import Expression from './shared/Expression.js';
/** @extends Node<'Class'> */
export default class Class extends Node {
type: 'Class';
name: string;
expression: Expression;
/** @type {string} */
name;
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
/** @type {import('./shared/Expression.js').default} */
expression;
/**
* @param {import('../Component.js').default} component
* @param {import('./shared/Node.js').default} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').TemplateNode} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.name = info.name;
this.expression = info.expression
? new Expression(component, this, scope, info.expression)
: null;

@ -1,14 +1,20 @@
import { TemplateNode } from '../../interfaces';
import Component from '../Component';
import Node from './shared/Node';
import TemplateScope from './shared/TemplateScope';
import Node from './shared/Node.js';
/** @extends Node<'Comment'> */
export default class Comment extends Node {
type: 'Comment';
data: string;
ignores: string[];
/** @type {string} */
data;
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
/** @type {string[]} */
ignores;
/**
* @param {import('../Component.js').default} component
* @param {import('./shared/Node.js').default} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').TemplateNode} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.data = info.data;
this.ignores = info.ignores;

@ -1,16 +1,11 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { Context, unpack_destructuring } from './shared/Context';
import { ConstTag as ConstTagType } from '../../interfaces';
import { INodeAllowConstTag } from './interfaces';
import Node from './shared/Node.js';
import Expression from './shared/Expression.js';
import { unpack_destructuring } from './shared/Context.js';
import { walk } from 'estree-walker';
import { extract_identifiers } from 'periscopic';
import is_reference, { NodeWithPropertyDefinition } from 'is-reference';
import get_object from '../utils/get_object';
import compiler_errors from '../compiler_errors';
import { Node as ESTreeNode } from 'estree';
import is_reference from 'is-reference';
import get_object from '../utils/get_object.js';
import compiler_errors from '../compiler_errors.js';
const allowed_parents = new Set([
'EachBlock',
@ -22,47 +17,65 @@ const allowed_parents = new Set([
'ElseBlock'
]);
/** @extends Node<'ConstTag'> */
export default class ConstTag extends Node {
type: 'ConstTag';
expression: Expression;
contexts: Context[] = [];
node: ConstTagType;
scope: TemplateScope;
context_rest_properties: Map<string, ESTreeNode> = new Map();
/** @type {import('./shared/Expression.js').default} */
expression;
assignees: Set<string> = new Set();
dependencies: Set<string> = new Set();
/** @type {import('./shared/Context.js').Context[]} */
contexts = [];
constructor(
component: Component,
parent: INodeAllowConstTag,
scope: TemplateScope,
info: ConstTagType
) {
super(component, parent, scope, info);
/** @type {import('../../interfaces.js').ConstTag} */
node;
/** @type {import('./shared/TemplateScope.js').default} */
scope;
/** @type {Map<string, import('estree').Node>} */
context_rest_properties = new Map();
/** @type {Set<string>} */
assignees = new Set();
/** @type {Set<string>} */
dependencies = new Set();
/**
* @param {import('../Component.js').default} component
* @param {import('./interfaces.js').INodeAllowConstTag} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').ConstTag} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
if (!allowed_parents.has(parent.type)) {
component.error(info, compiler_errors.invalid_const_placement);
}
this.node = info;
this.scope = scope;
const { assignees, dependencies } = this;
extract_identifiers(info.expression.left).forEach(({ name }) => {
assignees.add(name);
const owner = this.scope.get_owner(name);
if (owner === parent) {
component.error(info, compiler_errors.invalid_const_declaration(name));
extract_identifiers(info.expression.left).forEach(
/** @param {any}params_0 */ ({ name }) => {
assignees.add(name);
const owner = this.scope.get_owner(name);
if (owner === parent) {
component.error(info, compiler_errors.invalid_const_declaration(name));
}
}
});
);
walk(info.expression.right, {
/**
* @param {any} node
* @param {any} parent
*/
enter(node, parent) {
if (
is_reference(node as NodeWithPropertyDefinition, parent as NodeWithPropertyDefinition)
is_reference(
/** @type {import('is-reference').NodeWithPropertyDefinition} */ (node),
/** @type {import('is-reference').NodeWithPropertyDefinition} */ (parent)
)
) {
const identifier = get_object(node as any);
const identifier = get_object(/** @type {any} */ (node));
const { name } = identifier;
dependencies.add(name);
}
@ -79,16 +92,18 @@ export default class ConstTag extends Node {
context_rest_properties: this.context_rest_properties
});
this.expression = new Expression(this.component, this, this.scope, this.node.expression.right);
this.contexts.forEach((context) => {
if (context.type !== 'DestructuredVariable') return;
const owner = this.scope.get_owner(context.key.name);
if (owner && owner.type === 'ConstTag' && owner.parent === this.parent) {
this.component.error(
this.node,
compiler_errors.invalid_const_declaration(context.key.name)
);
this.contexts.forEach(
/** @param {any} context */ (context) => {
if (context.type !== 'DestructuredVariable') return;
const owner = this.scope.get_owner(context.key.name);
if (owner && owner.type === 'ConstTag' && owner.parent === this.parent) {
this.component.error(
this.node,
compiler_errors.invalid_const_declaration(context.key.name)
);
}
this.scope.add(context.key.name, this.expression.dependencies, this);
}
this.scope.add(context.key.name, this.expression.dependencies, this);
});
);
}
}

@ -1,20 +1,23 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import { INode } from './interfaces';
import { Node as EsTreeNode } from 'estree';
import Node from './shared/Node.js';
import Expression from './shared/Expression.js';
/** @extends Node<'DebugTag'> */
export default class DebugTag extends Node {
type: 'DebugTag';
expressions: Expression[];
/** @type {import('./shared/Expression.js').default[]} */
expressions;
constructor(component: Component, parent: INode, scope: TemplateScope, info: TemplateNode) {
/**
* @param {import('../Component.js').default} component
* @param {import('./interfaces.js').INode} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').TemplateNode} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.expressions = info.identifiers.map((node: EsTreeNode) => {
return new Expression(component, parent, scope, node);
});
this.expressions = info.identifiers.map(
/** @param {import('estree').Node} node */ (node) => {
return new Expression(component, parent, scope, node);
}
);
}
}

@ -1,69 +1,75 @@
import Node from './shared/Node';
import Binding from './Binding';
import EventHandler from './EventHandler';
import fuzzymatch from '../../utils/fuzzymatch';
import Action from './Action';
import Component from '../Component';
import list from '../../utils/list';
import TemplateScope from './shared/TemplateScope';
import { Element } from '../../interfaces';
import compiler_warnings from '../compiler_warnings';
import compiler_errors from '../compiler_errors';
import Node from './shared/Node.js';
import Binding from './Binding.js';
import EventHandler from './EventHandler.js';
import fuzzymatch from '../../utils/fuzzymatch.js';
import Action from './Action.js';
import list from '../../utils/list.js';
import compiler_warnings from '../compiler_warnings.js';
import compiler_errors from '../compiler_errors.js';
const valid_bindings = ['fullscreenElement', 'visibilityState'];
/** @extends Node<'Document'> */
export default class Document extends Node {
type: 'Document';
handlers: EventHandler[] = [];
bindings: Binding[] = [];
actions: Action[] = [];
/** @type {import('./EventHandler.js').default[]} */
handlers = [];
constructor(component: Component, parent: Node, scope: TemplateScope, info: Element) {
super(component, parent, scope, info);
/** @type {import('./Binding.js').default[]} */
bindings = [];
/** @type {import('./Action.js').default[]} */
actions = [];
info.attributes.forEach((node) => {
if (node.type === 'EventHandler') {
this.handlers.push(new EventHandler(component, this, scope, node));
} else if (node.type === 'Binding') {
if (!~valid_bindings.indexOf(node.name)) {
const match = fuzzymatch(node.name, valid_bindings);
if (match) {
return component.error(
node,
compiler_errors.invalid_binding_on(
node.name,
'<svelte:document>',
` (did you mean '${match}'?)`
)
);
} else {
return component.error(
node,
compiler_errors.invalid_binding_on(
node.name,
'<svelte:document>',
` — valid bindings are ${list(valid_bindings)}`
)
);
/**
* @param {import('../Component.js').default} component
* @param {import('./shared/Node.js').default} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').Element} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
info.attributes.forEach(
/** @param {any} node */ (node) => {
if (node.type === 'EventHandler') {
this.handlers.push(new EventHandler(component, this, scope, node));
} else if (node.type === 'Binding') {
if (!~valid_bindings.indexOf(node.name)) {
const match = fuzzymatch(node.name, valid_bindings);
if (match) {
return component.error(
node,
compiler_errors.invalid_binding_on(
node.name,
'<svelte:document>',
` (did you mean '${match}'?)`
)
);
} else {
return component.error(
node,
compiler_errors.invalid_binding_on(
node.name,
'<svelte:document>',
` — valid bindings are ${list(valid_bindings)}`
)
);
}
}
this.bindings.push(new Binding(component, this, scope, node));
} else if (node.type === 'Action') {
this.actions.push(new Action(component, this, scope, node));
} else {
// TODO there shouldn't be anything else here...
}
this.bindings.push(new Binding(component, this, scope, node));
} else if (node.type === 'Action') {
this.actions.push(new Action(component, this, scope, node));
} else {
// TODO there shouldn't be anything else here...
}
});
);
this.validate();
}
private validate() {
/** @private */
validate() {
const handlers_map = new Set();
this.handlers.forEach((handler) => handlers_map.add(handler.name));
this.handlers.forEach(/** @param {any} handler */ (handler) => handlers_map.add(handler.name));
if (handlers_map.has('mouseenter') || handlers_map.has('mouseleave')) {
this.component.warn(this, compiler_warnings.avoid_mouse_events_on_document);
}

@ -1,46 +1,66 @@
import ElseBlock from './ElseBlock';
import Expression from './shared/Expression';
import TemplateScope from './shared/TemplateScope';
import AbstractBlock from './shared/AbstractBlock';
import Element from './Element';
import ConstTag from './ConstTag';
import { Context, unpack_destructuring } from './shared/Context';
import { Node } from 'estree';
import Component from '../Component';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
import { INode } from './interfaces';
import get_const_tags from './shared/get_const_tags';
import ElseBlock from './ElseBlock.js';
import Expression from './shared/Expression.js';
import AbstractBlock from './shared/AbstractBlock.js';
import { unpack_destructuring } from './shared/Context.js';
import compiler_errors from '../compiler_errors.js';
import get_const_tags from './shared/get_const_tags.js';
/** @extends AbstractBlock<'EachBlock'> */
export default class EachBlock extends AbstractBlock {
type: 'EachBlock';
/** @type {import('./shared/Expression.js').default} */
expression;
/** @type {import('estree').Node} */
context_node;
/** @type {string} */
iterations;
/** @type {string} */
index;
/** @type {string} */
context;
/** @type {import('./shared/Expression.js').default} */
key;
expression: Expression;
context_node: Node;
/** @type {import('./shared/TemplateScope.js').default} */
scope;
iterations: string;
index: string;
context: string;
key: Expression;
scope: TemplateScope;
contexts: Context[];
const_tags: ConstTag[];
has_animation: boolean;
/** @type {import('./shared/Context.js').Context[]} */
contexts;
/** @type {import('./ConstTag.js').default[]} */
const_tags;
/** @type {boolean} */
has_animation;
/** */
has_binding = false;
/** */
has_index_binding = false;
context_rest_properties: Map<string, Node>;
else?: ElseBlock;
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
/** @type {Map<string, import('estree').Node>} */
context_rest_properties;
/** @type {import('./ElseBlock.js').default} */
else;
/**
* @param {import('../Component.js').default} component
* @param {import('estree').Node} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').TemplateNode} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();
this.expression = new Expression(component, this, scope, info.expression);
this.context = info.context.name || 'each'; // TODO this is used to facilitate binding; currently fails with destructuring
this.context_node = info.context;
this.index = info.index;
this.scope = scope.child();
this.context_rest_properties = new Map();
this.contexts = [];
@ -51,43 +71,47 @@ export default class EachBlock extends AbstractBlock {
component,
context_rest_properties: this.context_rest_properties
});
this.contexts.forEach((context) => {
if (context.type !== 'DestructuredVariable') return;
this.scope.add(context.key.name, this.expression.dependencies, this);
});
this.contexts.forEach(
/** @param {any} context */ (context) => {
if (context.type !== 'DestructuredVariable') return;
this.scope.add(context.key.name, this.expression.dependencies, this);
}
);
if (this.index) {
// index can only change if this is a keyed each block
const dependencies = info.key ? this.expression.dependencies : new Set([]);
this.scope.add(this.index, dependencies, this);
}
this.key = info.key ? new Expression(component, this, this.scope, info.key) : null;
this.has_animation = false;
[this.const_tags, this.children] = get_const_tags(info.children, component, this, this);
if (this.has_animation) {
this.children = this.children.filter((child) => !isEmptyNode(child) && !isCommentNode(child));
this.children = this.children.filter(
/** @param {any} child */ (child) => !isEmptyNode(child) && !isCommentNode(child)
);
if (this.children.length !== 1) {
const child = this.children.find((child) => !!(child as Element).animation);
component.error((child as Element).animation, compiler_errors.invalid_animation_sole);
const child = this.children.find(
/** @param {any} child */ (child) =>
!!(/** @type {import('./Element.js').default} */ (child).animation)
);
component.error(
/** @type {import('./Element.js').default} */ (child).animation,
compiler_errors.invalid_animation_sole
);
return;
}
}
this.warn_if_empty_block();
this.else = info.else ? new ElseBlock(component, this, this.scope, info.else) : null;
}
}
function isEmptyNode(node: INode) {
/** @param {import('./interfaces.js').INode} node */
function isEmptyNode(node) {
return node.type === 'Text' && node.data.trim() === '';
}
function isCommentNode(node: INode) {
/** @param {import('./interfaces.js').INode} node */
function isCommentNode(node) {
return node.type === 'Comment';
}

File diff suppressed because it is too large Load Diff

@ -1,22 +1,24 @@
import AbstractBlock from './shared/AbstractBlock';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import Node from './shared/Node';
import ConstTag from './ConstTag';
import get_const_tags from './shared/get_const_tags';
import AbstractBlock from './shared/AbstractBlock.js';
import get_const_tags from './shared/get_const_tags.js';
/** @extends AbstractBlock<'ElseBlock'> */
export default class ElseBlock extends AbstractBlock {
type: 'ElseBlock';
scope: TemplateScope;
const_tags: ConstTag[];
/** @type {import('./shared/TemplateScope.js').default} */
scope;
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
/** @type {import('./ConstTag.js').default[]} */
const_tags;
/**
* @param {import('../Component.js').default} component
* @param {import('./shared/Node.js').default} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').TemplateNode} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.scope = scope.child();
[this.const_tags, this.children] = get_const_tags(info.children, component, this, this);
this.warn_if_empty_block();
}
}

@ -1,37 +1,40 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
import Component from '../Component';
import { sanitize } from '../../utils/names';
import { Identifier } from 'estree';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import Node from './shared/Node.js';
import Expression from './shared/Expression.js';
import { sanitize } from '../../utils/names.js';
const regex_contains_term_function_expression = /FunctionExpression/;
/** @extends Node<'EventHandler'> */
export default class EventHandler extends Node {
type: 'EventHandler';
name: string;
modifiers: Set<string>;
expression: Expression;
handler_name: Identifier;
/** @type {string} */
name;
/** @type {Set<string>} */
modifiers;
/** @type {import('./shared/Expression.js').default} */
expression;
/** @type {import('estree').Identifier} */
handler_name;
/** */
uses_context = false;
/** */
can_make_passive = false;
constructor(
component: Component,
parent: Node,
template_scope: TemplateScope,
info: TemplateNode
) {
/**
* @param {import('../Component.js').default} component
* @param {import('./shared/Node.js').default} parent
* @param {import('./shared/TemplateScope.js').default} template_scope
* @param {import('../../interfaces.js').TemplateNode} info
*/
constructor(component, parent, template_scope, info) {
super(component, parent, template_scope, info);
this.name = info.name;
this.modifiers = new Set(info.modifiers);
if (info.expression) {
this.expression = new Expression(component, this, template_scope, info.expression);
this.uses_context = this.expression.uses_context;
if (
regex_contains_term_function_expression.test(info.expression.type) &&
info.expression.params.length === 0
@ -41,16 +44,15 @@ export default class EventHandler extends Node {
this.can_make_passive = true;
} else if (info.expression.type === 'Identifier') {
let node = component.node_for_declaration.get(info.expression.name);
if (node) {
if (node.type === 'VariableDeclaration') {
// for `const handleClick = () => {...}`, we want the [arrow] function expression node
const declarator = node.declarations.find(
(d) => (d.id as Identifier).name === info.expression.name
/** @param {any} d */
(d) => /** @type {import('estree').Identifier} */ (d.id).name === info.expression.name
);
node = declarator && declarator.init;
}
if (
node &&
(node.type === 'FunctionExpression' ||
@ -67,16 +69,15 @@ export default class EventHandler extends Node {
}
}
get reassigned(): boolean {
/** @returns {boolean} */
get reassigned() {
if (!this.expression) {
return false;
}
const node = this.expression.node;
if (regex_contains_term_function_expression.test(node.type)) {
return false;
}
return this.expression.dynamic_dependencies().length > 0;
}
}

@ -1,21 +1,25 @@
import Node from './shared/Node';
import Component from '../Component';
import map_children from './shared/map_children';
import Block from '../render_dom/Block';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import { TemplateNode } from '../../interfaces';
import Node from './shared/Node.js';
import map_children from './shared/map_children.js';
import TemplateScope from './shared/TemplateScope.js';
/** @extends Node<'Fragment'> */
export default class Fragment extends Node {
type: 'Fragment';
block: Block;
children: INode[];
scope: TemplateScope;
/** @type {import('../render_dom/Block.js').default} */
block;
constructor(component: Component, info: TemplateNode) {
/** @type {import('./interfaces.js').INode[]} */
children;
/** @type {import('./shared/TemplateScope.js').default} */
scope;
/**
* @param {import('../Component.js').default} component
* @param {import('../../interfaces.js').TemplateNode} info
*/
constructor(component, info) {
const scope = new TemplateScope();
super(component, null, scope, info);
this.scope = scope;
this.children = map_children(component, this, scope, info.children);
}

@ -1,36 +1,40 @@
import Node from './shared/Node';
import map_children from './shared/map_children';
import hash from '../utils/hash';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
import { regex_non_whitespace_character } from '../../utils/patterns';
import Node from './shared/Node.js';
import map_children from './shared/map_children.js';
import hash from '../utils/hash.js';
import compiler_errors from '../compiler_errors.js';
import { regex_non_whitespace_character } from '../../utils/patterns.js';
/** @extends Node<'Head'> */
export default class Head extends Node {
type: 'Head';
children: any[]; // TODO
id: string;
/** @type {any[]} */
children; // TODO
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
/** @type {string} */
id;
/**
* @param {import('../Component.js').default} component
* @param {import('./shared/Node.js').default} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').TemplateNode} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.cannot_use_innerhtml();
if (info.attributes.length) {
component.error(info.attributes[0], compiler_errors.invalid_attribute_head);
return;
}
this.children = map_children(
component,
parent,
scope,
info.children.filter((child) => {
return child.type !== 'Text' || regex_non_whitespace_character.test(child.data);
})
info.children.filter(
/** @param {any} child */ (child) => {
return child.type !== 'Text' || regex_non_whitespace_character.test(child.data);
}
)
);
if (this.children.length > 0) {
this.id = `svelte-${hash(this.component.source.slice(this.start, this.end))}`;
}

@ -1,31 +1,36 @@
import ElseBlock from './ElseBlock';
import Expression from './shared/Expression';
import AbstractBlock from './shared/AbstractBlock';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import Node from './shared/Node';
import ConstTag from './ConstTag';
import get_const_tags from './shared/get_const_tags';
import ElseBlock from './ElseBlock.js';
import Expression from './shared/Expression.js';
import AbstractBlock from './shared/AbstractBlock.js';
import get_const_tags from './shared/get_const_tags.js';
/** @extends AbstractBlock<'IfBlock'> */
export default class IfBlock extends AbstractBlock {
type: 'IfBlock';
expression: Expression;
else: ElseBlock;
scope: TemplateScope;
const_tags: ConstTag[];
/** @type {import('./shared/Expression.js').default} */
expression;
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
/** @type {import('./ElseBlock.js').default} */
else;
/** @type {import('./shared/TemplateScope.js').default} */
scope;
/** @type {import('./ConstTag.js').default[]} */
const_tags;
/**
* @param {import('../Component.js').default} component
* @param {import('./shared/Node.js').default} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').TemplateNode} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.scope = scope.child();
this.cannot_use_innerhtml();
this.not_static_content();
this.expression = new Expression(component, this, this.scope, info.expression);
[this.const_tags, this.children] = get_const_tags(info.children, component, this, this);
this.else = info.else ? new ElseBlock(component, this, scope, info.else) : null;
this.warn_if_empty_block();
}
}

@ -1,115 +1,125 @@
import Node from './shared/Node';
import Attribute from './Attribute';
import map_children from './shared/map_children';
import Binding from './Binding';
import EventHandler from './EventHandler';
import Expression from './shared/Expression';
import Component from '../Component';
import Let from './Let';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
import { regex_only_whitespaces } from '../../utils/patterns';
import Node from './shared/Node.js';
import Attribute from './Attribute.js';
import map_children from './shared/map_children.js';
import Binding from './Binding.js';
import EventHandler from './EventHandler.js';
import Expression from './shared/Expression.js';
import Let from './Let.js';
import compiler_errors from '../compiler_errors.js';
import { regex_only_whitespaces } from '../../utils/patterns.js';
/** @extends Node<'InlineComponent'> */
export default class InlineComponent extends Node {
type: 'InlineComponent';
name: string;
expression: Expression;
attributes: Attribute[] = [];
bindings: Binding[] = [];
handlers: EventHandler[] = [];
lets: Let[] = [];
css_custom_properties: Attribute[] = [];
children: INode[];
scope: TemplateScope;
namespace: string;
/** @type {string} */
name;
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
/** @type {import('./shared/Expression.js').default} */
expression;
/** @type {import('./Binding.js').default[]} */
bindings = [];
/** @type {import('./EventHandler.js').default[]} */
handlers = [];
/** @type {import('./Let.js').default[]} */
lets = [];
/** @type {import('./Attribute.js').default[]} */
css_custom_properties = [];
/** @type {import('./interfaces.js').INode[]} */
children;
/** @type {import('./shared/TemplateScope.js').default} */
scope;
/** @type {string} */
namespace;
/**
* @param {import('../Component.js').default} component
* @param {import('./shared/Node.js').default} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').TemplateNode} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();
if (info.name !== 'svelte:component' && info.name !== 'svelte:self') {
const name = info.name.split('.')[0]; // accommodate namespaces
component.warn_if_undefined(name, info, scope);
component.add_reference(this as any, name);
component.add_reference(/** @type {any} */ (this), name);
}
this.name = info.name;
this.namespace = get_namespace(parent, component.namespace);
this.expression =
this.name === 'svelte:component'
? new Expression(component, this, scope, info.expression)
: null;
info.attributes.forEach((node) => {
/* eslint-disable no-fallthrough */
switch (node.type) {
case 'Action':
return component.error(node, compiler_errors.invalid_action);
case 'Attribute':
if (node.name.startsWith('--')) {
this.css_custom_properties.push(new Attribute(component, this, scope, node));
info.attributes.forEach(
/** @param {any} node */ (node) => {
/* eslint-disable no-fallthrough */
switch (node.type) {
case 'Action':
return component.error(node, compiler_errors.invalid_action);
case 'Attribute':
if (node.name.startsWith('--')) {
this.css_custom_properties.push(new Attribute(component, this, scope, node));
break;
}
// fallthrough
case 'Spread':
this.attributes.push(new Attribute(component, this, scope, node));
break;
}
// fallthrough
case 'Spread':
this.attributes.push(new Attribute(component, this, scope, node));
break;
case 'Binding':
this.bindings.push(new Binding(component, this, scope, node));
break;
case 'Class':
return component.error(node, compiler_errors.invalid_class);
case 'EventHandler':
this.handlers.push(new EventHandler(component, this, scope, node));
break;
case 'Let':
this.lets.push(new Let(component, this, scope, node));
break;
case 'Transition':
return component.error(node, compiler_errors.invalid_transition);
case 'StyleDirective':
return component.error(node, compiler_errors.invalid_component_style_directive);
default:
throw new Error(`Not implemented: ${node.type}`);
case 'Binding':
this.bindings.push(new Binding(component, this, scope, node));
break;
case 'Class':
return component.error(node, compiler_errors.invalid_class);
case 'EventHandler':
this.handlers.push(new EventHandler(component, this, scope, node));
break;
case 'Let':
this.lets.push(new Let(component, this, scope, node));
break;
case 'Transition':
return component.error(node, compiler_errors.invalid_transition);
case 'StyleDirective':
return component.error(node, compiler_errors.invalid_component_style_directive);
default:
throw new Error(`Not implemented: ${node.type}`);
}
/* eslint-enable no-fallthrough */
}
/* eslint-enable no-fallthrough */
});
);
if (this.lets.length > 0) {
this.scope = scope.child();
this.lets.forEach((l) => {
const dependencies = new Set([l.name.name]);
l.names.forEach((name) => {
this.scope.add(name, dependencies, this);
});
});
this.lets.forEach(
/** @param {any} l */ (l) => {
const dependencies = new Set([l.name.name]);
l.names.forEach(
/** @param {any} name */ (name) => {
this.scope.add(name, dependencies, this);
}
);
}
);
} else {
this.scope = scope;
}
this.handlers.forEach((handler) => {
handler.modifiers.forEach((modifier) => {
if (modifier !== 'once') {
return component.error(handler, compiler_errors.invalid_event_modifier_component);
}
});
});
this.handlers.forEach(
/** @param {any} handler */ (handler) => {
handler.modifiers.forEach(
/** @param {any} modifier */ (modifier) => {
if (modifier !== 'once') {
return component.error(handler, compiler_errors.invalid_event_modifier_component);
}
}
);
}
);
const children = [];
for (let i = info.children.length - 1; i >= 0; i--) {
const child = info.children[i];
@ -118,7 +128,9 @@ export default class InlineComponent extends Node {
info.children.splice(i, 1);
} else if (
(child.type === 'Element' || child.type === 'InlineComponent' || child.type === 'Slot') &&
child.attributes.find((attribute) => attribute.name === 'slot')
child.attributes.find(
/** @param {any} attribute */ (attribute) => attribute.name === 'slot'
)
) {
const slot_template = {
start: child.start,
@ -128,7 +140,6 @@ export default class InlineComponent extends Node {
attributes: [],
children: [child]
};
// transfer attributes
for (let i = child.attributes.length - 1; i >= 0; i--) {
const attribute = child.attributes[i];
@ -147,15 +158,13 @@ export default class InlineComponent extends Node {
child.children.splice(i, 1);
}
}
children.push(slot_template);
info.children.splice(i, 1);
} else if (child.type === 'Comment' && children.length > 0) {
children[children.length - 1].children.unshift(child);
}
}
if (info.children.some((node) => not_whitespace_text(node))) {
if (info.children.some(/** @param {any} node */ (node) => not_whitespace_text(node))) {
children.push({
start: info.start,
end: info.end,
@ -165,27 +174,30 @@ export default class InlineComponent extends Node {
children: info.children
});
}
this.children = map_children(component, this, this.scope, children);
}
get slot_template_name() {
return this.attributes
.find((attribute) => attribute.name === 'slot')
.get_static_value() as string;
return /** @type {string} */ (
this.attributes
.find(/** @param {any} attribute */ (attribute) => attribute.name === 'slot')
.get_static_value()
);
}
}
/** @param {any} node */
function not_whitespace_text(node) {
return !(node.type === 'Text' && regex_only_whitespaces.test(node.data));
}
function get_namespace(parent: Node, explicit_namespace: string) {
/**
* @param {import('./shared/Node.js').default} parent
* @param {string} explicit_namespace
*/
function get_namespace(parent, explicit_namespace) {
const parent_element = parent.find_nearest(/^Element/);
if (!parent_element) {
return explicit_namespace;
}
return parent_element.namespace;
}

@ -1,25 +1,24 @@
import Expression from './shared/Expression';
import map_children from './shared/map_children';
import AbstractBlock from './shared/AbstractBlock';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import Node from './shared/Node';
import Expression from './shared/Expression.js';
import map_children from './shared/map_children.js';
import AbstractBlock from './shared/AbstractBlock.js';
/** @extends AbstractBlock<'KeyBlock'> */
export default class KeyBlock extends AbstractBlock {
type: 'KeyBlock';
/** @type {import('./shared/Expression.js').default} */
expression;
expression: Expression;
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
/**
* @param {import('../Component.js').default} component
* @param {import('./shared/Node.js').default} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').TemplateNode} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();
this.expression = new Expression(component, this, scope, info.expression);
this.children = map_children(component, this, scope, info.children);
this.warn_if_empty_block();
}
}

@ -1,44 +1,45 @@
import Node from './shared/Node';
import Component from '../Component';
import Node from './shared/Node.js';
import { walk } from 'estree-walker';
import { BasePattern, Identifier } from 'estree';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
import compiler_errors from '../compiler_errors.js';
const applicable = new Set(['Identifier', 'ObjectExpression', 'ArrayExpression', 'Property']);
/** @extends Node<'Let'> */
export default class Let extends Node {
type: 'Let';
name: Identifier;
value: Identifier;
names: string[] = [];
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
/** @type {import('estree').Identifier} */
name;
/** @type {import('estree').Identifier} */
value;
/** @type {string[]} */
names = [];
/**
* @param {import('../Component.js').default} component
* @param {import('./shared/Node.js').default} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').TemplateNode} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.name = { type: 'Identifier', name: info.name };
const { names } = this;
if (info.expression) {
this.value = info.expression;
walk(info.expression, {
enter(node: Identifier | BasePattern) {
/** @param {import('estree').Identifier | import('estree').BasePattern} node */
enter(node) {
if (!applicable.has(node.type)) {
return component.error(node as any, compiler_errors.invalid_let);
return component.error(/** @type {any} */ (node), compiler_errors.invalid_let);
}
if (node.type === 'Identifier') {
names.push((node as Identifier).name);
names.push(/** @type {import('estree').Identifier} */ (node).name);
}
// slightly unfortunate hack
if (node.type === 'ArrayExpression') {
node.type = 'ArrayPattern';
}
if (node.type === 'ObjectExpression') {
node.type = 'ObjectPattern';
}

@ -1,5 +1,4 @@
import Tag from './shared/Tag';
import Tag from './shared/Tag.js';
export default class MustacheTag extends Tag {
type: 'MustacheTag';
}
/** @extends Tag<'MustacheTag'> */
export default class MustacheTag extends Tag {}

@ -1,5 +1,4 @@
import Node from './shared/Node';
import Node from './shared/Node.js';
export default class Options extends Node {
type: 'Options';
}
/** @extends Node<'Options'> */
export default class Options extends Node {}

@ -1,16 +1,17 @@
import map_children from './shared/map_children';
import AbstractBlock from './shared/AbstractBlock';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import Node from './shared/Node';
import map_children from './shared/map_children.js';
import AbstractBlock from './shared/AbstractBlock.js';
/** @extends AbstractBlock<'PendingBlock'> */
export default class PendingBlock extends AbstractBlock {
type: 'PendingBlock';
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
/**
* @param {import('../Component.js').default} component
* @param {import('./shared/Node.js').default} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').TemplateNode} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.children = map_children(component, parent, scope, info.children);
if (!info.skip) {
this.warn_if_empty_block();
}

@ -1,7 +1,13 @@
import Tag from './shared/Tag';
import Tag from './shared/Tag.js';
/** @extends Tag<'RawMustacheTag'> */
export default class RawMustacheTag extends Tag {
type: 'RawMustacheTag';
/**
* @param {any} component
* @param {any} parent
* @param {any} scope
* @param {any} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.cannot_use_innerhtml();

@ -1,67 +1,79 @@
import Element from './Element';
import Attribute from './Attribute';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
import Element from './Element.js';
import Attribute from './Attribute.js';
import compiler_errors from '../compiler_errors.js';
/** @extends Element */
export default class Slot extends Element {
/** @type {'Slot'} */
// @ts-ignore Slot elements have the 'Slot' type, but TypeScript doesn't allow us to have 'Slot' when it extends Element
type: 'Slot';
name: string;
children: INode[];
slot_name: string;
values: Map<string, Attribute> = new Map();
type = 'Slot';
constructor(component: Component, parent: INode, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
/** @type {string} */
slot_name;
info.attributes.forEach((attr) => {
if (attr.type !== 'Attribute' && attr.type !== 'Spread') {
return component.error(attr, compiler_errors.invalid_slot_directive);
}
/** @type {Map<string, import('./Attribute.js').default>} */
values = new Map();
if (attr.name === 'name') {
if (attr.value.length !== 1 || attr.value[0].type !== 'Text') {
return component.error(attr, compiler_errors.dynamic_slot_name);
/**
* @param {import('../Component.js').default} component
* @param {import('./interfaces.js').INode} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').TemplateNode} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
info.attributes.forEach(
/** @param {any} attr */ (attr) => {
if (attr.type !== 'Attribute' && attr.type !== 'Spread') {
return component.error(attr, compiler_errors.invalid_slot_directive);
}
this.slot_name = attr.value[0].data;
if (this.slot_name === 'default') {
return component.error(attr, compiler_errors.invalid_slot_name);
if (attr.name === 'name') {
if (attr.value.length !== 1 || attr.value[0].type !== 'Text') {
return component.error(attr, compiler_errors.dynamic_slot_name);
}
this.slot_name = attr.value[0].data;
if (this.slot_name === 'default') {
return component.error(attr, compiler_errors.invalid_slot_name);
}
}
this.values.set(attr.name, new Attribute(component, this, scope, attr));
}
this.values.set(attr.name, new Attribute(component, this, scope, attr));
});
);
if (!this.slot_name) this.slot_name = 'default';
if (this.slot_name === 'default') {
// if this is the default slot, add our dependencies to any
// other slots (which inherit our slot values) that were
// previously encountered
component.slots.forEach((slot) => {
this.values.forEach((attribute, name) => {
if (!slot.values.has(name)) {
slot.values.set(name, attribute);
}
});
});
component.slots.forEach(
/** @param {any} slot */ (slot) => {
this.values.forEach(
/**
* @param {any} attribute
* @param {any} name
*/ (attribute, name) => {
if (!slot.values.has(name)) {
slot.values.set(name, attribute);
}
}
);
}
);
} else if (component.slots.has('default')) {
// otherwise, go the other way — inherit values from
// a previously encountered default slot
const default_slot = component.slots.get('default');
default_slot.values.forEach((attribute, name) => {
if (!this.values.has(name)) {
this.values.set(name, attribute);
default_slot.values.forEach(
/**
* @param {any} attribute
* @param {any} name
*/ (attribute, name) => {
if (!this.values.has(name)) {
this.values.set(name, attribute);
}
}
});
);
}
component.slots.set(this.slot_name, this);
this.cannot_use_innerhtml();
this.not_static_content();
}

@ -1,65 +1,76 @@
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import Node from './shared/Node';
import Let from './Let';
import Attribute from './Attribute';
import { INode } from './interfaces';
import compiler_errors from '../compiler_errors';
import get_const_tags from './shared/get_const_tags';
import ConstTag from './ConstTag';
import Node from './shared/Node.js';
import Let from './Let.js';
import Attribute from './Attribute.js';
import compiler_errors from '../compiler_errors.js';
import get_const_tags from './shared/get_const_tags.js';
/** @extends Node<'SlotTemplate'> */
export default class SlotTemplate extends Node {
type: 'SlotTemplate';
scope: TemplateScope;
children: INode[];
lets: Let[] = [];
const_tags: ConstTag[];
slot_attribute: Attribute;
slot_template_name: string = 'default';
/** @type {import('./shared/TemplateScope.js').default} */
scope;
constructor(component: Component, parent: INode, scope: TemplateScope, info: any) {
super(component, parent, scope, info);
/** @type {import('./interfaces.js').INode[]} */
children;
this.validate_slot_template_placement();
/** @type {import('./Let.js').default[]} */
lets = [];
scope = scope.child();
/** @type {import('./ConstTag.js').default[]} */
const_tags;
info.attributes.forEach((node) => {
switch (node.type) {
case 'Let': {
const l = new Let(component, this, scope, node);
this.lets.push(l);
const dependencies = new Set([l.name.name]);
/** @type {import('./Attribute.js').default} */
slot_attribute;
l.names.forEach((name) => {
scope.add(name, dependencies, this);
});
break;
}
case 'Attribute': {
if (node.name === 'slot') {
this.slot_attribute = new Attribute(component, this, scope, node);
if (!this.slot_attribute.is_static) {
return component.error(node, compiler_errors.invalid_slot_attribute);
}
const value = this.slot_attribute.get_static_value();
if (typeof value === 'boolean') {
return component.error(node, compiler_errors.invalid_slot_attribute_value_missing);
}
this.slot_template_name = value as string;
/** @type {string} */
slot_template_name = 'default';
/**
* @param {import('../Component.js').default} component
* @param {import('./interfaces.js').INode} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {any} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.validate_slot_template_placement();
scope = scope.child();
info.attributes.forEach(
/** @param {any} node */ (node) => {
switch (node.type) {
case 'Let': {
const l = new Let(component, this, scope, node);
this.lets.push(l);
const dependencies = new Set([l.name.name]);
l.names.forEach(
/** @param {any} name */ (name) => {
scope.add(name, dependencies, this);
}
);
break;
}
throw new Error(`Invalid attribute '${node.name}' in <svelte:fragment>`);
case 'Attribute': {
if (node.name === 'slot') {
this.slot_attribute = new Attribute(component, this, scope, node);
if (!this.slot_attribute.is_static) {
return component.error(node, compiler_errors.invalid_slot_attribute);
}
const value = this.slot_attribute.get_static_value();
if (typeof value === 'boolean') {
return component.error(node, compiler_errors.invalid_slot_attribute_value_missing);
}
this.slot_template_name = /** @type {string} */ (value);
break;
}
throw new Error(`Invalid attribute '${node.name}' in <svelte:fragment>`);
}
default:
throw new Error(`Not implemented: ${node.type}`);
}
default:
throw new Error(`Not implemented: ${node.type}`);
}
});
);
this.scope = scope;
[this.const_tags, this.children] = get_const_tags(info.children, component, this, this);
}
validate_slot_template_placement() {
if (this.parent.type !== 'InlineComponent') {
return this.component.error(this, compiler_errors.invalid_slotted_content_fragment);

@ -1,27 +1,35 @@
import { TemplateNode } from '../../interfaces';
import list from '../../utils/list';
import compiler_errors from '../compiler_errors';
import Component from '../Component';
import { nodes_to_template_literal } from '../utils/nodes_to_template_literal';
import Expression from './shared/Expression';
import Node from './shared/Node';
import TemplateScope from './shared/TemplateScope';
import list from '../../utils/list.js';
import compiler_errors from '../compiler_errors.js';
import { nodes_to_template_literal } from '../utils/nodes_to_template_literal.js';
import Expression from './shared/Expression.js';
import Node from './shared/Node.js';
const valid_modifiers = new Set(['important']);
/** @extends Node<'StyleDirective'> */
export default class StyleDirective extends Node {
type: 'StyleDirective';
name: string;
modifiers: Set<string>;
expression: Expression;
should_cache: boolean;
/** @type {string} */
name;
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
/** @type {Set<string>} */
modifiers;
/** @type {import('./shared/Expression.js').default} */
expression;
/** @type {boolean} */
should_cache;
/**
* @param {import('../Component.js').default} component
* @param {import('./shared/Node.js').default} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').TemplateNode} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.name = info.name;
this.modifiers = new Set(info.modifiers);
for (const modifier of this.modifiers) {
if (!valid_modifiers.has(modifier)) {
component.error(
@ -30,18 +38,17 @@ export default class StyleDirective extends Node {
);
}
}
// Convert the value array to an expression so it's easier to handle
// the StyleDirective going forward.
if (info.value === true || (info.value.length === 1 && info.value[0].type === 'MustacheTag')) {
const identifier =
info.value === true
? ({
? /** @type {any} */ ({
type: 'Identifier',
start: info.end - info.name.length,
end: info.end,
name: info.name
} as any)
})
: info.value[0].expression;
this.expression = new Expression(component, this, scope, identifier);
this.should_cache = false;
@ -51,7 +58,6 @@ export default class StyleDirective extends Node {
this.should_cache = raw_expression.expressions.length > 0;
}
}
get important() {
return this.modifiers.has('important');
}

@ -1,53 +1,54 @@
import Node from './shared/Node';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import { TemplateNode } from '../../interfaces';
import { regex_non_whitespace_character } from '../../utils/patterns';
import Node from './shared/Node.js';
import { regex_non_whitespace_character } from '../../utils/patterns.js';
// Whitespace inside one of these elements will not result in
// a whitespace node being created in any circumstances. (This
// list is almost certainly very incomplete)
const elements_without_text = new Set(['audio', 'datalist', 'dl', 'optgroup', 'select', 'video']);
const regex_ends_with_svg = /svg$/;
const regex_non_whitespace_characters = /[\S\u00A0]/;
/** @extends Node<'Text'> */
export default class Text extends Node {
type: 'Text';
data: string;
synthetic: boolean;
/** @type {string} */
data;
/** @type {boolean} */
synthetic;
constructor(component: Component, parent: INode, scope: TemplateScope, info: TemplateNode) {
/**
* @param {import('../Component.js').default} component
* @param {import('./interfaces.js').INode} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').TemplateNode} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.data = info.data;
this.synthetic = info.synthetic || false;
}
should_skip() {
if (regex_non_whitespace_character.test(this.data)) return false;
const parent_element = this.find_nearest(/(?:Element|InlineComponent|SlotTemplate|Head)/);
if (!parent_element) return false;
if (parent_element.type === 'Head') return true;
if (parent_element.type === 'InlineComponent')
return parent_element.children.length === 1 && this === parent_element.children[0];
// svg namespace exclusions
if (regex_ends_with_svg.test(parent_element.namespace)) {
if (this.prev && this.prev.type === 'Element' && this.prev.name === 'tspan') return false;
}
return parent_element.namespace || elements_without_text.has(parent_element.name);
}
keep_space(): boolean {
/** @returns {boolean} */
keep_space() {
if (this.component.component_options.preserveWhitespace) return true;
return this.within_pre();
}
within_pre(): boolean {
/** @returns {boolean} */
within_pre() {
let node = this.parent;
while (node) {
if (node.type === 'Element' && node.name === 'pre') {
@ -55,14 +56,13 @@ export default class Text extends Node {
}
node = node.parent;
}
return false;
}
use_space(): boolean {
/** @returns {boolean} */
use_space() {
if (this.component.compile_options.preserveWhitespace) return false;
if (regex_non_whitespace_characters.test(this.data)) return false;
return !this.within_pre();
}
}

@ -1,29 +1,32 @@
import TemplateScope from './shared/TemplateScope';
import AbstractBlock from './shared/AbstractBlock';
import AwaitBlock from './AwaitBlock';
import Component from '../Component';
import { TemplateNode } from '../../interfaces';
import get_const_tags from './shared/get_const_tags';
import ConstTag from './ConstTag';
import AbstractBlock from './shared/AbstractBlock.js';
import get_const_tags from './shared/get_const_tags.js';
/** @extends AbstractBlock<'ThenBlock'> */
export default class ThenBlock extends AbstractBlock {
type: 'ThenBlock';
scope: TemplateScope;
const_tags: ConstTag[];
/** @type {import('./shared/TemplateScope.js').default} */
scope;
constructor(component: Component, parent: AwaitBlock, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
/** @type {import('./ConstTag.js').default[]} */
const_tags;
/**
* @param {import('../Component.js').default} component
* @param {import('./AwaitBlock.js').default} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').TemplateNode} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.scope = scope.child();
if (parent.then_node) {
parent.then_contexts.forEach((context) => {
if (context.type !== 'DestructuredVariable') return;
this.scope.add(context.key.name, parent.expression.dependencies, this);
});
parent.then_contexts.forEach(
/** @param {any} context */ (context) => {
if (context.type !== 'DestructuredVariable') return;
this.scope.add(context.key.name, parent.expression.dependencies, this);
}
);
}
[this.const_tags, this.children] = get_const_tags(info.children, component, this, parent);
if (!info.skip) {
this.warn_if_empty_block();
}

@ -1,30 +1,35 @@
import Node from './shared/Node';
import map_children, { Children } from './shared/map_children';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
import Node from './shared/Node.js';
import map_children from './shared/map_children.js';
import compiler_errors from '../compiler_errors.js';
/** @extends Node<'Title'> */
export default class Title extends Node {
type: 'Title';
children: Children;
should_cache: boolean;
/** @type {import('./shared/map_children.js').Children} */
children;
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
/** @type {boolean} */
should_cache;
/**
* @param {import('../Component.js').default} component
* @param {import('./shared/Node.js').default} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').TemplateNode} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.children = map_children(component, parent, scope, info.children);
if (info.attributes.length > 0) {
component.error(info.attributes[0], compiler_errors.illegal_attribute_title);
return;
}
info.children.forEach((child) => {
if (child.type !== 'Text' && child.type !== 'MustacheTag') {
return component.error(child, compiler_errors.illegal_structure_title);
info.children.forEach(
/** @param {any} child */ (child) => {
if (child.type !== 'Text' && child.type !== 'MustacheTag') {
return component.error(child, compiler_errors.illegal_structure_title);
}
}
});
);
this.should_cache =
info.children.length === 1
? info.children[0].type !== 'Identifier' || scope.names.has(info.children[0].name)

@ -1,29 +1,34 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import Element from './Element';
import compiler_errors from '../compiler_errors';
import Node from './shared/Node.js';
import Expression from './shared/Expression.js';
import compiler_errors from '../compiler_errors.js';
/** @extends Node<'Transition'> */
export default class Transition extends Node {
type: 'Transition';
name: string;
directive: string;
expression: Expression;
is_local: boolean;
/** @type {string} */
name;
constructor(component: Component, parent: Element, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
/** @type {string} */
directive;
component.warn_if_undefined(info.name, info, scope);
/** @type {import('./shared/Expression.js').default} */
expression;
this.name = info.name;
component.add_reference(this as any, info.name.split('.')[0]);
/** @type {boolean} */
is_local;
/**
* @param {import('../Component.js').default} component
* @param {import('./Element.js').default} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').TemplateNode} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
component.warn_if_undefined(info.name, info, scope);
this.name = info.name;
component.add_reference(/** @type {any} */ (this), info.name.split('.')[0]);
this.directive = info.intro && info.outro ? 'transition' : info.intro ? 'in' : 'out';
this.is_local = info.modifiers.includes('local');
if ((info.intro && parent.intro) || (info.outro && parent.outro)) {
const parent_transition = parent.intro || parent.outro;
component.error(
@ -32,7 +37,6 @@ export default class Transition extends Node {
);
return;
}
this.expression = info.expression
? new Expression(component, this, scope, info.expression)
: null;

@ -1,14 +1,11 @@
import Node from './shared/Node';
import Binding from './Binding';
import EventHandler from './EventHandler';
import flatten_reference from '../utils/flatten_reference';
import fuzzymatch from '../../utils/fuzzymatch';
import list from '../../utils/list';
import Action from './Action';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
import Node from './shared/Node.js';
import Binding from './Binding.js';
import EventHandler from './EventHandler.js';
import flatten_reference from '../utils/flatten_reference.js';
import fuzzymatch from '../../utils/fuzzymatch.js';
import list from '../../utils/list.js';
import Action from './Action.js';
import compiler_errors from '../compiler_errors.js';
const valid_bindings = [
'innerWidth',
@ -21,61 +18,69 @@ const valid_bindings = [
'online'
];
/** @extends Node<'Window'> */
export default class Window extends Node {
type: 'Window';
handlers: EventHandler[] = [];
bindings: Binding[] = [];
actions: Action[] = [];
/** @type {import('./EventHandler.js').default[]} */
handlers = [];
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
info.attributes.forEach((node) => {
if (node.type === 'EventHandler') {
this.handlers.push(new EventHandler(component, this, scope, node));
} else if (node.type === 'Binding') {
if (node.expression.type !== 'Identifier') {
const { parts } = flatten_reference(node.expression);
// TODO is this constraint necessary?
return component.error(node.expression, compiler_errors.invalid_binding_window(parts));
}
/** @type {import('./Binding.js').default[]} */
bindings = [];
if (!~valid_bindings.indexOf(node.name)) {
const match =
node.name === 'width'
? 'innerWidth'
: node.name === 'height'
? 'innerHeight'
: fuzzymatch(node.name, valid_bindings);
/** @type {import('./Action.js').default[]} */
actions = [];
if (match) {
return component.error(
node,
compiler_errors.invalid_binding_on(
node.name,
'<svelte:window>',
` (did you mean '${match}'?)`
)
);
} else {
return component.error(
node,
compiler_errors.invalid_binding_on(
node.name,
'<svelte:window>',
` — valid bindings are ${list(valid_bindings)}`
)
);
/**
* @param {import('../Component.js').default} component
* @param {import('./shared/Node.js').default} parent
* @param {import('./shared/TemplateScope.js').default} scope
* @param {import('../../interfaces.js').TemplateNode} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
info.attributes.forEach(
/** @param {any} node */ (node) => {
if (node.type === 'EventHandler') {
this.handlers.push(new EventHandler(component, this, scope, node));
} else if (node.type === 'Binding') {
if (node.expression.type !== 'Identifier') {
const { parts } = flatten_reference(node.expression);
// TODO is this constraint necessary?
return component.error(node.expression, compiler_errors.invalid_binding_window(parts));
}
if (!~valid_bindings.indexOf(node.name)) {
const match =
node.name === 'width'
? 'innerWidth'
: node.name === 'height'
? 'innerHeight'
: fuzzymatch(node.name, valid_bindings);
if (match) {
return component.error(
node,
compiler_errors.invalid_binding_on(
node.name,
'<svelte:window>',
` (did you mean '${match}'?)`
)
);
} else {
return component.error(
node,
compiler_errors.invalid_binding_on(
node.name,
'<svelte:window>',
` — valid bindings are ${list(valid_bindings)}`
)
);
}
}
this.bindings.push(new Binding(component, this, scope, node));
} else if (node.type === 'Action') {
this.actions.push(new Action(component, this, scope, node));
} else {
// TODO there shouldn't be anything else here...
}
this.bindings.push(new Binding(component, this, scope, node));
} else if (node.type === 'Action') {
this.actions.push(new Action(component, this, scope, node));
} else {
// TODO there shouldn't be anything else here...
}
});
);
}
}

@ -1,5 +1,4 @@
import Tag from './shared/Tag';
import Action from './Action';
import Animation from './Animation';
import Attribute from './Attribute';

@ -1,24 +1,31 @@
import Block from '../../render_dom/Block';
import Component from '../../Component';
import Node from './Node';
import { INode } from '../interfaces';
import compiler_warnings from '../../compiler_warnings';
import Node from './Node.js';
import compiler_warnings from '../../compiler_warnings.js';
const regex_non_whitespace_characters = /[^ \r\n\f\v\t]/;
/**
* @template {string} Type
* @extends Node<Type>
*/
export default class AbstractBlock extends Node {
block: Block;
children: INode[];
/** @type {import('../../render_dom/Block.js').default} */
block;
constructor(component: Component, parent, scope, info: any) {
/** @type {import('../interfaces.js').INode[]} */
children;
/**
* @param {import('../../Component.js').default} component
* @param {any} parent
* @param {any} scope
* @param {any} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
}
warn_if_empty_block() {
if (!this.children || this.children.length > 1) return;
const child = this.children[0];
if (!child || (child.type === 'Text' && !regex_non_whitespace_characters.test(child.data))) {
this.component.warn(this, compiler_warnings.empty_block);
}

@ -1,28 +1,20 @@
import { x } from 'code-red';
import { Node, Identifier, Expression, PrivateIdentifier, Pattern } from 'estree';
import { walk } from 'estree-walker';
import is_reference, { NodeWithPropertyDefinition } from 'is-reference';
import { clone } from '../../../utils/clone';
import Component from '../../Component';
import flatten_reference from '../../utils/flatten_reference';
import TemplateScope from './TemplateScope';
export type Context = DestructuredVariable | ComputedProperty;
interface ComputedProperty {
type: 'ComputedProperty';
property_name: Identifier;
key: Expression | PrivateIdentifier;
}
interface DestructuredVariable {
type: 'DestructuredVariable';
key: Identifier;
name?: string;
modifier: (node: Node) => Node;
default_modifier: (node: Node, to_ctx: (name: string) => Node) => Node;
}
import is_reference from 'is-reference';
import { clone } from '../../../utils/clone.js';
import flatten_reference from '../../utils/flatten_reference.js';
/**
* @param {{
* contexts: Context[];
* node: import('estree').Pattern;
* modifier?: DestructuredVariable['modifier'];
* default_modifier?: DestructuredVariable['default_modifier'];
* scope: import('./TemplateScope.js').default;
* component: import('../../Component.js').default;
* context_rest_properties: Map<string, import('estree').Node>;
* in_rest_element?: boolean;
* }} params
*/
export function unpack_destructuring({
contexts,
node,
@ -32,38 +24,28 @@ export function unpack_destructuring({
component,
context_rest_properties,
in_rest_element = false
}: {
contexts: Context[];
node: Pattern;
modifier?: DestructuredVariable['modifier'];
default_modifier?: DestructuredVariable['default_modifier'];
scope: TemplateScope;
component: Component;
context_rest_properties: Map<string, Node>;
in_rest_element?: boolean;
}) {
if (!node) return;
if (node.type === 'Identifier') {
contexts.push({
type: 'DestructuredVariable',
key: node as Identifier,
key: /** @type {import('estree').Identifier} */ (node),
modifier,
default_modifier
});
if (in_rest_element) {
context_rest_properties.set(node.name, node);
}
} else if (node.type === 'ArrayPattern') {
node.elements.forEach((element: Pattern | null, i: number) => {
node.elements.forEach((element, i) => {
if (!element) {
return;
} else if (element.type === 'RestElement') {
unpack_destructuring({
contexts,
node: element.argument,
modifier: (node) => x`${modifier(node)}.slice(${i})` as Node,
modifier: (node) =>
/** @type {import('estree').Node} */ (x`${modifier(node)}.slice(${i})`),
default_modifier,
scope,
component,
@ -73,18 +55,19 @@ export function unpack_destructuring({
} else if (element.type === 'AssignmentPattern') {
const n = contexts.length;
mark_referenced(element.right, scope, component);
unpack_destructuring({
contexts,
node: element.left,
modifier: (node) => x`${modifier(node)}[${i}]`,
default_modifier: (node, to_ctx) =>
x`${node} !== undefined ? ${node} : ${update_reference(
contexts,
n,
element.right,
to_ctx
)}` as Node,
/** @type {import('estree').Node} */ (
x`${node} !== undefined ? ${node} : ${update_reference(
contexts,
n,
element.right,
to_ctx
)}`
),
scope,
component,
context_rest_properties,
@ -94,7 +77,7 @@ export function unpack_destructuring({
unpack_destructuring({
contexts,
node: element,
modifier: (node) => x`${modifier(node)}[${i}]` as Node,
modifier: (node) => /** @type {import('estree').Node} */ (x`${modifier(node)}[${i}]`),
default_modifier,
scope,
component,
@ -105,14 +88,15 @@ export function unpack_destructuring({
});
} else if (node.type === 'ObjectPattern') {
const used_properties = [];
node.properties.forEach((property) => {
if (property.type === 'RestElement') {
unpack_destructuring({
contexts,
node: property.argument,
modifier: (node) =>
x`@object_without_properties(${modifier(node)}, [${used_properties}])` as Node,
/** @type {import('estree').Node} */ (
x`@object_without_properties(${modifier(node)}, [${used_properties}])`
),
default_modifier,
scope,
component,
@ -123,18 +107,16 @@ export function unpack_destructuring({
const key = property.key;
const value = property.value;
let new_modifier: (node: Node) => Node;
/** @type {(node: import('estree').Node) => import('estree').Node} */
let new_modifier;
if (property.computed) {
// e.g { [computedProperty]: ... }
const property_name = component.get_unique_name('computed_property');
contexts.push({
type: 'ComputedProperty',
property_name,
key
});
new_modifier = (node) => x`${modifier(node)}[${property_name}]`;
used_properties.push(x`${property_name}`);
} else if (key.type === 'Identifier') {
@ -148,24 +130,23 @@ export function unpack_destructuring({
new_modifier = (node) => x`${modifier(node)}["${property_name}"]`;
used_properties.push(x`"${property_name}"`);
}
if (value.type === 'AssignmentPattern') {
// e.g. { property = default } or { property: newName = default }
const n = contexts.length;
mark_referenced(value.right, scope, component);
unpack_destructuring({
contexts,
node: value.left,
modifier: new_modifier,
default_modifier: (node, to_ctx) =>
x`${node} !== undefined ? ${node} : ${update_reference(
contexts,
n,
value.right,
to_ctx
)}` as Node,
/** @type {import('estree').Node} */ (
x`${node} !== undefined ? ${node} : ${update_reference(
contexts,
n,
value.right,
to_ctx
)}`
),
scope,
component,
context_rest_properties,
@ -189,13 +170,16 @@ export function unpack_destructuring({
}
}
function update_reference(
contexts: Context[],
n: number,
expression: Expression,
to_ctx: (name: string) => Node
): Node {
const find_from_context = (node: Identifier) => {
/**
* @param {Context[]} contexts
* @param {number} n
* @param {import('estree').Expression} expression
* @param {(name: string) => import('estree').Node} to_ctx
* @returns {import('estree').Node}
*/
function update_reference(contexts, n, expression, to_ctx) {
/** @param {import('estree').Identifier} node */
const find_from_context = (node) => {
for (let i = n; i < contexts.length; i++) {
const cur_context = contexts[i];
if (cur_context.type !== 'DestructuredVariable') continue;
@ -206,28 +190,35 @@ function update_reference(
}
return to_ctx(node.name);
};
if (expression.type === 'Identifier') {
return find_from_context(expression);
}
// NOTE: avoid unnecessary deep clone?
expression = clone(expression) as Expression;
expression = /** @type {import('estree').Expression} */ (clone(expression));
walk(expression, {
enter(node, parent: Node) {
if (is_reference(node as NodeWithPropertyDefinition, parent as NodeWithPropertyDefinition)) {
this.replace(find_from_context(node as Identifier));
enter(node, parent) {
if (
is_reference(
/** @type {import('is-reference').NodeWithPropertyDefinition} */ (node),
/** @type {import('is-reference').NodeWithPropertyDefinition} */ (parent)
)
) {
this.replace(find_from_context(/** @type {import('estree').Identifier} */ (node)));
this.skip();
}
}
});
return expression;
}
function mark_referenced(node: Node, scope: TemplateScope, component: Component) {
/**
* @param {import('estree').Node} node
* @param {import('./TemplateScope.js').default} scope
* @param {import('../../Component.js').default} component
*/
function mark_referenced(node, scope, component) {
walk(node, {
enter(node: any, parent: any) {
enter(node, parent) {
if (is_reference(node, parent)) {
const { name } = flatten_reference(node);
if (!scope.is_let(name) && !scope.names.has(name)) {
@ -237,3 +228,21 @@ function mark_referenced(node: Node, scope: TemplateScope, component: Component)
}
});
}
/** @typedef {DestructuredVariable | ComputedProperty} Context */
/**
* @typedef {Object} ComputedProperty
* @property {'ComputedProperty'} type
* @property {import('estree').Identifier} property_name
* @property {import('estree').Expression|import('estree').PrivateIdentifier} key
*/
/**
* @typedef {Object} DestructuredVariable
* @property {'DestructuredVariable'} type
* @property {import('estree').Identifier} key
* @property {string} [name]
* @property {(node:import('estree').Node)=>import('estree').Node} modifier
* @property {(node:import('estree').Node,to_ctx:(name:string)=>import('estree').Node)=>import('estree').Node} default_modifier
*/

@ -1,102 +1,110 @@
import Component from '../../Component';
import { walk } from 'estree-walker';
import is_reference from 'is-reference';
import flatten_reference from '../../utils/flatten_reference';
import { create_scopes, Scope, extract_names } from '../../utils/scope';
import { sanitize } from '../../../utils/names';
import TemplateScope from './TemplateScope';
import get_object from '../../utils/get_object';
import Block from '../../render_dom/Block';
import is_dynamic from '../../render_dom/wrappers/shared/is_dynamic';
import flatten_reference from '../../utils/flatten_reference.js';
import { create_scopes, extract_names } from '../../utils/scope.js';
import { sanitize } from '../../../utils/names.js';
import get_object from '../../utils/get_object.js';
import is_dynamic from '../../render_dom/wrappers/shared/is_dynamic.js';
import { b } from 'code-red';
import { invalidate } from '../../render_dom/invalidate';
import { Node, FunctionExpression, Identifier } from 'estree';
import { INode } from '../interfaces';
import { is_reserved_keyword } from '../../utils/reserved_keywords';
import replace_object from '../../utils/replace_object';
import is_contextual from './is_contextual';
import EachBlock from '../EachBlock';
import { clone } from '../../../utils/clone';
import compiler_errors from '../../compiler_errors';
type Owner = INode;
import { invalidate } from '../../render_dom/invalidate.js';
import { is_reserved_keyword } from '../../utils/reserved_keywords.js';
import replace_object from '../../utils/replace_object.js';
import is_contextual from './is_contextual.js';
import { clone } from '../../../utils/clone.js';
import compiler_errors from '../../compiler_errors.js';
const regex_contains_term_function_expression = /FunctionExpression/;
export default class Expression {
type: 'Expression' = 'Expression' as const;
component: Component;
owner: Owner;
node: Node;
references: Set<string> = new Set();
dependencies: Set<string> = new Set();
contextual_dependencies: Set<string> = new Set();
/** @type {'Expression'} */
type = 'Expression';
template_scope: TemplateScope;
scope: Scope;
scope_map: WeakMap<Node, Scope>;
/** @type {import('../../Component.js').default} */
component;
declarations: Array<Node | Node[]> = [];
uses_context = false;
/** @type {import('../interfaces.js').INode} */
owner;
/** @type {import('estree').Node} */
node;
/** @type {Set<string>} */
references = new Set();
/** @type {Set<string>} */
dependencies = new Set();
/** @type {Set<string>} */
contextual_dependencies = new Set();
/** @type {import('./TemplateScope.js').default} */
template_scope;
manipulated: Node;
/** @type {import('../../utils/scope.js').Scope} */
scope;
constructor(
component: Component,
owner: Owner,
template_scope: TemplateScope,
info: Node,
lazy?: boolean
) {
/** @type {WeakMap<import('estree').Node, import('../../utils/scope.js').Scope>} */
scope_map;
/** @type {Array<import('estree').Node | import('estree').Node[]>} */
declarations = [];
/** */
uses_context = false;
/** @type {import('estree').Node} */
manipulated;
/**
* @param {import('../../Component.js').default} component *
* @param {import('../interfaces.js').INode} owner *
* @param {import('./TemplateScope.js').default} template_scope *
* @param {import('estree').Node} info *
* @param {boolean} [lazy] undefined
*/
constructor(component, owner, template_scope, info, lazy) {
// TODO revert to direct property access in prod?
Object.defineProperties(this, {
component: {
value: component
}
});
this.node = info;
this.template_scope = template_scope;
this.owner = owner;
const { dependencies, contextual_dependencies, references } = this;
let { map, scope } = create_scopes(info);
this.scope = scope;
this.scope_map = map;
const expression = this;
let function_expression;
// discover dependencies, but don't change the code yet
walk(info, {
enter(node: any, parent: any, key: string) {
/**
* @param {any} node
* @param {any} parent
* @param {string} key
*/
enter(node, parent, key) {
// don't manipulate shorthand props twice
if (key === 'key' && parent.shorthand) return;
// don't manipulate `import.meta`, `new.target`
if (node.type === 'MetaProperty') return this.skip();
if (map.has(node)) {
scope = map.get(node);
}
if (!function_expression && regex_contains_term_function_expression.test(node.type)) {
function_expression = node;
}
if (is_reference(node, parent)) {
const { name, nodes } = flatten_reference(node);
references.add(name);
if (scope.has(name)) return;
if (name[0] === '$') {
const store_name = name.slice(1);
if (template_scope.names.has(store_name) || scope.has(store_name)) {
return component.error(node, compiler_errors.contextual_store);
}
}
if (template_scope.is_let(name)) {
if (!lazy) {
contextual_dependencies.add(name);
@ -104,33 +112,26 @@ export default class Expression {
}
} else if (template_scope.names.has(name)) {
expression.uses_context = true;
contextual_dependencies.add(name);
const owner = template_scope.get_owner(name);
const is_index = owner.type === 'EachBlock' && owner.key && name === owner.index;
if (!lazy || is_index) {
template_scope.dependencies_for_name
.get(name)
.forEach((name) => dependencies.add(name));
.forEach(/** @param {any} name */ (name) => dependencies.add(name));
}
} else {
if (!lazy) {
dependencies.add(name);
}
component.add_reference(node, name);
component.warn_if_undefined(name, nodes[0], template_scope);
}
this.skip();
}
// track any assignments from template expressions as mutable
let names;
let deep = false;
if (function_expression) {
if (node.type === 'AssignmentExpression') {
deep = node.left.type === 'MemberExpression';
@ -140,119 +141,126 @@ export default class Expression {
names = extract_names(get_object(node.argument));
}
}
if (names) {
names.forEach((name) => {
if (template_scope.names.has(name)) {
if (template_scope.is_const(name)) {
component.error(node, compiler_errors.invalid_const_update(name));
}
template_scope.dependencies_for_name.get(name).forEach((name) => {
names.forEach(
/** @param {any} name */ (name) => {
if (template_scope.names.has(name)) {
if (template_scope.is_const(name)) {
component.error(node, compiler_errors.invalid_const_update(name));
}
template_scope.dependencies_for_name.get(name).forEach(
/** @param {any} name */ (name) => {
const variable = component.var_lookup.get(name);
if (variable) variable[deep ? 'mutated' : 'reassigned'] = true;
}
);
const each_block = template_scope.get_owner(name);
/** @type {import('../EachBlock.js').default} */ (each_block).has_binding = true;
} else {
component.add_reference(node, name);
const variable = component.var_lookup.get(name);
if (variable) variable[deep ? 'mutated' : 'reassigned'] = true;
});
const each_block = template_scope.get_owner(name);
(each_block as EachBlock).has_binding = true;
} else {
component.add_reference(node, name);
const variable = component.var_lookup.get(name);
if (variable) {
variable[deep ? 'mutated' : 'reassigned'] = true;
}
const declaration: any = scope.find_owner(name)?.declarations.get(name);
if (variable) {
variable[deep ? 'mutated' : 'reassigned'] = true;
}
if (declaration) {
if (declaration.kind === 'const' && !deep) {
/** @type {any} */
const declaration = scope.find_owner(name)?.declarations.get(name);
if (declaration) {
if (declaration.kind === 'const' && !deep) {
component.error(node, {
code: 'assignment-to-const',
message: 'You are assigning to a const'
});
}
} else if (variable && variable.writable === false && !deep) {
component.error(node, {
code: 'assignment-to-const',
message: 'You are assigning to a const'
});
}
} else if (variable && variable.writable === false && !deep) {
component.error(node, {
code: 'assignment-to-const',
message: 'You are assigning to a const'
});
}
}
});
);
}
},
leave(node: Node) {
/** @param {import('estree').Node} node */
leave(node) {
if (map.has(node)) {
scope = scope.parent;
}
if (node === function_expression) {
function_expression = null;
}
}
});
}
dynamic_dependencies() {
return Array.from(this.dependencies).filter((name) => {
if (this.template_scope.is_let(name)) return true;
if (is_reserved_keyword(name)) return true;
const variable = this.component.var_lookup.get(name);
return is_dynamic(variable);
});
return Array.from(this.dependencies).filter(
/** @param {any} name */ (name) => {
if (this.template_scope.is_let(name)) return true;
if (is_reserved_keyword(name)) return true;
const variable = this.component.var_lookup.get(name);
return is_dynamic(variable);
}
);
}
dynamic_contextual_dependencies() {
return Array.from(this.contextual_dependencies).filter((name) => {
return Array.from(this.template_scope.dependencies_for_name.get(name)).some(
(variable_name) => {
const variable = this.component.var_lookup.get(variable_name);
return is_dynamic(variable);
}
);
});
return Array.from(this.contextual_dependencies).filter(
/** @param {any} name */ (name) => {
return Array.from(this.template_scope.dependencies_for_name.get(name)).some(
/** @param {any} variable_name */
(variable_name) => {
const variable = this.component.var_lookup.get(variable_name);
return is_dynamic(variable);
}
);
}
);
}
// TODO move this into a render-dom wrapper?
manipulate(block?: Block, ctx?: string | void) {
/**
* @param {import('../../render_dom/Block.js').default} [block]
* @param {string | void} [ctx]
*/
manipulate(block, ctx) {
// TODO ideally we wouldn't end up calling this method
// multiple times
if (this.manipulated) return this.manipulated;
const { component, declarations, scope_map: map, template_scope, owner } = this;
let scope = this.scope;
let function_expression;
let dependencies: Set<string>;
let contextual_dependencies: Set<string>;
/** @type {Set<string>} */
let dependencies;
/** @type {Set<string>} */
let contextual_dependencies;
const node = walk(this.node, {
enter(node: any, parent: any) {
/**
* @param {any} node
* @param {any} parent
*/
enter(node, parent) {
if (node.type === 'Property' && node.shorthand) {
node.value = clone(node.value);
node.shorthand = false;
}
if (map.has(node)) {
scope = map.get(node);
}
if (node.type === 'Identifier' && is_reference(node, parent)) {
const { name } = flatten_reference(node);
if (scope.has(name)) return;
if (function_expression) {
if (template_scope.names.has(name)) {
contextual_dependencies.add(name);
template_scope.dependencies_for_name.get(name).forEach((dependency) => {
dependencies.add(dependency);
});
template_scope.dependencies_for_name.get(name).forEach(
/** @param {any} dependency */ (dependency) => {
dependencies.add(dependency);
}
);
} else {
dependencies.add(name);
component.add_reference(node, name); // TODO is this redundant/misplaced?
@ -261,15 +269,12 @@ export default class Expression {
const reference = block.renderer.reference(node, ctx);
this.replace(reference);
}
this.skip();
}
if (!function_expression) {
if (node.type === 'AssignmentExpression') {
// TODO should this be a warning/error? `<p>{foo = 1}</p>`
}
if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
function_expression = node;
dependencies = new Set();
@ -278,32 +283,33 @@ export default class Expression {
}
},
leave(node: Node, parent: Node) {
/**
* @param {import('estree').Node} node
* @param {import('estree').Node} parent
*/
leave(node, parent) {
if (map.has(node)) scope = scope.parent;
if (node === function_expression) {
const id = component.get_unique_name(sanitize(get_function_name(node, owner)));
const declaration = b`const ${id} = ${node}`;
const extract_functions = () => {
const deps = Array.from(contextual_dependencies);
const function_expression = node as FunctionExpression;
const function_expression = /** @type {import('estree').FunctionExpression} */ (node);
const has_args = function_expression.params.length > 0;
function_expression.params = [
...deps.map((name) => ({ type: 'Identifier', name } as Identifier)),
...deps.map(
/** @param {any} name */ (name) =>
/** @type {import('estree').Identifier} */ ({ type: 'Identifier', name })
),
...function_expression.params
];
const context_args = deps.map((name) => block.renderer.reference(name, ctx));
const context_args = deps.map(
/** @param {any} name */ (name) => block.renderer.reference(name, ctx)
);
component.partly_hoisted.push(declaration);
block.renderer.add_to_context(id.name);
const callee = block.renderer.reference(id);
this.replace(id as any);
this.replace(/** @type {any} */ (id));
const func_declaration = has_args
? b`function ${id}(...args) {
return ${callee}(${context_args}, ...args);
@ -313,20 +319,25 @@ export default class Expression {
}`;
return { deps, func_declaration };
};
if (owner.type === 'ConstTag') {
// we need a combo block/init recipe
if (contextual_dependencies.size === 0) {
let child_scope = scope;
walk(node, {
enter(node: Node, parent: any) {
/**
* @param {import('estree').Node} node
* @param {any} parent
*/
enter(node, parent) {
if (map.has(node)) child_scope = map.get(node);
if (node.type === 'Identifier' && is_reference(node, parent)) {
if (child_scope.has(node.name)) return;
this.replace(block.renderer.reference(node, ctx));
}
},
leave(node: Node) {
/** @param {import('estree').Node} node */
leave(node) {
if (map.has(node)) child_scope = child_scope.parent;
}
});
@ -337,9 +348,7 @@ export default class Expression {
} else if (dependencies.size === 0 && contextual_dependencies.size === 0) {
// we can hoist this out of the component completely
component.fully_hoisted.push(declaration);
this.replace(id as any);
this.replace(/** @type {any} */ (id));
component.add_var(node, {
name: id.name,
internal: true,
@ -349,23 +358,24 @@ export default class Expression {
} else if (contextual_dependencies.size === 0) {
// function can be hoisted inside the component init
component.partly_hoisted.push(declaration);
block.renderer.add_to_context(id.name);
this.replace(block.renderer.reference(id));
} else {
// we need a combo block/init recipe
const { deps, func_declaration } = extract_functions();
if (owner.type === 'Attribute' && owner.parent.name === 'slot') {
const dep_scopes = new Set<INode>(deps.map((name) => template_scope.get_owner(name)));
/** @type {Set<import('../interfaces.js').INode>} */
const dep_scopes = new Set(
deps.map(/** @param {any} name */ (name) => template_scope.get_owner(name))
);
// find the nearest scopes
let node: INode = owner.parent;
/** @type {import('../interfaces.js').INode} */
let node = owner.parent;
while (node && !dep_scopes.has(node)) {
node = node.parent;
}
const func_expression = func_declaration[0];
if (node.type === 'InlineComponent' || node.type === 'SlotTemplate') {
// <Comp let:data />
this.replace(func_expression);
@ -375,7 +385,8 @@ export default class Expression {
block.renderer.add_to_context(func_id.name, true);
// rename #ctx -> child_ctx;
walk(func_expression, {
enter(node: Node) {
/** @param {import('estree').Node} node */
enter(node) {
if (node.type === 'Identifier' && node.name === '#ctx') {
node.name = 'child_ctx';
}
@ -383,11 +394,13 @@ export default class Expression {
});
// add to get_xxx_context
// child_ctx[x] = function () { ... }
(template_scope.get_owner(deps[0]) as EachBlock).contexts.push({
/** @type {import('../EachBlock.js').default} */ (
template_scope.get_owner(deps[0])
).contexts.push({
type: 'DestructuredVariable',
key: func_id,
modifier: () => func_expression,
default_modifier: (node) => node
default_modifier: /** @param {any} node */ (node) => node
});
this.replace(block.renderer.reference(func_id));
}
@ -395,47 +408,44 @@ export default class Expression {
declarations.push(func_declaration);
}
}
function_expression = null;
dependencies = null;
contextual_dependencies = null;
if (parent && parent.type === 'Property') {
parent.method = false;
}
}
if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') {
const assignee = node.type === 'AssignmentExpression' ? node.left : node.argument;
const object_name = get_object(assignee).name;
if (scope.has(object_name)) return;
// normally (`a = 1`, `b.c = 2`), there'll be a single name
// (a or b). In destructuring cases (`[d, e] = [e, d]`) there
// may be more, in which case we need to tack the extra ones
// onto the initial function call
const names = new Set(extract_names(assignee as Node));
const traced: Set<string> = new Set();
names.forEach((name) => {
const dependencies = template_scope.dependencies_for_name.get(name);
if (dependencies) {
dependencies.forEach((name) => traced.add(name));
} else {
traced.add(name);
const names = new Set(extract_names(/** @type {import('estree').Node} */ (assignee)));
/** @type {Set<string>} */
const traced = new Set();
names.forEach(
/** @param {any} name */ (name) => {
const dependencies = template_scope.dependencies_for_name.get(name);
if (dependencies) {
dependencies.forEach(/** @param {any} name */ (name) => traced.add(name));
} else {
traced.add(name);
}
}
});
);
const context = block.bindings.get(object_name);
if (context) {
// for `{#each array as item}`
// replace `item = 1` to `each_array[each_index] = 1`, this allow us to mutate the array
// rather than mutating the local `item` variable
const { snippet, object, property } = context;
const replaced: any = replace_object(assignee, snippet);
/** @type {any} */
const replaced = replace_object(assignee, snippet);
if (node.type === 'AssignmentExpression') {
node.left = replaced;
} else {
@ -444,31 +454,32 @@ export default class Expression {
contextual_dependencies.add(object.name);
contextual_dependencies.add(property.name);
}
this.replace(invalidate(block.renderer, scope, node, traced));
}
}
});
if (declarations.length > 0) {
block.maintain_context = true;
declarations.forEach((declaration) => {
block.chunks.init.push(declaration);
});
declarations.forEach(
/** @param {any} declaration */ (declaration) => {
block.chunks.init.push(declaration);
}
);
}
return (this.manipulated = node as Node);
return (this.manipulated = /** @type {import('estree').Node} */ (node));
}
}
/**
* @param {any} _node
* @param {any} parent
*/
function get_function_name(_node, parent) {
if (parent.type === 'EventHandler') {
return `${parent.name}_handler`;
}
if (parent.type === 'Action') {
return `${parent.name}_function`;
}
return 'func';
}

@ -1,29 +1,66 @@
import Attribute from '../Attribute';
import Component from '../../Component';
import { INode } from '../interfaces';
import Text from '../Text';
import { TemplateNode } from '../../../interfaces';
/**
* @template {string} Type
* @template {import('../interfaces.js').INode} [Parent=import('../interfaces.js').INode]
*/
export default class Node {
readonly start: number;
readonly end: number;
readonly component: Component;
readonly parent: INode;
readonly type: string;
/**
* @readonly
* @type {number}
*/
start;
/**
* @readonly
* @type {number}
*/
end;
/**
* @readonly
* @type {import('../../Component.js').default}
*/
component;
/**
* @readonly
* @type {Parent}
*/
parent;
/**
* @readonly
* @type {Type}
*/
type;
/** @type {import('../interfaces.js').INode} */
prev;
/** @type {import('../interfaces.js').INode} */
next;
/** @type {boolean} */
can_use_innerhtml;
/** @type {boolean} */
is_static_content;
prev?: INode;
next?: INode;
/** @type {string} */
var;
can_use_innerhtml: boolean;
is_static_content: boolean;
var: string;
attributes: Attribute[];
/** @type {import('../Attribute.js').default[]} */
attributes = [];
constructor(component: Component, parent: Node, _scope, info: TemplateNode) {
/**
* @param {import('../../Component.js').default} component
* @param {Node} parent
* @param {any} _scope
* @param {import('../../../interfaces.js').TemplateNode} info
*/
constructor(component, parent, _scope, info) {
this.start = info.start;
this.end = info.end;
this.type = info.type;
this.type = /** @type {Type} */ (info.type);
// this makes properties non-enumerable, which makes logging
// bearable. might have a performance cost. TODO remove in prod?
Object.defineProperties(this, {
@ -34,48 +71,43 @@ export default class Node {
value: parent
}
});
this.can_use_innerhtml = true;
this.is_static_content = true;
}
cannot_use_innerhtml() {
if (this.can_use_innerhtml !== false) {
this.can_use_innerhtml = false;
if (this.parent) this.parent.cannot_use_innerhtml();
}
}
not_static_content() {
this.is_static_content = false;
if (this.parent) this.parent.not_static_content();
}
find_nearest(selector: RegExp) {
/** @param {RegExp} selector */
find_nearest(selector) {
if (selector.test(this.type)) return this;
if (this.parent) return this.parent.find_nearest(selector);
}
get_static_attribute_value(name: string) {
const attribute =
this.attributes &&
this.attributes.find(
(attr: Attribute) => attr.type === 'Attribute' && attr.name.toLowerCase() === name
);
/** @param {string} name */
get_static_attribute_value(name) {
const attribute = this.attributes.find(
/** @param {import('../Attribute.js').default} attr */
(attr) => attr.type === 'Attribute' && attr.name.toLowerCase() === name
);
if (!attribute) return null;
if (attribute.is_true) return true;
if (attribute.chunks.length === 0) return '';
if (attribute.chunks.length === 1 && attribute.chunks[0].type === 'Text') {
return (attribute.chunks[0] as Text).data;
return /** @type {import('../Text.js').default} */ (attribute.chunks[0]).data;
}
return null;
}
has_ancestor(type: string) {
/** @param {string} type */
has_ancestor(type) {
return this.parent ? this.parent.type === type || this.parent.has_ancestor(type) : false;
}
}

@ -1,18 +1,28 @@
import Node from './Node';
import Expression from './Expression';
import Node from './Node.js';
import Expression from './Expression.js';
/**
* @template {'MustacheTag' | 'RawMustacheTag'} [Type='MustacheTag' | 'RawMustacheTag']
* @extends Node<Type>
*/
export default class Tag extends Node {
type: 'MustacheTag' | 'RawMustacheTag';
expression: Expression;
should_cache: boolean;
/** @type {import('./Expression.js').default} */
expression;
/** @type {boolean} */
should_cache;
/**
* @param {any} component
* @param {any} parent
* @param {any} scope
* @param {any} info
*/
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
component.tags.push(this);
this.cannot_use_innerhtml();
this.expression = new Expression(component, this, scope, info.expression);
this.should_cache =
info.expression.type !== 'Identifier' ||
(this.expression.dependencies.size && scope.names.has(info.expression.name));

@ -1,53 +1,64 @@
import EachBlock from '../EachBlock';
import ThenBlock from '../ThenBlock';
import CatchBlock from '../CatchBlock';
import InlineComponent from '../InlineComponent';
import Element from '../Element';
import SlotTemplate from '../SlotTemplate';
import ConstTag from '../ConstTag';
export default class TemplateScope {
/**
* @typedef {import('../EachBlock').default
* | import('../ThenBlock').default
* | import('../CatchBlock').default
* | import('../InlineComponent').default
* | import('../Element').default
* | import('../SlotTemplate').default
* | import('../ConstTag').default} NodeWithScope
*/
type NodeWithScope =
| EachBlock
| ThenBlock
| CatchBlock
| InlineComponent
| Element
| SlotTemplate
| ConstTag;
/** @type {Set<string>} */
names;
export default class TemplateScope {
names: Set<string>;
dependencies_for_name: Map<string, Set<string>>;
owners: Map<string, NodeWithScope> = new Map();
parent?: TemplateScope;
/** @type {Map<string, Set<string>>} */
dependencies_for_name;
/** @type {Map<string, NodeWithScope>} */
owners = new Map();
constructor(parent?: TemplateScope) {
/** @type {TemplateScope} */
parent;
/** @param {TemplateScope} [parent] undefined */
constructor(parent) {
this.parent = parent;
this.names = new Set(parent ? parent.names : []);
this.dependencies_for_name = new Map(parent ? parent.dependencies_for_name : []);
}
add(name, dependencies: Set<string>, owner) {
/**
* @param {any} name
* @param {Set<string>} dependencies
* @param {any} owner
*/
add(name, dependencies, owner) {
this.names.add(name);
this.dependencies_for_name.set(name, dependencies);
this.owners.set(name, owner);
return this;
}
child() {
const child = new TemplateScope(this);
return child;
}
is_top_level(name: string) {
/** @param {string} name */
is_top_level(name) {
return !this.parent || (!this.names.has(name) && this.parent.is_top_level(name));
}
get_owner(name: string): NodeWithScope {
/**
* @param {string} name
* @returns {NodeWithScope}
*/
get_owner(name) {
return this.owners.get(name) || (this.parent && this.parent.get_owner(name));
}
is_let(name: string) {
/** @param {string} name */
is_let(name) {
const owner = this.get_owner(name);
return (
owner &&
@ -57,12 +68,14 @@ export default class TemplateScope {
);
}
is_await(name: string) {
/** @param {string} name */
is_await(name) {
const owner = this.get_owner(name);
return owner && (owner.type === 'ThenBlock' || owner.type === 'CatchBlock');
}
is_const(name: string) {
/** @param {string} name */
is_const(name) {
const owner = this.get_owner(name);
return owner && owner.type === 'ConstTag';
}

@ -1,98 +1,115 @@
import { TemplateNode, ConstTag as ConstTagType } from '../../../interfaces';
import Component from '../../Component';
import ConstTag from '../ConstTag';
import map_children from './map_children';
import { INodeAllowConstTag, INode } from '../interfaces';
import check_graph_for_cycles from '../../utils/check_graph_for_cycles';
import compiler_errors from '../../compiler_errors';
export default function get_const_tags(
children: TemplateNode[],
component: Component,
node: INodeAllowConstTag,
parent: INode
): [ConstTag[], Array<Exclude<INode, ConstTag>>] {
const const_tags: ConstTagType[] = [];
const others: Array<Exclude<TemplateNode, ConstTagType>> = [];
import ConstTag from '../ConstTag.js';
import map_children from './map_children.js';
import check_graph_for_cycles from '../../utils/check_graph_for_cycles.js';
import compiler_errors from '../../compiler_errors.js';
/**
* @param {import('../../../interfaces.js').TemplateNode[]} children
* @param {import('../../Component.js').default} component
* @param {import('../interfaces.js').INodeAllowConstTag} node
* @param {import('../interfaces.js').INode} parent
* @returns {[ConstTag[], Array<Exclude<import('../interfaces.js').INode, ConstTag>>]}
*/
export default function get_const_tags(children, component, node, parent) {
/** @type {import('../../../interfaces.js').ConstTag[]} */
const const_tags = [];
/** @type {Array<Exclude<import('../../../interfaces.js').TemplateNode, import('../../../interfaces.js').ConstTag>>} */
const others = [];
for (const child of children) {
if (child.type === 'ConstTag') {
const_tags.push(child as ConstTagType);
const_tags.push(/** @type {import('../../../interfaces.js').ConstTag} */ (child));
} else {
others.push(child);
}
}
const consts_nodes = const_tags.map((tag) => new ConstTag(component, node, node.scope, tag));
const consts_nodes = const_tags.map(
/** @param {any} tag */ (tag) => new ConstTag(component, node, node.scope, tag)
);
const sorted_consts_nodes = sort_consts_nodes(consts_nodes, component);
sorted_consts_nodes.forEach((node) => node.parse_expression());
sorted_consts_nodes.forEach(/** @param {any} node */ (node) => node.parse_expression());
const children_nodes = map_children(component, parent, node.scope, others);
return [sorted_consts_nodes, children_nodes as Array<Exclude<INode, ConstTag>>];
return [
sorted_consts_nodes,
/** @type {Array<Exclude<import('../interfaces.js').INode, ConstTag>>} */ (children_nodes)
];
}
function sort_consts_nodes(consts_nodes: ConstTag[], component: Component) {
type ConstNode = {
assignees: Set<string>;
dependencies: Set<string>;
node: ConstTag;
};
const sorted_consts_nodes: ConstNode[] = [];
const unsorted_consts_nodes: ConstNode[] = consts_nodes.map((node) => {
return {
assignees: node.assignees,
dependencies: node.dependencies,
node
};
});
/**
* @param {ConstTag[]} consts_nodes
* @param {import('../../Component.js').default} component
*/
function sort_consts_nodes(consts_nodes, component) {
/** @typedef {{ assignees: Set<string>; dependencies: Set<string>; node: ConstTag; }} ConstNode */
/** @type {ConstNode[]} */
const sorted_consts_nodes = [];
/** @type {ConstNode[]} */
const unsorted_consts_nodes = consts_nodes.map(
/** @param {any} node */ (node) => {
return {
assignees: node.assignees,
dependencies: node.dependencies,
node
};
}
);
const lookup = new Map();
unsorted_consts_nodes.forEach((node) => {
node.assignees.forEach((name) => {
if (!lookup.has(name)) {
lookup.set(name, []);
}
lookup.get(name).push(node);
});
});
unsorted_consts_nodes.forEach(
/** @param {any} node */ (node) => {
node.assignees.forEach(
/** @param {any} name */ (name) => {
if (!lookup.has(name)) {
lookup.set(name, []);
}
lookup.get(name).push(node);
}
);
}
);
const cycle = check_graph_for_cycles(
unsorted_consts_nodes.reduce((acc, node) => {
node.assignees.forEach((v) => {
node.dependencies.forEach((w) => {
if (!node.assignees.has(w)) {
acc.push([v, w]);
unsorted_consts_nodes.reduce(
/**
* @param {any} acc
* @param {any} node
*/ (acc, node) => {
node.assignees.forEach(
/** @param {any} v */ (v) => {
node.dependencies.forEach(
/** @param {any} w */ (w) => {
if (!node.assignees.has(w)) {
acc.push([v, w]);
}
}
);
}
});
});
return acc;
}, [])
);
return acc;
},
[]
)
);
if (cycle && cycle.length) {
const nodeList = lookup.get(cycle[0]);
const node = nodeList[0];
component.error(node.node, compiler_errors.cyclical_const_tags(cycle));
}
const add_node = (node: ConstNode) => {
/** @param {ConstNode} node */
const add_node = (node) => {
if (sorted_consts_nodes.includes(node)) return;
node.dependencies.forEach((name) => {
if (node.assignees.has(name)) return;
const earlier_nodes = lookup.get(name);
if (earlier_nodes) {
earlier_nodes.forEach(add_node);
node.dependencies.forEach(
/** @param {any} name */ (name) => {
if (node.assignees.has(name)) return;
const earlier_nodes = lookup.get(name);
if (earlier_nodes) {
earlier_nodes.forEach(add_node);
}
}
});
);
sorted_consts_nodes.push(node);
};
unsorted_consts_nodes.forEach(add_node);
return sorted_consts_nodes.map((node) => node.node);
return sorted_consts_nodes.map(/** @param {any} node */ (node) => node.node);
}

@ -1,18 +1,17 @@
import Component from '../../Component';
import TemplateScope from './TemplateScope';
import { is_reserved_keyword } from '../../utils/reserved_keywords';
import { is_reserved_keyword } from '../../utils/reserved_keywords.js';
export default function is_contextual(component: Component, scope: TemplateScope, name: string) {
/**
* @param {import('../../Component.js').default} component
* @param {import('./TemplateScope.js').default} scope
* @param {string} name
*/
export default function is_contextual(component, scope, name) {
if (is_reserved_keyword(name)) return true;
// if it's a name below root scope, it's contextual
if (!scope.is_top_level(name)) return true;
const variable = component.var_lookup.get(name);
// hoistables, module declarations, and imports are non-contextual
if (!variable || variable.hoistable) return false;
// assume contextual
return true;
}

@ -1,28 +1,28 @@
import AwaitBlock from '../AwaitBlock';
import Body from '../Body';
import ConstTag from '../ConstTag';
import Comment from '../Comment';
import EachBlock from '../EachBlock';
import Document from '../Document';
import Element from '../Element';
import Head from '../Head';
import IfBlock from '../IfBlock';
import InlineComponent from '../InlineComponent';
import KeyBlock from '../KeyBlock';
import MustacheTag from '../MustacheTag';
import Options from '../Options';
import RawMustacheTag from '../RawMustacheTag';
import DebugTag from '../DebugTag';
import Slot from '../Slot';
import SlotTemplate from '../SlotTemplate';
import Text from '../Text';
import Title from '../Title';
import Window from '../Window';
import { TemplateNode } from '../../../interfaces';
import { push_array } from '../../../utils/push_array';
import AwaitBlock from '../AwaitBlock.js';
import Body from '../Body.js';
import ConstTag from '../ConstTag.js';
import Comment from '../Comment.js';
import EachBlock from '../EachBlock.js';
import Document from '../Document.js';
import Element from '../Element.js';
import Head from '../Head.js';
import IfBlock from '../IfBlock.js';
import InlineComponent from '../InlineComponent.js';
import KeyBlock from '../KeyBlock.js';
import MustacheTag from '../MustacheTag.js';
import Options from '../Options.js';
import RawMustacheTag from '../RawMustacheTag.js';
import DebugTag from '../DebugTag.js';
import Slot from '../Slot.js';
import SlotTemplate from '../SlotTemplate.js';
import Text from '../Text.js';
import Title from '../Title.js';
import Window from '../Window.js';
import { push_array } from '../../../utils/push_array.js';
export type Children = ReturnType<typeof map_children>;
/** @typedef {ReturnType<typeof map_children>} Children */
/** @param {any} type */
function get_constructor(type) {
switch (type) {
case 'AwaitBlock':
@ -70,27 +70,29 @@ function get_constructor(type) {
}
}
export default function map_children(component, parent, scope, children: TemplateNode[]) {
/**
* @param {any} component
* @param {any} parent
* @param {any} scope
* @param {import('../../../interfaces.js').TemplateNode[]} children
*/
export default function map_children(component, parent, scope, children) {
let last = null;
let ignores = [];
return children.map((child) => {
const constructor = get_constructor(child.type);
const use_ignores = child.type !== 'Text' && child.type !== 'Comment' && ignores.length;
if (use_ignores) component.push_ignores(ignores);
const node = new constructor(component, parent, scope, child);
if (use_ignores) component.pop_ignores(), (ignores = []);
if (node.type === 'Comment' && node.ignores.length) {
push_array(ignores, node.ignores);
return children.map(
/** @param {any} child */ (child) => {
const constructor = get_constructor(child.type);
const use_ignores = child.type !== 'Text' && child.type !== 'Comment' && ignores.length;
if (use_ignores) component.push_ignores(ignores);
const node = new constructor(component, parent, scope, child);
if (use_ignores) component.pop_ignores(), (ignores = []);
if (node.type === 'Comment' && node.ignores.length) {
push_array(ignores, node.ignores);
}
if (last) last.next = node;
node.prev = last;
last = node;
return node;
}
if (last) last.next = node;
node.prev = last;
last = node;
return node;
});
);
}

@ -1,96 +1,133 @@
import Renderer, { BindingGroup } from './Renderer';
import Wrapper from './wrappers/shared/Wrapper';
import { b, x } from 'code-red';
import { Node, Identifier, ArrayPattern } from 'estree';
import { is_head } from './wrappers/shared/is_head';
import { regex_double_quotes } from '../../utils/patterns';
export interface Bindings {
object: Identifier;
property: Identifier;
snippet: Node;
store: string;
modifier: (node: Node) => Node;
}
export interface BlockOptions {
parent?: Block;
name: Identifier;
type: string;
renderer?: Renderer;
comment?: string;
key?: Identifier;
bindings?: Map<string, Bindings>;
dependencies?: Set<string>;
}
import { is_head } from './wrappers/shared/is_head.js';
import { regex_double_quotes } from '../../utils/patterns.js';
export default class Block {
parent?: Block;
renderer: Renderer;
name: Identifier;
type: string;
comment?: string;
wrappers: Wrapper[];
key: Identifier;
first: Identifier;
dependencies: Set<string> = new Set();
bindings: Map<string, Bindings>;
binding_group_initialised: Set<string> = new Set();
binding_groups: Set<BindingGroup> = new Set();
chunks: {
declarations: Array<Node | Node[]>;
init: Array<Node | Node[]>;
create: Array<Node | Node[]>;
claim: Array<Node | Node[]>;
hydrate: Array<Node | Node[]>;
mount: Array<Node | Node[]>;
measure: Array<Node | Node[]>;
restore_measurements: Array<Node | Node[]>;
fix: Array<Node | Node[]>;
animate: Array<Node | Node[]>;
intro: Array<Node | Node[]>;
update: Array<Node | Node[]>;
outro: Array<Node | Node[]>;
destroy: Array<Node | Node[]>;
};
event_listeners: Node[] = [];
maintain_context: boolean;
has_animation: boolean;
has_intros: boolean;
has_outros: boolean;
has_intro_method: boolean; // could have the method without the transition, due to siblings
has_outro_method: boolean;
outros: number;
aliases: Map<string, Identifier>;
variables: Map<string, { id: Identifier; init?: Node }> = new Map();
get_unique_name: (name: string) => Identifier;
/**
* @typedef {Object} Bindings
* @property {import('estree').Identifier} object
* @property {import('estree').Identifier} property
* @property {import('estree').Node} snippet
* @property {string} store
* @property {(node:import('estree').Node) => import('estree').Node} modifier
*/
/**
* @typedef {Object} BlockOptions
* @property {Block} [parent]
* @property {import('estree').Identifier} name
* @property {string} type
* @property {import('./Renderer.js').default} [renderer]
* @property {string} [comment]
* @property {import('estree').Identifier} [key]
* @property {Map<string,Bindings>} [bindings]
* @property {Set<string>} [dependencies]
*/
/** @type {Block} */
parent;
/** @type {import('./Renderer.js').default} */
renderer;
/** @type {import('estree').Identifier} */
name;
/** @type {string} */
type;
/** @type {string} */
comment;
/** @type {import('./wrappers/shared/Wrapper.js').default[]} */
wrappers;
/** @type {import('estree').Identifier} */
key;
/** @type {import('estree').Identifier} */
first;
/** @type {Set<string>} */
dependencies = new Set();
/** @type {Map<string, Bindings>} */
bindings;
/** @type {Set<string>} */
binding_group_initialised = new Set();
/** @type {Set<import('./Renderer.js').BindingGroup>} */
binding_groups = new Set();
/**
* @type {{
* declarations: Array<import('estree').Node | import('estree').Node[]>;
* init: Array<import('estree').Node | import('estree').Node[]>;
* create: Array<import('estree').Node | import('estree').Node[]>;
* claim: Array<import('estree').Node | import('estree').Node[]>;
* hydrate: Array<import('estree').Node | import('estree').Node[]>;
* mount: Array<import('estree').Node | import('estree').Node[]>;
* measure: Array<import('estree').Node | import('estree').Node[]>;
* restore_measurements: Array<import('estree').Node | import('estree').Node[]>;
* fix: Array<import('estree').Node | import('estree').Node[]>;
* animate: Array<import('estree').Node | import('estree').Node[]>;
* intro: Array<import('estree').Node | import('estree').Node[]>;
* update: Array<import('estree').Node | import('estree').Node[]>;
* outro: Array<import('estree').Node | import('estree').Node[]>;
* destroy: Array<import('estree').Node | import('estree').Node[]>;
* }}
*/
chunks;
/** @type {import('estree').Node[]} */
event_listeners = [];
/** @type {boolean} */
maintain_context;
/** @type {boolean} */
has_animation;
/** @type {boolean} */
has_intros;
/** @type {boolean} */
has_outros;
/** @type {boolean} */
has_intro_method; // could have the method without the transition, due to siblings
/** @type {boolean} */
has_outro_method;
/** @type {number} */
outros;
/** @type {Map<string, import('estree').Identifier>} */
aliases;
/** @type {Map<string, { id: import('estree').Identifier; init?: import('estree').Node }>} */
variables = new Map();
/** @type {(name: string) => import('estree').Identifier} */
get_unique_name;
/** */
has_update_method = false;
autofocus?: { element_var: string; condition_expression?: any };
constructor(options: BlockOptions) {
/** @type {{ element_var: string; condition_expression?: any }} */
autofocus;
/** @param {BlockOptions} options */
constructor(options) {
this.parent = options.parent;
this.renderer = options.renderer;
this.name = options.name;
this.type = options.type;
this.comment = options.comment;
this.wrappers = [];
// for keyed each blocks
this.key = options.key;
this.first = null;
this.bindings = options.bindings;
this.chunks = {
declarations: [],
init: [],
@ -107,44 +144,35 @@ export default class Block {
outro: [],
destroy: []
};
this.has_animation = false;
this.has_intro_method = false; // a block could have an intro method but not intro transitions, e.g. if a sibling block has intros
this.has_outro_method = false;
this.outros = 0;
this.get_unique_name = this.renderer.component.get_unique_name_maker();
this.aliases = new Map();
if (this.key) this.aliases.set('key', this.get_unique_name('key'));
}
assign_variable_names() {
const seen: Set<string> = new Set();
const dupes: Set<string> = new Set();
/** @type {Set<string>} */
const seen = new Set();
/** @type {Set<string>} */
const dupes = new Set();
let i = this.wrappers.length;
while (i--) {
const wrapper = this.wrappers[i];
if (!wrapper.var) continue;
if (seen.has(wrapper.var.name)) {
dupes.add(wrapper.var.name);
}
seen.add(wrapper.var.name);
}
const counts = new Map();
i = this.wrappers.length;
while (i--) {
const wrapper = this.wrappers[i];
if (!wrapper.var) continue;
let suffix = '';
if (dupes.has(wrapper.var.name)) {
const i = counts.get(wrapper.var.name) || 0;
@ -155,31 +183,30 @@ export default class Block {
}
}
add_dependencies(dependencies: Set<string>) {
/** @param {Set<string>} dependencies */
add_dependencies(dependencies) {
dependencies.forEach((dependency) => {
this.dependencies.add(dependency);
});
this.has_update_method = true;
if (this.parent) {
this.parent.add_dependencies(dependencies);
}
}
add_element(
id: Identifier,
render_statement: Node,
claim_statement: Node,
parent_node: Node,
no_detach?: boolean
) {
/**
* @param {import('estree').Identifier} id
* @param {import('estree').Node} render_statement
* @param {import('estree').Node} claim_statement
* @param {import('estree').Node} parent_node
* @param {boolean} [no_detach]
*/
add_element(id, render_statement, claim_statement, parent_node, no_detach) {
this.add_variable(id);
this.chunks.create.push(b`${id} = ${render_statement};`);
if (this.renderer.options.hydratable) {
this.chunks.claim.push(b`${id} = ${claim_statement || render_statement};`);
}
if (parent_node) {
this.chunks.mount.push(b`@append(${parent_node}, ${id});`);
if (is_head(parent_node) && !no_detach) this.chunks.destroy.push(b`@detach(${id});`);
@ -189,12 +216,14 @@ export default class Block {
}
}
add_intro(local?: boolean) {
/** @param {boolean} [local] */
add_intro(local) {
this.has_intros = this.has_intro_method = true;
if (!local && this.parent) this.parent.add_intro();
}
add_outro(local?: boolean) {
/** @param {boolean} [local] */
add_outro(local) {
this.has_outros = this.has_outro_method = true;
this.outros += 1;
if (!local && this.parent) this.parent.add_outro();
@ -204,42 +233,43 @@ export default class Block {
this.has_animation = true;
}
add_variable(id: Identifier, init?: Node) {
/**
* @param {import('estree').Identifier} id
* @param {import('estree').Node} [init]
*/
add_variable(id, init) {
if (this.variables.has(id.name)) {
throw new Error(`Variable '${id.name}' already initialised with a different value`);
}
this.variables.set(id.name, { id, init });
}
alias(name: string) {
/** @param {string} name */
alias(name) {
if (!this.aliases.has(name)) {
this.aliases.set(name, this.get_unique_name(name));
}
return this.aliases.get(name);
}
child(options: BlockOptions) {
/** @param {BlockOptions} options */
child(options) {
return new Block(Object.assign({}, this, { key: null }, options, { parent: this }));
}
get_contents(key?: any) {
/** @param {any} [key] */
get_contents(key) {
const { dev } = this.renderer.options;
if (this.has_outros) {
this.add_variable({ type: 'Identifier', name: '#current' });
if (this.chunks.intro.length > 0) {
this.chunks.intro.push(b`#current = true;`);
this.chunks.mount.push(b`#current = true;`);
}
if (this.chunks.outro.length > 0) {
this.chunks.outro.push(b`#current = false;`);
}
}
if (this.autofocus) {
if (this.autofocus.condition_expression) {
this.chunks.mount.push(
@ -249,34 +279,28 @@ export default class Block {
this.chunks.mount.push(b`${this.autofocus.element_var}.focus();`);
}
}
this.render_binding_groups();
this.render_listeners();
const properties: Record<string, any> = {};
/** @type {Record<string, any>} */
const properties = {};
const noop = x`@noop`;
properties.key = key;
if (this.first) {
properties.first = x`null`;
this.chunks.hydrate.push(b`this.first = ${this.first};`);
}
if (this.chunks.create.length === 0 && this.chunks.hydrate.length === 0) {
properties.create = noop;
} else {
const hydrate =
this.chunks.hydrate.length > 0 &&
(this.renderer.options.hydratable ? b`this.h();` : this.chunks.hydrate);
properties.create = x`function #create() {
${this.chunks.create}
${hydrate}
}`;
}
if (this.renderer.options.hydratable || this.chunks.claim.length > 0) {
if (this.chunks.claim.length === 0 && this.chunks.hydrate.length === 0) {
properties.claim = noop;
@ -287,13 +311,11 @@ export default class Block {
}`;
}
}
if (this.renderer.options.hydratable && this.chunks.hydrate.length > 0) {
properties.hydrate = x`function #hydrate() {
${this.chunks.hydrate}
}`;
}
if (this.chunks.mount.length === 0) {
properties.mount = noop;
} else if (this.event_listeners.length === 0) {
@ -305,45 +327,39 @@ export default class Block {
${this.chunks.mount}
}`;
}
if (this.has_update_method || this.maintain_context) {
if (this.chunks.update.length === 0 && !this.maintain_context) {
properties.update = noop;
} else {
const ctx = this.maintain_context ? x`#new_ctx` : x`#ctx`;
let dirty: Identifier | ArrayPattern = { type: 'Identifier', name: '#dirty' };
/** @type {import('estree').Identifier | import('estree').ArrayPattern} */
let dirty = { type: 'Identifier', name: '#dirty' };
if (!this.renderer.context_overflow && !this.parent) {
dirty = { type: 'ArrayPattern', elements: [dirty] };
}
properties.update = x`function #update(${ctx}, ${dirty}) {
${this.maintain_context && b`#ctx = ${ctx};`}
${this.chunks.update}
}`;
}
}
if (this.has_animation) {
properties.measure = x`function #measure() {
${this.chunks.measure}
}`;
if (this.chunks.restore_measurements.length) {
properties.restore_measurements = x`function #restore_measurements(#measurement) {
${this.chunks.restore_measurements}
}`;
}
properties.fix = x`function #fix() {
${this.chunks.fix}
}`;
properties.animate = x`function #animate() {
${this.chunks.animate}
}`;
}
if (this.has_intro_method || this.has_outro_method) {
if (this.chunks.intro.length === 0) {
properties.intro = noop;
@ -353,7 +369,6 @@ export default class Block {
${this.chunks.intro}
}`;
}
if (this.chunks.outro.length === 0) {
properties.outro = noop;
} else {
@ -362,7 +377,6 @@ export default class Block {
}`;
}
}
if (this.chunks.destroy.length === 0) {
properties.destroy = noop;
} else {
@ -370,7 +384,6 @@ export default class Block {
${this.chunks.destroy}
}`;
}
if (!this.renderer.component.compile_options.dev) {
// allow shorthand names
for (const name in properties) {
@ -379,7 +392,8 @@ export default class Block {
}
}
const return_value: any = x`{
/** @type {any} */
const return_value = x`{
key: ${properties.key},
first: ${properties.first},
c: ${properties.create},
@ -395,9 +409,7 @@ export default class Block {
o: ${properties.outro},
d: ${properties.destroy}
}`;
const block = dev && this.get_unique_name('block');
const body = b`
${this.chunks.declarations}
@ -423,11 +435,11 @@ export default class Block {
return ${return_value};`
}
`;
return body;
}
has_content(): boolean {
/** @returns {boolean} */
has_content() {
return (
!!this.first ||
this.event_listeners.length > 0 ||
@ -446,13 +458,12 @@ export default class Block {
render() {
const key = this.key && this.get_unique_name('key');
const args: any[] = [x`#ctx`];
/** @type {any[]} */
const args = [x`#ctx`];
if (key) args.unshift(key);
const fn = b`function ${this.name}(${args}) {
${this.get_contents(key)}
}`;
return this.comment
? b`
// ${this.comment}
@ -460,28 +471,25 @@ export default class Block {
: fn;
}
render_listeners(chunk: string = '') {
/** @param {string} chunk */
render_listeners(chunk = '') {
if (this.event_listeners.length > 0) {
this.add_variable({ type: 'Identifier', name: '#mounted' });
this.chunks.destroy.push(b`#mounted = false`);
const dispose: Identifier = {
/** @type {import('estree').Identifier} */
const dispose = {
type: 'Identifier',
name: `#dispose${chunk}`
};
this.add_variable(dispose);
if (this.event_listeners.length === 1) {
this.chunks.mount.push(
b`
this.chunks.mount.push(b`
if (!#mounted) {
${dispose} = ${this.event_listeners[0]};
#mounted = true;
}
`
);
`);
this.chunks.destroy.push(b`${dispose}();`);
} else {
this.chunks.mount.push(b`
@ -492,12 +500,10 @@ export default class Block {
#mounted = true;
}
`);
this.chunks.destroy.push(b`@run_all(${dispose});`);
}
}
}
render_binding_groups() {
for (const binding_group of this.binding_groups) {
binding_group.render(this);

@ -1,103 +1,103 @@
import Block from './Block';
import { CompileOptions, Var } from '../../interfaces';
import Component from '../Component';
import FragmentWrapper from './wrappers/Fragment';
import Block from './Block.js';
import FragmentWrapper from './wrappers/Fragment.js';
import { x } from 'code-red';
import {
Node,
Identifier,
MemberExpression,
Literal,
Expression,
BinaryExpression,
UnaryExpression,
ArrayExpression
} from 'estree';
import flatten_reference from '../utils/flatten_reference';
import { reserved_keywords } from '../utils/reserved_keywords';
import { renderer_invalidate } from './invalidate';
import flatten_reference from '../utils/flatten_reference.js';
import { reserved_keywords } from '../utils/reserved_keywords.js';
import { renderer_invalidate } from './invalidate.js';
interface ContextMember {
name: string;
index: Literal;
is_contextual: boolean;
is_non_contextual: boolean;
variable: Var;
priority: number;
}
export default class Renderer {
/**
* @typedef {Object} ContextMember
* @property {string} name
* @property {import('estree').Literal} index
* @property {boolean} is_contextual
* @property {boolean} is_non_contextual
* @property {import('../../interfaces.js').Var} variable
* @property {number} priority
*/
type BitMasks = Array<{
n: number;
names: string[];
}>;
/**
* @typedef {Array<{
* n: number;
* names: string[];
* }>} BitMasks
*/
export interface BindingGroup {
binding_group: (to_reference?: boolean) => Node;
contexts: string[];
list_dependencies: Set<string>;
keypath: string;
add_element: (block: Block, element: Identifier) => void;
render: (block: Block) => void;
}
/** @type {import('../Component.js').default} */
component; // TODO Maybe Renderer shouldn't know about Component?
export default class Renderer {
component: Component; // TODO Maybe Renderer shouldn't know about Component?
options: CompileOptions;
/** @type {import('../../interfaces.js').CompileOptions} */
options;
/** @type {ContextMember[]} */
context = [];
/** @type {ContextMember[]} */
initial_context = [];
/** @type {Map<string, ContextMember>} */
context_lookup = new Map();
/** @type {boolean} */
context_overflow;
context: ContextMember[] = [];
initial_context: ContextMember[] = [];
context_lookup: Map<string, ContextMember> = new Map();
context_overflow: boolean;
blocks: Array<Block | Node | Node[]> = [];
readonly: Set<string> = new Set();
meta_bindings: Array<Node | Node[]> = []; // initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
binding_groups: Map<string, BindingGroup> = new Map();
/** @type {Array<import('./Block.js').default | import('estree').Node | import('estree').Node[]>} */
blocks = [];
block: Block;
fragment: FragmentWrapper;
/** @type {Set<string>} */
readonly = new Set();
file_var: Identifier;
locate: (c: number) => { line: number; column: number };
/** @type {Array<import('estree').Node | import('estree').Node[]>} */
meta_bindings = []; // initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
constructor(component: Component, options: CompileOptions) {
/** @type {Map<string, BindingGroup>} */
binding_groups = new Map();
/** @type {import('./Block.js').default} */
block;
/** @type {import('./wrappers/Fragment.js').default} */
fragment;
/** @type {import('estree').Identifier} */
file_var;
/** @type {(c: number) => { line: number; column: number }} */
locate;
/**
* @param {import('../Component.js').default} component
* @param {import('../../interfaces.js').CompileOptions} options
*/
constructor(component, options) {
this.component = component;
this.options = options;
this.locate = component.locate; // TODO messy
this.file_var = options.dev && this.component.get_unique_name('file');
component.vars
.filter((v) => !v.hoistable || (v.export_name && !v.module))
.forEach((v) => this.add_to_context(v.name));
// ensure store values are included in context
component.vars.filter((v) => v.subscribable).forEach((v) => this.add_to_context(`$${v.name}`));
reserved_keywords.forEach((keyword) => {
if (component.var_lookup.has(keyword)) {
this.add_to_context(keyword);
}
});
if (component.slots.size > 0) {
this.add_to_context('$$scope');
this.add_to_context('#slots');
}
// main block
this.block = new Block({
renderer: this,
name: null,
type: 'component',
key: null,
bindings: new Map(),
dependencies: new Set()
});
this.block.has_update_method = true;
this.fragment = new FragmentWrapper(
this,
this.block,
@ -106,26 +106,20 @@ export default class Renderer {
true,
null
);
// TODO messy
this.blocks.forEach((block) => {
if (block instanceof Block) {
block.assign_variable_names();
}
});
this.block.assign_variable_names();
this.fragment.render(this.block, null, x`#nodes` as Identifier);
this.fragment.render(this.block, null, /** @type {import('estree').Identifier} */ (x`#nodes`));
this.context_overflow = this.context.length > 31;
this.context.forEach((member) => {
const { variable } = member;
if (variable) {
member.priority += 2;
if (variable.mutated || variable.reassigned) member.priority += 4;
// these determine whether variable is included in initial context
// array, so must have the highest priority
if (variable.is_reactive_dependency && (variable.mutated || variable.reassigned))
@ -137,17 +131,16 @@ export default class Renderer {
// array, so must have the highest priority
member.priority += 8;
}
if (!member.is_contextual) {
member.priority += 1;
}
});
this.context.sort(
(a, b) => b.priority - a.priority || (a.index.value as number) - (b.index.value as number)
(a, b) =>
b.priority - a.priority ||
/** @type {number} */ (a.index.value) - /** @type {number} */ (b.index.value)
);
this.context.forEach((member, i) => (member.index.value = i));
let i = this.context.length;
while (i--) {
const member = this.context[i];
@ -166,69 +159,74 @@ export default class Renderer {
this.initial_context = this.context.slice(0, i + 1);
}
add_to_context(name: string, contextual = false) {
/**
* @param {string} name
* @param {any} contextual
*/
add_to_context(name, contextual = false) {
if (!this.context_lookup.has(name)) {
const member: ContextMember = {
/** @type {ContextMember} */
const member = {
name,
index: { type: 'Literal', value: this.context.length }, // index is updated later, but set here to preserve order within groups
index: { type: 'Literal', value: this.context.length },
is_contextual: false,
is_non_contextual: false, // shadowed vars could be contextual and non-contextual
is_non_contextual: false,
variable: null,
priority: 0
};
this.context_lookup.set(name, member);
this.context.push(member);
}
const member = this.context_lookup.get(name);
if (contextual) {
member.is_contextual = true;
} else {
member.is_non_contextual = true;
member.variable = this.component.var_lookup.get(name);
}
return member;
}
invalidate(name: string, value?: unknown, main_execution_context: boolean = false) {
/**
* @param {string} name
* @param {unknown} [value]
* @param {boolean} main_execution_context
*/
invalidate(name, value, main_execution_context = false) {
return renderer_invalidate(this, name, value, main_execution_context);
}
dirty(names: string[], is_reactive_declaration = false): Expression {
/**
* @param {string[]} names
* @param {any} is_reactive_declaration
* @returns {import('estree').Expression}
*/
dirty(names, is_reactive_declaration = false) {
const renderer = this;
const dirty = (is_reactive_declaration ? x`$$self.$$.dirty` : x`#dirty`) as
| Identifier
| MemberExpression;
const dirty = /** @type {| import('estree').Identifier
| import('estree').MemberExpression} */ (
is_reactive_declaration ? x`$$self.$$.dirty` : x`#dirty`
);
const get_bitmask = () => {
const bitmask: BitMasks = [];
/** @type {BitMasks} */
const bitmask = [];
names.forEach((name) => {
const member = renderer.context_lookup.get(name);
if (!member) return;
if (member.index.value === -1) {
throw new Error('unset index');
}
const value = member.index.value as number;
const value = /** @type {number} */ (member.index.value);
const i = (value / 31) | 0;
const n = 1 << value % 31;
if (!bitmask[i]) bitmask[i] = { n: 0, names: [] };
bitmask[i].n |= n;
bitmask[i].names.push(name);
});
return bitmask;
};
// TODO: context-overflow make it less gross
return {
return /** @type {any} */ ({
// Using a ParenthesizedExpression allows us to create
// the expression lazily. TODO would be better if
// context was determined before rendering, so that
@ -236,11 +234,11 @@ export default class Renderer {
type: 'ParenthesizedExpression',
get expression() {
const bitmask = get_bitmask();
if (!bitmask.length) {
return x`${dirty} & /*${names.join(', ')}*/ 0` as BinaryExpression;
return /** @type {import('estree').BinaryExpression} */ (
x`${dirty} & /*${names.join(', ')}*/ 0`
);
}
if (renderer.context_overflow) {
return bitmask
.map((b, i) => ({ b, i }))
@ -248,18 +246,22 @@ export default class Renderer {
.map(({ b, i }) => x`${dirty}[${i}] & /*${b.names.join(', ')}*/ ${b.n}`)
.reduce((lhs, rhs) => x`${lhs} | ${rhs}`);
}
return x`${dirty} & /*${names.join(', ')}*/ ${bitmask[0].n}` as BinaryExpression;
return /** @type {import('estree').BinaryExpression} */ (
x`${dirty} & /*${names.join(', ')}*/ ${bitmask[0].n}`
);
}
} as any;
});
}
// NOTE: this method may be called before this.context_overflow / this.context is fully defined
// therefore, they can only be evaluated later in a getter function
get_initial_dirty(): UnaryExpression | ArrayExpression {
/** @returns {import('estree').UnaryExpression | import('estree').ArrayExpression} */
get_initial_dirty() {
const _this = this;
// TODO: context-overflow make it less gross
const val: UnaryExpression = x`-1` as UnaryExpression;
/** @type {import('estree').UnaryExpression} */
const val = /** @type {import('estree').UnaryExpression} */ (x`-1`);
return {
get type() {
return _this.context_overflow ? 'ArrayExpression' : 'UnaryExpression';
@ -279,32 +281,43 @@ export default class Renderer {
};
}
reference(node: string | Identifier | MemberExpression, ctx: string | void = '#ctx') {
/**
* @param {string | import('estree').Identifier | import('estree').MemberExpression} node
* @param {string | void} ctx
*/
reference(node, ctx = '#ctx') {
if (typeof node === 'string') {
node = { type: 'Identifier', name: node };
}
const { name, nodes } = flatten_reference(node);
const member = this.context_lookup.get(name);
// TODO is this correct?
if (this.component.var_lookup.get(name)) {
this.component.add_reference(node, name);
}
if (member !== undefined) {
const replacement = x`/*${member.name}*/ ${ctx}[${member.index}]` as MemberExpression;
const replacement = /** @type {import('estree').MemberExpression} */ (
x`/*${member.name}*/ ${ctx}[${member.index}]`
);
if (nodes[0].loc) replacement.object.loc = nodes[0].loc;
nodes[0] = replacement;
return nodes.reduce((lhs, rhs) => x`${lhs}.${rhs}`);
}
return node;
}
remove_block(block: Block | Node | Node[]) {
/** @param {import('./Block.js').default | import('estree').Node | import('estree').Node[]} block */
remove_block(block) {
this.blocks.splice(this.blocks.indexOf(block), 1);
}
}
/**
* @typedef {Object} BindingGroup
* @property {(to_reference?:boolean)=>import('estree').Node} binding_group
* @property {string[]} contexts
* @property {Set<string>} list_dependencies
* @property {string} keypath
* @property {(block:Block,element:import('estree').PrivateIdentifier) => void} add_element
* @property {(block:Block)=>void} render
*/

@ -1,66 +1,48 @@
import type { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping';
import { b, x, p } from 'code-red';
import Component from '../Component';
import Renderer from './Renderer';
import { CompileOptions, CssResult } from '../../interfaces';
import Renderer from './Renderer.js';
import { walk } from 'estree-walker';
import { extract_names, Scope } from 'periscopic';
import { invalidate } from './invalidate';
import Block from './Block';
import {
ImportDeclaration,
ClassDeclaration,
Node,
Statement,
ObjectExpression,
Expression
} from 'estree';
import { apply_preprocessor_sourcemap } from '../../utils/mapped_code';
import { flatten } from '../../utils/flatten';
import check_enable_sourcemap from '../utils/check_enable_sourcemap';
import { push_array } from '../../utils/push_array';
export default function dom(
component: Component,
options: CompileOptions
): { js: Node[]; css: CssResult } {
import { extract_names } from 'periscopic';
import { invalidate } from './invalidate.js';
import { apply_preprocessor_sourcemap } from '../../utils/mapped_code.js';
import { flatten } from '../../utils/flatten.js';
import check_enable_sourcemap from '../utils/check_enable_sourcemap.js';
import { push_array } from '../../utils/push_array.js';
/**
* @param {import('../Component.js').default} component
* @param {import('../../interfaces.js').CompileOptions} options
* @returns {{ js: import('estree').Node[]; css: import('../../interfaces.js').CssResult; }}
*/
export default function dom(component, options) {
const { name } = component;
const renderer = new Renderer(component, options);
const { block } = renderer;
block.has_outro_method = true;
/** @type {import('estree').Node[][]} */
const body = [];
if (renderer.file_var) {
const file = component.file ? x`"${component.file}"` : x`undefined`;
body.push(b`const ${renderer.file_var} = ${file};`);
}
const css = component.stylesheet.render(options.filename);
const css_sourcemap_enabled = check_enable_sourcemap(options.enableSourcemap, 'css');
if (css_sourcemap_enabled) {
css.map = apply_preprocessor_sourcemap(
options.filename,
css.map,
options.sourcemap as string | RawSourceMap | DecodedSourceMap
/** @type {string | import('@ampproject/remapping').RawSourceMap | import('@ampproject/remapping').DecodedSourceMap} */ (
options.sourcemap
)
);
} else {
css.map = null;
}
const styles =
css_sourcemap_enabled && component.stylesheet.has_styles && options.dev
? `${css.code}\n/*# sourceMappingURL=${css.map.toUrl()} */`
: css.code;
const add_css = component.get_unique_name('add_css');
const should_add_css = !!styles && (options.customElement || options.css === 'injected');
if (should_add_css) {
body.push(b`
function ${add_css}(target) {
@ -68,41 +50,38 @@ export default function dom(
}
`);
}
// fix order
// TODO the deconflicted names of blocks are reversed... should set them here
const blocks = renderer.blocks.slice().reverse();
push_array(
body,
blocks.map((block) => {
// TODO this is a horrible mess — renderer.blocks
// contains a mixture of Blocks and Nodes
if ((block as Block).render) return (block as Block).render();
if (/** @type {import('./Block.js').default} */ (block).render)
return /** @type {import('./Block.js').default} */ (block).render();
return block;
})
);
if (options.dev && !options.hydratable) {
block.chunks.claim.push(
b`throw new @_Error("options.hydrate only works if the component was compiled with the \`hydratable: true\` option");`
);
}
const uses_slots = component.var_lookup.has('$$slots');
let compute_slots: Node[] | undefined;
/** @type {import('estree').Node[] | undefined} */
let compute_slots;
if (uses_slots) {
compute_slots = b`
const $$slots = @compute_slots(#slots);
`;
}
const uses_props = component.var_lookup.has('$$props');
const uses_rest = component.var_lookup.has('$$restProps');
const $$props = uses_props || uses_rest ? '$$new_props' : '$$props';
const props = component.vars.filter((variable) => !variable.module && variable.export_name);
const writable_props = props.filter((variable) => variable.writable);
const omit_props_names = component.get_unique_name('omit_props_names');
const compute_rest = x`@compute_rest_props($$props, ${omit_props_names.name})`;
const rest = uses_rest
@ -111,7 +90,6 @@ export default function dom(
let $$restProps = ${compute_rest};
`
: null;
const set =
uses_props || uses_rest || writable_props.length > 0 || component.slots.size > 0
? x`
@ -146,18 +124,22 @@ export default function dom(
}
`
: null;
const accessors = [];
const not_equal = component.component_options.immutable ? x`@not_equal` : x`@safe_not_equal`;
let missing_props_check: Node[] | Node;
let inject_state: Expression;
let capture_state: Expression;
let props_inject: Node[] | Node;
/** @type {import('estree').Node[] | import('estree').Node} */
let missing_props_check;
/** @type {import('estree').Expression} */
let inject_state;
/** @type {import('estree').Expression} */
let capture_state;
/** @type {import('estree').Node[] | import('estree').Node} */
let props_inject;
props.forEach((prop) => {
const variable = component.var_lookup.get(prop.name);
if (!variable.writable || component.component_options.accessors) {
accessors.push({
type: 'MethodDefinition',
@ -181,7 +163,6 @@ export default function dom(
}`
});
}
if (component.component_options.accessors) {
if (variable.writable && !renderer.readonly.has(prop.name)) {
accessors.push({
@ -214,7 +195,6 @@ export default function dom(
});
}
});
component.instance_exports_from.forEach((exports_from) => {
const import_declaration = {
...exports_from,
@ -222,8 +202,7 @@ export default function dom(
specifiers: [],
source: exports_from.source
};
component.imports.push(import_declaration as ImportDeclaration);
component.imports.push(/** @type {import('estree').ImportDeclaration} */ (import_declaration));
exports_from.specifiers.forEach((specifier) => {
if (component.component_options.accessors) {
const name = component.get_unique_name(specifier.exported.name);
@ -233,7 +212,6 @@ export default function dom(
imported: specifier.local,
local: name
});
accessors.push({
type: 'MethodDefinition',
kind: 'get',
@ -254,11 +232,9 @@ export default function dom(
}
});
});
if (component.compile_options.dev) {
// checking that expected ones were passed
const expected = props.filter((prop) => prop.writable && !prop.initialised);
if (expected.length) {
missing_props_check = b`
$$self.$$.on_mount.push(function () {
@ -271,19 +247,15 @@ export default function dom(
});
`;
}
const capturable_vars = component.vars.filter(
(v) => !v.internal && !v.global && !v.name.startsWith('$$')
);
if (capturable_vars.length > 0) {
capture_state = x`() => ({ ${capturable_vars.map((prop) => p`${prop.name}`)} })`;
}
const injectable_vars = capturable_vars.filter(
(v) => !v.module && v.writable && v.name[0] !== '$'
);
if (uses_props || injectable_vars.length > 0) {
inject_state = x`
${$$props} => {
@ -300,7 +272,6 @@ export default function dom(
)}
}
`;
props_inject = b`
if ($$props && "$$inject" in $$props) {
$$self.$inject_state($$props.$$inject);
@ -308,18 +279,17 @@ export default function dom(
`;
}
}
// instrument assignments
if (component.ast.instance) {
let scope = component.instance_scope;
const map = component.instance_scope_map;
let execution_context: Node | null = null;
/** @type {import('estree').Node | null} */
let execution_context = null;
walk(component.ast.instance.content, {
enter(node: Node) {
enter(node) {
if (map.has(node)) {
scope = map.get(node) as Scope;
scope = /** @type {import('periscopic').Scope} */ (map.get(node));
if (!execution_context && !scope.block) {
execution_context = node;
}
@ -331,47 +301,37 @@ export default function dom(
execution_context = node;
}
},
leave(node: Node) {
leave(node) {
if (map.has(node)) {
scope = scope.parent;
}
if (execution_context === node) {
execution_context = null;
}
if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') {
const assignee = node.type === 'AssignmentExpression' ? node.left : node.argument;
// normally (`a = 1`, `b.c = 2`), there'll be a single name
// (a or b). In destructuring cases (`[d, e] = [e, d]`) there
// may be more, in which case we need to tack the extra ones
// onto the initial function call
const names = new Set(extract_names(assignee as Node));
const names = new Set(extract_names(/** @type {import('estree').Node} */ (assignee)));
this.replace(invalidate(renderer, scope, node, names, execution_context === null));
}
}
});
component.rewrite_props(({ name, reassigned, export_name }) => {
const value = `$${name}`;
const i = renderer.context_lookup.get(`$${name}`).index;
const insert =
reassigned || export_name
? b`${`$$subscribe_${name}`}()`
: b`@component_subscribe($$self, ${name}, #value => $$invalidate(${i}, ${value} = #value))`;
if (component.compile_options.dev) {
return b`@validate_store(${name}, '${name}'); ${insert}`;
}
return insert;
});
}
const args = [x`$$self`];
const has_invalidate =
props.length > 0 ||
@ -394,26 +354,20 @@ export default function dom(
}
`);
}
body.push(b`
${component.extract_javascript(component.ast.module)}
${component.fully_hoisted}
`);
const filtered_props = props.filter((prop) => {
const variable = component.var_lookup.get(prop.name);
if (variable.hoistable) return false;
return prop.name[0] !== '$';
});
const reactive_stores = component.vars.filter(
(variable) => variable.name[0] === '$' && variable.name[1] !== '$'
);
const instance_javascript = component.extract_javascript(component.ast.instance);
const has_definition =
component.compile_options.dev ||
(instance_javascript && instance_javascript.length > 0) ||
@ -424,11 +378,9 @@ export default function dom(
component.reactive_declarations.length > 0 ||
capture_state ||
inject_state;
const definition = has_definition
? component.alias('instance')
: { type: 'Literal', value: null };
const reactive_store_subscriptions = reactive_stores
.filter((store) => {
const variable = component.var_lookup.get(store.name.slice(1));
@ -442,63 +394,57 @@ export default function dom(
}, ${name} = $$value));
`
);
const resubscribable_reactive_store_unsubscribers = reactive_stores
.filter((store) => {
const variable = component.var_lookup.get(store.name.slice(1));
return variable && (variable.reassigned || variable.export_name);
})
.map(({ name }) => b`$$self.$$.on_destroy.push(() => ${`$$unsubscribe_${name.slice(1)}`}());`);
if (has_definition) {
const reactive_declarations: Node | Node[] = [];
const fixed_reactive_declarations: Node[] = []; // not really 'reactive' but whatever
/** @type {import('estree').Node | import('estree').Node[]} */
const reactive_declarations = [];
/** @type {import('estree').Node[]} */
const fixed_reactive_declarations = []; // not really 'reactive' but whatever
component.reactive_declarations.forEach((d) => {
const dependencies = Array.from(d.dependencies);
const uses_rest_or_props = !!dependencies.find((n) => n === '$$props' || n === '$$restProps');
const writable = dependencies.filter((n) => {
const variable = component.var_lookup.get(n);
return variable && (variable.export_name || variable.mutated || variable.reassigned);
});
const condition =
!uses_rest_or_props && writable.length > 0 && renderer.dirty(writable, true);
let statement = d.node; // TODO remove label (use d.node.body) if it's not referenced
if (condition) statement = b`if (${condition}) { ${statement} }`[0] as Statement;
if (condition)
statement = /** @type {import('estree').Statement} */ (
b`if (${condition}) { ${statement} }`[0]
);
if (condition || uses_rest_or_props) {
reactive_declarations.push(statement);
} else {
fixed_reactive_declarations.push(statement);
}
});
const injected = Array.from(component.injected_reactive_declaration_vars).filter((name) => {
const variable = component.var_lookup.get(name);
return variable.injected && variable.name[0] !== '$';
});
const reactive_store_declarations = reactive_stores.map((variable) => {
const $name = variable.name;
const name = $name.slice(1);
const store = component.var_lookup.get(name);
if (store && (store.reassigned || store.export_name)) {
const unsubscribe = `$$unsubscribe_${name}`;
const subscribe = `$$subscribe_${name}`;
const i = renderer.context_lookup.get($name).index;
return b`let ${$name}, ${unsubscribe} = @noop, ${subscribe} = () => (${unsubscribe}(), ${unsubscribe} = @subscribe(${name}, $$value => $$invalidate(${i}, ${$name} = $$value)), ${name})`;
}
return b`let ${$name};`;
});
let unknown_props_check: Node[] | undefined;
/** @type {import('estree').Node[] | undefined} */
let unknown_props_check;
if (component.compile_options.dev && !(uses_props || uses_rest)) {
unknown_props_check = b`
const writable_props = [${writable_props.map((prop) => x`'${prop.export_name}'`)}];
@ -509,18 +455,16 @@ export default function dom(
});
`;
}
const return_value = {
type: 'ArrayExpression',
elements: renderer.initial_context.map(
(member) =>
({
/** @type {import('estree').Expression} */ ({
type: 'Identifier',
name: member.name
} as Expression)
})
)
};
body.push(b`
function ${definition}(${args}) {
${injected.map((name) => b`let ${name};`)}
@ -583,26 +527,24 @@ export default function dom(
}
`);
}
const prop_indexes = x`{
const prop_indexes = /** @type {import('estree').ObjectExpression} */ (
x`{
${props
.filter((v) => v.export_name && !v.module)
.map((v) => p`${v.export_name}: ${renderer.context_lookup.get(v.name).index}`)}
}` as ObjectExpression;
}`
);
let dirty;
if (renderer.context_overflow) {
dirty = x`[]`;
for (let i = 0; i < renderer.context.length; i += 31) {
dirty.elements.push(x`-1`);
/** @type {any} */ (dirty).elements.push(x`-1`);
}
}
const superclass = {
type: 'Identifier',
name: options.dev ? '@SvelteComponentDev' : '@SvelteComponent'
};
const optional_parameters = [];
if (should_add_css) {
optional_parameters.push(add_css);
@ -612,25 +554,24 @@ export default function dom(
if (dirty) {
optional_parameters.push(dirty);
}
const declaration = b`
const declaration = /** @type {import('estree').ClassDeclaration} */ (
b`
class ${name} extends ${superclass} {
constructor(options) {
super(${options.dev && 'options'});
@init(this, options, ${definition}, ${
has_create_fragment ? 'create_fragment' : 'null'
}, ${not_equal}, ${prop_indexes}, ${optional_parameters});
has_create_fragment ? 'create_fragment' : 'null'
}, ${not_equal}, ${prop_indexes}, ${optional_parameters});
${
options.dev &&
b`@dispatch_dev("SvelteRegisterComponent", { component: this, tagName: "${name.name}", options, id: create_fragment.name });`
}
}
}
`[0] as ClassDeclaration;
`[0]
);
push_array(declaration.body.body, accessors);
body.push(declaration);
body.push(/** @type {any} */ (declaration));
if (options.customElement) {
const props_str = writable_props.reduce((def, prop) => {
def[prop.export_name] =
@ -647,7 +588,6 @@ export default function dom(
.join(',');
const use_shadow_dom =
component.component_options.customElement?.shadow !== 'none' ? 'true' : 'false';
if (component.component_options.customElement?.tag) {
body.push(
b`@_customElements.define("${
@ -664,6 +604,5 @@ export default function dom(
);
}
}
return { js: flatten(body), css };
}

@ -1,52 +1,52 @@
import { nodes_match } from '../../utils/nodes_match';
import { Scope } from 'periscopic';
import { nodes_match } from '../../utils/nodes_match.js';
import { x } from 'code-red';
import { Node, Expression } from 'estree';
import Renderer from './Renderer';
import { Var } from '../../interfaces';
export function invalidate(
renderer: Renderer,
scope: Scope,
node: Node,
names: Set<string>,
main_execution_context: boolean = false
) {
/**
* @param {import('./Renderer.js').default} renderer
* @param {import('periscopic').Scope} scope
* @param {import('estree').Node} node
* @param {Set<string>} names
* @param {boolean} main_execution_context
* @returns {any}
*/
export function invalidate(renderer, scope, node, names, main_execution_context = false) {
const { component } = renderer;
const [head, ...tail] = Array.from(names)
.filter((name) => {
const owner = scope.find_owner(name);
return !owner || owner === component.instance_scope;
})
.map((name) => component.var_lookup.get(name))
.filter((variable) => {
return (
variable &&
!variable.hoistable &&
!variable.global &&
!variable.module &&
(variable.referenced ||
variable.subscribable ||
variable.is_reactive_dependency ||
variable.export_name ||
variable.name[0] === '$')
);
}) as Var[];
function get_invalidated(variable: Var, node?: Expression) {
const [head, ...tail] = /** @type {import('../../interfaces.js').Var[]} */ (
Array.from(names)
.filter((name) => {
const owner = scope.find_owner(name);
return !owner || owner === component.instance_scope;
})
.map((name) => component.var_lookup.get(name))
.filter((variable) => {
return (
variable &&
!variable.hoistable &&
!variable.global &&
!variable.module &&
(variable.referenced ||
variable.subscribable ||
variable.is_reactive_dependency ||
variable.export_name ||
variable.name[0] === '$')
);
})
);
/**
* @param {import('../../interfaces.js').Var} variable
* @param {import('estree').Expression} [node]
*/
function get_invalidated(variable, node) {
if (main_execution_context && !variable.subscribable && variable.name[0] !== '$') {
return node;
}
return renderer_invalidate(renderer, variable.name, undefined, main_execution_context);
}
if (!head) {
return node;
}
component.has_reactive_assignments = true;
if (
node.type === 'AssignmentExpression' &&
node.operator === '=' &&
@ -55,10 +55,8 @@ export function invalidate(
) {
return get_invalidated(head, node);
}
const is_store_value = head.name[0] === '$' && head.name[1] !== '$';
const extra_args = tail.map((variable) => get_invalidated(variable)).filter(Boolean);
if (is_store_value) {
return x`@set_store_value(${head.name.slice(1)}, ${node}, ${head.name}, ${extra_args})`;
}
@ -82,23 +80,22 @@ export function invalidate(
// skip `$$invalidate` if it is in the main execution context
invalidate = extra_args.length ? [node, ...extra_args] : node;
}
if (head.subscribable && head.reassigned) {
const subscribe = `$$subscribe_${head.name}`;
invalidate = x`${subscribe}(${invalidate})`;
}
return invalidate;
}
export function renderer_invalidate(
renderer: Renderer,
name: string,
value?: unknown,
main_execution_context: boolean = false
) {
/**
* @param {import('./Renderer.js').default} renderer
* @param {string} name
* @param {any} [value]
* @param {boolean} main_execution_context
* @returns {import('estree').Node}
*/
export function renderer_invalidate(renderer, name, value, main_execution_context = false) {
const variable = renderer.component.var_lookup.get(name);
if (variable && variable.subscribable && (variable.reassigned || variable.export_name)) {
if (main_execution_context) {
return x`${`$$subscribe_${name}`}(${value || name})`;
@ -107,11 +104,9 @@ export function renderer_invalidate(
return x`${`$$subscribe_${name}`}($$invalidate(${member.index}, ${value || name}))`;
}
}
if (name[0] === '$' && name[1] !== '$') {
return x`${name.slice(1)}.set(${value || name})`;
}
if (
variable &&
(variable.module ||
@ -122,7 +117,6 @@ export function renderer_invalidate(
) {
return value || name;
}
if (value) {
if (main_execution_context) {
return x`${value}`;
@ -131,12 +125,9 @@ export function renderer_invalidate(
return x`$$invalidate(${member.index}, ${value})`;
}
}
if (main_execution_context) return;
// if this is a reactive declaration, invalidate dependencies recursively
const deps = new Set([name]);
deps.forEach((name) => {
const reactive_declarations = renderer.component.reactive_declarations.filter((x) =>
x.assignees.has(name)
@ -147,11 +138,9 @@ export function renderer_invalidate(
});
});
});
// TODO ideally globals etc wouldn't be here in the first place
const filtered = Array.from(deps).filter((n) => renderer.context_lookup.has(n));
if (!filtered.length) return null;
return filtered
.map((n) => x`$$invalidate(${renderer.context_lookup.get(n).index}, ${n})`)
.reduce((lhs, rhs) => x`${lhs}, ${rhs}`);

@ -1,55 +1,60 @@
import Wrapper from './shared/Wrapper';
import Renderer from '../Renderer';
import Block from '../Block';
import AwaitBlock from '../../nodes/AwaitBlock';
import create_debugging_comment from './shared/create_debugging_comment';
import Wrapper from './shared/Wrapper.js';
import create_debugging_comment from './shared/create_debugging_comment.js';
import { b, x } from 'code-red';
import FragmentWrapper from './Fragment';
import PendingBlock from '../../nodes/PendingBlock';
import ThenBlock from '../../nodes/ThenBlock';
import CatchBlock from '../../nodes/CatchBlock';
import { Context } from '../../nodes/shared/Context';
import { Identifier, Literal, Node } from 'estree';
import { add_const_tags, add_const_tags_context } from './shared/add_const_tags';
import Expression from '../../nodes/shared/Expression';
type Status = 'pending' | 'then' | 'catch';
import FragmentWrapper from './Fragment.js';
import ThenBlock from '../../nodes/ThenBlock.js';
import CatchBlock from '../../nodes/CatchBlock.js';
import { add_const_tags, add_const_tags_context } from './shared/add_const_tags.js';
import Expression from '../../nodes/shared/Expression.js';
/** @extends Wrapper<import('../../nodes/PendingBlock.js').default | import('../../nodes/ThenBlock.js').default | import('../../nodes/CatchBlock.js').default> */
class AwaitBlockBranch extends Wrapper {
parent: AwaitBlockWrapper;
node: PendingBlock | ThenBlock | CatchBlock;
block: Block;
fragment: FragmentWrapper;
is_dynamic: boolean;
/** @typedef {'pending' | 'then' | 'catch'} Status */
/** @type {import('../Block.js').default} */
block;
/** @type {import('./Fragment.js').default} */
fragment;
/** @type {boolean} */
is_dynamic;
var = null;
status: Status;
value: string;
value_index: Literal;
value_contexts: Context[];
is_destructured: boolean;
/** @type {Status} */
status;
/** @type {string} */
value;
/** @type {import('estree').Literal} */
value_index;
/** @type {import('../../nodes/shared/Context.js').Context[]} */
value_contexts;
constructor(
status: Status,
renderer: Renderer,
block: Block,
parent: AwaitBlockWrapper,
node: PendingBlock | ThenBlock | CatchBlock,
strip_whitespace: boolean,
next_sibling: Wrapper
) {
/** @type {boolean} */
is_destructured;
/**
* @param {Status} status
* @param {import('../Renderer.js').default} renderer
* @param {import('../Block.js').default} block
* @param {AwaitBlockWrapper} parent
* @param {import('../../nodes/PendingBlock.js').default | import('../../nodes/ThenBlock.js').default | import('../../nodes/CatchBlock.js').default} node
* @param {boolean} strip_whitespace
* @param {import('./shared/Wrapper.js').default} next_sibling
*/
constructor(status, renderer, block, parent, node, strip_whitespace, next_sibling) {
super(renderer, block, parent, node);
this.status = status;
this.block = block.child({
comment: create_debugging_comment(node, this.renderer.component),
name: this.renderer.component.get_unique_name(`create_${status}_block`),
type: status
});
this.add_context(parent.node[status + '_node'], parent.node[status + '_contexts']);
this.fragment = new FragmentWrapper(
renderer,
this.block,
@ -58,13 +63,15 @@ class AwaitBlockBranch extends Wrapper {
strip_whitespace,
next_sibling
);
this.is_dynamic = this.block.dependencies.size > 0;
}
add_context(node: Node | null, contexts: Context[]) {
/**
* @param {import('estree').Node | null} node
* @param {import('../../nodes/shared/Context.js').Context[]} contexts
*/
add_context(node, contexts) {
if (!node) return;
if (node.type === 'Identifier') {
this.value = node.name;
this.renderer.add_to_context(this.value, true);
@ -79,24 +86,30 @@ class AwaitBlockBranch extends Wrapper {
this.is_destructured = true;
}
this.value_index = this.renderer.context_lookup.get(this.value).index;
if (this.has_consts(this.node)) {
add_const_tags_context(this.renderer, this.node.const_tags);
}
}
has_consts(node: PendingBlock | ThenBlock | CatchBlock): node is ThenBlock | CatchBlock {
/**
* @param {import('../../nodes/PendingBlock.js').default | import('../../nodes/ThenBlock.js').default | import('../../nodes/CatchBlock.js').default} node
* @returns {node is import('../../nodes/ThenBlock.js').default | import('../../nodes/CatchBlock.js').default}
*/
has_consts(node) {
return node instanceof ThenBlock || node instanceof CatchBlock;
}
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
/**
* @param {import('../Block.js').default} block
* @param {import('estree').Identifier} parent_node
* @param {import('estree').Identifier} parent_nodes
*/
render(block, parent_node, parent_nodes) {
this.fragment.render(block, parent_node, parent_nodes);
if (this.is_destructured || (this.has_consts(this.node) && this.node.const_tags.length > 0)) {
this.render_get_context();
}
}
render_get_context() {
const props = this.is_destructured
? this.value_contexts.map((prop) => {
@ -109,6 +122,7 @@ class AwaitBlockBranch extends Wrapper {
);
return b`const ${prop.property_name} = ${expression.manipulate(this.block, '#ctx')};`;
} else {
/** @param {any} name */
const to_ctx = (name) => this.renderer.reference(name);
return b`#ctx[${
this.block.renderer.context_lookup.get(prop.key.name).index
@ -116,11 +130,9 @@ class AwaitBlockBranch extends Wrapper {
}
})
: null;
const const_tags_props = this.has_consts(this.node)
? add_const_tags(this.block, this.node.const_tags, '#ctx')
: null;
const get_context = this.block.renderer.component.get_unique_name(`get_${this.status}_context`);
this.block.renderer.blocks.push(b`
function ${get_context}(#ctx) {
@ -135,34 +147,36 @@ class AwaitBlockBranch extends Wrapper {
}
}
/** @extends Wrapper<import('../../nodes/AwaitBlock.js').default> */
export default class AwaitBlockWrapper extends Wrapper {
node: AwaitBlock;
pending: AwaitBlockBranch;
then: AwaitBlockBranch;
catch: AwaitBlockBranch;
var: Identifier = { type: 'Identifier', name: 'await_block' };
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: AwaitBlock,
strip_whitespace: boolean,
next_sibling: Wrapper
) {
/** @type {AwaitBlockBranch} */
pending;
/** @type {AwaitBlockBranch} */
then;
/** @type {AwaitBlockBranch} */
catch;
/** @type {import('estree').Identifier} */
var = { type: 'Identifier', name: 'await_block' };
/**
* @param {import('../Renderer.js').default} renderer
* @param {import('../Block.js').default} block
* @param {import('./shared/Wrapper.js').default} parent
* @param {import('../../nodes/AwaitBlock.js').default} node
* @param {boolean} strip_whitespace
* @param {import('./shared/Wrapper.js').default} next_sibling
*/
constructor(renderer, block, parent, node, strip_whitespace, next_sibling) {
super(renderer, block, parent, node);
block.add_dependencies(this.node.expression.dependencies);
let is_dynamic = false;
let has_intros = false;
let has_outros = false;
['pending', 'then', 'catch'].forEach((status: Status) => {
/** @type {const} */ (['pending', 'then', 'catch']).forEach((status) => {
const child = this.node[status];
const branch = new AwaitBlockBranch(
status,
renderer,
@ -172,46 +186,42 @@ export default class AwaitBlockWrapper extends Wrapper {
strip_whitespace,
next_sibling
);
renderer.blocks.push(branch.block);
if (branch.is_dynamic) {
is_dynamic = true;
// TODO should blocks update their own parents?
block.add_dependencies(branch.block.dependencies);
}
if (branch.block.has_intros) has_intros = true;
if (branch.block.has_outros) has_outros = true;
this[status] = branch;
});
['pending', 'then', 'catch'].forEach((status) => {
this[status].block.has_update_method = is_dynamic;
this[status].block.has_intro_method = has_intros;
this[status].block.has_outro_method = has_outros;
});
if (has_outros) {
block.add_outro();
}
}
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
/**
* @param {import('../Block.js').default} block
* @param {import('estree').Identifier} parent_node
* @param {import('estree').Identifier} parent_nodes
*/
render(block, parent_node, parent_nodes) {
const anchor = this.get_or_create_anchor(block, parent_node, parent_nodes);
const update_mount_node = this.get_update_mount_node(anchor);
const snippet = this.node.expression.manipulate(block);
const info = block.get_unique_name('info');
const promise = block.get_unique_name('promise');
block.add_variable(promise);
block.maintain_context = true;
const info_props: any = x`{
/** @type {any} */
const info_props = x`{
ctx: #ctx,
current: null,
token: null,
@ -223,53 +233,40 @@ export default class AwaitBlockWrapper extends Wrapper {
error: ${this.catch.value_index},
blocks: ${this.pending.block.has_outro_method && x`[,,,]`}
}`;
block.chunks.init.push(b`
let ${info} = ${info_props};
`);
block.chunks.init.push(b`
@handle_promise(${promise} = ${snippet}, ${info});
`);
block.chunks.create.push(b`
${info}.block.c();
`);
if (parent_nodes && this.renderer.options.hydratable) {
block.chunks.claim.push(b`
${info}.block.l(${parent_nodes});
`);
}
const initial_mount_node = parent_node || '#target';
const anchor_node = parent_node ? 'null' : '#anchor';
const has_transitions =
this.pending.block.has_intro_method || this.pending.block.has_outro_method;
block.chunks.mount.push(b`
${info}.block.m(${initial_mount_node}, ${info}.anchor = ${anchor_node});
${info}.mount = () => ${update_mount_node};
${info}.anchor = ${anchor};
`);
if (has_transitions) {
block.chunks.intro.push(b`@transition_in(${info}.block);`);
}
const dependencies = this.node.expression.dynamic_dependencies();
const update_await_block_branch = b`@update_await_block_branch(${info}, #ctx, #dirty)`;
if (dependencies.length > 0) {
const condition = x`
${block.renderer.dirty(dependencies)} &&
${promise} !== (${promise} = ${snippet}) &&
@handle_promise(${promise}, ${info})`;
block.chunks.update.push(b`${info}.ctx = #ctx;`);
if (this.pending.block.has_update_method) {
block.chunks.update.push(b`
if (${condition}) {
@ -290,7 +287,6 @@ export default class AwaitBlockWrapper extends Wrapper {
`);
}
}
if (this.pending.block.has_outro_method) {
block.chunks.outro.push(b`
for (let #i = 0; #i < 3; #i += 1) {
@ -299,15 +295,13 @@ export default class AwaitBlockWrapper extends Wrapper {
}
`);
}
block.chunks.destroy.push(b`
${info}.block.d(${parent_node ? null : 'detaching'});
${info}.token = null;
${info} = null;
`);
[this.pending, this.then, this.catch].forEach((branch) => {
branch.render(branch.block, null, x`#nodes` as Identifier);
branch.render(branch.block, null, /** @type {import('estree').Identifier} */ (x`#nodes`));
});
}
}

@ -1,24 +1,31 @@
import Block from '../Block';
import Wrapper from './shared/Wrapper';
import Wrapper from './shared/Wrapper.js';
import { x } from 'code-red';
import Body from '../../nodes/Body';
import { Identifier } from 'estree';
import EventHandler from './Element/EventHandler';
import add_event_handlers from './shared/add_event_handlers';
import { TemplateNode } from '../../../interfaces';
import Renderer from '../Renderer';
import add_actions from './shared/add_actions';
import EventHandler from './Element/EventHandler.js';
import add_event_handlers from './shared/add_event_handlers.js';
import add_actions from './shared/add_actions.js';
/** @extends Wrapper<import('../../nodes/Body.js').default> */
export default class BodyWrapper extends Wrapper {
node: Body;
handlers: EventHandler[];
/** @type {import('./Element/EventHandler.js').default[]} */
handlers;
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: TemplateNode) {
/**
* @param {import('../Renderer.js').default} renderer
* @param {import('../Block.js').default} block
* @param {import('./shared/Wrapper.js').default} parent
* @param {import('../../nodes/Body.js').default} node
*/
constructor(renderer, block, parent, node) {
super(renderer, block, parent, node);
this.handlers = this.node.handlers.map((handler) => new EventHandler(handler, this));
}
render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) {
/**
* @param {import('../Block.js').default} block
* @param {import('estree').Identifier} _parent_node
* @param {import('estree').Identifier} _parent_nodes
*/
render(block, _parent_node, _parent_nodes) {
add_event_handlers(block, x`@_document.body`, this.handlers);
add_actions(block, x`@_document.body`, this.node.actions);
}

@ -1,22 +1,26 @@
import Renderer from '../Renderer';
import Block from '../Block';
import Comment from '../../nodes/Comment';
import Wrapper from './shared/Wrapper';
import Wrapper from './shared/Wrapper.js';
import { x } from 'code-red';
import { Identifier } from 'estree';
/** @extends Wrapper<import('../../nodes/Comment.js').default> */
export default class CommentWrapper extends Wrapper {
node: Comment;
var: Identifier;
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: Comment) {
/**
* @param {import('../Renderer.js').default} renderer
* @param {import('../Block.js').default} block
* @param {import('./shared/Wrapper.js').default} parent
* @param {import('../../nodes/Comment.js').default} node
*/
constructor(renderer, block, parent, node) {
super(renderer, block, parent, node);
this.var = x`c` as Identifier;
this.var = /** @type {import('estree').Identifier} */ (x`c`);
}
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
/**
* @param {import('../Block.js').default} block
* @param {import('estree').Identifier} parent_node
* @param {import('estree').Identifier} parent_nodes
*/
render(block, parent_node, parent_nodes) {
if (!this.renderer.options.preserveComments) return;
const string_literal = {
type: 'Literal',
value: this.node.data,
@ -25,7 +29,6 @@ export default class CommentWrapper extends Wrapper {
end: this.renderer.locate(this.node.end)
}
};
block.add_element(
this.var,
x`@comment(${string_literal})`,
@ -33,10 +36,8 @@ export default class CommentWrapper extends Wrapper {
parent_node
);
}
text() {
if (!this.renderer.options.preserveComments) return '';
return `<!--${this.node.data}-->`;
}
}

@ -1,85 +1,78 @@
import Renderer from '../Renderer';
import Wrapper from './shared/Wrapper';
import Block from '../Block';
import DebugTag from '../../nodes/DebugTag';
import add_to_set from '../../utils/add_to_set';
import Wrapper from './shared/Wrapper.js';
import add_to_set from '../../utils/add_to_set.js';
import { b, p } from 'code-red';
import { Identifier, DebuggerStatement } from 'estree';
/** @extends Wrapper<import('../../nodes/DebugTag.js').default> */
export default class DebugTagWrapper extends Wrapper {
node: DebugTag;
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: DebugTag,
_strip_whitespace: boolean,
_next_sibling: Wrapper
) {
/**
* @param {import('../Renderer.js').default} renderer
* @param {import('../Block.js').default} block
* @param {import('./shared/Wrapper.js').default} parent
* @param {import('../../nodes/DebugTag.js').default} node
* @param {boolean} _strip_whitespace
* @param {import('./shared/Wrapper.js').default} _next_sibling
*/
constructor(renderer, block, parent, node, _strip_whitespace, _next_sibling) {
super(renderer, block, parent, node);
}
render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) {
/**
* @param {import('../Block.js').default} block
* @param {import('estree').Identifier} _parent_node
* @param {import('estree').Identifier} _parent_nodes
*/
render(block, _parent_node, _parent_nodes) {
const { renderer } = this;
const { component } = renderer;
if (!renderer.options.dev) return;
const { var_lookup } = component;
const start = component.locate(this.node.start + 1);
const end = { line: start.line, column: start.column + 6 };
const loc = { start, end };
const debug: DebuggerStatement = {
/** @type {import('estree').DebuggerStatement} */
const debug = {
type: 'DebuggerStatement',
loc
};
if (this.node.expressions.length === 0) {
// Debug all
block.chunks.create.push(debug);
block.chunks.update.push(debug);
} else {
const log: Identifier = {
/** @type {import('estree').Identifier} */
const log = {
type: 'Identifier',
name: 'log',
loc
};
const dependencies: Set<string> = new Set();
/** @type {Set<string>} */
const dependencies = new Set();
this.node.expressions.forEach((expression) => {
add_to_set(dependencies, expression.dependencies);
});
const contextual_identifiers = this.node.expressions
.filter((e) => {
const variable = var_lookup.get((e.node as Identifier).name);
const variable = var_lookup.get(/** @type {import('estree').Identifier} */ (e.node).name);
return !(variable && variable.hoistable);
})
.map((e) => (e.node as Identifier).name);
.map((e) => /** @type {import('estree').Identifier} */ (e.node).name);
const logged_identifiers = this.node.expressions.map(
(e) => p`${(e.node as Identifier).name}`
(e) => p`${/** @type {import('estree').Identifier} */ (e.node).name}`
);
const debug_statements = b`
${contextual_identifiers.map((name) => b`const ${name} = ${renderer.reference(name)};`)}
@_console.${log}({ ${logged_identifiers} });
debugger;`;
if (dependencies.size) {
const condition = renderer.dirty(Array.from(dependencies));
block.chunks.update.push(b`
if (${condition}) {
${debug_statements}
}
`);
}
block.chunks.create.push(b`{
${debug_statements}
}`);

@ -1,54 +1,58 @@
import Block from '../Block';
import Wrapper from './shared/Wrapper';
import Wrapper from './shared/Wrapper.js';
import { b, x } from 'code-red';
import Document from '../../nodes/Document';
import { Identifier } from 'estree';
import EventHandler from './Element/EventHandler';
import add_event_handlers from './shared/add_event_handlers';
import { TemplateNode } from '../../../interfaces';
import Renderer from '../Renderer';
import add_actions from './shared/add_actions';
import EventHandler from './Element/EventHandler.js';
import add_event_handlers from './shared/add_event_handlers.js';
import add_actions from './shared/add_actions.js';
const associated_events = {
fullscreenElement: ['fullscreenchange'],
visibilityState: ['visibilitychange']
};
const readonly = new Set(['fullscreenElement', 'visibilityState']);
/** @extends Wrapper<import('../../nodes/Document.js').default> */
export default class DocumentWrapper extends Wrapper {
node: Document;
handlers: EventHandler[];
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: TemplateNode) {
/** @type {import('./Element/EventHandler.js').default[]} */
handlers;
/**
* @param {import('../Renderer.js').default} renderer
* @param {import('../Block.js').default} block
* @param {import('./shared/Wrapper.js').default} parent
* @param {import('../../nodes/Document.js').default} node
*/
constructor(renderer, block, parent, node) {
super(renderer, block, parent, node);
this.handlers = this.node.handlers.map((handler) => new EventHandler(handler, this));
}
render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) {
/**
* @param {import('../Block.js').default} block
* @param {import('estree').Identifier} _parent_node
* @param {import('estree').Identifier} _parent_nodes
*/
render(block, _parent_node, _parent_nodes) {
const { renderer } = this;
const { component } = renderer;
const events: Record<string, Array<{ name: string; value: string }>> = {};
const bindings: Record<string, string> = {};
/** @type {Record<string, Array<{ name: string; value: string }>>} */
const events = {};
/** @type {Record<string, string>} */
const bindings = {};
add_event_handlers(block, x`@_document`, this.handlers);
add_actions(block, x`@_document`, this.node.actions);
this.node.bindings.forEach((binding) => {
// TODO: what if it's a MemberExpression?
const binding_name = (binding.expression.node as Identifier).name;
const binding_name = /** @type {import('estree').Identifier} */ (binding.expression.node)
.name;
// in dev mode, throw if read-only values are written to
if (readonly.has(binding.name)) {
renderer.readonly.add(binding_name);
}
bindings[binding.name] = binding_name;
const binding_events = associated_events[binding.name];
const property = binding.name;
binding_events.forEach((associated_event) => {
if (!events[associated_event]) events[associated_event] = [];
events[associated_event].push({
@ -57,32 +61,25 @@ export default class DocumentWrapper extends Wrapper {
});
});
});
Object.keys(events).forEach((event) => {
const id = block.get_unique_name(`ondocument${event}`);
const props = events[event];
renderer.add_to_context(id.name);
const fn = renderer.reference(id.name);
props.forEach((prop) => {
renderer.meta_bindings.push(b`this._state.${prop.name} = @_document.${prop.value};`);
});
block.event_listeners.push(x`
@listen(@_document, "${event}", ${fn})
`);
component.partly_hoisted.push(b`
function ${id}() {
${props.map((prop) => renderer.invalidate(prop.name, x`${prop.name} = @_document.${prop.value}`))}
}
`);
block.chunks.init.push(b`
@add_render_callback(${fn});
`);
component.has_reactive_assignments = true;
});
}

@ -1,41 +1,40 @@
import Renderer from '../Renderer';
import Block from '../Block';
import Wrapper from './shared/Wrapper';
import create_debugging_comment from './shared/create_debugging_comment';
import EachBlock from '../../nodes/EachBlock';
import FragmentWrapper from './Fragment';
import Wrapper from './shared/Wrapper.js';
import create_debugging_comment from './shared/create_debugging_comment.js';
import FragmentWrapper from './Fragment.js';
import { b, x } from 'code-red';
import ElseBlock from '../../nodes/ElseBlock';
import { Identifier, Node } from 'estree';
import get_object from '../../utils/get_object';
import { add_const_tags, add_const_tags_context } from './shared/add_const_tags';
import Expression from '../../nodes/shared/Expression';
import get_object from '../../utils/get_object.js';
import { add_const_tags, add_const_tags_context } from './shared/add_const_tags.js';
import Expression from '../../nodes/shared/Expression.js';
/** @extends Wrapper<import('../../nodes/ElseBlock.js').default> */
export class ElseBlockWrapper extends Wrapper {
node: ElseBlock;
block: Block;
fragment: FragmentWrapper;
is_dynamic: boolean;
/** @type {import('../Block.js').default} */
block;
/** @type {import('./Fragment.js').default} */
fragment;
/** @type {boolean} */
is_dynamic;
var = null;
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: ElseBlock,
strip_whitespace: boolean,
next_sibling: Wrapper
) {
/**
* @param {import('../Renderer.js').default} renderer
* @param {import('../Block.js').default} block
* @param {import('./shared/Wrapper.js').default} parent
* @param {import('../../nodes/ElseBlock.js').default} node
* @param {boolean} strip_whitespace
* @param {import('./shared/Wrapper.js').default} next_sibling
*/
constructor(renderer, block, parent, node, strip_whitespace, next_sibling) {
super(renderer, block, parent, node);
add_const_tags_context(renderer, this.node.const_tags);
this.block = block.child({
comment: create_debugging_comment(node, this.renderer.component),
name: this.renderer.component.get_unique_name('create_else_block'),
type: 'else'
});
this.fragment = new FragmentWrapper(
renderer,
this.block,
@ -44,75 +43,83 @@ export class ElseBlockWrapper extends Wrapper {
strip_whitespace,
next_sibling
);
this.is_dynamic = this.block.dependencies.size > 0;
}
}
/** @extends Wrapper<import('../../nodes/EachBlock.js').default> */
export default class EachBlockWrapper extends Wrapper {
block: Block;
node: EachBlock;
fragment: FragmentWrapper;
else?: ElseBlockWrapper;
vars: {
create_each_block: Identifier;
each_block_value: Identifier;
get_each_context: Identifier;
iterations: Identifier;
fixed_length: number;
data_length: Node | number;
view_length: Node | number;
};
context_props: Array<Node | Node[]>;
index_name: Identifier;
updates: Array<Node | Node[]> = [];
dependencies: Set<string>;
var: Identifier = { type: 'Identifier', name: 'each' };
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: EachBlock,
strip_whitespace: boolean,
next_sibling: Wrapper
) {
/** @type {import('../Block.js').default} */
block;
/** @type {import('./Fragment.js').default} */
fragment;
/** @type {ElseBlockWrapper} */
else;
/**
* @type {{
* create_each_block: import('estree').Identifier;
* each_block_value: import('estree').Identifier;
* get_each_context: import('estree').Identifier;
* iterations: import('estree').Identifier;
* fixed_length: number;
* data_length: import('estree').Node | number;
* view_length: import('estree').Node | number;
* }}
*/
vars;
/** @type {Array<import('estree').Node | import('estree').Node[]>} */
context_props;
/** @type {import('estree').Identifier} */
index_name;
/** @type {Array<import('estree').Node | import('estree').Node[]>} */
updates = [];
/** @type {Set<string>} */
dependencies;
/** @type {import('estree').Identifier} */
var = { type: 'Identifier', name: 'each' };
/**
* @param {import('../Renderer.js').default} renderer
* @param {import('../Block.js').default} block
* @param {import('./shared/Wrapper.js').default} parent
* @param {import('../../nodes/EachBlock.js').default} node
* @param {boolean} strip_whitespace
* @param {import('./shared/Wrapper.js').default} next_sibling
*/
constructor(renderer, block, parent, node, strip_whitespace, next_sibling) {
super(renderer, block, parent, node);
const { dependencies } = node.expression;
block.add_dependencies(dependencies);
this.node.contexts.forEach((context) => {
if (context.type !== 'DestructuredVariable') return;
renderer.add_to_context(context.key.name, true);
});
add_const_tags_context(renderer, this.node.const_tags);
this.block = block.child({
comment: create_debugging_comment(this.node, this.renderer.component),
name: renderer.component.get_unique_name('create_each_block'),
type: 'each',
// @ts-ignore todo: probably error
key: node.key as string,
key: /** @type {string} */ (node.key),
bindings: new Map(block.bindings)
});
// TODO this seems messy
this.block.has_animation = this.node.has_animation;
this.index_name = this.node.index
? { type: 'Identifier', name: this.node.index }
: renderer.component.get_unique_name(`${this.node.context}_index`);
const fixed_length =
node.expression.node.type === 'ArrayExpression' &&
node.expression.node.elements.every((element) => element.type !== 'SpreadElement')
? node.expression.node.elements.length
: null;
// hack the sourcemap, so that if data is missing the bug
// is easy to find
let c = this.node.start + 2;
@ -124,46 +131,41 @@ export default class EachBlockWrapper extends Wrapper {
name: 'length',
loc: { start, end }
};
const each_block_value = renderer.component.get_unique_name(`${this.var.name}_value`);
const iterations = block.get_unique_name(`${this.var.name}_blocks`);
renderer.add_to_context(each_block_value.name, true);
renderer.add_to_context(this.index_name.name, true);
this.vars = {
create_each_block: this.block.name,
each_block_value,
get_each_context: renderer.component.get_unique_name(`get_${this.var.name}_context`),
iterations,
// optimisation for array literal
fixed_length,
data_length: fixed_length === null ? x`${each_block_value}.${length}` : fixed_length,
view_length: fixed_length === null ? x`${iterations}.length` : fixed_length
};
const object = get_object(node.expression.node);
const store =
object.type === 'Identifier' && object.name[0] === '$' ? object.name.slice(1) : null;
node.contexts.forEach((prop) => {
if (prop.type !== 'DestructuredVariable') return;
this.block.bindings.set(prop.key.name, {
object: this.vars.each_block_value,
property: this.index_name,
modifier: prop.modifier,
snippet: prop.modifier(x`${this.vars.each_block_value}[${this.index_name}]` as Node),
snippet: prop.modifier(
/** @type {import('estree').Node} */ (
x`${this.vars.each_block_value}[${this.index_name}]`
)
),
store
});
});
if (this.node.index) {
this.block.get_unique_name(this.node.index); // this prevents name collisions (#1254)
}
renderer.blocks.push(this.block);
this.fragment = new FragmentWrapper(
renderer,
this.block,
@ -172,7 +174,6 @@ export default class EachBlockWrapper extends Wrapper {
strip_whitespace,
next_sibling
);
if (this.node.else) {
this.else = new ElseBlockWrapper(
renderer,
@ -182,50 +183,51 @@ export default class EachBlockWrapper extends Wrapper {
strip_whitespace,
next_sibling
);
renderer.blocks.push(this.else.block);
if (this.else.is_dynamic) {
this.block.add_dependencies(this.else.block.dependencies);
}
}
block.add_dependencies(this.block.dependencies);
if (this.block.has_outros || (this.else && this.else.block.has_outros)) {
block.add_outro();
}
}
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
/**
* @param {import('../Block.js').default} block
* @param {import('estree').Identifier} parent_node
* @param {import('estree').Identifier} parent_nodes
*/
render(block, parent_node, parent_nodes) {
if (this.fragment.nodes.length === 0) return;
const { renderer } = this;
const { component } = renderer;
const needs_anchor = this.next
? !this.next.is_dom_node()
: !parent_node || !this.parent.is_dom_node();
const snippet = this.node.expression.manipulate(block);
block.chunks.init.push(b`let ${this.vars.each_block_value} = ${snippet};`);
if (this.renderer.options.dev) {
block.chunks.init.push(b`@validate_each_argument(${this.vars.each_block_value});`);
}
const initial_anchor_node: Identifier = {
/** @type {import('estree').Identifier} */
const initial_anchor_node = {
type: 'Identifier',
name: parent_node ? 'null' : '#anchor'
};
const initial_mount_node: Identifier = parent_node || { type: 'Identifier', name: '#target' };
/** @type {import('estree').Identifier} */
const initial_mount_node = parent_node || { type: 'Identifier', name: '#target' };
const update_anchor_node = needs_anchor
? block.get_unique_name(`${this.var.name}_anchor`)
: (this.next && this.next.var) || { type: 'Identifier', name: 'null' };
const update_mount_node: Identifier = this.get_update_mount_node(
update_anchor_node as Identifier
);
/** @type {import('estree').Identifier} */
const update_mount_node = this.get_update_mount_node(
/** @type {import('estree').Identifier} */ (update_anchor_node)
);
const args = {
block,
parent_node,
@ -236,24 +238,21 @@ export default class EachBlockWrapper extends Wrapper {
update_anchor_node,
update_mount_node
};
const all_dependencies = new Set(this.block.dependencies); // TODO should be dynamic deps only
this.node.expression.dynamic_dependencies().forEach((dependency: string) => {
this.node.expression.dynamic_dependencies().forEach((dependency) => {
all_dependencies.add(dependency);
});
if (this.node.key) {
this.node.key.dynamic_dependencies().forEach((dependency: string) => {
this.node.key.dynamic_dependencies().forEach((dependency) => {
all_dependencies.add(dependency);
});
}
this.dependencies = all_dependencies;
if (this.node.key) {
this.render_keyed(args);
} else {
this.render_unkeyed(args);
}
if (this.block.has_intro_method || this.block.has_outro_method) {
block.chunks.intro.push(b`
for (let #i = 0; #i < ${this.vars.data_length}; #i += 1) {
@ -261,16 +260,14 @@ export default class EachBlockWrapper extends Wrapper {
}
`);
}
if (needs_anchor) {
block.add_element(
update_anchor_node as Identifier,
/** @type {import('estree').Identifier} */ (update_anchor_node),
x`@empty()`,
parent_nodes && x`@empty()`,
parent_node
);
}
if (this.else) {
let else_ctx = x`#ctx`;
if (this.else.node.const_tags.length > 0) {
@ -285,22 +282,18 @@ export default class EachBlockWrapper extends Wrapper {
else_ctx = x`${get_ctx_name}(#ctx)`;
}
const each_block_else = component.get_unique_name(`${this.var.name}_else`);
block.chunks.init.push(b`let ${each_block_else} = null;`);
// TODO neaten this up... will end up with an empty line in the block
block.chunks.init.push(b`
if (!${this.vars.data_length}) {
${each_block_else} = ${this.else.block.name}(${else_ctx});
}
`);
block.chunks.create.push(b`
if (${each_block_else}) {
${each_block_else}.c();
}
`);
if (this.renderer.options.hydratable) {
block.chunks.claim.push(b`
if (${each_block_else}) {
@ -308,17 +301,14 @@ export default class EachBlockWrapper extends Wrapper {
}
`);
}
block.chunks.mount.push(b`
if (${each_block_else}) {
${each_block_else}.m(${initial_mount_node}, ${initial_anchor_node});
}
`);
const has_transitions = !!(
this.else.block.has_intro_method || this.else.block.has_outro_method
);
const destroy_block_else = this.else.block.has_outro_method
? b`
@group_outros();
@ -329,7 +319,6 @@ export default class EachBlockWrapper extends Wrapper {
: b`
${each_block_else}.d(1);
${each_block_else} = null;`;
if (this.else.block.has_update_method) {
this.updates.push(b`
if (!${this.vars.data_length} && ${each_block_else}) {
@ -357,12 +346,10 @@ export default class EachBlockWrapper extends Wrapper {
}
`);
}
block.chunks.destroy.push(b`
if (${each_block_else}) ${each_block_else}.d(${parent_node ? '' : 'detaching'});
`);
}
if (this.updates.length) {
block.chunks.update.push(b`
if (${block.renderer.dirty(Array.from(all_dependencies))}) {
@ -370,19 +357,21 @@ export default class EachBlockWrapper extends Wrapper {
}
`);
}
this.fragment.render(this.block, null, x`#nodes` as Identifier);
this.fragment.render(this.block, null, /** @type {import('estree').Identifier} */ (x`#nodes`));
if (this.else) {
this.else.fragment.render(this.else.block, null, x`#nodes` as Identifier);
this.else.fragment.render(
this.else.block,
null,
/** @type {import('estree').Identifier} */ (x`#nodes`)
);
}
this.context_props = this.node.contexts.map((prop) => {
if (prop.type === 'DestructuredVariable') {
const to_ctx = (name: string) =>
/** @param {string} name */
const to_ctx = (name) =>
renderer.context_lookup.has(name)
? x`child_ctx[${renderer.context_lookup.get(name).index}]`
: ({ type: 'Identifier', name } as Node);
: /** @type {import('estree').Node} */ ({ type: 'Identifier', name });
return b`child_ctx[${
renderer.context_lookup.get(prop.key.name).index
}] = ${prop.default_modifier(prop.modifier(x`list[i]`), to_ctx)};`;
@ -396,7 +385,6 @@ export default class EachBlockWrapper extends Wrapper {
return b`const ${prop.property_name} = ${expression.manipulate(block, 'child_ctx')};`;
}
});
if (this.node.has_binding)
this.context_props.push(
b`child_ctx[${renderer.context_lookup.get(this.vars.each_block_value.name).index}] = list;`
@ -405,7 +393,6 @@ export default class EachBlockWrapper extends Wrapper {
this.context_props.push(
b`child_ctx[${renderer.context_lookup.get(this.index_name.name).index}] = i;`
);
// TODO which is better — Object.create(array) or array.slice()?
renderer.blocks.push(b`
function ${this.vars.get_each_context}(#ctx, list, i) {
@ -416,7 +403,18 @@ export default class EachBlockWrapper extends Wrapper {
}
`);
}
/**
* @param {{
* block: import('../Block.js').default;
* parent_node: import('estree').Identifier;
* parent_nodes: import('estree').Identifier;
* snippet: import('estree').Node;
* initial_anchor_node: import('estree').Identifier;
* initial_mount_node: import('estree').Identifier;
* update_anchor_node: import('estree').Identifier;
* update_mount_node: import('estree').Identifier;
* }} params
*/
render_keyed({
block,
parent_node,
@ -426,31 +424,18 @@ export default class EachBlockWrapper extends Wrapper {
initial_mount_node,
update_anchor_node,
update_mount_node
}: {
block: Block;
parent_node: Identifier;
parent_nodes: Identifier;
snippet: Node;
initial_anchor_node: Identifier;
initial_mount_node: Identifier;
update_anchor_node: Identifier;
update_mount_node: Identifier;
}) {
const { create_each_block, iterations, data_length, view_length } = this.vars;
const get_key = block.get_unique_name('get_key');
const lookup = block.get_unique_name(`${this.var.name}_lookup`);
block.add_variable(iterations, x`[]`);
block.add_variable(lookup, x`new @_Map()`);
if (this.fragment.nodes[0].is_dom_node()) {
this.block.first = this.fragment.nodes[0].var;
} else {
this.block.first = this.block.get_unique_name('first');
this.block.add_element(this.block.first, x`@empty()`, parent_nodes && x`@empty()`, null);
}
block.chunks.init.push(b`
const ${get_key} = #ctx => ${this.node.key.manipulate(block)};
@ -464,13 +449,11 @@ export default class EachBlockWrapper extends Wrapper {
${lookup}.set(key, ${iterations}[#i] = ${create_each_block}(key, child_ctx));
}
`);
block.chunks.create.push(b`
for (let #i = 0; #i < ${view_length}; #i += 1) {
${iterations}[#i].c();
}
`);
if (parent_nodes && this.renderer.options.hydratable) {
block.chunks.claim.push(b`
for (let #i = 0; #i < ${view_length}; #i += 1) {
@ -478,7 +461,6 @@ export default class EachBlockWrapper extends Wrapper {
}
`);
}
block.chunks.mount.push(b`
for (let #i = 0; #i < ${view_length}; #i += 1) {
if (${iterations}[#i]) {
@ -486,9 +468,7 @@ export default class EachBlockWrapper extends Wrapper {
}
}
`);
const dynamic = this.block.has_update_method;
const destroy = this.node.has_animation
? this.block.has_outros
? '@fix_and_outro_and_destroy_block'
@ -496,10 +476,8 @@ export default class EachBlockWrapper extends Wrapper {
: this.block.has_outros
? '@outro_and_destroy_block'
: '@destroy_block';
if (this.dependencies.size) {
this.block.maintain_context = true;
this.updates.push(b`
${this.vars.each_block_value} = ${snippet};
${this.renderer.options.dev && b`@validate_each_argument(${this.vars.each_block_value});`}
@ -525,7 +503,6 @@ export default class EachBlockWrapper extends Wrapper {
${this.block.has_outros && b`@check_outros();`}
`);
}
if (this.block.has_outros) {
block.chunks.outro.push(b`
for (let #i = 0; #i < ${view_length}; #i += 1) {
@ -533,14 +510,23 @@ export default class EachBlockWrapper extends Wrapper {
}
`);
}
block.chunks.destroy.push(b`
for (let #i = 0; #i < ${view_length}; #i += 1) {
${iterations}[#i].d(${parent_node ? null : 'detaching'});
}
`);
}
/**
* @param {{
* block: import('../Block.js').default;
* parent_nodes: import('estree').Identifier;
* snippet: import('estree').Node;
* initial_anchor_node: import('estree').Identifier;
* initial_mount_node: import('estree').Identifier;
* update_anchor_node: import('estree').Identifier;
* update_mount_node: import('estree').Identifier;
* }} params
*/
render_unkeyed({
block,
parent_nodes,
@ -549,17 +535,8 @@ export default class EachBlockWrapper extends Wrapper {
initial_mount_node,
update_anchor_node,
update_mount_node
}: {
block: Block;
parent_nodes: Identifier;
snippet: Node;
initial_anchor_node: Identifier;
initial_mount_node: Identifier;
update_anchor_node: Identifier;
update_mount_node: Identifier;
}) {
const { create_each_block, iterations, fixed_length, data_length, view_length } = this.vars;
block.chunks.init.push(b`
let ${iterations} = [];
@ -567,13 +544,11 @@ export default class EachBlockWrapper extends Wrapper {
${iterations}[#i] = ${create_each_block}(${this.vars.get_each_context}(#ctx, ${this.vars.each_block_value}, #i));
}
`);
block.chunks.create.push(b`
for (let #i = 0; #i < ${view_length}; #i += 1) {
${iterations}[#i].c();
}
`);
if (parent_nodes && this.renderer.options.hydratable) {
block.chunks.claim.push(b`
for (let #i = 0; #i < ${view_length}; #i += 1) {
@ -581,7 +556,6 @@ export default class EachBlockWrapper extends Wrapper {
}
`);
}
block.chunks.mount.push(b`
for (let #i = 0; #i < ${view_length}; #i += 1) {
if (${iterations}[#i]) {
@ -589,10 +563,8 @@ export default class EachBlockWrapper extends Wrapper {
}
}
`);
if (this.dependencies.size) {
const has_transitions = !!(this.block.has_intro_method || this.block.has_outro_method);
const for_loop_body = this.block.has_update_method
? b`
if (${iterations}[#i]) {
@ -623,14 +595,12 @@ export default class EachBlockWrapper extends Wrapper {
${iterations}[#i].m(${update_mount_node}, ${update_anchor_node});
}
`;
const start = this.block.has_update_method ? 0 : '#old_length';
let remove_old_blocks: Node[];
/** @type {import('estree').Node[]} */
let remove_old_blocks;
if (this.block.has_outros) {
const out = block.get_unique_name('out');
block.chunks.init.push(b`
const ${out} = i => @transition_out(${iterations}[i], 1, 1, () => {
${iterations}[i] = null;
@ -653,7 +623,6 @@ export default class EachBlockWrapper extends Wrapper {
${!fixed_length && b`${view_length} = ${data_length};`}
`;
}
// We declare `i` as block scoped here, as the `remove_old_blocks` code
// may rely on continuing where this iteration stopped.
const update = b`
@ -670,10 +639,8 @@ export default class EachBlockWrapper extends Wrapper {
${remove_old_blocks}
`;
this.updates.push(update);
}
if (this.block.has_outros) {
block.chunks.outro.push(b`
${iterations} = ${iterations}.filter(@_Boolean);
@ -682,7 +649,6 @@ export default class EachBlockWrapper extends Wrapper {
}
`);
}
block.chunks.destroy.push(b`@destroy_each(${iterations}, detaching);`);
}
}

@ -1,16 +1,10 @@
import Attribute from '../../../nodes/Attribute';
import Block from '../../Block';
import fix_attribute_casing from './fix_attribute_casing';
import ElementWrapper from './index';
import { string_literal } from '../../../utils/stringify';
import fix_attribute_casing from './fix_attribute_casing.js';
import { string_literal } from '../../../utils/stringify.js';
import { b, x } from 'code-red';
import Expression from '../../../nodes/shared/Expression';
import Text from '../../../nodes/Text';
import handle_select_value_binding from './handle_select_value_binding';
import { Identifier, Node } from 'estree';
import { namespaces } from '../../../../utils/namespaces';
import { BooleanAttributes, boolean_attributes } from '../../../../../shared/boolean_attributes';
import { regex_double_quotes } from '../../../../utils/patterns';
import handle_select_value_binding from './handle_select_value_binding.js';
import { namespaces } from '../../../../utils/namespaces.js';
import { boolean_attributes } from '../../../../../shared/boolean_attributes.js';
import { regex_double_quotes } from '../../../../utils/patterns.js';
const non_textlike_input_types = new Set([
'button',
@ -28,64 +22,82 @@ const non_textlike_input_types = new Set([
]);
export class BaseAttributeWrapper {
node: Attribute;
parent: ElementWrapper;
constructor(parent: ElementWrapper, block: Block, node: Attribute) {
/** @type {import('../../../nodes/Attribute.js').default} */
node;
/** @type {import('./index.js').default} */
parent;
/**
* @param {import('./index.js').default} parent
* @param {import('../../Block.js').default} block
* @param {import('../../../nodes/Attribute.js').default} node
*/
constructor(parent, block, node) {
this.node = node;
this.parent = parent;
if (node.dependencies.size > 0) {
block.add_dependencies(node.dependencies);
}
}
render(_block: Block) {}
/** @param {import('../../Block.js').default} _block */
render(_block) {}
}
const regex_minus_sign = /-/;
const regex_invalid_variable_identifier_characters = /[^a-zA-Z_$]/g;
/** @extends BaseAttributeWrapper */
export default class AttributeWrapper extends BaseAttributeWrapper {
node: Attribute;
parent: ElementWrapper;
metadata: any;
name: string;
property_name: string;
is_indirectly_bound_value: boolean;
is_src: boolean;
is_select_value_attribute: boolean;
is_input_value: boolean;
should_cache: boolean;
last: Identifier;
/** @type {any} */
metadata;
constructor(parent: ElementWrapper, block: Block, node: Attribute) {
super(parent, block, node);
/** @type {string} */
name;
/** @type {string} */
property_name;
/** @type {boolean} */
is_indirectly_bound_value;
/** @type {boolean} */
is_src;
/** @type {boolean} */
is_select_value_attribute;
/** @type {boolean} */
is_input_value;
/** @type {boolean} */
should_cache;
/** @type {import('estree').Identifier} */
last;
constructor(parent, block, node) {
super(parent, block, node);
if (node.dependencies.size > 0) {
// special case — <option value={foo}> — see below
if (this.parent.node.name === 'option' && node.name === 'value') {
let select: ElementWrapper = this.parent;
let select = this.parent;
while (select && (select.node.type !== 'Element' || select.node.name !== 'select')) {
// @ts-ignore todo: doublecheck this, but looks to be correct
select = select.parent;
}
if (select && select.select_binding_dependencies) {
select.select_binding_dependencies.forEach((prop) => {
this.node.dependencies.forEach((dependency: string) => {
this.node.dependencies.forEach((dependency) => {
this.parent.renderer.component.indirect_dependencies.get(prop).add(dependency);
});
});
}
}
if (node.name === 'value') {
handle_select_value_binding(this, node.dependencies);
this.parent.has_dynamic_value = true;
}
}
if (this.parent.node.namespace == namespaces.foreign) {
// leave attribute case alone for elements in the "foreign" namespace
this.name = this.node.name;
@ -104,7 +116,6 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
this.is_select_value_attribute = this.name === 'value' && this.parent.node.name === 'select';
this.is_input_value = this.name === 'value' && this.parent.node.name === 'input';
}
// TODO retire this exception in favour of https://github.com/sveltejs/svelte/issues/3750
this.is_src =
this.name === 'src' &&
@ -112,10 +123,10 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
this.should_cache = should_cache(this);
}
render(block: Block) {
/** @param {import('../../Block.js').default} block */
render(block) {
const element = this.parent;
const { name, property_name, should_cache, is_indirectly_bound_value } = this;
// xlink is a special case... we could maybe extend this to generic
// namespaced attributes but I'm not sure that's applicable in
// HTML5?
@ -124,31 +135,27 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
: name.slice(0, 6) === 'xlink:'
? '@xlink_attr'
: '@attr';
const is_legacy_input_type =
element.renderer.component.compile_options.legacy &&
name === 'type' &&
this.parent.node.name === 'input';
const dependencies = this.get_dependencies();
const value = this.get_value(block);
let updater: Node[];
/** @type {import('estree').Node[]} */
let updater;
const init = this.get_init(block, value);
if (is_legacy_input_type) {
block.chunks.hydrate.push(b`@set_input_type(${element.var}, ${init});`);
updater = b`@set_input_type(${element.var}, ${should_cache ? this.last : value});`;
} else if (this.is_select_value_attribute) {
// annoying special case
const is_multiple_select = element.node.get_static_attribute_value('multiple');
if (is_multiple_select) {
updater = b`@select_options(${element.var}, ${value});`;
} else {
updater = b`@select_option(${element.var}, ${value});`;
}
block.chunks.mount.push(b`
${updater}
`);
@ -166,17 +173,14 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
block.chunks.hydrate.push(b`${method}(${element.var}, "${name}", ${init});`);
updater = b`${method}(${element.var}, "${name}", ${should_cache ? this.last : value});`;
}
if (is_indirectly_bound_value) {
const update_value = b`@set_input_value(${element.var}, ${element.var}.__value);`;
block.chunks.hydrate.push(update_value);
updater = b`
${updater}
${update_value};
`;
}
if (this.node.name === 'value' && dependencies.length > 0) {
if (this.parent.bindings.some((binding) => binding.node.name === 'group')) {
this.parent.dynamic_value_condition = block.get_unique_name('value_has_changed');
@ -187,16 +191,13 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
`;
}
}
if (dependencies.length > 0) {
const condition = this.get_dom_update_conditions(block, block.renderer.dirty(dependencies));
block.chunks.update.push(b`
if (${condition}) {
${updater}
}`);
}
// special case autofocus. has to be handled in a bit of a weird way
if (name === 'autofocus') {
block.autofocus = {
@ -206,7 +207,11 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
}
}
get_init(block: Block, value) {
/**
* @param {import('../../Block.js').default} block
* @param {any} value
*/
get_init(block, value) {
this.last =
this.should_cache &&
block.get_unique_name(
@ -215,25 +220,24 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
'_'
)}_value`
);
if (this.should_cache) block.add_variable(this.last);
return this.should_cache ? x`${this.last} = ${value}` : value;
}
get_dom_update_conditions(block: Block, dependency_condition: Node) {
/**
* @param {import('../../Block.js').default} block
* @param {import('estree').Node} dependency_condition
*/
get_dom_update_conditions(block, dependency_condition) {
const { property_name, should_cache, last } = this;
const element = this.parent;
const value = this.get_value(block);
let condition = dependency_condition;
if (should_cache) {
condition = this.is_src
? x`${condition} && (!@src_url_equal(${element.var}.src, (${last} = ${value})))`
: x`${condition} && (${last} !== (${last} = ${value}))`;
}
if (this.is_input_value) {
const type = element.node.get_static_attribute_value('type');
if (type !== true && !non_textlike_input_types.has(type)) {
@ -242,19 +246,15 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
}`;
}
}
if (block.has_outros) {
condition = x`!#current || ${condition}`;
}
return condition;
}
get_dependencies() {
const node_dependencies = this.node.get_dependencies();
const dependencies = new Set(node_dependencies);
node_dependencies.forEach((prop: string) => {
node_dependencies.forEach((prop) => {
const indirect_dependencies = this.parent.renderer.component.indirect_dependencies.get(prop);
if (indirect_dependencies) {
indirect_dependencies.forEach((indirect_dependency) => {
@ -262,10 +262,8 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
});
}
});
return Array.from(dependencies);
}
get_metadata() {
if (this.parent.node.namespace) return null;
const metadata = attribute_lookup[this.name];
@ -274,7 +272,8 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
return metadata;
}
get_value(block: Block) {
/** @param {import('../../Block.js').default} block */
get_value(block) {
if (this.node.is_true) {
if (this.metadata && boolean_attributes.has(this.metadata.property_name.toLowerCase())) {
return x`true`;
@ -282,56 +281,54 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
return x`""`;
}
if (this.node.chunks.length === 0) return x`""`;
// TODO some of this code is repeated in Tag.ts — would be good to
// DRY it out if that's possible without introducing crazy indirection
if (this.node.chunks.length === 1) {
return this.node.chunks[0].type === 'Text'
? string_literal((this.node.chunks[0] as Text).data)
: (this.node.chunks[0] as Expression).manipulate(block);
? string_literal(
/** @type {import('../../../nodes/Text.js').default} */ (this.node.chunks[0]).data
)
: /** @type {import('../../../nodes/shared/Expression.js').default} */ (
this.node.chunks[0]
).manipulate(block);
}
let value =
this.node.name === 'class'
? this.get_class_name_text(block)
: this.render_chunks(block).reduce((lhs, rhs) => x`${lhs} + ${rhs}`);
// '{foo} {bar}' — treat as string concatenation
if (this.node.chunks[0].type !== 'Text') {
value = x`"" + ${value}`;
}
return value;
}
get_class_name_text(block: Block) {
const scoped_css = this.node.chunks.some((chunk: Text) => chunk.synthetic);
/** @param {import('../../Block.js').default} block */
get_class_name_text(block) {
const scoped_css = this.node.chunks.some(
(/** @type {import('../../../nodes/Text.js').default} */ chunk) => chunk.synthetic
);
const rendered = this.render_chunks(block);
if (scoped_css && rendered.length === 2) {
// we have a situation like class={possiblyUndefined}
rendered[0] = x`@null_to_empty(${rendered[0]})`;
}
return rendered.reduce((lhs, rhs) => x`${lhs} + ${rhs}`);
}
render_chunks(block: Block) {
/** @param {import('../../Block.js').default} block */
render_chunks(block) {
return this.node.chunks.map((chunk) => {
if (chunk.type === 'Text') {
return string_literal(chunk.data);
}
return chunk.manipulate(block);
});
}
stringify() {
if (this.node.is_true) return '';
const value = this.node.chunks;
if (value.length === 0) return '=""';
return `="${value
.map((chunk) => {
return chunk.type === 'Text'
@ -341,12 +338,13 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
.join('')}"`;
}
}
// source: https://html.spec.whatwg.org/multipage/indices.html
type AttributeMetadata = { property_name?: string; applies_to?: string[] };
const attribute_lookup: { [key in BooleanAttributes]: AttributeMetadata } & {
[key in string]: AttributeMetadata;
} = {
/**
* @type {{
* [key in import('../../../../../shared/boolean_attributes.js').BooleanAttributes]: { property_name?: string; applies_to?: string[] } } &
* { [key in string]: { property_name?: string; applies_to?: string[] }; }
* }
*/
const attribute_lookup = {
allowfullscreen: { property_name: 'allowFullscreen', applies_to: ['iframe'] },
allowpaymentrequest: { property_name: 'allowPaymentRequest', applies_to: ['iframe'] },
async: { applies_to: ['script'] },
@ -398,19 +396,19 @@ const attribute_lookup: { [key in BooleanAttributes]: AttributeMetadata } & {
]
}
};
Object.keys(attribute_lookup).forEach((name) => {
const metadata = attribute_lookup[name];
if (!metadata.property_name) metadata.property_name = name;
});
function should_cache(attribute: AttributeWrapper) {
/** @param {AttributeWrapper} attribute */
function should_cache(attribute) {
return attribute.is_src || attribute.node.should_cache();
}
const regex_contains_checked_or_group = /checked|group/;
function is_indirectly_bound_value(attribute: AttributeWrapper) {
/** @param {AttributeWrapper} attribute */
function is_indirectly_bound_value(attribute) {
const element = attribute.parent;
return (
attribute.name === 'value' &&

@ -1,56 +1,62 @@
import { b, x } from 'code-red';
import Binding from '../../../nodes/Binding';
import ElementWrapper from '../Element';
import InlineComponentWrapper from '../InlineComponent';
import get_object from '../../../utils/get_object';
import replace_object from '../../../utils/replace_object';
import Block from '../../Block';
import Renderer, { BindingGroup } from '../../Renderer';
import flatten_reference from '../../../utils/flatten_reference';
import { Node, Identifier } from 'estree';
import add_to_set from '../../../utils/add_to_set';
import mark_each_block_bindings from '../shared/mark_each_block_bindings';
import handle_select_value_binding from './handle_select_value_binding';
import { regex_box_size } from '../../../../utils/patterns';
import get_object from '../../../utils/get_object.js';
import replace_object from '../../../utils/replace_object.js';
import flatten_reference from '../../../utils/flatten_reference.js';
import add_to_set from '../../../utils/add_to_set.js';
import mark_each_block_bindings from '../shared/mark_each_block_bindings.js';
import handle_select_value_binding from './handle_select_value_binding.js';
import { regex_box_size } from '../../../../utils/patterns.js';
/** */
export default class BindingWrapper {
node: Binding;
parent: ElementWrapper | InlineComponentWrapper;
object: string;
handler: {
uses_context: boolean;
mutation: Node | Node[];
contextual_dependencies: Set<string>;
lhs?: Node;
};
snippet: Node;
is_readonly: boolean;
needs_lock: boolean;
binding_group: BindingGroup;
constructor(block: Block, node: Binding, parent: ElementWrapper | InlineComponentWrapper) {
/** @type {import('../../../nodes/Binding.js').default} */
node = undefined;
/** @type {import('./index.js').default | import('../InlineComponent/index.js').default} */
parent = undefined;
/** @type {string} */
object = undefined;
/**
* @type {{
* uses_context: boolean;
* mutation: import('estree').Node | import('estree').Node[];
* contextual_dependencies: Set<string>;
* lhs?: import('estree').Node;
* }}
*/
handler = undefined;
/** @type {import('estree').Node} */
snippet = undefined;
/** @type {boolean} */
is_readonly = undefined;
/** @type {boolean} */
needs_lock = undefined;
/** @type {import('../../Renderer.js').BindingGroup} */
binding_group = undefined;
/**
* @param {import('../../Block.js').default} block
* @param {import('../../../nodes/Binding.js').default} node
* @param {import('./index.js').default | import('../InlineComponent/index.js').default} parent
*/
constructor(block, node, parent) {
this.node = node;
this.parent = parent;
const { dependencies } = this.node.expression;
block.add_dependencies(dependencies);
// TODO does this also apply to e.g. `<input type='checkbox' bind:group='foo'>`?
handle_select_value_binding(this, dependencies);
if (node.is_contextual) {
mark_each_block_bindings(this.parent, this.node);
}
this.object = get_object(this.node.expression.node).name;
if (this.node.name === 'group') {
this.binding_group = get_binding_group(parent.renderer, this, block);
}
// view to model
this.handler = get_event_handler(
this,
@ -59,18 +65,13 @@ export default class BindingWrapper {
this.object,
this.node.raw_expression
);
this.snippet = this.node.expression.manipulate(block);
this.is_readonly = this.node.is_readonly;
this.needs_lock = this.node.name === 'currentTime'; // TODO others?
}
get_dependencies() {
const dependencies = new Set(this.node.expression.dependencies);
this.node.expression.dependencies.forEach((prop: string) => {
this.node.expression.dependencies.forEach((prop) => {
const indirect_dependencies = this.parent.renderer.component.indirect_dependencies.get(prop);
if (indirect_dependencies) {
indirect_dependencies.forEach((indirect_dependency) => {
@ -78,17 +79,14 @@ export default class BindingWrapper {
});
}
});
if (this.binding_group) {
this.binding_group.list_dependencies.forEach((dep) => dependencies.add(dep));
}
return dependencies;
}
get_update_dependencies() {
const object = this.object;
const dependencies = new Set<string>();
const dependencies = new Set();
if (this.node.expression.template_scope.names.has(object)) {
this.node.expression.template_scope.dependencies_for_name
.get(object)
@ -96,7 +94,6 @@ export default class BindingWrapper {
} else {
dependencies.add(object);
}
const result = new Set(dependencies);
dependencies.forEach((dependency) => {
const indirect_dependencies =
@ -107,32 +104,34 @@ export default class BindingWrapper {
});
}
});
return result;
}
is_readonly_media_attribute() {
return this.node.is_readonly_media_attribute();
}
render(block: Block, lock: Identifier) {
/**
* @param {import('../../Block.js').default} block
* @param {import('estree').Identifier} lock
*/
render(block, lock) {
if (this.is_readonly) return;
const { parent } = this;
const update_conditions: any[] = this.needs_lock ? [x`!${lock}`] : [];
const mount_conditions: any[] = [];
let update_or_condition: any = null;
/** @type {any[]} */
const update_conditions = this.needs_lock ? [x`!${lock}`] : [];
const dependency_array = Array.from(this.get_dependencies());
/** @type {any[]} */
const mount_conditions = [];
/** @type {any} */
let update_or_condition = null;
const dependency_array = Array.from(this.get_dependencies());
if (dependency_array.length > 0) {
update_conditions.push(block.renderer.dirty(dependency_array));
}
if (parent.node.name === 'input') {
const type = parent.node.get_static_attribute_value('type');
if (
type === null ||
type === '' ||
@ -147,72 +146,60 @@ export default class BindingWrapper {
update_conditions.push(x`@to_number(${parent.var}.${this.node.name}) !== ${this.snippet}`);
}
}
// model to view
let update_dom = get_dom_updater(parent, this, false);
let mount_dom = get_dom_updater(parent, this, true);
// special cases
switch (this.node.name) {
case 'group': {
block.renderer.add_to_context('$$binding_groups');
this.binding_group.add_element(block, this.parent.var);
if ((this.parent as ElementWrapper).has_dynamic_value) {
update_or_condition = (this.parent as ElementWrapper).dynamic_value_condition;
if (/** @type {import('./index.js').default} */ (this.parent).has_dynamic_value) {
update_or_condition = /** @type {import('./index.js').default} */ (this.parent)
.dynamic_value_condition;
}
break;
}
case 'textContent':
update_conditions.push(x`${this.snippet} !== ${parent.var}.textContent`);
mount_conditions.push(x`${this.snippet} !== void 0`);
break;
case 'innerText':
update_conditions.push(x`${this.snippet} !== ${parent.var}.innerText`);
mount_conditions.push(x`${this.snippet} !== void 0`);
break;
case 'innerHTML':
update_conditions.push(x`${this.snippet} !== ${parent.var}.innerHTML`);
mount_conditions.push(x`${this.snippet} !== void 0`);
break;
case 'currentTime':
update_conditions.push(x`!@_isNaN(${this.snippet})`);
mount_dom = null;
break;
case 'playbackRate':
case 'volume':
update_conditions.push(x`!@_isNaN(${this.snippet})`);
mount_conditions.push(x`!@_isNaN(${this.snippet})`);
break;
case 'paused': {
// this is necessary to prevent audio restarting by itself
const last = block.get_unique_name(`${parent.var.name}_is_paused`);
block.add_variable(last, x`true`);
update_conditions.push(x`${last} !== (${last} = ${this.snippet})`);
update_dom = b`${parent.var}[${last} ? "pause" : "play"]();`;
mount_dom = null;
break;
}
case 'value':
if (parent.node.get_static_attribute_value('type') === 'file') {
update_dom = null;
mount_dom = null;
}
}
if (update_dom) {
if (update_conditions.length > 0) {
let condition = update_conditions.reduce((lhs, rhs) => x`${lhs} && ${rhs}`);
if (update_or_condition) condition = x`${update_or_condition} || (${condition})`;
block.chunks.update.push(b`
if (${condition}) {
${update_dom}
@ -222,11 +209,9 @@ export default class BindingWrapper {
block.chunks.update.push(update_dom);
}
}
if (mount_dom) {
if (mount_conditions.length > 0) {
const condition = mount_conditions.reduce((lhs, rhs) => x`${lhs} && ${rhs}`);
block.chunks.mount.push(b`
if (${condition}) {
${mount_dom}
@ -239,21 +224,19 @@ export default class BindingWrapper {
}
}
function get_dom_updater(
element: ElementWrapper | InlineComponentWrapper,
binding: BindingWrapper,
mounting: boolean
) {
/**
* @param {import('./index.js').default | import('../InlineComponent/index.js').default} element
* @param {BindingWrapper} binding
* @param {boolean} mounting
*/
function get_dom_updater(element, binding, mounting) {
const { node } = element;
if (binding.is_readonly_media_attribute()) {
return null;
}
if (binding.node.name === 'this') {
return null;
}
if (node.name === 'select') {
return node.get_static_attribute_value('multiple') === true
? b`@select_options(${element.var}, ${binding.snippet})`
@ -261,34 +244,35 @@ function get_dom_updater(
? b`@select_option(${element.var}, ${binding.snippet}, true)`
: b`@select_option(${element.var}, ${binding.snippet})`;
}
if (binding.node.name === 'group') {
const type = node.get_static_attribute_value('type');
const condition =
type === 'checkbox'
? x`~(${binding.snippet} || []).indexOf(${element.var}.__value)`
: x`${element.var}.__value === ${binding.snippet}`;
return b`${element.var}.checked = ${condition};`;
}
if (binding.node.name === 'value') {
return b`@set_input_value(${element.var}, ${binding.snippet});`;
}
return b`${element.var}.${binding.node.name} = ${binding.snippet};`;
}
function get_binding_group(renderer: Renderer, binding: BindingWrapper, block: Block) {
/**
* @param {import('../../Renderer.js').default} renderer
* @param {BindingWrapper} binding
* @param {import('../../Block.js').default} block
*/
function get_binding_group(renderer, binding, block) {
const value = binding.node;
const { parts } = flatten_reference(value.raw_expression);
let keypath = parts.join('.');
const contexts = [];
const contextual_dependencies = new Set<string>();
const contextual_dependencies = new Set();
const { template_scope } = value.expression;
const add_contextual_dependency = (dep: string) => {
/** @param {string} dep */
const add_contextual_dependency = (dep) => {
contextual_dependencies.add(dep);
const owner = template_scope.get_owner(dep);
if (owner.type === 'EachBlock') {
@ -300,11 +284,10 @@ function get_binding_group(renderer: Renderer, binding: BindingWrapper, block: B
for (const dep of value.expression.contextual_dependencies) {
add_contextual_dependency(dep);
}
for (const dep of contextual_dependencies) {
const context = block.bindings.get(dep);
let key: string;
let name: string;
let key;
let name;
if (context) {
key = context.object.name;
name = context.property.name;
@ -315,13 +298,12 @@ function get_binding_group(renderer: Renderer, binding: BindingWrapper, block: B
keypath = `${key}@${keypath}`;
contexts.push(name);
}
// create a global binding_group across blocks
if (!renderer.binding_groups.has(keypath)) {
const index = renderer.binding_groups.size;
// the bind:group depends on the list in the {#each} block as well
// as reordering (removing and adding back to the DOM) may affect the value
const list_dependencies = new Set<string>();
const list_dependencies = new Set();
let parent = value.parent;
while (parent) {
if (parent.type === 'EachBlock') {
@ -331,21 +313,17 @@ function get_binding_group(renderer: Renderer, binding: BindingWrapper, block: B
}
parent = parent.parent;
}
/**
* When using bind:group with logic blocks, the inputs with bind:group may be scattered across different blocks.
* This therefore keeps track of all the <input> elements that have the same bind:group within the same block.
*/
const elements = new Map<Block, any>();
const elements = new Map();
contexts.forEach((context) => {
renderer.add_to_context(context, true);
});
renderer.binding_groups.set(keypath, {
binding_group: () => {
let obj = x`$$binding_groups[${index}]`;
if (contexts.length > 0) {
contexts.forEach((secondary_index) => {
obj = x`${obj}[${secondary_index}]`;
@ -389,38 +367,32 @@ function get_binding_group(renderer: Renderer, binding: BindingWrapper, block: B
}
});
}
// register the binding_group for the block
const binding_group = renderer.binding_groups.get(keypath);
block.binding_groups.add(binding_group);
return binding_group;
}
function get_event_handler(
binding: BindingWrapper,
renderer: Renderer,
block: Block,
name: string,
lhs: Node
): {
uses_context: boolean;
mutation: Node | Node[];
contextual_dependencies: Set<string>;
lhs?: Node;
} {
const contextual_dependencies = new Set<string>(binding.node.expression.contextual_dependencies);
/**
* @param {BindingWrapper} binding
* @param {import('../../Renderer.js').default} renderer
* @param {import('../../Block.js').default} block
* @param {string} name
* @param {import('estree').Node} lhs
* @returns {{ uses_context: boolean; mutation: import('estree').Node | import('estree').Node[]; contextual_dependencies: Set<string>; lhs?: import('estree').Node; }}
*/
function get_event_handler(binding, renderer, block, name, lhs) {
const contextual_dependencies = new Set(binding.node.expression.contextual_dependencies);
const context = block.bindings.get(name);
let set_store: Node[] | undefined;
/** @type {import('estree').Node[] | undefined} */
let set_store;
if (context) {
const { object, property, store, snippet } = context;
lhs = replace_object(lhs, snippet);
contextual_dependencies.add(object.name);
contextual_dependencies.add(property.name);
contextual_dependencies.delete(name);
if (store) {
set_store = b`${store}.set(${`$${store}`});`;
}
@ -431,49 +403,42 @@ function get_event_handler(
set_store = b`${store}.set(${object.name});`;
}
}
const value = get_value_from_dom(renderer, binding.parent, binding, contextual_dependencies);
const mutation = b`
${lhs} = ${value};
${set_store}
`;
return {
uses_context: binding.node.is_contextual || binding.node.expression.uses_context, // TODO this is messy
uses_context: binding.node.is_contextual || binding.node.expression.uses_context,
mutation,
contextual_dependencies,
lhs
};
}
function get_value_from_dom(
_renderer: Renderer,
element: ElementWrapper | InlineComponentWrapper,
binding: BindingWrapper,
contextual_dependencies: Set<string>
) {
/**
* @param {import('../../Renderer.js').default} _renderer
* @param {import('./index.js').default | import('../InlineComponent/index.js').default} element
* @param {BindingWrapper} binding
* @param {Set<string>} contextual_dependencies
*/
function get_value_from_dom(_renderer, element, binding, contextual_dependencies) {
const { node } = element;
const { name } = binding.node;
if (name === 'this') {
return x`$$value`;
}
// <div bind:contentRect|contentBoxSize|borderBoxSize|devicePixelContentBoxSize>
if (regex_box_size.test(name)) {
return x`@ResizeObserverSingleton.entries.get(this)?.${name}`;
}
// <select bind:value='selected>
if (node.name === 'select') {
return node.get_static_attribute_value('multiple') === true
? x`@select_multiple_value(this)`
: x`@select_value(this)`;
}
const type = node.get_static_attribute_value('type');
// <input type='checkbox' bind:group='foo'>
if (name === 'group') {
if (type === 'checkbox') {
@ -481,19 +446,15 @@ function get_value_from_dom(
add_to_set(contextual_dependencies, contexts);
return x`@get_binding_group_value(${binding_group()}, this.__value, this.checked)`;
}
return x`this.__value`;
}
// <input type='range|number' bind:value>
if (type === 'range' || type === 'number') {
return x`@to_number(this.${name})`;
}
if (name === 'buffered' || name === 'seekable' || name === 'played') {
return x`@time_ranges_to_array(this.${name})`;
}
// everything else
return x`this.${name}`;
}

@ -1,23 +1,24 @@
import EventHandler from '../../../nodes/EventHandler';
import Wrapper from '../shared/Wrapper';
import Block from '../../Block';
import { b, x, p } from 'code-red';
import { Expression } from 'estree';
const TRUE = x`true`;
const FALSE = x`false`;
export default class EventHandlerWrapper {
node: EventHandler;
parent: Wrapper;
/** @type {import('../../../nodes/EventHandler.js').default} */
node;
constructor(node: EventHandler, parent: Wrapper) {
/** @type {import('../shared/Wrapper.js').default} */
parent;
/**
* @param {import('../../../nodes/EventHandler.js').default} node
* @param {import('../shared/Wrapper.js').default} parent
*/
constructor(node, parent) {
this.node = node;
this.parent = parent;
if (!node.expression) {
this.parent.renderer.add_to_context(node.handler_name.name);
this.parent.renderer.component.partly_hoisted.push(b`
function ${node.handler_name.name}(event) {
@bubble.call(this, $$self, event);
@ -26,11 +27,11 @@ export default class EventHandlerWrapper {
}
}
get_snippet(block: Block) {
/** @param {import('../../Block.js').default} block */
get_snippet(block) {
const snippet = this.node.expression
? this.node.expression.manipulate(block)
: block.renderer.reference(this.node.handler_name);
if (this.node.reassigned) {
block.maintain_context = true;
return x`function () { if (@is_function(${snippet})) ${snippet}.apply(this, arguments); }`;
@ -38,18 +39,19 @@ export default class EventHandlerWrapper {
return snippet;
}
render(block: Block, target: string | Expression) {
/**
* @param {import('../../Block.js').default} block
* @param {string | import('estree').Expression} target
*/
render(block, target) {
let snippet = this.get_snippet(block);
if (this.node.modifiers.has('preventDefault')) snippet = x`@prevent_default(${snippet})`;
if (this.node.modifiers.has('stopPropagation')) snippet = x`@stop_propagation(${snippet})`;
if (this.node.modifiers.has('stopImmediatePropagation'))
snippet = x`@stop_immediate_propagation(${snippet})`;
if (this.node.modifiers.has('self')) snippet = x`@self(${snippet})`;
if (this.node.modifiers.has('trusted')) snippet = x`@trusted(${snippet})`;
const args = [];
const opts = ['nonpassive', 'passive', 'once', 'capture'].filter((mod) =>
this.node.modifiers.has(mod)
);
@ -64,13 +66,11 @@ export default class EventHandlerWrapper {
} else if (block.renderer.options.dev) {
args.push(FALSE);
}
if (block.renderer.options.dev) {
args.push(this.node.modifiers.has('preventDefault') ? TRUE : FALSE);
args.push(this.node.modifiers.has('stopPropagation') ? TRUE : FALSE);
args.push(this.node.modifiers.has('stopImmediatePropagation') ? TRUE : FALSE);
}
block.event_listeners.push(x`@listen(${target}, "${this.node.name}", ${snippet}, ${args})`);
}
}

@ -1,3 +1,3 @@
import { BaseAttributeWrapper } from './Attribute';
import { BaseAttributeWrapper } from './Attribute.js';
export default class SpreadAttributeWrapper extends BaseAttributeWrapper {}

@ -1,33 +1,18 @@
import { b, x } from 'code-red';
import Attribute from '../../../nodes/Attribute';
import Block from '../../Block';
import AttributeWrapper from './Attribute';
import ElementWrapper from '../Element';
import { string_literal } from '../../../utils/stringify';
import add_to_set from '../../../utils/add_to_set';
import Expression from '../../../nodes/shared/Expression';
import Text from '../../../nodes/Text';
export interface StyleProp {
key: string;
value: Array<Text | Expression>;
important: boolean;
}
import AttributeWrapper from './Attribute.js';
import { string_literal } from '../../../utils/stringify.js';
import add_to_set from '../../../utils/add_to_set.js';
/** @extends AttributeWrapper */
export default class StyleAttributeWrapper extends AttributeWrapper {
node: Attribute;
parent: ElementWrapper;
render(block: Block) {
/** @param {import('../../Block.js').default} block */
render(block) {
const style_props = optimize_style(this.node.chunks);
if (!style_props) return super.render(block);
style_props.forEach((prop: StyleProp) => {
style_props.forEach((prop) => {
let value;
if (is_dynamic(prop.value)) {
const prop_dependencies: Set<string> = new Set();
const prop_dependencies = new Set();
value = prop.value
.map((chunk) => {
if (chunk.type === 'Text') {
@ -38,95 +23,80 @@ export default class StyleAttributeWrapper extends AttributeWrapper {
}
})
.reduce((lhs, rhs) => x`${lhs} + ${rhs}`);
// TODO is this necessary? style.setProperty always treats value as string, no?
// if (prop.value.length === 1 || prop.value[0].type !== 'Text') {
// value = x`"" + ${value}`;
// }
if (prop_dependencies.size) {
let condition = block.renderer.dirty(Array.from(prop_dependencies));
if (block.has_outros) {
condition = x`!#current || ${condition}`;
}
const update = b`
if (${condition}) {
@set_style(${this.parent.var}, "${prop.key}", ${value}, ${prop.important ? 1 : null});
}`;
block.chunks.update.push(update);
}
} else {
value = string_literal((prop.value[0] as Text).data);
value = string_literal(
/** @type {import('../../../nodes/Text.js').default} */ (prop.value[0]).data
);
}
block.chunks.hydrate.push(
b`@set_style(${this.parent.var}, "${prop.key}", ${value}, ${prop.important ? 1 : null});`
);
});
}
}
const regex_style_prop_key = /^\s*([\w-]+):\s*/;
function optimize_style(value: Array<Text | Expression>) {
const props: StyleProp[] = [];
/** @param {Array<import('../../../nodes/Text.js').default | import('../../../nodes/shared/Expression.js').default>} value */
function optimize_style(value) {
/** @type {Array<{ key: string; value: Array<import('../../../nodes/Text.js').default | import('../../../nodes/shared/Expression.js').default>; important: boolean; }>} */
const props = [];
let chunks = value.slice();
while (chunks.length) {
const chunk = chunks[0];
if (chunk.type !== 'Text') return null;
const key_match = regex_style_prop_key.exec(chunk.data);
if (!key_match) return null;
const key = key_match[1];
const offset = key_match.index + key_match[0].length;
const remaining_data = chunk.data.slice(offset);
if (remaining_data) {
chunks[0] = {
chunks[0] = /** @type {import('../../../nodes/Text.js').default} */ ({
start: chunk.start + offset,
end: chunk.end,
type: 'Text',
data: remaining_data
} as Text;
});
} else {
chunks.shift();
}
const result = get_style_value(chunks);
props.push({ key, value: result.value, important: result.important });
chunks = result.chunks;
}
return props;
}
const regex_important_flag = /\s*!important\s*$/;
const regex_semicolon_or_whitespace = /[;\s]/;
function get_style_value(chunks: Array<Text | Expression>) {
const value: Array<Text | Expression> = [];
/** @param {Array<import('../../../nodes/Text.js').default | import('../../../nodes/shared/Expression.js').default>} chunks */
function get_style_value(chunks) {
/** @type {Array<import('../../../nodes/Text.js').default | import('../../../nodes/shared/Expression.js').default>} */
const value = [];
let in_url = false;
let quote_mark = null;
let escaped = false;
let closed = false;
while (chunks.length && !closed) {
const chunk = chunks.shift();
if (chunk.type === 'Text') {
let c = 0;
while (c < chunk.data.length) {
const char = chunk.data[c];
if (escaped) {
escaped = false;
} else if (char === '\\') {
@ -143,46 +113,42 @@ function get_style_value(chunks: Array<Text | Expression>) {
closed = true;
break;
}
c += 1;
}
if (c > 0) {
value.push({
type: 'Text',
start: chunk.start,
end: chunk.start + c,
data: chunk.data.slice(0, c)
} as Text);
value.push(
/** @type {import('../../../nodes/Text.js').default} */ ({
type: 'Text',
start: chunk.start,
end: chunk.start + c,
data: chunk.data.slice(0, c)
})
);
}
while (regex_semicolon_or_whitespace.test(chunk.data[c])) c += 1;
const remaining_data = chunk.data.slice(c);
if (remaining_data) {
chunks.unshift({
start: chunk.start + c,
end: chunk.end,
type: 'Text',
data: remaining_data
} as Text);
chunks.unshift(
/** @type {import('../../../nodes/Text.js').default} */ ({
start: chunk.start + c,
end: chunk.end,
type: 'Text',
data: remaining_data
})
);
break;
}
} else {
value.push(chunk);
}
}
let important = false;
const last_chunk = value[value.length - 1];
if (last_chunk && last_chunk.type === 'Text' && regex_important_flag.test(last_chunk.data)) {
important = true;
last_chunk.data = last_chunk.data.replace(regex_important_flag, '');
if (!last_chunk.data) value.pop();
}
return {
chunks,
value,
@ -190,6 +156,7 @@ function get_style_value(chunks: Array<Text | Expression>) {
};
}
function is_dynamic(value: Array<Text | Expression>) {
/** @param {Array<import('../../../nodes/Text.js').default | import('../../../nodes/shared/Expression.js').default>} value */
function is_dynamic(value) {
return value.length > 1 || value[0].type !== 'Text';
}

@ -9,7 +9,10 @@ svg_attributes.forEach((name) => {
svg_attribute_lookup.set(name.toLowerCase(), name);
});
export default function fix_attribute_casing(name: string) {
/**
* @param {string} name
*/
export default function fix_attribute_casing(name) {
name = name.toLowerCase();
return svg_attribute_lookup.get(name) || name;
}

@ -1,15 +1,12 @@
import AttributeWrapper from './Attribute';
import BindingWrapper from './Binding';
import ElementWrapper from './index';
export default function handle_select_value_binding(
attr: AttributeWrapper | BindingWrapper,
dependencies: Set<string>
) {
/**
* @param {import('./Attribute.js').default | import('./Binding.js').default} attr
* @param {Set<string>} dependencies
*/
export default function handle_select_value_binding(attr, dependencies) {
const { parent } = attr;
if (parent.node.name === 'select') {
(parent as ElementWrapper).select_binding_dependencies = dependencies;
dependencies.forEach((prop: string) => {
/** @type {import('./index.js').default} */ (parent).select_binding_dependencies = dependencies;
dependencies.forEach((prop) => {
parent.renderer.component.indirect_dependencies.set(prop, new Set());
});
}

File diff suppressed because it is too large Load Diff

@ -1,29 +1,24 @@
import Wrapper from './shared/Wrapper';
import AwaitBlock from './AwaitBlock';
import Body from './Body';
import DebugTag from './DebugTag';
import Document from './Document';
import EachBlock from './EachBlock';
import Element from './Element';
import Head from './Head';
import IfBlock from './IfBlock';
import KeyBlock from './KeyBlock';
import InlineComponent from './InlineComponent/index';
import MustacheTag from './MustacheTag';
import RawMustacheTag from './RawMustacheTag';
import Slot from './Slot';
import SlotTemplate from './SlotTemplate';
import Text from './Text';
import Comment from './Comment';
import Title from './Title';
import Window from './Window';
import { INode } from '../../nodes/interfaces';
import Renderer from '../Renderer';
import Block from '../Block';
import { trim_start, trim_end } from '../../../utils/trim';
import { link } from '../../../utils/link';
import { Identifier } from 'estree';
import { regex_starts_with_whitespace } from '../../../utils/patterns';
import AwaitBlock from './AwaitBlock.js';
import Body from './Body.js';
import DebugTag from './DebugTag.js';
import Document from './Document.js';
import EachBlock from './EachBlock.js';
import Element from './Element/index.js';
import Head from './Head.js';
import IfBlock from './IfBlock.js';
import KeyBlock from './KeyBlock.js';
import InlineComponent from './InlineComponent/index.js';
import MustacheTag from './MustacheTag.js';
import RawMustacheTag from './RawMustacheTag.js';
import Slot from './Slot.js';
import SlotTemplate from './SlotTemplate.js';
import Text from './Text.js';
import Comment from './Comment.js';
import Title from './Title.js';
import Window from './Window.js';
import { trim_start, trim_end } from '../../../utils/trim.js';
import { link } from '../../../utils/link.js';
import { regex_starts_with_whitespace } from '../../../utils/patterns.js';
const wrappers = {
AwaitBlock,
@ -47,7 +42,12 @@ const wrappers = {
Window
};
function trimmable_at(child: INode, next_sibling: Wrapper): boolean {
/**
* @param {import('../../nodes/interfaces.js').INode} child
* @param {import('./shared/Wrapper.js').default} next_sibling
* @returns {boolean}
*/
function trimmable_at(child, next_sibling) {
// Whitespace is trimmable if one of the following is true:
// The child and its sibling share a common nearest each block (not at an each block boundary)
// The next sibling's previous node is an each block
@ -58,43 +58,42 @@ function trimmable_at(child: INode, next_sibling: Wrapper): boolean {
}
export default class FragmentWrapper {
nodes: Wrapper[];
constructor(
renderer: Renderer,
block: Block,
nodes: INode[],
parent: Wrapper,
strip_whitespace: boolean,
next_sibling: Wrapper
) {
/** @type {import('./shared/Wrapper.js').default[]} */
nodes;
/**
* @param {import('../Renderer.js').default} renderer
* @param {import('../Block.js').default} block
* @param {import('../../nodes/interfaces.js').INode[]} nodes
* @param {import('./shared/Wrapper.js').default} parent
* @param {boolean} strip_whitespace
* @param {import('./shared/Wrapper.js').default} next_sibling
*/
constructor(renderer, block, nodes, parent, strip_whitespace, next_sibling) {
this.nodes = [];
let last_child: Wrapper;
let window_wrapper: Window | undefined;
/** @type {import('./shared/Wrapper.js').default} */
let last_child;
/** @type {import('./Window.js').default | undefined} */
let window_wrapper;
let i = nodes.length;
while (i--) {
const child = nodes[i];
if (!child.type) {
throw new Error('missing type');
}
if (!(child.type in wrappers)) {
throw new Error(`TODO implement ${child.type}`);
}
// special case — this is an easy way to remove whitespace surrounding
// <svelte:window/>. lil hacky but it works
if (child.type === 'Window') {
window_wrapper = new Window(renderer, block, parent, child);
continue;
}
if (child.type === 'Text') {
let { data } = child;
// We want to remove trailing whitespace inside an element/component/block,
// *unless* there is no whitespace between this node and its next sibling
if (this.nodes.length === 0) {
@ -103,29 +102,24 @@ export default class FragmentWrapper {
regex_starts_with_whitespace.test(next_sibling.node.data) &&
trimmable_at(child, next_sibling)
: !child.has_ancestor('EachBlock');
if (should_trim && !child.keep_space()) {
data = trim_end(data);
if (!data) continue;
}
}
// glue text nodes (which could e.g. be separated by comments) together
if (last_child && last_child.node.type === 'Text') {
(last_child as Text).data = data + (last_child as Text).data;
/** @type {import('./Text.js').default} */ (last_child).data =
data + /** @type {import('./Text.js').default} */ (last_child).data;
continue;
}
const wrapper = new Text(renderer, block, parent, child, data);
if (wrapper.skip) continue;
this.nodes.unshift(wrapper);
link(last_child, (last_child = wrapper));
} else {
const Wrapper = wrappers[child.type];
if (!Wrapper || (child.type === 'Comment' && !renderer.options.preserveComments)) continue;
const wrapper = new Wrapper(
renderer,
block,
@ -135,34 +129,34 @@ export default class FragmentWrapper {
last_child || next_sibling
);
this.nodes.unshift(wrapper);
link(last_child, (last_child = wrapper));
}
}
if (strip_whitespace) {
const first = this.nodes[0] as Text;
const first = /** @type {import('./Text.js').default} */ (this.nodes[0]);
if (first && first.node.type === 'Text' && !first.node.keep_space()) {
first.data = trim_start(first.data);
if (!first.data) {
first.var = null;
this.nodes.shift();
if (this.nodes[0]) {
this.nodes[0].prev = null;
}
}
}
}
if (window_wrapper) {
this.nodes.unshift(window_wrapper);
link(last_child, window_wrapper);
}
}
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
/**
* @param {import('../Block.js').default} block
* @param {import('estree').Identifier} parent_node
* @param {import('estree').Identifier} parent_nodes
*/
render(block, parent_node, parent_nodes) {
for (let i = 0; i < this.nodes.length; i += 1) {
this.nodes[i].render(block, parent_node, parent_nodes);
}

@ -1,25 +1,22 @@
import Wrapper from './shared/Wrapper';
import Renderer from '../Renderer';
import Block from '../Block';
import Head from '../../nodes/Head';
import FragmentWrapper from './Fragment';
import Wrapper from './shared/Wrapper.js';
import FragmentWrapper from './Fragment.js';
import { x, b } from 'code-red';
import { Identifier } from 'estree';
/** @extends Wrapper<import('../../nodes/Head.js').default> */
export default class HeadWrapper extends Wrapper {
fragment: FragmentWrapper;
node: Head;
/** @type {import('./Fragment.js').default} */
fragment;
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: Head,
strip_whitespace: boolean,
next_sibling: Wrapper
) {
/**
* @param {import('../Renderer.js').default} renderer
* @param {import('../Block.js').default} block
* @param {import('./shared/Wrapper.js').default} parent
* @param {import('../../nodes/Head.js').default} node
* @param {boolean} strip_whitespace
* @param {import('./shared/Wrapper.js').default} next_sibling
*/
constructor(renderer, block, parent, node, strip_whitespace, next_sibling) {
super(renderer, block, parent, node);
this.fragment = new FragmentWrapper(
renderer,
block,
@ -30,17 +27,25 @@ export default class HeadWrapper extends Wrapper {
);
}
render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) {
let nodes: Identifier;
/**
* @param {import('../Block.js').default} block
* @param {import('estree').Identifier} _parent_node
* @param {import('estree').Identifier} _parent_nodes
*/
render(block, _parent_node, _parent_nodes) {
/** @type {import('estree').Identifier} */
let nodes;
if (this.renderer.options.hydratable && this.fragment.nodes.length) {
nodes = block.get_unique_name('head_nodes');
block.chunks.claim.push(
b`const ${nodes} = @head_selector('${this.node.id}', @_document.head);`
);
}
this.fragment.render(block, x`@_document.head` as unknown as Identifier, nodes);
this.fragment.render(
block,
/** @type {unknown} */ /** @type {import('estree').Identifier} */ (x`@_document.head`),
nodes
);
if (nodes && this.renderer.options.hydratable) {
block.chunks.claim.push(b`${nodes}.forEach(@detach);`);
}

@ -1,52 +1,57 @@
import Wrapper from './shared/Wrapper';
import Renderer from '../Renderer';
import Block from '../Block';
import EachBlock from '../../nodes/EachBlock';
import IfBlock from '../../nodes/IfBlock';
import create_debugging_comment from './shared/create_debugging_comment';
import ElseBlock from '../../nodes/ElseBlock';
import FragmentWrapper from './Fragment';
import Wrapper from './shared/Wrapper.js';
import create_debugging_comment from './shared/create_debugging_comment.js';
import FragmentWrapper from './Fragment.js';
import { b, x } from 'code-red';
import { walk } from 'estree-walker';
import { is_head } from './shared/is_head';
import { Identifier, Node } from 'estree';
import { push_array } from '../../../utils/push_array';
import { add_const_tags, add_const_tags_context } from './shared/add_const_tags';
import { is_head } from './shared/is_head.js';
import { push_array } from '../../../utils/push_array.js';
import { add_const_tags, add_const_tags_context } from './shared/add_const_tags.js';
type DetachingOrNull = 'detaching' | null;
function is_else_if(node: ElseBlock) {
/** @param {import('../../nodes/ElseBlock.js').default} node */
function is_else_if(node) {
return node && node.children.length === 1 && node.children[0].type === 'IfBlock';
}
/** @extends Wrapper<import('../../nodes/IfBlock.js').default | import('../../nodes/ElseBlock.js').default> */
class IfBlockBranch extends Wrapper {
block: Block;
fragment: FragmentWrapper;
dependencies?: string[];
condition?: any;
snippet?: Node;
is_dynamic: boolean;
node: IfBlock | ElseBlock;
/** @type {import('../Block.js').default} */
block;
/** @type {import('./Fragment.js').default} */
fragment;
/** @type {string[]} */
dependencies;
/** @type {any} */
condition;
/** @type {import('estree').Node} */
snippet;
/** @type {boolean} */
is_dynamic;
/** */
var = null;
get_ctx_name: Node | undefined;
constructor(
renderer: Renderer,
block: Block,
parent: IfBlockWrapper,
node: IfBlock | ElseBlock,
strip_whitespace: boolean,
next_sibling: Wrapper
) {
/** @type {import('estree').Node | undefined} */
get_ctx_name;
/**
* @param {import('../Renderer.js').default} renderer
* @param {import('../Block.js').default} block
* @param {IfBlockWrapper} parent
* @param {import('../../nodes/IfBlock.js').default | import('../../nodes/ElseBlock.js').default} node
* @param {boolean} strip_whitespace
* @param {import('./shared/Wrapper.js').default} next_sibling
*/
constructor(renderer, block, parent, node, strip_whitespace, next_sibling) {
super(renderer, block, parent, node);
const { expression } = node as IfBlock;
const { expression } = /** @type {import('../../nodes/IfBlock.js').default} */ (node);
const is_else = !expression;
if (expression) {
this.dependencies = expression.dynamic_dependencies();
// TODO is this the right rule? or should any non-reference count?
// const should_cache = !is_reference(expression.node, null) && dependencies.length > 0;
let should_cache = false;
@ -57,25 +62,23 @@ class IfBlockBranch extends Wrapper {
}
}
});
if (should_cache) {
this.condition = block.get_unique_name('show_if');
this.snippet = expression.manipulate(block) as Node;
this.snippet = /** @type {import('estree').Node} */ (expression.manipulate(block));
} else {
this.condition = expression.manipulate(block);
}
}
add_const_tags_context(renderer, this.node.const_tags);
this.block = block.child({
comment: create_debugging_comment(node, parent.renderer.component),
name: parent.renderer.component.get_unique_name(
is_else ? 'create_else_block' : 'create_if_block'
),
type: (node as IfBlock).expression ? 'if' : 'else'
type: /** @type {import('../../nodes/IfBlock.js').default} */ (node).expression
? 'if'
: 'else'
});
this.fragment = new FragmentWrapper(
renderer,
this.block,
@ -84,9 +87,7 @@ class IfBlockBranch extends Wrapper {
strip_whitespace,
next_sibling
);
this.is_dynamic = this.block.dependencies.size > 0;
if (node.const_tags.length > 0) {
this.get_ctx_name = parent.renderer.component.get_unique_name(
is_else ? 'get_else_ctx' : 'get_if_ctx'
@ -95,54 +96,57 @@ class IfBlockBranch extends Wrapper {
}
}
/** @extends Wrapper<import('../../nodes/IfBlock.js').default> */
export default class IfBlockWrapper extends Wrapper {
node: IfBlock;
branches: IfBlockBranch[];
needs_update = false;
/** @typedef {'detaching' | null} DetachingOrNull */
var: Identifier = { type: 'Identifier', name: 'if_block' };
/** @type {IfBlockBranch[]} */
branches;
/** */
needs_update = false;
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: EachBlock,
strip_whitespace: boolean,
next_sibling: Wrapper
) {
/** @type {import('estree').Identifier} */
var = { type: 'Identifier', name: 'if_block' };
/**
* @param {import('../Renderer.js').default} renderer
* @param {import('../Block.js').default} block
* @param {import('./shared/Wrapper.js').default} parent
* @param {import('../../nodes/IfBlock.js').default} node
* @param {boolean} strip_whitespace
* @param {import('./shared/Wrapper.js').default} next_sibling
*/
constructor(renderer, block, parent, node, strip_whitespace, next_sibling) {
super(renderer, block, parent, node);
this.branches = [];
const blocks: Block[] = [];
/** @type {import('../Block.js').default[]} */
const blocks = [];
let is_dynamic = false;
let has_intros = false;
let has_outros = false;
const create_branches = (node: IfBlock) => {
/** @param {import('../../nodes/IfBlock.js').default} node */
const create_branches = (node) => {
const branch = new IfBlockBranch(renderer, block, this, node, strip_whitespace, next_sibling);
this.branches.push(branch);
blocks.push(branch.block);
block.add_dependencies(node.expression.dependencies);
if (branch.block.dependencies.size > 0) {
// the condition, or its contents, is dynamic
is_dynamic = true;
block.add_dependencies(branch.block.dependencies);
}
if (branch.dependencies && branch.dependencies.length > 0) {
// the condition itself is dynamic
this.needs_update = true;
}
if (branch.block.has_intros) has_intros = true;
if (branch.block.has_outros) has_outros = true;
if (is_else_if(node.else)) {
create_branches(node.else.children[0] as IfBlock);
create_branches(
/** @type {import('../../nodes/IfBlock.js').default} */ (node.else.children[0])
);
} else if (node.else) {
const branch = new IfBlockBranch(
renderer,
@ -152,50 +156,44 @@ export default class IfBlockWrapper extends Wrapper {
strip_whitespace,
next_sibling
);
this.branches.push(branch);
blocks.push(branch.block);
if (branch.block.dependencies.size > 0) {
is_dynamic = true;
block.add_dependencies(branch.block.dependencies);
}
if (branch.block.has_intros) has_intros = true;
if (branch.block.has_outros) has_outros = true;
}
};
create_branches(this.node);
blocks.forEach((block) => {
block.has_update_method = is_dynamic;
block.has_intro_method = has_intros;
block.has_outro_method = has_outros;
});
push_array(renderer.blocks, blocks);
}
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
/**
* @param {import('../Block.js').default} block
* @param {import('estree').Identifier} parent_node
* @param {import('estree').Identifier} parent_nodes
*/
render(block, parent_node, parent_nodes) {
const name = this.var;
const needs_anchor = this.next
? !this.next.is_dom_node()
: !parent_node || !this.parent.is_dom_node();
const anchor = needs_anchor
? block.get_unique_name(`${this.var.name}_anchor`)
: (this.next && this.next.var) || 'null';
const has_else = !this.branches[this.branches.length - 1].condition;
const if_exists_condition = has_else ? null : name;
const dynamic = this.branches[0].block.has_update_method; // can use [0] as proxy for all, since they necessarily have the same value
const has_intros = this.branches[0].block.has_intro_method;
const has_outros = this.branches[0].block.has_outro_method;
const has_transitions = has_intros || has_outros;
this.branches.forEach((branch) => {
if (branch.get_ctx_name) {
this.renderer.blocks.push(b`
@ -207,16 +205,14 @@ export default class IfBlockWrapper extends Wrapper {
`);
}
});
const vars = { name, anchor, if_exists_condition, has_else, has_transitions };
const detaching: DetachingOrNull = parent_node && !is_head(parent_node) ? null : 'detaching';
/** @type {DetachingOrNull} */
const detaching = parent_node && !is_head(parent_node) ? null : 'detaching';
if (this.node.else) {
this.branches.forEach((branch) => {
if (branch.snippet) block.add_variable(branch.condition);
});
if (has_outros) {
this.render_compound_with_outros(
block,
@ -226,25 +222,21 @@ export default class IfBlockWrapper extends Wrapper {
vars,
detaching
);
block.chunks.outro.push(b`@transition_out(${name});`);
} else {
this.render_compound(block, parent_node, parent_nodes, dynamic, vars, detaching);
}
} else {
this.render_simple(block, parent_node, parent_nodes, dynamic, vars, detaching);
if (has_outros) {
block.chunks.outro.push(b`@transition_out(${name});`);
}
}
if (if_exists_condition) {
block.chunks.create.push(b`if (${if_exists_condition}) ${name}.c();`);
} else {
block.chunks.create.push(b`${name}.c();`);
}
if (parent_nodes && this.renderer.options.hydratable) {
if (if_exists_condition) {
block.chunks.claim.push(b`if (${if_exists_condition}) ${name}.l(${parent_nodes});`);
@ -252,32 +244,41 @@ export default class IfBlockWrapper extends Wrapper {
block.chunks.claim.push(b`${name}.l(${parent_nodes});`);
}
}
if (has_intros || has_outros) {
block.chunks.intro.push(b`@transition_in(${name});`);
}
if (needs_anchor) {
block.add_element(
anchor as Identifier,
/** @type {import('estree').Identifier} */ (anchor),
x`@empty()`,
parent_nodes && x`@empty()`,
parent_node
);
}
this.branches.forEach((branch) => {
branch.fragment.render(branch.block, null, x`#nodes` as unknown as Identifier);
branch.fragment.render(
branch.block,
null,
/** @type {unknown} */ /** @type {import('estree').Identifier} */ (x`#nodes`)
);
});
}
/**
* @param {import('../Block.js').default} block
* @param {import('estree').Identifier} parent_node
* @param {import('estree').Identifier} _parent_nodes
* @param {boolean} dynamic
* @param {any} opts
* @param {DetachingOrNull} detaching
*/
render_compound(
block: Block,
parent_node: Identifier,
_parent_nodes: Identifier,
dynamic: boolean,
block,
parent_node,
_parent_nodes,
dynamic,
{ name, anchor, has_else, if_exists_condition, has_transitions },
detaching: DetachingOrNull
detaching
) {
const select_block_type = this.renderer.component.get_unique_name('select_block_type');
const current_block_type = block.get_unique_name('current_block_type');
@ -286,11 +287,9 @@ export default class IfBlockWrapper extends Wrapper {
? block.get_unique_name('select_block_ctx')
: null;
const if_ctx = select_block_ctx ? x`${select_block_ctx}(#ctx, ${current_block_type})` : x`#ctx`;
const get_block = has_else
? x`${current_block_type}(${if_ctx})`
: x`${current_block_type} && ${current_block_type}(${if_ctx})`;
if (this.needs_update) {
block.chunks.init.push(b`
function ${select_block_type}(#ctx, #dirty) {
@ -321,7 +320,6 @@ export default class IfBlockWrapper extends Wrapper {
}
`);
}
if (need_select_block_ctx) {
// if all branches needs create a context
if (this.branches.every((branch) => branch.get_ctx_name)) {
@ -353,15 +351,12 @@ export default class IfBlockWrapper extends Wrapper {
`);
}
}
block.chunks.init.push(b`
let ${current_block_type} = ${select_block_type}(#ctx, ${this.renderer.get_initial_dirty()});
let ${name} = ${get_block};
`);
const initial_mount_node = parent_node || '#target';
const anchor_node = parent_node ? 'null' : '#anchor';
if (if_exists_condition) {
block.chunks.mount.push(
b`if (${if_exists_condition}) ${name}.m(${initial_mount_node}, ${anchor_node});`
@ -369,10 +364,8 @@ export default class IfBlockWrapper extends Wrapper {
} else {
block.chunks.mount.push(b`${name}.m(${initial_mount_node}, ${anchor_node});`);
}
if (this.needs_update) {
const update_mount_node = this.get_update_mount_node(anchor);
const change_block = b`
${if_exists_condition ? b`if (${if_exists_condition}) ${name}.d(1)` : b`${name}.d(1)`};
${name} = ${get_block};
@ -382,7 +375,6 @@ export default class IfBlockWrapper extends Wrapper {
${name}.m(${update_mount_node}, ${anchor});
}
`;
if (dynamic) {
block.chunks.update.push(b`
if (${current_block_type} === (${current_block_type} = ${select_block_type}(#ctx, #dirty)) && ${name}) {
@ -405,7 +397,6 @@ export default class IfBlockWrapper extends Wrapper {
block.chunks.update.push(b`${name}.p(${if_ctx}, #dirty);`);
}
}
if (if_exists_condition) {
block.chunks.destroy.push(b`
if (${if_exists_condition}) {
@ -418,16 +409,24 @@ export default class IfBlockWrapper extends Wrapper {
`);
}
}
// if any of the siblings have outros, we need to keep references to the blocks
// (TODO does this only apply to bidi transitions?)
/**
* @param {import('../Block.js').default} block
* @param {import('estree').Identifier} parent_node
* @param {import('estree').Identifier} _parent_nodes
* @param {boolean} dynamic
* @param {any} opts
* @param {DetachingOrNull} detaching
*/
render_compound_with_outros(
block: Block,
parent_node: Identifier,
_parent_nodes: Identifier,
dynamic: boolean,
block,
parent_node,
_parent_nodes,
dynamic,
{ name, anchor, has_else, has_transitions, if_exists_condition },
detaching: DetachingOrNull
detaching
) {
const select_block_type = this.renderer.component.get_unique_name('select_block_type');
const current_block_type_index = block.get_unique_name('current_block_type_index');
@ -441,14 +440,11 @@ export default class IfBlockWrapper extends Wrapper {
const if_ctx = select_block_ctx
? x`${select_block_ctx}(#ctx, ${current_block_type_index})`
: x`#ctx`;
const if_current_block_type_index = has_else
? (nodes: Node[]) => nodes
: (nodes: Node[]) => b`if (~${current_block_type_index}) { ${nodes} }`;
? (nodes) => nodes
: (nodes) => b`if (~${current_block_type_index}) { ${nodes} }`;
block.add_variable(current_block_type_index);
block.add_variable(name);
block.chunks.init.push(b`
const ${if_block_creators} = [
${this.branches.map((branch) => branch.block.name)}
@ -487,7 +483,6 @@ export default class IfBlockWrapper extends Wrapper {
`
}
`);
if (need_select_block_ctx) {
// if all branches needs create a context
if (this.branches.every((branch) => branch.get_ctx_name)) {
@ -517,7 +512,6 @@ export default class IfBlockWrapper extends Wrapper {
`);
}
}
if (has_else) {
block.chunks.init.push(b`
${current_block_type_index} = ${select_block_type}(#ctx, ${this.renderer.get_initial_dirty()});
@ -530,19 +524,15 @@ export default class IfBlockWrapper extends Wrapper {
}
`);
}
const initial_mount_node = parent_node || '#target';
const anchor_node = parent_node ? 'null' : '#anchor';
block.chunks.mount.push(
if_current_block_type_index(
b`${if_blocks}[${current_block_type_index}].m(${initial_mount_node}, ${anchor_node});`
)
);
if (this.needs_update) {
const update_mount_node = this.get_update_mount_node(anchor);
const destroy_old_block = b`
@group_outros();
@transition_out(${if_blocks}[${previous_block_index}], 1, 1, () => {
@ -550,7 +540,6 @@ export default class IfBlockWrapper extends Wrapper {
});
@check_outros();
`;
const create_new_block = b`
${name} = ${if_blocks}[${current_block_type_index}];
if (!${name}) {
@ -562,7 +551,6 @@ export default class IfBlockWrapper extends Wrapper {
${has_transitions && b`@transition_in(${name}, 1);`}
${name}.m(${update_mount_node}, ${anchor});
`;
const change_block = has_else
? b`
${destroy_old_block}
@ -580,12 +568,10 @@ export default class IfBlockWrapper extends Wrapper {
${name} = null;
}
`;
block.chunks.update.push(b`
let ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(#ctx, #dirty);
`);
if (dynamic) {
block.chunks.update.push(b`
if (${current_block_type_index} === ${previous_block_index}) {
@ -608,37 +594,38 @@ export default class IfBlockWrapper extends Wrapper {
block.chunks.update.push(b`${name}.p(${if_ctx}, #dirty);`);
}
}
block.chunks.destroy.push(
if_current_block_type_index(b`${if_blocks}[${current_block_type_index}].d(${detaching});`)
);
}
/**
* @param {import('../Block.js').default} block
* @param {import('estree').Identifier} parent_node
* @param {import('estree').Identifier} _parent_nodes
* @param {boolean} dynamic
* @param {any} opts
* @param {DetachingOrNull} detaching
*/
render_simple(
block: Block,
parent_node: Identifier,
_parent_nodes: Identifier,
dynamic: boolean,
block,
parent_node,
_parent_nodes,
dynamic,
{ name, anchor, if_exists_condition, has_transitions },
detaching: DetachingOrNull
detaching
) {
const branch = this.branches[0];
const if_ctx = branch.get_ctx_name ? x`${branch.get_ctx_name}(#ctx)` : x`#ctx`;
if (branch.snippet) block.add_variable(branch.condition, branch.snippet);
block.chunks.init.push(b`
let ${name} = ${branch.condition} && ${branch.block.name}(${if_ctx});
`);
const initial_mount_node = parent_node || '#target';
const anchor_node = parent_node ? 'null' : '#anchor';
block.chunks.mount.push(b`if (${name}) ${name}.m(${initial_mount_node}, ${anchor_node});`);
if (branch.dependencies.length > 0) {
const update_mount_node = this.get_update_mount_node(anchor);
const enter = b`
if (${name}) {
${dynamic && b`${name}.p(${if_ctx}, #dirty);`}
@ -655,7 +642,6 @@ export default class IfBlockWrapper extends Wrapper {
${name}.m(${update_mount_node}, ${anchor});
}
`;
if (branch.snippet) {
block.chunks.update.push(
b`if (${block.renderer.dirty(branch.dependencies)}) ${branch.condition} = ${
@ -663,7 +649,6 @@ export default class IfBlockWrapper extends Wrapper {
}`
);
}
// no `p()` here — we don't want to update outroing nodes,
// as that will typically result in glitching
if (branch.block.has_outro_method) {
@ -693,7 +678,6 @@ export default class IfBlockWrapper extends Wrapper {
if (${branch.condition}) ${name}.p(${if_ctx}, #dirty);
`);
}
if (if_exists_condition) {
block.chunks.destroy.push(b`
if (${if_exists_condition}) ${name}.d(${detaching});

@ -1,82 +1,74 @@
import Wrapper from '../shared/Wrapper';
import BindingWrapper from '../Element/Binding';
import Renderer from '../../Renderer';
import Block from '../../Block';
import InlineComponent from '../../../nodes/InlineComponent';
import FragmentWrapper from '../Fragment';
import SlotTemplateWrapper from '../SlotTemplate';
import { sanitize } from '../../../../utils/names';
import add_to_set from '../../../utils/add_to_set';
import Wrapper from '../shared/Wrapper.js';
import BindingWrapper from '../Element/Binding.js';
import SlotTemplateWrapper from '../SlotTemplate.js';
import { sanitize } from '../../../../utils/names.js';
import add_to_set from '../../../utils/add_to_set.js';
import { b, x, p } from 'code-red';
import Attribute from '../../../nodes/Attribute';
import TemplateScope from '../../../nodes/shared/TemplateScope';
import is_dynamic from '../shared/is_dynamic';
import bind_this from '../shared/bind_this';
import { Node, Identifier, ObjectExpression } from 'estree';
import EventHandler from '../Element/EventHandler';
import is_dynamic from '../shared/is_dynamic.js';
import bind_this from '../shared/bind_this.js';
import EventHandler from '../Element/EventHandler.js';
import { extract_names } from 'periscopic';
import mark_each_block_bindings from '../shared/mark_each_block_bindings';
import { string_to_member_expression } from '../../../utils/string_to_member_expression';
import SlotTemplate from '../../../nodes/SlotTemplate';
import { is_head } from '../shared/is_head';
import compiler_warnings from '../../../compiler_warnings';
import { namespaces } from '../../../../utils/namespaces';
import { extract_ignores_above_node } from '../../../../utils/extract_svelte_ignore';
type SlotDefinition = {
block: Block;
scope: TemplateScope;
get_context?: Node;
get_changes?: Node;
};
import mark_each_block_bindings from '../shared/mark_each_block_bindings.js';
import { string_to_member_expression } from '../../../utils/string_to_member_expression.js';
import { is_head } from '../shared/is_head.js';
import compiler_warnings from '../../../compiler_warnings.js';
import { namespaces } from '../../../../utils/namespaces.js';
import { extract_ignores_above_node } from '../../../../utils/extract_svelte_ignore.js';
const regex_invalid_variable_identifier_characters = /[^a-zA-Z_$]/g;
/** @extends Wrapper<import('../../../nodes/InlineComponent.js').default> */
export default class InlineComponentWrapper extends Wrapper {
var: Identifier;
slots: Map<string, SlotDefinition> = new Map();
node: InlineComponent;
fragment: FragmentWrapper;
children: Array<Wrapper | FragmentWrapper> = [];
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: InlineComponent,
strip_whitespace: boolean,
next_sibling: Wrapper
) {
/**
* @typedef {{
* block: import('../../Block.js').default;
* scope: import('../../../nodes/shared/TemplateScope.js').default;
* get_context?: import('estree').Node;
* get_changes?: import('estree').Node;
* }} SlotDefinition
*/
/** @type {Map<string, SlotDefinition>} */
slots = new Map();
/** @type {import('../Fragment.js').default} */
fragment;
/** @type {Array<Wrapper | import('../Fragment.js').default>} */
children = [];
/**
* @param {import('../../Renderer.js').default} renderer
* @param {import('../../Block.js').default} block
* @param {import('../shared/Wrapper.js').default} parent
* @param {import('../../../nodes/InlineComponent.js').default} node
* @param {boolean} strip_whitespace
* @param {import('../shared/Wrapper.js').default} next_sibling
*/
constructor(renderer, block, parent, node, strip_whitespace, next_sibling) {
super(renderer, block, parent, node);
if (this.node.expression) {
block.add_dependencies(this.node.expression.dependencies);
}
this.node.attributes.forEach((attr) => {
block.add_dependencies(attr.dependencies);
});
this.node.bindings.forEach((binding) => {
if (binding.is_contextual) {
mark_each_block_bindings(this, binding);
}
block.add_dependencies(binding.expression.dependencies);
});
this.node.handlers.forEach((handler) => {
if (handler.expression) {
block.add_dependencies(handler.expression.dependencies);
}
});
this.node.css_custom_properties.forEach((attr) => {
block.add_dependencies(attr.dependencies);
});
this.var = {
type: 'Identifier',
type: /** @type {const} */ ('Identifier'),
name: (this.node.name === 'svelte:self'
? renderer.component.name.name
: this.node.name === 'svelte:component'
@ -84,31 +76,32 @@ export default class InlineComponentWrapper extends Wrapper {
: sanitize(this.node.name)
).toLowerCase()
};
if (this.node.children.length) {
this.node.lets.forEach((l) => {
extract_names(l.value || l.name).forEach((name) => {
renderer.add_to_context(name, true);
});
});
this.children = this.node.children.map(
(child) =>
new SlotTemplateWrapper(
renderer,
block,
this,
child as SlotTemplate,
/** @type {import('../../../nodes/SlotTemplate.js').default} */ (child),
strip_whitespace,
next_sibling
)
);
}
block.add_outro();
}
set_slot(name: string, slot_definition: SlotDefinition) {
/**
* @param {string} name
* @param {SlotDefinition} slot_definition
*/
set_slot(name, slot_definition) {
if (this.slots.has(name)) {
if (name === 'default') {
throw new Error('Found elements without slot attribute when using slot="default"');
@ -117,14 +110,12 @@ export default class InlineComponentWrapper extends Wrapper {
}
this.slots.set(name, slot_definition);
}
warn_if_reactive() {
const { name } = this.node;
const variable = this.renderer.component.var_lookup.get(name);
if (!variable) {
return;
}
const ignores = extract_ignores_above_node(this.node);
this.renderer.component.push_ignores(ignores);
if (variable.reassigned || variable.export_name || variable.is_reactive_dependency) {
@ -133,30 +124,33 @@ export default class InlineComponentWrapper extends Wrapper {
this.renderer.component.pop_ignores();
}
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
/**
* @param {import('../../Block.js').default} block
* @param {import('estree').Identifier} parent_node
* @param {import('estree').Identifier} parent_nodes
*/
render(block, parent_node, parent_nodes) {
this.warn_if_reactive();
const { renderer } = this;
const { component } = renderer;
const name = this.var;
block.add_variable(name);
const component_opts = /** @type {import('estree').ObjectExpression} */ (x`{}`);
const component_opts = x`{}` as ObjectExpression;
const statements: Array<Node | Node[]> = [];
const updates: Array<Node | Node[]> = [];
/** @type {Array<import('estree').Node | import('estree').Node[]>} */
const statements = [];
/** @type {Array<import('estree').Node | import('estree').Node[]>} */
const updates = [];
this.children.forEach((child) => {
this.renderer.add_to_context('$$scope', true);
child.render(block, null, x`#nodes` as Identifier);
child.render(block, null, /** @type {import('estree').Identifier} */ (x`#nodes`));
});
let props: Identifier | undefined;
/** @type {import('estree').Identifier | undefined} */
let props;
const name_changes = block.get_unique_name(`${name.name}_changes`);
const uses_spread = !!this.node.attributes.find((a) => a.is_spread);
// removing empty slot
for (const slot of this.slots.keys()) {
if (!this.slots.get(slot).block.has_content()) {
@ -164,7 +158,6 @@ export default class InlineComponentWrapper extends Wrapper {
this.slots.delete(slot);
}
}
const has_css_custom_properties = this.node.css_custom_properties.length > 0;
const is_svg_namespace = this.node.namespace === namespaces.svg;
const css_custom_properties_wrapper_element = is_svg_namespace ? 'g' : 'div';
@ -174,7 +167,6 @@ export default class InlineComponentWrapper extends Wrapper {
if (has_css_custom_properties) {
block.add_variable(css_custom_properties_wrapper);
}
const initial_props =
this.slots.size > 0
? [
@ -190,14 +182,12 @@ export default class InlineComponentWrapper extends Wrapper {
}`
]
: [];
const attribute_object = uses_spread
? x`{ ${initial_props} }`
: x`{
${this.node.attributes.map((attr) => p`${attr.name}: ${attr.get_value(block)}`)},
${initial_props}
}`;
if (this.node.attributes.length || this.node.bindings.length || initial_props.length) {
if (!uses_spread && this.node.bindings.length === 0) {
component_opts.properties.push(p`props: ${attribute_object}`);
@ -206,7 +196,6 @@ export default class InlineComponentWrapper extends Wrapper {
component_opts.properties.push(p`props: ${props}`);
}
}
if (component.compile_options.dev) {
// TODO this is a terrible hack, but without it the component
// will complain that options.target is missing. This would
@ -214,19 +203,15 @@ export default class InlineComponentWrapper extends Wrapper {
// APIs
component_opts.properties.push(p`$$inline: true`);
}
const fragment_dependencies = new Set(this.slots.size ? ['$$scope'] : []);
this.slots.forEach((slot) => {
slot.block.dependencies.forEach((name) => {
const is_let = slot.scope.is_let(name);
const variable = renderer.component.var_lookup.get(name);
if (is_let || is_dynamic(variable)) fragment_dependencies.add(name);
});
});
const dynamic_attributes = this.node.attributes.filter((a) => a.get_dependencies().length > 0);
if (
!uses_spread &&
(dynamic_attributes.length > 0 ||
@ -235,34 +220,30 @@ export default class InlineComponentWrapper extends Wrapper {
) {
updates.push(b`const ${name_changes} = {};`);
}
if (this.node.attributes.length) {
if (uses_spread) {
const levels = block.get_unique_name(`${this.var.name}_spread_levels`);
const initial_props = [];
const changes = [];
const all_dependencies: Set<string> = new Set();
/** @type {Set<string>} */
const all_dependencies = new Set();
this.node.attributes.forEach((attr) => {
add_to_set(all_dependencies, attr.dependencies);
});
this.node.attributes.forEach((attr, i) => {
const { name, dependencies } = attr;
const condition =
dependencies.size > 0 && dependencies.size !== all_dependencies.size
? renderer.dirty(Array.from(dependencies))
: null;
const unchanged = dependencies.size === 0;
let change_object: Node | ReturnType<typeof x>;
/** @type {import('estree').Node | ReturnType<typeof x>} */
let change_object;
if (attr.is_spread) {
const value = attr.expression.manipulate(block);
initial_props.push(value);
let value_object = value;
if (attr.expression.node.type !== 'ObjectExpression') {
value_object = x`@get_spread_object(${value})`;
@ -273,7 +254,6 @@ export default class InlineComponentWrapper extends Wrapper {
initial_props.push(obj);
change_object = obj;
}
changes.push(
unchanged
? x`${levels}[${i}]`
@ -282,22 +262,18 @@ export default class InlineComponentWrapper extends Wrapper {
: change_object
);
});
block.chunks.init.push(b`
const ${levels} = [
${initial_props}
];
`);
statements.push(b`
for (let #i = 0; #i < ${levels}.length; #i += 1) {
${props} = @assign(${props}, ${levels}[#i]);
}
`);
if (all_dependencies.size) {
const condition = renderer.dirty(Array.from(all_dependencies));
updates.push(b`
const ${name_changes} = ${condition} ? @get_spread_update(${levels}, [
${changes}
@ -309,11 +285,10 @@ export default class InlineComponentWrapper extends Wrapper {
`);
}
} else {
dynamic_attributes.forEach((attribute: Attribute) => {
dynamic_attributes.forEach((attribute) => {
const dependencies = attribute.get_dependencies();
if (dependencies.length > 0) {
const condition = renderer.dirty(dependencies);
updates.push(b`
if (${condition}) ${name_changes}.${attribute.name} = ${attribute.get_value(block)};
`);
@ -321,35 +296,27 @@ export default class InlineComponentWrapper extends Wrapper {
});
}
}
if (fragment_dependencies.size > 0) {
updates.push(b`
if (${renderer.dirty(Array.from(fragment_dependencies))}) {
${name_changes}.$$scope = { dirty: #dirty, ctx: #ctx };
}`);
}
const munged_bindings = this.node.bindings.map((binding) => {
component.has_reactive_assignments = true;
if (binding.name === 'this') {
return bind_this(component, block, new BindingWrapper(block, binding, this), this.var);
}
const id = component.get_unique_name(`${this.var.name}_${binding.name}_binding`);
renderer.add_to_context(id.name);
const callee = renderer.reference(id);
const updating = block.get_unique_name(`updating_${binding.name}`);
block.add_variable(updating);
const snippet = binding.expression.manipulate(block);
statements.push(b`
if (${snippet} !== void 0) {
${props}.${binding.name} = ${snippet};
}`);
updates.push(b`
if (!${updating} && ${renderer.dirty(Array.from(binding.expression.dependencies))}) {
${updating} = true;
@ -357,12 +324,9 @@ export default class InlineComponentWrapper extends Wrapper {
@add_flush_callback(() => ${updating} = false);
}
`);
const contextual_dependencies = Array.from(binding.expression.contextual_dependencies);
const dependencies = Array.from(binding.expression.dependencies);
let lhs = binding.raw_expression;
if (binding.is_contextual && binding.expression.node.type === 'Identifier') {
// bind:x={y} — we can't just do `y = x`, we need to
// to `array[index] = x;
@ -372,7 +336,8 @@ export default class InlineComponentWrapper extends Wrapper {
contextual_dependencies.push(object.name, property.name);
}
const params: Identifier[] = [x`#value` as Identifier];
/** @type {import('estree').Identifier[]} */
const params = [/** @type {import('estree').Identifier} */ (x`#value`)];
const args = [x`#value`];
if (contextual_dependencies.length > 0) {
contextual_dependencies.forEach((name) => {
@ -380,20 +345,16 @@ export default class InlineComponentWrapper extends Wrapper {
type: 'Identifier',
name
});
renderer.add_to_context(name, true);
args.push(renderer.reference(name));
});
block.maintain_context = true; // TODO put this somewhere more logical
}
block.chunks.init.push(b`
function ${id}(#value) {
${callee}(${args});
}
`);
let invalidate_binding = b`
${lhs} = #value;
${renderer.invalidate(dependencies[0])};
@ -405,40 +366,31 @@ export default class InlineComponentWrapper extends Wrapper {
}
`;
}
const body = b`
function ${id}(${params}) {
${invalidate_binding}
}
`;
component.partly_hoisted.push(body);
return b`@binding_callbacks.push(() => @bind(${this.var}, '${binding.name}', ${id}));`;
});
const munged_handlers = this.node.handlers.map((handler) => {
const event_handler = new EventHandler(handler, this);
let snippet = event_handler.get_snippet(block);
if (handler.modifiers.has('once')) snippet = x`@once(${snippet})`;
return b`${name}.$on("${handler.name}", ${snippet});`;
});
const mount_target = has_css_custom_properties
? css_custom_properties_wrapper
: parent_node || '#target';
const mount_anchor = has_css_custom_properties ? 'null' : parent_node ? 'null' : '#anchor';
const to_claim = parent_nodes && this.renderer.options.hydratable;
let claim_nodes = parent_nodes;
if (this.node.name === 'svelte:component') {
const switch_value = block.get_unique_name('switch_value');
const switch_props = block.get_unique_name('switch_props');
const snippet = this.node.expression.manipulate(block);
const dependencies = this.node.expression.dynamic_dependencies();
if (has_css_custom_properties) {
this.set_css_custom_properties(
block,
@ -447,7 +399,6 @@ export default class InlineComponentWrapper extends Wrapper {
is_svg_namespace
);
}
block.chunks.init.push(b`
var ${switch_value} = ${snippet};
@ -468,9 +419,7 @@ export default class InlineComponentWrapper extends Wrapper {
${munged_handlers}
}
`);
block.chunks.create.push(b`if (${name}) @create_component(${name}.$$.fragment);`);
if (css_custom_properties_wrapper)
this.create_css_custom_properties_wrapper_mount_chunk(
block,
@ -480,7 +429,6 @@ export default class InlineComponentWrapper extends Wrapper {
block.chunks.mount.push(
b`if (${name}) @mount_component(${name}, ${mount_target}, ${mount_anchor});`
);
if (to_claim) {
if (css_custom_properties_wrapper)
claim_nodes = this.create_css_custom_properties_wrapper_claim_chunk(
@ -494,13 +442,11 @@ export default class InlineComponentWrapper extends Wrapper {
b`if (${name}) @claim_component(${name}.$$.fragment, ${claim_nodes});`
);
}
if (updates.length) {
block.chunks.update.push(b`
${updates}
`);
}
const tmp_anchor = this.get_or_create_anchor(block, parent_node, parent_nodes);
const anchor = has_css_custom_properties ? 'null' : tmp_anchor;
const update_mount_node = has_css_custom_properties
@ -511,12 +457,10 @@ export default class InlineComponentWrapper extends Wrapper {
(tmp_anchor.name !== 'null'
? b`@insert(${tmp_anchor}.parentNode, ${css_custom_properties_wrapper}, ${tmp_anchor});`
: b`@insert(${parent_node}, ${css_custom_properties_wrapper}, ${tmp_anchor});`);
let update_condition = x`${switch_value} !== (${switch_value} = ${snippet})`;
if (dependencies.length > 0) {
update_condition = x`${block.renderer.dirty(dependencies)} && ${update_condition}`;
}
block.chunks.update.push(b`
if (${update_condition}) {
if (${name}) {
@ -546,13 +490,10 @@ export default class InlineComponentWrapper extends Wrapper {
${updates.length > 0 && b`${name}.$set(${name_changes});`}
}
`);
block.chunks.intro.push(b`
if (${name}) @transition_in(${name}.$$.fragment, #local);
`);
block.chunks.outro.push(b`if (${name}) @transition_out(${name}.$$.fragment, #local);`);
block.chunks.destroy.push(
b`if (${name}) @destroy_component(${name}, ${parent_node ? null : 'detaching'});`
);
@ -561,7 +502,6 @@ export default class InlineComponentWrapper extends Wrapper {
this.node.name === 'svelte:self'
? component.name
: this.renderer.reference(string_to_member_expression(this.node.name));
block.chunks.init.push(b`
${
(this.node.attributes.length > 0 || this.node.bindings.length > 0) &&
@ -574,7 +514,6 @@ export default class InlineComponentWrapper extends Wrapper {
${munged_bindings}
${munged_handlers}
`);
if (has_css_custom_properties) {
this.set_css_custom_properties(
block,
@ -584,7 +523,6 @@ export default class InlineComponentWrapper extends Wrapper {
);
}
block.chunks.create.push(b`@create_component(${name}.$$.fragment);`);
if (css_custom_properties_wrapper)
this.create_css_custom_properties_wrapper_mount_chunk(
block,
@ -592,7 +530,6 @@ export default class InlineComponentWrapper extends Wrapper {
css_custom_properties_wrapper
);
block.chunks.mount.push(b`@mount_component(${name}, ${mount_target}, ${mount_anchor});`);
if (to_claim) {
if (css_custom_properties_wrapper)
claim_nodes = this.create_css_custom_properties_wrapper_claim_chunk(
@ -604,30 +541,32 @@ export default class InlineComponentWrapper extends Wrapper {
);
block.chunks.claim.push(b`@claim_component(${name}.$$.fragment, ${claim_nodes});`);
}
block.chunks.intro.push(b`
@transition_in(${name}.$$.fragment, #local);
`);
if (updates.length) {
block.chunks.update.push(b`
${updates}
${name}.$set(${name_changes});
`);
}
block.chunks.destroy.push(b`
@destroy_component(${name}, ${parent_node ? null : 'detaching'});
`);
block.chunks.outro.push(b`@transition_out(${name}.$$.fragment, #local);`);
}
}
private create_css_custom_properties_wrapper_mount_chunk(
block: Block,
parent_node: Identifier,
css_custom_properties_wrapper: Identifier | null
/**
* @private
* @param {import('../../Block.js').default} block
* @param {import('estree').Identifier} parent_node
* @param {import('estree').Identifier | null} css_custom_properties_wrapper
*/
create_css_custom_properties_wrapper_mount_chunk(
block,
parent_node,
css_custom_properties_wrapper
) {
if (parent_node) {
block.chunks.mount.push(b`@append(${parent_node}, ${css_custom_properties_wrapper})`);
@ -644,12 +583,20 @@ export default class InlineComponentWrapper extends Wrapper {
}
}
private create_css_custom_properties_wrapper_claim_chunk(
block: Block,
parent_nodes: Identifier,
css_custom_properties_wrapper: Identifier | null,
css_custom_properties_wrapper_element: string,
is_svg_namespace: boolean
/**
* @private
* @param {import('../../Block.js').default} block
* @param {import('estree').Identifier} parent_nodes
* @param {import('estree').Identifier | null} css_custom_properties_wrapper
* @param {string} css_custom_properties_wrapper_element
* @param {boolean} is_svg_namespace
*/
create_css_custom_properties_wrapper_claim_chunk(
block,
parent_nodes,
css_custom_properties_wrapper,
css_custom_properties_wrapper_element,
is_svg_namespace
) {
const nodes = block.get_unique_name(`${css_custom_properties_wrapper.name}_nodes`);
const claim_element = is_svg_namespace ? x`@claim_svg_element` : x`@claim_element`;
@ -660,11 +607,18 @@ export default class InlineComponentWrapper extends Wrapper {
return nodes;
}
private set_css_custom_properties(
block: Block,
css_custom_properties_wrapper: Identifier,
css_custom_properties_wrapper_element: string,
is_svg_namespace: boolean
/**
* @private
* @param {import('../../Block.js').default} block
* @param {import('estree').Identifier} css_custom_properties_wrapper
* @param {string} css_custom_properties_wrapper_element
* @param {boolean} is_svg_namespace
*/
set_css_custom_properties(
block,
css_custom_properties_wrapper,
css_custom_properties_wrapper_element,
is_svg_namespace
) {
const element = is_svg_namespace ? x`@svg_element` : x`@element`;
block.chunks.create.push(
@ -685,7 +639,6 @@ export default class InlineComponentWrapper extends Wrapper {
if (should_cache) block.add_variable(last);
const value = attr.get_value(block);
const init = should_cache ? x`${last} = ${value}` : value;
block.chunks.hydrate.push(
b`@set_style(${css_custom_properties_wrapper}, "${attr.name}", ${init});`
);

@ -1,31 +1,33 @@
import Wrapper from './shared/Wrapper';
import Renderer from '../Renderer';
import Block from '../Block';
import KeyBlock from '../../nodes/KeyBlock';
import create_debugging_comment from './shared/create_debugging_comment';
import FragmentWrapper from './Fragment';
import Wrapper from './shared/Wrapper.js';
import create_debugging_comment from './shared/create_debugging_comment.js';
import FragmentWrapper from './Fragment.js';
import { b, x } from 'code-red';
import { Identifier } from 'estree';
/** @extends Wrapper<import('../../nodes/KeyBlock.js').default> */
export default class KeyBlockWrapper extends Wrapper {
node: KeyBlock;
fragment: FragmentWrapper;
block: Block;
dependencies: string[];
var: Identifier = { type: 'Identifier', name: 'key_block' };
/** @type {import('./Fragment.js').default} */
fragment;
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: KeyBlock,
strip_whitespace: boolean,
next_sibling: Wrapper
) {
super(renderer, block, parent, node);
/** @type {import('../Block.js').default} */
block;
this.dependencies = node.expression.dynamic_dependencies();
/** @type {string[]} */
dependencies;
/** @type {import('estree').Identifier} */
var = { type: 'Identifier', name: 'key_block' };
/**
* @param {import('../Renderer.js').default} renderer
* @param {import('../Block.js').default} block
* @param {import('./shared/Wrapper.js').default} parent
* @param {import('../../nodes/KeyBlock.js').default} node
* @param {boolean} strip_whitespace
* @param {import('./shared/Wrapper.js').default} next_sibling
*/
constructor(renderer, block, parent, node, strip_whitespace, next_sibling) {
super(renderer, block, parent, node);
this.dependencies = node.expression.dynamic_dependencies();
if (this.dependencies.length) {
block = block.child({
comment: create_debugging_comment(node, renderer.component),
@ -35,7 +37,6 @@ export default class KeyBlockWrapper extends Wrapper {
block.add_dependencies(node.expression.dependencies);
renderer.blocks.push(block);
}
this.block = block;
this.fragment = new FragmentWrapper(
renderer,
@ -47,7 +48,12 @@ export default class KeyBlockWrapper extends Wrapper {
);
}
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
/**
* @param {import('../Block.js').default} block
* @param {import('estree').Identifier} parent_node
* @param {import('estree').Identifier} parent_nodes
*/
render(block, parent_node, parent_nodes) {
if (this.dependencies.length === 0) {
this.render_static_key(block, parent_node, parent_nodes);
} else {
@ -55,27 +61,37 @@ export default class KeyBlockWrapper extends Wrapper {
}
}
render_static_key(_block: Block, parent_node: Identifier, parent_nodes: Identifier) {
/**
* @param {import('../Block.js').default} _block
* @param {import('estree').Identifier} parent_node
* @param {import('estree').Identifier} parent_nodes
*/
render_static_key(_block, parent_node, parent_nodes) {
this.fragment.render(this.block, parent_node, parent_nodes);
}
render_dynamic_key(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
this.fragment.render(this.block, null, x`#nodes` as unknown as Identifier);
/**
* @param {import('../Block.js').default} block
* @param {import('estree').Identifier} parent_node
* @param {import('estree').Identifier} parent_nodes
*/
render_dynamic_key(block, parent_node, parent_nodes) {
this.fragment.render(
this.block,
null,
/** @type {unknown} */ /** @type {import('estree').Identifier} */ (x`#nodes`)
);
const has_transitions = !!(this.block.has_intro_method || this.block.has_outro_method);
const dynamic = this.block.has_update_method;
const previous_key = block.get_unique_name('previous_key');
const snippet = this.node.expression.manipulate(block);
block.add_variable(previous_key, snippet);
const not_equal = this.renderer.component.component_options.immutable
? x`@not_equal`
: x`@safe_not_equal`;
const condition = x`${this.renderer.dirty(
this.dependencies
)} && ${not_equal}(${previous_key}, ${previous_key} = ${snippet})`;
block.chunks.init.push(b`
let ${this.var} = ${this.block.name}(#ctx);
`);
@ -102,7 +118,6 @@ export default class KeyBlockWrapper extends Wrapper {
${has_transitions && b`@transition_in(${this.var}, 1)`}
${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor});
`;
if (dynamic) {
block.chunks.update.push(b`
if (${condition}) {
@ -118,12 +133,10 @@ export default class KeyBlockWrapper extends Wrapper {
}
`);
}
if (has_transitions) {
block.chunks.intro.push(b`@transition_in(${this.var})`);
block.chunks.outro.push(b`@transition_out(${this.var})`);
}
block.chunks.destroy.push(b`${this.var}.d(detaching)`);
}
}

@ -1,43 +1,42 @@
import Renderer from '../Renderer';
import Block from '../Block';
import Tag from './shared/Tag';
import Wrapper from './shared/Wrapper';
import MustacheTag from '../../nodes/MustacheTag';
import RawMustacheTag from '../../nodes/RawMustacheTag';
import Tag from './shared/Tag.js';
import { x } from 'code-red';
import { Identifier, Expression } from 'estree';
import ElementWrapper from './Element';
import AttributeWrapper from './Element/Attribute';
import ElementWrapper from './Element/index.js';
/** @extends Tag */
export default class MustacheTagWrapper extends Tag {
var: Identifier = { type: 'Identifier', name: 't' };
/** @type {import('estree').Identifier} */
var = { type: 'Identifier', name: 't' };
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: MustacheTag | RawMustacheTag
) {
/**
* @param {import('../Renderer.js').default} renderer
* @param {import('../Block.js').default} block
* @param {import('./shared/Wrapper.js').default} parent
* @param {import('../../nodes/MustacheTag.js').default | import('../../nodes/RawMustacheTag.js').default} node
*/
constructor(renderer, block, parent, node) {
super(renderer, block, parent, node);
}
render(
block: Block,
parent_node: Identifier,
parent_nodes: Identifier,
data: Record<string, unknown> | undefined
) {
/**
* @param {import('../Block.js').default} block
* @param {import('estree').Identifier} parent_node
* @param {import('estree').Identifier} parent_nodes
* @param {Record<string, unknown> | undefined} data
*/
render(block, parent_node, parent_nodes, data) {
const contenteditable_attributes =
this.parent instanceof ElementWrapper &&
this.parent.attributes.filter((a) => a.node.name === 'contenteditable');
const spread_attributes =
this.parent instanceof ElementWrapper &&
this.parent.attributes.filter((a) => a.node.is_spread);
let contenteditable_attr_value: Expression | true | undefined = undefined;
/** @type {import('estree').Expression | true | undefined} */
let contenteditable_attr_value = undefined;
if (contenteditable_attributes.length > 0) {
const attribute = contenteditable_attributes[0] as AttributeWrapper;
const attribute = /** @type {import('./Element/Attribute.js').default} */ (
contenteditable_attributes[0]
);
if ([true, 'true', ''].includes(attribute.node.get_static_value())) {
contenteditable_attr_value = true;
} else {
@ -46,7 +45,6 @@ export default class MustacheTagWrapper extends Tag {
} else if (spread_attributes.length > 0 && data.element_data_name) {
contenteditable_attr_value = x`${data.element_data_name}['contenteditable']`;
}
const { init } = this.rename_this_method(block, (value) => {
if (contenteditable_attr_value) {
if (contenteditable_attr_value === true) {
@ -58,7 +56,6 @@ export default class MustacheTagWrapper extends Tag {
return x`@set_data(${this.var}, ${value})`;
}
});
block.add_element(
this.var,
x`@text(${init})`,

@ -1,56 +1,50 @@
import { namespaces } from '../../../utils/namespaces';
import { namespaces } from '../../../utils/namespaces.js';
import { b, x } from 'code-red';
import Renderer from '../Renderer';
import Block from '../Block';
import Tag from './shared/Tag';
import Wrapper from './shared/Wrapper';
import Element from '../../nodes/Element';
import MustacheTag from '../../nodes/MustacheTag';
import RawMustacheTag from '../../nodes/RawMustacheTag';
import { is_head } from './shared/is_head';
import { Identifier, Node } from 'estree';
import Tag from './shared/Tag.js';
import { is_head } from './shared/is_head.js';
/** @extends Tag */
export default class RawMustacheTagWrapper extends Tag {
var: Identifier = { type: 'Identifier', name: 'raw' };
/** @type {import('estree').Identifier} */
var = { type: 'Identifier', name: 'raw' };
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: MustacheTag | RawMustacheTag
) {
/**
* @param {import('../Renderer.js').default} renderer
* @param {import('../Block.js').default} block
* @param {import('./shared/Wrapper.js').default} parent
* @param {import('../../nodes/MustacheTag.js').default | import('../../nodes/RawMustacheTag.js').default} node
*/
constructor(renderer, block, parent, node) {
super(renderer, block, parent, node);
}
render(block: Block, parent_node: Identifier, _parent_nodes: Identifier) {
/**
* @param {import('../Block.js').default} block
* @param {import('estree').Identifier} parent_node
* @param {import('estree').Identifier} _parent_nodes
*/
render(block, parent_node, _parent_nodes) {
const in_head = is_head(parent_node);
const can_use_innerhtml = !in_head && parent_node && !this.prev && !this.next;
if (can_use_innerhtml) {
const insert = (content: Node) => b`${parent_node}.innerHTML = ${content};`[0];
/** @param {import('estree').Node} content */
const insert = (content) => b`${parent_node}.innerHTML = ${content};`[0];
const { init } = this.rename_this_method(block, (content) => insert(content));
block.chunks.mount.push(insert(init));
} else {
const needs_anchor =
in_head ||
(this.next ? !this.next.is_dom_node() : !this.parent || !this.parent.is_dom_node());
const html_tag = block.get_unique_name('html_tag');
const html_anchor = needs_anchor && block.get_unique_name('html_anchor');
block.add_variable(html_tag);
const { init } = this.rename_this_method(block, (content) => x`${html_tag}.p(${content})`);
const update_anchor = needs_anchor ? html_anchor : this.next ? this.next.var : 'null';
const parent_element = this.node.find_nearest(/^Element/) as Element;
const parent_element = /** @type {import('../../nodes/Element.js').default} */ (
this.node.find_nearest(/^Element/)
);
const is_svg = parent_element && parent_element.namespace === namespaces.svg;
block.chunks.create.push(b`${html_tag} = new @HtmlTag(${is_svg ? 'true' : 'false'});`);
if (this.renderer.options.hydratable) {
block.chunks.claim.push(
b`${html_tag} = @claim_html_tag(${_parent_nodes}, ${is_svg ? 'true' : 'false'});`
@ -60,11 +54,9 @@ export default class RawMustacheTagWrapper extends Tag {
block.chunks.mount.push(
b`${html_tag}.m(${init}, ${parent_node || '#target'}, ${parent_node ? null : '#anchor'});`
);
if (needs_anchor) {
block.add_element(html_anchor, x`@empty()`, x`@empty()`, parent_node);
}
if (!parent_node || in_head) {
block.chunks.destroy.push(b`if (detaching) ${html_tag}.d();`);
}

@ -1,36 +1,40 @@
import Wrapper from './shared/Wrapper';
import Renderer from '../Renderer';
import Block from '../Block';
import Slot from '../../nodes/Slot';
import FragmentWrapper from './Fragment';
import Wrapper from './shared/Wrapper.js';
import FragmentWrapper from './Fragment.js';
import { b, p, x } from 'code-red';
import { sanitize } from '../../../utils/names';
import add_to_set from '../../utils/add_to_set';
import get_slot_data from '../../utils/get_slot_data';
import { is_reserved_keyword } from '../../utils/reserved_keywords';
import is_dynamic from './shared/is_dynamic';
import { Identifier, ObjectExpression, Node } from 'estree';
import create_debugging_comment from './shared/create_debugging_comment';
import { sanitize } from '../../../utils/names.js';
import add_to_set from '../../utils/add_to_set.js';
import get_slot_data from '../../utils/get_slot_data.js';
import { is_reserved_keyword } from '../../utils/reserved_keywords.js';
import is_dynamic from './shared/is_dynamic.js';
import create_debugging_comment from './shared/create_debugging_comment.js';
/** @extends Wrapper<import('../../nodes/Slot.js').default> */
export default class SlotWrapper extends Wrapper {
node: Slot;
fragment: FragmentWrapper;
fallback: Block | null = null;
slot_block: Block;
var: Identifier = { type: 'Identifier', name: 'slot' };
dependencies: Set<string> = new Set(['$$scope']);
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: Slot,
strip_whitespace: boolean,
next_sibling: Wrapper
) {
/** @type {import('./Fragment.js').default} */
fragment;
/** @type {import('../Block.js').default | null} */
fallback = null;
/** @type {import('../Block.js').default} */
slot_block;
/** @type {import('estree').Identifier} */
var = { type: 'Identifier', name: 'slot' };
/** @type {Set<string>} */
dependencies = new Set(['$$scope']);
/**
* @param {import('../Renderer.js').default} renderer
* @param {import('../Block.js').default} block
* @param {import('./shared/Wrapper.js').default} parent
* @param {import('../../nodes/Slot.js').default} node
* @param {boolean} strip_whitespace
* @param {import('./shared/Wrapper.js').default} next_sibling
*/
constructor(renderer, block, parent, node, strip_whitespace, next_sibling) {
super(renderer, block, parent, node);
if (this.node.children.length) {
this.fallback = block.child({
comment: create_debugging_comment(this.node.children[0], this.renderer.component),
@ -39,7 +43,6 @@ export default class SlotWrapper extends Wrapper {
});
renderer.blocks.push(this.fallback);
}
this.fragment = new FragmentWrapper(
renderer,
this.fallback,
@ -48,31 +51,35 @@ export default class SlotWrapper extends Wrapper {
strip_whitespace,
next_sibling
);
this.node.values.forEach((attribute) => {
add_to_set(this.dependencies, attribute.dependencies);
});
block.add_dependencies(this.dependencies);
// we have to do this, just in case
block.add_intro();
block.add_outro();
}
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
/**
* @param {import('../Block.js').default} block
* @param {import('estree').Identifier} parent_node
* @param {import('estree').Identifier} parent_nodes
*/
render(block, parent_node, parent_nodes) {
const { renderer } = this;
const { slot_name } = this.node;
if (this.slot_block) {
block = this.slot_block;
}
let get_slot_changes_fn: Identifier | 'null';
let get_slot_spread_changes_fn: Identifier | undefined;
let get_slot_context_fn: Identifier | 'null';
/** @type {import('estree').Identifier | 'null'} */
let get_slot_changes_fn;
/** @type {import('estree').Identifier | undefined} */
let get_slot_spread_changes_fn;
/** @type {import('estree').Identifier | 'null'} */
let get_slot_context_fn;
if (this.node.values.size > 0) {
get_slot_changes_fn = renderer.component.get_unique_name(
`get_${sanitize(slot_name)}_slot_changes`
@ -80,11 +87,8 @@ export default class SlotWrapper extends Wrapper {
get_slot_context_fn = renderer.component.get_unique_name(
`get_${sanitize(slot_name)}_slot_context`
);
const changes = x`{}` as ObjectExpression;
const spread_dynamic_dependencies = new Set<string>();
const changes = /** @type {import('estree').ObjectExpression} */ (x`{}`);
const spread_dynamic_dependencies = new Set();
this.node.values.forEach((attribute) => {
if (attribute.type === 'Spread') {
add_to_set(
@ -95,18 +99,15 @@ export default class SlotWrapper extends Wrapper {
const dynamic_dependencies = Array.from(attribute.dependencies).filter((name) =>
this.is_dependency_dynamic(name)
);
if (dynamic_dependencies.length > 0) {
changes.properties.push(p`${attribute.name}: ${renderer.dirty(dynamic_dependencies)}`);
}
}
});
renderer.blocks.push(b`
const ${get_slot_changes_fn} = #dirty => ${changes};
const ${get_slot_context_fn} = #ctx => ${get_slot_data(this.node.values, block)};
`);
if (spread_dynamic_dependencies.size) {
get_slot_spread_changes_fn = renderer.component.get_unique_name(
`get_${sanitize(slot_name)}_slot_spread_changes`
@ -121,22 +122,23 @@ export default class SlotWrapper extends Wrapper {
get_slot_changes_fn = 'null';
get_slot_context_fn = 'null';
}
let has_fallback = !!this.fallback;
if (this.fallback) {
this.fragment.render(this.fallback, null, x`#nodes` as Identifier);
this.fragment.render(
this.fallback,
null,
/** @type {import('estree').Identifier} */ (x`#nodes`)
);
has_fallback = this.fallback.has_content();
if (!has_fallback) {
renderer.remove_block(this.fallback);
}
}
const slot = block.get_unique_name(`${sanitize(slot_name)}_slot`);
const slot_definition = block.get_unique_name(`${sanitize(slot_name)}_slot_template`);
const slot_or_fallback = has_fallback
? block.get_unique_name(`${sanitize(slot_name)}_slot_or_fallback`)
: slot;
block.chunks.init.push(b`
const ${slot_definition} = ${renderer.reference('#slots')}.${slot_name};
const ${slot} = @create_slot(${slot_definition}, #ctx, ${renderer.reference(
@ -144,36 +146,27 @@ export default class SlotWrapper extends Wrapper {
)}, ${get_slot_context_fn});
${has_fallback ? b`const ${slot_or_fallback} = ${slot} || ${this.fallback.name}(#ctx);` : null}
`);
block.chunks.create.push(b`if (${slot_or_fallback}) ${slot_or_fallback}.c();`);
if (renderer.options.hydratable) {
block.chunks.claim.push(b`if (${slot_or_fallback}) ${slot_or_fallback}.l(${parent_nodes});`);
}
block.chunks.mount.push(b`
if (${slot_or_fallback}) {
${slot_or_fallback}.m(${parent_node || '#target'}, ${parent_node ? 'null' : '#anchor'});
}
`);
block.chunks.intro.push(b`@transition_in(${slot_or_fallback}, #local);`);
block.chunks.outro.push(b`@transition_out(${slot_or_fallback}, #local);`);
const dynamic_dependencies = Array.from(this.dependencies).filter((name) =>
this.is_dependency_dynamic(name)
);
const fallback_dynamic_dependencies = has_fallback
? Array.from(this.fallback.dependencies).filter((name) => this.is_dependency_dynamic(name))
: [];
let condition = renderer.dirty(dynamic_dependencies);
if (block.has_outros) {
condition = x`!#current || ${condition}`;
}
// conditions to treat everything as dirty
const all_dirty_conditions = [
get_slot_spread_changes_fn ? x`${get_slot_spread_changes_fn}(#dirty)` : null,
@ -183,14 +176,14 @@ export default class SlotWrapper extends Wrapper {
? all_dirty_conditions.reduce((condition1, condition2) => x`${condition1} || ${condition2}`)
: null;
let slot_update: Node[];
/** @type {import('estree').Node[]} */
let slot_update;
if (all_dirty_condition) {
const dirty = x`${all_dirty_condition} ? @get_all_dirty_from_scope(${renderer.reference(
'$$scope'
)}) : @get_slot_changes(${slot_definition}, ${renderer.reference(
'$$scope'
)}, #dirty, ${get_slot_changes_fn})`;
slot_update = b`
if (${slot}.p && ${condition}) {
@update_slot_base(${slot}, ${slot_definition}, #ctx, ${renderer.reference(
@ -207,14 +200,12 @@ export default class SlotWrapper extends Wrapper {
}
`;
}
let fallback_condition = renderer.dirty(fallback_dynamic_dependencies);
let fallback_dirty = x`#dirty`;
if (block.has_outros) {
fallback_condition = x`!#current || ${fallback_condition}`;
fallback_dirty = x`!#current ? ${renderer.get_initial_dirty()} : ${fallback_dirty}`;
}
const fallback_update =
has_fallback &&
fallback_dynamic_dependencies.length > 0 &&
@ -223,7 +214,6 @@ export default class SlotWrapper extends Wrapper {
${slot_or_fallback}.p(#ctx, ${fallback_dirty});
}
`;
if (fallback_update) {
block.chunks.update.push(b`
if (${slot}) {
@ -239,11 +229,11 @@ export default class SlotWrapper extends Wrapper {
}
`);
}
block.chunks.destroy.push(b`if (${slot_or_fallback}) ${slot_or_fallback}.d(detaching);`);
}
is_dependency_dynamic(name: string) {
/** @param {string} name */
is_dependency_dynamic(name) {
if (name === '$$scope') return true;
if (this.node.scope.is_let(name)) return true;
if (is_reserved_keyword(name)) return true;

@ -1,57 +1,51 @@
import Wrapper from './shared/Wrapper';
import Renderer from '../Renderer';
import Block from '../Block';
import FragmentWrapper from './Fragment';
import create_debugging_comment from './shared/create_debugging_comment';
import { get_slot_definition } from './shared/get_slot_definition';
import Wrapper from './shared/Wrapper.js';
import FragmentWrapper from './Fragment.js';
import create_debugging_comment from './shared/create_debugging_comment.js';
import { get_slot_definition } from './shared/get_slot_definition.js';
import { b, x } from 'code-red';
import { sanitize } from '../../../utils/names';
import { Identifier } from 'estree';
import InlineComponentWrapper from './InlineComponent';
import { sanitize } from '../../../utils/names.js';
import { extract_names } from 'periscopic';
import SlotTemplate from '../../nodes/SlotTemplate';
import { add_const_tags, add_const_tags_context } from './shared/add_const_tags';
import { add_const_tags, add_const_tags_context } from './shared/add_const_tags.js';
/** @extends Wrapper<import('../../nodes/SlotTemplate.js').default> */
export default class SlotTemplateWrapper extends Wrapper {
node: SlotTemplate;
fragment: FragmentWrapper;
block: Block;
parent: InlineComponentWrapper;
/** @type {import('./Fragment.js').default} */
fragment;
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: SlotTemplate,
strip_whitespace: boolean,
next_sibling: Wrapper
) {
super(renderer, block, parent, node);
/** @type {import('../Block.js').default} */
block;
/**
* @param {import('../Renderer.js').default} renderer
* @param {import('../Block.js').default} block
* @param {import('./shared/Wrapper.js').default} parent
* @param {import('../../nodes/SlotTemplate.js').default} node
* @param {boolean} strip_whitespace
* @param {import('./shared/Wrapper.js').default} next_sibling
*/
constructor(renderer, block, parent, node, strip_whitespace, next_sibling) {
super(renderer, block, parent, node);
const { scope, lets, const_tags, slot_template_name } = this.node;
lets.forEach((l) => {
extract_names(l.value || l.name).forEach((name) => {
renderer.add_to_context(name, true);
});
});
add_const_tags_context(renderer, const_tags);
this.block = block.child({
comment: create_debugging_comment(this.node, this.renderer.component),
name: this.renderer.component.get_unique_name(`create_${sanitize(slot_template_name)}_slot`),
type: 'slot'
});
this.renderer.blocks.push(this.block);
const seen = new Set(lets.map((l) => l.name.name));
this.parent.node.lets.forEach((l) => {
if (!seen.has(l.name.name)) lets.push(l);
});
this.parent.set_slot(slot_template_name, get_slot_definition(this.block, scope, lets));
/** @type {import('./InlineComponent/index.js').default} */ (this.parent).set_slot(
slot_template_name,
get_slot_definition(this.block, scope, lets)
);
this.fragment = new FragmentWrapper(
renderer,
this.block,
@ -60,13 +54,10 @@ export default class SlotTemplateWrapper extends Wrapper {
strip_whitespace,
next_sibling
);
this.block.parent.add_dependencies(this.block.dependencies);
}
render() {
this.fragment.render(this.block, null, x`#nodes` as Identifier);
this.fragment.render(this.block, null, /** @type {import('estree').Identifier} */ (x`#nodes`));
if (this.node.const_tags.length > 0) {
this.render_get_context();
}

@ -1,29 +1,36 @@
import Renderer from '../Renderer';
import Block from '../Block';
import Text from '../../nodes/Text';
import Wrapper from './shared/Wrapper';
import Wrapper from './shared/Wrapper.js';
import { x } from 'code-red';
import { Identifier } from 'estree';
/** @extends Wrapper<import('../../nodes/Text.js').default> */
export default class TextWrapper extends Wrapper {
node: Text;
_data: string;
skip: boolean;
var: Identifier;
/** @type {string} */
_data;
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: Text, data: string) {
super(renderer, block, parent, node);
/** @type {boolean} */
skip;
/** @type {import('estree').Identifier} */
var;
/**
* @param {import('../Renderer.js').default} renderer
* @param {import('../Block.js').default} block
* @param {import('./shared/Wrapper.js').default} parent
* @param {import('../../nodes/Text.js').default} node
* @param {string} data
*/
constructor(renderer, block, parent, node, data) {
super(renderer, block, parent, node);
this.skip = this.node.should_skip();
this._data = data;
this.var = (this.skip ? null : x`t`) as unknown as Identifier;
this.var = /** @type {unknown} */ /** @type {import('estree').Identifier} */ (
this.skip ? null : x`t`
);
}
use_space() {
return this.node.use_space();
}
set data(value: string) {
set data(value) {
// when updating `this.data` during optimisation
// propagate the changes over to the underlying node
// so that the node.use_space reflects on the latest `data` value
@ -33,10 +40,14 @@ export default class TextWrapper extends Wrapper {
return this._data;
}
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
/**
* @param {import('../Block.js').default} block
* @param {import('estree').Identifier} parent_node
* @param {import('estree').Identifier} parent_nodes
*/
render(block, parent_node, parent_nodes) {
if (this.skip) return;
const use_space = this.use_space();
const string_literal = {
type: 'Literal',
value: this.data,
@ -45,7 +56,6 @@ export default class TextWrapper extends Wrapper {
end: this.renderer.locate(this.node.end)
}
};
block.add_element(
this.var,
use_space ? x`@space()` : x`@text(${string_literal})`,
@ -53,7 +63,7 @@ export default class TextWrapper extends Wrapper {
(use_space
? x`@claim_space(${parent_nodes})`
: x`@claim_text(${parent_nodes}, ${string_literal})`),
parent_node as Identifier
/** @type {import('estree').Identifier} */ (parent_node)
);
}
}

@ -1,36 +1,34 @@
import { b, x } from 'code-red';
import Wrapper from './shared/Wrapper';
import Renderer from '../Renderer';
import Block from '../Block';
import Title from '../../nodes/Title';
import { string_literal } from '../../utils/stringify';
import add_to_set from '../../utils/add_to_set';
import Text from '../../nodes/Text';
import { Identifier } from 'estree';
import MustacheTag from '../../nodes/MustacheTag';
import Wrapper from './shared/Wrapper.js';
import { string_literal } from '../../utils/stringify.js';
import add_to_set from '../../utils/add_to_set.js';
/** @extends Wrapper<import('../../nodes/Title.js').default> */
export default class TitleWrapper extends Wrapper {
node: Title;
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: Title,
_strip_whitespace: boolean,
_next_sibling: Wrapper
) {
/**
* @param {import('../Renderer.js').default} renderer
* @param {import('../Block.js').default} block
* @param {import('./shared/Wrapper.js').default} parent
* @param {import('../../nodes/Title.js').default} node
* @param {boolean} _strip_whitespace
* @param {import('./shared/Wrapper.js').default} _next_sibling
*/
constructor(renderer, block, parent, node, _strip_whitespace, _next_sibling) {
super(renderer, block, parent, node);
}
render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) {
/**
* @param {import('../Block.js').default} block
* @param {import('estree').Identifier} _parent_node
* @param {import('estree').Identifier} _parent_nodes
*/
render(block, _parent_node, _parent_nodes) {
const is_dynamic = !!this.node.children.find((node) => node.type !== 'Text');
if (is_dynamic) {
let value;
const all_dependencies: Set<string> = new Set();
/** @type {Set<string>} */
const all_dependencies = new Set();
// TODO some of this code is repeated in Tag.ts — would be good to
// DRY it out if that's possible without introducing crazy indirection
if (this.node.children.length === 1) {
@ -44,43 +42,34 @@ export default class TitleWrapper extends Wrapper {
value = this.node.children
.map((chunk) => {
if (chunk.type === 'Text') return string_literal(chunk.data);
(chunk as MustacheTag).expression.dependencies.forEach((d) => {
/** @type {import('../../nodes/MustacheTag.js').default} */ (
chunk
).expression.dependencies.forEach((d) => {
all_dependencies.add(d);
});
return (chunk as MustacheTag).expression.manipulate(block);
return /** @type {import('../../nodes/MustacheTag.js').default} */ (
chunk
).expression.manipulate(block);
})
.reduce((lhs, rhs) => x`${lhs} + ${rhs}`);
if (this.node.children[0].type !== 'Text') {
value = x`"" + ${value}`;
}
}
const last = this.node.should_cache && block.get_unique_name('title_value');
if (this.node.should_cache) block.add_variable(last);
const init = this.node.should_cache ? x`${last} = ${value}` : value;
block.chunks.init.push(b`@_document.title = ${init};`);
const updater = b`@_document.title = ${this.node.should_cache ? last : value};`;
if (all_dependencies.size) {
const dependencies = Array.from(all_dependencies);
let condition = block.renderer.dirty(dependencies);
if (block.has_outros) {
condition = x`!#current || ${condition}`;
}
if (this.node.should_cache) {
condition = x`${condition} && (${last} !== (${last} = ${value}))`;
}
block.chunks.update.push(b`
if (${condition}) {
${updater}
@ -89,9 +78,10 @@ export default class TitleWrapper extends Wrapper {
} else {
const value =
this.node.children.length > 0
? string_literal((this.node.children[0] as Text).data)
? string_literal(
/** @type {import('../../nodes/Text.js').default} */ (this.node.children[0]).data
)
: x`""`;
block.chunks.hydrate.push(b`@_document.title = ${value};`);
}
}

@ -1,13 +1,8 @@
import Renderer from '../Renderer';
import Block from '../Block';
import Wrapper from './shared/Wrapper';
import Wrapper from './shared/Wrapper.js';
import { b, x } from 'code-red';
import add_event_handlers from './shared/add_event_handlers';
import Window from '../../nodes/Window';
import add_actions from './shared/add_actions';
import { Identifier } from 'estree';
import { TemplateNode } from '../../../interfaces';
import EventHandler from './Element/EventHandler';
import add_event_handlers from './shared/add_event_handlers.js';
import add_actions from './shared/add_actions.js';
import EventHandler from './Element/EventHandler.js';
const associated_events = {
innerWidth: 'resize',
@ -15,16 +10,13 @@ const associated_events = {
outerWidth: 'resize',
outerHeight: 'resize',
devicePixelRatio: 'resize',
scrollX: 'scroll',
scrollY: 'scroll'
};
const properties = {
scrollX: 'pageXOffset',
scrollY: 'pageYOffset'
};
const readonly = new Set([
'innerWidth',
'innerHeight',
@ -34,74 +26,76 @@ const readonly = new Set([
'online'
]);
/** @extends Wrapper<import('../../nodes/Window.js').default> */
export default class WindowWrapper extends Wrapper {
node: Window;
handlers: EventHandler[];
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: TemplateNode) {
/** @type {import('./Element/EventHandler.js').default[]} */
handlers;
/**
* @param {import('../Renderer.js').default} renderer
* @param {import('../Block.js').default} block
* @param {import('./shared/Wrapper.js').default} parent
* @param {import('../../nodes/Window.js').default} node
*/
constructor(renderer, block, parent, node) {
super(renderer, block, parent, node);
this.handlers = this.node.handlers.map((handler) => new EventHandler(handler, this));
}
render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) {
/**
* @param {import('../Block.js').default} block
* @param {import('estree').Identifier} _parent_node
* @param {import('estree').Identifier} _parent_nodes
*/
render(block, _parent_node, _parent_nodes) {
const { renderer } = this;
const { component } = renderer;
const events: Record<string, Array<{ name: string; value: string }>> = {};
const bindings: Record<string, string> = {};
/** @type {Record<string, Array<{ name: string; value: string }>>} */
const events = {};
/** @type {Record<string, string>} */
const bindings = {};
add_actions(block, '@_window', this.node.actions);
add_event_handlers(block, '@_window', this.handlers);
this.node.bindings.forEach((binding) => {
// TODO: what if it's a MemberExpression?
const binding_name = (binding.expression.node as Identifier).name;
const binding_name = /** @type {import('estree').Identifier} */ (binding.expression.node)
.name;
// in dev mode, throw if read-only values are written to
if (readonly.has(binding.name)) {
renderer.readonly.add(binding_name);
}
bindings[binding.name] = binding_name;
// bind:online is a special case, we need to listen for two separate events
if (binding.name === 'online') return;
const associated_event = associated_events[binding.name];
const property = properties[binding.name] || binding.name;
if (!events[associated_event]) events[associated_event] = [];
events[associated_event].push({
name: binding_name,
value: property
});
});
const scrolling = block.get_unique_name('scrolling');
const clear_scrolling = block.get_unique_name('clear_scrolling');
const scrolling_timeout = block.get_unique_name('scrolling_timeout');
Object.keys(events).forEach((event) => {
const id = block.get_unique_name(`onwindow${event}`);
const props = events[event];
renderer.add_to_context(id.name);
const fn = renderer.reference(id.name);
if (event === 'scroll') {
// TODO other bidirectional bindings...
block.add_variable(scrolling, x`false`);
block.add_variable(clear_scrolling, x`() => { ${scrolling} = false }`);
block.add_variable(scrolling_timeout);
const condition =
bindings.scrollX && bindings.scrollY
? x`"${bindings.scrollX}" in this._state || "${bindings.scrollY}" in this._state`
: x`"${bindings.scrollX || bindings.scrollY}" in this._state`;
const scrollX = bindings.scrollX && x`this._state.${bindings.scrollX}`;
const scrollY = bindings.scrollY && x`this._state.${bindings.scrollY}`;
renderer.meta_bindings.push(b`
if (${condition}) {
@_scrollTo(${scrollX || '@_window.pageXOffset'}, ${scrollY || '@_window.pageYOffset'});
@ -109,7 +103,6 @@ export default class WindowWrapper extends Wrapper {
${scrollX && `${scrollX} = @_window.pageXOffset;`}
${scrollY && `${scrollY} = @_window.pageYOffset;`}
`);
block.event_listeners.push(x`
@listen(@_window, "${event}", () => {
${scrolling} = true;
@ -122,36 +115,29 @@ export default class WindowWrapper extends Wrapper {
props.forEach((prop) => {
renderer.meta_bindings.push(b`this._state.${prop.name} = @_window.${prop.value};`);
});
block.event_listeners.push(x`
@listen(@_window, "${event}", ${fn})
`);
}
component.partly_hoisted.push(b`
function ${id}() {
${props.map((prop) => renderer.invalidate(prop.name, x`${prop.name} = @_window.${prop.value}`))}
}
`);
block.chunks.init.push(b`
@add_render_callback(${fn});
`);
component.has_reactive_assignments = true;
});
// special case... might need to abstract this out if we add more special cases
if (bindings.scrollX || bindings.scrollY) {
const condition = renderer.dirty([bindings.scrollX, bindings.scrollY].filter(Boolean));
const scrollX = bindings.scrollX
? renderer.reference(bindings.scrollX)
: x`@_window.pageXOffset`;
const scrollY = bindings.scrollY
? renderer.reference(bindings.scrollY)
: x`@_window.pageYOffset`;
block.chunks.update.push(b`
if (${condition} && !${scrolling}) {
${scrolling} = true;
@ -161,30 +147,24 @@ export default class WindowWrapper extends Wrapper {
}
`);
}
// another special case. (I'm starting to think these are all special cases.)
if (bindings.online) {
const id = block.get_unique_name('onlinestatuschanged');
const name = bindings.online;
renderer.add_to_context(id.name);
const reference = renderer.reference(id.name);
component.partly_hoisted.push(b`
function ${id}() {
${renderer.invalidate(name, x`${name} = @_navigator.onLine`)}
}
`);
block.chunks.init.push(b`
@add_render_callback(${reference});
`);
block.event_listeners.push(
x`@listen(@_window, "online", ${reference})`,
x`@listen(@_window, "offline", ${reference})`
);
component.has_reactive_assignments = true;
}
}

@ -1,52 +1,46 @@
import { b, x } from 'code-red';
import Wrapper from './Wrapper';
import Renderer from '../../Renderer';
import Block from '../../Block';
import MustacheTag from '../../../nodes/MustacheTag';
import RawMustacheTag from '../../../nodes/RawMustacheTag';
import { Node } from 'estree';
import Wrapper from './Wrapper.js';
/**
* @template {import('../../../nodes/MustacheTag.js').default | import('../../../nodes/RawMustacheTag.js').default} NodeType
* @extends Wrapper<NodeType>
*/
export default class Tag extends Wrapper {
node: MustacheTag | RawMustacheTag;
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: MustacheTag | RawMustacheTag
) {
/**
* @param {import('../../Renderer.js').default} renderer
* @param {import('../../Block.js').default} block
* @param {import('./Wrapper.js').default} parent
* @param {NodeType} node
*/
constructor(renderer, block, parent, node) {
super(renderer, block, parent, node);
block.add_dependencies(node.expression.dependencies);
}
rename_this_method(block: Block, update: (value: Node) => Node | Node[]) {
/**
* @param {import('../../Block.js').default} block
* @param {(value: import('estree').Node) => import('estree').Node | import('estree').Node[]} update
*/
rename_this_method(block, update) {
const dependencies = this.node.expression.dynamic_dependencies();
let snippet = this.node.expression.manipulate(block);
const value = this.node.should_cache && block.get_unique_name(`${this.var.name}_value`);
const content = this.node.should_cache ? value : snippet;
snippet = x`${snippet} + ""`;
if (this.node.should_cache) block.add_variable(value, snippet); // TODO may need to coerce snippet to string
if (dependencies.length > 0) {
let condition = block.renderer.dirty(dependencies);
if (block.has_outros) {
condition = x`!#current || ${condition}`;
}
const update_cached_value = x`${value} !== (${value} = ${snippet})`;
if (this.node.should_cache) {
condition = x`${condition} && ${update_cached_value}`;
}
block.chunks.update.push(b`if (${condition}) ${update(content as Node)}`);
block.chunks.update.push(
b`if (${condition}) ${update(/** @type {import('estree').Node} */ (content))}`
);
}
return { init: content };
}
}

@ -1,22 +1,35 @@
import Renderer from '../../Renderer';
import Block from '../../Block';
import { x } from 'code-red';
import { TemplateNode } from '../../../../interfaces';
import { Identifier } from 'estree';
/**
* @template {import('../../../../interfaces.js').TemplateNode} [NodeType=import('../../../../interfaces.js').TemplateNode]
*/
export default class Wrapper {
renderer: Renderer;
parent: Wrapper;
node: TemplateNode;
/** @type {import('../../Renderer.js').default} */
renderer;
prev: Wrapper | null;
next: Wrapper | null;
/** @type {Wrapper} */
parent;
var: Identifier;
/** @type {NodeType} */
node;
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: TemplateNode) {
this.node = node;
/** @type {Wrapper | null} */
prev;
/** @type {Wrapper | null} */
next;
/** @type {import('estree').Identifier} */
var;
/**
* @param {import('../../Renderer.js').default} renderer
* @param {import('../../Block.js').default} block
* @param {Wrapper} parent
* @param {NodeType} node
*/
constructor(renderer, block, parent, node) {
this.node = node;
// make these non-enumerable so that they can be logged sensibly
// (TODO in dev only?)
Object.defineProperties(this, {
@ -27,11 +40,15 @@ export default class Wrapper {
value: parent
}
});
block.wrappers.push(this);
}
get_or_create_anchor(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
/**
* @param {import('../../Block.js').default} block
* @param {import('estree').Identifier} parent_node
* @param {import('estree').Identifier} parent_nodes
*/
get_or_create_anchor(block, parent_node, parent_nodes) {
// TODO use this in EachBlock and IfBlock — tricky because
// children need to be created first
const needs_anchor = this.next
@ -40,37 +57,39 @@ export default class Wrapper {
const anchor = needs_anchor
? block.get_unique_name(`${this.var.name}_anchor`)
: (this.next && this.next.var) || { type: 'Identifier', name: 'null' };
if (needs_anchor) {
block.add_element(
anchor,
x`@empty()`,
parent_nodes && x`@empty()`,
parent_node as Identifier
/** @type {import('estree').Identifier} */ (parent_node)
);
}
return anchor;
}
get_update_mount_node(anchor: Identifier): Identifier {
return (
/**
* @param {import('estree').Identifier} anchor
* @returns {import('estree').Identifier}
*/
get_update_mount_node(anchor) {
return /** @type {import('estree').Identifier} */ (
this.parent && this.parent.is_dom_node() ? this.parent.var : x`${anchor}.parentNode`
) as Identifier;
);
}
is_dom_node() {
return (
this.node.type === 'Element' || this.node.type === 'Text' || this.node.type === 'MustacheTag'
);
}
render(
_block: Block,
_parent_node: Identifier,
_parent_nodes: Identifier,
_data: Record<string, any> = undefined
) {
/**
* @param {import('../../Block.js').default} _block
* @param {import('estree').Identifier} _parent_node
* @param {import('estree').Identifier} _parent_nodes
* @param {Record<string, any>} _data
*/
render(_block, _parent_node, _parent_nodes, _data = undefined) {
throw Error('Wrapper class is not renderable');
}
}

@ -1,37 +1,42 @@
import { b, x } from 'code-red';
import Block from '../../Block';
import Action from '../../../nodes/Action';
import { Expression, Node } from 'estree';
import is_contextual from '../../../nodes/shared/is_contextual';
export default function add_actions(block: Block, target: string | Expression, actions: Action[]) {
import is_contextual from '../../../nodes/shared/is_contextual.js';
/**
* @param {import('../../Block.js').default} block
* @param {string | import('estree').Expression} target
* @param {import('../../../nodes/Action.js').default[]} actions
*/
export default function add_actions(block, target, actions) {
actions.forEach((action) => add_action(block, target, action));
}
const regex_invalid_variable_identifier_characters = /[^a-zA-Z0-9_$]/g;
export function add_action(block: Block, target: string | Expression, action: Action) {
/**
* @param {import('../../Block.js').default} block
* @param {string | import('estree').Expression} target
* @param {import('../../../nodes/Action.js').default} action
*/
export function add_action(block, target, action) {
const { expression, template_scope } = action;
let snippet: Node | undefined;
let dependencies: string[] | undefined;
/** @type {import('estree').Node | undefined} */
let snippet;
/** @type {string[] | undefined} */
let dependencies;
if (expression) {
snippet = expression.manipulate(block);
dependencies = expression.dynamic_dependencies();
}
const id = block.get_unique_name(
`${action.name.replace(regex_invalid_variable_identifier_characters, '_')}_action`
);
block.add_variable(id);
const [obj, ...properties] = action.name.split('.');
const fn = is_contextual(action.component, template_scope, obj)
? block.renderer.reference(obj)
: obj;
if (properties.length) {
const member_expression = properties.reduce((lhs, rhs) => x`${lhs}.${rhs}`, fn);
block.event_listeners.push(
@ -42,14 +47,11 @@ export function add_action(block: Block, target: string | Expression, action: Ac
x`@action_destroyer(${id} = ${fn}.call(null, ${target}, ${snippet}))`
);
}
if (dependencies && dependencies.length > 0) {
let condition = x`${id} && @is_function(${id}.update)`;
if (dependencies.length > 0) {
condition = x`${condition} && ${block.renderer.dirty(dependencies)}`;
}
block.chunks.update.push(b`if (${condition}) ${id}.update.call(null, ${snippet});`);
}
}

@ -1,19 +1,22 @@
import ConstTag from '../../../nodes/ConstTag';
import Block from '../../Block';
import { b, Node, x } from 'code-red';
import Renderer from '../../Renderer';
import Expression from '../../../nodes/shared/Expression';
import { b, x } from 'code-red';
import Expression from '../../../nodes/shared/Expression.js';
export function add_const_tags(block: Block, const_tags: ConstTag[], ctx: string) {
/**
* @param {import('../../Block.js').default} block
* @param {import('../../../nodes/ConstTag.js').default[]} const_tags
* @param {string} ctx
*/
export function add_const_tags(block, const_tags, ctx) {
const const_tags_props = [];
const_tags.forEach((const_tag, i) => {
const name = `#constants_${i}`;
const_tags_props.push(b`const ${name} = ${const_tag.expression.manipulate(block, ctx)}`);
const to_ctx = (name: string) =>
/** @param {string} name */
const to_ctx = (name) =>
block.renderer.context_lookup.has(name)
? x`${ctx}[${block.renderer.context_lookup.get(name).index}]`
: ({ type: 'Identifier', name } as Node);
: /** @type {import('code-red').Node} */ ({ type: 'Identifier', name });
const_tag.contexts.forEach((context) => {
if (context.type === 'DestructuredVariable') {
const_tags_props.push(
@ -37,7 +40,11 @@ export function add_const_tags(block: Block, const_tags: ConstTag[], ctx: string
return const_tags_props;
}
export function add_const_tags_context(renderer: Renderer, const_tags: ConstTag[]) {
/**
* @param {import('../../Renderer.js').default} renderer
* @param {import('../../../nodes/ConstTag.js').default[]} const_tags
*/
export function add_const_tags_context(renderer, const_tags) {
const_tags.forEach((const_tag) => {
const_tag.contexts.forEach((context) => {
if (context.type !== 'DestructuredVariable') return;

@ -1,19 +1,17 @@
import Block from '../../Block';
import EventHandler from '../Element/EventHandler';
import { Expression } from 'estree';
export default function add_event_handlers(
block: Block,
target: string | Expression,
handlers: EventHandler[]
) {
/**
* @param {import('../../Block.js').default} block
* @param {string | import('estree').Expression} target
* @param {import('../Element/EventHandler.js').default[]} handlers
*/
export default function add_event_handlers(block, target, handlers) {
handlers.forEach((handler) => add_event_handler(block, target, handler));
}
export function add_event_handler(
block: Block,
target: string | Expression,
handler: EventHandler
) {
/**
* @param {import('../../Block.js').default} block
* @param {string | import('estree').Expression} target
* @param {import('../Element/EventHandler.js').default} handler
*/
export function add_event_handler(block, target, handler) {
handler.render(block, target);
}

@ -1,24 +1,18 @@
import { b, x } from 'code-red';
import Component from '../../../Component';
import Block from '../../Block';
import BindingWrapper from '../Element/Binding';
import { Identifier } from 'estree';
import { compare_node } from '../../../utils/compare_node';
import { compare_node } from '../../../utils/compare_node.js';
export default function bind_this(
component: Component,
block: Block,
binding: BindingWrapper,
variable: Identifier
) {
/**
* @param {import('../../../Component.js').default} component
* @param {import('../../Block.js').default} block
* @param {import('../Element/Binding.js').default} binding
* @param {import('estree').Identifier} variable
*/
export default function bind_this(component, block, binding, variable) {
const fn = component.get_unique_name(`${variable.name}_binding`);
block.renderer.add_to_context(fn.name);
const callee = block.renderer.reference(fn.name);
const { contextual_dependencies, mutation } = binding.handler;
const dependencies = binding.get_update_dependencies();
const body = b`
${mutation}
${Array.from(dependencies)
@ -26,9 +20,9 @@ export default function bind_this(
.filter((dep) => !contextual_dependencies.has(dep))
.map((dep) => b`${block.renderer.invalidate(dep)};`)}
`;
if (contextual_dependencies.size) {
const params: Identifier[] = Array.from(contextual_dependencies).map((name) => ({
/** @type {import('estree').Identifier[]} */
const params = Array.from(contextual_dependencies).map((name) => ({
type: 'Identifier',
name
}));
@ -39,7 +33,6 @@ export default function bind_this(
});
}
`);
const alias_map = new Map();
const args = [];
for (let id of params) {
@ -61,21 +54,17 @@ export default function bind_this(
block.add_variable(id, value);
}
}
const assign = block.get_unique_name(`assign_${variable.name}`);
const unassign = block.get_unique_name(`unassign_${variable.name}`);
block.chunks.init.push(b`
const ${assign} = () => ${callee}(${variable}, ${args});
const ${unassign} = () => ${callee}(null, ${args});
`);
const condition = Array.from(args)
.map(
(name) => x`${name} !== ${block.renderer.reference(alias_map.get(name.name) || name.name)}`
)
.reduce((lhs, rhs) => x`${lhs} || ${rhs}`);
// we push unassign and unshift assign so that references are
// nulled out before they're created, to avoid glitches
// with shifting indices
@ -85,11 +74,9 @@ export default function bind_this(
${args.map((a) => b`${a} = ${block.renderer.reference(alias_map.get(a.name) || a.name)}`)};
${assign}();
}`);
block.chunks.destroy.push(b`${unassign}();`);
return b`${assign}();`;
}
component.partly_hoisted.push(b`
function ${fn}($$value) {
@binding_callbacks[$$value ? 'unshift' : 'push'](() => {
@ -97,7 +84,6 @@ export default function bind_this(
});
}
`);
block.chunks.destroy.push(b`${callee}(null);`);
return b`${callee}(${variable});`;
}

@ -1,18 +1,19 @@
import Component from '../../../Component';
import { INode } from '../../../nodes/interfaces';
import { regex_whitespace_characters } from '../../../../utils/patterns';
import { regex_whitespace_characters } from '../../../../utils/patterns.js';
export default function create_debugging_comment(node: INode, component: Component) {
/**
* @param {import('../../../nodes/interfaces.js').INode} node
* @param {import('../../../Component.js').default} component
*/
export default function create_debugging_comment(node, component) {
const { locate, source } = component;
let c = node.start;
if (node.type === 'ElseBlock') {
while (source[c - 1] !== '{') c -= 1;
while (source[c - 1] === '{') c -= 1;
}
let d: number;
/** @type {number} */
let d;
if (node.type === 'InlineComponent' || node.type === 'Element' || node.type === 'SlotTemplate') {
if (node.children.length) {
d = node.children[0].start;
@ -30,9 +31,7 @@ export default function create_debugging_comment(node: INode, component: Compone
while (source[d] !== '}' && d <= source.length) d += 1;
while (source[d] === '}') d += 1;
}
const start = locate(c);
const loc = `(${start.line}:${start.column})`;
return `${loc} ${source.slice(c, d)}`.replace(regex_whitespace_characters, ' ');
}

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

Loading…
Cancel
Save