tsnite 0.2.0 → 0.2.2
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/dist/cache.js +1 -21
- package/dist/loader.js +23 -126
- package/package.json +3 -1
- package/dist/parse.js +0 -435
package/dist/cache.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { mkdir, rm
|
|
2
|
+
import { mkdir, rm } from 'node:fs/promises';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
export const resolveCache = new Map();
|
|
5
|
-
const statCache = new Map();
|
|
6
5
|
function getTranspileCacheDir() {
|
|
7
6
|
return join(import.meta.dirname, '..', 'node_modules', '.cache', 'tsnite');
|
|
8
7
|
}
|
|
@@ -12,27 +11,9 @@ function getTSConfigPath() {
|
|
|
12
11
|
function hash(value) {
|
|
13
12
|
return createHash('sha1').update(value).digest('hex');
|
|
14
13
|
}
|
|
15
|
-
export async function existsWithCache(filePath) {
|
|
16
|
-
const cached = statCache.get(filePath);
|
|
17
|
-
if (cached)
|
|
18
|
-
return cached.exists;
|
|
19
|
-
try {
|
|
20
|
-
const stats = await stat(filePath);
|
|
21
|
-
const exists = stats.isFile();
|
|
22
|
-
statCache.set(filePath, { exists, mtime: stats.mtimeMs });
|
|
23
|
-
return exists;
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
statCache.set(filePath, { exists: false });
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
14
|
export function clearResolveCache() {
|
|
31
15
|
resolveCache.clear();
|
|
32
16
|
}
|
|
33
|
-
export function invalidateStatCache(filePath) {
|
|
34
|
-
statCache.delete(filePath);
|
|
35
|
-
}
|
|
36
17
|
export function getTranspileCacheFile(filePath) {
|
|
37
18
|
return join(getTranspileCacheDir(), `${hash(filePath)}.json`);
|
|
38
19
|
}
|
|
@@ -46,7 +27,6 @@ export function isTSConfigPath(filePath) {
|
|
|
46
27
|
return resolve(filePath) === getTSConfigPath();
|
|
47
28
|
}
|
|
48
29
|
export async function invalidateFileCaches(filePath) {
|
|
49
|
-
invalidateStatCache(filePath);
|
|
50
30
|
clearResolveCache();
|
|
51
31
|
if (isTSConfigPath(filePath)) {
|
|
52
32
|
await clearTranspileCache();
|
package/dist/loader.js
CHANGED
|
@@ -1,75 +1,25 @@
|
|
|
1
1
|
import { transform } from 'oxc-transform';
|
|
2
|
+
import { ResolverFactory } from 'oxc-resolver';
|
|
2
3
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
3
4
|
import { readFile, stat, writeFile } from 'node:fs/promises';
|
|
4
5
|
import { writeSync } from 'node:fs';
|
|
5
|
-
import {
|
|
6
|
+
import { relative } from 'node:path';
|
|
6
7
|
import { createHash } from 'node:crypto';
|
|
7
|
-
import {
|
|
8
|
-
import { ensureTranspileCacheDir, existsWithCache, getTranspileCacheFile, resolveCache } from './cache.js';
|
|
8
|
+
import { ensureTranspileCacheDir, getTranspileCacheFile, resolveCache } from './cache.js';
|
|
9
9
|
import { gray, green, yellow } from './util.js';
|
|
10
|
-
const tsconfigCache = { paths: null, baseUrl: null };
|
|
11
10
|
const transpileCache = new Map();
|
|
12
11
|
const MAX_TRANSPILE_CACHE_ENTRIES = 256;
|
|
13
12
|
const TS_EXTENSIONS = ['.cts', '.mts', '.tsx', '.ts'];
|
|
14
13
|
const SHOULD_LOG_TRANSPILE = process.env.TSNITE_LOG_TRANSPILE === '1';
|
|
14
|
+
const TRANSPILE_CONFIG_HASH = hash('target:esnext|jsx:automatic|decorator:legacy|emitDecoratorMetadata:true|helpers:runtime');
|
|
15
|
+
const resolver = new ResolverFactory({
|
|
16
|
+
conditionNames: ['node', 'import'],
|
|
17
|
+
extensions: ['.cts', '.mts', '.tsx', '.ts', '.cjs', '.mjs', '.js', '.json'],
|
|
18
|
+
tsconfig: 'auto'
|
|
19
|
+
});
|
|
15
20
|
function hasTypeScriptExtension(value) {
|
|
16
21
|
return TS_EXTENSIONS.some((extension) => value.endsWith(extension));
|
|
17
22
|
}
|
|
18
|
-
function isTypeScriptSpecifier(specifier) {
|
|
19
|
-
const extension = extname(specifier);
|
|
20
|
-
return extension === '' || hasTypeScriptExtension(extension);
|
|
21
|
-
}
|
|
22
|
-
function getTypeScriptTryFiles(basePath) {
|
|
23
|
-
return [
|
|
24
|
-
basePath,
|
|
25
|
-
...TS_EXTENSIONS.map((extension) => basePath + extension),
|
|
26
|
-
...TS_EXTENSIONS.map((extension) => join(basePath, 'index' + extension))
|
|
27
|
-
];
|
|
28
|
-
}
|
|
29
|
-
async function resolveTypeScriptFile(basePath) {
|
|
30
|
-
for (const file of getTypeScriptTryFiles(basePath)) {
|
|
31
|
-
if (await existsWithCache(file)) {
|
|
32
|
-
return file;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
function matchPathPattern(specifier, pattern) {
|
|
38
|
-
const starIndex = pattern.indexOf('*');
|
|
39
|
-
if (starIndex === -1) {
|
|
40
|
-
return specifier === pattern ? [] : null;
|
|
41
|
-
}
|
|
42
|
-
const prefix = pattern.slice(0, starIndex);
|
|
43
|
-
const suffix = pattern.slice(starIndex + 1);
|
|
44
|
-
if (!specifier.startsWith(prefix) || !specifier.endsWith(suffix)) {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
return [specifier.slice(prefix.length, specifier.length - suffix.length)];
|
|
48
|
-
}
|
|
49
|
-
function applyPathMapping(target, matches) {
|
|
50
|
-
let mapped = target;
|
|
51
|
-
for (const match of matches) {
|
|
52
|
-
mapped = mapped.replace('*', match);
|
|
53
|
-
}
|
|
54
|
-
return mapped;
|
|
55
|
-
}
|
|
56
|
-
async function resolveTsConfigPath(specifier, paths, baseUrl) {
|
|
57
|
-
if (!paths || !baseUrl || !isTypeScriptSpecifier(specifier)) {
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
for (const [pattern, targets] of Object.entries(paths)) {
|
|
61
|
-
const matches = matchPathPattern(specifier, pattern);
|
|
62
|
-
if (!matches)
|
|
63
|
-
continue;
|
|
64
|
-
for (const target of targets) {
|
|
65
|
-
const mappedTarget = applyPathMapping(target, matches);
|
|
66
|
-
const resolved = await resolveTypeScriptFile(join(baseUrl, mappedTarget));
|
|
67
|
-
if (resolved)
|
|
68
|
-
return resolved;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
23
|
function hash(value) {
|
|
74
24
|
return createHash('sha1').update(value).digest('hex');
|
|
75
25
|
}
|
|
@@ -125,42 +75,11 @@ async function writeCachedTranspile(filename, entry) {
|
|
|
125
75
|
// Ignore cache write failures and continue with fresh output.
|
|
126
76
|
}
|
|
127
77
|
}
|
|
128
|
-
async function loadTSConfig() {
|
|
129
|
-
if (tsconfigCache.paths !== null && tsconfigCache.baseUrl !== null) {
|
|
130
|
-
return tsconfigCache;
|
|
131
|
-
}
|
|
132
|
-
try {
|
|
133
|
-
const data = await readFile(join(process.cwd(), 'tsconfig.json'), 'utf-8');
|
|
134
|
-
const { compilerOptions } = parse(data);
|
|
135
|
-
const paths = compilerOptions?.paths ?? null;
|
|
136
|
-
const baseUrl = compilerOptions?.baseUrl;
|
|
137
|
-
tsconfigCache.paths = paths || null;
|
|
138
|
-
tsconfigCache.baseUrl =
|
|
139
|
-
baseUrl ? join(process.cwd(), baseUrl) : process.cwd();
|
|
140
|
-
return tsconfigCache;
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
tsconfigCache.paths = null;
|
|
144
|
-
tsconfigCache.baseUrl = process.cwd();
|
|
145
|
-
return tsconfigCache;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
78
|
export async function resolve(specifier, ctx, next) {
|
|
149
|
-
|
|
150
|
-
const resolvedTsConfigPath = await resolveTsConfigPath(specifier, paths, baseUrl);
|
|
151
|
-
if (resolvedTsConfigPath) {
|
|
152
|
-
const url = pathToFileURL(resolvedTsConfigPath).href;
|
|
153
|
-
resolveCache.set(`${ctx.parentURL}::${specifier}`, url);
|
|
154
|
-
return {
|
|
155
|
-
url,
|
|
156
|
-
format: 'module',
|
|
157
|
-
shortCircuit: true
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
79
|
+
if (typeof ctx.parentURL !== 'string') {
|
|
161
80
|
return next(specifier, ctx);
|
|
162
81
|
}
|
|
163
|
-
if (
|
|
82
|
+
if (ctx.parentURL.startsWith('file://') === false) {
|
|
164
83
|
return next(specifier, ctx);
|
|
165
84
|
}
|
|
166
85
|
const cacheKey = `${ctx.parentURL}::${specifier}`;
|
|
@@ -176,28 +95,10 @@ export async function resolve(specifier, ctx, next) {
|
|
|
176
95
|
};
|
|
177
96
|
}
|
|
178
97
|
const parentPath = fileURLToPath(ctx.parentURL);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
basePath + '.ts',
|
|
184
|
-
basePath + '.tsx',
|
|
185
|
-
basePath + '.mts',
|
|
186
|
-
basePath + '.cts',
|
|
187
|
-
basePath + '.js',
|
|
188
|
-
basePath + '.mjs',
|
|
189
|
-
basePath + '.cjs',
|
|
190
|
-
join(basePath, 'index.ts'),
|
|
191
|
-
join(basePath, 'index.tsx'),
|
|
192
|
-
join(basePath, 'index.mts'),
|
|
193
|
-
join(basePath, 'index.cts'),
|
|
194
|
-
join(basePath, 'index.js'),
|
|
195
|
-
join(basePath, 'index.mjs'),
|
|
196
|
-
join(basePath, 'index.cjs')
|
|
197
|
-
];
|
|
198
|
-
for (const file of tryFiles) {
|
|
199
|
-
if (await existsWithCache(file)) {
|
|
200
|
-
const url = pathToFileURL(file).href;
|
|
98
|
+
try {
|
|
99
|
+
const result = await resolver.resolveFileAsync(parentPath, specifier);
|
|
100
|
+
if (result.path) {
|
|
101
|
+
const url = pathToFileURL(result.path).href;
|
|
201
102
|
resolveCache.set(cacheKey, url);
|
|
202
103
|
return {
|
|
203
104
|
url,
|
|
@@ -206,6 +107,10 @@ export async function resolve(specifier, ctx, next) {
|
|
|
206
107
|
};
|
|
207
108
|
}
|
|
208
109
|
}
|
|
110
|
+
catch {
|
|
111
|
+
resolveCache.set(cacheKey, null);
|
|
112
|
+
return next(specifier, ctx);
|
|
113
|
+
}
|
|
209
114
|
resolveCache.set(cacheKey, null);
|
|
210
115
|
return next(specifier, ctx);
|
|
211
116
|
}
|
|
@@ -213,14 +118,9 @@ export async function load(url, ctx, next) {
|
|
|
213
118
|
if (!url.startsWith('file://') || !hasTypeScriptExtension(url)) {
|
|
214
119
|
return next(url, ctx);
|
|
215
120
|
}
|
|
216
|
-
const { paths, baseUrl } = await loadTSConfig();
|
|
217
121
|
const filename = fileURLToPath(url);
|
|
218
122
|
const fileStats = await stat(filename);
|
|
219
|
-
const
|
|
220
|
-
baseUrl: baseUrl || process.cwd(),
|
|
221
|
-
paths: paths ?? {}
|
|
222
|
-
}));
|
|
223
|
-
const cachedCode = await readCachedTranspile(filename, fileStats.mtimeMs, fileStats.size, configHash);
|
|
123
|
+
const cachedCode = await readCachedTranspile(filename, fileStats.mtimeMs, fileStats.size, TRANSPILE_CONFIG_HASH);
|
|
224
124
|
if (cachedCode !== null) {
|
|
225
125
|
return { format: 'module', source: cachedCode, shortCircuit: true };
|
|
226
126
|
}
|
|
@@ -233,11 +133,8 @@ export async function load(url, ctx, next) {
|
|
|
233
133
|
},
|
|
234
134
|
sourcemap: true,
|
|
235
135
|
sourceType: 'module',
|
|
236
|
-
target: '
|
|
237
|
-
decorator: {
|
|
238
|
-
emitDecoratorMetadata: true,
|
|
239
|
-
legacy: true
|
|
240
|
-
}
|
|
136
|
+
target: 'esnext',
|
|
137
|
+
decorator: { legacy: true, emitDecoratorMetadata: true }
|
|
241
138
|
});
|
|
242
139
|
if (SHOULD_LOG_TRANSPILE) {
|
|
243
140
|
const transpileTimeMs = Date.now() - transpileStart;
|
|
@@ -248,7 +145,7 @@ export async function load(url, ctx, next) {
|
|
|
248
145
|
code,
|
|
249
146
|
mtimeMs: fileStats.mtimeMs,
|
|
250
147
|
size: fileStats.size,
|
|
251
|
-
configHash
|
|
148
|
+
configHash: TRANSPILE_CONFIG_HASH
|
|
252
149
|
});
|
|
253
150
|
return { format: 'module', source: code, shortCircuit: true };
|
|
254
151
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tsnite",
|
|
3
3
|
"description": "TypeScript at full throttle—fast, safe, unstoppable. 🚀",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.2",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
7
7
|
"esm",
|
|
@@ -31,8 +31,10 @@
|
|
|
31
31
|
"prepare": "husky"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@oxc-project/runtime": "^0.127.0",
|
|
34
35
|
"chokidar": "^5.0.0",
|
|
35
36
|
"commander": "^14.0.3",
|
|
37
|
+
"oxc-resolver": "^11.19.1",
|
|
36
38
|
"oxc-transform": "^0.127.0"
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
package/dist/parse.js
DELETED
|
@@ -1,435 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* JSON5 parse — zero dependencies, ESM + TypeScript
|
|
3
|
-
*
|
|
4
|
-
* Suporta toda a spec JSON5:
|
|
5
|
-
* - Chaves sem aspas (identificadores ES5/Unicode)
|
|
6
|
-
* - Strings single ou double quoted com escapes e line continuation
|
|
7
|
-
* - Comentários // e /* ... * /
|
|
8
|
-
* - Trailing commas em objetos e arrays
|
|
9
|
-
* - Números: hex, Infinity, NaN, +/- leading/trailing decimal
|
|
10
|
-
* - Reviver opcional (mesmo comportamento do JSON.parse nativo)
|
|
11
|
-
*/
|
|
12
|
-
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
13
|
-
function err(ctx, msg) {
|
|
14
|
-
throw new SyntaxError(`JSON5: ${msg} at ${ctx.line}:${ctx.col}`);
|
|
15
|
-
}
|
|
16
|
-
function current(ctx) {
|
|
17
|
-
return ctx.src[ctx.pos] ?? '';
|
|
18
|
-
}
|
|
19
|
-
function peek(ctx, offset = 1) {
|
|
20
|
-
return ctx.src[ctx.pos + offset] ?? '';
|
|
21
|
-
}
|
|
22
|
-
function advance(ctx) {
|
|
23
|
-
const ch = ctx.src[ctx.pos++] ?? '';
|
|
24
|
-
if (ch === '\n') {
|
|
25
|
-
ctx.line++;
|
|
26
|
-
ctx.col = 1;
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
ctx.col++;
|
|
30
|
-
}
|
|
31
|
-
return ch;
|
|
32
|
-
}
|
|
33
|
-
function startsWith(ctx, s) {
|
|
34
|
-
return ctx.src.startsWith(s, ctx.pos);
|
|
35
|
-
}
|
|
36
|
-
function consume(ctx, s) {
|
|
37
|
-
ctx.pos += s.length;
|
|
38
|
-
ctx.col += s.length;
|
|
39
|
-
}
|
|
40
|
-
// ─── Whitespace + Comments ────────────────────────────────────────────────────
|
|
41
|
-
const WHITESPACE = new Set([
|
|
42
|
-
' ',
|
|
43
|
-
'\t',
|
|
44
|
-
'\r',
|
|
45
|
-
'\n',
|
|
46
|
-
'\u00A0',
|
|
47
|
-
'\u2028',
|
|
48
|
-
'\u2029',
|
|
49
|
-
'\uFEFF'
|
|
50
|
-
]);
|
|
51
|
-
function skipWhitespaceAndComments(ctx) {
|
|
52
|
-
while (ctx.pos < ctx.src.length) {
|
|
53
|
-
const ch = current(ctx);
|
|
54
|
-
if (WHITESPACE.has(ch)) {
|
|
55
|
-
advance(ctx);
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
// Comentário de linha: //
|
|
59
|
-
if (ch === '/' && peek(ctx, 1) === '/') {
|
|
60
|
-
advance(ctx);
|
|
61
|
-
advance(ctx);
|
|
62
|
-
while (ctx.pos < ctx.src.length &&
|
|
63
|
-
current(ctx) !== '\n' &&
|
|
64
|
-
current(ctx) !== '\r' &&
|
|
65
|
-
current(ctx) !== '\u2028' &&
|
|
66
|
-
current(ctx) !== '\u2029')
|
|
67
|
-
advance(ctx);
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
// Comentário de bloco: /* ... */
|
|
71
|
-
if (ch === '/' && peek(ctx, 1) === '*') {
|
|
72
|
-
advance(ctx);
|
|
73
|
-
advance(ctx);
|
|
74
|
-
while (ctx.pos < ctx.src.length) {
|
|
75
|
-
if (current(ctx) === '*' && peek(ctx, 1) === '/') {
|
|
76
|
-
advance(ctx);
|
|
77
|
-
advance(ctx);
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
advance(ctx);
|
|
81
|
-
}
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
// ─── String ───────────────────────────────────────────────────────────────────
|
|
88
|
-
function parseString(ctx) {
|
|
89
|
-
const quote = advance(ctx); // ' ou "
|
|
90
|
-
let result = '';
|
|
91
|
-
while (ctx.pos < ctx.src.length) {
|
|
92
|
-
const ch = current(ctx);
|
|
93
|
-
if (ch === quote) {
|
|
94
|
-
advance(ctx);
|
|
95
|
-
return result;
|
|
96
|
-
}
|
|
97
|
-
if (ch === '\\') {
|
|
98
|
-
advance(ctx);
|
|
99
|
-
const esc = advance(ctx);
|
|
100
|
-
switch (esc) {
|
|
101
|
-
case '"':
|
|
102
|
-
result += '"';
|
|
103
|
-
break;
|
|
104
|
-
case "'":
|
|
105
|
-
result += "'";
|
|
106
|
-
break;
|
|
107
|
-
case '\\':
|
|
108
|
-
result += '\\';
|
|
109
|
-
break;
|
|
110
|
-
case '/':
|
|
111
|
-
result += '/';
|
|
112
|
-
break;
|
|
113
|
-
case 'b':
|
|
114
|
-
result += '\b';
|
|
115
|
-
break;
|
|
116
|
-
case 'f':
|
|
117
|
-
result += '\f';
|
|
118
|
-
break;
|
|
119
|
-
case 'n':
|
|
120
|
-
result += '\n';
|
|
121
|
-
break;
|
|
122
|
-
case 'r':
|
|
123
|
-
result += '\r';
|
|
124
|
-
break;
|
|
125
|
-
case 't':
|
|
126
|
-
result += '\t';
|
|
127
|
-
break;
|
|
128
|
-
case 'v':
|
|
129
|
-
result += '\v';
|
|
130
|
-
break;
|
|
131
|
-
case '0':
|
|
132
|
-
if (/[0-9]/.test(current(ctx)))
|
|
133
|
-
err(ctx, 'Octal escapes are not allowed');
|
|
134
|
-
result += '\0';
|
|
135
|
-
break;
|
|
136
|
-
case 'x': {
|
|
137
|
-
const hex = ctx.src.slice(ctx.pos, ctx.pos + 2);
|
|
138
|
-
if (!/^[0-9a-fA-F]{2}$/.test(hex))
|
|
139
|
-
err(ctx, 'Invalid hex escape sequence');
|
|
140
|
-
result += String.fromCharCode(parseInt(hex, 16));
|
|
141
|
-
ctx.pos += 2;
|
|
142
|
-
ctx.col += 2;
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
case 'u': {
|
|
146
|
-
if (current(ctx) === '{') {
|
|
147
|
-
// \u{XXXXXX}
|
|
148
|
-
advance(ctx);
|
|
149
|
-
let hexStr = '';
|
|
150
|
-
while (ctx.pos < ctx.src.length && current(ctx) !== '}')
|
|
151
|
-
hexStr += advance(ctx);
|
|
152
|
-
if (current(ctx) !== '}')
|
|
153
|
-
err(ctx, 'Unterminated unicode escape');
|
|
154
|
-
advance(ctx);
|
|
155
|
-
const cp = parseInt(hexStr, 16);
|
|
156
|
-
if (isNaN(cp) || cp > 0x10ffff)
|
|
157
|
-
err(ctx, `Invalid unicode code point: ${hexStr}`);
|
|
158
|
-
result += String.fromCodePoint(cp);
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
// \uXXXX
|
|
162
|
-
const hex = ctx.src.slice(ctx.pos, ctx.pos + 4);
|
|
163
|
-
if (!/^[0-9a-fA-F]{4}$/.test(hex))
|
|
164
|
-
err(ctx, 'Invalid unicode escape sequence');
|
|
165
|
-
result += String.fromCharCode(parseInt(hex, 16));
|
|
166
|
-
ctx.pos += 4;
|
|
167
|
-
ctx.col += 4;
|
|
168
|
-
}
|
|
169
|
-
break;
|
|
170
|
-
}
|
|
171
|
-
// Line continuation
|
|
172
|
-
case '\n':
|
|
173
|
-
case '\u2028':
|
|
174
|
-
case '\u2029':
|
|
175
|
-
break;
|
|
176
|
-
case '\r':
|
|
177
|
-
if (current(ctx) === '\n')
|
|
178
|
-
advance(ctx);
|
|
179
|
-
break;
|
|
180
|
-
default:
|
|
181
|
-
result += esc;
|
|
182
|
-
}
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
if (ch === '\n' || ch === '\r' || ch === '\u2028' || ch === '\u2029') {
|
|
186
|
-
err(ctx, 'Unterminated string literal');
|
|
187
|
-
}
|
|
188
|
-
result += advance(ctx);
|
|
189
|
-
}
|
|
190
|
-
err(ctx, 'Unterminated string literal');
|
|
191
|
-
}
|
|
192
|
-
// ─── Identifier (chave sem aspas) ─────────────────────────────────────────────
|
|
193
|
-
const IDENT_START = /[\p{L}\p{Nl}$_]/u;
|
|
194
|
-
const IDENT_PART = /[\p{L}\p{Nl}\p{Mn}\p{Mc}\p{Nd}\p{Pc}$_\u200C\u200D]/u;
|
|
195
|
-
function isIdentStart(ch) {
|
|
196
|
-
return IDENT_START.test(ch);
|
|
197
|
-
}
|
|
198
|
-
function isIdentPart(ch) {
|
|
199
|
-
return IDENT_PART.test(ch);
|
|
200
|
-
}
|
|
201
|
-
function parseIdentifier(ctx) {
|
|
202
|
-
const first = current(ctx);
|
|
203
|
-
if (!isIdentStart(first))
|
|
204
|
-
err(ctx, `Unexpected character: ${JSON.stringify(first)}`);
|
|
205
|
-
let name = advance(ctx);
|
|
206
|
-
while (ctx.pos < ctx.src.length && isIdentPart(current(ctx))) {
|
|
207
|
-
name += advance(ctx);
|
|
208
|
-
}
|
|
209
|
-
return name;
|
|
210
|
-
}
|
|
211
|
-
// ─── Number ───────────────────────────────────────────────────────────────────
|
|
212
|
-
function parseNumber(ctx) {
|
|
213
|
-
let numStr = '';
|
|
214
|
-
// Sinal opcional
|
|
215
|
-
if (current(ctx) === '+' || current(ctx) === '-') {
|
|
216
|
-
numStr += advance(ctx);
|
|
217
|
-
}
|
|
218
|
-
// Infinity
|
|
219
|
-
if (startsWith(ctx, 'Infinity')) {
|
|
220
|
-
consume(ctx, 'Infinity');
|
|
221
|
-
return numStr === '-' ? -Infinity : Infinity;
|
|
222
|
-
}
|
|
223
|
-
// NaN
|
|
224
|
-
if (startsWith(ctx, 'NaN')) {
|
|
225
|
-
consume(ctx, 'NaN');
|
|
226
|
-
return NaN;
|
|
227
|
-
}
|
|
228
|
-
// Hexadecimal: 0x / 0X
|
|
229
|
-
if (current(ctx) === '0' && (peek(ctx, 1) === 'x' || peek(ctx, 1) === 'X')) {
|
|
230
|
-
const sign = numStr; // pode ser '-' ou ''
|
|
231
|
-
numStr += advance(ctx); // '0'
|
|
232
|
-
numStr += advance(ctx); // 'x'
|
|
233
|
-
if (!/[0-9a-fA-F]/.test(current(ctx)))
|
|
234
|
-
err(ctx, 'Invalid hexadecimal number');
|
|
235
|
-
while (ctx.pos < ctx.src.length && /[0-9a-fA-F_]/.test(current(ctx))) {
|
|
236
|
-
const ch = advance(ctx);
|
|
237
|
-
if (ch !== '_')
|
|
238
|
-
numStr += ch;
|
|
239
|
-
}
|
|
240
|
-
const abs = parseInt(numStr, 16); // parseInt ignora o prefixo 0x corretamente
|
|
241
|
-
return sign === '-' ? -abs : abs;
|
|
242
|
-
}
|
|
243
|
-
// Parte inteira
|
|
244
|
-
while (ctx.pos < ctx.src.length && /[0-9_]/.test(current(ctx))) {
|
|
245
|
-
const ch = advance(ctx);
|
|
246
|
-
if (ch !== '_')
|
|
247
|
-
numStr += ch;
|
|
248
|
-
}
|
|
249
|
-
// Parte decimal
|
|
250
|
-
if (ctx.pos < ctx.src.length && current(ctx) === '.') {
|
|
251
|
-
numStr += advance(ctx);
|
|
252
|
-
while (ctx.pos < ctx.src.length && /[0-9_]/.test(current(ctx))) {
|
|
253
|
-
const ch = advance(ctx);
|
|
254
|
-
if (ch !== '_')
|
|
255
|
-
numStr += ch;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
// Expoente
|
|
259
|
-
if (ctx.pos < ctx.src.length &&
|
|
260
|
-
(current(ctx) === 'e' || current(ctx) === 'E')) {
|
|
261
|
-
numStr += advance(ctx);
|
|
262
|
-
if (current(ctx) === '+' || current(ctx) === '-')
|
|
263
|
-
numStr += advance(ctx);
|
|
264
|
-
while (ctx.pos < ctx.src.length && /[0-9_]/.test(current(ctx))) {
|
|
265
|
-
const ch = advance(ctx);
|
|
266
|
-
if (ch !== '_')
|
|
267
|
-
numStr += ch;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
const n = Number(numStr);
|
|
271
|
-
if (isNaN(n))
|
|
272
|
-
err(ctx, `Invalid number: ${numStr}`);
|
|
273
|
-
return n;
|
|
274
|
-
}
|
|
275
|
-
// ─── Value ────────────────────────────────────────────────────────────────────
|
|
276
|
-
function parseValue(ctx) {
|
|
277
|
-
skipWhitespaceAndComments(ctx);
|
|
278
|
-
if (ctx.pos >= ctx.src.length)
|
|
279
|
-
err(ctx, 'Unexpected end of input');
|
|
280
|
-
const ch = current(ctx);
|
|
281
|
-
if (ch === '"' || ch === "'")
|
|
282
|
-
return parseString(ctx);
|
|
283
|
-
if (ch === '{')
|
|
284
|
-
return parseObject(ctx);
|
|
285
|
-
if (ch === '[')
|
|
286
|
-
return parseArray(ctx);
|
|
287
|
-
if (startsWith(ctx, 'true')) {
|
|
288
|
-
consume(ctx, 'true');
|
|
289
|
-
return true;
|
|
290
|
-
}
|
|
291
|
-
if (startsWith(ctx, 'false')) {
|
|
292
|
-
consume(ctx, 'false');
|
|
293
|
-
return false;
|
|
294
|
-
}
|
|
295
|
-
if (startsWith(ctx, 'null')) {
|
|
296
|
-
consume(ctx, 'null');
|
|
297
|
-
return null;
|
|
298
|
-
}
|
|
299
|
-
if (ch === '+' ||
|
|
300
|
-
ch === '-' ||
|
|
301
|
-
ch === '.' ||
|
|
302
|
-
(ch >= '0' && ch <= '9') ||
|
|
303
|
-
startsWith(ctx, 'Infinity') ||
|
|
304
|
-
startsWith(ctx, 'NaN'))
|
|
305
|
-
return parseNumber(ctx);
|
|
306
|
-
err(ctx, `Unexpected token: ${JSON.stringify(ch)}`);
|
|
307
|
-
}
|
|
308
|
-
// ─── Object ───────────────────────────────────────────────────────────────────
|
|
309
|
-
function parseObject(ctx) {
|
|
310
|
-
advance(ctx); // {
|
|
311
|
-
const obj = {};
|
|
312
|
-
skipWhitespaceAndComments(ctx);
|
|
313
|
-
if (current(ctx) === '}') {
|
|
314
|
-
advance(ctx);
|
|
315
|
-
return obj;
|
|
316
|
-
}
|
|
317
|
-
while (ctx.pos < ctx.src.length) {
|
|
318
|
-
skipWhitespaceAndComments(ctx);
|
|
319
|
-
if (ctx.pos >= ctx.src.length)
|
|
320
|
-
err(ctx, 'Unterminated object');
|
|
321
|
-
// Trailing comma
|
|
322
|
-
if (current(ctx) === '}') {
|
|
323
|
-
advance(ctx);
|
|
324
|
-
return obj;
|
|
325
|
-
}
|
|
326
|
-
// Chave: string ou identifier
|
|
327
|
-
const ch = current(ctx);
|
|
328
|
-
const key = ch === '"' || ch === "'" ? parseString(ctx) : parseIdentifier(ctx);
|
|
329
|
-
skipWhitespaceAndComments(ctx);
|
|
330
|
-
if (current(ctx) !== ':')
|
|
331
|
-
err(ctx, `Expected ':' after key ${JSON.stringify(key)}`);
|
|
332
|
-
advance(ctx); // :
|
|
333
|
-
const entry = [key, parseValue(ctx)];
|
|
334
|
-
obj[entry[0]] = entry[1];
|
|
335
|
-
skipWhitespaceAndComments(ctx);
|
|
336
|
-
if (current(ctx) === ',') {
|
|
337
|
-
advance(ctx);
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
if (current(ctx) === '}') {
|
|
341
|
-
advance(ctx);
|
|
342
|
-
return obj;
|
|
343
|
-
}
|
|
344
|
-
err(ctx, 'Expected "," or "}" in object');
|
|
345
|
-
}
|
|
346
|
-
err(ctx, 'Unterminated object');
|
|
347
|
-
}
|
|
348
|
-
// ─── Array ────────────────────────────────────────────────────────────────────
|
|
349
|
-
function parseArray(ctx) {
|
|
350
|
-
advance(ctx); // [
|
|
351
|
-
const arr = [];
|
|
352
|
-
skipWhitespaceAndComments(ctx);
|
|
353
|
-
if (current(ctx) === ']') {
|
|
354
|
-
advance(ctx);
|
|
355
|
-
return arr;
|
|
356
|
-
}
|
|
357
|
-
while (ctx.pos < ctx.src.length) {
|
|
358
|
-
skipWhitespaceAndComments(ctx);
|
|
359
|
-
if (ctx.pos >= ctx.src.length)
|
|
360
|
-
err(ctx, 'Unterminated array');
|
|
361
|
-
// Trailing comma
|
|
362
|
-
if (current(ctx) === ']') {
|
|
363
|
-
advance(ctx);
|
|
364
|
-
return arr;
|
|
365
|
-
}
|
|
366
|
-
arr.push(parseValue(ctx));
|
|
367
|
-
skipWhitespaceAndComments(ctx);
|
|
368
|
-
if (current(ctx) === ',') {
|
|
369
|
-
advance(ctx);
|
|
370
|
-
continue;
|
|
371
|
-
}
|
|
372
|
-
if (current(ctx) === ']') {
|
|
373
|
-
advance(ctx);
|
|
374
|
-
return arr;
|
|
375
|
-
}
|
|
376
|
-
err(ctx, 'Expected "," or "]" in array');
|
|
377
|
-
}
|
|
378
|
-
err(ctx, 'Unterminated array');
|
|
379
|
-
}
|
|
380
|
-
function applyReviver(reviver, holder, key) {
|
|
381
|
-
const val = holder[key];
|
|
382
|
-
if (val !== null && typeof val === 'object') {
|
|
383
|
-
const obj = val;
|
|
384
|
-
for (const k of Object.keys(obj)) {
|
|
385
|
-
const newVal = applyReviver(reviver, obj, k);
|
|
386
|
-
if (newVal === undefined)
|
|
387
|
-
delete obj[k];
|
|
388
|
-
else
|
|
389
|
-
obj[k] = newVal;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
return reviver.call(holder, key, val);
|
|
393
|
-
}
|
|
394
|
-
// ─── API pública ──────────────────────────────────────────────────────────────
|
|
395
|
-
/**
|
|
396
|
-
* Faz o parse de uma string JSON5 e retorna o valor JavaScript correspondente.
|
|
397
|
-
*
|
|
398
|
-
* @param source - String JSON5 a ser parseada.
|
|
399
|
-
* @param reviver - Função opcional chamada para cada par chave/valor (igual ao `JSON.parse`).
|
|
400
|
-
* @returns O valor JavaScript resultante tipado como `T`.
|
|
401
|
-
*
|
|
402
|
-
* @throws {SyntaxError} Se a string não for JSON5 válido.
|
|
403
|
-
*
|
|
404
|
-
* @example
|
|
405
|
-
* ```ts
|
|
406
|
-
* // Sem generic — retorna Json5Value
|
|
407
|
-
* const raw = parse(`{ host: 'localhost' }`)
|
|
408
|
-
*
|
|
409
|
-
* // Com generic — cast para o tipo desejado
|
|
410
|
-
* interface TsConfig {
|
|
411
|
-
* compilerOptions: {
|
|
412
|
-
* paths: Record<string, string[]>
|
|
413
|
-
* baseUrl: string
|
|
414
|
-
* }
|
|
415
|
-
* }
|
|
416
|
-
* const { compilerOptions: { paths, baseUrl } } = parse<TsConfig>(data)
|
|
417
|
-
* ```
|
|
418
|
-
*/
|
|
419
|
-
export function parse(source, reviver) {
|
|
420
|
-
const ctx = {
|
|
421
|
-
src: String(source),
|
|
422
|
-
pos: 0,
|
|
423
|
-
line: 1,
|
|
424
|
-
col: 1
|
|
425
|
-
};
|
|
426
|
-
const result = parseValue(ctx);
|
|
427
|
-
skipWhitespaceAndComments(ctx);
|
|
428
|
-
if (ctx.pos < ctx.src.length) {
|
|
429
|
-
err(ctx, `Unexpected token after value: ${JSON.stringify(current(ctx))}`);
|
|
430
|
-
}
|
|
431
|
-
if (typeof reviver === 'function') {
|
|
432
|
-
return applyReviver(reviver, { '': result }, '');
|
|
433
|
-
}
|
|
434
|
-
return result;
|
|
435
|
-
}
|