@git.zone/tsbundle 2.11.3 → 2.12.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/interfaces/index.d.ts +1 -0
- package/dist_ts/mod_custom/index.d.ts +10 -3
- package/dist_ts/mod_custom/index.js +277 -86
- package/dist_ts/mod_esbuild/index.child.d.ts +4 -8
- package/dist_ts/mod_esbuild/index.child.js +51 -44
- package/dist_ts/mod_esbuild/plugins.d.ts +5 -2
- package/dist_ts/mod_esbuild/plugins.js +5 -3
- package/dist_ts/mod_esbuild/workerplugin.d.ts +12 -0
- package/dist_ts/mod_esbuild/workerplugin.js +254 -0
- package/dist_ts/mod_output/artifactpublisher.d.ts +43 -0
- package/dist_ts/mod_output/artifactpublisher.js +925 -0
- package/dist_ts/mod_output/index.d.ts +1 -0
- package/dist_ts/mod_output/index.js +11 -3
- package/dist_ts/mod_rolldown/index.child.d.ts +4 -2
- package/dist_ts/mod_rolldown/index.child.js +47 -36
- package/dist_ts/mod_rspack/index.child.d.ts +4 -2
- package/dist_ts/mod_rspack/index.child.js +73 -115
- package/dist_ts/plugins.d.ts +3 -1
- package/dist_ts/plugins.js +4 -2
- package/dist_ts/tsbundle.class.tsbundle.js +98 -16
- package/package.json +10 -9
- package/readme.hints.md +20 -4
- package/readme.md +23 -6
- package/third-party-notices.md +29 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/interfaces/index.ts +1 -0
- package/ts/mod_custom/index.ts +347 -96
- package/ts/mod_esbuild/index.child.ts +78 -47
- package/ts/mod_esbuild/plugins.ts +9 -2
- package/ts/mod_esbuild/workerplugin.ts +328 -0
- package/ts/mod_output/artifactpublisher.ts +1185 -0
- package/ts/mod_output/index.ts +10 -2
- package/ts/mod_rolldown/index.child.ts +65 -42
- package/ts/mod_rspack/index.child.ts +95 -127
- package/ts/plugins.ts +3 -1
- package/ts/tsbundle.class.tsbundle.ts +102 -14
package/ts/mod_custom/index.ts
CHANGED
|
@@ -2,20 +2,36 @@ import * as plugins from './plugins.js';
|
|
|
2
2
|
import * as paths from '../paths.js';
|
|
3
3
|
import * as interfaces from '../interfaces/index.js';
|
|
4
4
|
import { TsBundle } from '../tsbundle.class.tsbundle.js';
|
|
5
|
-
import { HtmlHandler } from '../mod_html/index.js';
|
|
6
5
|
import { Base64TsOutput } from '../mod_output/index.js';
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import {
|
|
7
|
+
chunkManifestFile,
|
|
8
|
+
createProcessOwnership,
|
|
9
|
+
getChunkNamespace,
|
|
10
|
+
isProcessOwnershipActive,
|
|
11
|
+
publishGeneratedArtifacts,
|
|
12
|
+
startOwnershipHeartbeat,
|
|
13
|
+
withArtifactPublicationLocks,
|
|
14
|
+
type IAdditionalArtifact,
|
|
15
|
+
} from '../mod_output/artifactpublisher.js';
|
|
16
|
+
|
|
17
|
+
const legacyTempDir = '.nogit/tsbundle-temp';
|
|
18
|
+
const localTempPrefix = 'tsbundle-temp-';
|
|
19
|
+
const cleanupTempPrefix = '.tsbundle-temp-cleanup-';
|
|
9
20
|
const toolCacheMarkerFile = '.gitzone-tool-cache.json';
|
|
10
21
|
const toolCacheOwner = '@git.zone/tsbundle';
|
|
11
22
|
const tempCacheKind = 'bundle-temp-workspace';
|
|
12
23
|
const staleTempThresholdMs = 24 * 60 * 60 * 1000;
|
|
24
|
+
const minimumTempLeaseTimeoutMs = 2 * 60 * 1000;
|
|
13
25
|
|
|
14
26
|
interface IToolCacheMarker {
|
|
15
27
|
owner: string;
|
|
16
28
|
kind: string;
|
|
17
29
|
safeToPrune: boolean;
|
|
18
30
|
createdAt: string;
|
|
31
|
+
invocationId?: string;
|
|
32
|
+
leaseManaged?: boolean;
|
|
33
|
+
processId?: number;
|
|
34
|
+
processIdentity?: string;
|
|
19
35
|
schemaVersion: 1;
|
|
20
36
|
}
|
|
21
37
|
|
|
@@ -23,6 +39,7 @@ interface ITempWorkspace {
|
|
|
23
39
|
tempDir: string;
|
|
24
40
|
canCleanupWorkspace: boolean;
|
|
25
41
|
cleanupOnFailure: boolean;
|
|
42
|
+
marker?: IToolCacheMarker;
|
|
26
43
|
}
|
|
27
44
|
|
|
28
45
|
export class CustomBundleHandler {
|
|
@@ -30,6 +47,7 @@ export class CustomBundleHandler {
|
|
|
30
47
|
private config!: interfaces.ITsbundleConfig;
|
|
31
48
|
private cliOptions: interfaces.ICustomBundleCliOptions;
|
|
32
49
|
private didRunStartupCleanup = false;
|
|
50
|
+
private readonly invocationId = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
33
51
|
|
|
34
52
|
constructor(cwd: string = paths.cwd, cliOptions: interfaces.ICustomBundleCliOptions = {}) {
|
|
35
53
|
this.cwd = cwd;
|
|
@@ -78,16 +96,17 @@ export class CustomBundleHandler {
|
|
|
78
96
|
const tempWorkspace = await this.prepareTempWorkspace();
|
|
79
97
|
const tempDir = tempWorkspace.tempDir;
|
|
80
98
|
const tempBundlePath = plugins.path.join(tempDir, 'bundle.js');
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
await plugins.fsPromises.rm(tempBundlePath, { force: true });
|
|
85
|
-
await plugins.fsPromises.rm(`${tempBundlePath}.map`, { force: true });
|
|
99
|
+
const stopTempHeartbeat = tempWorkspace.marker
|
|
100
|
+
? startOwnershipHeartbeat(plugins.path.join(tempDir, toolCacheMarkerFile))
|
|
101
|
+
: undefined;
|
|
86
102
|
|
|
87
103
|
// Build the bundle to temp location
|
|
88
104
|
const tsbundle = new TsBundle();
|
|
89
105
|
let completedSuccessfully = false;
|
|
90
106
|
try {
|
|
107
|
+
// A marked workspace can survive a failed or explicitly retained build.
|
|
108
|
+
// Clear prior generated artifacts so stale chunks cannot leak into output.
|
|
109
|
+
await this.clearTempBuildArtifacts(tempDir);
|
|
91
110
|
await tsbundle.build(
|
|
92
111
|
this.cwd,
|
|
93
112
|
bundleConfig.from,
|
|
@@ -96,6 +115,7 @@ export class CustomBundleHandler {
|
|
|
96
115
|
bundler,
|
|
97
116
|
production: bundleConfig.production || false,
|
|
98
117
|
sourcemap: bundleConfig.sourcemap !== false,
|
|
118
|
+
chunkOwner: plugins.path.basename(bundleConfig.to),
|
|
99
119
|
}
|
|
100
120
|
);
|
|
101
121
|
|
|
@@ -106,18 +126,19 @@ export class CustomBundleHandler {
|
|
|
106
126
|
}
|
|
107
127
|
|
|
108
128
|
completedSuccessfully = true;
|
|
109
|
-
} catch (error:
|
|
129
|
+
} catch (error: unknown) {
|
|
110
130
|
console.error(`\n\x1b[31m❌ Bundle failed: ${bundleConfig.from} -> ${bundleConfig.to}\x1b[0m`);
|
|
111
131
|
// Don't re-print error details - they were already shown by the child process.
|
|
112
132
|
// Propagate the failure so package builds cannot succeed without their bundle.
|
|
113
133
|
throw error;
|
|
114
134
|
} finally {
|
|
135
|
+
stopTempHeartbeat?.();
|
|
115
136
|
if (completedSuccessfully && tempWorkspace.canCleanupWorkspace && !this.shouldKeepTemp(bundleConfig)) {
|
|
116
|
-
await this.cleanupTempWorkspace('successful bundle', tempDir);
|
|
137
|
+
await this.cleanupTempWorkspace('successful bundle', tempDir, tempWorkspace.marker);
|
|
117
138
|
} else if (completedSuccessfully) {
|
|
118
139
|
console.log(`Keeping tsbundle temp workspace: ${tempDir}`);
|
|
119
140
|
} else if (tempWorkspace.cleanupOnFailure && !this.shouldKeepTemp(bundleConfig)) {
|
|
120
|
-
await this.cleanupTempWorkspace('failed bundle', tempDir);
|
|
141
|
+
await this.cleanupTempWorkspace('failed bundle', tempDir, tempWorkspace.marker);
|
|
121
142
|
}
|
|
122
143
|
}
|
|
123
144
|
}
|
|
@@ -132,42 +153,77 @@ export class CustomBundleHandler {
|
|
|
132
153
|
return false;
|
|
133
154
|
}
|
|
134
155
|
|
|
135
|
-
|
|
136
|
-
|
|
156
|
+
let didCleanup = await this.cleanupStaleMarkedTempWorkspaces(
|
|
157
|
+
this.getProjectTempRoot(),
|
|
158
|
+
thresholdMsArg,
|
|
159
|
+
);
|
|
160
|
+
didCleanup = await this.cleanupStaleMarkedTempWorkspaces(
|
|
161
|
+
plugins.os.tmpdir(),
|
|
162
|
+
thresholdMsArg,
|
|
163
|
+
) || didCleanup;
|
|
164
|
+
|
|
165
|
+
const legacyTempPath = this.getTempDir();
|
|
166
|
+
const marker = await this.readTempMarker(legacyTempPath);
|
|
137
167
|
if (!marker) {
|
|
138
|
-
return
|
|
168
|
+
return didCleanup;
|
|
139
169
|
}
|
|
140
170
|
|
|
141
171
|
const createdAt = Date.parse(marker.createdAt);
|
|
142
|
-
if (Number.isFinite(createdAt)
|
|
143
|
-
return
|
|
172
|
+
if (!Number.isFinite(createdAt) || Date.now() - createdAt < thresholdMsArg) {
|
|
173
|
+
return didCleanup;
|
|
174
|
+
}
|
|
175
|
+
if (this.isTempMarkerActive(marker, legacyTempPath, thresholdMsArg)) {
|
|
176
|
+
return didCleanup;
|
|
144
177
|
}
|
|
145
178
|
|
|
146
|
-
await this.cleanupTempWorkspace(
|
|
147
|
-
|
|
179
|
+
didCleanup = await this.cleanupTempWorkspace(
|
|
180
|
+
'stale marked temp workspace',
|
|
181
|
+
legacyTempPath,
|
|
182
|
+
marker,
|
|
183
|
+
) || didCleanup;
|
|
184
|
+
return didCleanup;
|
|
148
185
|
}
|
|
149
186
|
|
|
150
187
|
private getTempDir(): string {
|
|
151
|
-
return plugins.path.join(this.cwd,
|
|
188
|
+
return plugins.path.join(this.cwd, legacyTempDir);
|
|
152
189
|
}
|
|
153
190
|
|
|
154
|
-
private
|
|
155
|
-
|
|
156
|
-
const actual = plugins.path.resolve(tempDirArg);
|
|
157
|
-
if (actual !== expected) {
|
|
158
|
-
throw new Error(`Refusing to clean unexpected tsbundle temp path: ${tempDirArg}`);
|
|
159
|
-
}
|
|
191
|
+
private getProjectTempRoot(): string {
|
|
192
|
+
return plugins.path.join(this.cwd, '.nogit');
|
|
160
193
|
}
|
|
161
194
|
|
|
162
195
|
private assertManagedTempDir(tempDirArg: string): void {
|
|
163
|
-
const expected = plugins.path.resolve(this.cwd,
|
|
196
|
+
const expected = plugins.path.resolve(this.cwd, legacyTempDir);
|
|
164
197
|
const actual = plugins.path.resolve(tempDirArg);
|
|
198
|
+
const projectTempRoot = plugins.path.resolve(this.getProjectTempRoot());
|
|
165
199
|
const osTempRoot = plugins.path.resolve(plugins.os.tmpdir());
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
200
|
+
const isExpectedTemp = actual === expected;
|
|
201
|
+
const basename = plugins.path.basename(actual);
|
|
202
|
+
const hasManagedName = basename.startsWith(localTempPrefix)
|
|
203
|
+
|| basename.startsWith(cleanupTempPrefix);
|
|
204
|
+
const isProjectTemp = plugins.path.dirname(actual) === projectTempRoot && hasManagedName;
|
|
205
|
+
const isOsTemp = plugins.path.dirname(actual) === osTempRoot && hasManagedName;
|
|
206
|
+
if (!isExpectedTemp && !isProjectTemp && !isOsTemp) {
|
|
169
207
|
throw new Error(`Refusing to clean unexpected tsbundle temp path: ${tempDirArg}`);
|
|
170
208
|
}
|
|
209
|
+
|
|
210
|
+
const stat = plugins.fsSync.lstatSync(actual);
|
|
211
|
+
if (stat.isSymbolicLink()) {
|
|
212
|
+
throw new Error(`Refusing to use symbolic-link temp path: ${tempDirArg}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const physicalActual = plugins.fsSync.realpathSync(actual);
|
|
216
|
+
const physicalParent = plugins.fsSync.realpathSync(plugins.path.dirname(actual));
|
|
217
|
+
const physicalCwd = plugins.fsSync.realpathSync(this.cwd);
|
|
218
|
+
const physicalExpected = plugins.path.join(physicalCwd, legacyTempDir);
|
|
219
|
+
const physicalProjectTempRoot = plugins.path.join(physicalCwd, '.nogit');
|
|
220
|
+
const physicalOsTempRoot = plugins.fsSync.realpathSync(osTempRoot);
|
|
221
|
+
const isPhysicalExpected = isExpectedTemp && physicalActual === physicalExpected;
|
|
222
|
+
const isPhysicalProjectTemp = isProjectTemp && physicalParent === physicalProjectTempRoot;
|
|
223
|
+
const isPhysicalOsTemp = isOsTemp && physicalParent === physicalOsTempRoot;
|
|
224
|
+
if (!isPhysicalExpected && !isPhysicalProjectTemp && !isPhysicalOsTemp) {
|
|
225
|
+
throw new Error(`Refusing to use temp path outside its physical owner: ${tempDirArg}`);
|
|
226
|
+
}
|
|
171
227
|
}
|
|
172
228
|
|
|
173
229
|
private isTruthy(valueArg: unknown): boolean {
|
|
@@ -181,6 +237,43 @@ export class CustomBundleHandler {
|
|
|
181
237
|
return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'y';
|
|
182
238
|
}
|
|
183
239
|
|
|
240
|
+
private hasErrorCode(errorArg: unknown): errorArg is NodeJS.ErrnoException {
|
|
241
|
+
return typeof errorArg === 'object' && errorArg !== null && 'code' in errorArg;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private isTempMarkerActive(
|
|
245
|
+
markerArg: IToolCacheMarker,
|
|
246
|
+
tempDirArg: string,
|
|
247
|
+
leaseTimeoutMsArg: number,
|
|
248
|
+
): boolean {
|
|
249
|
+
if (!markerArg.processId) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
return isProcessOwnershipActive(
|
|
253
|
+
{
|
|
254
|
+
leaseManaged: markerArg.leaseManaged,
|
|
255
|
+
processId: markerArg.processId,
|
|
256
|
+
processIdentity: markerArg.processIdentity,
|
|
257
|
+
},
|
|
258
|
+
plugins.path.join(tempDirArg, toolCacheMarkerFile),
|
|
259
|
+
Math.max(leaseTimeoutMsArg, minimumTempLeaseTimeoutMs),
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private markersMatch(
|
|
264
|
+
firstMarkerArg: IToolCacheMarker,
|
|
265
|
+
secondMarkerArg: IToolCacheMarker,
|
|
266
|
+
): boolean {
|
|
267
|
+
return firstMarkerArg.owner === secondMarkerArg.owner
|
|
268
|
+
&& firstMarkerArg.kind === secondMarkerArg.kind
|
|
269
|
+
&& firstMarkerArg.createdAt === secondMarkerArg.createdAt
|
|
270
|
+
&& firstMarkerArg.invocationId === secondMarkerArg.invocationId
|
|
271
|
+
&& firstMarkerArg.processId === secondMarkerArg.processId
|
|
272
|
+
&& firstMarkerArg.processIdentity === secondMarkerArg.processIdentity
|
|
273
|
+
&& firstMarkerArg.leaseManaged === secondMarkerArg.leaseManaged
|
|
274
|
+
&& firstMarkerArg.schemaVersion === secondMarkerArg.schemaVersion;
|
|
275
|
+
}
|
|
276
|
+
|
|
184
277
|
private shouldKeepTemp(bundleConfigArg?: interfaces.IBundleConfig): boolean {
|
|
185
278
|
return this.isTruthy(process.env.TSBUNDLE_KEEP_TEMP)
|
|
186
279
|
|| this.cliOptions.keepTemp === true
|
|
@@ -190,73 +283,184 @@ export class CustomBundleHandler {
|
|
|
190
283
|
}
|
|
191
284
|
|
|
192
285
|
private async prepareTempWorkspace(): Promise<ITempWorkspace> {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const marker = await this.readTempMarker(tempDirArg);
|
|
196
|
-
if (marker) {
|
|
197
|
-
return { tempDir: tempDirArg, canCleanupWorkspace: true, cleanupOnFailure: false };
|
|
198
|
-
}
|
|
286
|
+
return await this.createIsolatedTempWorkspace();
|
|
287
|
+
}
|
|
199
288
|
|
|
200
|
-
|
|
289
|
+
private async createIsolatedTempWorkspace(): Promise<ITempWorkspace> {
|
|
290
|
+
const projectTempRoot = this.getProjectTempRoot();
|
|
291
|
+
await plugins.fsPromises.mkdir(projectTempRoot, { recursive: true });
|
|
292
|
+
const rootStat = plugins.fsSync.lstatSync(projectTempRoot);
|
|
293
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
294
|
+
throw new Error(`Refusing to use unsafe project temp root: ${projectTempRoot}`);
|
|
295
|
+
}
|
|
296
|
+
const expectedPhysicalRoot = plugins.path.join(plugins.fsSync.realpathSync(this.cwd), '.nogit');
|
|
297
|
+
if (plugins.fsSync.realpathSync(projectTempRoot) !== expectedPhysicalRoot) {
|
|
298
|
+
throw new Error(`Refusing to use project temp root outside the project: ${projectTempRoot}`);
|
|
299
|
+
}
|
|
300
|
+
const tempDir = await plugins.fsPromises.mkdtemp(
|
|
301
|
+
plugins.path.join(projectTempRoot, localTempPrefix),
|
|
302
|
+
);
|
|
201
303
|
try {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
304
|
+
const marker = await this.writeTempMarker(tempDir);
|
|
305
|
+
return {
|
|
306
|
+
tempDir,
|
|
307
|
+
canCleanupWorkspace: true,
|
|
308
|
+
cleanupOnFailure: true,
|
|
309
|
+
marker,
|
|
310
|
+
};
|
|
311
|
+
} catch (error: unknown) {
|
|
312
|
+
await plugins.fsPromises.rm(tempDir, { recursive: true, force: true });
|
|
313
|
+
throw error;
|
|
205
314
|
}
|
|
315
|
+
}
|
|
206
316
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
317
|
+
private async clearTempBuildArtifacts(tempDirArg: string): Promise<void> {
|
|
318
|
+
this.assertManagedTempDir(tempDirArg);
|
|
319
|
+
const entries = await plugins.fsPromises.readdir(tempDirArg, { withFileTypes: true });
|
|
320
|
+
for (const entry of entries) {
|
|
321
|
+
if (entry.name === toolCacheMarkerFile) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
await plugins.fsPromises.rm(plugins.path.join(tempDirArg, entry.name), {
|
|
325
|
+
recursive: true,
|
|
326
|
+
force: true,
|
|
327
|
+
});
|
|
212
328
|
}
|
|
213
|
-
|
|
214
|
-
await this.writeTempMarker(tempDirArg);
|
|
215
|
-
return { tempDir: tempDirArg, canCleanupWorkspace: true, cleanupOnFailure: false };
|
|
216
329
|
}
|
|
217
330
|
|
|
218
|
-
private async
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
331
|
+
private async listBundleArtifacts(tempDirArg: string): Promise<string[]> {
|
|
332
|
+
const result: string[] = [];
|
|
333
|
+
const collect = async (directoryArg: string): Promise<void> => {
|
|
334
|
+
const entries = await plugins.fsPromises.readdir(directoryArg, { withFileTypes: true });
|
|
335
|
+
for (const entry of entries) {
|
|
336
|
+
if (entry.name === toolCacheMarkerFile || entry.name === chunkManifestFile) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const entryPath = plugins.path.join(directoryArg, entry.name);
|
|
340
|
+
if (entry.isDirectory()) {
|
|
341
|
+
await collect(entryPath);
|
|
342
|
+
} else if (entry.isFile()) {
|
|
343
|
+
result.push(entryPath);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
await collect(tempDirArg);
|
|
348
|
+
return result.sort((a, b) => a.localeCompare(b));
|
|
222
349
|
}
|
|
223
350
|
|
|
224
|
-
private async writeTempMarker(
|
|
351
|
+
private async writeTempMarker(
|
|
352
|
+
tempDirArg: string,
|
|
353
|
+
exclusive = false,
|
|
354
|
+
): Promise<IToolCacheMarker> {
|
|
355
|
+
await plugins.fsPromises.mkdir(tempDirArg, { recursive: true });
|
|
225
356
|
this.assertManagedTempDir(tempDirArg);
|
|
226
357
|
const marker: IToolCacheMarker = {
|
|
227
358
|
owner: toolCacheOwner,
|
|
228
359
|
kind: tempCacheKind,
|
|
229
360
|
safeToPrune: true,
|
|
230
361
|
createdAt: new Date().toISOString(),
|
|
362
|
+
invocationId: this.invocationId,
|
|
363
|
+
...createProcessOwnership(),
|
|
231
364
|
schemaVersion: 1,
|
|
232
365
|
};
|
|
233
|
-
await plugins.fsPromises.mkdir(tempDirArg, { recursive: true });
|
|
234
366
|
await plugins.fsPromises.writeFile(
|
|
235
367
|
plugins.path.join(tempDirArg, toolCacheMarkerFile),
|
|
236
368
|
`${JSON.stringify(marker, null, 2)}\n`,
|
|
369
|
+
{ flag: exclusive ? 'wx' : 'w', mode: 0o600 },
|
|
237
370
|
);
|
|
371
|
+
return marker;
|
|
238
372
|
}
|
|
239
373
|
|
|
240
374
|
private async readTempMarker(tempDirArg: string): Promise<IToolCacheMarker | undefined> {
|
|
241
|
-
this.assertManagedTempDir(tempDirArg);
|
|
242
375
|
try {
|
|
376
|
+
this.assertManagedTempDir(tempDirArg);
|
|
243
377
|
const markerRaw = await plugins.fsPromises.readFile(plugins.path.join(tempDirArg, toolCacheMarkerFile), 'utf8');
|
|
244
|
-
|
|
378
|
+
let marker: Partial<IToolCacheMarker>;
|
|
379
|
+
try {
|
|
380
|
+
marker = JSON.parse(markerRaw) as Partial<IToolCacheMarker>;
|
|
381
|
+
} catch {
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
245
384
|
if (
|
|
246
385
|
marker.owner === toolCacheOwner &&
|
|
247
386
|
marker.kind === tempCacheKind &&
|
|
248
|
-
|
|
249
|
-
|
|
387
|
+
marker.safeToPrune === true &&
|
|
388
|
+
(marker.invocationId === undefined || (
|
|
389
|
+
typeof marker.invocationId === 'string'
|
|
390
|
+
&& /^[A-Za-z0-9._-]{1,128}$/.test(marker.invocationId)
|
|
391
|
+
)) &&
|
|
392
|
+
(marker.processId === undefined || (
|
|
393
|
+
typeof marker.processId === 'number'
|
|
394
|
+
&& Number.isSafeInteger(marker.processId)
|
|
395
|
+
&& marker.processId > 0
|
|
396
|
+
)) &&
|
|
397
|
+
(marker.processIdentity === undefined || typeof marker.processIdentity === 'string') &&
|
|
398
|
+
(marker.leaseManaged === undefined || typeof marker.leaseManaged === 'boolean') &&
|
|
399
|
+
marker.schemaVersion === 1
|
|
250
400
|
) {
|
|
251
401
|
return marker as IToolCacheMarker;
|
|
252
402
|
}
|
|
253
|
-
} catch {
|
|
254
|
-
|
|
403
|
+
} catch (error: unknown) {
|
|
404
|
+
if (this.hasErrorCode(error) && error.code === 'ENOENT') {
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
407
|
+
throw error;
|
|
255
408
|
}
|
|
256
409
|
|
|
257
410
|
return undefined;
|
|
258
411
|
}
|
|
259
412
|
|
|
413
|
+
private async cleanupStaleMarkedTempWorkspaces(
|
|
414
|
+
rootDirectoryArg: string,
|
|
415
|
+
thresholdMsArg: number,
|
|
416
|
+
): Promise<boolean> {
|
|
417
|
+
let didCleanup = false;
|
|
418
|
+
let entries;
|
|
419
|
+
try {
|
|
420
|
+
entries = await plugins.fsPromises.readdir(rootDirectoryArg, { withFileTypes: true });
|
|
421
|
+
} catch (error: unknown) {
|
|
422
|
+
if (this.hasErrorCode(error) && error.code === 'ENOENT') {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
for (const entry of entries) {
|
|
428
|
+
if (
|
|
429
|
+
!entry.isDirectory()
|
|
430
|
+
|| (!entry.name.startsWith(localTempPrefix) && !entry.name.startsWith(cleanupTempPrefix))
|
|
431
|
+
) {
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const tempDir = plugins.path.join(rootDirectoryArg, entry.name);
|
|
435
|
+
let marker: IToolCacheMarker | undefined;
|
|
436
|
+
try {
|
|
437
|
+
marker = await this.readTempMarker(tempDir);
|
|
438
|
+
} catch (error: unknown) {
|
|
439
|
+
if (this.hasErrorCode(error) && error.code === 'ENOENT') {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
throw error;
|
|
443
|
+
}
|
|
444
|
+
if (!marker) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
const createdAt = Date.parse(marker.createdAt);
|
|
448
|
+
if (
|
|
449
|
+
!Number.isFinite(createdAt)
|
|
450
|
+
|| Date.now() - createdAt < thresholdMsArg
|
|
451
|
+
|| this.isTempMarkerActive(marker, tempDir, thresholdMsArg)
|
|
452
|
+
) {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
didCleanup = await this.cleanupTempWorkspace(
|
|
456
|
+
'stale isolated temp workspace',
|
|
457
|
+
tempDir,
|
|
458
|
+
marker,
|
|
459
|
+
) || didCleanup;
|
|
460
|
+
}
|
|
461
|
+
return didCleanup;
|
|
462
|
+
}
|
|
463
|
+
|
|
260
464
|
private async getDirectorySize(pathArg: string): Promise<number> {
|
|
261
465
|
let total = 0;
|
|
262
466
|
let entries;
|
|
@@ -294,15 +498,53 @@ export class CustomBundleHandler {
|
|
|
294
498
|
return `${value.toFixed(decimals)} ${units[unitIndex]}`;
|
|
295
499
|
}
|
|
296
500
|
|
|
297
|
-
private async cleanupTempWorkspace(
|
|
298
|
-
|
|
501
|
+
private async cleanupTempWorkspace(
|
|
502
|
+
reasonArg: string,
|
|
503
|
+
tempDir = this.getTempDir(),
|
|
504
|
+
expectedMarkerArg?: IToolCacheMarker,
|
|
505
|
+
): Promise<boolean> {
|
|
506
|
+
try {
|
|
507
|
+
this.assertManagedTempDir(tempDir);
|
|
508
|
+
} catch (error: unknown) {
|
|
509
|
+
if (this.hasErrorCode(error) && error.code === 'ENOENT') {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
throw error;
|
|
513
|
+
}
|
|
299
514
|
const marker = await this.readTempMarker(tempDir);
|
|
300
|
-
if (!marker) {
|
|
301
|
-
return;
|
|
515
|
+
if (!marker || (expectedMarkerArg && !this.markersMatch(marker, expectedMarkerArg))) {
|
|
516
|
+
return false;
|
|
302
517
|
}
|
|
303
|
-
const
|
|
304
|
-
|
|
518
|
+
const quarantinePath = plugins.path.join(
|
|
519
|
+
plugins.path.dirname(tempDir),
|
|
520
|
+
`${cleanupTempPrefix}${this.invocationId}-${Math.random().toString(36).slice(2)}`,
|
|
521
|
+
);
|
|
522
|
+
try {
|
|
523
|
+
await plugins.fsPromises.rename(tempDir, quarantinePath);
|
|
524
|
+
} catch (error: unknown) {
|
|
525
|
+
if (this.hasErrorCode(error) && error.code === 'ENOENT') {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
throw error;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const quarantinedMarker = await this.readTempMarker(quarantinePath);
|
|
532
|
+
if (!quarantinedMarker || !this.markersMatch(marker, quarantinedMarker)) {
|
|
533
|
+
try {
|
|
534
|
+
if (!plugins.fsSync.existsSync(tempDir)) {
|
|
535
|
+
await plugins.fsPromises.rename(quarantinePath, tempDir);
|
|
536
|
+
}
|
|
537
|
+
} catch (error: unknown) {
|
|
538
|
+
if (!this.hasErrorCode(error) || !['EEXIST', 'ENOENT'].includes(error.code || '')) {
|
|
539
|
+
throw error;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
const reclaimedBytes = await this.getDirectorySize(quarantinePath);
|
|
545
|
+
await plugins.fsPromises.rm(quarantinePath, { recursive: true, force: true });
|
|
305
546
|
console.log(`Cleaned tsbundle temp workspace after ${reasonArg}, reclaimed ${this.formatBytes(reclaimedBytes)}`);
|
|
547
|
+
return true;
|
|
306
548
|
}
|
|
307
549
|
|
|
308
550
|
/**
|
|
@@ -313,10 +555,12 @@ export class CustomBundleHandler {
|
|
|
313
555
|
tempBundlePath: string
|
|
314
556
|
): Promise<void> {
|
|
315
557
|
const base64Output = new Base64TsOutput(this.cwd);
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
558
|
+
const tempDir = plugins.path.dirname(tempBundlePath);
|
|
559
|
+
for (const artifactPath of await this.listBundleArtifacts(tempDir)) {
|
|
560
|
+
const relativePath = plugins.path.relative(tempDir, artifactPath);
|
|
561
|
+
const artifactContent = await plugins.fs.file(artifactPath).read();
|
|
562
|
+
base64Output.addFile(relativePath, artifactContent);
|
|
563
|
+
}
|
|
320
564
|
|
|
321
565
|
// Add included files
|
|
322
566
|
if (bundleConfig.includeFiles && bundleConfig.includeFiles.length > 0) {
|
|
@@ -330,7 +574,10 @@ export class CustomBundleHandler {
|
|
|
330
574
|
}
|
|
331
575
|
|
|
332
576
|
// Write the TypeScript output
|
|
333
|
-
|
|
577
|
+
const outputPath = plugins.smartpath.transform.toAbsolute(bundleConfig.to, this.cwd) as string;
|
|
578
|
+
await withArtifactPublicationLocks(outputPath, async () => {
|
|
579
|
+
await base64Output.writeToFile(bundleConfig.to, bundleConfig.maxLineLength);
|
|
580
|
+
});
|
|
334
581
|
}
|
|
335
582
|
|
|
336
583
|
/**
|
|
@@ -340,35 +587,42 @@ export class CustomBundleHandler {
|
|
|
340
587
|
bundleConfig: interfaces.IBundleConfig,
|
|
341
588
|
tempBundlePath: string
|
|
342
589
|
): Promise<void> {
|
|
343
|
-
// Move bundle to final destination
|
|
344
590
|
const toPath = plugins.smartpath.transform.toAbsolute(bundleConfig.to, this.cwd) as string;
|
|
345
591
|
const toDir = plugins.path.dirname(toPath);
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
await plugins.fs.file(toPath).write(bundleContent);
|
|
350
|
-
console.log(`Bundle written to: ${bundleConfig.to}`);
|
|
351
|
-
|
|
352
|
-
if (bundleConfig.sourcemap === false) {
|
|
353
|
-
await plugins.fsPromises.rm(`${toPath}.map`, { force: true });
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// Process included files (copy them)
|
|
592
|
+
const tempDir = plugins.path.dirname(tempBundlePath);
|
|
593
|
+
const logicalOutputName = plugins.path.basename(toPath);
|
|
594
|
+
const additionalArtifacts: IAdditionalArtifact[] = [];
|
|
357
595
|
if (bundleConfig.includeFiles && bundleConfig.includeFiles.length > 0) {
|
|
358
|
-
const htmlHandler = new HtmlHandler();
|
|
359
|
-
const outputDir = plugins.path.dirname(toPath);
|
|
360
|
-
|
|
361
596
|
for (const entry of bundleConfig.includeFiles) {
|
|
362
597
|
const pattern = typeof entry === 'string' ? entry : entry.from;
|
|
363
|
-
await this.
|
|
598
|
+
additionalArtifacts.push(...await this.collectIncludedFiles(pattern, toDir));
|
|
364
599
|
}
|
|
365
600
|
}
|
|
601
|
+
await publishGeneratedArtifacts({
|
|
602
|
+
additionalArtifacts,
|
|
603
|
+
sourceDirectory: tempDir,
|
|
604
|
+
sourceMainPath: tempBundlePath,
|
|
605
|
+
targetPath: toPath,
|
|
606
|
+
lockTargetPath: plugins.path.join(toDir, '.tsbundle-output-directory'),
|
|
607
|
+
logicalOutputName,
|
|
608
|
+
chunkNamespace: getChunkNamespace(logicalOutputName),
|
|
609
|
+
sourceMapsEnabled: bundleConfig.sourcemap !== false,
|
|
610
|
+
});
|
|
611
|
+
console.log(`Bundle written to: ${bundleConfig.to}`);
|
|
612
|
+
for (const artifact of additionalArtifacts) {
|
|
613
|
+
const relativePath = plugins.path.relative(this.cwd, artifact.sourcePath);
|
|
614
|
+
console.log(`Copied: ${relativePath} -> ${artifact.targetPath}`);
|
|
615
|
+
}
|
|
366
616
|
}
|
|
367
617
|
|
|
368
618
|
/**
|
|
369
|
-
*
|
|
619
|
+
* Resolve files matching a pattern for transactional publication.
|
|
370
620
|
*/
|
|
371
|
-
private async
|
|
621
|
+
private async collectIncludedFiles(
|
|
622
|
+
pattern: string,
|
|
623
|
+
outputDir: string,
|
|
624
|
+
): Promise<IAdditionalArtifact[]> {
|
|
625
|
+
const artifacts: IAdditionalArtifact[] = [];
|
|
372
626
|
const absolutePattern = plugins.smartpath.transform.toAbsolute(pattern, this.cwd) as string;
|
|
373
627
|
const patternDir = plugins.path.dirname(absolutePattern);
|
|
374
628
|
const patternBase = plugins.path.basename(absolutePattern);
|
|
@@ -379,7 +633,7 @@ export class CustomBundleHandler {
|
|
|
379
633
|
const dirExists = await plugins.fs.directory(dirPath).exists();
|
|
380
634
|
if (!dirExists) {
|
|
381
635
|
console.log(`Directory does not exist: ${dirPath}`);
|
|
382
|
-
return;
|
|
636
|
+
return artifacts;
|
|
383
637
|
}
|
|
384
638
|
|
|
385
639
|
const isRecursive = pattern.includes('**');
|
|
@@ -397,24 +651,21 @@ export class CustomBundleHandler {
|
|
|
397
651
|
if (!entry.isDirectory && regex.test(entry.name)) {
|
|
398
652
|
// entry.path is already absolute from smartfs
|
|
399
653
|
const fullPath = entry.path;
|
|
400
|
-
const relativePath = plugins.path.relative(this.cwd, fullPath);
|
|
401
654
|
const destPath = plugins.path.join(outputDir, plugins.path.basename(entry.path));
|
|
402
|
-
|
|
403
|
-
await plugins.fs.file(fullPath).copy(destPath);
|
|
404
|
-
console.log(`Copied: ${relativePath} -> ${destPath}`);
|
|
655
|
+
artifacts.push({ sourcePath: fullPath, targetPath: destPath });
|
|
405
656
|
}
|
|
406
657
|
}
|
|
407
658
|
} else {
|
|
408
659
|
const fileExists = await plugins.fs.file(absolutePattern).exists();
|
|
409
660
|
if (!fileExists) {
|
|
410
661
|
console.log(`File does not exist: ${absolutePattern}`);
|
|
411
|
-
return;
|
|
662
|
+
return artifacts;
|
|
412
663
|
}
|
|
413
664
|
const fileName = plugins.path.basename(absolutePattern);
|
|
414
665
|
const destPath = plugins.path.join(outputDir, fileName);
|
|
415
|
-
|
|
416
|
-
console.log(`Copied: ${pattern} -> ${destPath}`);
|
|
666
|
+
artifacts.push({ sourcePath: absolutePattern, targetPath: destPath });
|
|
417
667
|
}
|
|
668
|
+
return artifacts;
|
|
418
669
|
}
|
|
419
670
|
}
|
|
420
671
|
|