sveast 0.4.0 → 0.6.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 CHANGED
@@ -34,7 +34,8 @@ sveast is a drop-in for `parse` in most tools: change the import, and pass `loc:
34
34
  | `loc`, `name_loc` | Always | With `loc: true`; otherwise only `start`/`end` offsets, for a faster parse and a smaller AST |
35
35
  | Errors | `CompileError` | `ParseError`: same `code`, `message`, `position`, `start`, `end` and `frame`; no `filename`; `reason`, the message without its link |
36
36
  | AST formats | Modern, legacy (`modern: false`), error-tolerant (`loose`) | Modern |
37
- | Scope | Parsing, `parseCss`, analysis, compilation | Parsing (`parse`, `parseModule`, `isValidType`) |
37
+ | Scope | Parsing, `parseCss`, analysis, compilation | Parsing (`parse`, `parseModule`, `parseImportsExports`, `isValidType`), walking the AST (`walk`, `visitorKeys`), and finding references and bindings (`isReference`, `extractIdentifiers`) |
38
+ | Non-ASCII identifiers | acorn's tables, Unicode 17 | The engine's own Unicode data, which is smaller to ship: the same as acorn's in Node 24 and Bun; an engine on another Unicode version differs on the letters added in between |
38
39
  | TypeScript-only errors | Reported, e.g. modifier order or initializers in ambient contexts | Not reported: 108 of the 2,449 TypeScript conformance tests acorn-typescript rejects still parse |
39
40
 
40
41
  ## API
@@ -45,6 +46,8 @@ sveast is a drop-in for `parse` in most tools: change the import, and pass `loc:
45
46
  |:---|:---|
46
47
  | `loc` | Add `loc` (line and column) to script and expression nodes, and `name_loc` to elements, attributes and directives, as svelte does. Default `false`: they cost time and memory, and most tools only need `start`/`end`. |
47
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. Default `true`. |
49
+ | `script` | Parse each `<script>`'s JavaScript or TypeScript. With `false`, `instance` and `module` keep their attributes, `start` and `end`, and `content` is a `Program` with its `start` and `end` but an empty `body`; the scripts' comments aren't in `comments`, and their syntax errors aren't reported. Expressions in the markup are still parsed. For a pass that only reads the markup, such as collecting which components a file renders and with what props; on the benchmark corpus, it parses 3.4× faster. Default `true`. |
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 comments in the markup and CSS comments are kept. For tools that never read comments; on Carbon's components, it parses 3 to 6% faster and the ASTs take 8% less memory. Default `true`. |
48
51
 
49
52
  With `loc: true`, the result equals svelte's. With the defaults, it's svelte's without `loc` and `name_loc`.
50
53
 
@@ -52,7 +55,48 @@ A syntax error throws a `ParseError` with svelte's `code` (e.g. `"block_unclosed
52
55
 
53
56
  ### `parseModule(source, options?) => Program`
54
57
 
55
- Parses a JavaScript or TypeScript module, such as a `.ts` file a component imports, the way a component's `<script>` is parsed: estree plus TypeScript nodes, with comments attached as `leadingComments`/`trailingComments`. Options: `typescript` and `loc`, both default `false`.
58
+ Parses a JavaScript or TypeScript module, such as a `.ts` file a component imports, the way a component's `<script>` is parsed: estree plus TypeScript nodes, with comments attached as `leadingComments`/`trailingComments`. Options: `typescript` and `loc`, both default `false`, and `comments`, default `true`; with `false`, no node has `leadingComments` or `trailingComments`.
59
+
60
+ ### `parseImportsExports(source, options?) => ModuleDeclaration[]`
61
+
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, so `start`, `end`, `specifiers` (with `imported`, `local` and `importKind`) and `source` are all there: `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
+
64
+ The rest is skipped by a tokenizer that only tracks strings, comments, templates, regular expressions and brackets, and only the statements it finds are parsed. A syntax error in one of them throws `parseModule`'s `ParseError`, including a name exported twice; errors elsewhere aren't reported.
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`. |
70
+
71
+ On Carbon's 327 component scripts, mostly JSDoc-documented `export let` props, it takes 10.7 ms against `parseModule`'s 33.8 ms, and 4.5 ms with `localExports: false`; the largest script, 63 kB, takes 0.37 ms against 2.7 ms. It checks the TypeScript conformance tests, kit, immich and bits-ui against `parseModule`, with an `import` inserted after every top-level statement: the same nodes on all 24,354 modules.
72
+
73
+ ```ts
74
+ import { parseImportsExports } from "sveast";
75
+
76
+ function dependencies(source: string, typescript = false): string[] {
77
+ return parseImportsExports(source, { typescript, localExports: false }).flatMap(
78
+ (node) => ("source" in node && node.source ? [node.source.value] : []),
79
+ );
80
+ }
81
+ ```
82
+
83
+ ### `createParser(support?) => { parse, parseModule, parseImportsExports }` from `sveast/core`
84
+
85
+ `parse`, `parseModule` and `parseImportsExports` without the two parts most tools can do without: the TypeScript plugin and the table of HTML's named character references. Together they're about a third of a bundle: `parse` and `parseModule` from `sveast` minify to 57.3 kB gzipped, and from `createParser()` to 40.3 kB. Pass back the parts you need; with both, the parsers are the same as `sveast`'s.
86
+
87
+ ```ts
88
+ import { createParser } from "sveast/core";
89
+ import { typescript } from "sveast/typescript";
90
+
91
+ const { parse, parseModule, parseImportsExports } = createParser({ typescript });
92
+ ```
93
+
94
+ | Support | Without it |
95
+ |:---|:---|
96
+ | `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 |
97
+ | `entities` from `sveast/entities` (+10.0 kB gzipped) | Text and attribute values decode numeric references, such as `&#169;`, and `&amp;`, `&apos;`, `&gt;`, `&lt;` and `&quot;`, but leave other names, such as `&copy;`, as written, so `Text.data` differs from svelte's where the source uses them |
98
+
99
+ `sveast/core` also exports `ParseError`, the same class as `sveast`'s, and the types.
56
100
 
