sveast 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +197 -174
- package/core.d.ts +1 -1
- package/core.js +1 -1
- package/entities.js +1 -1
- package/index.d.ts +195 -2
- package/index.js +1 -1
- package/is-valid-type.js +1 -1
- package/lexer.d.ts +88 -0
- package/lexer.js +2 -2
- package/module.d.ts +1243 -0
- package/module.js +1 -0
- package/package.json +7 -2
- package/parse-imports-exports.js +1 -1
- package/parse-module.js +1 -1
- package/parse-sections.js +1 -0
- package/parse.js +1 -1
- package/runes.js +1 -0
- package/shared-1qapk0wt.js +2 -0
- package/shared-25z9p1fv.js +10 -0
- package/shared-3yc3kmnb.js +2 -0
- package/shared-40v3xx3k.js +2 -0
- package/shared-4gksznp7.js +3 -0
- package/shared-96tm9k91.js +2 -0
- package/shared-bxg3hhpk.js +5 -0
- package/shared-g9knrb0x.js +2 -0
- package/shared-k5pfyxpa.js +2 -0
- package/shared-np5jnnrw.js +2 -0
- package/shared-pnv8wmzz.js +3 -0
- package/shared-r8yddmex.js +2 -0
- package/shared-rf0vnv09.js +2 -0
- package/shared-rfa6cx21.js +3 -0
- package/shared-wentwa9g.js +2 -0
- package/shared-yqw5faw6.js +2 -0
- package/shared-zd01zsyh.js +10 -0
- package/typescript.js +1 -1
- package/walk.d.ts +80 -2
- package/walk.js +1 -1
- package/shared-1xjsv3h6.js +0 -2
- package/shared-as9can03.js +0 -10
- package/shared-b37zemjd.js +0 -2
- package/shared-drsxcq8p.js +0 -2
- package/shared-g16dt48m.js +0 -2
- package/shared-gyydaea2.js +0 -3
- package/shared-mwp38arc.js +0 -2
- package/shared-pmkdz7sc.js +0 -3
- package/shared-pt7hcxxy.js +0 -2
- package/shared-pyjcz2f8.js +0 -12
- package/shared-qnfwr59k.js +0 -2
- package/shared-yr6e6h72.js +0 -2
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# sveast
|
|
2
2
|
|
|
3
|
-
> Zero-dependency Svelte 5 parser
|
|
3
|
+
> Zero-dependency Svelte 5 parser: svelte/compiler's AST, 2.4× faster.
|
|
4
4
|
|
|
5
|
-
Parse `.svelte` components, including `<script lang="ts">`, into the same AST, and the same errors, as `svelte/compiler`'s `parse(source, { modern: true })`. sveast
|
|
5
|
+
Parse `.svelte` components, including `<script lang="ts">`, into the same AST, and the same errors, as `svelte/compiler`'s `parse(source, { modern: true })`. sveast has no dependencies and uses no Node APIs, so it runs in the browser as well as in Node, Bun and Deno.
|
|
6
|
+
|
|
7
|
+
It's for tools that analyze components statically, such as documentation and type generators that read a component's props, events and slots. They need only the AST, but parse every component in a library, often on every change. [sveld](https://github.com/carbon-design-system/sveld), which generates TypeScript definitions and documentation for Svelte components, uses it.
|
|
6
8
|
|
|
7
9
|
```sh
|
|
8
10
|
bun i sveast
|
|
@@ -17,6 +19,21 @@ ast.instance?.content.body; // [VariableDeclaration]
|
|
|
17
19
|
ast.fragment.nodes; // [Text, RegularElement]
|
|
18
20
|
```
|
|
19
21
|
|
|
22
|
+
| Function | For | Import from |
|
|
23
|
+
|:---|:---|:---|
|
|
24
|
+
| [`parse`](#parsesource-options--astroot) | A component's AST, the same as svelte's | `sveast` |
|
|
25
|
+
| [`parseSections`](#parsesectionssource-options--astroot) | A component's AST without the markup: its `<script>`s, `<style>` and `<svelte:options>` | `sveast` |
|
|
26
|
+
| [`parseModule`](#parsemodulesource-options--program) | A `.js` or `.ts` module's AST, the same as a component's `<script>` | `sveast` |
|
|
27
|
+
| [`parseImportsExports`](#parseimportsexportssource-options--moduledeclaration) | A module's `import` and `export` statements, without parsing the rest | `sveast` |
|
|
28
|
+
| [`lexImportsExports`](#leximportsexportssource--lexedstatement) | Their offsets, sources and names, without loading a parser | `sveast/lexer` |
|
|
29
|
+
| [`lexComponent`](#lexcomponentsource--lexedcomponent) | A component's `<script>`s, `<style>` and `<svelte:options>`, with their attributes and offsets, without loading a parser | `sveast/lexer` |
|
|
30
|
+
| [`createParser`, `createModuleParser`](#smaller-bundles-sveastcore-and-sveastmodule) | The parsers without TypeScript, named HTML entities or the template parser, for a smaller bundle | `sveast/core`, `sveast/module` |
|
|
31
|
+
| [`isValidType`](#isvalidtypetext-options--boolean) | Whether a JSDoc type is safe to copy into a `.d.ts` | `sveast` |
|
|
32
|
+
| [`isRunesMode`](#isrunesmodesource--boolean) | Whether svelte compiles a component in runes mode, mostly without parsing it | `sveast` |
|
|
33
|
+
| [`walk`, `visitorKeys`, `markupVisitorKeys`](#walknode-visitor-keys--void) | Visit, skip, stop at or replace nodes, in the whole AST or only the markup | `sveast`, `sveast/walk` |
|
|
34
|
+
| [`isReference`](#isreferencenode-parent--boolean), [`extractIdentifiers`](#extractidentifierspattern--identifier) | Which identifiers are references, and which ones a pattern declares | `sveast`, `sveast/walk` |
|
|
35
|
+
| [`createLocator`](#createlocatorsource--offset--position) | Lines and columns for the nodes you report, without parsing with `loc` | `sveast`, `sveast/walk` |
|
|
36
|
+
|
|
20
37
|
## Migrating from svelte/compiler
|
|
21
38
|
|
|
22
39
|
sveast is a drop-in for `parse` in most tools: change the import, and pass `loc: true` if you read line and column numbers.
|
|
@@ -34,41 +51,105 @@ sveast is a drop-in for `parse` in most tools: change the import, and pass `loc:
|
|
|
34
51
|
| `loc`, `name_loc` | Always | With `loc: true`; otherwise only `start`/`end` offsets, for a faster parse and a smaller AST |
|
|
35
52
|
| Errors | `CompileError` | `ParseError`: same `code`, `message`, `position`, `start`, `end` and `frame`; no `filename`; `reason`, the message without its link |
|
|
36
53
|
| AST formats | Modern, legacy (`modern: false`), error-tolerant (`loose`) | Modern |
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
|
|
54
|
+
| Non-ASCII identifiers | Its own Unicode 17 tables | The JavaScript engine's Unicode data, which is smaller to ship: the same in Node 24 and Bun; an engine on another Unicode version differs on the letters added in between |
|
|
55
|
+
| TypeScript-only errors | Reported, e.g. modifier order or initializers in ambient contexts | Not reported: code that only TypeScript's own checks reject still parses |
|
|
56
|
+
|
|
57
|
+
## Examples
|
|
58
|
+
|
|
59
|
+
**List the components a file renders**, e.g. to build a dependency graph:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { parse, walk } from "sveast";
|
|
63
|
+
|
|
64
|
+
function componentsUsed(source: string): string[] {
|
|
65
|
+
const names = new Set<string>();
|
|
66
|
+
walk(parse(source, { css: false }).fragment, {
|
|
67
|
+
enter(node) {
|
|
68
|
+
if (node.type === "Component") names.add(node.name);
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
return [...names]; // ["Button", "Modal.Root"]
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Report syntax errors**, e.g. in a pre-commit check, with svelte's own messages:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { ParseError, parse } from "sveast";
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
parse(source, { css: false });
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (!(error instanceof ParseError)) throw error;
|
|
84
|
+
console.error(`${file}:${error.start?.line}:${error.start?.column} ${error.code}\n${error.frame}`);
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
[`examples/`](examples) has runnable versions of these and more, each a CLI you can point at your own components: a module graph, unused imports and variables, unused CSS classes, runes-or-legacy detection, `$props()` and their defaults, `.d.ts` props from JSDoc, and styles without parsing them.
|
|
89
|
+
|
|
90
|
+
## Parity and performance
|
|
91
|
+
|
|
92
|
+
- **svelte/compiler parity.** Tested on over 12,000 components: carbon-components-svelte, svelte's own test suite, bits-ui, shadcn-svelte, skeleton, flowbite-svelte, svelte.dev, immich, SvelteKit, melt-ui, layerchart and paneforge. Every one either parses to svelte's AST, `loc` included, or throws svelte's error with the same code, message, position and frame. A differential fuzzer compares the two parsers on mutated components.
|
|
93
|
+
- **TypeScript.** `<script lang="ts">` and `.ts` modules parse to svelte's AST, key order and `loc` included, on 6,604 real-world modules and on all but 2 of the 9,586 TypeScript conformance tests svelte parses.
|
|
94
|
+
- **About 2.4× faster** than svelte/compiler, and its ASTs retain about half the memory.
|
|
95
|
+
- **A smaller bundle and a faster load:** `parse` is less than half of svelte/compiler's size, and a fresh process imports it in about 5 ms against 45 ms.
|
|
96
|
+
|
|
97
|
+
Apple M2, medians of warm calls. The corpus is `tests/corpus`: all of carbon-components-svelte plus svelte's and sveld's test inputs.
|
|
98
|
+
|
|
99
|
+
| Input | sveast | sveast, `loc: true` | svelte/compiler |
|
|
100
|
+
|:---|:---|:---|:---|
|
|
101
|
+
| Corpus, 400 components, 1.5 MB | **64.7 ms** | 84.0 ms | 155 ms (2.4×) |
|
|
102
|
+
| Carbon's 5 largest components, 318 kB | **13.8 ms** | | 31.3 ms (2.3×) |
|
|
103
|
+
| `lang="ts"` components, 22 files | **0.79 ms** | | 2.40 ms (3.0×) |
|
|
104
|
+
|
|
105
|
+
| `parse` alone, minified for the browser | Gzipped |
|
|
106
|
+
|:---|:---|
|
|
107
|
+
| svelte/compiler | 126.7 kB |
|
|
108
|
+
| sveast | **58.5 kB** |
|
|
109
|
+
| `sveast/core`'s `createParser()`, without TypeScript and named HTML entities | **41.0 kB** |
|
|
110
|
+
|
|
111
|
+
In a fresh process, importing the parser takes 5.3 ms with sveast and 44.7 ms with `svelte/compiler`, and a first parse of the whole corpus takes 65 ms against 161 ms. Keeping ten parses of the corpus alive retains 147 MB with sveast, 260 MB with `loc: true`, and 294 MB with `svelte/compiler`.
|
|
40
112
|
|
|
41
113
|
## API
|
|
42
114
|
|
|
43
115
|
### `parse(source, options?) => AST.Root`
|
|
44
116
|
|
|
45
|
-
| Option | Description |
|
|
46
|
-
|
|
47
|
-
| `loc` | Add `loc` (line and column) to script and expression nodes, and `name_loc` to elements, attributes and directives, as svelte does.
|
|
48
|
-
| `css` | Parse `<style>` into rules and selectors. With `false`, `css` keeps its `start`, `end` and `content`, but `children` and `comments` are empty and CSS syntax errors aren't reported.
|
|
49
|
-
| `script` | Parse each `<script
|
|
50
|
-
| `comments` | Collect JavaScript comments, in scripts, expressions and tags. With `false`, `comments` is empty and no node has `leadingComments` or `trailingComments`, including the HTML comment before a `<script>` that svelte copies into its `content.leadingComments`. HTML
|
|
117
|
+
| Option | Default | Description |
|
|
118
|
+
|:---|:---|:---|
|
|
119
|
+
| `loc` | `false` | Add `loc` (line and column) to script and expression nodes, and `name_loc` to elements, attributes and directives, as svelte does. They cost time and memory, and most tools only need `start`/`end`. |
|
|
120
|
+
| `css` | `true` | Parse `<style>` into rules and selectors. With `false`, `css` keeps its `start`, `end` and `content`, but `children` and `comments` are empty and CSS syntax errors aren't reported. It also keeps `<style lang="scss">` from failing to parse as CSS. |
|
|
121
|
+
| `script` | `true` | Parse each `<script>`. With `false`, `instance` and `module` keep their attributes, `start` and `end`, and `content` is a `Program` with an empty `body`; their comments aren't collected and their syntax errors aren't reported. Expressions in the markup are still parsed. For a pass that only reads the markup; 3.4× faster on the benchmark corpus. |
|
|
122
|
+
| `comments` | `true` | Collect JavaScript comments, in scripts, expressions and tags. With `false`, `comments` is empty and no node has `leadingComments` or `trailingComments`, including the HTML comment before a `<script>` that svelte copies into its `content.leadingComments`. HTML and CSS comments are kept. 3 to 6% faster, and 8% less memory. |
|
|
51
123
|
|
|
52
124
|
With `loc: true`, the result equals svelte's. With the defaults, it's svelte's without `loc` and `name_loc`.
|
|
53
125
|
|
|
54
|
-
A syntax error throws a `ParseError` with svelte's `code` (e.g. `"block_unclosed"`), `message`, `position` (`[start, end]` offsets), `start`/`end` (`{ line, column, character }`) and `frame` (the source around the error). `message` is always the reason, a line break, then the link to the error's docs, `https://svelte.dev/e/${code}
|
|
126
|
+
A syntax error throws a `ParseError` with svelte's `code` (e.g. `"block_unclosed"`), `message`, `position` (`[start, end]` offsets), `start`/`end` (`{ line, column, character }`) and `frame` (the source around the error). `message` is always the reason, a line break, then the link to the error's docs, `https://svelte.dev/e/${code}`; `reason` is the reason on its own, e.g. `"Unexpected token"`.
|
|
127
|
+
|
|
128
|
+
### `parseSections(source, options?) => AST.Root`
|
|
129
|
+
|
|
130
|
+
`parse` for a pass that only reads a component's scripts, such as its props or its module script's exports: the top-level `<script>`s, `<style>` and `<svelte:options>` are parsed as `parse` parses them, and the markup between them is skipped as [`lexComponent`](#lexcomponentsource--lexedcomponent) skips it. `fragment.nodes` is empty, `comments` has only the scripts' comments, and the markup's syntax errors aren't reported; the rest is `parse`'s, offsets and the HTML comment before a `<script>` included. It takes `parse`'s options, and on the benchmark corpus it's 1.5× faster, where most of what's left is the scripts. It's a function of its own so that a bundle with only `parse` doesn't carry the lexer; `createParser` doesn't return it.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { parseSections } from "sveast";
|
|
134
|
+
|
|
135
|
+
const { instance, module } = parseSections(source, { css: false });
|
|
136
|
+
module?.content.body; // the module script's statements, with parse's offsets
|
|
137
|
+
```
|
|
55
138
|
|
|
56
139
|
### `parseModule(source, options?) => Program`
|
|
57
140
|
|
|
58
|
-
Parses a JavaScript or TypeScript module, such as a `.ts` file a component imports, the
|
|
141
|
+
Parses a JavaScript or TypeScript module, such as a `.ts` file a component imports, into the same AST a component's `<script>` gets, with comments attached as `leadingComments`/`trailingComments`. Options: `typescript` and `loc`, default `false`, and `comments`, default `true`.
|
|
59
142
|
|
|
60
143
|
### `parseImportsExports(source, options?) => ModuleDeclaration[]`
|
|
61
144
|
|
|
62
|
-
A module's top-level `import` and `export` statements, without parsing the rest, e.g. to rewrite a component's imports or follow a module graph. Each node is the one `parseModule(source, { comments: false })` has for that statement
|
|
145
|
+
A module's top-level `import` and `export` statements, without parsing the rest, e.g. to rewrite a component's imports or follow a module graph. Each node is the one `parseModule(source, { comments: false })` has for that statement: `ImportDeclaration`, `ExportNamedDeclaration`, `ExportDefaultDeclaration` and `ExportAllDeclaration`, and, in TypeScript, `TSImportEqualsDeclaration`, `TSExportAssignment` and `TSNamespaceExportDeclaration`. `import(...)` and `import.meta` are expressions, so they aren't returned. Unlike a regex, it never matches an `import` in a comment, a string, a template or a nested block such as `declare module "a" { ... }`.
|
|
63
146
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
| Option | Description |
|
|
67
|
-
|:---|:---|
|
|
68
|
-
| `typescript` | Parse TypeScript: `import type`, `export type`, `import a = require("a")`. Default `false`. |
|
|
69
|
-
| `localExports` | Return the exports of the module's own bindings, such as `export let a`, `export function b() {}`, `export { c }` and `export default d`, which are parsed in full. With `false`, only imports and the exports that have a `from` (`export * from "e"`, `export { f } from "f"`, `export import`) are returned, and the rest is skipped. Default `true`. |
|
|
147
|
+
It stops after the last `import` or `export` that could start a statement, so the code after a component's imports is never read: on Carbon's component scripts, it takes a third of `parseModule`'s time, and about a seventh with `localExports: false`. A syntax error in a statement it returns throws a `ParseError`, including a name exported twice; errors elsewhere aren't reported.
|
|
70
148
|
|
|
71
|
-
|
|
149
|
+
| Option | Default | Description |
|
|
150
|
+
|:---|:---|:---|
|
|
151
|
+
| `typescript` | `false` | Parse TypeScript: `import type`, `export type`, `import a = require("a")`. |
|
|
152
|
+
| `localExports` | `true` | Return the exports of the module's own bindings, such as `export let a`, `export function b() {}`, `export { c }` and `export default d`, parsed in full. With `false`, only imports and the exports that have a `from` (`export * from "e"`, `export { f } from "f"`, `export import`) are returned. |
|
|
72
153
|
|
|
73
154
|
```ts
|
|
74
155
|
import { parseImportsExports } from "sveast";
|
|
@@ -80,11 +161,11 @@ function dependencies(source: string, typescript = false): string[] {
|
|
|
80
161
|
}
|
|
81
162
|
```
|
|
82
163
|
|
|
83
|
-
### `lexImportsExports(source) => LexedStatement[]`
|
|
164
|
+
### `lexImportsExports(source) => LexedStatement[]`
|
|
84
165
|
|
|
85
|
-
A module's top-level `import` statements and `export … from` re-exports, read by hand instead of parsed
|
|
166
|
+
From `sveast/lexer`, and also `sveast`. A module's top-level `import` statements and `export … from` re-exports, read by hand instead of parsed, for a tool on a hot or eagerly loaded path, such as a preprocessor that rewrites a component's imports, that needs each statement's offsets, source and names, but not the AST. `sveast/lexer` loads no parser, so a fresh process imports it in 1.4 ms, and on Carbon's component scripts it takes 40% of the time `parseImportsExports` takes with `localExports: false`.
|
|
86
167
|
|
|
87
|
-
It finds
|
|
168
|
+
It finds the statements `parseImportsExports` returns with `localExports: false`, with the same offsets, sources and names. It doesn't check syntax and never throws: a statement it can't read, such as `import { a from "a"` or `import a = require("a")`, has `source: null` and no specifiers, and ends where it stopped reading. `import type` and `type` before a name are always read.
|
|
88
169
|
|
|
89
170
|
```ts
|
|
90
171
|
import { lexImportsExports } from "sveast/lexer";
|
|
@@ -108,213 +189,155 @@ for (const statement of lexImportsExports(source)) {
|
|
|
108
189
|
| `*` | | `all`, `local: "*"`, `exported: null` |
|
|
109
190
|
| `b as c`, `"b-c" as d` | `named`, `imported: "b"`, `local: "c"` | `named`, `local: "b"`, `exported: "c"` |
|
|
110
191
|
|
|
111
|
-
Names are as the module sees them: escapes decoded, and a string name
|
|
192
|
+
Names are as the module sees them: escapes decoded, and a string name without its quotes. A specifier's `typeOnly` is also `true` in an `import type` or `export type`. Its `start` is that of `type` before the name, if any, and `end` is after the last name; for `export *`, after the `*`.
|
|
112
193
|
|
|
113
|
-
|
|
194
|
+
### `lexComponent(source) => LexedComponent`
|
|
114
195
|
|
|
115
|
-
|
|
196
|
+
From `sveast/lexer`, and also `sveast`. A component's top-level `<script>`, `<script module>`, `<style>` and `<svelte:options>`, with their attributes and the offsets of their tags and content, found without parsing: e.g. to read a component's scripts and their imports, its language or its options, or to rewrite a script, on a path that can't afford a parser. On the benchmark corpus it's about 50× faster than `parse`.
|
|
116
197
|
|
|
117
|
-
|
|
198
|
+
It skips the markup by tracking only tags, attribute values, blocks, and the brackets, strings, comments and templates of each `{…}` expression, so a `<script>` in `<svelte:head>`, a block, an attribute, an expression or an HTML comment isn't one, and it stops after the last `<script`, `<style` or `<svelte:options`. Where `parse` accepts the component, the sections are its `instance`, `module`, `css` and `options`, with the same offsets, into the source without a leading byte order mark. It doesn't check syntax and never throws.
|
|
118
199
|
|
|
119
200
|
```ts
|
|
120
|
-
import {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
201
|
+
import { lexComponent } from "sveast/lexer";
|
|
202
|
+
|
|
203
|
+
const { typescript, instance, module, css, options } = lexComponent(source);
|
|
204
|
+
typescript; // whether `parse` reads it as TypeScript, decided by the first <script> with a lang
|
|
205
|
+
instance?.content; // { start, end }: source.slice(start, end) is the script
|
|
206
|
+
instance?.attributes; // [{ name: "lang", value: "ts", start, end }]
|
|
207
|
+
module?.context; // "module"
|
|
208
|
+
options?.attributes; // [{ name: "runes", value: "{true}", start, end }]
|
|
124
209
|
```
|
|
125
210
|
|
|
126
|
-
|
|
127
|
-
|:---|:---|
|
|
128
|
-
| `typescript` from `sveast/typescript` (+8.8 kB gzipped) | A component with `<script lang="ts">`, or `parseModule` or `parseImportsExports` with `typescript: true`, throws an `Error`, not a `ParseError`: a missing import, not a syntax error, and never a JavaScript AST of TypeScript |
|
|
129
|
-
| `entities` from `sveast/entities` (+10.0 kB gzipped) | Text and attribute values decode numeric references, such as `©`, and `&`, `'`, `>`, `<` and `"`, but leave other names, such as `©`, as written, so `Text.data` differs from svelte's where the source uses them |
|
|
130
|
-
|
|
131
|
-
`sveast/core` also exports `ParseError`, the same class as `sveast`'s, and the types.
|
|
132
|
-
|
|
133
|
-
### `isValidType(text, options?) => boolean`
|
|
211
|
+
An attribute's `value` is as written, without its quotes: character references aren't decoded, an expression keeps its braces, and an attribute without a value has `true`.
|
|
134
212
|
|
|
135
|
-
|
|
213
|
+
`typescript` is what `createParser` needs to know to load the TypeScript parser only for the components that use it:
|
|
136
214
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
Visits `node` and every node under it, depth-first and in source order, calling `visitor.enter(node, parent, key, index)` before a node's children and `visitor.leave(node, parent, key, index)` after them. `parent[key]` is the node, or `parent[key][index]` when the field is an array; all three are `null` for the node you pass. Return `SKIP` from `enter` to skip the node's children; `leave` is still called, so a stack you push in `enter` and pop in `leave` stays balanced. Return `STOP` from `enter` or `leave` to end the walk there: no more `enter` or `leave` calls, not even `leave` on the node's ancestors, e.g. to find the first use of a rune. Return another node from `enter` to put it in this one's place: `walk` writes it to `parent[key]` (or `parent[key][index]`), then calls `enter` on it and visits its children instead, and `leave` gets the replacement. So `return node.expression` on each `TSAsExpression` strips `a as B as C` down to `a`, one layer per `enter`. The node you pass to `walk` can't be replaced. Any other return value, `false` included, is ignored, so an expression-bodied `enter: (node) => node.type === "Component" && names.add(node.name)` still visits every node. Checking `node.type` narrows `node`.
|
|
142
|
-
|
|
143
|
-
It works on any node the parsers return: a component's `Root`, a `Fragment`, an expression, a `parseModule` program, a `<style>`'s rules. It only reads the fields that hold child nodes, so it never descends into `loc`, comments or strings, and it throws on a node type it doesn't know. Two exceptions to source order: a component's sections are visited in scope order, `module`, `instance`, `fragment`, then `css`; and a template literal's `quasis` come before its `expressions`. `Root.options` isn't a node, so `<svelte:options>`'s attributes aren't visited; read them from `ast.options?.attributes`. acorn shares one `Identifier` between both names of `import { a }`, so it's visited once as `imported` and once as `local`, and likewise as `local` and `exported` in `export { a }`.
|
|
144
|
-
|
|
145
|
-
`visitorKeys` is the table `walk` reads: the fields of each node type that hold child nodes, in source order, e.g. `visitorKeys.IfBlock` is `["test", "consequent", "alternate"]`. Use it with another walker, or to write your own.
|
|
215
|
+
```ts
|
|
216
|
+
import { createParser } from "sveast/core";
|
|
217
|
+
import { lexComponent } from "sveast/lexer";
|
|
146
218
|
|
|
147
|
-
|
|
219
|
+
let parser = createParser();
|
|
220
|
+
let loaded = false;
|
|
148
221
|
|
|
149
|
-
|
|
222
|
+
async function parseComponent(source: string) {
|
|
223
|
+
if (!loaded && lexComponent(source).typescript) {
|
|
224
|
+
const { typescript } = await import("sveast/typescript");
|
|
225
|
+
parser = createParser({ typescript });
|
|
226
|
+
loaded = true;
|
|
227
|
+
}
|
|
228
|
+
return parser.parse(source);
|
|
229
|
+
}
|
|
230
|
+
```
|
|
150
231
|
|
|
151
|
-
|
|
232
|
+
### Smaller bundles: `sveast/core` and `sveast/module`
|
|
152
233
|
|
|
153
|
-
|
|
234
|
+
`createParser(support?)` from `sveast/core` returns `parse`, `parseModule` and `parseImportsExports` without the TypeScript parser and the table of HTML's named character references, which most tools can do without. `createModuleParser(support?)` from `sveast/module` returns `parseModule` and `parseImportsExports` without the template parser either, for tools that read `.js` and `.ts` files but never components, such as a bundler plugin following a module graph. Pass back the parts you need; with them, the parsers are the same as `sveast`'s.
|
|
154
235
|
|
|
155
236
|
```ts
|
|
156
|
-
import {
|
|
237
|
+
import { createParser } from "sveast/core";
|
|
238
|
+
import { typescript } from "sveast/typescript";
|
|
157
239
|
|
|
158
|
-
const
|
|
159
|
-
walk(parseModule("const a = b.c({ d: e });"), {
|
|
160
|
-
enter(node, parent) {
|
|
161
|
-
if (node.type === "Identifier" && isReference(node, parent)) used.add(node.name);
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
used; // Set { "a", "b", "e" }
|
|
240
|
+
const { parse, parseModule, parseImportsExports } = createParser({ typescript });
|
|
165
241
|
```
|
|
166
242
|
|
|
167
|
-
|
|
243
|
+
| Support | Without it |
|
|
244
|
+
|:---|:---|
|
|
245
|
+
| `typescript` from `sveast/typescript` | A component with `<script lang="ts">`, or `typescript: true`, throws an `Error`, not a `ParseError`: a missing import, not a syntax error, and never a JavaScript AST of TypeScript |
|
|
246
|
+
| `entities` from `sveast/entities` (`createParser` only) | Numeric references, such as `©`, and `&`, `'`, `>`, `<` and `"` are decoded, but other names, such as `©`, are left as written, so `Text.data` differs from svelte's where the source uses them |
|
|
168
247
|
|
|
169
|
-
|
|
248
|
+
Both also export `ParseError`, the same class as `sveast`'s, and the node types.
|
|
170
249
|
|
|
171
|
-
|
|
250
|
+
### `isValidType(text, options?) => boolean`
|
|
172
251
|
|
|
173
|
-
|
|
174
|
-
import { extractIdentifiers, parseModule } from "sveast";
|
|
252
|
+
Whether `text` is exactly one TypeScript type, such as a JSDoc `{"sm" | "lg"}` a tool is about to copy into a `.d.ts`. The text is parsed as a type on its own, not wrapped in a statement, so a `;`, a line break or a `}` in it can't end the type and smuggle in a statement: `string; let x = 1` and `{ a: string } }` are `false`. Whitespace and comments around the type are allowed. Results aren't cached.
|
|
175
253
|
|
|
176
|
-
|
|
177
|
-
if (declaration.type === "VariableDeclaration") {
|
|
178
|
-
declaration.declarations.flatMap((d) => extractIdentifiers(d.id)).map((node) => node.name); // ["a", "c"]
|
|
179
|
-
}
|
|
180
|
-
```
|
|
254
|
+
A `//` comment runs to the end of the line, so `string // the size` is a valid type but breaks `CustomEvent<${text}>`. Pass `inline: true` when the text goes before more code on the same line: it's then also `false` unless every `//` comment in it ends with a line break.
|
|
181
255
|
|
|
182
|
-
|
|
256
|
+
### `isRunesMode(source) => boolean`
|
|
183
257
|
|
|
184
|
-
|
|
185
|
-
import { STOP, walk } from "sveast/walk";
|
|
186
|
-
```
|
|
258
|
+
Whether svelte compiles the component in runes mode, as `compile(source).metadata.runes` says: by `<svelte:options runes>` if it has one, otherwise by whether it uses a rune, such as `$state` or `$props`, or `await` outside a function in its instance script or markup. A `$state` that subscribes to a store named `state`, or that a function declares, isn't a rune, as in svelte. E.g. to track a migration to Svelte 5, or to pick the rules a linter applies.
|
|
187
259
|
|
|
188
|
-
|
|
260
|
+
Most components are decided without parsing: `<svelte:options>` is read with `lexComponent`, a component without a rune's name or `await` isn't in runes mode, and one whose scripts call a rune in a way that can't be a declaration, a method, a type or a store is. The rest is parsed, without the markup unless a rune's name or `await` is in it, and a syntax error there throws a `ParseError`. On 12,808 components from other Svelte projects, it takes a twelfth of the time parsing them does.
|
|
189
261
|
|
|
190
|
-
|
|
262
|
+
### `walk(node, visitor, keys?) => void`
|
|
191
263
|
|
|
192
|
-
|
|
264
|
+
Visits `node` and every node under it, depth-first and in source order, calling `visitor.enter(node, parent, key, index)` before a node's children and `visitor.leave(node, parent, key, index)` after them. `parent[key]` is the node, or `parent[key][index]` when the field is an array; all three are `null` for the node you pass. Checking `node.type` narrows `node`.
|
|
193
265
|
|
|
194
|
-
|
|
195
|
-
import { parseModule, type TSInterfaceDeclaration } from "sveast";
|
|
266
|
+
What `enter` and `leave` return controls the walk:
|
|
196
267
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
```
|
|
268
|
+
- **`SKIP`** from `enter` skips the node's children. `leave` is still called, so a stack you push in `enter` and pop in `leave` stays balanced.
|
|
269
|
+
- **`STOP`** from `enter` or `leave` ends the walk there, without `leave` on the node's ancestors, e.g. to find the first use of a rune.
|
|
270
|
+
- **A node** from `enter` replaces this one: `walk` writes it to `parent[key]` (or `parent[key][index]`), then enters it and visits its children instead, and `leave` gets the replacement. `return node.expression` on each `TSAsExpression` strips `a as B as C` down to `a`. The node you pass to `walk` can't be replaced.
|
|
271
|
+
- **Anything else**, `false` included, is ignored, so `enter: (node) => node.type === "Component" && names.add(node.name)` still visits every node.
|
|
202
272
|
|
|
203
|
-
|
|
273
|
+
It works on any node the parsers return: a `Root`, a `Fragment`, an expression, a `Program`, a `<style>`'s rules. It never descends into `loc`, comments or strings, and throws on a node type it doesn't know. A component's sections are visited in scope order, `module`, `instance`, `fragment`, then `css`, and a template literal's `quasis` before its `expressions`. `<svelte:options>`'s attributes aren't visited; read them from `ast.options?.attributes`. In `import { a }` and `export { a }`, one `Identifier` is both names, so it's visited twice.
|
|
204
274
|
|
|
205
|
-
|
|
275
|
+
`visitorKeys` is the table `walk` reads: the fields of each node type that hold child nodes, in source order, e.g. `visitorKeys.IfBlock` is `["test", "consequent", "alternate"]`. Pass another table as `keys` to visit other fields; a node whose type it has no entry for throws. `markupVisitorKeys` visits only the markup: `Root`'s `fragment`, elements' attributes and fragments, attribute values and blocks' fragments, but no script, style or expression. For a query about the markup, such as the components a file renders or the classes it uses, it's 4.8× faster on the benchmark corpus than walking everything:
|
|
206
276
|
|
|
207
277
|
```ts
|
|
208
|
-
import {
|
|
278
|
+
import { markupVisitorKeys, walk } from "sveast";
|
|
209
279
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
280
|
+
walk(
|
|
281
|
+
ast,
|
|
282
|
+
{
|
|
213
283
|
enter(node) {
|
|
214
284
|
if (node.type === "Component") names.add(node.name);
|
|
215
285
|
},
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
286
|
+
},
|
|
287
|
+
markupVisitorKeys,
|
|
288
|
+
);
|
|
219
289
|
```
|
|
220
290
|
|
|
221
|
-
|
|
291
|
+
`sveast/walk` exports `walk`, `SKIP`, `STOP`, `visitorKeys`, `markupVisitorKeys`, `isReference`, `extractIdentifiers`, `createLocator` and the types without loading a parser, for code that walks ASTs from elsewhere, such as a cache.
|
|
222
292
|
|
|
223
|
-
|
|
224
|
-
import { parse, walk } from "sveast";
|
|
293
|
+
### `isReference(node, parent) => boolean`
|
|
225
294
|
|
|
226
|
-
function
|
|
227
|
-
const ast = parse(source);
|
|
228
|
-
const declared = new Set<string>();
|
|
229
|
-
const used = new Set<string>();
|
|
230
|
-
let dynamic = false;
|
|
231
|
-
if (ast.css) {
|
|
232
|
-
walk(ast.css, {
|
|
233
|
-
enter(node) {
|
|
234
|
-
if (node.type === "ClassSelector") declared.add(node.name);
|
|
235
|
-
},
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
walk(ast.fragment, {
|
|
239
|
-
enter(node) {
|
|
240
|
-
if (node.type === "ClassDirective") used.add(node.name);
|
|
241
|
-
if (node.type !== "Attribute" || node.name !== "class") return;
|
|
242
|
-
for (const part of node.value === true ? [] : [node.value].flat()) {
|
|
243
|
-
if (part.type === "ExpressionTag") dynamic = true;
|
|
244
|
-
else for (const name of part.data.split(/\s+/)) if (name) used.add(name);
|
|
245
|
-
}
|
|
246
|
-
},
|
|
247
|
-
});
|
|
248
|
-
return { unused: [...declared].filter((name) => !used.has(name)), dynamic };
|
|
249
|
-
}
|
|
250
|
-
```
|
|
295
|
+
Whether `node` is an `Identifier` that names a variable, function, class, import or other binding, where it's declared or where it's used, given the `parent` `walk` passes. It's the check svelte's analyzer makes, from [`is-reference`](https://github.com/Rich-Harris/is-reference): `false` for a property or method name (`b` in `a.b`, `{ b: a }` and `class { b() {} }`, unless computed), the renamed side of an import or export (`b` in `import { b as a }` and `export { a as b }`), and labels. Unlike `is-reference`, it's also `false` for `import.meta` and `new.target`, an import attribute's key, and `b` in `export * as b from`.
|
|
251
296
|
|
|
252
|
-
|
|
297
|
+
In TypeScript, a name in a type isn't a reference (`B` in `let a: B`, interface and type alias names), but a value is: the expression of `as`, `satisfies`, `<T>a`, `a!`, `a<T>` and `export =`, an enum's name and member initializers, a parameter property (`a` in `constructor(private a)`), and both sides of `import a = b`. Names of namespaces and of functions without a body (overloads, `declare function`) aren't references.
|
|
253
298
|
|
|
254
299
|
```ts
|
|
255
|
-
import {
|
|
300
|
+
import { isReference, parseModule, walk } from "sveast";
|
|
256
301
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
init.callee.name === "$props";
|
|
265
|
-
if (isProps && id.type === "ObjectPattern") {
|
|
266
|
-
return id.properties.flatMap((p) =>
|
|
267
|
-
p.type === "Property" && p.key.type === "Identifier" ? [p.key.name] : [],
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
return [];
|
|
273
|
-
}
|
|
302
|
+
const used = new Set<string>();
|
|
303
|
+
walk(parseModule("const a = b.c({ d: e });"), {
|
|
304
|
+
enter(node, parent) {
|
|
305
|
+
if (node.type === "Identifier" && isReference(node, parent)) used.add(node.name);
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
used; // Set { "a", "b", "e" }
|
|
274
309
|
```
|
|
275
310
|
|
|
276
|
-
|
|
311
|
+
### `extractIdentifiers(pattern) => Identifier[]`
|
|
312
|
+
|
|
313
|
+
The `Identifier` nodes a pattern binds, in source order, as svelte's `extract_identifiers` returns them: `a`, `c`, `d`, `e` and `f` in `let { a, b: [c, ...d], e = 1, ...f } = x`. Property keys, defaults and type annotations aren't included, and a member expression binds nothing, so `[a.b, c] = x` yields only `c`. It takes any binding or assignment target: a declarator's `id`, a function's params, including a parameter property, a catch clause's `param`, an each block's `context`, a snippet's `parameters`, or an assignment's `left`, where `a!`, `a as T`, `a satisfies T` and `<T>a` yield `a`.
|
|
277
314
|
|
|
278
315
|
```ts
|
|
279
|
-
import {
|
|
316
|
+
import { extractIdentifiers, parseModule } from "sveast";
|
|
280
317
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (!(error instanceof ParseError)) throw error;
|
|
285
|
-
console.error(`${file}:${error.start?.line}:${error.start?.column} ${error.code}\n${error.frame}`);
|
|
318
|
+
const [declaration] = parseModule("let { a, b: [c] } = x;").body;
|
|
319
|
+
if (declaration.type === "VariableDeclaration") {
|
|
320
|
+
declaration.declarations.flatMap((d) => extractIdentifiers(d.id)).map((node) => node.name); // ["a", "c"]
|
|
286
321
|
}
|
|
287
322
|
```
|
|
288
323
|
|
|
289
|
-
|
|
324
|
+
### `createLocator(source) => (offset) => Position`
|
|
325
|
+
|
|
326
|
+
A function from an offset to its `{ line, column }`, line from 1 and column from 0, as `loc: true` gives them: for a tool that parses without `loc`, which is faster, and needs lines only for what it reports. Lines are found on the first call, and each lookup starts its search at the line found last, so lookups in source order are fastest. Lines end at `\n`, as in the markup's `loc` and `ParseError`; acorn's `loc` in scripts and modules also ends them at a lone `\r`, U+2028 and U+2029. `parse` drops a leading byte order mark, so pass the source without one.
|
|
290
327
|
|
|
291
328
|
```ts
|
|
292
|
-
import { parse } from "sveast";
|
|
329
|
+
import { createLocator, parse, walk } from "sveast";
|
|
293
330
|
|
|
294
|
-
const
|
|
331
|
+
const locate = createLocator(source);
|
|
332
|
+
walk(parse(source), {
|
|
333
|
+
enter(node) {
|
|
334
|
+
if (node.type === "Component") console.log(node.name, locate(node.start).line);
|
|
335
|
+
},
|
|
336
|
+
});
|
|
295
337
|
```
|
|
296
338
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
- **svelte/compiler parity.** Tested on over 12,000 components: carbon-components-svelte, svelte's own test suite, bits-ui, shadcn-svelte, skeleton, flowbite-svelte, svelte.dev, immich, SvelteKit, melt-ui, layerchart and paneforge. Every one either parses to svelte's AST, `loc` included, or throws svelte's error with the same code, message, position and frame. CI runs 442 of them, the smallest set that covers every parser line, AST shape and error code the full set does, and a differential fuzzer compares the two parsers on mutated components.
|
|
300
|
-
- **TypeScript without acorn-typescript.** The built-in plugin produces the same AST as `@sveltejs/acorn-typescript`, key order and `loc` included, on 6,604 real-world modules and on all but 2 of the 9,586 TypeScript conformance tests that acorn-typescript parses. It rejects the same redeclarations, and supports decorators.
|
|
301
|
-
- **About 2.4× faster than svelte/compiler** (2.2–3.0× depending on the input), and its ASTs retain 47% less memory without `loc`.
|
|
302
|
-
- **Fast to load.** A fresh process imports sveast in about 5 ms, against about 45 ms for `svelte/compiler`.
|
|
303
|
-
- **Zero dependencies,** types included. 67 kB gzipped, acorn included.
|
|
304
|
-
|
|
305
|
-
## Benchmarks
|
|
306
|
-
|
|
307
|
-
Apple M2, medians of warm calls. Each task parses every file in the set once. The corpus is `tests/corpus`: all of carbon-components-svelte plus svelte's and sveld's test inputs.
|
|
308
|
-
|
|
309
|
-
| Input | sveast | sveast, `loc: true` | svelte/compiler |
|
|
310
|
-
|:---|:---|:---|:---|
|
|
311
|
-
| Corpus, 400 components, 1.5 MB | **64.7 ms** | 84.0 ms | 155 ms (2.4×) |
|
|
312
|
-
| Carbon's 5 largest components, 318 kB | **13.8 ms** | | 31.3 ms (2.3×) |
|
|
313
|
-
| `lang="ts"` components, 22 files | **0.79 ms** | | 2.40 ms (3.0×) |
|
|
339
|
+
### Types
|
|
314
340
|
|
|
315
|
-
|
|
316
|
-
|:---|:---|:---|
|
|
317
|
-
| Carbon's 98 `.d.ts` modules | **3.19 ms** | 8.07 ms (2.5×) |
|
|
318
|
-
| The corpus's `lang="ts"` scripts on their own | **0.58 ms** | 1.29 ms (2.2×) |
|
|
341
|
+
`AST` is svelte's `AST` namespace (`AST.Root`, `AST.RegularElement`, `AST.CSS.Rule`, ...), corrected to match what the parser returns: `name_loc` and a comment's `loc` are optional, `Root.instance`/`module` are absent rather than `null` when there's no such `<script>`, `Root.js` is declared, and every directive has `modifiers`. Each function's options and results are exported too, e.g. `ParseOptions` and `LexedStatement`.
|
|
319
342
|
|
|
320
|
-
|
|
343
|
+
The script's node types are exported as well (`Program`, `Node`, `Statement`, `Expression`, `Identifier`, ...), so you don't need a separate types package. They include `start`/`end` on every node, and the TypeScript nodes (`TSInterfaceDeclaration`, `TSTypeAnnotation`, ...; `TSNode` is their union) and fields (`typeAnnotation`, `typeParameters`, `returnType`, `importKind`, ...). The TypeScript nodes are in the `Statement`, `Declaration` and `Expression` unions, so checking `node.type` narrows to them. Where the grammar only allows a string, such as an import's `source`, the type is `StringLiteral`, a `Literal` whose `value` is a `string`.
|
package/core.d.ts
CHANGED
|
@@ -109,7 +109,7 @@ export interface ParseImportsExportsOptions {
|
|
|
109
109
|
localExports?: boolean;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
/** TypeScript support for {@link createParser}, from `sveast/typescript`. */
|
|
112
|
+
/** TypeScript support for {@link createParser} and {@link createModuleParser}, from `sveast/typescript`. */
|
|
113
113
|
export interface TypeScriptSupport {
|
|
114
114
|
readonly support: "typescript";
|
|
115
115
|
}
|
package/core.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import"./shared-40v3xx3k.js";import"./shared-bxg3hhpk.js";import"./shared-zd01zsyh.js";import{e}from"./shared-25z9p1fv.js";import{me}from"./shared-k5pfyxpa.js";import{D}from"./shared-4gksznp7.js";import"./shared-rfa6cx21.js";import{g}from"./shared-yqw5faw6.js";import{b}from"./shared-r8yddmex.js";import{U,ht}from"./shared-wentwa9g.js";function u(o={}){let p=U(o.typescript),s={typescript:p,entityNames:ht(o.entities)??me};return{parse:(t,r)=>D(t,r,s),parseModule:(t,r={})=>g(t,r,p),parseImportsExports:(t,r={})=>b(t,r,p)}}export{e as ParseError,u as createParser};
|
package/entities.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./shared-
|
|
1
|
+
import"./shared-k5pfyxpa.js";import{x}from"./shared-3yc3kmnb.js";import{ct}from"./shared-wentwa9g.js";var p=ct(x);export{p as entities};
|