espolar 0.2.0 → 0.2.1
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/package.json +3 -2
- package/src/api.ts +88 -0
- package/src/index.ts +14 -0
- package/src/mappings.ts +85 -0
- package/src/printer.ts +284 -0
- package/src/printers.ts +2276 -0
- package/src/types.ts +61 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "espolar",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
|
-
"dist"
|
|
14
|
+
"dist",
|
|
15
|
+
"src"
|
|
15
16
|
],
|
|
16
17
|
"license": "MIT",
|
|
17
18
|
"repository": {
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Mapping } from "@volar/source-map";
|
|
2
|
+
import type { AST, AST_NODE_TYPES, Comment } from "./types.ts";
|
|
3
|
+
import type { SourceRange } from "./mappings.ts";
|
|
4
|
+
|
|
5
|
+
export interface PrintResult<Data> {
|
|
6
|
+
code: string;
|
|
7
|
+
mappings: Mapping<Data>[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PrintOptionsBase<Data> {
|
|
11
|
+
source: string;
|
|
12
|
+
isUntouched?: (node: AST.Node) => boolean | SourceRange;
|
|
13
|
+
combineMappingData?: (left: Data, right: Data) => Data;
|
|
14
|
+
printers?: Printers<Data>;
|
|
15
|
+
getLeadingComments?: (node: AST.Node) => Comment[] | undefined;
|
|
16
|
+
getTrailingComments?: (node: AST.Node) => Comment[] | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Provide additional source range for the left parenthesis of `CallExpression` and `NewExpression`.
|
|
19
|
+
* This is useful for language tools that want to provide signature hints when user enter `(`.
|
|
20
|
+
*
|
|
21
|
+
* @notes This hook will not interact with parentheses around the callee.
|
|
22
|
+
*
|
|
23
|
+
* @param node The `CallExpression` or `NewExpression` node.
|
|
24
|
+
* @returns The source range of the left parenthesis, `undefined` if not available.
|
|
25
|
+
*/
|
|
26
|
+
experimentalGetLeftParenSourceRange?: (
|
|
27
|
+
node: AST.CallExpression | AST.NewExpression,
|
|
28
|
+
) => SourceRange | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface MappingDataOptions<Data> {
|
|
32
|
+
getMappingData: (node?: AST.Node | null) => Data;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type MappingDataUndefinedOptions = {
|
|
36
|
+
[K in keyof MappingDataOptions<0>]?: undefined;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type PrintOptions<Data> = PrintOptionsBase<Data> &
|
|
40
|
+
([Data] extends [undefined]
|
|
41
|
+
? MappingDataUndefinedOptions
|
|
42
|
+
: MappingDataOptions<Data>);
|
|
43
|
+
|
|
44
|
+
export interface PrinterContext<Data = any> {
|
|
45
|
+
readonly options: PrintOptions<Data>;
|
|
46
|
+
readonly source: string;
|
|
47
|
+
readonly generatedOffset: number;
|
|
48
|
+
write(text: string): void;
|
|
49
|
+
writeMapped(
|
|
50
|
+
text: string,
|
|
51
|
+
sourceStart: number,
|
|
52
|
+
sourceEnd: number,
|
|
53
|
+
data?: Data,
|
|
54
|
+
): void;
|
|
55
|
+
writeNode(node: AST.Node | null | undefined): void;
|
|
56
|
+
writeNodeList(
|
|
57
|
+
nodes: readonly (AST.Node | null | undefined)[],
|
|
58
|
+
separator: string,
|
|
59
|
+
): void;
|
|
60
|
+
writeNodeListWithSourceGaps(
|
|
61
|
+
nodes: readonly (AST.Node | null | undefined)[],
|
|
62
|
+
fallbackSeparator: string,
|
|
63
|
+
): void;
|
|
64
|
+
writeSource(start: number, end: number, data?: Data): void;
|
|
65
|
+
writePreservedNode(node: AST.Node): void;
|
|
66
|
+
appendMapping(
|
|
67
|
+
sourceRange: SourceRange,
|
|
68
|
+
generatedStart: number,
|
|
69
|
+
generatedEnd: number,
|
|
70
|
+
data?: Data,
|
|
71
|
+
): void;
|
|
72
|
+
/** Extra mappings that won't be merged automatically */
|
|
73
|
+
createExtraMapping(
|
|
74
|
+
sourceRange: SourceRange,
|
|
75
|
+
generatedStart: number,
|
|
76
|
+
generatedEnd: number,
|
|
77
|
+
data?: Data,
|
|
78
|
+
): void;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type NodePrinter<Key extends AST_NODE_TYPES, Data> = (
|
|
82
|
+
node: Extract<AST.Node, { type: Key }>,
|
|
83
|
+
context: PrinterContext<Data>,
|
|
84
|
+
) => void;
|
|
85
|
+
|
|
86
|
+
export type Printers<Data> = {
|
|
87
|
+
[K in AST_NODE_TYPES]?: NodePrinter<K, Data>;
|
|
88
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export {
|
|
2
|
+
defaultCombineMappingData,
|
|
3
|
+
defaultIsUntouched,
|
|
4
|
+
print,
|
|
5
|
+
} from "./printer.ts";
|
|
6
|
+
export { defaultPrinters } from "./printers.ts";
|
|
7
|
+
export type {
|
|
8
|
+
NodePrinter,
|
|
9
|
+
PrintOptions,
|
|
10
|
+
PrintResult,
|
|
11
|
+
PrinterContext,
|
|
12
|
+
} from "./api.ts";
|
|
13
|
+
export type { AST, NodeLike, Comment } from "./types.ts";
|
|
14
|
+
export type { SourceRange } from "./mappings.ts";
|
package/src/mappings.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Mapping } from "@volar/source-map";
|
|
2
|
+
import type { AST } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export interface InternalMapping<Data = unknown> {
|
|
5
|
+
sourceStart: number;
|
|
6
|
+
sourceEnd: number;
|
|
7
|
+
generatedStart: number;
|
|
8
|
+
generatedEnd: number;
|
|
9
|
+
data: Data;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SourceRange {
|
|
13
|
+
start: number;
|
|
14
|
+
end: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getNodeRange(node: AST.Node): SourceRange | undefined {
|
|
18
|
+
if (Array.isArray(node.range) && node.range.length === 2) {
|
|
19
|
+
const [start, end] = node.range;
|
|
20
|
+
if (Number.isInteger(start) && Number.isInteger(end) && start <= end) {
|
|
21
|
+
return { start, end };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const { start, end } = node;
|
|
25
|
+
if (
|
|
26
|
+
typeof start === "number" &&
|
|
27
|
+
typeof end === "number" &&
|
|
28
|
+
Number.isInteger(start) &&
|
|
29
|
+
Number.isInteger(end) &&
|
|
30
|
+
start <= end
|
|
31
|
+
) {
|
|
32
|
+
return { start, end };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function pushMapping<Data>(
|
|
39
|
+
mappings: InternalMapping<Data>[],
|
|
40
|
+
mapping: InternalMapping<Data>,
|
|
41
|
+
combineMappingData: (left: Data, right: Data) => Data,
|
|
42
|
+
): void {
|
|
43
|
+
const previous = mappings.at(-1);
|
|
44
|
+
if (previous && canMerge(previous, mapping)) {
|
|
45
|
+
previous.sourceEnd = mapping.sourceEnd;
|
|
46
|
+
previous.generatedEnd = mapping.generatedEnd;
|
|
47
|
+
previous.data = combineMappingData(previous.data, mapping.data);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
mappings.push(mapping);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Two adjacent mappings with identical lengths can be merged into one mapping.
|
|
56
|
+
* @param left
|
|
57
|
+
* @param right
|
|
58
|
+
* @returns
|
|
59
|
+
*/
|
|
60
|
+
function canMerge<Data>(
|
|
61
|
+
left: InternalMapping<Data>,
|
|
62
|
+
right: InternalMapping<Data>,
|
|
63
|
+
): boolean {
|
|
64
|
+
return (
|
|
65
|
+
left.sourceEnd - left.sourceStart === left.generatedEnd - left.generatedStart &&
|
|
66
|
+
right.sourceEnd - right.sourceStart === right.generatedEnd - right.generatedStart &&
|
|
67
|
+
left.sourceEnd === right.sourceStart &&
|
|
68
|
+
left.generatedEnd === right.generatedStart
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function toVolarMapping<Data>(
|
|
73
|
+
mapping: InternalMapping<Data>,
|
|
74
|
+
): Mapping<Data> {
|
|
75
|
+
const sourceLength = mapping.sourceEnd - mapping.sourceStart;
|
|
76
|
+
const generatedLength = mapping.generatedEnd - mapping.generatedStart;
|
|
77
|
+
return {
|
|
78
|
+
sourceOffsets: [mapping.sourceStart],
|
|
79
|
+
generatedOffsets: [mapping.generatedStart],
|
|
80
|
+
lengths: [sourceLength],
|
|
81
|
+
generatedLengths:
|
|
82
|
+
generatedLength !== sourceLength ? [generatedLength] : undefined,
|
|
83
|
+
data: mapping.data,
|
|
84
|
+
};
|
|
85
|
+
}
|
package/src/printer.ts
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NodePrinter,
|
|
3
|
+
PrintOptions,
|
|
4
|
+
PrintResult,
|
|
5
|
+
PrinterContext,
|
|
6
|
+
Printers,
|
|
7
|
+
} from "./api.ts";
|
|
8
|
+
import { defaultPrinters } from "./printers.ts";
|
|
9
|
+
import type { AST, AST_NODE_TYPES, Comment, NodeLike } from "./types.ts";
|
|
10
|
+
import {
|
|
11
|
+
getNodeRange,
|
|
12
|
+
pushMapping,
|
|
13
|
+
toVolarMapping,
|
|
14
|
+
type InternalMapping,
|
|
15
|
+
type SourceRange,
|
|
16
|
+
} from "./mappings.ts";
|
|
17
|
+
|
|
18
|
+
interface InternalPrinterContext extends PrinterContext<any> {
|
|
19
|
+
// make typescript happy about complex types
|
|
20
|
+
options: any;
|
|
21
|
+
result(): PrintResult<any>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writeComment(context: InternalPrinterContext, comment: Comment): void {
|
|
25
|
+
if (comment.type === "Line") {
|
|
26
|
+
context.write("//" + comment.value + "\n");
|
|
27
|
+
} else {
|
|
28
|
+
context.write("/*" + comment.value + "*/");
|
|
29
|
+
if (comment.value.includes("\n")) {
|
|
30
|
+
context.write("\n");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function print<Data = undefined>(
|
|
36
|
+
node: AST.Node,
|
|
37
|
+
options: PrintOptions<Data>,
|
|
38
|
+
): PrintResult<Data>;
|
|
39
|
+
export function print<Data = undefined>(
|
|
40
|
+
node: import("estree").Node,
|
|
41
|
+
options: PrintOptions<Data>,
|
|
42
|
+
): PrintResult<Data>;
|
|
43
|
+
export function print<Data = undefined>(
|
|
44
|
+
node: NodeLike,
|
|
45
|
+
options: PrintOptions<Data>,
|
|
46
|
+
): PrintResult<Data>;
|
|
47
|
+
export function print<Data = undefined>(
|
|
48
|
+
node: unknown,
|
|
49
|
+
options: PrintOptions<Data>,
|
|
50
|
+
): PrintResult<Data> {
|
|
51
|
+
const context = createPrinterContext(options);
|
|
52
|
+
context.writeNode(node as AST.Node);
|
|
53
|
+
return context.result();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function defaultIsUntouched(node: AST.Node): boolean | SourceRange {
|
|
57
|
+
return getNodeRange(node) || false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function defaultCombineMappingData<T>(left: T, right: T): T {
|
|
61
|
+
if (left === right) {
|
|
62
|
+
return left;
|
|
63
|
+
}
|
|
64
|
+
throw new Error(
|
|
65
|
+
"Cannot combine mapping data with different values when no custom combineMappingData function is provided",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function createPrinterContext<Data>(
|
|
70
|
+
options: PrintOptions<Data>,
|
|
71
|
+
): InternalPrinterContext {
|
|
72
|
+
const chunks: string[] = [];
|
|
73
|
+
const mappings: InternalMapping<Data>[] = [];
|
|
74
|
+
const extraMappings: InternalMapping<Data>[] = [];
|
|
75
|
+
let generatedOffset = 0;
|
|
76
|
+
|
|
77
|
+
const isUntouched = options.isUntouched ?? defaultIsUntouched;
|
|
78
|
+
const getMappingData = options.getMappingData ?? ((): any => undefined);
|
|
79
|
+
const combineMappingData =
|
|
80
|
+
options.combineMappingData ?? defaultCombineMappingData;
|
|
81
|
+
const printers: Printers<Data> = {
|
|
82
|
+
...defaultPrinters,
|
|
83
|
+
...options.printers,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const appendMapping = (
|
|
87
|
+
sourceRange: SourceRange,
|
|
88
|
+
generatedStart: number,
|
|
89
|
+
generatedEnd: number,
|
|
90
|
+
data: Data,
|
|
91
|
+
) => {
|
|
92
|
+
pushMapping(
|
|
93
|
+
mappings,
|
|
94
|
+
{
|
|
95
|
+
sourceStart: sourceRange.start,
|
|
96
|
+
sourceEnd: sourceRange.end,
|
|
97
|
+
generatedStart,
|
|
98
|
+
generatedEnd,
|
|
99
|
+
data,
|
|
100
|
+
},
|
|
101
|
+
combineMappingData,
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const context: InternalPrinterContext = {
|
|
106
|
+
options: options,
|
|
107
|
+
source: options.source,
|
|
108
|
+
get generatedOffset() {
|
|
109
|
+
return generatedOffset;
|
|
110
|
+
},
|
|
111
|
+
write(text) {
|
|
112
|
+
if (text.length === 0) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
chunks.push(text);
|
|
116
|
+
generatedOffset += text.length;
|
|
117
|
+
},
|
|
118
|
+
writeMapped(text, sourceStart, sourceEnd, data) {
|
|
119
|
+
if (text.length === 0 || sourceEnd < sourceStart) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const generatedStart = generatedOffset;
|
|
123
|
+
context.write(text);
|
|
124
|
+
appendMapping(
|
|
125
|
+
{ start: sourceStart, end: sourceEnd },
|
|
126
|
+
generatedStart,
|
|
127
|
+
generatedOffset,
|
|
128
|
+
data ?? getMappingData(null),
|
|
129
|
+
);
|
|
130
|
+
},
|
|
131
|
+
writeNode(node) {
|
|
132
|
+
if (!node) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const range = getNodeRange(node);
|
|
137
|
+
const untouchedRet = isUntouched(node);
|
|
138
|
+
if (untouchedRet) {
|
|
139
|
+
const sourceRange = untouchedRet === true ? range : untouchedRet;
|
|
140
|
+
if (!sourceRange) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Node of type ${node.type} is marked as untouched but does not have valid source offsets`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
context.writeSource(
|
|
146
|
+
sourceRange.start,
|
|
147
|
+
sourceRange.end,
|
|
148
|
+
getMappingData(node),
|
|
149
|
+
);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const printer = printers[node.type] as
|
|
154
|
+
| NodePrinter<AST_NODE_TYPES, Data>
|
|
155
|
+
| undefined;
|
|
156
|
+
if (!printer) {
|
|
157
|
+
throw new Error(`No printer registered for node type ${node.type}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const leadingComments = options.getLeadingComments?.(node);
|
|
161
|
+
if (leadingComments) {
|
|
162
|
+
for (const c of leadingComments) {
|
|
163
|
+
writeComment(context, c);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const generatedStart = generatedOffset;
|
|
168
|
+
printer(node, context);
|
|
169
|
+
const generatedEnd = generatedOffset;
|
|
170
|
+
|
|
171
|
+
const trailingComments = options.getTrailingComments?.(node);
|
|
172
|
+
if (trailingComments) {
|
|
173
|
+
for (const c of trailingComments) {
|
|
174
|
+
writeComment(context, c);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// If children nodes don't emit any mapping but the parent node itself
|
|
178
|
+
// can produce mapping, add that mapping
|
|
179
|
+
const lastMappingGeneratedEnd = mappings.at(-1)?.generatedEnd ?? 0;
|
|
180
|
+
if (range && lastMappingGeneratedEnd <= generatedStart) {
|
|
181
|
+
appendMapping(
|
|
182
|
+
range,
|
|
183
|
+
generatedStart,
|
|
184
|
+
generatedEnd,
|
|
185
|
+
getMappingData(node),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
writeNodeList(nodes, separator) {
|
|
190
|
+
let needsSeparator = false;
|
|
191
|
+
for (const node of nodes) {
|
|
192
|
+
if (!node) {
|
|
193
|
+
if (needsSeparator) {
|
|
194
|
+
context.write(separator);
|
|
195
|
+
}
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (needsSeparator) {
|
|
200
|
+
context.write(separator);
|
|
201
|
+
}
|
|
202
|
+
context.writeNode(node);
|
|
203
|
+
needsSeparator = true;
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
writeNodeListWithSourceGaps(nodes, fallbackSeparator) {
|
|
207
|
+
let lastRangeEnd: number | undefined;
|
|
208
|
+
let wroteNode = false;
|
|
209
|
+
|
|
210
|
+
for (const node of nodes) {
|
|
211
|
+
if (!node) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const range = getNodeRange(node);
|
|
216
|
+
if (wroteNode) {
|
|
217
|
+
if (
|
|
218
|
+
lastRangeEnd !== undefined &&
|
|
219
|
+
range &&
|
|
220
|
+
range.start >= lastRangeEnd
|
|
221
|
+
) {
|
|
222
|
+
context.writeSource(
|
|
223
|
+
lastRangeEnd,
|
|
224
|
+
range.start,
|
|
225
|
+
getMappingData(null),
|
|
226
|
+
);
|
|
227
|
+
} else {
|
|
228
|
+
context.write(fallbackSeparator);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
context.writeNode(node);
|
|
233
|
+
wroteNode = true;
|
|
234
|
+
|
|
235
|
+
if (range) {
|
|
236
|
+
lastRangeEnd = range.end;
|
|
237
|
+
} else {
|
|
238
|
+
lastRangeEnd = undefined;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
writeSource(start, end, data) {
|
|
243
|
+
if (end < start) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const generatedStart = generatedOffset;
|
|
248
|
+
context.write(options.source.slice(start, end));
|
|
249
|
+
appendMapping(
|
|
250
|
+
{ start, end },
|
|
251
|
+
generatedStart,
|
|
252
|
+
generatedOffset,
|
|
253
|
+
data ?? getMappingData(null),
|
|
254
|
+
);
|
|
255
|
+
},
|
|
256
|
+
writePreservedNode(node) {
|
|
257
|
+
const range = getNodeRange(node);
|
|
258
|
+
if (!range) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`Cannot preserve node ${node.type} without source offsets`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
context.writeSource(range.start, range.end, getMappingData(node));
|
|
264
|
+
},
|
|
265
|
+
appendMapping,
|
|
266
|
+
createExtraMapping(sourceRange, generatedStart, generatedEnd, data) {
|
|
267
|
+
extraMappings.push({
|
|
268
|
+
sourceStart: sourceRange.start,
|
|
269
|
+
sourceEnd: sourceRange.end,
|
|
270
|
+
generatedStart,
|
|
271
|
+
generatedEnd,
|
|
272
|
+
data,
|
|
273
|
+
});
|
|
274
|
+
},
|
|
275
|
+
result() {
|
|
276
|
+
return {
|
|
277
|
+
code: chunks.join(""),
|
|
278
|
+
mappings: [...mappings, ...extraMappings].map(toVolarMapping),
|
|
279
|
+
};
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
return context;
|
|
284
|
+
}
|