57
101
  ### `isValidType(text, options?) => boolean`
58
102
 
@@ -60,9 +104,58 @@ Whether `text` is exactly one TypeScript type, such as a JSDoc `{"sm" | "lg"}` a
60
104
 
61
105
  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 the text ends with a line break. Block comments and `//` inside strings, as in `"http://a" | "https://b"`, are fine either way.
62
106
 
107
+ ### `walk(node, visitor) => void`
108
+
109
+ 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`.
110
+
111
+ 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 }`.
112
+
113
+ `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.
114
+
115
+ ### `isReference(node, parent) => boolean`
116
+
117
+ 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 the [`is-reference`](https://github.com/Rich-Harris/is-reference) package: `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` and `new` in `import.meta` and `new.target`, an import attribute's key, and `b` in `export * as b from`, and for any node that isn't an `Identifier`.
118
+
119
+ TypeScript nodes hold types, so an `Identifier` whose parent is a TypeScript node isn't a reference (`B` in `let a: B`, `typeof b` in a type, interface and type alias names), except where it's a value: the expression of `as`, `satisfies`, `<T>a`, `a!`, `a<T>` and `export =`, an enum member's initializer, a parameter property (`a` in `constructor(private a)`), an enum's name, and the name and target of `import a = b`. Names of namespaces and of functions without a body (overloads, `declare function`) aren't references.
120
+
121
+ acorn shares one `Identifier` between both names of `import { a }` and `export { a }`, and `walk` visits it twice, so it's a reference on both visits.
122
+
123
+ ```ts
124
+ import { isReference, parseModule, walk } from "sveast";
125
+
126
+ const used = new Set<string>();
127
+ walk(parseModule("const a = b.c({ d: e });"), {
128
+ enter(node, parent) {
129
+ if (node.type === "Identifier" && isReference(node, parent)) used.add(node.name);
130
+ },
131
+ });
132
+ used; // Set { "a", "b", "e" }
133
+ ```
134
+
135
+ ### `extractIdentifiers(pattern) => Identifier[]`
136
+
137
+ 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. It returns nodes rather than names, so you keep their positions; `.map((node) => node.name)` for the names.
138
+
139
+ It takes any binding or assignment target: a declarator's `id`, a function's params, including a parameter property (`a` in `constructor(private a)`), a catch clause's `param`, an each block's `context`, a snippet's `parameters`, or an assignment's `left`. A member expression binds nothing, so `[a.b, c] = x` yields only `c`, as in svelte. On an assignment's `left`, `a!`, `a as T`, `a satisfies T` and `<T>a` yield `a`.
140
+
141
+ ```ts
142
+ import { extractIdentifiers, parseModule } from "sveast";
143
+
144
+ const [declaration] = parseModule("let { a, b: [c] } = x;").body;
145
+ if (declaration.type === "VariableDeclaration") {
146
+ declaration.declarations.flatMap((d) => extractIdentifiers(d.id)).map((node) => node.name); // ["a", "c"]
147
+ }
148
+ ```
149
+
150
+ `walk`, `SKIP`, `STOP`, `visitorKeys`, `Visitor`, `isReference`, `extractIdentifiers` and the types are also exported from `sveast/walk`, which loads neither acorn nor the parser: for code that walks ASTs it gets from elsewhere, such as a cache, and must not pay for loading the parser. They're the same values as `sveast`'s.
151
+
152
+ ```ts
153
+ import { STOP, walk } from "sveast/walk";
154
+ ```
155
+
63
156
  ### Types
64
157
 
65
- `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`. `ParseOptions` is exported too.
158
+ `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`. `ParseOptions`, `ParseModuleOptions` and `ParseImportsExportsOptions` are exported too.
66
159
 
67
160
  The estree node types are exported as well (`Program`, `Node`, `Statement`, `Expression`, `Identifier`, ...), so you don't need `@types/estree`. They're estree's, plus what the parser adds: `start`/`end` on every node, and the TypeScript plugin's nodes (`TSInterfaceDeclaration`, `TSTypeAnnotation`, `TSTypeReference`, ...; `TSNode` is their union) and fields (`typeAnnotation`, `typeParameters`, `typeArguments`, `returnType`, `importKind`/`exportKind`, ...). 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`.
