svelte-ag 1.0.69 → 1.0.70

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;AA6HD,wBAAsB,YAAY,CAAC,OAAO,EAAE,mBAAmB,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAMjH;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;AAgGD,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
+ {"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: project.distDir,
103
+ outDir,
98
104
  output
99
105
  });
100
106
  return TrieNode.buildAliasTrie(preparedConfig, filteredPaths);
101
107
  }
102
- async function resolveAliases(project, options = {}) {
103
- await replaceTscAliasPaths({
104
- aliasTrie: await createFilteredAliasTrie(project, options.excludeAliases ?? []),
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 copyProjectSource(project) {
114
- await rm(project.distDir, {
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(project.rootDir, { recursive: true });
119
- await cp(project.srcDir, project.distDir, {
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
- export async function buildProject(project, options = {}) {
125
- await copyProjectSource(project);
126
- await resolveAliases(project, {
127
- ...options,
128
- excludeAliases: [...new Set((options.excludeAliases ?? []).map(normalizeAliasPrefix).filter(Boolean))]
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
- logInfo(`rebuilding ${formatPath(project.tsconfigPath, cwd)} after ${reason}`);
192
- await buildProject(project, options);
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.69",
3
+ "version": "1.0.70",
4
4
  "description": "Useful svelte components",
5
5
  "bugs": "https://github.com/ageorgeh/svelte-ag/issues",
6
6
  "repository": {
@@ -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: project.distDir,
160
+ outDir,
150
161
  output
151
162
  });
152
163
 
153
164
  return TrieNode.buildAliasTrie(preparedConfig, filteredPaths);
154
165
  }
155
166
 
156
- async function resolveAliases(project: ResolvePathsProject, options: ResolvePathsOptions = {}): Promise<void> {
157
- await replaceTscAliasPaths({
158
- aliasTrie: await createFilteredAliasTrie(project, options.excludeAliases ?? []),
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 copyProjectSource(project: ResolvePathsProject): Promise<void> {
169
- await rm(project.distDir, {
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(project.rootDir, { recursive: true });
174
- await cp(project.srcDir, project.distDir, {
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
- export async function buildProject(project: ResolvePathsProject, options: ResolvePathsOptions = {}): Promise<void> {
181
- await copyProjectSource(project);
182
- await resolveAliases(project, {
183
- ...options,
184
- excludeAliases: [...new Set((options.excludeAliases ?? []).map(normalizeAliasPrefix).filter(Boolean))]
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
- logInfo(`rebuilding ${formatPath(project.tsconfigPath, cwd)} after ${reason}`);
279
- await buildProject(project, options);
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
  }