espolar 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Piovium Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # espolar
2
+
3
+ ESTree-compatible AST printer designed for JavaScript/TypeScript language tooling.
4
+
5
+ ## Features
6
+
7
+ - **Source preservation** — untouched nodes (configurable) are printed verbatim from the original source, preserving whitespace, comments, formatting, and ASI safety.
8
+ - **Volar.js mappings** — generates `Mapping<Data>[]` mapping source ranges to generated ranges, with automatic merging of adjacent mappings.
9
+ - **Full TypeScript support** — handles all `@typescript-eslint/types` AST node types (TSESTree), compatible with acorn-typescript parsed ASTs.
10
+ - **Extensible** — every aspect is customizable: untouched detection, mapping data, comments injection, printer overrides per node type.
11
+ - **Zero runtime dependencies** — keeps the library minimal, only 10 kB gzipped.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pnpm add espolar
17
+ ```
18
+
19
+ Requires Node.js >= 26.
20
+
21
+ ## Usage
22
+
23
+ ```ts
24
+ import { print } from "espolar";
25
+ import type { PrintOptions } from "espolar";
26
+
27
+ const ast = /* parse to AST.Program with loc/range */;
28
+
29
+ const result = print(ast, {
30
+ source: originalCode,
31
+ });
32
+
33
+ console.log(result.code); // generated source string
34
+ console.log(result.mappings); // Volar.js Mapping<{}>[]
35
+ ```
36
+
37
+ ### Custom printer
38
+
39
+ ```ts
40
+ import { print } from "espolar";
41
+
42
+ const result = print(ast, {
43
+ source,
44
+ printers: {
45
+ Literal(node, ctx) {
46
+ ctx.write(`"overridden"`);
47
+ },
48
+ },
49
+ });
50
+ ```
51
+
52
+ ### Custom mapping data
53
+
54
+ ```ts
55
+ import { print } from "espolar";
56
+
57
+ const result = print(ast, {
58
+ source,
59
+ getMappingData: (node) => (node ? [node.type] : []),
60
+ combineMappingData: (left, right) => {
61
+ return [...left, ...right];
62
+ },
63
+ });
64
+ ```
65
+
66
+ ## API
67
+
68
+ ### `print(node, options)`
69
+
70
+ Main entry point. Returns `PrintResult<Data>`.
71
+
72
+ #### `PrintOptions<Data>`
73
+
74
+ | Option | Type | Description |
75
+ | --------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------ |
76
+ | `source` | `string` | The original source code (required) |
77
+ | `isUntouched` | `(node) => boolean \| SourceRange` | Determine if a node should be preserved from source. Default: checks `range`/`start`/`end` |
78
+ | `getMappingData` | `(node?) => Data` | Extract data for each mapping entry. Default: `() => ({})` |
79
+ | `combineMappingData` | `(left, right) => Data` | Merge data when adjacent mappings are combined. Default: returns `right` |
80
+ | `printers` | `Printers<Data>` | Override printers for specific `AST_NODE_TYPES` |
81
+ | `getLeadingComments` | `(node) => Comment[] \| undefined` | Return comments to print before a touched node |
82
+ | `getTrailingComments` | `(node) => Comment[] \| undefined` | Return comments to print after a touched node |
83
+
84
+ #### `PrintResult<Data>`
85
+
86
+ | Field | Type | Description |
87
+ | ---------- | ----------------- | ----------------------------------------- |
88
+ | `code` | `string` | The generated source code |
89
+ | `mappings` | `Mapping<Data>[]` | Volar.js source mappings (range-to-range) |
90
+
91
+ ### `PrinterContext<Data>`
92
+
93
+ The context object passed to each printer function.
94
+
95
+ | Method/Property | Description |
96
+ | ------------------------------------------------------- | ----------------------------------------------- |
97
+ | `readonly source: string` | The original source string |
98
+ | `readonly generatedOffset: number` | Current output position |
99
+ | `write(text: string)` | Emit text to the output |
100
+ | `writeNode(node)` | Dispatch to a printer (preserving if untouched) |
101
+ | `writeNodeList(nodes, separator)` | Print a list with explicit separator |
102
+ | `writeNodeListWithSourceGaps(nodes, fallbackSeparator)` | Print a list, copying source gaps between nodes |
103
+ | `writePreservedNode(node)` | Force-preserve a node from source |
104
+ | `writeSource(start, end, data)` | Copy source range and add mapping |
105
+ | `getLeadingComments(node)` | Query leading comments |
106
+ | `getTrailingComments(node)` | Query trailing comments |
107
+
108
+ ### Exported helpers
109
+
110
+ | Export | Description |
111
+ | --------------------------- | ------------------------------------ |
112
+ | `defaultIsUntouched` | Default untouched detection function |
113
+ | `defaultGetMappingData` | Default mapping data extractor |
114
+ | `defaultCombineMappingData` | Default mapping data combiner |
115
+
116
+ ## Architecture
117
+
118
+ The printer works in a single pass:
119
+
120
+ 1. If a node is **untouched** (has source range and `isUntouched` returns truthy), its original source text is copied verbatim with a 1:1 mapping.
121
+ 2. Otherwise, the node is dispatched to its registered **printer function**, which recursively calls `writeNode` on children.
122
+ 3. Adjacent untouched siblings have their mappings **merged** into a single combined range.
123
+
124
+ This design preserves original formatting for unmodified code while only reconstructing the parts that changed.
125
+
126
+ ## Development
127
+
128
+ ```bash
129
+ pnpm install # Install dependencies
130
+ pnpm test # Run tests (vitest)
131
+ pnpm coverage # Run tests with V8 coverage
132
+ pnpm typecheck # TypeScript type checking
133
+ pnpm build # Build via tsdown
134
+ ```
135
+
136
+ Tests use `acorn` + `@sveltejs/acorn-typescript` to parse TypeScript source into AST inputs.
137
+
138
+ ## License
139
+
140
+ MIT
@@ -0,0 +1,99 @@
1
+ import { Mapping } from "@volar/source-map";
2
+ import { AST_NODE_TYPES, TSESTree as AST } from "@typescript-eslint/types";
3
+
4
+ //#region src/types.d.ts
5
+ declare module "@typescript-eslint/types" {
6
+ namespace TSESTree {
7
+ interface NodeOrTokenData {
8
+ start?: number;
9
+ end?: number;
10
+ }
11
+ interface BabelTSFunctionSignatureBase {
12
+ typeAnnotation?: TSTypeAnnotation;
13
+ parameters?: Parameter[];
14
+ }
15
+ interface TSFunctionType extends BabelTSFunctionSignatureBase {}
16
+ interface TSConstructorType extends BabelTSFunctionSignatureBase {}
17
+ interface TSCallSignatureDeclaration extends BabelTSFunctionSignatureBase {}
18
+ interface TSConstructSignatureDeclaration extends BabelTSFunctionSignatureBase {}
19
+ interface TSIndexSignature extends BabelTSFunctionSignatureBase {}
20
+ interface TSMethodSignatureComputedName extends BabelTSFunctionSignatureBase {}
21
+ interface TSMethodSignatureNonComputedName extends BabelTSFunctionSignatureBase {}
22
+ interface BabelTSAbstractBase {
23
+ abstract?: boolean;
24
+ accessor?: boolean;
25
+ }
26
+ interface MethodDefinitionComputedName extends BabelTSAbstractBase {}
27
+ interface MethodDefinitionNonComputedName extends BabelTSAbstractBase {}
28
+ interface PropertyDefinitionComputedName extends BabelTSAbstractBase {}
29
+ interface PropertyDefinitionNonComputedName extends BabelTSAbstractBase {}
30
+ interface BabelClassBase {
31
+ superTypeParameters?: TSTypeParameterInstantiation;
32
+ }
33
+ interface ClassDeclarationWithName extends BabelClassBase {}
34
+ interface ClassDeclarationWithOptionalName extends BabelClassBase {}
35
+ interface ClassExpression extends BabelClassBase {}
36
+ }
37
+ }
38
+ interface NodeLike {
39
+ type: string;
40
+ loc?: AST.SourceLocation | null;
41
+ start?: number;
42
+ end?: number;
43
+ range?: [number, number];
44
+ }
45
+ interface Comment {
46
+ type: "Line" | "Block";
47
+ value: string;
48
+ loc?: AST.SourceLocation;
49
+ start?: number;
50
+ end?: number;
51
+ range?: [number, number];
52
+ }
53
+ //#endregion
54
+ //#region src/mappings.d.ts
55
+ interface SourceRange {
56
+ start: number;
57
+ end: number;
58
+ }
59
+ //#endregion
60
+ //#region src/api.d.ts
61
+ interface PrintResult<Data> {
62
+ code: string;
63
+ mappings: Mapping<Data>[];
64
+ }
65
+ interface PrintOptions<Data> {
66
+ source: string;
67
+ isUntouched?: (node: AST.Node) => boolean | SourceRange;
68
+ getMappingData?: (node?: AST.Node | null) => Data;
69
+ combineMappingData?: (left: Data, right: Data) => Data;
70
+ printers?: Printers<Data>;
71
+ getLeadingComments?: (node: AST.Node) => Comment[] | undefined;
72
+ getTrailingComments?: (node: AST.Node) => Comment[] | undefined;
73
+ }
74
+ interface PrinterContext<Data> {
75
+ readonly source: string;
76
+ readonly generatedOffset: number;
77
+ write(text: string): void;
78
+ writeNode(node: AST.Node | null | undefined): void;
79
+ writeNodeList(nodes: readonly (AST.Node | null | undefined)[], separator: string): void;
80
+ writeNodeListWithSourceGaps(nodes: readonly (AST.Node | null | undefined)[], fallbackSeparator: string): void;
81
+ writePreservedNode(node: AST.Node): void;
82
+ writeSource(start: number, end: number, data: Data): void;
83
+ getLeadingComments: (node: AST.Node) => Comment[] | undefined;
84
+ getTrailingComments: (node: AST.Node) => Comment[] | undefined;
85
+ }
86
+ type NodePrinter<Key extends AST_NODE_TYPES, Data> = (node: Extract<AST.Node, {
87
+ type: Key;
88
+ }>, context: PrinterContext<Data>) => void;
89
+ type Printers<Data> = { [K in AST_NODE_TYPES]?: NodePrinter<K, Data> };
90
+ //#endregion
91
+ //#region src/printer.d.ts
92
+ declare function print<Data>(node: AST.Node, options: PrintOptions<Data>): PrintResult<Data>;
93
+ declare function print<Data>(node: import("estree").Node, options: PrintOptions<Data>): PrintResult<Data>;
94
+ declare function print<Data>(node: NodeLike, options: PrintOptions<Data>): PrintResult<Data>;
95
+ declare function defaultIsUntouched(node: AST.Node): boolean | SourceRange;
96
+ declare function defaultGetMappingData(node?: AST.Node | null): unknown;
97
+ declare function defaultCombineMappingData(left: unknown, right: unknown): unknown;
98
+ //#endregion
99
+ export { type AST, type Comment, type NodeLike, type NodePrinter, type PrintOptions, type PrintResult, type PrinterContext, defaultCombineMappingData, defaultGetMappingData, defaultIsUntouched, print };