diff --git a/.eslintignore b/.eslintignore index d123c10530..67a89ff12c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,7 +4,7 @@ _output test/*/samples/*/output.js # automatically generated -internal_exports.ts +internal_exports.js # output files animate/*.js diff --git a/.gitignore b/.gitignore index e70d995994..2231de9065 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.prettierignore b/.prettierignore index b21b38dc35..15f58330d5 100644 --- a/.prettierignore +++ b/.prettierignore @@ -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* diff --git a/generate-types.mjs b/generate-types.mjs new file mode 100644 index 0000000000..707bf746fd --- /dev/null +++ b/generate-types.mjs @@ -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): 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): void;\n}', '$set(props: Partial): 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`); diff --git a/package.json b/package.json index 45123e4e7e..f4286f07d4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 475c9adda5..9af0b02a60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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'} diff --git a/rollup.config.mjs b/rollup.config.mjs index 54b988b51b..d7f59264dc 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -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, diff --git a/scripts/globals-extractor.mjs b/scripts/globals-extractor.mjs index 47dbc240ff..b9706dfb92 100644 --- a/scripts/globals-extractor.mjs +++ b/scripts/globals-extractor.mjs @@ -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. diff --git a/src/compiler/Stats.ts b/src/compiler/Stats.ts index e22c18e0f0..637a3fcc69 100644 --- a/src/compiler/Stats.ts +++ b/src/compiler/Stats.ts @@ -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 }; diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index 14f68a08f0..5a4b063651 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -1,160 +1,188 @@ -import type { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping'; import { walk } from 'estree-walker'; import { getLocator } from 'locate-character'; -import Stats from '../Stats'; -import { reserved, is_valid } from '../utils/names'; -import globals from '../utils/globals'; -import { namespaces, valid_namespaces } from '../utils/namespaces'; -import create_module from './create_module'; -import { create_scopes, extract_names, Scope, extract_identifiers } from './utils/scope'; -import Stylesheet from './css/Stylesheet'; -import Fragment from './nodes/Fragment'; -import internal_exports from './internal_exports'; -import { Ast, CompileOptions, Var, Warning, CssResult, Attribute } from '../interfaces'; -import error from '../utils/error'; -import get_code_frame from '../utils/get_code_frame'; -import flatten_reference from './utils/flatten_reference'; -import is_used_as_reference from './utils/is_used_as_reference'; -import is_reference, { NodeWithPropertyDefinition } from 'is-reference'; -import TemplateScope from './nodes/shared/TemplateScope'; -import fuzzymatch from '../utils/fuzzymatch'; -import get_object from './utils/get_object'; -import Slot from './nodes/Slot'; -import { - Node, - ImportDeclaration, - ExportNamedDeclaration, - Identifier, - ExpressionStatement, - AssignmentExpression, - Literal, - Property, - RestElement, - ExportDefaultDeclaration, - ExportAllDeclaration, - FunctionDeclaration, - FunctionExpression, - VariableDeclarator, - ObjectExpression, - Pattern, - Expression -} from 'estree'; -import add_to_set from './utils/add_to_set'; -import check_graph_for_cycles from './utils/check_graph_for_cycles'; +import { reserved, is_valid } from '../utils/names.js'; +import globals from '../utils/globals.js'; +import { namespaces, valid_namespaces } from '../utils/namespaces.js'; +import create_module from './create_module.js'; +import { create_scopes, extract_names, extract_identifiers } from './utils/scope.js'; +import Stylesheet from './css/Stylesheet.js'; +import Fragment from './nodes/Fragment.js'; +import internal_exports from './internal_exports.js'; +import error from '../utils/error.js'; +import get_code_frame from '../utils/get_code_frame.js'; +import flatten_reference from './utils/flatten_reference.js'; +import is_used_as_reference from './utils/is_used_as_reference.js'; +import is_reference from 'is-reference'; +import fuzzymatch from '../utils/fuzzymatch.js'; +import get_object from './utils/get_object.js'; +import add_to_set from './utils/add_to_set.js'; +import check_graph_for_cycles from './utils/check_graph_for_cycles.js'; import { print, b } from 'code-red'; -import { is_reserved_keyword } from './utils/reserved_keywords'; -import { apply_preprocessor_sourcemap } from '../utils/mapped_code'; -import Element from './nodes/Element'; -import { clone } from '../utils/clone'; -import compiler_warnings from './compiler_warnings'; -import compiler_errors from './compiler_errors'; +import { is_reserved_keyword } from './utils/reserved_keywords.js'; +import { apply_preprocessor_sourcemap } from '../utils/mapped_code.js'; +import { clone } from '../utils/clone.js'; +import compiler_warnings from './compiler_warnings.js'; +import compiler_errors from './compiler_errors.js'; import { extract_ignores_above_position, extract_svelte_ignore_from_comments -} from '../utils/extract_svelte_ignore'; -import check_enable_sourcemap from './utils/check_enable_sourcemap'; -import Tag from './nodes/shared/Tag'; - -interface ComponentOptions { - namespace?: string; - immutable?: boolean; - accessors?: boolean; - preserveWhitespace?: boolean; - customElement?: { - tag: string | null; - shadow?: 'open' | 'none'; - props?: Record< - string, - { - attribute?: string; - reflect?: boolean; - type?: 'String' | 'Boolean' | 'Number' | 'Array' | 'Object'; - } - >; - }; -} +} from '../utils/extract_svelte_ignore.js'; +import check_enable_sourcemap from './utils/check_enable_sourcemap.js'; const regex_leading_directory_separator = /^[/\\]/; const regex_starts_with_term_export = /^Export/; const regex_contains_term_function = /Function/; export default class Component { - stats: Stats; - warnings: Warning[]; - ignores: Set; - ignore_stack: Array> = []; - - ast: Ast; - original_ast: Ast; - source: string; - name: Identifier; - compile_options: CompileOptions; - fragment: Fragment; - module_scope: Scope; - instance_scope: Scope; - instance_scope_map: WeakMap; - - component_options: ComponentOptions; - namespace: string; - tag: string; - accessors: boolean; - - vars: Var[] = []; - var_lookup: Map = new Map(); - - imports: ImportDeclaration[] = []; - exports_from: ExportNamedDeclaration[] = []; - instance_exports_from: ExportNamedDeclaration[] = []; - - hoistable_nodes: Set = new Set(); - node_for_declaration: Map = new Map(); - partly_hoisted: Array = []; - fully_hoisted: Array = []; - reactive_declarations: Array<{ - assignees: Set; - dependencies: Set; - node: Node; - declaration: Node; - }> = []; - reactive_declaration_nodes: Set = new Set(); + /** @type {import('../Stats.js').default} */ + stats; + + /** @type {import('../interfaces.js').Warning[]} */ + warnings; + + /** @type {Set} */ + ignores; + + /** @type {Array>} */ + ignore_stack = []; + + /** @type {import('../interfaces.js').Ast} */ + ast; + + /** @type {import('../interfaces.js').Ast} */ + original_ast; + + /** @type {string} */ + source; + + /** @type {import('estree').Identifier} */ + name; + + /** @type {import('../interfaces.js').CompileOptions} */ + compile_options; + + /** @type {import('./nodes/Fragment.js').default} */ + fragment; + + /** @type {import('./utils/scope.js').Scope} */ + module_scope; + + /** @type {import('./utils/scope.js').Scope} */ + instance_scope; + + /** @type {WeakMap} */ + instance_scope_map; + + /** @type {ComponentOptions} */ + component_options; + + /** @type {string} */ + namespace; + + /** @type {string} */ + tag; + + /** @type {boolean} */ + accessors; + + /** @type {import('../interfaces.js').Var[]} */ + vars = []; + + /** @type {Map} */ + var_lookup = new Map(); + + /** @type {import('estree').ImportDeclaration[]} */ + imports = []; + + /** @type {import('estree').ExportNamedDeclaration[]} */ + exports_from = []; + + /** @type {import('estree').ExportNamedDeclaration[]} */ + instance_exports_from = []; + + /** @type {Set} */ + hoistable_nodes = new Set(); + + /** @type {Map} */ + node_for_declaration = new Map(); + + /** @type {Array} */ + partly_hoisted = []; + + /** @type {Array} */ + fully_hoisted = []; + /** + * @type {Array<{ + * assignees: Set; + * dependencies: Set; + * node: import('estree').Node; + * declaration: import('estree').Node; + * }>} + */ + reactive_declarations = []; + + /** @type {Set} */ + reactive_declaration_nodes = new Set(); + /** */ has_reactive_assignments = false; - injected_reactive_declaration_vars: Set = new Set(); - helpers: Map = new Map(); - globals: Map = new Map(); - indirect_dependencies: Map> = new Map(); + /** @type {Set} */ + injected_reactive_declaration_vars = new Set(); - file: string; - locate: (c: number) => { line: number; column: number }; + /** @type {Map} */ + helpers = new Map(); - elements: Element[] = []; - stylesheet: Stylesheet; + /** @type {Map} */ + globals = new Map(); - aliases: Map = new Map(); - used_names: Set = new Set(); - globally_used_names: Set = new Set(); + /** @type {Map>} */ + indirect_dependencies = new Map(); - slots: Map = new Map(); - slot_outlets: Set = new Set(); + /** @type {string} */ + file; - tags: Tag[] = []; + /** @type {(c: number) => { line: number; column: number }} */ + locate; - constructor( - ast: Ast, - source: string, - name: string, - compile_options: CompileOptions, - stats: Stats, - warnings: Warning[] - ) { - this.name = { type: 'Identifier', name }; + /** @type {import('./nodes/Element.js').default[]} */ + elements = []; + /** @type {import('./css/Stylesheet.js').default} */ + stylesheet; + + /** @type {Map} */ + aliases = new Map(); + + /** @type {Set} */ + used_names = new Set(); + + /** @type {Set} */ + globally_used_names = new Set(); + + /** @type {Map} */ + slots = new Map(); + + /** @type {Set} */ + slot_outlets = new Set(); + + /** @type {import('./nodes/shared/Tag.js').default[]} */ + tags = []; + + /** + * @param {import('../interfaces.js').Ast} ast + * @param {string} source + * @param {string} name + * @param {import('../interfaces.js').CompileOptions} compile_options + * @param {import('../Stats.js').default} stats + * @param {import('../interfaces.js').Warning[]} warnings + */ + constructor(ast, source, name, compile_options, stats, warnings) { + this.name = { type: 'Identifier', name }; this.stats = stats; this.warnings = warnings; this.ast = ast; this.source = source; this.compile_options = compile_options; - // the instance JS gets mutated, so we park // a copy here for later. TODO this feels gross this.original_ast = clone({ @@ -163,7 +191,6 @@ export default class Component { instance: ast.instance, module: ast.module }); - this.file = compile_options.filename && (typeof process !== 'undefined' @@ -172,7 +199,6 @@ export default class Component { .replace(regex_leading_directory_separator, '') : compile_options.filename); this.locate = getLocator(this.source, { offsetLine: 1 }); - // styles this.stylesheet = new Stylesheet({ source, @@ -183,19 +209,15 @@ export default class Component { get_css_hash: compile_options.cssHash }); this.stylesheet.validate(this); - this.component_options = process_component_options(this, this.ast.html.children); this.namespace = namespaces[this.component_options.namespace] || this.component_options.namespace; - if (compile_options.customElement) { this.tag = this.component_options.customElement?.tag || compile_options.tag || this.name.name; } else { this.tag = this.name.name; } - this.walk_module_js(); - this.push_ignores( this.ast.instance ? extract_ignores_above_position(this.ast.instance.start, this.ast.html.children) @@ -203,10 +225,8 @@ export default class Component { ); this.walk_instance_js_pre_template(); this.pop_ignores(); - this.fragment = new Fragment(this, ast.html); this.name = this.get_unique_name(name); - this.push_ignores( this.ast.instance ? extract_ignores_above_position(this.ast.instance.start, this.ast.html.children) @@ -214,29 +234,35 @@ export default class Component { ); this.walk_instance_js_post_template(); this.pop_ignores(); - - this.elements.forEach((element) => this.stylesheet.apply(element)); + this.elements.forEach(/** @param {any} element */ (element) => this.stylesheet.apply(element)); this.stylesheet.reify(); this.stylesheet.warn_on_unused_selectors(this); } - add_var(node: Node, variable: Var, add_to_lookup = true) { + /** + * @param {import('estree').Node} node + * @param {import('../interfaces.js').Var} variable + * @param {any} add_to_lookup + */ + add_var(node, variable, add_to_lookup = true) { this.vars.push(variable); - if (add_to_lookup) { if (this.var_lookup.has(variable.name)) { const exists_var = this.var_lookup.get(variable.name); if (exists_var.module && exists_var.imported) { - this.error(node as any, compiler_errors.illegal_variable_declaration); + this.error(/** @type {any} */ (node), compiler_errors.illegal_variable_declaration); } } this.var_lookup.set(variable.name, variable); } } - add_reference(node: Node, name: string) { + /** + * @param {import('estree').Node} node + * @param {string} name + */ + add_reference(node, name) { const variable = this.var_lookup.get(name); - if (variable) { variable.referenced = true; } else if (is_reserved_keyword(name)) { @@ -253,9 +279,7 @@ export default class Component { mutated: true, writable: true }); - const subscribable_name = name.slice(1); - const variable = this.var_lookup.get(subscribable_name); if (variable) { variable.referenced = true; @@ -265,43 +289,47 @@ export default class Component { if (this.compile_options.varsReport === 'full') { this.add_var(node, { name, referenced: true }, false); } - this.used_names.add(name); } } - 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); } - apply_stylesheet(element: Element) { + /** @param {import('./nodes/Element.js').default} element */ + apply_stylesheet(element) { this.elements.push(element); } - global(name: string) { + /** @param {string} name */ + global(name) { const alias = this.alias(name); this.globals.set(name, alias); return alias; } - generate(result?: { js: Node[]; css: CssResult }) { + /** @param {{ js: import('estree').Node[]; css: import('../interfaces.js').CssResult }} [result] */ + generate(result) { let js = null; let css = null; - if (result) { const { compile_options, name } = this; const { format = 'esm' } = compile_options; - const banner = `${this.file ? `${this.file} ` : ''}generated by Svelte v${'__VERSION__'}`; - const program: any = { type: 'Program', body: result.js }; - + /** @type {any} */ + const program = { type: 'Program', body: result.js }; walk(program, { - enter: (node: Node, parent: Node, key) => { + enter: /** + * @param {import('estree').Node} node + * @param {import('estree').Node} parent + * @param {any} key + */ (node, parent, key) => { if (node.type === 'Identifier') { if (node.name[0] === '@') { if (node.name[1] === '_') { @@ -309,7 +337,6 @@ export default class Component { node.name = alias.name; } else { let name = node.name.slice(1); - if (compile_options.hydratable) { if (internal_exports.has(`${name}_hydration`)) { name += '_hydration'; @@ -317,7 +344,6 @@ export default class Component { name += 'Hydration'; } } - if (compile_options.dev) { if (internal_exports.has(`${name}_dev`)) { name += '_dev'; @@ -325,15 +351,15 @@ export default class Component { name += 'Dev'; } } - const alias = this.alias(name); this.helpers.set(name, alias); node.name = alias.name; } } else if (node.name[0] !== '#' && !is_valid(node.name)) { // this hack allows x`foo.${bar}` where bar could be invalid - const literal: Literal = { type: 'Literal', value: node.name }; + /** @type {import('estree').Literal} */ + const literal = { type: 'Literal', value: node.name }; if (parent.type === 'Property' && key === 'key') { parent.key = literal; } else if (parent.type === 'MemberExpression' && key === 'property') { @@ -344,19 +370,21 @@ export default class Component { } } }); - const referenced_globals = Array.from( this.globals, + /** @param {any}params_0 */ ([name, alias]) => name !== alias.name && { name, alias } ).filter(Boolean); if (referenced_globals.length) { this.helpers.set('globals', this.alias('globals')); } - const imported_helpers = Array.from(this.helpers, ([name, alias]) => ({ - name, - alias - })); - + const imported_helpers = Array.from( + this.helpers, + /** @param {any}params_0 */ ([name, alias]) => ({ + name, + alias + }) + ); create_module( program, format, @@ -367,40 +395,38 @@ export default class Component { referenced_globals, this.imports, this.vars - .filter((variable) => variable.module && variable.export_name) - .map((variable) => ({ - name: variable.name, - as: variable.export_name - })), + .filter( + /** @param {any} variable */ (variable) => variable.module && variable.export_name + ) + .map( + /** @param {any} variable */ (variable) => ({ + name: variable.name, + as: variable.export_name + }) + ), this.exports_from ); - css = compile_options.customElement ? { code: null, map: null } : result.css; - const js_sourcemap_enabled = check_enable_sourcemap(compile_options.enableSourcemap, 'js'); - if (!js_sourcemap_enabled) { js = print(program); js.map = null; } else { const sourcemap_source_filename = get_sourcemap_source_filename(compile_options); - js = print(program, { sourceMapSource: sourcemap_source_filename }); - js.map.sources = [sourcemap_source_filename]; - js.map.sourcesContent = [this.source]; - js.map = apply_preprocessor_sourcemap( sourcemap_source_filename, js.map, - compile_options.sourcemap as string | RawSourceMap | DecodedSourceMap + /** @type {string | import('@ampproject/remapping').RawSourceMap | import('@ampproject/remapping').DecodedSourceMap} */ ( + compile_options.sourcemap + ) ); } } - return { js, css, @@ -411,7 +437,12 @@ export default class Component { }; } - get_unique_name(name: string, scope?: Scope): Identifier { + /** + * @param {string} name + * @param {import('./utils/scope.js').Scope} [scope] + * @returns {import('estree').Identifier} + */ + get_unique_name(name, scope) { let alias = name; for ( let i = 1; @@ -427,19 +458,27 @@ export default class Component { this.used_names.add(alias); return { type: 'Identifier', name: alias }; } - get_unique_name_maker() { const local_used_names = new Set(); - function add(name: string) { + /** @param {string} name */ + function add(name) { local_used_names.add(name); } - reserved.forEach(add); internal_exports.forEach(add); - this.var_lookup.forEach((_value, key) => add(key)); + this.var_lookup.forEach( + /** + * @param {any} _value + * @param {any} key + */ (_value, key) => add(key) + ); - return (name: string): Identifier => { + /** + * @param {string} name + * @returns {import('estree').Identifier} + */ + return (name) => { let alias = name; for ( let i = 1; @@ -448,7 +487,6 @@ export default class Component { ); local_used_names.add(alias); this.globally_used_names.add(alias); - return { type: 'Identifier', name: alias @@ -456,39 +494,40 @@ export default class Component { }; } - get_vars_report(): Var[] { + /** @returns {import('../interfaces.js').Var[]} */ + get_vars_report() { const { compile_options, vars } = this; - const vars_report = compile_options.varsReport === false ? [] : compile_options.varsReport === 'full' ? vars - : vars.filter((v) => !v.global && !v.internal); - - return vars_report.map((v) => ({ - name: v.name, - export_name: v.export_name || null, - injected: v.injected || false, - module: v.module || false, - mutated: v.mutated || false, - reassigned: v.reassigned || false, - referenced: v.referenced || false, - writable: v.writable || false, - referenced_from_script: v.referenced_from_script || false - })); + : vars.filter(/** @param {any} v */ (v) => !v.global && !v.internal); + return vars_report.map( + /** @param {any} v */ (v) => ({ + name: v.name, + export_name: v.export_name || null, + injected: v.injected || false, + module: v.module || false, + mutated: v.mutated || false, + reassigned: v.reassigned || false, + referenced: v.referenced || false, + writable: v.writable || false, + referenced_from_script: v.referenced_from_script || false + }) + ); } - - error( - pos: { - start: number; - end: number; - }, - e: { - code: string; - message: string; - } - ) { + /** + * @param {{ + * start: number; + * end: number; + * }} pos + * @param {{ + * code: string; + * message: string; + * }} e + */ + error(pos, e) { if (this.compile_options.errorMode === 'warn') { this.warn(pos, e); } else { @@ -502,26 +541,23 @@ export default class Component { }); } } - - warn( - pos: { - start: number; - end: number; - }, - warning: { - code: string; - message: string; - } - ) { + /** + * @param {{ + * start: number; + * end: number; + * }} pos + * @param {{ + * code: string; + * message: string; + * }} warning + */ + warn(pos, warning) { if (this.ignores && this.ignores.has(warning.code)) { return; } - const start = this.locate(pos.start); const end = this.locate(pos.end); - const frame = get_code_frame(this.source, start.line - 1, start.column); - this.warnings.push({ code: warning.code, message: warning.message, @@ -534,10 +570,15 @@ export default class Component { }); } + /** @param {any} node */ extract_imports(node) { this.imports.push(node); } + /** + * @param {any} node + * @param {any} module_script + */ extract_exports(node, module_script = false) { const ignores = extract_svelte_ignore_from_comments(node); if (ignores.length) this.push_ignores(ignores); @@ -546,14 +587,15 @@ export default class Component { return result; } - private _extract_exports( - node: ExportDefaultDeclaration | ExportNamedDeclaration | ExportAllDeclaration, - module_script: boolean - ) { + /** + * @private + * @param {import('estree').ExportDefaultDeclaration | import('estree').ExportNamedDeclaration | import('estree').ExportAllDeclaration} node + * @param {boolean} module_script + */ + _extract_exports(node, module_script) { if (node.type === 'ExportDefaultDeclaration') { - return this.error(node as any, compiler_errors.default_export); + return this.error(/** @type {any} */ (node), compiler_errors.default_export); } - if (node.type === 'ExportNamedDeclaration') { if (node.source) { if (module_script) { @@ -565,120 +607,133 @@ export default class Component { } if (node.declaration) { if (node.declaration.type === 'VariableDeclaration') { - node.declaration.declarations.forEach((declarator) => { - extract_names(declarator.id).forEach((name) => { - const variable = this.var_lookup.get(name); - variable.export_name = name; - if ( - declarator.init?.type === 'Literal' && - typeof declarator.init.value === 'boolean' - ) { - variable.is_boolean = true; - } + node.declaration.declarations.forEach( + /** @param {any} declarator */ (declarator) => { + extract_names(declarator.id).forEach( + /** @param {any} name */ (name) => { + const variable = this.var_lookup.get(name); + variable.export_name = name; + if ( + declarator.init?.type === 'Literal' && + typeof declarator.init.value === 'boolean' + ) { + variable.is_boolean = true; + } + if ( + !module_script && + variable.writable && + !( + variable.referenced || + variable.referenced_from_script || + variable.subscribable + ) + ) { + this.warn( + /** @type {any} */ (declarator), + compiler_warnings.unused_export_let(this.name.name, name) + ); + } + } + ); + } + ); + } else { + const { name } = node.declaration.id; + const variable = this.var_lookup.get(name); + variable.export_name = name; + } + return node.declaration; + } else { + node.specifiers.forEach( + /** @param {any} specifier */ (specifier) => { + const variable = this.var_lookup.get(specifier.local.name); + if (variable) { + variable.export_name = specifier.exported.name; if ( !module_script && variable.writable && !(variable.referenced || variable.referenced_from_script || variable.subscribable) ) { this.warn( - declarator as any, - compiler_warnings.unused_export_let(this.name.name, name) + /** @type {any} */ (specifier), + compiler_warnings.unused_export_let(this.name.name, specifier.exported.name) ); } - }); - }); - } else { - const { name } = node.declaration.id; - - const variable = this.var_lookup.get(name); - variable.export_name = name; - } - - return node.declaration; - } else { - node.specifiers.forEach((specifier) => { - const variable = this.var_lookup.get(specifier.local.name); - - if (variable) { - variable.export_name = specifier.exported.name; - - if ( - !module_script && - variable.writable && - !(variable.referenced || variable.referenced_from_script || variable.subscribable) - ) { - this.warn( - specifier as any, - compiler_warnings.unused_export_let(this.name.name, specifier.exported.name) - ); } } - }); - + ); return null; } } } + /** @param {any} script */ extract_javascript(script) { if (!script) return null; - - return script.content.body.filter((node) => { - if (!node) return false; - if (this.hoistable_nodes.has(node)) return false; - if (this.reactive_declaration_nodes.has(node)) return false; - if (node.type === 'ImportDeclaration') return false; - if (node.type === 'ExportDeclaration' && node.specifiers.length > 0) return false; - return true; - }); + return script.content.body.filter( + /** @param {any} node */ (node) => { + if (!node) return false; + if (this.hoistable_nodes.has(node)) return false; + if (this.reactive_declaration_nodes.has(node)) return false; + if (node.type === 'ImportDeclaration') return false; + if (node.type === 'ExportDeclaration' && node.specifiers.length > 0) return false; + return true; + } + ); } - walk_module_js() { const component = this; const script = this.ast.module; if (!script) return; - walk(script.content, { - enter(node: Node) { + /** @param {import('estree').Node} node */ + enter(node) { if (node.type === 'LabeledStatement' && node.label.name === '$') { - component.warn(node as any, compiler_warnings.module_script_reactive_declaration); + component.warn( + /** @type {any} */ (node), + compiler_warnings.module_script_reactive_declaration + ); } } }); - const { scope, globals } = create_scopes(script.content); this.module_scope = scope; - - scope.declarations.forEach((node, name) => { - if (name[0] === '$') { - return this.error(node as any, compiler_errors.illegal_declaration); - } - - const writable = - node.type === 'VariableDeclaration' && (node.kind === 'var' || node.kind === 'let'); - const imported = node.type.startsWith('Import'); - - this.add_var(node, { - name, - module: true, - hoistable: true, - writable, - imported - }); - }); - - globals.forEach((node, name) => { - if (name[0] === '$') { - return this.error(node as any, compiler_errors.illegal_subscription); - } else { + scope.declarations.forEach( + /** + * @param {any} node + * @param {any} name + */ (node, name) => { + if (name[0] === '$') { + return this.error(/** @type {any} */ (node), compiler_errors.illegal_declaration); + } + const writable = + node.type === 'VariableDeclaration' && (node.kind === 'var' || node.kind === 'let'); + const imported = node.type.startsWith('Import'); this.add_var(node, { name, - global: true, - hoistable: true + module: true, + hoistable: true, + writable, + imported }); } - }); - + ); + globals.forEach( + /** + * @param {any} node + * @param {any} name + */ (node, name) => { + if (name[0] === '$') { + return this.error(/** @type {any} */ (node), compiler_errors.illegal_subscription); + } else { + this.add_var(node, { + name, + global: true, + hoistable: true + }); + } + } + ); const { body } = script.content; let i = body.length; while (--i >= 0) { @@ -687,7 +742,6 @@ export default class Component { this.extract_imports(node); body.splice(i, 1); } - if (regex_starts_with_term_export.test(node.type)) { const replacement = this.extract_exports(node, true); if (replacement) { @@ -698,153 +752,156 @@ export default class Component { } } } - walk_instance_js_pre_template() { const script = this.ast.instance; if (!script) return; - // inject vars for reactive declarations - script.content.body.forEach((node) => { - if (node.type !== 'LabeledStatement') return; - if (node.body.type !== 'ExpressionStatement') return; - - const { expression } = node.body; - if (expression.type !== 'AssignmentExpression') return; - if (expression.left.type === 'MemberExpression') return; - - extract_names(expression.left).forEach((name) => { - if (!this.var_lookup.has(name) && name[0] !== '$') { - this.injected_reactive_declaration_vars.add(name); - } - }); - }); - + script.content.body.forEach( + /** @param {any} node */ (node) => { + if (node.type !== 'LabeledStatement') return; + if (node.body.type !== 'ExpressionStatement') return; + const { expression } = node.body; + if (expression.type !== 'AssignmentExpression') return; + if (expression.left.type === 'MemberExpression') return; + extract_names(expression.left).forEach( + /** @param {any} name */ (name) => { + if (!this.var_lookup.has(name) && name[0] !== '$') { + this.injected_reactive_declaration_vars.add(name); + } + } + ); + } + ); const { scope: instance_scope, map, globals } = create_scopes(script.content); this.instance_scope = instance_scope; this.instance_scope_map = map; - - instance_scope.declarations.forEach((node, name) => { - if (name[0] === '$') { - return this.error(node as any, compiler_errors.illegal_declaration); + instance_scope.declarations.forEach( + /** + * @param {any} node + * @param {any} name + */ (node, name) => { + if (name[0] === '$') { + return this.error(/** @type {any} */ (node), compiler_errors.illegal_declaration); + } + const writable = + node.type === 'VariableDeclaration' && (node.kind === 'var' || node.kind === 'let'); + const imported = node.type.startsWith('Import'); + this.add_var(node, { + name, + initialised: instance_scope.initialised_declarations.has(name), + writable, + imported + }); + this.node_for_declaration.set(name, node); } - - const writable = - node.type === 'VariableDeclaration' && (node.kind === 'var' || node.kind === 'let'); - const imported = node.type.startsWith('Import'); - - this.add_var(node, { - name, - initialised: instance_scope.initialised_declarations.has(name), - writable, - imported - }); - - this.node_for_declaration.set(name, node); - }); - + ); // NOTE: add store variable first, then only $store value // as `$store` will mark `store` variable as referenced and subscribable const global_keys = Array.from(globals.keys()); const sorted_globals = [ - ...global_keys.filter((key) => key[0] !== '$'), - ...global_keys.filter((key) => key[0] === '$') + ...global_keys.filter(/** @param {any} key */ (key) => key[0] !== '$'), + ...global_keys.filter(/** @param {any} key */ (key) => key[0] === '$') ]; - - sorted_globals.forEach((name) => { - if (this.var_lookup.has(name)) return; - const node = globals.get(name); - - if (this.injected_reactive_declaration_vars.has(name)) { - this.add_var(node, { - name, - injected: true, - writable: true, - reassigned: true, - initialised: true - }); - } else if (is_reserved_keyword(name)) { - this.add_var(node, { - name, - injected: true - }); - } else if (name[0] === '$') { - if (name === '$' || name[1] === '$') { - return this.error(node as any, compiler_errors.illegal_global(name)); - } - - this.add_var(node, { - name, - injected: true, - mutated: true, - writable: true - }); - - this.add_reference(node, name.slice(1)); - - const variable = this.var_lookup.get(name.slice(1)); - if (variable) { - variable.subscribable = true; - variable.referenced_from_script = true; + sorted_globals.forEach( + /** @param {any} name */ (name) => { + if (this.var_lookup.has(name)) return; + const node = globals.get(name); + if (this.injected_reactive_declaration_vars.has(name)) { + this.add_var(node, { + name, + injected: true, + writable: true, + reassigned: true, + initialised: true + }); + } else if (is_reserved_keyword(name)) { + this.add_var(node, { + name, + injected: true + }); + } else if (name[0] === '$') { + if (name === '$' || name[1] === '$') { + return this.error(/** @type {any} */ (node), compiler_errors.illegal_global(name)); + } + this.add_var(node, { + name, + injected: true, + mutated: true, + writable: true + }); + this.add_reference(node, name.slice(1)); + const variable = this.var_lookup.get(name.slice(1)); + if (variable) { + variable.subscribable = true; + variable.referenced_from_script = true; + } + } else { + this.add_var(node, { + name, + global: true, + hoistable: true + }); } - } else { - this.add_var(node, { - name, - global: true, - hoistable: true - }); } - }); - + ); this.track_references_and_mutations(); } - walk_instance_js_post_template() { const script = this.ast.instance; if (!script) return; - this.post_template_walk(); - this.hoist_instance_declarations(); this.extract_reactive_declarations(); this.check_if_tags_content_dynamic(); } - post_template_walk() { const script = this.ast.instance; if (!script) return; - const component = this; const { content } = script; const { instance_scope, instance_scope_map: map } = this; - let scope = instance_scope; - const to_remove = []; + + /** + * @param {any} parent + * @param {any} prop + * @param {any} index + */ const remove = (parent, prop, index) => { to_remove.unshift([parent, prop, index]); }; let scope_updated = false; - const current_function_stack = []; - let current_function: FunctionDeclaration | FunctionExpression = null; + /** @type {import('estree').FunctionDeclaration | import('estree').FunctionExpression} */ + let current_function = null; walk(content, { - enter(node: Node, parent: Node, prop, index) { + /** + * @param {import('estree').Node} node + * @param {import('estree').Node} parent + * @param {any} prop + * @param {any} index + */ + enter(node, parent, prop, index) { if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') { current_function_stack.push((current_function = node)); } - if (map.has(node)) { scope = map.get(node); } - let deep = false; - let names: string[] = []; + /** @type {string[]} */ + let names = []; if (node.type === 'AssignmentExpression') { if (node.left.type === 'ArrayPattern') { walk(node.left, { - enter(node: Node, parent: Node) { + /** + * @param {import('estree').Node} node + * @param {import('estree').Node} parent + */ + enter(node, parent) { if ( node.type === 'Identifier' && parent.type !== 'MemberExpression' && @@ -864,34 +921,32 @@ export default class Component { names.push(name); } if (names.length > 0) { - names.forEach((name) => { - let current_scope = scope; - let declaration; - - while (current_scope) { - if (current_scope.declarations.has(name)) { - declaration = current_scope.declarations.get(name); - break; + names.forEach( + /** @param {any} name */ (name) => { + let current_scope = scope; + let declaration; + while (current_scope) { + if (current_scope.declarations.has(name)) { + declaration = current_scope.declarations.get(name); + break; + } + current_scope = current_scope.parent; + } + if (declaration && /** @type {any} */ (declaration).kind === 'const' && !deep) { + component.error(/** @type {any} */ (node), { + code: 'assignment-to-const', + message: 'You are assigning to a const' + }); } - current_scope = current_scope.parent; - } - - if (declaration && declaration.kind === 'const' && !deep) { - component.error(node as any, { - code: 'assignment-to-const', - message: 'You are assigning to a const' - }); } - }); + ); } - if (node.type === 'ImportDeclaration') { component.extract_imports(node); // TODO: to use actual remove remove(parent, prop, index); return this.skip(); } - if (regex_starts_with_term_export.test(node.type)) { const replacement = component.extract_exports(node); if (replacement) { @@ -902,16 +957,15 @@ export default class Component { } return this.skip(); } - component.warn_on_undefined_store_value_references(node, parent, prop, scope); }, - leave(node: Node) { + /** @param {import('estree').Node} node */ + leave(node) { if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') { current_function_stack.pop(); current_function = current_function_stack[current_function_stack.length - 1]; } - // do it on leave, to prevent infinite loop if ( component.compile_options.dev && @@ -928,13 +982,11 @@ export default class Component { scope_updated = true; } } - if (map.has(node)) { scope = scope.parent; } } }); - for (const [parent, prop, index] of to_remove) { if (parent) { if (index !== null) { @@ -944,49 +996,46 @@ export default class Component { } } } - if (scope_updated) { const { scope, map } = create_scopes(script.content); this.instance_scope = scope; this.instance_scope_map = map; } } - track_references_and_mutations() { const script = this.ast.instance; if (!script) return; - const component = this; const { content } = script; const { instance_scope, module_scope, instance_scope_map: map } = this; - let scope = instance_scope; - walk(content, { - enter(node: Node, parent: Node) { + /** + * @param {import('estree').Node} node + * @param {import('estree').Node} parent + */ + enter(node, parent) { if (map.has(node)) { scope = map.get(node); } - if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') { const assignee = node.type === 'AssignmentExpression' ? node.left : node.argument; - const names = extract_names(assignee as Node); - + const names = extract_names(/** @type {import('estree').Node} */ (assignee)); const deep = assignee.type === 'MemberExpression'; - - names.forEach((name) => { - const scope_owner = scope.find_owner(name); - if ( - scope_owner !== null - ? scope_owner === instance_scope - : module_scope && module_scope.has(name) - ) { - const variable = component.var_lookup.get(name); - variable[deep ? 'mutated' : 'reassigned'] = true; + names.forEach( + /** @param {any} name */ (name) => { + const scope_owner = scope.find_owner(name); + if ( + scope_owner !== null + ? scope_owner === instance_scope + : module_scope && module_scope.has(name) + ) { + const variable = component.var_lookup.get(name); + variable[deep ? 'mutated' : 'reassigned'] = true; + } } - }); + ); } - if (is_used_as_reference(node, parent)) { const object = get_object(node); if (scope.find_owner(object.name) === instance_scope) { @@ -996,7 +1045,8 @@ export default class Component { } }, - leave(node: Node) { + /** @param {import('estree').Node} node */ + leave(node) { if (map.has(node)) { scope = scope.parent; } @@ -1004,25 +1054,28 @@ export default class Component { }); } - warn_on_undefined_store_value_references( - node: Node, - parent: Node, - prop: string | number | symbol, - scope: Scope - ) { + /** + * @param {import('estree').Node} node + * @param {import('estree').Node} parent + * @param {string | number | symbol} prop + * @param {import('./utils/scope.js').Scope} scope + */ + warn_on_undefined_store_value_references(node, parent, prop, scope) { if (node.type === 'LabeledStatement' && node.label.name === '$' && parent.type !== 'Program') { - this.warn(node as any, compiler_warnings.non_top_level_reactive_declaration); + this.warn(/** @type {any} */ (node), compiler_warnings.non_top_level_reactive_declaration); } - - if (is_reference(node as NodeWithPropertyDefinition, parent as NodeWithPropertyDefinition)) { + if ( + is_reference( + /** @type {import('is-reference').NodeWithPropertyDefinition} */ (node), + /** @type {import('is-reference').NodeWithPropertyDefinition} */ (parent) + ) + ) { const object = get_object(node); const { name } = object; - if (name[0] === '$') { if (!scope.has(name)) { this.warn_if_undefined(name, object, null); } - if ( name[1] !== '$' && scope.has(name.slice(1)) && @@ -1034,14 +1087,20 @@ export default class Component { (parent.type === 'VariableDeclarator' && prop === 'id') ) ) { - return this.error(node as any, compiler_errors.contextual_store); + return this.error(/** @type {any} */ (node), compiler_errors.contextual_store); } } } } } - loop_protect(node, scope: Scope, timeout: number): Node | null { + /** + * @param {any} node + * @param {import('./utils/scope.js').Scope} scope + * @param {number} timeout + * @returns {import('estree').Node} + */ + loop_protect(node, scope, timeout) { if ( node.type === 'WhileStatement' || node.type === 'ForStatement' || @@ -1049,10 +1108,8 @@ export default class Component { ) { const guard = this.get_unique_name('guard', scope); this.used_names.add(guard.name); - const before = b`const ${guard} = @loop_guard(${timeout})`; const inside = b`${guard}();`; - // wrap expression statement with BlockStatement if (node.body.type !== 'BlockStatement') { node.body = { @@ -1061,7 +1118,6 @@ export default class Component { }; } node.body.body.push(inside[0]); - return { type: 'BlockStatement', body: [before[0], node] @@ -1070,38 +1126,36 @@ export default class Component { return null; } - rewrite_props(get_insert: (variable: Var) => Node[]) { + /** @param {(variable: import('../interfaces.js').Var) => import('estree').Node[]} get_insert */ + rewrite_props(get_insert) { if (!this.ast.instance) return; - const component = this; const { instance_scope, instance_scope_map: map } = this; let scope = instance_scope; - walk(this.ast.instance.content, { - enter(node: Node) { + /** @param {import('estree').Node} node */ + enter(node) { if (regex_contains_term_function.test(node.type)) { return this.skip(); } - if (map.has(node)) { scope = map.get(node); } - if (node.type === 'ExportNamedDeclaration' && node.declaration) { return this.replace(node.declaration); } - if (node.type === 'VariableDeclaration') { // NOTE: `var` does not follow block scoping if (node.kind === 'var' || scope === instance_scope) { const inserts = []; const props = []; - function add_new_props( - exported: Identifier, - local: Pattern, - default_value: Expression - ) { + /** + * @param {import('estree').Identifier} exported + * @param {import('estree').Pattern} local + * @param {import('estree').Expression} default_value + */ + function add_new_props(exported, local, default_value) { props.push({ type: 'Property', method: false, @@ -1118,7 +1172,6 @@ export default class Component { : local }); } - // transform // ``` // export let { x, y = 123 } = OBJ, z = 456 @@ -1131,12 +1184,15 @@ export default class Component { for (let index = 0; index < node.declarations.length; index++) { const declarator = node.declarations[index]; if (declarator.id.type !== 'Identifier') { - function get_new_name(local: Identifier): Identifier { + /** + * @param {import('estree').Identifier} local + * @returns {import('estree').Identifier} + */ + function get_new_name(local) { const variable = component.var_lookup.get(local.name); if (variable.subscribable) { inserts.push(get_insert(variable)); } - if (variable.export_name && variable.writable) { const alias_name = component.get_unique_name(local.name); add_new_props( @@ -1149,28 +1205,30 @@ export default class Component { return local; } - function rename_identifiers(param: Pattern) { + /** @param {import('estree').Pattern} param */ + function rename_identifiers(param) { switch (param.type) { case 'ObjectPattern': { - const handle_prop = (prop: Property | RestElement) => { + /** @param {import('estree').Property | import('estree').RestElement} prop */ + const handle_prop = (prop) => { if (prop.type === 'RestElement') { rename_identifiers(prop); } else if (prop.value.type === 'Identifier') { prop.value = get_new_name(prop.value); } else { - rename_identifiers(prop.value as Pattern); + rename_identifiers(/** @type {import('estree').Pattern} */ (prop.value)); } }; - param.properties.forEach(handle_prop); break; } case 'ArrayPattern': { - const handle_element = ( - element: Pattern | null, - index: number, - array: Array - ) => { + /** + * @param {import('estree').Pattern | null} element + * @param {number} index + * @param {Array} array + */ + const handle_element = (element, index, array) => { if (element) { if (element.type === 'Identifier') { array[index] = get_new_name(element); @@ -1179,11 +1237,9 @@ export default class Component { } } }; - param.elements.forEach(handle_element); break; } - case 'RestElement': if (param.argument.type === 'Identifier') { param.argument = get_new_name(param.argument); @@ -1191,7 +1247,6 @@ export default class Component { rename_identifiers(param.argument); } break; - case 'AssignmentPattern': if (param.left.type === 'Identifier') { param.left = get_new_name(param.left); @@ -1201,7 +1256,6 @@ export default class Component { break; } } - rename_identifiers(declarator.id); } else { const { name } = declarator.id; @@ -1220,78 +1274,75 @@ export default class Component { } } } - this.replace( - b` + /** @type {any} */ ( + b` ${node.declarations.length ? node : null} ${props.length > 0 && b`let { ${props} } = $$props;`} ${inserts} - ` as any + ` + ) ); return this.skip(); } } }, - leave(node: Node) { + /** @param {import('estree').Node} node */ + leave(node) { if (map.has(node)) { scope = scope.parent; } } }); } - hoist_instance_declarations() { // we can safely hoist variable declarations that are // initialised to literals, and functions that don't // reference instance variables other than other // hoistable functions. TODO others? - const { hoistable_nodes, var_lookup, injected_reactive_declaration_vars, imports } = this; - const top_level_function_declarations = new Map(); - const { body } = this.ast.instance.content; - for (let i = 0; i < body.length; i += 1) { const node = body[i]; - if (node.type === 'VariableDeclaration') { - const all_hoistable = node.declarations.every((d) => { - if (!d.init) return false; - if (d.init.type !== 'Literal') return false; - - // everything except const values can be changed by e.g. svelte devtools - // which means we can't hoist it - if (node.kind !== 'const' && this.compile_options.dev) return false; - - const { name } = d.id as Identifier; - - const v = this.var_lookup.get(name); - if (v.reassigned) return false; - if (v.export_name) return false; - - if (this.var_lookup.get(name).reassigned) return false; - if (this.vars.find((variable) => variable.name === name && variable.module)) { - return false; + const all_hoistable = node.declarations.every( + /** @param {any} d */ (d) => { + if (!d.init) return false; + if (d.init.type !== 'Literal') return false; + // everything except const values can be changed by e.g. svelte devtools + // which means we can't hoist it + if (node.kind !== 'const' && this.compile_options.dev) return false; + const { name } = /** @type {import('estree').Identifier} */ (d.id); + const v = this.var_lookup.get(name); + if (v.reassigned) return false; + if (v.export_name) return false; + if (this.var_lookup.get(name).reassigned) return false; + if ( + this.vars.find( + /** @param {any} variable */ (variable) => variable.name === name && variable.module + ) + ) { + return false; + } + return true; } - - return true; - }); - + ); if (all_hoistable) { - node.declarations.forEach((d) => { - const variable = this.var_lookup.get((d.id as Identifier).name); - variable.hoistable = true; - }); - + node.declarations.forEach( + /** @param {any} d */ (d) => { + const variable = this.var_lookup.get( + /** @type {import('estree').Identifier} */ (d.id).name + ); + variable.hoistable = true; + } + ); hoistable_nodes.add(node); - body.splice(i--, 1); this.fully_hoisted.push(node); } } - if ( node.type === 'ExportNamedDeclaration' && node.declaration && @@ -1299,59 +1350,53 @@ export default class Component { ) { top_level_function_declarations.set(node.declaration.id.name, node); } - if (node.type === 'FunctionDeclaration') { top_level_function_declarations.set(node.id.name, node); } } - const checked = new Set(); const walking = new Set(); + /** @param {any} fn_declaration */ const is_hoistable = (fn_declaration) => { if (fn_declaration.type === 'ExportNamedDeclaration') { fn_declaration = fn_declaration.declaration; } - const instance_scope = this.instance_scope; let scope = this.instance_scope; const map = this.instance_scope_map; - let hoistable = true; - // handle cycles walking.add(fn_declaration); - walk(fn_declaration, { - enter(node: Node, parent) { + /** + * @param {import('estree').Node} node + * @param {any} parent + */ + enter(node, parent) { if (!hoistable) return this.skip(); - if (map.has(node)) { scope = map.get(node); } - if ( - is_reference(node as NodeWithPropertyDefinition, parent as NodeWithPropertyDefinition) + is_reference( + /** @type {import('is-reference').NodeWithPropertyDefinition} */ (node), + /** @type {import('is-reference').NodeWithPropertyDefinition} */ (parent) + ) ) { const { name } = flatten_reference(node); const owner = scope.find_owner(name); - if (injected_reactive_declaration_vars.has(name)) { hoistable = false; } else if (name[0] === '$' && !owner) { hoistable = false; } else if (owner === instance_scope) { const variable = var_lookup.get(name); - if (variable.reassigned || variable.mutated) hoistable = false; - if (name === fn_declaration.id.name) return; - if (variable.hoistable) return; - if (top_level_function_declarations.has(name)) { const other_declaration = top_level_function_declarations.get(name); - if (walking.has(other_declaration)) { hoistable = false; } else if ( @@ -1366,258 +1411,275 @@ export default class Component { hoistable = false; } } - this.skip(); } }, - leave(node: Node) { + /** @param {import('estree').Node} node */ + leave(node) { if (map.has(node)) { scope = scope.parent; } } }); - checked.add(fn_declaration); walking.delete(fn_declaration); - return hoistable; }; - for (const [name, node] of top_level_function_declarations) { if (is_hoistable(node)) { const variable = this.var_lookup.get(name); variable.hoistable = true; hoistable_nodes.add(node); - const i = body.indexOf(node); body.splice(i, 1); this.fully_hoisted.push(node); } } - for (const { specifiers } of imports) { for (const specifier of specifiers) { const variable = var_lookup.get(specifier.local.name); - if (!variable.mutated || variable.subscribable) { variable.hoistable = true; } } } } - extract_reactive_declarations() { const component = this; - - const unsorted_reactive_declarations: Array<{ - assignees: Set; - dependencies: Set; - node: Node; - declaration: Node; - }> = []; - - this.ast.instance.content.body.forEach((node) => { - const ignores = extract_svelte_ignore_from_comments(node); - if (ignores.length) this.push_ignores(ignores); - - if (node.type === 'LabeledStatement' && node.label.name === '$') { - this.reactive_declaration_nodes.add(node); - - const assignees = new Set(); - const assignee_nodes = new Set(); - const dependencies = new Set(); - const module_dependencies = new Set(); - - let scope = this.instance_scope; - const { declarations: outset_scope_decalarations } = this.instance_scope; - const map = this.instance_scope_map; - - walk(node.body, { - enter(node: Node, parent) { - if (node.type === 'VariableDeclaration' && node.kind === 'var') { - const is_var_in_outset = node.declarations.some((declaration: VariableDeclarator) => { - const names: string[] = extract_names(declaration.id); - return !!names.find((name: string) => { - const var_node = outset_scope_decalarations.get(name); - return var_node === node; - }); - }); - if (is_var_in_outset) { - return component.error(node as any, compiler_errors.invalid_var_declaration); + /** + * @type {Array<{ + * assignees: Set; + * dependencies: Set; + * node: import('estree').Node; + * declaration: import('estree').Node; + * }>} + */ + const unsorted_reactive_declarations = []; + this.ast.instance.content.body.forEach( + /** @param {any} node */ (node) => { + const ignores = extract_svelte_ignore_from_comments(node); + if (ignores.length) this.push_ignores(ignores); + if (node.type === 'LabeledStatement' && node.label.name === '$') { + this.reactive_declaration_nodes.add(node); + const assignees = new Set(); + const assignee_nodes = new Set(); + const dependencies = new Set(); + const module_dependencies = new Set(); + let scope = this.instance_scope; + const { declarations: outset_scope_decalarations } = this.instance_scope; + const map = this.instance_scope_map; + walk(node.body, { + /** + * @param {import('estree').Node} node + * @param {any} parent + */ + enter(node, parent) { + if (node.type === 'VariableDeclaration' && node.kind === 'var') { + const is_var_in_outset = node.declarations.some( + /** @param {import('estree').VariableDeclarator} declaration */ (declaration) => { + /** @type {string[]} */ + const names = extract_names(declaration.id); + return !!names.find( + /** @param {string} name */ (name) => { + const var_node = outset_scope_decalarations.get(name); + return var_node === node; + } + ); + } + ); + if (is_var_in_outset) { + return component.error( + /** @type {any} */ (node), + compiler_errors.invalid_var_declaration + ); + } } - } - if (map.has(node)) { - scope = map.get(node); - } - - if (node.type === 'AssignmentExpression') { - const left = get_object(node.left); - - extract_identifiers(left).forEach((node) => { - assignee_nodes.add(node); - assignees.add(node.name); - }); - - if (node.operator !== '=') { - dependencies.add(left.name); + if (map.has(node)) { + scope = map.get(node); } - } else if (node.type === 'UpdateExpression') { - const identifier = get_object(node.argument); - assignees.add(identifier.name); - } else if ( - is_reference(node as NodeWithPropertyDefinition, parent as NodeWithPropertyDefinition) - ) { - const identifier = get_object(node); - if (!assignee_nodes.has(identifier)) { - const { name } = identifier; - const owner = scope.find_owner(name); - const variable = component.var_lookup.get(name); - let should_add_as_dependency = true; - - if (variable) { - variable.is_reactive_dependency = true; - if (variable.module && variable.writable) { - should_add_as_dependency = false; - module_dependencies.add(name); + if (node.type === 'AssignmentExpression') { + const left = get_object(node.left); + extract_identifiers(left).forEach( + /** @param {any} node */ (node) => { + assignee_nodes.add(node); + assignees.add(node.name); } + ); + if (node.operator !== '=') { + dependencies.add(left.name); } - const is_writable_or_mutated = variable && (variable.writable || variable.mutated); - if ( - should_add_as_dependency && - (!owner || owner === component.instance_scope) && - (name[0] === '$' || is_writable_or_mutated) - ) { - dependencies.add(name); + } else if (node.type === 'UpdateExpression') { + const identifier = get_object(node.argument); + assignees.add(identifier.name); + } else if ( + is_reference( + /** @type {import('is-reference').NodeWithPropertyDefinition} */ (node), + /** @type {import('is-reference').NodeWithPropertyDefinition} */ (parent) + ) + ) { + const identifier = get_object(node); + if (!assignee_nodes.has(identifier)) { + const { name } = identifier; + const owner = scope.find_owner(name); + const variable = component.var_lookup.get(name); + let should_add_as_dependency = true; + if (variable) { + variable.is_reactive_dependency = true; + if (variable.module && variable.writable) { + should_add_as_dependency = false; + module_dependencies.add(name); + } + } + const is_writable_or_mutated = + variable && (variable.writable || variable.mutated); + if ( + should_add_as_dependency && + (!owner || owner === component.instance_scope) && + (name[0] === '$' || is_writable_or_mutated) + ) { + dependencies.add(name); + } } + this.skip(); } + }, - this.skip(); - } - }, - - leave(node: Node) { - if (map.has(node)) { - scope = scope.parent; + /** @param {import('estree').Node} node */ + leave(node) { + if (map.has(node)) { + scope = scope.parent; + } } + }); + if (module_dependencies.size > 0 && dependencies.size === 0) { + component.warn( + /** @type {any} */ (node.body), + compiler_warnings.module_script_variable_reactive_declaration( + Array.from(module_dependencies) + ) + ); } - }); - - if (module_dependencies.size > 0 && dependencies.size === 0) { - component.warn( - node.body as any, - compiler_warnings.module_script_variable_reactive_declaration( - Array.from(module_dependencies) - ) - ); + const { expression } = /** @type {import('estree').ExpressionStatement} */ (node.body); + const declaration = + expression && /** @type {import('estree').AssignmentExpression} */ (expression).left; + unsorted_reactive_declarations.push({ + assignees, + dependencies, + node, + declaration + }); } - - const { expression } = node.body as ExpressionStatement; - const declaration = expression && (expression as AssignmentExpression).left; - - unsorted_reactive_declarations.push({ - assignees, - dependencies, - node, - declaration - }); + if (ignores.length) this.pop_ignores(); } - - if (ignores.length) this.pop_ignores(); - }); - + ); const lookup = new Map(); - - unsorted_reactive_declarations.forEach((declaration) => { - declaration.assignees.forEach((name) => { - if (!lookup.has(name)) { - lookup.set(name, []); - } - - // TODO warn or error if a name is assigned to in - // multiple reactive declarations? - lookup.get(name).push(declaration); - }); - }); - + unsorted_reactive_declarations.forEach( + /** @param {any} declaration */ (declaration) => { + declaration.assignees.forEach( + /** @param {any} name */ (name) => { + if (!lookup.has(name)) { + lookup.set(name, []); + } + // TODO warn or error if a name is assigned to in + // multiple reactive declarations? + lookup.get(name).push(declaration); + } + ); + } + ); const cycle = check_graph_for_cycles( - unsorted_reactive_declarations.reduce((acc, declaration) => { - declaration.assignees.forEach((v) => { - declaration.dependencies.forEach((w) => { - if (!declaration.assignees.has(w)) { - acc.push([v, w]); + unsorted_reactive_declarations.reduce( + /** + * @param {any} acc + * @param {any} declaration + */ (acc, declaration) => { + declaration.assignees.forEach( + /** @param {any} v */ (v) => { + declaration.dependencies.forEach( + /** @param {any} w */ (w) => { + if (!declaration.assignees.has(w)) { + acc.push([v, w]); + } + } + ); } - }); - }); - return acc; - }, []) + ); + return acc; + }, + [] + ) ); - if (cycle && cycle.length) { const declarationList = lookup.get(cycle[0]); const declaration = declarationList[0]; return this.error(declaration.node, compiler_errors.cyclical_reactive_declaration(cycle)); } + /** @param {any} declaration */ const add_declaration = (declaration) => { if (this.reactive_declarations.includes(declaration)) return; - - declaration.dependencies.forEach((name) => { - if (declaration.assignees.has(name)) return; - const earlier_declarations = lookup.get(name); - if (earlier_declarations) { - earlier_declarations.forEach(add_declaration); + declaration.dependencies.forEach( + /** @param {any} name */ (name) => { + if (declaration.assignees.has(name)) return; + const earlier_declarations = lookup.get(name); + if (earlier_declarations) { + earlier_declarations.forEach(add_declaration); + } } - }); - + ); this.reactive_declarations.push(declaration); }; - unsorted_reactive_declarations.forEach(add_declaration); } - check_if_tags_content_dynamic() { - this.tags.forEach((tag) => { - tag.check_if_content_dynamic(); - }); + this.tags.forEach( + /** @param {any} tag */ (tag) => { + tag.check_if_content_dynamic(); + } + ); } - warn_if_undefined(name: string, node, template_scope: TemplateScope) { + /** + * @param {string} name + * @param {any} node + * @param {import('./nodes/shared/TemplateScope.js').default} template_scope + */ + warn_if_undefined(name, node, template_scope) { if (name[0] === '$') { if (name === '$' || (name[1] === '$' && !is_reserved_keyword(name))) { return this.error(node, compiler_errors.illegal_global(name)); } - this.has_reactive_assignments = true; // TODO does this belong here? - if (is_reserved_keyword(name)) return; - name = name.slice(1); } - if (this.var_lookup.has(name) && !this.var_lookup.get(name).global) return; if (template_scope && template_scope.names.has(name)) return; if (globals.has(name) && node.type !== 'InlineComponent') return; - this.warn(node, compiler_warnings.missing_declaration(name, !!this.ast.instance)); } + /** @param {any} ignores */ push_ignores(ignores) { this.ignores = new Set(this.ignores || []); add_to_set(this.ignores, ignores); this.ignore_stack.push(this.ignores); } - pop_ignores() { this.ignore_stack.pop(); this.ignores = this.ignore_stack[this.ignore_stack.length - 1]; } } - const regex_valid_tag_name = /^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/; -function process_component_options(component: Component, nodes) { - const component_options: ComponentOptions = { +/** + * @param {Component} component + * @param {any} nodes + */ +function process_component_options(component, nodes) { + /** @type {ComponentOptions} */ + const component_options = { immutable: component.compile_options.immutable || false, accessors: 'accessors' in component.compile_options @@ -1626,217 +1688,216 @@ function process_component_options(component: Component, nodes) { preserveWhitespace: !!component.compile_options.preserveWhitespace, namespace: component.compile_options.namespace }; + const node = nodes.find(/** @param {any} node */ (node) => node.name === 'svelte:options'); - const node = nodes.find((node) => node.name === 'svelte:options'); - + /** + * @param {any} attribute + * @param {any}params_0 + */ function get_value(attribute, { code, message }) { const { value } = attribute; const chunk = value[0]; - if (!chunk) return true; - if (value.length > 1) { return component.error(attribute, { code, message }); } - if (chunk.type === 'Text') return chunk.data; - if (chunk.expression.type !== 'Literal') { return component.error(attribute, { code, message }); } - return chunk.expression.value; } - if (node) { - node.attributes.forEach((attribute) => { - if (attribute.type === 'Attribute') { - const { name } = attribute; - - function parse_tag(attribute: Attribute, tag: string) { - if (typeof tag !== 'string' && tag !== null) { - return component.error(attribute, compiler_errors.invalid_tag_attribute); - } - - if (tag && !regex_valid_tag_name.test(tag)) { - return component.error(attribute, compiler_errors.invalid_tag_property); - } - - if (tag && !component.compile_options.customElement) { - component.warn(attribute, compiler_warnings.missing_custom_element_compile_options); - } - - component_options.customElement = component_options.customElement || ({} as any); - component_options.customElement.tag = tag; - } - - switch (name) { - case 'tag': { - component.warn(attribute, compiler_warnings.tag_option_deprecated); - parse_tag(attribute, get_value(attribute, compiler_errors.invalid_tag_attribute)); - break; + node.attributes.forEach( + /** @param {any} attribute */ (attribute) => { + if (attribute.type === 'Attribute') { + const { name } = attribute; + + /** + * @param {import('../interfaces.js').Attribute} attribute + * @param {string} tag + */ + function parse_tag(attribute, tag) { + if (typeof tag !== 'string' && tag !== null) { + return component.error(attribute, compiler_errors.invalid_tag_attribute); + } + if (tag && !regex_valid_tag_name.test(tag)) { + return component.error(attribute, compiler_errors.invalid_tag_property); + } + if (tag && !component.compile_options.customElement) { + component.warn(attribute, compiler_warnings.missing_custom_element_compile_options); + } + component_options.customElement = + component_options.customElement || /** @type {any} */ ({}); + component_options.customElement.tag = tag; } - - case 'customElement': { - component_options.customElement = component_options.customElement || ({} as any); - - const { value } = attribute; - - if (value[0].type === 'MustacheTag' && value[0].expression?.value === null) { - component_options.customElement.tag = null; - break; - } else if (value[0].type === 'Text') { + switch (name) { + case 'tag': { + component.warn(attribute, compiler_warnings.tag_option_deprecated); parse_tag(attribute, get_value(attribute, compiler_errors.invalid_tag_attribute)); break; - } else if (value[0].expression.type !== 'ObjectExpression') { - return component.error(attribute, compiler_errors.invalid_customElement_attribute); } - - const tag = value[0].expression.properties.find((prop: any) => prop.key.name === 'tag'); - if (tag) { - parse_tag(tag, tag.value?.value); - } else { - return component.error(attribute, compiler_errors.invalid_customElement_attribute); - } - - const props = value[0].expression.properties.find( - (prop: any) => prop.key.name === 'props' - ); - if (props) { - const error = () => - component.error(attribute, compiler_errors.invalid_props_attribute); - if (props.value?.type !== 'ObjectExpression') { - return error(); + case 'customElement': { + component_options.customElement = + component_options.customElement || /** @type {any} */ ({}); + const { value } = attribute; + if (value[0].type === 'MustacheTag' && value[0].expression?.value === null) { + component_options.customElement.tag = null; + break; + } else if (value[0].type === 'Text') { + parse_tag(attribute, get_value(attribute, compiler_errors.invalid_tag_attribute)); + break; + } else if (value[0].expression.type !== 'ObjectExpression') { + return component.error(attribute, compiler_errors.invalid_customElement_attribute); } - - component_options.customElement.props = {}; - - for (const property of (props.value as ObjectExpression).properties) { - if ( - property.type !== 'Property' || - property.computed || - property.key.type !== 'Identifier' || - property.value.type !== 'ObjectExpression' - ) { + const tag = value[0].expression.properties.find( + /** @param {any} prop */ (prop) => prop.key.name === 'tag' + ); + if (tag) { + parse_tag(tag, tag.value?.value); + } else { + return component.error(attribute, compiler_errors.invalid_customElement_attribute); + } + const props = value[0].expression.properties.find( + /** @param {any} prop */ + (prop) => prop.key.name === 'props' + ); + if (props) { + const error = () => + component.error(attribute, compiler_errors.invalid_props_attribute); + if (props.value?.type !== 'ObjectExpression') { return error(); } - component_options.customElement.props[property.key.name] = {}; - for (const prop of property.value.properties) { + component_options.customElement.props = {}; + for (const property of /** @type {import('estree').ObjectExpression} */ ( + props.value + ).properties) { if ( - prop.type !== 'Property' || - prop.computed || - prop.key.type !== 'Identifier' || - prop.value.type !== 'Literal' + property.type !== 'Property' || + property.computed || + property.key.type !== 'Identifier' || + property.value.type !== 'ObjectExpression' ) { return error(); } - if ( - ['reflect', 'attribute', 'type'].indexOf(prop.key.name) === -1 || - (prop.key.name === 'type' && - ['String', 'Number', 'Boolean', 'Array', 'Object'].indexOf( - prop.value.value as string - ) === -1) || - (prop.key.name === 'reflect' && typeof prop.value.value !== 'boolean') || - (prop.key.name === 'attribute' && typeof prop.value.value !== 'string') - ) { - return error(); + component_options.customElement.props[property.key.name] = {}; + for (const prop of property.value.properties) { + if ( + prop.type !== 'Property' || + prop.computed || + prop.key.type !== 'Identifier' || + prop.value.type !== 'Literal' + ) { + return error(); + } + if ( + ['reflect', 'attribute', 'type'].indexOf(prop.key.name) === -1 || + (prop.key.name === 'type' && + ['String', 'Number', 'Boolean', 'Array', 'Object'].indexOf( + /** @type {string} */ (prop.value.value) + ) === -1) || + (prop.key.name === 'reflect' && typeof prop.value.value !== 'boolean') || + (prop.key.name === 'attribute' && typeof prop.value.value !== 'string') + ) { + return error(); + } + component_options.customElement.props[property.key.name][prop.key.name] = + prop.value.value; } - component_options.customElement.props[property.key.name][prop.key.name] = - prop.value.value; } } + const shadow = value[0].expression.properties.find( + /** @param {any} prop */ + (prop) => prop.key.name === 'shadow' + ); + if (shadow) { + const shadowdom = shadow.value?.value; + if (shadowdom !== 'open' && shadowdom !== 'none') { + return component.error(shadow, compiler_errors.invalid_shadow_attribute); + } + component_options.customElement.shadow = shadowdom; + } + break; } - - const shadow = value[0].expression.properties.find( - (prop: any) => prop.key.name === 'shadow' - ); - if (shadow) { - const shadowdom = shadow.value?.value; - - if (shadowdom !== 'open' && shadowdom !== 'none') { - return component.error(shadow, compiler_errors.invalid_shadow_attribute); + case 'namespace': { + const ns = get_value(attribute, compiler_errors.invalid_namespace_attribute); + if (typeof ns !== 'string') { + return component.error(attribute, compiler_errors.invalid_namespace_attribute); } - - component_options.customElement.shadow = shadowdom; + if (valid_namespaces.indexOf(ns) === -1) { + const match = fuzzymatch(ns, valid_namespaces); + return component.error( + attribute, + compiler_errors.invalid_namespace_property(ns, match) + ); + } + component_options.namespace = ns; + break; } - - break; - } - - case 'namespace': { - const ns = get_value(attribute, compiler_errors.invalid_namespace_attribute); - - if (typeof ns !== 'string') { - return component.error(attribute, compiler_errors.invalid_namespace_attribute); + case 'accessors': + case 'immutable': + case 'preserveWhitespace': { + const value = get_value(attribute, compiler_errors.invalid_attribute_value(name)); + if (typeof value !== 'boolean') { + return component.error(attribute, compiler_errors.invalid_attribute_value(name)); + } + component_options[name] = value; + break; } - - if (valid_namespaces.indexOf(ns) === -1) { - const match = fuzzymatch(ns, valid_namespaces); + default: return component.error( attribute, - compiler_errors.invalid_namespace_property(ns, match) + compiler_errors.invalid_options_attribute_unknown(name) ); - } - - component_options.namespace = ns; - break; - } - - case 'accessors': - case 'immutable': - case 'preserveWhitespace': { - const value = get_value(attribute, compiler_errors.invalid_attribute_value(name)); - - if (typeof value !== 'boolean') { - return component.error(attribute, compiler_errors.invalid_attribute_value(name)); - } - - component_options[name] = value; - break; } - - default: - return component.error( - attribute, - compiler_errors.invalid_options_attribute_unknown(name) - ); + } else { + return component.error(attribute, compiler_errors.invalid_options_attribute); } - } else { - return component.error(attribute, compiler_errors.invalid_options_attribute); } - }); + ); } - return component_options; } -function get_relative_path(from: string, to: string) { +/** + * @param {string} from + * @param {string} to + */ +function get_relative_path(from, to) { const from_parts = from.split(/[/\\]/); const to_parts = to.split(/[/\\]/); - from_parts.pop(); // get dirname - while (from_parts[0] === to_parts[0]) { from_parts.shift(); to_parts.shift(); } - if (from_parts.length) { let i = from_parts.length; while (i--) from_parts[i] = '..'; } - return from_parts.concat(to_parts).join('/'); } -function get_basename(filename: string) { +/** @param {string} filename */ +function get_basename(filename) { return filename.split(/[/\\]/).pop(); } -function get_sourcemap_source_filename(compile_options: CompileOptions) { +/** @param {import('../interfaces.js').CompileOptions} compile_options */ +function get_sourcemap_source_filename(compile_options) { if (!compile_options.filename) return null; - return compile_options.outputFilename ? get_relative_path(compile_options.outputFilename, compile_options.filename) : get_basename(compile_options.filename); } + +/** @typedef {Object} ComponentOptions + * @property {string} [namespace] + * @property {boolean} [immutable] + * @property {boolean} [accessors] + * @property {boolean} [preserveWhitespace] + * @property {Object} [customElement] + * @property {string|null} customElement.tag + * @property {'open'|'none'} [customElement.shadow] + * @property {Record} [customElement.props] + */ diff --git a/src/compiler/compile/compiler_errors.ts b/src/compiler/compile/compiler_errors.ts index 40c5758842..02b0f7120a 100644 --- a/src/compiler/compile/compiler_errors.ts +++ b/src/compiler/compile/compiler_errors.ts @@ -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 ` + (is_radio ? ' — for , 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 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