tsnite 0.2.1 → 0.2.3
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 +21 -1
- package/dist/loader.js +152 -43
- package/dist/parse.js +435 -0
- package/package.json +4 -4
package/dist/cache.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { mkdir, rm } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, rm, stat } from 'node:fs/promises';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
export const resolveCache = new Map();
|
|
5
|
+
const statCache = new Map();
|
|
5
6
|
function getTranspileCacheDir() {
|
|
6
7
|
return join(import.meta.dirname, '..', 'node_modules', '.cache', 'tsnite');
|
|
7
8
|
}
|
|
@@ -11,9 +12,27 @@ function getTSConfigPath() {
|
|
|
11
12
|
function hash(value) {
|
|
12
13
|
return createHash('sha1').update(value).digest('hex');
|
|
13
14
|
}
|
|
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
|
+
}
|
|
14
30
|
export function clearResolveCache() {
|
|
15
31
|
resolveCache.clear();
|
|
16
32
|
}
|
|
33
|
+
export function invalidateStatCache(filePath) {
|
|
34
|
+
statCache.delete(filePath);
|
|
35
|
+
}
|
|
17
36
|
export function getTranspileCacheFile(filePath) {
|
|
18
37
|
return join(getTranspileCacheDir(), `${hash(filePath)}.json`);
|
|
19
38
|
}
|
|
@@ -27,6 +46,7 @@ export function isTSConfigPath(filePath) {
|
|
|
27
46
|
return resolve(filePath) === getTSConfigPath();
|
|
28
47
|
}
|
|
29
48
|
export async function invalidateFileCaches(filePath) {
|
|
49
|
+
invalidateStatCache(filePath);
|
|
30
50
|
clearResolveCache();
|
|
31
51
|
if (isTSConfigPath(filePath)) {
|
|
32
52
|
await clearTranspileCache();
|
package/dist/loader.js
CHANGED
|
@@ -1,25 +1,72 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ResolverFactory } from 'oxc-resolver';
|
|
1
|
+
import { transformFile } from '@swc/core';
|
|
3
2
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
3
|
import { readFile, stat, writeFile } from 'node:fs/promises';
|
|
5
|
-
import {
|
|
6
|
-
import { relative } from 'node:path';
|
|
4
|
+
import { extname, join, dirname } from 'node:path';
|
|
7
5
|
import { createHash } from 'node:crypto';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
6
|
+
import { parse } from './parse.js';
|
|
7
|
+
import { ensureTranspileCacheDir, existsWithCache, getTranspileCacheFile, resolveCache } from './cache.js';
|
|
8
|
+
const tsconfigCache = { paths: null, baseUrl: null };
|
|
10
9
|
const transpileCache = new Map();
|
|
11
10
|
const MAX_TRANSPILE_CACHE_ENTRIES = 256;
|
|
12
11
|
const TS_EXTENSIONS = ['.cts', '.mts', '.tsx', '.ts'];
|
|
13
|
-
const SHOULD_LOG_TRANSPILE = process.env.TSNITE_LOG_TRANSPILE === '1';
|
|
14
|
-
const TRANSPILE_CONFIG_HASH = hash('target:es2024|jsx:automatic|decorator:legacy');
|
|
15
|
-
const resolver = new ResolverFactory({
|
|
16
|
-
conditionNames: ['node', 'import'],
|
|
17
|
-
extensions: ['.cts', '.mts', '.tsx', '.ts', '.cjs', '.mjs', '.js', '.json'],
|
|
18
|
-
tsconfig: 'auto'
|
|
19
|
-
});
|
|
20
12
|
function hasTypeScriptExtension(value) {
|
|
21
13
|
return TS_EXTENSIONS.some((extension) => value.endsWith(extension));
|
|
22
14
|
}
|
|
15
|
+
function isTypeScriptSpecifier(specifier) {
|
|
16
|
+
const extension = extname(specifier);
|
|
17
|
+
return extension === '' || hasTypeScriptExtension(extension);
|
|
18
|
+
}
|
|
19
|
+
function getTypeScriptTryFiles(basePath) {
|
|
20
|
+
return [
|
|
21
|
+
basePath,
|
|
22
|
+
...TS_EXTENSIONS.map((extension) => basePath + extension),
|
|
23
|
+
...TS_EXTENSIONS.map((extension) => join(basePath, 'index' + extension))
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
async function resolveTypeScriptFile(basePath) {
|
|
27
|
+
for (const file of getTypeScriptTryFiles(basePath)) {
|
|
28
|
+
if (await existsWithCache(file)) {
|
|
29
|
+
return file;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function matchPathPattern(specifier, pattern) {
|
|
35
|
+
const starIndex = pattern.indexOf('*');
|
|
36
|
+
if (starIndex === -1) {
|
|
37
|
+
return specifier === pattern ? [] : null;
|
|
38
|
+
}
|
|
39
|
+
const prefix = pattern.slice(0, starIndex);
|
|
40
|
+
const suffix = pattern.slice(starIndex + 1);
|
|
41
|
+
if (!specifier.startsWith(prefix) || !specifier.endsWith(suffix)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return [specifier.slice(prefix.length, specifier.length - suffix.length)];
|
|
45
|
+
}
|
|
46
|
+
function applyPathMapping(target, matches) {
|
|
47
|
+
let mapped = target;
|
|
48
|
+
for (const match of matches) {
|
|
49
|
+
mapped = mapped.replace('*', match);
|
|
50
|
+
}
|
|
51
|
+
return mapped;
|
|
52
|
+
}
|
|
53
|
+
async function resolveTsConfigPath(specifier, paths, baseUrl) {
|
|
54
|
+
if (!paths || !baseUrl || !isTypeScriptSpecifier(specifier)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
for (const [pattern, targets] of Object.entries(paths)) {
|
|
58
|
+
const matches = matchPathPattern(specifier, pattern);
|
|
59
|
+
if (!matches)
|
|
60
|
+
continue;
|
|
61
|
+
for (const target of targets) {
|
|
62
|
+
const mappedTarget = applyPathMapping(target, matches);
|
|
63
|
+
const resolved = await resolveTypeScriptFile(join(baseUrl, mappedTarget));
|
|
64
|
+
if (resolved)
|
|
65
|
+
return resolved;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
23
70
|
function hash(value) {
|
|
24
71
|
return createHash('sha1').update(value).digest('hex');
|
|
25
72
|
}
|
|
@@ -75,11 +122,42 @@ async function writeCachedTranspile(filename, entry) {
|
|
|
75
122
|
// Ignore cache write failures and continue with fresh output.
|
|
76
123
|
}
|
|
77
124
|
}
|
|
125
|
+
async function loadTSConfig() {
|
|
126
|
+
if (tsconfigCache.paths !== null && tsconfigCache.baseUrl !== null) {
|
|
127
|
+
return tsconfigCache;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const data = await readFile(join(process.cwd(), 'tsconfig.json'), 'utf-8');
|
|
131
|
+
const { compilerOptions } = parse(data);
|
|
132
|
+
const paths = compilerOptions?.paths ?? null;
|
|
133
|
+
const baseUrl = compilerOptions?.baseUrl;
|
|
134
|
+
tsconfigCache.paths = paths || null;
|
|
135
|
+
tsconfigCache.baseUrl =
|
|
136
|
+
baseUrl ? join(process.cwd(), baseUrl) : process.cwd();
|
|
137
|
+
return tsconfigCache;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
tsconfigCache.paths = null;
|
|
141
|
+
tsconfigCache.baseUrl = process.cwd();
|
|
142
|
+
return tsconfigCache;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
78
145
|
export async function resolve(specifier, ctx, next) {
|
|
79
|
-
|
|
146
|
+
const { paths, baseUrl } = await loadTSConfig();
|
|
147
|
+
const resolvedTsConfigPath = await resolveTsConfigPath(specifier, paths, baseUrl);
|
|
148
|
+
if (resolvedTsConfigPath) {
|
|
149
|
+
const url = pathToFileURL(resolvedTsConfigPath).href;
|
|
150
|
+
resolveCache.set(`${ctx.parentURL}::${specifier}`, url);
|
|
151
|
+
return {
|
|
152
|
+
url,
|
|
153
|
+
format: 'module',
|
|
154
|
+
shortCircuit: true
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
80
158
|
return next(specifier, ctx);
|
|
81
159
|
}
|
|
82
|
-
if (
|
|
160
|
+
if (!isTypeScriptSpecifier(specifier)) {
|
|
83
161
|
return next(specifier, ctx);
|
|
84
162
|
}
|
|
85
163
|
const cacheKey = `${ctx.parentURL}::${specifier}`;
|
|
@@ -95,10 +173,28 @@ export async function resolve(specifier, ctx, next) {
|
|
|
95
173
|
};
|
|
96
174
|
}
|
|
97
175
|
const parentPath = fileURLToPath(ctx.parentURL);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
176
|
+
const parentDir = dirname(parentPath);
|
|
177
|
+
const basePath = join(parentDir, specifier);
|
|
178
|
+
const tryFiles = [
|
|
179
|
+
basePath,
|
|
180
|
+
basePath + '.ts',
|
|
181
|
+
basePath + '.tsx',
|
|
182
|
+
basePath + '.mts',
|
|
183
|
+
basePath + '.cts',
|
|
184
|
+
basePath + '.js',
|
|
185
|
+
basePath + '.mjs',
|
|
186
|
+
basePath + '.cjs',
|
|
187
|
+
join(basePath, 'index.ts'),
|
|
188
|
+
join(basePath, 'index.tsx'),
|
|
189
|
+
join(basePath, 'index.mts'),
|
|
190
|
+
join(basePath, 'index.cts'),
|
|
191
|
+
join(basePath, 'index.js'),
|
|
192
|
+
join(basePath, 'index.mjs'),
|
|
193
|
+
join(basePath, 'index.cjs')
|
|
194
|
+
];
|
|
195
|
+
for (const file of tryFiles) {
|
|
196
|
+
if (await existsWithCache(file)) {
|
|
197
|
+
const url = pathToFileURL(file).href;
|
|
102
198
|
resolveCache.set(cacheKey, url);
|
|
103
199
|
return {
|
|
104
200
|
url,
|
|
@@ -107,10 +203,6 @@ export async function resolve(specifier, ctx, next) {
|
|
|
107
203
|
};
|
|
108
204
|
}
|
|
109
205
|
}
|
|
110
|
-
catch {
|
|
111
|
-
resolveCache.set(cacheKey, null);
|
|
112
|
-
return next(specifier, ctx);
|
|
113
|
-
}
|
|
114
206
|
resolveCache.set(cacheKey, null);
|
|
115
207
|
return next(specifier, ctx);
|
|
116
208
|
}
|
|
@@ -118,37 +210,54 @@ export async function load(url, ctx, next) {
|
|
|
118
210
|
if (!url.startsWith('file://') || !hasTypeScriptExtension(url)) {
|
|
119
211
|
return next(url, ctx);
|
|
120
212
|
}
|
|
213
|
+
const { paths, baseUrl } = await loadTSConfig();
|
|
121
214
|
const filename = fileURLToPath(url);
|
|
122
215
|
const fileStats = await stat(filename);
|
|
123
|
-
const
|
|
216
|
+
const configHash = hash(JSON.stringify({
|
|
217
|
+
baseUrl: baseUrl || process.cwd(),
|
|
218
|
+
paths: paths ?? {}
|
|
219
|
+
}));
|
|
220
|
+
const cachedCode = await readCachedTranspile(filename, fileStats.mtimeMs, fileStats.size, configHash);
|
|
124
221
|
if (cachedCode !== null) {
|
|
125
222
|
return { format: 'module', source: cachedCode, shortCircuit: true };
|
|
126
223
|
}
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
224
|
+
const { code } = await transformFile(filename, {
|
|
225
|
+
filename,
|
|
226
|
+
jsc: {
|
|
227
|
+
baseUrl: baseUrl || process.cwd(),
|
|
228
|
+
parser: {
|
|
229
|
+
syntax: 'typescript',
|
|
230
|
+
tsx: url.endsWith('.tsx'),
|
|
231
|
+
decorators: true
|
|
232
|
+
},
|
|
233
|
+
target: 'es2022',
|
|
234
|
+
keepClassNames: true,
|
|
235
|
+
experimental: {
|
|
236
|
+
emitAssertForImportAttributes: true
|
|
237
|
+
},
|
|
238
|
+
transform: {
|
|
239
|
+
decoratorMetadata: true,
|
|
240
|
+
legacyDecorator: true,
|
|
241
|
+
react: {
|
|
242
|
+
runtime: 'automatic',
|
|
243
|
+
development: true
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
paths: paths ?? {}
|
|
133
247
|
},
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
248
|
+
module: {
|
|
249
|
+
type: 'es6',
|
|
250
|
+
strict: true,
|
|
251
|
+
outFileExtension: 'ts',
|
|
252
|
+
resolveFully: true
|
|
253
|
+
},
|
|
254
|
+
sourceMaps: 'inline'
|
|
141
255
|
});
|
|
142
|
-
if (SHOULD_LOG_TRANSPILE) {
|
|
143
|
-
const transpileTimeMs = Date.now() - transpileStart;
|
|
144
|
-
const displayPath = relative(process.cwd(), filename);
|
|
145
|
-
writeSync(1, `${gray('Transpiled')} ${yellow(displayPath)} ${gray('in')} ${green(`${transpileTimeMs}ms`)}\n`);
|
|
146
|
-
}
|
|
147
256
|
await writeCachedTranspile(filename, {
|
|
148
257
|
code,
|
|
149
258
|
mtimeMs: fileStats.mtimeMs,
|
|
150
259
|
size: fileStats.size,
|
|
151
|
-
configHash
|
|
260
|
+
configHash
|
|
152
261
|
});
|
|
153
262
|
return { format: 'module', source: code, shortCircuit: true };
|
|
154
263
|
}
|
package/dist/parse.js
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
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
|
+
}
|
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.3",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
7
7
|
"esm",
|
|
@@ -31,10 +31,9 @@
|
|
|
31
31
|
"prepare": "husky"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@swc/core": "^1.15.30",
|
|
34
35
|
"chokidar": "^5.0.0",
|
|
35
|
-
"commander": "^14.0.3"
|
|
36
|
-
"oxc-resolver": "^11.19.1",
|
|
37
|
-
"oxc-transform": "^0.127.0"
|
|
36
|
+
"commander": "^14.0.3"
|
|
38
37
|
},
|
|
39
38
|
"devDependencies": {
|
|
40
39
|
"@commitlint/cli": "^20.5.0",
|
|
@@ -51,6 +50,7 @@
|
|
|
51
50
|
"jest-environment-node": "^30.3.0",
|
|
52
51
|
"lint-staged": "^16.4.0",
|
|
53
52
|
"prettier": "^3.8.3",
|
|
53
|
+
"rimraf": "^6.1.3",
|
|
54
54
|
"tsc-alias": "^1.8.16",
|
|
55
55
|
"typescript": "^6.0.3",
|
|
56
56
|
"typescript-eslint": "^8.59.0"
|