svelte-ag 1.0.69 → 1.0.71
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-paths.d.ts","sourceRoot":"","sources":["../../src/lib/scripts/resolve-paths.ts"],"names":[],"mappings":"AAgCA,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,IAAI,IAAI,CAAC;IACd,QAAQ,EAAE,mBAAmB,EAAE,CAAC;CACjC;AAED,UAAU,UAAU;IAClB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;CACpB;
|
|
1
|
+
{"version":3,"file":"resolve-paths.d.ts","sourceRoot":"","sources":["../../src/lib/scripts/resolve-paths.ts"],"names":[],"mappings":"AAgCA,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,IAAI,IAAI,CAAC;IACd,QAAQ,EAAE,mBAAmB,EAAE,CAAC;CACjC;AAED,UAAU,UAAU;IAClB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;CACpB;AA2SD,wBAAsB,YAAY,CAAC,OAAO,EAAE,mBAAmB,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBjH;AAED,wBAAsB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC,CA8CpG;AAED,wBAAsB,aAAa,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAcrG;AAoID,wBAAsB,aAAa,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAenG;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CA0DvD;AAyBD,wBAAgB,iBAAiB,CAAC,SAAS,SAAkB,EAAE,SAAS,SAAkB,GAAG,OAAO,CAMnG;AAED,wBAAsB,IAAI,CAAC,IAAI,WAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAStE"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { cp, glob, mkdir, rm, stat } from 'node:fs/promises';
|
|
1
|
+
import { cp, glob, mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { realpathSync, watch } from 'node:fs';
|
|
3
|
-
import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { replaceTscAliasPaths } from 'tsc-alias';
|
|
5
|
+
import { prepareSingleFileReplaceTscAliasPaths, replaceTscAliasPaths } from 'tsc-alias';
|
|
6
6
|
import { loadConfig, prepareConfig } from 'tsc-alias/dist/helpers/config.js';
|
|
7
7
|
import { Output, TrieNode } from 'tsc-alias/dist/utils/index.js';
|
|
8
8
|
const DEFAULT_INPUTS = ['tsconfig.json'];
|
|
@@ -74,10 +74,16 @@ async function expandInput(input, cwd) {
|
|
|
74
74
|
function normalizeAliasPrefix(alias) {
|
|
75
75
|
return alias.replace(/\/\*$/, '').replace(/\/+$/, '');
|
|
76
76
|
}
|
|
77
|
+
function normalizeExcludedAliases(excludedAliases = []) {
|
|
78
|
+
return [...new Set(excludedAliases.map(normalizeAliasPrefix).filter(Boolean))];
|
|
79
|
+
}
|
|
80
|
+
function createTemporaryPath(rootDir, prefix) {
|
|
81
|
+
return resolve(rootDir, `.${prefix}-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.tmp`);
|
|
82
|
+
}
|
|
77
83
|
function createSilentOutput() {
|
|
78
84
|
return new Output(false, false);
|
|
79
85
|
}
|
|
80
|
-
async function createFilteredAliasTrie(project, excludedAliases) {
|
|
86
|
+
async function createFilteredAliasTrie(project, excludedAliases, outDir) {
|
|
81
87
|
if (excludedAliases.length === 0) {
|
|
82
88
|
return undefined;
|
|
83
89
|
}
|
|
@@ -94,14 +100,18 @@ async function createFilteredAliasTrie(project, excludedAliases) {
|
|
|
94
100
|
inputGlob: REPLACEABLE_FILE_EXTENSIONS.inputGlob,
|
|
95
101
|
outputCheck: [...REPLACEABLE_FILE_EXTENSIONS.outputCheck]
|
|
96
102
|
},
|
|
97
|
-
outDir
|
|
103
|
+
outDir,
|
|
98
104
|
output
|
|
99
105
|
});
|
|
100
106
|
return TrieNode.buildAliasTrie(preparedConfig, filteredPaths);
|
|
101
107
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
function isReplaceableOutputFile(filePath) {
|
|
109
|
+
const normalizedFilePath = filePath.toLowerCase();
|
|
110
|
+
return REPLACEABLE_FILE_EXTENSIONS.outputCheck.some((extension) => normalizedFilePath.endsWith(`.${extension.toLowerCase()}`));
|
|
111
|
+
}
|
|
112
|
+
async function createSingleFileAliasReplacer(project, excludedAliases) {
|
|
113
|
+
return prepareSingleFileReplaceTscAliasPaths({
|
|
114
|
+
aliasTrie: await createFilteredAliasTrie(project, excludedAliases, project.distDir),
|
|
105
115
|
configFile: project.tsconfigPath,
|
|
106
116
|
outDir: project.distDir,
|
|
107
117
|
fileExtensions: {
|
|
@@ -110,24 +120,141 @@ async function resolveAliases(project, options = {}) {
|
|
|
110
120
|
}
|
|
111
121
|
});
|
|
112
122
|
}
|
|
113
|
-
async function
|
|
114
|
-
await
|
|
123
|
+
async function resolveAliases(project, outDir, options = {}) {
|
|
124
|
+
await replaceTscAliasPaths({
|
|
125
|
+
aliasTrie: await createFilteredAliasTrie(project, options.excludeAliases ?? [], outDir),
|
|
126
|
+
configFile: project.tsconfigPath,
|
|
127
|
+
outDir,
|
|
128
|
+
fileExtensions: {
|
|
129
|
+
inputGlob: REPLACEABLE_FILE_EXTENSIONS.inputGlob,
|
|
130
|
+
outputCheck: [...REPLACEABLE_FILE_EXTENSIONS.outputCheck]
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
async function copyProjectSource(project, outDir) {
|
|
135
|
+
await rm(outDir, {
|
|
115
136
|
force: true,
|
|
116
137
|
recursive: true
|
|
117
138
|
});
|
|
118
|
-
await mkdir(
|
|
119
|
-
await cp(project.srcDir,
|
|
139
|
+
await mkdir(dirname(outDir), { recursive: true });
|
|
140
|
+
await cp(project.srcDir, outDir, {
|
|
120
141
|
force: true,
|
|
121
142
|
recursive: true
|
|
122
143
|
});
|
|
123
144
|
}
|
|
124
|
-
|
|
125
|
-
await
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
145
|
+
async function collectProjectTree(rootDir, currentDir = rootDir) {
|
|
146
|
+
if (!(await pathExists(currentDir))) {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
150
|
+
const paths = [];
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
const entryPath = join(currentDir, entry.name);
|
|
153
|
+
const relativePath = relative(rootDir, entryPath);
|
|
154
|
+
paths.push(relativePath);
|
|
155
|
+
if (entry.isDirectory()) {
|
|
156
|
+
paths.push(...(await collectProjectTree(rootDir, entryPath)));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return paths;
|
|
160
|
+
}
|
|
161
|
+
async function publishStagedProject(stageDir, distDir) {
|
|
162
|
+
const stagedPaths = await collectProjectTree(stageDir);
|
|
163
|
+
const stagedPathSet = new Set(stagedPaths);
|
|
164
|
+
for (const relativePath of stagedPaths
|
|
165
|
+
.filter((path) => path !== '')
|
|
166
|
+
.sort((left, right) => left.localeCompare(right))) {
|
|
167
|
+
const stagedPath = resolve(stageDir, relativePath);
|
|
168
|
+
const distPath = resolve(distDir, relativePath);
|
|
169
|
+
const stagedStats = await stat(stagedPath);
|
|
170
|
+
if (stagedStats.isDirectory()) {
|
|
171
|
+
await mkdir(distPath, { recursive: true });
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
await mkdir(dirname(distPath), { recursive: true });
|
|
175
|
+
await rename(stagedPath, distPath);
|
|
176
|
+
}
|
|
177
|
+
if (!(await pathExists(distDir))) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const distPaths = await collectProjectTree(distDir);
|
|
181
|
+
for (const relativePath of distPaths.sort((left, right) => right.length - left.length || right.localeCompare(left))) {
|
|
182
|
+
if (!stagedPathSet.has(relativePath)) {
|
|
183
|
+
await rm(resolve(distDir, relativePath), {
|
|
184
|
+
force: true,
|
|
185
|
+
recursive: true
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function resolveFileContents(project, distPath, sourceContents, excludedAliases) {
|
|
191
|
+
if (!isReplaceableOutputFile(distPath)) {
|
|
192
|
+
return sourceContents;
|
|
193
|
+
}
|
|
194
|
+
const resolveAliasesInFile = await createSingleFileAliasReplacer(project, excludedAliases);
|
|
195
|
+
return resolveAliasesInFile({
|
|
196
|
+
fileContents: sourceContents,
|
|
197
|
+
filePath: distPath
|
|
129
198
|
});
|
|
130
199
|
}
|
|
200
|
+
async function publishFileAtomically(project, sourcePath, distPath, excludedAliases) {
|
|
201
|
+
const tempPath = createTemporaryPath(dirname(distPath), basename(distPath));
|
|
202
|
+
await mkdir(dirname(distPath), { recursive: true });
|
|
203
|
+
if (isReplaceableOutputFile(distPath)) {
|
|
204
|
+
const sourceContents = await readFile(sourcePath, 'utf8');
|
|
205
|
+
const resolvedContents = await resolveFileContents(project, distPath, sourceContents, excludedAliases);
|
|
206
|
+
await writeFile(tempPath, resolvedContents, 'utf8');
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
await cp(sourcePath, tempPath, { force: true });
|
|
210
|
+
}
|
|
211
|
+
await rename(tempPath, distPath);
|
|
212
|
+
}
|
|
213
|
+
async function syncProjectSourcePath(project, sourceRelativePath, excludedAliases) {
|
|
214
|
+
const sourcePath = resolve(project.srcDir, sourceRelativePath);
|
|
215
|
+
const normalizedRelativePath = relative(project.srcDir, sourcePath);
|
|
216
|
+
if (normalizedRelativePath.startsWith('..') || isAbsolute(normalizedRelativePath)) {
|
|
217
|
+
await buildProject(project, {
|
|
218
|
+
excludeAliases: excludedAliases
|
|
219
|
+
});
|
|
220
|
+
return 'rebuilt';
|
|
221
|
+
}
|
|
222
|
+
const distPath = resolve(project.distDir, normalizedRelativePath);
|
|
223
|
+
if (!(await pathExists(sourcePath))) {
|
|
224
|
+
await rm(distPath, {
|
|
225
|
+
force: true,
|
|
226
|
+
recursive: true
|
|
227
|
+
});
|
|
228
|
+
return 'synced';
|
|
229
|
+
}
|
|
230
|
+
const sourceStats = await stat(sourcePath);
|
|
231
|
+
if (!sourceStats.isFile()) {
|
|
232
|
+
await buildProject(project, {
|
|
233
|
+
excludeAliases: excludedAliases
|
|
234
|
+
});
|
|
235
|
+
return 'rebuilt';
|
|
236
|
+
}
|
|
237
|
+
await publishFileAtomically(project, sourcePath, distPath, excludedAliases);
|
|
238
|
+
return 'synced';
|
|
239
|
+
}
|
|
240
|
+
export async function buildProject(project, options = {}) {
|
|
241
|
+
const excludedAliases = normalizeExcludedAliases(options.excludeAliases);
|
|
242
|
+
const stageDir = createTemporaryPath(project.rootDir, basename(project.distDir));
|
|
243
|
+
try {
|
|
244
|
+
await copyProjectSource(project, stageDir);
|
|
245
|
+
await resolveAliases(project, stageDir, {
|
|
246
|
+
...options,
|
|
247
|
+
excludeAliases: excludedAliases
|
|
248
|
+
});
|
|
249
|
+
await publishStagedProject(stageDir, project.distDir);
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
await rm(stageDir, {
|
|
253
|
+
force: true,
|
|
254
|
+
recursive: true
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
131
258
|
export async function findProjects(options = {}) {
|
|
132
259
|
const cwd = options.cwd ?? process.cwd();
|
|
133
260
|
const inputs = options.inputs && options.inputs.length > 0 ? options.inputs : DEFAULT_INPUTS;
|
|
@@ -177,7 +304,10 @@ function createDebouncedProjectRunner(project, cwd, options) {
|
|
|
177
304
|
let closed = false;
|
|
178
305
|
let activeBuild;
|
|
179
306
|
let pendingReason;
|
|
307
|
+
let pendingFullRebuild = false;
|
|
308
|
+
let pendingSourcePaths = new Set();
|
|
180
309
|
let timer;
|
|
310
|
+
const excludedAliases = normalizeExcludedAliases(options.excludeAliases);
|
|
181
311
|
const run = async () => {
|
|
182
312
|
if (closed) {
|
|
183
313
|
return;
|
|
@@ -186,10 +316,29 @@ function createDebouncedProjectRunner(project, cwd, options) {
|
|
|
186
316
|
return;
|
|
187
317
|
}
|
|
188
318
|
const reason = pendingReason ?? 'change';
|
|
319
|
+
const fullRebuild = pendingFullRebuild;
|
|
320
|
+
const sourcePaths = fullRebuild ? [] : [...pendingSourcePaths].sort((left, right) => left.localeCompare(right));
|
|
189
321
|
pendingReason = undefined;
|
|
322
|
+
pendingFullRebuild = false;
|
|
323
|
+
pendingSourcePaths = new Set();
|
|
190
324
|
activeBuild = (async () => {
|
|
191
|
-
|
|
192
|
-
|
|
325
|
+
if (fullRebuild || sourcePaths.length === 0) {
|
|
326
|
+
logInfo(`rebuilding ${formatPath(project.tsconfigPath, cwd)} after ${reason}`);
|
|
327
|
+
await buildProject(project, {
|
|
328
|
+
...options,
|
|
329
|
+
excludeAliases: excludedAliases
|
|
330
|
+
});
|
|
331
|
+
logInfo(`watch updated ${formatPath(project.distDir, cwd)}`);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
logInfo(`updating ${sourcePaths.length} changed path(s) in ${formatPath(project.tsconfigPath, cwd)} after ${reason}`);
|
|
335
|
+
for (const sourcePath of sourcePaths) {
|
|
336
|
+
const result = await syncProjectSourcePath(project, sourcePath, excludedAliases);
|
|
337
|
+
if (result === 'rebuilt') {
|
|
338
|
+
logInfo(`watch updated ${formatPath(project.distDir, cwd)}`);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
193
342
|
logInfo(`watch updated ${formatPath(project.distDir, cwd)}`);
|
|
194
343
|
})();
|
|
195
344
|
try {
|
|
@@ -210,11 +359,20 @@ function createDebouncedProjectRunner(project, cwd, options) {
|
|
|
210
359
|
timer = undefined;
|
|
211
360
|
}
|
|
212
361
|
},
|
|
213
|
-
schedule(reason) {
|
|
362
|
+
schedule(reason, sourceRelativePath) {
|
|
214
363
|
if (closed) {
|
|
215
364
|
return;
|
|
216
365
|
}
|
|
217
366
|
pendingReason = reason;
|
|
367
|
+
if (sourceRelativePath) {
|
|
368
|
+
if (!pendingFullRebuild) {
|
|
369
|
+
pendingSourcePaths.add(sourceRelativePath);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
pendingFullRebuild = true;
|
|
374
|
+
pendingSourcePaths.clear();
|
|
375
|
+
}
|
|
218
376
|
if (timer) {
|
|
219
377
|
clearTimeout(timer);
|
|
220
378
|
}
|
|
@@ -231,7 +389,7 @@ function watchProject(project, cwd, options) {
|
|
|
231
389
|
const runner = createDebouncedProjectRunner(project, cwd, options);
|
|
232
390
|
const sourceWatcher = watch(project.srcDir, { recursive: true }, (_eventType, fileName) => {
|
|
233
391
|
const suffix = fileName ? ` (${fileName.toString()})` : '';
|
|
234
|
-
runner.schedule(`src change${suffix}
|
|
392
|
+
runner.schedule(`src change${suffix}`, fileName?.toString());
|
|
235
393
|
});
|
|
236
394
|
const configWatcher = watch(project.tsconfigPath, () => {
|
|
237
395
|
runner.schedule('tsconfig change');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { mkdir, mkdtemp, readFile, rm, symlink, unlink, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, stat, symlink, unlink, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { pathToFileURL } from 'node:url';
|
|
@@ -137,6 +137,8 @@ describe('resolve-paths', () => {
|
|
|
137
137
|
const watcher = await watchProjects({
|
|
138
138
|
inputs: [join(root, 'tsconfig.json')]
|
|
139
139
|
});
|
|
140
|
+
const utilsDistPath = join(root, 'dist', 'lib', 'utils.ts');
|
|
141
|
+
const utilsBefore = await stat(utilsDistPath);
|
|
140
142
|
try {
|
|
141
143
|
await writeFile(join(root, 'src', 'index.ts'), ["import { answer } from '../utils';", '', 'export const doubled = answer * 2;', ''].join('\n'));
|
|
142
144
|
await unlink(join(root, 'src', 'styles.css'));
|
|
@@ -151,6 +153,8 @@ describe('resolve-paths', () => {
|
|
|
151
153
|
code: 'ENOENT'
|
|
152
154
|
});
|
|
153
155
|
});
|
|
156
|
+
const utilsAfter = await stat(utilsDistPath);
|
|
157
|
+
expect(utilsAfter.mtimeMs).toBe(utilsBefore.mtimeMs);
|
|
154
158
|
}
|
|
155
159
|
finally {
|
|
156
160
|
watcher.close();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-ag",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.71",
|
|
4
4
|
"description": "Useful svelte components",
|
|
5
5
|
"bugs": "https://github.com/ageorgeh/svelte-ag/issues",
|
|
6
6
|
"repository": {
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"sveltekit-superforms": "2.30.1",
|
|
63
63
|
"tailwind-merge": "^3.5.0",
|
|
64
64
|
"tailwind-variants": "^3.2.2",
|
|
65
|
-
"ts-ag": "^1.1.
|
|
65
|
+
"ts-ag": "^1.1.15",
|
|
66
66
|
"tsc-alias": "^1.8.16",
|
|
67
67
|
"valibot": "^1.3.1"
|
|
68
68
|
},
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { cp, glob, mkdir, rm, stat } from 'node:fs/promises';
|
|
1
|
+
import { cp, glob, mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { realpathSync, watch, type FSWatcher } from 'node:fs';
|
|
3
|
-
import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { replaceTscAliasPaths } from 'tsc-alias';
|
|
5
|
+
import { prepareSingleFileReplaceTscAliasPaths, replaceTscAliasPaths, type SingleFileReplacer } from 'tsc-alias';
|
|
6
6
|
import { loadConfig, prepareConfig } from 'tsc-alias/dist/helpers/config.js';
|
|
7
7
|
import { Output, TrieNode } from 'tsc-alias/dist/utils/index.js';
|
|
8
8
|
import type { Alias } from 'tsc-alias/dist/interfaces.js';
|
|
@@ -54,6 +54,8 @@ interface CliOptions {
|
|
|
54
54
|
watchMode: boolean;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
type ProjectUpdateResult = 'rebuilt' | 'synced';
|
|
58
|
+
|
|
57
59
|
function formatPath(targetPath: string, cwd = process.cwd()): string {
|
|
58
60
|
return relative(cwd, targetPath) || '.';
|
|
59
61
|
}
|
|
@@ -117,13 +119,22 @@ function normalizeAliasPrefix(alias: string): string {
|
|
|
117
119
|
return alias.replace(/\/\*$/, '').replace(/\/+$/, '');
|
|
118
120
|
}
|
|
119
121
|
|
|
122
|
+
function normalizeExcludedAliases(excludedAliases: string[] = []): string[] {
|
|
123
|
+
return [...new Set(excludedAliases.map(normalizeAliasPrefix).filter(Boolean))];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function createTemporaryPath(rootDir: string, prefix: string): string {
|
|
127
|
+
return resolve(rootDir, `.${prefix}-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.tmp`);
|
|
128
|
+
}
|
|
129
|
+
|
|
120
130
|
function createSilentOutput(): Output {
|
|
121
131
|
return new Output(false, false);
|
|
122
132
|
}
|
|
123
133
|
|
|
124
134
|
async function createFilteredAliasTrie(
|
|
125
135
|
project: ResolvePathsProject,
|
|
126
|
-
excludedAliases: string[]
|
|
136
|
+
excludedAliases: string[],
|
|
137
|
+
outDir: string
|
|
127
138
|
): Promise<TrieNode<Alias> | undefined> {
|
|
128
139
|
if (excludedAliases.length === 0) {
|
|
129
140
|
return undefined;
|
|
@@ -146,16 +157,27 @@ async function createFilteredAliasTrie(
|
|
|
146
157
|
inputGlob: REPLACEABLE_FILE_EXTENSIONS.inputGlob,
|
|
147
158
|
outputCheck: [...REPLACEABLE_FILE_EXTENSIONS.outputCheck]
|
|
148
159
|
},
|
|
149
|
-
outDir
|
|
160
|
+
outDir,
|
|
150
161
|
output
|
|
151
162
|
});
|
|
152
163
|
|
|
153
164
|
return TrieNode.buildAliasTrie(preparedConfig, filteredPaths);
|
|
154
165
|
}
|
|
155
166
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
167
|
+
function isReplaceableOutputFile(filePath: string): boolean {
|
|
168
|
+
const normalizedFilePath = filePath.toLowerCase();
|
|
169
|
+
|
|
170
|
+
return REPLACEABLE_FILE_EXTENSIONS.outputCheck.some((extension) =>
|
|
171
|
+
normalizedFilePath.endsWith(`.${extension.toLowerCase()}`)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function createSingleFileAliasReplacer(
|
|
176
|
+
project: ResolvePathsProject,
|
|
177
|
+
excludedAliases: string[]
|
|
178
|
+
): Promise<SingleFileReplacer> {
|
|
179
|
+
return prepareSingleFileReplaceTscAliasPaths({
|
|
180
|
+
aliasTrie: await createFilteredAliasTrie(project, excludedAliases, project.distDir),
|
|
159
181
|
configFile: project.tsconfigPath,
|
|
160
182
|
outDir: project.distDir,
|
|
161
183
|
fileExtensions: {
|
|
@@ -165,26 +187,189 @@ async function resolveAliases(project: ResolvePathsProject, options: ResolvePath
|
|
|
165
187
|
});
|
|
166
188
|
}
|
|
167
189
|
|
|
168
|
-
async function
|
|
169
|
-
|
|
190
|
+
async function resolveAliases(
|
|
191
|
+
project: ResolvePathsProject,
|
|
192
|
+
outDir: string,
|
|
193
|
+
options: ResolvePathsOptions = {}
|
|
194
|
+
): Promise<void> {
|
|
195
|
+
await replaceTscAliasPaths({
|
|
196
|
+
aliasTrie: await createFilteredAliasTrie(project, options.excludeAliases ?? [], outDir),
|
|
197
|
+
configFile: project.tsconfigPath,
|
|
198
|
+
outDir,
|
|
199
|
+
fileExtensions: {
|
|
200
|
+
inputGlob: REPLACEABLE_FILE_EXTENSIONS.inputGlob,
|
|
201
|
+
outputCheck: [...REPLACEABLE_FILE_EXTENSIONS.outputCheck]
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function copyProjectSource(project: ResolvePathsProject, outDir: string): Promise<void> {
|
|
207
|
+
await rm(outDir, {
|
|
170
208
|
force: true,
|
|
171
209
|
recursive: true
|
|
172
210
|
});
|
|
173
|
-
await mkdir(
|
|
174
|
-
await cp(project.srcDir,
|
|
211
|
+
await mkdir(dirname(outDir), { recursive: true });
|
|
212
|
+
await cp(project.srcDir, outDir, {
|
|
175
213
|
force: true,
|
|
176
214
|
recursive: true
|
|
177
215
|
});
|
|
178
216
|
}
|
|
179
217
|
|
|
180
|
-
|
|
181
|
-
await
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
218
|
+
async function collectProjectTree(rootDir: string, currentDir = rootDir): Promise<string[]> {
|
|
219
|
+
if (!(await pathExists(currentDir))) {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
224
|
+
const paths: string[] = [];
|
|
225
|
+
|
|
226
|
+
for (const entry of entries) {
|
|
227
|
+
const entryPath = join(currentDir, entry.name);
|
|
228
|
+
const relativePath = relative(rootDir, entryPath);
|
|
229
|
+
|
|
230
|
+
paths.push(relativePath);
|
|
231
|
+
|
|
232
|
+
if (entry.isDirectory()) {
|
|
233
|
+
paths.push(...(await collectProjectTree(rootDir, entryPath)));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return paths;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function publishStagedProject(stageDir: string, distDir: string): Promise<void> {
|
|
241
|
+
const stagedPaths = await collectProjectTree(stageDir);
|
|
242
|
+
const stagedPathSet = new Set(stagedPaths);
|
|
243
|
+
|
|
244
|
+
for (const relativePath of stagedPaths
|
|
245
|
+
.filter((path) => path !== '')
|
|
246
|
+
.sort((left, right) => left.localeCompare(right))) {
|
|
247
|
+
const stagedPath = resolve(stageDir, relativePath);
|
|
248
|
+
const distPath = resolve(distDir, relativePath);
|
|
249
|
+
const stagedStats = await stat(stagedPath);
|
|
250
|
+
|
|
251
|
+
if (stagedStats.isDirectory()) {
|
|
252
|
+
await mkdir(distPath, { recursive: true });
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
await mkdir(dirname(distPath), { recursive: true });
|
|
257
|
+
await rename(stagedPath, distPath);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!(await pathExists(distDir))) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const distPaths = await collectProjectTree(distDir);
|
|
265
|
+
|
|
266
|
+
for (const relativePath of distPaths.sort((left, right) => right.length - left.length || right.localeCompare(left))) {
|
|
267
|
+
if (!stagedPathSet.has(relativePath)) {
|
|
268
|
+
await rm(resolve(distDir, relativePath), {
|
|
269
|
+
force: true,
|
|
270
|
+
recursive: true
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function resolveFileContents(
|
|
277
|
+
project: ResolvePathsProject,
|
|
278
|
+
distPath: string,
|
|
279
|
+
sourceContents: string,
|
|
280
|
+
excludedAliases: string[]
|
|
281
|
+
): Promise<string> {
|
|
282
|
+
if (!isReplaceableOutputFile(distPath)) {
|
|
283
|
+
return sourceContents;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const resolveAliasesInFile = await createSingleFileAliasReplacer(project, excludedAliases);
|
|
287
|
+
|
|
288
|
+
return resolveAliasesInFile({
|
|
289
|
+
fileContents: sourceContents,
|
|
290
|
+
filePath: distPath
|
|
185
291
|
});
|
|
186
292
|
}
|
|
187
293
|
|
|
294
|
+
async function publishFileAtomically(
|
|
295
|
+
project: ResolvePathsProject,
|
|
296
|
+
sourcePath: string,
|
|
297
|
+
distPath: string,
|
|
298
|
+
excludedAliases: string[]
|
|
299
|
+
): Promise<void> {
|
|
300
|
+
const tempPath = createTemporaryPath(dirname(distPath), basename(distPath));
|
|
301
|
+
|
|
302
|
+
await mkdir(dirname(distPath), { recursive: true });
|
|
303
|
+
|
|
304
|
+
if (isReplaceableOutputFile(distPath)) {
|
|
305
|
+
const sourceContents = await readFile(sourcePath, 'utf8');
|
|
306
|
+
const resolvedContents = await resolveFileContents(project, distPath, sourceContents, excludedAliases);
|
|
307
|
+
await writeFile(tempPath, resolvedContents, 'utf8');
|
|
308
|
+
} else {
|
|
309
|
+
await cp(sourcePath, tempPath, { force: true });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
await rename(tempPath, distPath);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function syncProjectSourcePath(
|
|
316
|
+
project: ResolvePathsProject,
|
|
317
|
+
sourceRelativePath: string,
|
|
318
|
+
excludedAliases: string[]
|
|
319
|
+
): Promise<ProjectUpdateResult> {
|
|
320
|
+
const sourcePath = resolve(project.srcDir, sourceRelativePath);
|
|
321
|
+
const normalizedRelativePath = relative(project.srcDir, sourcePath);
|
|
322
|
+
|
|
323
|
+
if (normalizedRelativePath.startsWith('..') || isAbsolute(normalizedRelativePath)) {
|
|
324
|
+
await buildProject(project, {
|
|
325
|
+
excludeAliases: excludedAliases
|
|
326
|
+
});
|
|
327
|
+
return 'rebuilt';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const distPath = resolve(project.distDir, normalizedRelativePath);
|
|
331
|
+
|
|
332
|
+
if (!(await pathExists(sourcePath))) {
|
|
333
|
+
await rm(distPath, {
|
|
334
|
+
force: true,
|
|
335
|
+
recursive: true
|
|
336
|
+
});
|
|
337
|
+
return 'synced';
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const sourceStats = await stat(sourcePath);
|
|
341
|
+
|
|
342
|
+
if (!sourceStats.isFile()) {
|
|
343
|
+
await buildProject(project, {
|
|
344
|
+
excludeAliases: excludedAliases
|
|
345
|
+
});
|
|
346
|
+
return 'rebuilt';
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
await publishFileAtomically(project, sourcePath, distPath, excludedAliases);
|
|
350
|
+
|
|
351
|
+
return 'synced';
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export async function buildProject(project: ResolvePathsProject, options: ResolvePathsOptions = {}): Promise<void> {
|
|
355
|
+
const excludedAliases = normalizeExcludedAliases(options.excludeAliases);
|
|
356
|
+
const stageDir = createTemporaryPath(project.rootDir, basename(project.distDir));
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
await copyProjectSource(project, stageDir);
|
|
360
|
+
await resolveAliases(project, stageDir, {
|
|
361
|
+
...options,
|
|
362
|
+
excludeAliases: excludedAliases
|
|
363
|
+
});
|
|
364
|
+
await publishStagedProject(stageDir, project.distDir);
|
|
365
|
+
} finally {
|
|
366
|
+
await rm(stageDir, {
|
|
367
|
+
force: true,
|
|
368
|
+
recursive: true
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
188
373
|
export async function findProjects(options: ResolvePathsOptions = {}): Promise<ResolvePathsProject[]> {
|
|
189
374
|
const cwd = options.cwd ?? process.cwd();
|
|
190
375
|
const inputs = options.inputs && options.inputs.length > 0 ? options.inputs : DEFAULT_INPUTS;
|
|
@@ -255,12 +440,15 @@ function createDebouncedProjectRunner(
|
|
|
255
440
|
options: ResolvePathsOptions
|
|
256
441
|
): {
|
|
257
442
|
close(): void;
|
|
258
|
-
schedule(reason: string): void;
|
|
443
|
+
schedule(reason: string, sourceRelativePath?: string): void;
|
|
259
444
|
} {
|
|
260
445
|
let closed = false;
|
|
261
446
|
let activeBuild: Promise<void> | undefined;
|
|
262
447
|
let pendingReason: string | undefined;
|
|
448
|
+
let pendingFullRebuild = false;
|
|
449
|
+
let pendingSourcePaths = new Set<string>();
|
|
263
450
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
451
|
+
const excludedAliases = normalizeExcludedAliases(options.excludeAliases);
|
|
264
452
|
|
|
265
453
|
const run = async (): Promise<void> => {
|
|
266
454
|
if (closed) {
|
|
@@ -272,11 +460,36 @@ function createDebouncedProjectRunner(
|
|
|
272
460
|
}
|
|
273
461
|
|
|
274
462
|
const reason = pendingReason ?? 'change';
|
|
463
|
+
const fullRebuild = pendingFullRebuild;
|
|
464
|
+
const sourcePaths = fullRebuild ? [] : [...pendingSourcePaths].sort((left, right) => left.localeCompare(right));
|
|
275
465
|
pendingReason = undefined;
|
|
466
|
+
pendingFullRebuild = false;
|
|
467
|
+
pendingSourcePaths = new Set<string>();
|
|
276
468
|
|
|
277
469
|
activeBuild = (async () => {
|
|
278
|
-
|
|
279
|
-
|
|
470
|
+
if (fullRebuild || sourcePaths.length === 0) {
|
|
471
|
+
logInfo(`rebuilding ${formatPath(project.tsconfigPath, cwd)} after ${reason}`);
|
|
472
|
+
await buildProject(project, {
|
|
473
|
+
...options,
|
|
474
|
+
excludeAliases: excludedAliases
|
|
475
|
+
});
|
|
476
|
+
logInfo(`watch updated ${formatPath(project.distDir, cwd)}`);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
logInfo(
|
|
481
|
+
`updating ${sourcePaths.length} changed path(s) in ${formatPath(project.tsconfigPath, cwd)} after ${reason}`
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
for (const sourcePath of sourcePaths) {
|
|
485
|
+
const result = await syncProjectSourcePath(project, sourcePath, excludedAliases);
|
|
486
|
+
|
|
487
|
+
if (result === 'rebuilt') {
|
|
488
|
+
logInfo(`watch updated ${formatPath(project.distDir, cwd)}`);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
280
493
|
logInfo(`watch updated ${formatPath(project.distDir, cwd)}`);
|
|
281
494
|
})();
|
|
282
495
|
|
|
@@ -300,12 +513,20 @@ function createDebouncedProjectRunner(
|
|
|
300
513
|
timer = undefined;
|
|
301
514
|
}
|
|
302
515
|
},
|
|
303
|
-
schedule(reason: string) {
|
|
516
|
+
schedule(reason: string, sourceRelativePath?: string) {
|
|
304
517
|
if (closed) {
|
|
305
518
|
return;
|
|
306
519
|
}
|
|
307
520
|
|
|
308
521
|
pendingReason = reason;
|
|
522
|
+
if (sourceRelativePath) {
|
|
523
|
+
if (!pendingFullRebuild) {
|
|
524
|
+
pendingSourcePaths.add(sourceRelativePath);
|
|
525
|
+
}
|
|
526
|
+
} else {
|
|
527
|
+
pendingFullRebuild = true;
|
|
528
|
+
pendingSourcePaths.clear();
|
|
529
|
+
}
|
|
309
530
|
|
|
310
531
|
if (timer) {
|
|
311
532
|
clearTimeout(timer);
|
|
@@ -326,7 +547,7 @@ function watchProject(project: ResolvePathsProject, cwd: string, options: Resolv
|
|
|
326
547
|
|
|
327
548
|
const sourceWatcher = watch(project.srcDir, { recursive: true }, (_eventType, fileName) => {
|
|
328
549
|
const suffix = fileName ? ` (${fileName.toString()})` : '';
|
|
329
|
-
runner.schedule(`src change${suffix}
|
|
550
|
+
runner.schedule(`src change${suffix}`, fileName?.toString());
|
|
330
551
|
});
|
|
331
552
|
const configWatcher = watch(project.tsconfigPath, () => {
|
|
332
553
|
runner.schedule('tsconfig change');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { mkdir, mkdtemp, readFile, rm, symlink, unlink, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, stat, symlink, unlink, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { pathToFileURL } from 'node:url';
|
|
@@ -178,6 +178,8 @@ describe('resolve-paths', () => {
|
|
|
178
178
|
const watcher = await watchProjects({
|
|
179
179
|
inputs: [join(root, 'tsconfig.json')]
|
|
180
180
|
});
|
|
181
|
+
const utilsDistPath = join(root, 'dist', 'lib', 'utils.ts');
|
|
182
|
+
const utilsBefore = await stat(utilsDistPath);
|
|
181
183
|
|
|
182
184
|
try {
|
|
183
185
|
await writeFile(
|
|
@@ -197,6 +199,9 @@ describe('resolve-paths', () => {
|
|
|
197
199
|
code: 'ENOENT'
|
|
198
200
|
});
|
|
199
201
|
});
|
|
202
|
+
|
|
203
|
+
const utilsAfter = await stat(utilsDistPath);
|
|
204
|
+
expect(utilsAfter.mtimeMs).toBe(utilsBefore.mtimeMs);
|
|
200
205
|
} finally {
|
|
201
206
|
watcher.close();
|
|
202
207
|
}
|