goatlint-parser 0.125.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 +167 -0
- package/package.json +129 -0
- package/src-js/bindings.js +601 -0
- package/src-js/generated/constants.js +95 -0
- package/src-js/generated/deserialize/js.js +5839 -0
- package/src-js/generated/deserialize/js_range.js +6380 -0
- package/src-js/generated/deserialize/ts.js +6131 -0
- package/src-js/generated/deserialize/ts_range.js +6700 -0
- package/src-js/generated/lazy/constructors.js +13864 -0
- package/src-js/generated/lazy/type_ids.js +191 -0
- package/src-js/generated/lazy/walk.js +5802 -0
- package/src-js/generated/visit/keys.js +220 -0
- package/src-js/generated/visit/type_ids.js +177 -0
- package/src-js/generated/visit/visitor.d.ts +387 -0
- package/src-js/generated/visit/walk.js +2455 -0
- package/src-js/index.d.ts +312 -0
- package/src-js/index.js +108 -0
- package/src-js/raw-transfer/common.js +276 -0
- package/src-js/raw-transfer/eager.js +254 -0
- package/src-js/raw-transfer/lazy-common.js +11 -0
- package/src-js/raw-transfer/lazy.js +153 -0
- package/src-js/raw-transfer/node-array.js +365 -0
- package/src-js/raw-transfer/supported.js +52 -0
- package/src-js/raw-transfer/visitor.js +127 -0
- package/src-js/visit/index.js +41 -0
- package/src-js/visit/visitor.js +405 -0
- package/src-js/wasm.js +11 -0
- package/src-js/webcontainer-fallback.cjs +21 -0
- package/src-js/wrap.js +57 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/* auto-generated by NAPI-RS */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
|
|
4
|
+
import type { Program } from "goatlint-types";
|
|
5
|
+
import type { VisitorObject } from "./generated/visit/visitor.d.ts";
|
|
6
|
+
|
|
7
|
+
export * from "goatlint-types";
|
|
8
|
+
|
|
9
|
+
export { VisitorObject };
|
|
10
|
+
|
|
11
|
+
export const visitorKeys: Record<string, string[]>;
|
|
12
|
+
|
|
13
|
+
export class Visitor {
|
|
14
|
+
constructor(visitor: VisitorObject);
|
|
15
|
+
visit(program: Program): void;
|
|
16
|
+
}
|
|
17
|
+
export interface Comment {
|
|
18
|
+
type: 'Line' | 'Block'
|
|
19
|
+
value: string
|
|
20
|
+
start: number
|
|
21
|
+
end: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ErrorLabel {
|
|
25
|
+
message: string | null
|
|
26
|
+
start: number
|
|
27
|
+
end: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface OxcError {
|
|
31
|
+
severity: Severity
|
|
32
|
+
message: string
|
|
33
|
+
labels: Array<ErrorLabel>
|
|
34
|
+
helpMessage: string | null
|
|
35
|
+
codeframe: string | null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export declare const enum Severity {
|
|
39
|
+
Error = 'Error',
|
|
40
|
+
Warning = 'Warning',
|
|
41
|
+
Advice = 'Advice'
|
|
42
|
+
}
|
|
43
|
+
export declare class ParseResult {
|
|
44
|
+
get program(): import("goatlint-types").Program
|
|
45
|
+
get module(): EcmaScriptModule
|
|
46
|
+
get comments(): Array<Comment>
|
|
47
|
+
get errors(): Array<OxcError>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface DynamicImport {
|
|
51
|
+
start: number
|
|
52
|
+
end: number
|
|
53
|
+
moduleRequest: Span
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface EcmaScriptModule {
|
|
57
|
+
/**
|
|
58
|
+
* Has ESM syntax.
|
|
59
|
+
*
|
|
60
|
+
* i.e. `import` and `export` statements, and `import.meta`.
|
|
61
|
+
*
|
|
62
|
+
* Dynamic imports `import('foo')` are ignored since they can be used in non-ESM files.
|
|
63
|
+
*/
|
|
64
|
+
hasModuleSyntax: boolean
|
|
65
|
+
/** Import statements. */
|
|
66
|
+
staticImports: Array<StaticImport>
|
|
67
|
+
/** Export statements. */
|
|
68
|
+
staticExports: Array<StaticExport>
|
|
69
|
+
/** Dynamic import expressions. */
|
|
70
|
+
dynamicImports: Array<DynamicImport>
|
|
71
|
+
/** Span positions` of `import.meta` */
|
|
72
|
+
importMetas: Array<Span>
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ExportExportName {
|
|
76
|
+
kind: ExportExportNameKind
|
|
77
|
+
name: string | null
|
|
78
|
+
start: number | null
|
|
79
|
+
end: number | null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export declare const enum ExportExportNameKind {
|
|
83
|
+
/** `export { name } */
|
|
84
|
+
Name = 'Name',
|
|
85
|
+
/** `export default expression` */
|
|
86
|
+
Default = 'Default',
|
|
87
|
+
/** `export * from "mod" */
|
|
88
|
+
None = 'None'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ExportImportName {
|
|
92
|
+
kind: ExportImportNameKind
|
|
93
|
+
name: string | null
|
|
94
|
+
start: number | null
|
|
95
|
+
end: number | null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export declare const enum ExportImportNameKind {
|
|
99
|
+
/** `export { name } */
|
|
100
|
+
Name = 'Name',
|
|
101
|
+
/** `export * as ns from "mod"` */
|
|
102
|
+
All = 'All',
|
|
103
|
+
/** `export * from "mod"` */
|
|
104
|
+
AllButDefault = 'AllButDefault',
|
|
105
|
+
/** Does not have a specifier. */
|
|
106
|
+
None = 'None'
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface ExportLocalName {
|
|
110
|
+
kind: ExportLocalNameKind
|
|
111
|
+
name: string | null
|
|
112
|
+
start: number | null
|
|
113
|
+
end: number | null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export declare const enum ExportLocalNameKind {
|
|
117
|
+
/** `export { name } */
|
|
118
|
+
Name = 'Name',
|
|
119
|
+
/** `export default expression` */
|
|
120
|
+
Default = 'Default',
|
|
121
|
+
/**
|
|
122
|
+
* If the exported value is not locally accessible from within the module.
|
|
123
|
+
* `export default function () {}`
|
|
124
|
+
*/
|
|
125
|
+
None = 'None'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface ImportName {
|
|
129
|
+
kind: ImportNameKind
|
|
130
|
+
name: string | null
|
|
131
|
+
start: number | null
|
|
132
|
+
end: number | null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export declare const enum ImportNameKind {
|
|
136
|
+
/** `import { x } from "mod"` */
|
|
137
|
+
Name = 'Name',
|
|
138
|
+
/** `import * as ns from "mod"` */
|
|
139
|
+
NamespaceObject = 'NamespaceObject',
|
|
140
|
+
/** `import defaultExport from "mod"` */
|
|
141
|
+
Default = 'Default'
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Parse JS/TS source asynchronously on a separate thread.
|
|
146
|
+
*
|
|
147
|
+
* Note that not all of the workload can happen on a separate thread.
|
|
148
|
+
* Parsing on Rust side does happen in a separate thread, but deserialization of the AST to JS objects
|
|
149
|
+
* has to happen on current thread. This synchronous deserialization work typically outweighs
|
|
150
|
+
* the asynchronous parsing by a factor of between 3 and 20.
|
|
151
|
+
*
|
|
152
|
+
* i.e. the majority of the workload cannot be parallelized by using this method.
|
|
153
|
+
*
|
|
154
|
+
* Generally `parseSync` is preferable to use as it does not have the overhead of spawning a thread.
|
|
155
|
+
* If you need to parallelize parsing multiple files, it is recommended to use worker threads.
|
|
156
|
+
*/
|
|
157
|
+
export declare function parse(filename: string, sourceText: string, options?: ParserOptions | undefined | null): Promise<ParseResult>
|
|
158
|
+
|
|
159
|
+
export interface ParserOptions {
|
|
160
|
+
/** Treat the source text as `js`, `jsx`, `ts`, `tsx` or `dts`. */
|
|
161
|
+
lang?: 'js' | 'jsx' | 'ts' | 'tsx' | 'dts'
|
|
162
|
+
/** Treat the source text as `script` or `module` code. */
|
|
163
|
+
sourceType?: 'script' | 'module' | 'commonjs' | 'unambiguous' | undefined
|
|
164
|
+
/**
|
|
165
|
+
* Return an AST which includes TypeScript-related properties, or excludes them.
|
|
166
|
+
*
|
|
167
|
+
* `'js'` is default for JS / JSX files.
|
|
168
|
+
* `'ts'` is default for TS / TSX files.
|
|
169
|
+
* The type of the file is determined from `lang` option, or extension of provided `filename`.
|
|
170
|
+
*/
|
|
171
|
+
astType?: 'js' | 'ts'
|
|
172
|
+
/**
|
|
173
|
+
* Controls whether the `range` property is included on AST nodes.
|
|
174
|
+
* The `range` property is a `[number, number]` which indicates the start/end offsets
|
|
175
|
+
* of the node in the file contents.
|
|
176
|
+
*
|
|
177
|
+
* @default false
|
|
178
|
+
*/
|
|
179
|
+
range?: boolean
|
|
180
|
+
/**
|
|
181
|
+
* Emit `ParenthesizedExpression` and `TSParenthesizedType` in AST.
|
|
182
|
+
*
|
|
183
|
+
* If this option is true, parenthesized expressions are represented by
|
|
184
|
+
* (non-standard) `ParenthesizedExpression` and `TSParenthesizedType` nodes that
|
|
185
|
+
* have a single `expression` property containing the expression inside parentheses.
|
|
186
|
+
*
|
|
187
|
+
* @default true
|
|
188
|
+
*/
|
|
189
|
+
preserveParens?: boolean
|
|
190
|
+
/**
|
|
191
|
+
* Produce semantic errors with an additional AST pass.
|
|
192
|
+
* Semantic errors depend on symbols and scopes, where the parser does not construct.
|
|
193
|
+
* This adds a small performance overhead.
|
|
194
|
+
*
|
|
195
|
+
* @default false
|
|
196
|
+
*/
|
|
197
|
+
showSemanticErrors?: boolean
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Parse JS/TS source synchronously on current thread.
|
|
202
|
+
*
|
|
203
|
+
* This is generally preferable over `parse` (async) as it does not have the overhead
|
|
204
|
+
* of spawning a thread, and the majority of the workload cannot be parallelized anyway
|
|
205
|
+
* (see `parse` documentation for details).
|
|
206
|
+
*
|
|
207
|
+
* If you need to parallelize parsing multiple files, it is recommended to use worker threads
|
|
208
|
+
* with `parseSync` rather than using `parse`.
|
|
209
|
+
*/
|
|
210
|
+
export declare function parseSync(filename: string, sourceText: string, options?: ParserOptions | undefined | null): ParseResult
|
|
211
|
+
|
|
212
|
+
/** Returns `true` if raw transfer is supported on this platform. */
|
|
213
|
+
export declare function rawTransferSupported(): boolean
|
|
214
|
+
|
|
215
|
+
export interface Span {
|
|
216
|
+
start: number
|
|
217
|
+
end: number
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface StaticExport {
|
|
221
|
+
start: number
|
|
222
|
+
end: number
|
|
223
|
+
entries: Array<StaticExportEntry>
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface StaticExportEntry {
|
|
227
|
+
start: number
|
|
228
|
+
end: number
|
|
229
|
+
moduleRequest: ValueSpan | null
|
|
230
|
+
/** The name under which the desired binding is exported by the module`. */
|
|
231
|
+
importName: ExportImportName
|
|
232
|
+
/** The name used to export this binding by this module. */
|
|
233
|
+
exportName: ExportExportName
|
|
234
|
+
/** The name that is used to locally access the exported value from within the importing module. */
|
|
235
|
+
localName: ExportLocalName
|
|
236
|
+
/**
|
|
237
|
+
* Whether the export is a TypeScript `export type`.
|
|
238
|
+
*
|
|
239
|
+
* Examples:
|
|
240
|
+
*
|
|
241
|
+
* ```ts
|
|
242
|
+
* export type * from 'mod';
|
|
243
|
+
* export type * as ns from 'mod';
|
|
244
|
+
* export type { foo };
|
|
245
|
+
* export { type foo }:
|
|
246
|
+
* export type { foo } from 'mod';
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
isType: boolean
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export interface StaticImport {
|
|
253
|
+
/** Start of import statement. */
|
|
254
|
+
start: number
|
|
255
|
+
/** End of import statement. */
|
|
256
|
+
end: number
|
|
257
|
+
/**
|
|
258
|
+
* Import source.
|
|
259
|
+
*
|
|
260
|
+
* ```js
|
|
261
|
+
* import { foo } from "mod";
|
|
262
|
+
* // ^^^
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
moduleRequest: ValueSpan
|
|
266
|
+
/**
|
|
267
|
+
* Import specifiers.
|
|
268
|
+
*
|
|
269
|
+
* Empty for `import "mod"`.
|
|
270
|
+
*/
|
|
271
|
+
entries: Array<StaticImportEntry>
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export interface StaticImportEntry {
|
|
275
|
+
/**
|
|
276
|
+
* The name under which the desired binding is exported by the module.
|
|
277
|
+
*
|
|
278
|
+
* ```js
|
|
279
|
+
* import { foo } from "mod";
|
|
280
|
+
* // ^^^
|
|
281
|
+
* import { foo as bar } from "mod";
|
|
282
|
+
* // ^^^
|
|
283
|
+
* ```
|
|
284
|
+
*/
|
|
285
|
+
importName: ImportName
|
|
286
|
+
/**
|
|
287
|
+
* The name that is used to locally access the imported value from within the importing module.
|
|
288
|
+
* ```js
|
|
289
|
+
* import { foo } from "mod";
|
|
290
|
+
* // ^^^
|
|
291
|
+
* import { foo as bar } from "mod";
|
|
292
|
+
* // ^^^
|
|
293
|
+
* ```
|
|
294
|
+
*/
|
|
295
|
+
localName: ValueSpan
|
|
296
|
+
/**
|
|
297
|
+
* Whether this binding is for a TypeScript type-only import.
|
|
298
|
+
*
|
|
299
|
+
* `true` for the following imports:
|
|
300
|
+
* ```ts
|
|
301
|
+
* import type { foo } from "mod";
|
|
302
|
+
* import { type foo } from "mod";
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
isType: boolean
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export interface ValueSpan {
|
|
309
|
+
value: string
|
|
310
|
+
start: number
|
|
311
|
+
end: number
|
|
312
|
+
}
|
package/src-js/index.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { parse as parseBinding, parseSync as parseSyncBinding } from "./bindings.js";
|
|
3
|
+
import { wrap } from "./wrap.js";
|
|
4
|
+
|
|
5
|
+
export { default as visitorKeys } from "./generated/visit/keys.js";
|
|
6
|
+
export { Visitor } from "./visit/index.js";
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
ExportExportNameKind,
|
|
10
|
+
ExportImportNameKind,
|
|
11
|
+
ExportLocalNameKind,
|
|
12
|
+
ImportNameKind,
|
|
13
|
+
ParseResult,
|
|
14
|
+
Severity,
|
|
15
|
+
} from "./bindings.js";
|
|
16
|
+
export { rawTransferSupported } from "./raw-transfer/supported.js";
|
|
17
|
+
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
|
|
20
|
+
// Lazily loaded as needed
|
|
21
|
+
let parseSyncRaw = null,
|
|
22
|
+
parseRaw,
|
|
23
|
+
parseSyncLazy = null,
|
|
24
|
+
parseLazy,
|
|
25
|
+
LazyVisitor;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Lazy-load code related to raw transfer.
|
|
29
|
+
* @returns {undefined}
|
|
30
|
+
*/
|
|
31
|
+
function loadRawTransfer() {
|
|
32
|
+
if (parseSyncRaw === null) {
|
|
33
|
+
({ parseSyncRaw, parse: parseRaw } = require("./raw-transfer/eager.js"));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Lazy-load code related to raw transfer lazy deserialization.
|
|
39
|
+
* @returns {undefined}
|
|
40
|
+
*/
|
|
41
|
+
function loadRawTransferLazy() {
|
|
42
|
+
if (parseSyncLazy === null) {
|
|
43
|
+
({ parseSyncLazy, parse: parseLazy, Visitor: LazyVisitor } = require("./raw-transfer/lazy.js"));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parse JS/TS source synchronously on current thread.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} filename - Filename
|
|
51
|
+
* @param {string} sourceText - Source text of file
|
|
52
|
+
* @param {Object|undefined} options - Parsing options
|
|
53
|
+
* @returns {Object} - Object with property getters for `program`, `module`, `comments`, and `errors`
|
|
54
|
+
* @throws {Error} - If `experimentalRawTransfer` or `experimentalLazy` option is enabled,
|
|
55
|
+
* and raw transfer is not supported on this platform
|
|
56
|
+
*/
|
|
57
|
+
export function parseSync(filename, sourceText, options) {
|
|
58
|
+
if (options?.experimentalRawTransfer) {
|
|
59
|
+
loadRawTransfer();
|
|
60
|
+
return parseSyncRaw(filename, sourceText, options);
|
|
61
|
+
}
|
|
62
|
+
if (options?.experimentalLazy) {
|
|
63
|
+
loadRawTransferLazy();
|
|
64
|
+
return parseSyncLazy(filename, sourceText, options);
|
|
65
|
+
}
|
|
66
|
+
return wrap(parseSyncBinding(filename, sourceText, options));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Parse JS/TS source asynchronously on a separate thread.
|
|
71
|
+
*
|
|
72
|
+
* Note that not all of the workload can happen on a separate thread.
|
|
73
|
+
* Parsing on Rust side does happen in a separate thread, but deserialization of the AST to JS objects
|
|
74
|
+
* has to happen on current thread. This synchronous deserialization work typically outweighs
|
|
75
|
+
* the asynchronous parsing by a factor of between 3 and 20.
|
|
76
|
+
*
|
|
77
|
+
* i.e. the majority of the workload cannot be parallelized by using this method.
|
|
78
|
+
*
|
|
79
|
+
* Generally `parseSync` is preferable to use as it does not have the overhead of spawning a thread.
|
|
80
|
+
* If you need to parallelize parsing multiple files, it is recommended to use worker threads.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} filename - Filename
|
|
83
|
+
* @param {string} sourceText - Source text of file
|
|
84
|
+
* @param {Object|undefined} options - Parsing options
|
|
85
|
+
* @returns {Object} - Object with property getters for `program`, `module`, `comments`, and `errors`
|
|
86
|
+
* @throws {Error} - If `experimentalRawTransfer` or `experimentalLazy` option is enabled,
|
|
87
|
+
* and raw transfer is not supported on this platform
|
|
88
|
+
*/
|
|
89
|
+
export async function parse(filename, sourceText, options) {
|
|
90
|
+
if (options?.experimentalRawTransfer) {
|
|
91
|
+
loadRawTransfer();
|
|
92
|
+
return await parseRaw(filename, sourceText, options);
|
|
93
|
+
}
|
|
94
|
+
if (options?.experimentalLazy) {
|
|
95
|
+
loadRawTransferLazy();
|
|
96
|
+
return await parseLazy(filename, sourceText, options);
|
|
97
|
+
}
|
|
98
|
+
return wrap(await parseBinding(filename, sourceText, options));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get `Visitor` class to construct visitors with.
|
|
103
|
+
* @returns {function} - `Visitor` class
|
|
104
|
+
*/
|
|
105
|
+
export function experimentalGetLazyVisitor() {
|
|
106
|
+
loadRawTransferLazy();
|
|
107
|
+
return LazyVisitor;
|
|
108
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { BUFFER_ALIGN, BUFFER_SIZE, IS_TS_FLAG_POS } from "../generated/constants.js";
|
|
3
|
+
import {
|
|
4
|
+
getBufferOffset,
|
|
5
|
+
parseRaw as parseRawBinding,
|
|
6
|
+
parseRawSync as parseRawSyncBinding,
|
|
7
|
+
} from "../bindings.js";
|
|
8
|
+
import { rawTransferSupported } from "./supported.js";
|
|
9
|
+
|
|
10
|
+
// Throw an error if running on a platform which raw transfer doesn't support.
|
|
11
|
+
//
|
|
12
|
+
// Note: This module is lazy-loaded only when user calls `parseSync` or `parseAsync` with
|
|
13
|
+
// `experimentalRawTransfer` or `experimentalLazy` options, or calls `experimentalGetLazyVisitor`.
|
|
14
|
+
if (!rawTransferSupported()) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
"`experimentalRawTransfer` and `experimentalLazy` options are not supported " +
|
|
17
|
+
"on 32-bit or big-endian systems, versions of NodeJS prior to v22.0.0, " +
|
|
18
|
+
"versions of Deno prior to v2.0.0, or other runtimes",
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse JS/TS source synchronously on current thread using raw transfer.
|
|
24
|
+
*
|
|
25
|
+
* Convert the buffer returned by Rust to a JS object with provided `convert` function.
|
|
26
|
+
*
|
|
27
|
+
* This function contains logic shared by both `parseSyncRaw` and `parseSyncLazy`.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} filename - Filename
|
|
30
|
+
* @param {string} sourceText - Source text of file
|
|
31
|
+
* @param {Object} options - Parsing options
|
|
32
|
+
* @param {function} convert - Function to convert the buffer returned from Rust into a JS object
|
|
33
|
+
* @returns {Object} - The return value of `convert`
|
|
34
|
+
*/
|
|
35
|
+
export function parseSyncRawImpl(filename, sourceText, options, convert) {
|
|
36
|
+
const { buffer, sourceByteLen } = prepareRaw(sourceText);
|
|
37
|
+
parseRawSyncBinding(filename, buffer, sourceByteLen, options);
|
|
38
|
+
return convert(buffer, sourceText, sourceByteLen, options);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// User should not schedule more async tasks than there are available CPUs, as it hurts performance,
|
|
42
|
+
// but it's a common mistake in async JS code to do exactly that.
|
|
43
|
+
//
|
|
44
|
+
// That anti-pattern looks like this when applied to Oxc:
|
|
45
|
+
//
|
|
46
|
+
// ```js
|
|
47
|
+
// const asts = await Promise.all(
|
|
48
|
+
// files.map(
|
|
49
|
+
// async (filename) => {
|
|
50
|
+
// const sourceText = await fs.readFile(filename, 'utf8');
|
|
51
|
+
// const ast = await oxc.parseAsync(filename, sourceText);
|
|
52
|
+
// return ast;
|
|
53
|
+
// }
|
|
54
|
+
// )
|
|
55
|
+
// );
|
|
56
|
+
// ```
|
|
57
|
+
//
|
|
58
|
+
// In most cases, that'd just result in a bit of degraded performance, and higher memory use because
|
|
59
|
+
// of loading sources into memory prematurely.
|
|
60
|
+
//
|
|
61
|
+
// However, raw transfer uses a 6 GiB buffer for each parsing operation.
|
|
62
|
+
// Most of the memory pages in those buffers are never touched, so this does not consume a huge amount
|
|
63
|
+
// of physical memory, but it does still consume virtual memory.
|
|
64
|
+
//
|
|
65
|
+
// If we allowed creating a large number of 6 GiB buffers simultaneously, it would quickly consume
|
|
66
|
+
// virtual memory space and risk memory exhaustion. The code above would exhaust all of bottom half
|
|
67
|
+
// (heap) of 48-bit virtual memory space if `files.length >= 21_845`. This is not a number which
|
|
68
|
+
// is unrealistic in real world code.
|
|
69
|
+
//
|
|
70
|
+
// To guard against this possibility, we implement a simple queue.
|
|
71
|
+
// No more than `os.availableParallelism()` files can be parsed simultaneously, and any further calls to
|
|
72
|
+
// `parseAsyncRaw` will be put in a queue, to execute once other tasks complete.
|
|
73
|
+
//
|
|
74
|
+
// Fallback to `os.cpus().length` on versions of NodeJS prior to v18.14.0, which do not support
|
|
75
|
+
// `os.availableParallelism`.
|
|
76
|
+
let availableCores = os.availableParallelism ? os.availableParallelism() : os.cpus().length;
|
|
77
|
+
const queue = [];
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Parse JS/TS source asynchronously using raw transfer.
|
|
81
|
+
*
|
|
82
|
+
* Convert the buffer returned by Rust to a JS object with provided `convert` function.
|
|
83
|
+
*
|
|
84
|
+
* Queues up parsing operations if more calls than number of CPU cores (see above).
|
|
85
|
+
*
|
|
86
|
+
* This function contains logic shared by both `parseAsyncRaw` and `parseAsyncLazy`.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} filename - Filename
|
|
89
|
+
* @param {string} sourceText - Source text of file
|
|
90
|
+
* @param {Object} options - Parsing options
|
|
91
|
+
* @param {function} convert - Function to convert the buffer returned from Rust into a JS object
|
|
92
|
+
* @returns {Object} - The return value of `convert`
|
|
93
|
+
*/
|
|
94
|
+
export async function parseAsyncRawImpl(filename, sourceText, options, convert) {
|
|
95
|
+
// Wait for a free CPU core if all CPUs are currently busy.
|
|
96
|
+
//
|
|
97
|
+
// Note: `availableCores` is NOT decremented if have to wait in the queue first,
|
|
98
|
+
// and NOT incremented when parsing completes and it runs next task in the queue.
|
|
99
|
+
//
|
|
100
|
+
// This is to avoid a race condition if `parseAsyncRaw` is called during the microtick in between
|
|
101
|
+
// `resolve` being called below, and the promise resolving here. In that case the new task could
|
|
102
|
+
// start running, and then the promise resolves, and the queued task also starts running.
|
|
103
|
+
// We'd then have `availableParallelism() + 1` tasks running simultaneously. Potentially, this could
|
|
104
|
+
// happen repeatedly, with the number of tasks running simultaneously ever-increasing.
|
|
105
|
+
if (availableCores === 0) {
|
|
106
|
+
// All CPU cores are busy. Put this task in queue and wait for capacity to become available.
|
|
107
|
+
await new Promise((resolve, _) => {
|
|
108
|
+
queue.push(resolve);
|
|
109
|
+
});
|
|
110
|
+
} else {
|
|
111
|
+
// A CPU core is available. Mark core as busy, and run parsing now.
|
|
112
|
+
availableCores--;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Parse
|
|
116
|
+
const { buffer, sourceByteLen } = prepareRaw(sourceText);
|
|
117
|
+
await parseRawBinding(filename, buffer, sourceByteLen, options);
|
|
118
|
+
const data = convert(buffer, sourceText, sourceByteLen, options);
|
|
119
|
+
|
|
120
|
+
// Free the CPU core
|
|
121
|
+
if (queue.length > 0) {
|
|
122
|
+
// Some further tasks waiting in queue. Run the next one.
|
|
123
|
+
// Do not increment `availableCores` (see above).
|
|
124
|
+
const resolve = queue.shift();
|
|
125
|
+
resolve();
|
|
126
|
+
} else {
|
|
127
|
+
// No tasks waiting in queue. This CPU is now free.
|
|
128
|
+
availableCores++;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return data;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const ARRAY_BUFFER_SIZE = BUFFER_SIZE + BUFFER_ALIGN;
|
|
135
|
+
const ONE_GIB = 1 << 30;
|
|
136
|
+
|
|
137
|
+
// We keep a cache of buffers for raw transfer, so we can reuse them as much as possible.
|
|
138
|
+
//
|
|
139
|
+
// When processing multiple files, it's ideal if can reuse an existing buffer, as it's more likely to
|
|
140
|
+
// be warm in CPU cache, it avoids allocations, and it saves work for the garbage collector.
|
|
141
|
+
//
|
|
142
|
+
// However, we also don't want to keep a load of large buffers around indefinitely using up memory,
|
|
143
|
+
// if they're not going to be used again.
|
|
144
|
+
//
|
|
145
|
+
// We have no knowledge of what pattern over time user may process files in (could be lots in quick
|
|
146
|
+
// succession, or more occasionally in a long-running process). So we try to use flexible caching
|
|
147
|
+
// strategy which is adaptable to many usage patterns.
|
|
148
|
+
//
|
|
149
|
+
// We use a 2-tier cache.
|
|
150
|
+
// Tier 1 uses strong references, tier 2 uses weak references.
|
|
151
|
+
//
|
|
152
|
+
// When parsing is complete and the buffer is no longer in use, push it to `buffers` (tier 1 cache).
|
|
153
|
+
// Set a timer to clear the cache when no activity for 10 seconds.
|
|
154
|
+
//
|
|
155
|
+
// When the timer expires, move all the buffers from tier 1 cache into `oldBuffers` (tier 2).
|
|
156
|
+
// They are stored there as `WeakRef`s, so the garbage collector is free to reclaim them.
|
|
157
|
+
//
|
|
158
|
+
// On the next call to `parseSync` or `parseAsync`, promote any buffers in tier 2 cache which were not
|
|
159
|
+
// already garbage collected back into tier 1 cache. This is on assumption that parsing one file
|
|
160
|
+
// indicates parsing as a whole is an ongoing process, and there will likely be further calls to
|
|
161
|
+
// `parseSync` / `parseAsync` in future.
|
|
162
|
+
//
|
|
163
|
+
// The weak tier 2 cache is because V8 does not necessarily free memory as soon as it's able to be
|
|
164
|
+
// freed. We don't want to block it from freeing memory, but if it's not done that yet, there's no
|
|
165
|
+
// point creating a new buffer, when one already exists.
|
|
166
|
+
const CLEAR_BUFFERS_TIMEOUT = 10_000; // 10 seconds
|
|
167
|
+
const buffers = [],
|
|
168
|
+
oldBuffers = [];
|
|
169
|
+
let clearBuffersTimeout = null;
|
|
170
|
+
|
|
171
|
+
const textEncoder = new TextEncoder();
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get a buffer (from cache if possible), and copy source text into it.
|
|
175
|
+
*
|
|
176
|
+
* @param {string} sourceText - Source text of file
|
|
177
|
+
* @returns {Object} - Object of form `{ buffer, sourceByteLen }`.
|
|
178
|
+
* - `buffer`: `Uint8Array` containing the AST in raw form.
|
|
179
|
+
* - `sourceByteLen`: Length of source text in UTF-8 bytes
|
|
180
|
+
* (which may not be equal to `sourceText.length` if source contains non-ASCII characters).
|
|
181
|
+
*/
|
|
182
|
+
export function prepareRaw(sourceText) {
|
|
183
|
+
// Cancel timeout for clearing buffers
|
|
184
|
+
if (clearBuffersTimeout !== null) {
|
|
185
|
+
clearTimeout(clearBuffersTimeout);
|
|
186
|
+
clearBuffersTimeout = null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Revive any discarded buffers which have not yet been garbage collected
|
|
190
|
+
if (oldBuffers.length > 0) {
|
|
191
|
+
const revivedBuffers = [];
|
|
192
|
+
for (let oldBuffer of oldBuffers) {
|
|
193
|
+
oldBuffer = oldBuffer.deref();
|
|
194
|
+
if (oldBuffer !== undefined) revivedBuffers.push(oldBuffer);
|
|
195
|
+
}
|
|
196
|
+
oldBuffers.length = 0;
|
|
197
|
+
if (revivedBuffers.length > 0) buffers.unshift(...revivedBuffers);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Reuse existing buffer, or create a new one
|
|
201
|
+
const buffer = buffers.length > 0 ? buffers.pop() : createBuffer();
|
|
202
|
+
|
|
203
|
+
// Write source into start of buffer.
|
|
204
|
+
// `TextEncoder` cannot write into a `Uint8Array` larger than 1 GiB,
|
|
205
|
+
// so create a view into buffer of this size to write into.
|
|
206
|
+
const sourceBuffer = new Uint8Array(buffer.buffer, buffer.byteOffset, ONE_GIB);
|
|
207
|
+
const { read, written: sourceByteLen } = textEncoder.encodeInto(sourceText, sourceBuffer);
|
|
208
|
+
if (read !== sourceText.length) throw new Error("Failed to write source text into buffer");
|
|
209
|
+
|
|
210
|
+
return { buffer, sourceByteLen };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get if AST should be parsed as JS or TS.
|
|
215
|
+
* Rust side sets a `bool` in this position in buffer which is `true` if TS.
|
|
216
|
+
*
|
|
217
|
+
* @param {Uint8Array} buffer - Buffer containing AST in raw form
|
|
218
|
+
* @returns {boolean} - `true` if AST is JS, `false` if TS
|
|
219
|
+
*/
|
|
220
|
+
export function isJsAst(buffer) {
|
|
221
|
+
return buffer[IS_TS_FLAG_POS] === 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Return buffer to cache, to be reused.
|
|
226
|
+
* Set a timer to clear buffers.
|
|
227
|
+
*
|
|
228
|
+
* @param {Uint8Array} buffer - Buffer
|
|
229
|
+
* @returns {undefined}
|
|
230
|
+
*/
|
|
231
|
+
export function returnBufferToCache(buffer) {
|
|
232
|
+
buffers.push(buffer);
|
|
233
|
+
|
|
234
|
+
if (clearBuffersTimeout !== null) clearTimeout(clearBuffersTimeout);
|
|
235
|
+
clearBuffersTimeout = setTimeout(clearBuffersCache, CLEAR_BUFFERS_TIMEOUT);
|
|
236
|
+
clearBuffersTimeout.unref();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Downgrade buffers in tier 1 cache (`buffers`) to tier 2 (`oldBuffers`)
|
|
241
|
+
* so they can be garbage collected.
|
|
242
|
+
*
|
|
243
|
+
* @returns {undefined}
|
|
244
|
+
*/
|
|
245
|
+
function clearBuffersCache() {
|
|
246
|
+
clearBuffersTimeout = null;
|
|
247
|
+
|
|
248
|
+
for (const buffer of buffers) {
|
|
249
|
+
oldBuffers.push(new WeakRef(buffer));
|
|
250
|
+
}
|
|
251
|
+
buffers.length = 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Create a `Uint8Array` which is 2 GiB in size, with its start aligned on 4 GiB.
|
|
256
|
+
*
|
|
257
|
+
* Achieve this by creating a 6 GiB `ArrayBuffer`, getting the offset within it that's aligned to 4 GiB,
|
|
258
|
+
* chopping off that number of bytes from the start, and shortening to 2 GiB.
|
|
259
|
+
*
|
|
260
|
+
* It's always possible to obtain a 2 GiB slice aligned on 4 GiB within a 6 GiB buffer,
|
|
261
|
+
* no matter how the 6 GiB buffer is aligned.
|
|
262
|
+
*
|
|
263
|
+
* Note: On systems with virtual memory, this only consumes 6 GiB of *virtual* memory.
|
|
264
|
+
* It does not consume physical memory until data is actually written to the `Uint8Array`.
|
|
265
|
+
* Physical memory consumed corresponds to the quantity of data actually written.
|
|
266
|
+
*
|
|
267
|
+
* @returns {Uint8Array} - Buffer
|
|
268
|
+
*/
|
|
269
|
+
function createBuffer() {
|
|
270
|
+
const arrayBuffer = new ArrayBuffer(ARRAY_BUFFER_SIZE);
|
|
271
|
+
const offset = getBufferOffset(new Uint8Array(arrayBuffer));
|
|
272
|
+
const buffer = new Uint8Array(arrayBuffer, offset, BUFFER_SIZE);
|
|
273
|
+
buffer.int32 = new Int32Array(arrayBuffer, offset, BUFFER_SIZE / 4);
|
|
274
|
+
buffer.float64 = new Float64Array(arrayBuffer, offset, BUFFER_SIZE / 8);
|
|
275
|
+
return buffer;
|
|
276
|
+
}
|