@ttsc/unplugin 0.8.1 → 0.9.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 +8 -9
- package/lib/api.js +3 -2
- package/lib/api.js.map +1 -1
- package/lib/api.mjs +1 -1
- package/lib/bun.js +1 -1
- package/lib/bun.js.map +1 -1
- package/lib/bun.mjs +1 -1
- package/lib/bun.mjs.map +1 -1
- package/lib/core/index.d.ts +2 -2
- package/lib/core/index.js +7 -2
- package/lib/core/index.js.map +1 -1
- package/lib/core/index.mjs +8 -4
- package/lib/core/index.mjs.map +1 -1
- package/lib/core/options.d.ts +3 -3
- package/lib/core/transform.d.ts +10 -2
- package/lib/core/transform.js +231 -75
- package/lib/core/transform.js.map +1 -1
- package/lib/core/transform.mjs +232 -58
- package/lib/core/transform.mjs.map +1 -1
- package/package.json +4 -7
- package/src/bun.ts +1 -1
- package/src/core/index.ts +14 -4
- package/src/core/options.ts +3 -3
- package/src/core/transform.ts +301 -60
package/src/core/transform.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import * as Diff from "diff-match-patch-es";
|
|
2
1
|
import fs from "node:fs";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
|
-
import MagicString from "magic-string";
|
|
5
5
|
import type {
|
|
6
6
|
ITtscCompilerDiagnostic,
|
|
7
7
|
ITtscCompilerTransformation,
|
|
@@ -21,11 +21,27 @@ export interface TtscTransformAlias {
|
|
|
21
21
|
replacement: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export interface TtscCachedProjectTransform {
|
|
25
|
+
inputHashes: Record<string, string>;
|
|
26
|
+
projectRoot: string;
|
|
27
|
+
result: ITtscCompilerTransformation;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type TtscTransformCache = Map<
|
|
31
|
+
string,
|
|
32
|
+
Promise<TtscCachedProjectTransform>
|
|
33
|
+
>;
|
|
34
|
+
|
|
35
|
+
export function createTtscTransformCache(): TtscTransformCache {
|
|
36
|
+
return new Map();
|
|
37
|
+
}
|
|
38
|
+
|
|
24
39
|
export async function transformTtsc(
|
|
25
40
|
id: string,
|
|
26
41
|
source: string,
|
|
27
42
|
options: ResolvedTtscUnpluginOptions,
|
|
28
43
|
aliases?: unknown,
|
|
44
|
+
cache?: TtscTransformCache,
|
|
29
45
|
): Promise<TtscTransformResult | undefined> {
|
|
30
46
|
const clean = stripQuery(id);
|
|
31
47
|
if (clean.includes("\0")) {
|
|
@@ -40,28 +56,45 @@ export async function transformTtsc(
|
|
|
40
56
|
const tsconfigDir = path.dirname(tsconfig);
|
|
41
57
|
const baseUrl = resolveBaseUrl(tsconfigDir, options.compilerOptions);
|
|
42
58
|
const aliasPaths = createAliasPaths(baseUrl, aliases);
|
|
43
|
-
const
|
|
59
|
+
const key = createTransformCacheKey({
|
|
44
60
|
aliasPaths,
|
|
45
|
-
baseUrl,
|
|
46
61
|
compilerOptions: options.compilerOptions,
|
|
62
|
+
plugins: options.plugins,
|
|
47
63
|
tsconfig,
|
|
48
64
|
});
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
|
|
66
|
+
let transformed = cache?.get(key);
|
|
67
|
+
if (transformed !== undefined) {
|
|
68
|
+
const cached = await transformed;
|
|
69
|
+
if (matchesCachedSource(cached, file, source)) {
|
|
70
|
+
reportSuccessDiagnostics(cached.result);
|
|
71
|
+
const code = selectTransformedSource({
|
|
72
|
+
file,
|
|
73
|
+
projectRoot: cached.projectRoot,
|
|
74
|
+
result: cached.result,
|
|
75
|
+
});
|
|
76
|
+
return createTransformResult(source, code);
|
|
77
|
+
}
|
|
78
|
+
cache?.delete(key);
|
|
79
|
+
transformed = undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (transformed === undefined) {
|
|
83
|
+
transformed = transformProject({
|
|
84
|
+
aliasPaths,
|
|
85
|
+
baseUrl,
|
|
86
|
+
compilerOptions: options.compilerOptions,
|
|
87
|
+
currentFile: file,
|
|
88
|
+
currentSource: source,
|
|
53
89
|
plugins: options.plugins,
|
|
54
|
-
tsconfig
|
|
55
|
-
}).transform();
|
|
56
|
-
const code = selectTransformedSource({
|
|
57
|
-
file,
|
|
58
|
-
projectRoot: path.dirname(effectiveTsconfig),
|
|
59
|
-
result,
|
|
90
|
+
tsconfig,
|
|
60
91
|
});
|
|
61
|
-
|
|
62
|
-
} finally {
|
|
63
|
-
configured.dispose();
|
|
92
|
+
cache?.set(key, transformed);
|
|
64
93
|
}
|
|
94
|
+
const { projectRoot, result } = await transformed;
|
|
95
|
+
reportSuccessDiagnostics(result);
|
|
96
|
+
const code = selectTransformedSource({ file, projectRoot, result });
|
|
97
|
+
return createTransformResult(source, code);
|
|
65
98
|
}
|
|
66
99
|
|
|
67
100
|
export function stripQuery(id: string): string {
|
|
@@ -79,49 +112,157 @@ export function isDeclarationFile(id: string): boolean {
|
|
|
79
112
|
export function createTransformResult(
|
|
80
113
|
source: string,
|
|
81
114
|
code: string,
|
|
82
|
-
id: string,
|
|
83
115
|
): TtscTransformResult | undefined {
|
|
84
116
|
if (source === code) {
|
|
85
117
|
return undefined;
|
|
86
118
|
}
|
|
119
|
+
return { code };
|
|
120
|
+
}
|
|
87
121
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
122
|
+
function matchesCachedSource(
|
|
123
|
+
cached: TtscCachedProjectTransform,
|
|
124
|
+
file: string,
|
|
125
|
+
source: string,
|
|
126
|
+
): boolean {
|
|
127
|
+
const currentKey = toProjectKey(cached.projectRoot, file);
|
|
128
|
+
const currentHashes = collectProjectInputHashes(cached.projectRoot);
|
|
129
|
+
currentHashes[currentKey] = hashText(source);
|
|
130
|
+
return sameHashes(cached.inputHashes, currentHashes);
|
|
131
|
+
}
|
|
91
132
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
133
|
+
function collectInputHashes(props: {
|
|
134
|
+
currentFile: string;
|
|
135
|
+
currentSource: string;
|
|
136
|
+
projectRoot: string;
|
|
137
|
+
result: ITtscCompilerTransformation;
|
|
138
|
+
}): Record<string, string> {
|
|
139
|
+
const hashes = collectProjectInputHashes(props.projectRoot);
|
|
140
|
+
if (props.result.type !== "exception") {
|
|
141
|
+
for (const key of Object.keys(props.result.typescript)) {
|
|
142
|
+
const file = path.resolve(props.projectRoot, key);
|
|
143
|
+
try {
|
|
144
|
+
hashes[key] = hashText(fs.readFileSync(file, "utf8"));
|
|
145
|
+
} catch {
|
|
146
|
+
// A plugin may synthesize a virtual TypeScript file. It should not
|
|
147
|
+
// decide cache reuse for real source files.
|
|
148
|
+
}
|
|
98
149
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
150
|
+
}
|
|
151
|
+
hashes[toProjectKey(props.projectRoot, props.currentFile)] = hashText(
|
|
152
|
+
props.currentSource,
|
|
153
|
+
);
|
|
154
|
+
return hashes;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function collectProjectInputHashes(
|
|
158
|
+
projectRoot: string,
|
|
159
|
+
): Record<string, string> {
|
|
160
|
+
const hashes: Record<string, string> = {};
|
|
161
|
+
for (const file of listProjectInputFiles(projectRoot)) {
|
|
162
|
+
try {
|
|
163
|
+
hashes[toProjectKey(projectRoot, file)] = hashText(fs.readFileSync(file));
|
|
164
|
+
} catch {
|
|
165
|
+
// File watchers may observe a transform while another process is moving
|
|
166
|
+
// or deleting files. The missing key invalidates older cache entries.
|
|
102
167
|
}
|
|
168
|
+
}
|
|
169
|
+
return hashes;
|
|
170
|
+
}
|
|
103
171
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
172
|
+
function listProjectInputFiles(root: string): string[] {
|
|
173
|
+
const out: string[] = [];
|
|
174
|
+
const stack = [root];
|
|
175
|
+
while (stack.length !== 0) {
|
|
176
|
+
const current = stack.pop()!;
|
|
177
|
+
let entries: fs.Dirent[];
|
|
178
|
+
try {
|
|
179
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
180
|
+
} catch {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
if (isIgnoredProjectDirectory(entry.name)) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const file = path.join(current, entry.name);
|
|
188
|
+
if (entry.isDirectory()) {
|
|
189
|
+
stack.push(file);
|
|
190
|
+
} else if (entry.isFile()) {
|
|
191
|
+
out.push(file);
|
|
192
|
+
}
|
|
110
193
|
}
|
|
111
|
-
offset += text.length;
|
|
112
194
|
}
|
|
195
|
+
out.sort();
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
113
198
|
|
|
114
|
-
|
|
115
|
-
|
|
199
|
+
function isIgnoredProjectDirectory(name: string): boolean {
|
|
200
|
+
return (
|
|
201
|
+
name === ".git" ||
|
|
202
|
+
name === ".ttsc" ||
|
|
203
|
+
name === ".cache" ||
|
|
204
|
+
name === ".next" ||
|
|
205
|
+
name === ".nuxt" ||
|
|
206
|
+
name === ".svelte-kit" ||
|
|
207
|
+
name === ".turbo" ||
|
|
208
|
+
name === ".vite" ||
|
|
209
|
+
name === "build" ||
|
|
210
|
+
name === "coverage" ||
|
|
211
|
+
name === "dist" ||
|
|
212
|
+
name === "node_modules" ||
|
|
213
|
+
name === "out" ||
|
|
214
|
+
name === "temp" ||
|
|
215
|
+
name === "tmp"
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function sameHashes(
|
|
220
|
+
left: Record<string, string>,
|
|
221
|
+
right: Record<string, string>,
|
|
222
|
+
): boolean {
|
|
223
|
+
const leftKeys = Object.keys(left);
|
|
224
|
+
const rightKeys = Object.keys(right);
|
|
225
|
+
if (leftKeys.length !== rightKeys.length) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
return leftKeys.every((key) => right[key] === left[key]);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function hashText(input: string | Buffer): string {
|
|
232
|
+
return crypto.createHash("sha256").update(input).digest("hex");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function transformProject(props: {
|
|
236
|
+
aliasPaths: Record<string, string[]>;
|
|
237
|
+
baseUrl: string;
|
|
238
|
+
compilerOptions: Record<string, unknown>;
|
|
239
|
+
currentFile: string;
|
|
240
|
+
currentSource: string;
|
|
241
|
+
plugins?: ResolvedTtscUnpluginOptions["plugins"];
|
|
242
|
+
tsconfig: string;
|
|
243
|
+
}): Promise<TtscCachedProjectTransform> {
|
|
244
|
+
const configured = createTransformTsconfig(props);
|
|
245
|
+
const projectRoot = path.dirname(props.tsconfig);
|
|
246
|
+
try {
|
|
247
|
+
const result = new TtscCompiler({
|
|
248
|
+
cwd: projectRoot,
|
|
249
|
+
plugins: props.plugins,
|
|
250
|
+
projectRoot,
|
|
251
|
+
tsconfig: configured.path,
|
|
252
|
+
}).transform();
|
|
253
|
+
return {
|
|
254
|
+
inputHashes: collectInputHashes({
|
|
255
|
+
currentFile: props.currentFile,
|
|
256
|
+
currentSource: props.currentSource,
|
|
257
|
+
projectRoot,
|
|
258
|
+
result,
|
|
259
|
+
}),
|
|
260
|
+
projectRoot,
|
|
261
|
+
result,
|
|
262
|
+
};
|
|
263
|
+
} finally {
|
|
264
|
+
configured.dispose();
|
|
116
265
|
}
|
|
117
|
-
return {
|
|
118
|
-
code: magic.toString(),
|
|
119
|
-
map: magic.generateMap({
|
|
120
|
-
file: `${id}.map`,
|
|
121
|
-
includeContent: true,
|
|
122
|
-
source: id,
|
|
123
|
-
}),
|
|
124
|
-
};
|
|
125
266
|
}
|
|
126
267
|
|
|
127
268
|
function createTransformTsconfig(props: {
|
|
@@ -130,10 +271,13 @@ function createTransformTsconfig(props: {
|
|
|
130
271
|
compilerOptions: Record<string, unknown>;
|
|
131
272
|
tsconfig: string;
|
|
132
273
|
}): { path: string; dispose: () => void } {
|
|
133
|
-
const compilerOptions =
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
274
|
+
const compilerOptions = normalizeCompilerOptionsForGeneratedTsconfig(
|
|
275
|
+
{
|
|
276
|
+
...props.compilerOptions,
|
|
277
|
+
...createAliasCompilerOptions(props),
|
|
278
|
+
},
|
|
279
|
+
path.dirname(props.tsconfig),
|
|
280
|
+
);
|
|
137
281
|
if (Object.keys(compilerOptions).length === 0) {
|
|
138
282
|
return {
|
|
139
283
|
path: props.tsconfig,
|
|
@@ -141,18 +285,13 @@ function createTransformTsconfig(props: {
|
|
|
141
285
|
};
|
|
142
286
|
}
|
|
143
287
|
|
|
144
|
-
const directory = path.
|
|
145
|
-
const file = path.join(
|
|
146
|
-
directory,
|
|
147
|
-
`.ttsc-unplugin-${process.pid}-${Date.now()}-${Math.random()
|
|
148
|
-
.toString(36)
|
|
149
|
-
.slice(2)}.json`,
|
|
150
|
-
);
|
|
288
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "ttsc-unplugin-"));
|
|
289
|
+
const file = path.join(directory, "tsconfig.json");
|
|
151
290
|
fs.writeFileSync(
|
|
152
291
|
file,
|
|
153
292
|
JSON.stringify(
|
|
154
293
|
{
|
|
155
|
-
extends:
|
|
294
|
+
extends: normalizePath(props.tsconfig),
|
|
156
295
|
compilerOptions,
|
|
157
296
|
},
|
|
158
297
|
null,
|
|
@@ -162,10 +301,55 @@ function createTransformTsconfig(props: {
|
|
|
162
301
|
);
|
|
163
302
|
return {
|
|
164
303
|
path: file,
|
|
165
|
-
dispose: () => fs.rmSync(
|
|
304
|
+
dispose: () => fs.rmSync(directory, { force: true, recursive: true }),
|
|
166
305
|
};
|
|
167
306
|
}
|
|
168
307
|
|
|
308
|
+
function normalizeCompilerOptionsForGeneratedTsconfig(
|
|
309
|
+
compilerOptions: Record<string, unknown>,
|
|
310
|
+
tsconfigDir: string,
|
|
311
|
+
): Record<string, unknown> {
|
|
312
|
+
const output = { ...compilerOptions };
|
|
313
|
+
for (const key of ["baseUrl", "declarationDir", "outDir", "rootDir"]) {
|
|
314
|
+
if (typeof output[key] === "string") {
|
|
315
|
+
output[key] = path.resolve(tsconfigDir, output[key]);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
for (const key of ["rootDirs", "typeRoots"]) {
|
|
319
|
+
if (Array.isArray(output[key])) {
|
|
320
|
+
output[key] = output[key].map((entry) =>
|
|
321
|
+
typeof entry === "string" ? path.resolve(tsconfigDir, entry) : entry,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (hasPaths(output.paths) && typeof output.baseUrl !== "string") {
|
|
326
|
+
output.baseUrl = tsconfigDir;
|
|
327
|
+
}
|
|
328
|
+
if (Array.isArray(output.plugins)) {
|
|
329
|
+
output.plugins = output.plugins.map((entry) =>
|
|
330
|
+
normalizePluginConfigForGeneratedTsconfig(entry, tsconfigDir),
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
return output;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function normalizePluginConfigForGeneratedTsconfig(
|
|
337
|
+
entry: unknown,
|
|
338
|
+
tsconfigDir: string,
|
|
339
|
+
): unknown {
|
|
340
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
341
|
+
return entry;
|
|
342
|
+
}
|
|
343
|
+
const output: Record<string, unknown> = { ...entry };
|
|
344
|
+
for (const key of ["config", "source", "transform"]) {
|
|
345
|
+
const value = output[key];
|
|
346
|
+
if (typeof value === "string" && isRelativeSpecifier(value)) {
|
|
347
|
+
output[key] = path.resolve(tsconfigDir, value);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return output;
|
|
351
|
+
}
|
|
352
|
+
|
|
169
353
|
function createAliasCompilerOptions(props: {
|
|
170
354
|
aliasPaths: Record<string, string[]>;
|
|
171
355
|
baseUrl: string;
|
|
@@ -183,6 +367,15 @@ function createAliasCompilerOptions(props: {
|
|
|
183
367
|
};
|
|
184
368
|
}
|
|
185
369
|
|
|
370
|
+
function hasPaths(value: unknown): boolean {
|
|
371
|
+
return (
|
|
372
|
+
typeof value === "object" &&
|
|
373
|
+
value !== null &&
|
|
374
|
+
!Array.isArray(value) &&
|
|
375
|
+
Object.keys(value).length !== 0
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
186
379
|
function readPaths(value: unknown): Record<string, string[]> {
|
|
187
380
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
188
381
|
return {};
|
|
@@ -260,6 +453,44 @@ function normalizeAliases(aliases: unknown): TtscTransformAlias[] {
|
|
|
260
453
|
return [];
|
|
261
454
|
}
|
|
262
455
|
|
|
456
|
+
function createTransformCacheKey(props: {
|
|
457
|
+
aliasPaths: Record<string, string[]>;
|
|
458
|
+
compilerOptions: Record<string, unknown>;
|
|
459
|
+
plugins?: ResolvedTtscUnpluginOptions["plugins"];
|
|
460
|
+
tsconfig: string;
|
|
461
|
+
}): string {
|
|
462
|
+
return stableStringify({
|
|
463
|
+
aliasPaths: props.aliasPaths,
|
|
464
|
+
compilerOptions: props.compilerOptions,
|
|
465
|
+
plugins: props.plugins,
|
|
466
|
+
tsconfig: path.resolve(props.tsconfig),
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function stableStringify(value: unknown): string {
|
|
471
|
+
if (Array.isArray(value)) {
|
|
472
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
473
|
+
}
|
|
474
|
+
if (value && typeof value === "object") {
|
|
475
|
+
return `{${Object.entries(value)
|
|
476
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
477
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`)
|
|
478
|
+
.join(",")}}`;
|
|
479
|
+
}
|
|
480
|
+
return JSON.stringify(value);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function isRelativeSpecifier(value: string): boolean {
|
|
484
|
+
return (
|
|
485
|
+
value === "." ||
|
|
486
|
+
value === ".." ||
|
|
487
|
+
value.startsWith("./") ||
|
|
488
|
+
value.startsWith("../") ||
|
|
489
|
+
value.startsWith(".\\") ||
|
|
490
|
+
value.startsWith("..\\")
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
263
494
|
function isAlias(value: unknown): value is TtscTransformAlias {
|
|
264
495
|
return (
|
|
265
496
|
typeof value === "object" &&
|
|
@@ -296,6 +527,16 @@ function selectTransformedSource(props: {
|
|
|
296
527
|
throw new Error(`ttsc transform did not return output for ${props.file}`);
|
|
297
528
|
}
|
|
298
529
|
|
|
530
|
+
function reportSuccessDiagnostics(result: ITtscCompilerTransformation): void {
|
|
531
|
+
if (result.type !== "success" || result.diagnostics === undefined) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const text = formatDiagnostics(result.diagnostics);
|
|
535
|
+
if (text.length !== 0) {
|
|
536
|
+
process.stderr.write(`${text}\n`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
299
540
|
function formatDiagnostics(diagnostics: ITtscCompilerDiagnostic[]): string {
|
|
300
541
|
if (diagnostics.length === 0) {
|
|
301
542
|
return "ttsc transform failed";
|