diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 1843f327ec..2bd2e911a2 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -116,6 +116,7 @@ "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", "acorn": "^8.10.0", + "acorn-typescript": "^1.4.11", "aria-query": "^5.3.0", "axobject-query": "^4.0.0", "esm-env": "^1.0.0", diff --git a/packages/svelte/src/compiler/phases/1-parse/acorn.js b/packages/svelte/src/compiler/phases/1-parse/acorn.js index ef18589263..6d202d85a4 100644 --- a/packages/svelte/src/compiler/phases/1-parse/acorn.js +++ b/packages/svelte/src/compiler/phases/1-parse/acorn.js @@ -1,19 +1,32 @@ import * as acorn from 'acorn'; import { walk } from 'zimmerframe'; +import { tsPlugin } from 'acorn-typescript'; +import { strip_types } from './strip-types.js'; + +// @ts-expect-error +const ParserWithTS = acorn.Parser.extend(tsPlugin()); /** * @param {string} source */ export function parse(source) { const { onComment, add_comments } = get_comment_handlers(source); - const ast = acorn.parse(source, { - onComment, - sourceType: 'module', - ecmaVersion: 13, - locations: true - }); + const ast = /** @type {import('estree').Program} */ ( + strip_types( + // @ts-expect-error + ParserWithTS.parse(source, { + onComment, + sourceType: 'module', + ecmaVersion: 13, + locations: true + }) + ) + ); + + // @ts-expect-error add_comments(ast); - return /** @type {import('estree').Program} */ (ast); + + return ast; } /** @@ -22,14 +35,22 @@ export function parse(source) { */ export function parse_expression_at(source, index) { const { onComment, add_comments } = get_comment_handlers(source); - const ast = acorn.parseExpressionAt(source, index, { - onComment, - sourceType: 'module', - ecmaVersion: 13, - locations: true - }); + const ast = /** @type {import('estree').Expression} */ ( + strip_types( + // @ts-expect-error + ParserWithTS.parseExpressionAt(source, index, { + onComment, + sourceType: 'module', + ecmaVersion: 13, + locations: true + }) + ) + ); + + // @ts-expect-error add_comments(ast); - return /** @type {import('estree').Expression} */ (ast); + + return ast; } /** diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index a4a5010c41..be777b003d 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -67,10 +67,55 @@ function open(parser) { if (parser.eat('each')) { parser.require_whitespace(); - const expression = read_expression(parser); + const template = parser.template; + let end = parser.template.length; + + /** @type {import('estree').Expression | undefined} */ + let expression; + + // we have to do this loop because `{#each x as { y = z }}` fails to parse — + // the `as { y = z }` is treated as an Expression but it's actually a Pattern. + // the 'fix' is to backtrack and hide everything from the `as` onwards, until + // we get a valid expression + while (!expression) { + try { + expression = read_expression(parser); + } catch (err) { + end = /** @type {any} */ (err).position[0] - 2; + + while (end > start && parser.template.slice(end, end + 2) !== 'as') { + end -= 1; + } + + if (end <= start) throw err; + + // @ts-expect-error parser.template is meant to be readonly, this is a special case + parser.template = template.slice(0, end); + } + } + + // @ts-expect-error + parser.template = template; + parser.allow_whitespace(); // {#each} blocks must declare a context – {#each list as item} + if (!parser.match('as')) { + // this could be a TypeScript assertion that was erroneously eaten. + + if (expression.type === 'SequenceExpression') { + expression = expression.expressions[0]; + } + + // @ts-expect-error 'TSAsExpression' is not recognized as an estree type + if (expression.type === 'TSAsExpression') { + expression = /** @type {import('estree').Expression} */ ( + /** @type {any} */ (expression).expression + ); + parser.index = /** @type {number} */ (expression.end); + parser.allow_whitespace(); + } + } parser.eat('as', true); parser.require_whitespace(); diff --git a/packages/svelte/src/compiler/phases/1-parse/strip-types.js b/packages/svelte/src/compiler/phases/1-parse/strip-types.js new file mode 100644 index 0000000000..2c4e426b31 --- /dev/null +++ b/packages/svelte/src/compiler/phases/1-parse/strip-types.js @@ -0,0 +1,48 @@ +import { walk } from 'zimmerframe'; +import * as b from '../../utils/builders.js'; + +/** + * @param {import('estree').Node} node + */ +export function strip_types(node) { + return walk(node, null, { + _(node, context) { + // @ts-expect-error + if (node.exportKind === 'type' || node.importKind === 'type') { + console.log('>>>', node); + return b.empty; + } + + // @ts-expect-error + delete node.loc.start.index; + // @ts-expect-error + delete node.loc.end.index; + // // @ts-expect-error + // delete node.returnType; + // // @ts-expect-error + // delete node.importKind; + // // @ts-expect-error + // delete node.exportKind; + // // @ts-expect-error + // delete node.typeAnnotation; + // // @ts-expect-error + // delete node.typeParameters; + + // if (node.type.startsWith('TS')) { + // const ts_node = /** @type {any} */ (node); + + // switch (ts_node.type) { + // case 'TSAsExpression': + // case 'TSNonNullExpression': + // // hack to make sure parser skips over the type assertion + // ts_node.expression.end = ts_node.end; + // return context.visit(ts_node.expression); + // default: + // return b.empty; + // } + // } + + context.next(); + } + }); +} diff --git a/packages/svelte/tests/parser-legacy/samples/dynamic-import/output.json b/packages/svelte/tests/parser-legacy/samples/dynamic-import/output.json index 14ae36ae3a..696f6fcb5d 100644 --- a/packages/svelte/tests/parser-legacy/samples/dynamic-import/output.json +++ b/packages/svelte/tests/parser-legacy/samples/dynamic-import/output.json @@ -27,6 +27,7 @@ "body": [ { "type": "ImportDeclaration", + "importKind": "value", "start": 10, "end": 43, "loc": { @@ -42,6 +43,7 @@ "specifiers": [ { "type": "ImportSpecifier", + "importKind": "value", "start": 19, "end": 26, "loc": { diff --git a/packages/svelte/tests/parser-legacy/samples/each-block-destructured/output.json b/packages/svelte/tests/parser-legacy/samples/each-block-destructured/output.json index 9cbeb2be6c..733e1abe83 100644 --- a/packages/svelte/tests/parser-legacy/samples/each-block-destructured/output.json +++ b/packages/svelte/tests/parser-legacy/samples/each-block-destructured/output.json @@ -196,6 +196,7 @@ "body": [ { "type": "ExportNamedDeclaration", + "exportKind": "value", "start": 10, "end": 29, "loc": { diff --git a/packages/svelte/tests/parser-modern/samples/each-block-object-pattern/input.svelte b/packages/svelte/tests/parser-modern/samples/each-block-object-pattern/input.svelte new file mode 100644 index 0000000000..8ffe8a7287 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/each-block-object-pattern/input.svelte @@ -0,0 +1,3 @@ +{#each people as { name, cool = true }} +
{name} is {cool ? 'cool' : 'not cool'}
+{/each} diff --git a/packages/svelte/tests/parser-modern/samples/each-block-object-pattern/output.json b/packages/svelte/tests/parser-modern/samples/each-block-object-pattern/output.json new file mode 100644 index 0000000000..f89be70f77 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/each-block-object-pattern/output.json @@ -0,0 +1,317 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 94, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "EachBlock", + "start": 0, + "end": 94, + "expression": { + "type": "Identifier", + "start": 7, + "end": 13, + "loc": { + "start": { + "line": 1, + "column": 7 + }, + "end": { + "line": 1, + "column": 13 + } + }, + "name": "people" + }, + "body": { + "type": "Fragment", + "nodes": [ + { + "type": "Text", + "start": 39, + "end": 41, + "raw": "\n\t", + "data": "\n\t" + }, + { + "type": "RegularElement", + "start": 41, + "end": 86, + "name": "p", + "attributes": [], + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "ExpressionTag", + "start": 44, + "end": 50, + "expression": { + "type": "Identifier", + "start": 45, + "end": 49, + "loc": { + "start": { + "line": 2, + "column": 5 + }, + "end": { + "line": 2, + "column": 9 + } + }, + "name": "name" + } + }, + { + "type": "Text", + "start": 50, + "end": 54, + "raw": " is ", + "data": " is " + }, + { + "type": "ExpressionTag", + "start": 54, + "end": 82, + "expression": { + "type": "ConditionalExpression", + "start": 55, + "end": 81, + "loc": { + "start": { + "line": 2, + "column": 15 + }, + "end": { + "line": 2, + "column": 41 + } + }, + "test": { + "type": "Identifier", + "start": 55, + "end": 59, + "loc": { + "start": { + "line": 2, + "column": 15 + }, + "end": { + "line": 2, + "column": 19 + } + }, + "name": "cool" + }, + "consequent": { + "type": "Literal", + "start": 62, + "end": 68, + "loc": { + "start": { + "line": 2, + "column": 22 + }, + "end": { + "line": 2, + "column": 28 + } + }, + "value": "cool", + "raw": "'cool'" + }, + "alternate": { + "type": "Literal", + "start": 71, + "end": 81, + "loc": { + "start": { + "line": 2, + "column": 31 + }, + "end": { + "line": 2, + "column": 41 + } + }, + "value": "not cool", + "raw": "'not cool'" + } + } + } + ], + "transparent": true + } + }, + { + "type": "Text", + "start": 86, + "end": 87, + "raw": "\n", + "data": "\n" + } + ], + "transparent": false + }, + "context": { + "type": "ObjectPattern", + "start": 17, + "end": 38, + "loc": { + "start": { + "line": 1, + "column": 17 + }, + "end": { + "line": 1, + "column": 38 + } + }, + "properties": [ + { + "type": "Property", + "start": 19, + "end": 23, + "loc": { + "start": { + "line": 1, + "column": 19 + }, + "end": { + "line": 1, + "column": 23 + } + }, + "method": false, + "shorthand": true, + "computed": false, + "key": { + "type": "Identifier", + "start": 19, + "end": 23, + "loc": { + "start": { + "line": 1, + "column": 19 + }, + "end": { + "line": 1, + "column": 23 + } + }, + "name": "name" + }, + "kind": "init", + "value": { + "type": "Identifier", + "start": 19, + "end": 23, + "loc": { + "start": { + "line": 1, + "column": 19 + }, + "end": { + "line": 1, + "column": 23 + } + }, + "name": "name" + } + }, + { + "type": "Property", + "start": 25, + "end": 36, + "loc": { + "start": { + "line": 1, + "column": 25 + }, + "end": { + "line": 1, + "column": 36 + } + }, + "method": false, + "shorthand": true, + "computed": false, + "key": { + "type": "Identifier", + "start": 25, + "end": 29, + "loc": { + "start": { + "line": 1, + "column": 25 + }, + "end": { + "line": 1, + "column": 29 + } + }, + "name": "cool" + }, + "kind": "init", + "value": { + "type": "AssignmentPattern", + "start": 25, + "end": 36, + "loc": { + "start": { + "line": 1, + "column": 25 + }, + "end": { + "line": 1, + "column": 36 + } + }, + "left": { + "type": "Identifier", + "start": 25, + "end": 29, + "loc": { + "start": { + "line": 1, + "column": 25 + }, + "end": { + "line": 1, + "column": 29 + } + }, + "name": "cool" + }, + "right": { + "type": "Literal", + "start": 32, + "end": 36, + "loc": { + "start": { + "line": 1, + "column": 32 + }, + "end": { + "line": 1, + "column": 36 + } + }, + "value": true, + "raw": "true" + } + } + } + ] + } + } + ], + "transparent": false + }, + "options": null +} diff --git a/packages/svelte/tsconfig.json b/packages/svelte/tsconfig.json index 2f90cc693e..dc8c2d134b 100644 --- a/packages/svelte/tsconfig.json +++ b/packages/svelte/tsconfig.json @@ -10,6 +10,7 @@ "noErrorTruncation": true, "allowSyntheticDefaultImports": true, "verbatimModuleSyntax": true, + "skipLibCheck": true, "types": ["node"], "strict": true, "allowJs": true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1dffc51f17..f1f8c91fd0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: acorn: specifier: ^8.10.0 version: 8.11.2 + acorn-typescript: + specifier: ^1.4.11 + version: 1.4.11(acorn@8.11.2) aria-query: specifier: ^5.3.0 version: 5.3.0 @@ -2746,6 +2749,14 @@ packages: acorn: 8.11.2 dev: true + /acorn-typescript@1.4.11(acorn@8.11.2): + resolution: {integrity: sha512-cRGgp+4HMxMZAiMS61ZmQ3iuU/+A4g4ZYZsyLZdmvrEVN/TOwfJ40rPWcLqi3H5ut75SYAdOOJj6QGCcrkK57w==} + peerDependencies: + acorn: '>=8.9.0' + dependencies: + acorn: 8.11.2 + dev: false + /acorn-walk@8.3.0: resolution: {integrity: sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==} engines: {node: '>=0.4.0'}