68
161
 
@@ -80,28 +173,50 @@ for (const node of parseModule(source, { typescript: true }).body) {
80
173
  **List the components a file renders**, e.g. to build a dependency graph:
81
174
 
82
175
  ```ts
83
- import { parse } from "sveast";
176
+ import { parse, walk } from "sveast";
84
177
 
85
178
  function componentsUsed(source: string): string[] {
86
179
  const names = new Set<string>();
87
- const visit = (node: object): void => {
88
- if (
89
- "type" in node &&
90
- node.type === "Component" &&
91
- "name" in node &&
92
- typeof node.name === "string"
93
- ) {
94
- names.add(node.name);
95
- }
96
- for (const child of Object.values(node)) {
97
- if (typeof child === "object" && child !== null) visit(child);
98
- }
99
- };
100
- visit(parse(source, { css: false }).fragment);
180
+ walk(parse(source, { css: false }).fragment, {
181
+ enter(node) {
182
+ if (node.type === "Component") names.add(node.name);
183
+ },
184
+ });
101
185
  return [...names]; // ["Button", "Modal.Root"]
102
186
  }
103
187
  ```
104
188
 
189
+ **Find the classes a component's styles declare but its markup never uses**, e.g. to report dead CSS. A `class={...}` expression is reported as `dynamic` rather than guessed at:
190
+
191
+ ```ts
192
+ import { parse, walk } from "sveast";
193
+
194
+ function unusedClasses(source: string): { unused: string[]; dynamic: boolean } {
195
+ const ast = parse(source);
196
+ const declared = new Set<string>();
197
+ const used = new Set<string>();
198
+ let dynamic = false;
199
+ if (ast.css) {
200
+ walk(ast.css, {
201
+ enter(node) {
202
+ if (node.type === "ClassSelector") declared.add(node.name);
203
+ },
204
+ });
205
+ }
206
+ walk(ast.fragment, {
207
+ enter(node) {
208
+ if (node.type === "ClassDirective") used.add(node.name);
209
+ if (node.type !== "Attribute" || node.name !== "class") return;
210
+ for (const part of node.value === true ? [] : [node.value].flat()) {
211
+ if (part.type === "ExpressionTag") dynamic = true;
212
+ else for (const name of part.data.split(/\s+/)) if (name) used.add(name);
213
+ }
214
+ },
215
+ });
216
+ return { unused: [...declared].filter((name) => !used.has(name)), dynamic };
217
+ }
218
+ ```
219
+
105
220
  **Read the props a runes component declares:**
106
221
 
107
222
  ```ts
@@ -153,7 +268,7 @@ const css = parse(source, { css: false }).css?.content.styles ?? "";
153
268
  - **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.
154
269
  - **About 2.4× faster than svelte/compiler** (2.2–3.0× depending on the input), and its ASTs retain 47% less memory without `loc`.
155
270
  - **Fast to load.** A fresh process imports sveast in about 5 ms, against about 45 ms for `svelte/compiler`.
156
- - **Zero dependencies,** types included. 64 kB gzipped, acorn included.
271
+ - **Zero dependencies,** types included. 67 kB gzipped, acorn included.
157
272
 
158
273
  ## Benchmarks
159
274