@chidchanun/bcp 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -152
- package/docs/README.md +77 -126
- package/docs/api-manifest.json +3 -2
- package/docs/api-reference.md +22 -2
- package/docs/application-packaging.md +243 -0
- package/docs/auth-route-guards.md +72 -37
- package/docs/auth-session-store.md +204 -0
- package/docs/authentication.md +128 -44
- package/docs/deployment.md +94 -7
- package/docs/docs-web-manifest.json +8 -4
- package/docs/platform-manifest.json +22 -5
- package/docs/releases/0.2.4.md +152 -0
- package/docs/releases/0.2.5.md +152 -0
- package/package.json +1 -1
- package/packages/cli/src/application-packaging.ts +1162 -0
- package/packages/cli/src/args.ts +2 -0
- package/packages/cli/src/bootstrap.ts +1 -0
- package/packages/cli/src/index.ts +60 -2
- package/packages/client/src/auth.ts +16 -0
- package/packages/server/src/auth-guard.ts +95 -82
- package/packages/server/src/auth-session-store.ts +318 -0
- package/packages/server/src/auth.ts +443 -50
|
@@ -0,0 +1,1162 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createHash,
|
|
3
|
+
} from "node:crypto";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
export const APPLICATION_PACKAGE_SCHEMA_VERSION =
|
|
8
|
+
1;
|
|
9
|
+
export const APPLICATION_PACKAGE_TARGET =
|
|
10
|
+
"standalone-node";
|
|
11
|
+
export const APPLICATION_PACKAGE_NODE_RANGE =
|
|
12
|
+
">=24.11.0";
|
|
13
|
+
|
|
14
|
+
interface JsonObject {
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ApplicationPackageJson
|
|
19
|
+
extends JsonObject {
|
|
20
|
+
name?: string;
|
|
21
|
+
version?: string;
|
|
22
|
+
type?: string;
|
|
23
|
+
private?: boolean;
|
|
24
|
+
scripts?: Record<string, string>;
|
|
25
|
+
engines?: Record<string, string>;
|
|
26
|
+
dependencies?: Record<string, string>;
|
|
27
|
+
optionalDependencies?: Record<string, string>;
|
|
28
|
+
peerDependencies?: Record<string, string>;
|
|
29
|
+
peerDependenciesMeta?: JsonObject;
|
|
30
|
+
overrides?: JsonObject;
|
|
31
|
+
packageManager?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface PackageLockPackage
|
|
35
|
+
extends JsonObject {
|
|
36
|
+
name?: string;
|
|
37
|
+
version?: string;
|
|
38
|
+
link?: boolean;
|
|
39
|
+
dev?: boolean;
|
|
40
|
+
dependencies?: Record<string, string>;
|
|
41
|
+
optionalDependencies?: Record<string, string>;
|
|
42
|
+
peerDependencies?: Record<string, string>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface PackageLockJson
|
|
46
|
+
extends JsonObject {
|
|
47
|
+
name?: string;
|
|
48
|
+
version?: string;
|
|
49
|
+
lockfileVersion?: number;
|
|
50
|
+
packages?: Record<
|
|
51
|
+
string,
|
|
52
|
+
PackageLockPackage
|
|
53
|
+
>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ApplicationPackageFile {
|
|
57
|
+
path: string;
|
|
58
|
+
bytes: number;
|
|
59
|
+
sha256: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ApplicationPackageManifest {
|
|
63
|
+
schemaVersion: 1;
|
|
64
|
+
target: "standalone-node";
|
|
65
|
+
frameworkVersion: string;
|
|
66
|
+
application: {
|
|
67
|
+
name: string;
|
|
68
|
+
version: string;
|
|
69
|
+
};
|
|
70
|
+
runtime: {
|
|
71
|
+
node: string;
|
|
72
|
+
entrypoint: string;
|
|
73
|
+
startCommand: string;
|
|
74
|
+
};
|
|
75
|
+
install: {
|
|
76
|
+
command: string;
|
|
77
|
+
lockfile: boolean;
|
|
78
|
+
devDependenciesIncluded: false;
|
|
79
|
+
};
|
|
80
|
+
directories: {
|
|
81
|
+
client: string;
|
|
82
|
+
server: string;
|
|
83
|
+
public: string | null;
|
|
84
|
+
};
|
|
85
|
+
buildManifest: string;
|
|
86
|
+
environmentManifest: string;
|
|
87
|
+
deploymentManifest: string;
|
|
88
|
+
files: ApplicationPackageFile[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ApplicationPackagingResult {
|
|
92
|
+
outputDirectory: string;
|
|
93
|
+
packageManifest: ApplicationPackageManifest;
|
|
94
|
+
lockfileIncluded: boolean;
|
|
95
|
+
warnings: string[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface PackageApplicationOptions {
|
|
99
|
+
rootDirectory: string;
|
|
100
|
+
frameworkVersion: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function packageApplication(
|
|
104
|
+
options: PackageApplicationOptions
|
|
105
|
+
): ApplicationPackagingResult {
|
|
106
|
+
const rootDirectory =
|
|
107
|
+
path.resolve(
|
|
108
|
+
options.rootDirectory
|
|
109
|
+
);
|
|
110
|
+
const buildDirectory =
|
|
111
|
+
path.join(
|
|
112
|
+
rootDirectory,
|
|
113
|
+
".bcp-framework",
|
|
114
|
+
"build"
|
|
115
|
+
);
|
|
116
|
+
const serverFile =
|
|
117
|
+
path.join(
|
|
118
|
+
buildDirectory,
|
|
119
|
+
"server",
|
|
120
|
+
"server.mjs"
|
|
121
|
+
);
|
|
122
|
+
const buildManifestFile =
|
|
123
|
+
path.join(
|
|
124
|
+
buildDirectory,
|
|
125
|
+
"manifest.json"
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (
|
|
129
|
+
!fs.existsSync(serverFile) ||
|
|
130
|
+
!fs.existsSync(
|
|
131
|
+
buildManifestFile
|
|
132
|
+
)
|
|
133
|
+
) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
"BCP Framework: production build was not found. Run `bcp build` before packaging."
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const sourcePackageFile =
|
|
140
|
+
path.join(
|
|
141
|
+
rootDirectory,
|
|
142
|
+
"package.json"
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
if (
|
|
146
|
+
!fs.existsSync(
|
|
147
|
+
sourcePackageFile
|
|
148
|
+
)
|
|
149
|
+
) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
"BCP Framework: package.json was not found in the project root."
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const sourcePackage =
|
|
156
|
+
readJson<ApplicationPackageJson>(
|
|
157
|
+
sourcePackageFile
|
|
158
|
+
);
|
|
159
|
+
const productionPackage =
|
|
160
|
+
createProductionPackageJson(
|
|
161
|
+
sourcePackage
|
|
162
|
+
);
|
|
163
|
+
const outputDirectory =
|
|
164
|
+
path.join(
|
|
165
|
+
rootDirectory,
|
|
166
|
+
".bcp-framework",
|
|
167
|
+
"package"
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
fs.rmSync(
|
|
171
|
+
outputDirectory,
|
|
172
|
+
{
|
|
173
|
+
recursive: true,
|
|
174
|
+
force: true,
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
fs.mkdirSync(
|
|
178
|
+
outputDirectory,
|
|
179
|
+
{
|
|
180
|
+
recursive: true,
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
copyBuildDirectory(
|
|
185
|
+
buildDirectory,
|
|
186
|
+
outputDirectory,
|
|
187
|
+
"client",
|
|
188
|
+
true
|
|
189
|
+
);
|
|
190
|
+
copyBuildDirectory(
|
|
191
|
+
buildDirectory,
|
|
192
|
+
outputDirectory,
|
|
193
|
+
"server",
|
|
194
|
+
true
|
|
195
|
+
);
|
|
196
|
+
copyBuildDirectory(
|
|
197
|
+
buildDirectory,
|
|
198
|
+
outputDirectory,
|
|
199
|
+
"public",
|
|
200
|
+
false
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
fs.copyFileSync(
|
|
204
|
+
buildManifestFile,
|
|
205
|
+
path.join(
|
|
206
|
+
outputDirectory,
|
|
207
|
+
"manifest.json"
|
|
208
|
+
)
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
writeJson(
|
|
212
|
+
path.join(
|
|
213
|
+
outputDirectory,
|
|
214
|
+
"package.json"
|
|
215
|
+
),
|
|
216
|
+
productionPackage
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const warnings:
|
|
220
|
+
string[] = [];
|
|
221
|
+
const lockfileIncluded =
|
|
222
|
+
writeProductionLockfile(
|
|
223
|
+
rootDirectory,
|
|
224
|
+
outputDirectory,
|
|
225
|
+
productionPackage,
|
|
226
|
+
warnings
|
|
227
|
+
);
|
|
228
|
+
const installCommand =
|
|
229
|
+
lockfileIncluded
|
|
230
|
+
? "npm ci --omit=dev"
|
|
231
|
+
: "npm install --omit=dev";
|
|
232
|
+
|
|
233
|
+
const environmentManifest =
|
|
234
|
+
createEnvironmentManifest(
|
|
235
|
+
rootDirectory
|
|
236
|
+
);
|
|
237
|
+
writeJson(
|
|
238
|
+
path.join(
|
|
239
|
+
outputDirectory,
|
|
240
|
+
"bcp.env.json"
|
|
241
|
+
),
|
|
242
|
+
environmentManifest
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const deploymentManifest = {
|
|
246
|
+
schemaVersion:
|
|
247
|
+
APPLICATION_PACKAGE_SCHEMA_VERSION,
|
|
248
|
+
target:
|
|
249
|
+
APPLICATION_PACKAGE_TARGET,
|
|
250
|
+
runtime: {
|
|
251
|
+
name:
|
|
252
|
+
"node",
|
|
253
|
+
version:
|
|
254
|
+
APPLICATION_PACKAGE_NODE_RANGE,
|
|
255
|
+
},
|
|
256
|
+
install: {
|
|
257
|
+
command:
|
|
258
|
+
installCommand,
|
|
259
|
+
},
|
|
260
|
+
start: {
|
|
261
|
+
command:
|
|
262
|
+
"npm start",
|
|
263
|
+
entrypoint:
|
|
264
|
+
"server/server.mjs",
|
|
265
|
+
},
|
|
266
|
+
docker: {
|
|
267
|
+
file:
|
|
268
|
+
"Dockerfile",
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
writeJson(
|
|
273
|
+
path.join(
|
|
274
|
+
outputDirectory,
|
|
275
|
+
"bcp.deployment.json"
|
|
276
|
+
),
|
|
277
|
+
deploymentManifest
|
|
278
|
+
);
|
|
279
|
+
fs.writeFileSync(
|
|
280
|
+
path.join(
|
|
281
|
+
outputDirectory,
|
|
282
|
+
"Dockerfile"
|
|
283
|
+
),
|
|
284
|
+
createDockerfile(),
|
|
285
|
+
"utf8"
|
|
286
|
+
);
|
|
287
|
+
fs.writeFileSync(
|
|
288
|
+
path.join(
|
|
289
|
+
outputDirectory,
|
|
290
|
+
".dockerignore"
|
|
291
|
+
),
|
|
292
|
+
createDockerIgnore(),
|
|
293
|
+
"utf8"
|
|
294
|
+
);
|
|
295
|
+
fs.writeFileSync(
|
|
296
|
+
path.join(
|
|
297
|
+
outputDirectory,
|
|
298
|
+
"README.md"
|
|
299
|
+
),
|
|
300
|
+
createPackageReadme(
|
|
301
|
+
installCommand
|
|
302
|
+
),
|
|
303
|
+
"utf8"
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
const files =
|
|
307
|
+
collectPackageFiles(
|
|
308
|
+
outputDirectory,
|
|
309
|
+
new Set([
|
|
310
|
+
"bcp.package.json",
|
|
311
|
+
])
|
|
312
|
+
);
|
|
313
|
+
const packageManifest:
|
|
314
|
+
ApplicationPackageManifest = {
|
|
315
|
+
schemaVersion:
|
|
316
|
+
APPLICATION_PACKAGE_SCHEMA_VERSION,
|
|
317
|
+
target:
|
|
318
|
+
APPLICATION_PACKAGE_TARGET,
|
|
319
|
+
frameworkVersion:
|
|
320
|
+
options.frameworkVersion,
|
|
321
|
+
application: {
|
|
322
|
+
name:
|
|
323
|
+
normalizeApplicationName(
|
|
324
|
+
productionPackage.name
|
|
325
|
+
),
|
|
326
|
+
version:
|
|
327
|
+
normalizeApplicationVersion(
|
|
328
|
+
productionPackage.version
|
|
329
|
+
),
|
|
330
|
+
},
|
|
331
|
+
runtime: {
|
|
332
|
+
node:
|
|
333
|
+
APPLICATION_PACKAGE_NODE_RANGE,
|
|
334
|
+
entrypoint:
|
|
335
|
+
"server/server.mjs",
|
|
336
|
+
startCommand:
|
|
337
|
+
"npm start",
|
|
338
|
+
},
|
|
339
|
+
install: {
|
|
340
|
+
command:
|
|
341
|
+
installCommand,
|
|
342
|
+
lockfile:
|
|
343
|
+
lockfileIncluded,
|
|
344
|
+
devDependenciesIncluded:
|
|
345
|
+
false,
|
|
346
|
+
},
|
|
347
|
+
directories: {
|
|
348
|
+
client:
|
|
349
|
+
"client",
|
|
350
|
+
server:
|
|
351
|
+
"server",
|
|
352
|
+
public:
|
|
353
|
+
fs.existsSync(
|
|
354
|
+
path.join(
|
|
355
|
+
outputDirectory,
|
|
356
|
+
"public"
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
? "public"
|
|
360
|
+
: null,
|
|
361
|
+
},
|
|
362
|
+
buildManifest:
|
|
363
|
+
"manifest.json",
|
|
364
|
+
environmentManifest:
|
|
365
|
+
"bcp.env.json",
|
|
366
|
+
deploymentManifest:
|
|
367
|
+
"bcp.deployment.json",
|
|
368
|
+
files,
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
writeJson(
|
|
372
|
+
path.join(
|
|
373
|
+
outputDirectory,
|
|
374
|
+
"bcp.package.json"
|
|
375
|
+
),
|
|
376
|
+
packageManifest
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
outputDirectory,
|
|
381
|
+
packageManifest,
|
|
382
|
+
lockfileIncluded,
|
|
383
|
+
warnings,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function createProductionPackageJson(
|
|
388
|
+
source: ApplicationPackageJson
|
|
389
|
+
): ApplicationPackageJson {
|
|
390
|
+
const result:
|
|
391
|
+
ApplicationPackageJson = {
|
|
392
|
+
name:
|
|
393
|
+
normalizeApplicationName(
|
|
394
|
+
source.name
|
|
395
|
+
),
|
|
396
|
+
version:
|
|
397
|
+
normalizeApplicationVersion(
|
|
398
|
+
source.version
|
|
399
|
+
),
|
|
400
|
+
private: true,
|
|
401
|
+
type:
|
|
402
|
+
source.type === "commonjs"
|
|
403
|
+
? "commonjs"
|
|
404
|
+
: "module",
|
|
405
|
+
scripts: {
|
|
406
|
+
start:
|
|
407
|
+
"node server/server.mjs",
|
|
408
|
+
},
|
|
409
|
+
engines: {
|
|
410
|
+
node:
|
|
411
|
+
APPLICATION_PACKAGE_NODE_RANGE,
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
copySortedStringMap(
|
|
416
|
+
source,
|
|
417
|
+
result,
|
|
418
|
+
"dependencies"
|
|
419
|
+
);
|
|
420
|
+
copySortedStringMap(
|
|
421
|
+
source,
|
|
422
|
+
result,
|
|
423
|
+
"optionalDependencies"
|
|
424
|
+
);
|
|
425
|
+
copySortedStringMap(
|
|
426
|
+
source,
|
|
427
|
+
result,
|
|
428
|
+
"peerDependencies"
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
if (
|
|
432
|
+
source.peerDependenciesMeta &&
|
|
433
|
+
isPlainObject(
|
|
434
|
+
source.peerDependenciesMeta
|
|
435
|
+
)
|
|
436
|
+
) {
|
|
437
|
+
result.peerDependenciesMeta =
|
|
438
|
+
sortObject(
|
|
439
|
+
source.peerDependenciesMeta
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (
|
|
444
|
+
source.overrides &&
|
|
445
|
+
isPlainObject(
|
|
446
|
+
source.overrides
|
|
447
|
+
)
|
|
448
|
+
) {
|
|
449
|
+
result.overrides =
|
|
450
|
+
sortObject(
|
|
451
|
+
source.overrides
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (
|
|
456
|
+
typeof source.packageManager ===
|
|
457
|
+
"string" &&
|
|
458
|
+
source.packageManager.trim() !==
|
|
459
|
+
""
|
|
460
|
+
) {
|
|
461
|
+
result.packageManager =
|
|
462
|
+
source.packageManager;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return result;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function createProductionLockfile(
|
|
469
|
+
sourceLock:
|
|
470
|
+
PackageLockJson,
|
|
471
|
+
productionPackage:
|
|
472
|
+
ApplicationPackageJson
|
|
473
|
+
): PackageLockJson | null {
|
|
474
|
+
if (
|
|
475
|
+
sourceLock.lockfileVersion !== 3 ||
|
|
476
|
+
!sourceLock.packages ||
|
|
477
|
+
!sourceLock.packages[""]
|
|
478
|
+
) {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const sourcePackages =
|
|
483
|
+
sourceLock.packages;
|
|
484
|
+
const selected =
|
|
485
|
+
new Set<string>([
|
|
486
|
+
"",
|
|
487
|
+
]);
|
|
488
|
+
const queue:
|
|
489
|
+
string[] = [
|
|
490
|
+
"",
|
|
491
|
+
];
|
|
492
|
+
|
|
493
|
+
while (
|
|
494
|
+
queue.length > 0
|
|
495
|
+
) {
|
|
496
|
+
const packagePath =
|
|
497
|
+
queue.shift() as string;
|
|
498
|
+
const packageEntry =
|
|
499
|
+
packagePath === ""
|
|
500
|
+
? createRootLockPackage(
|
|
501
|
+
sourcePackages[""],
|
|
502
|
+
productionPackage
|
|
503
|
+
)
|
|
504
|
+
: sourcePackages[
|
|
505
|
+
packagePath
|
|
506
|
+
];
|
|
507
|
+
|
|
508
|
+
if (!packageEntry) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (
|
|
513
|
+
packagePath !== "" &&
|
|
514
|
+
packageEntry.link === true
|
|
515
|
+
) {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const dependencyNames =
|
|
520
|
+
collectDependencyNames(
|
|
521
|
+
packageEntry
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
for (
|
|
525
|
+
const dependencyName
|
|
526
|
+
of dependencyNames
|
|
527
|
+
) {
|
|
528
|
+
const resolvedPath =
|
|
529
|
+
resolveLockPackagePath(
|
|
530
|
+
sourcePackages,
|
|
531
|
+
packagePath,
|
|
532
|
+
dependencyName
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
if (
|
|
536
|
+
!resolvedPath ||
|
|
537
|
+
selected.has(
|
|
538
|
+
resolvedPath
|
|
539
|
+
)
|
|
540
|
+
) {
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const resolvedEntry =
|
|
545
|
+
sourcePackages[
|
|
546
|
+
resolvedPath
|
|
547
|
+
];
|
|
548
|
+
|
|
549
|
+
if (
|
|
550
|
+
!resolvedEntry ||
|
|
551
|
+
resolvedEntry.dev === true
|
|
552
|
+
) {
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
selected.add(
|
|
557
|
+
resolvedPath
|
|
558
|
+
);
|
|
559
|
+
queue.push(
|
|
560
|
+
resolvedPath
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const packages:
|
|
566
|
+
Record<
|
|
567
|
+
string,
|
|
568
|
+
PackageLockPackage
|
|
569
|
+
> = {};
|
|
570
|
+
|
|
571
|
+
for (
|
|
572
|
+
const packagePath
|
|
573
|
+
of Array.from(
|
|
574
|
+
selected
|
|
575
|
+
).sort()
|
|
576
|
+
) {
|
|
577
|
+
if (
|
|
578
|
+
packagePath === ""
|
|
579
|
+
) {
|
|
580
|
+
packages[""] =
|
|
581
|
+
createRootLockPackage(
|
|
582
|
+
sourcePackages[""],
|
|
583
|
+
productionPackage
|
|
584
|
+
);
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const entry =
|
|
589
|
+
sourcePackages[
|
|
590
|
+
packagePath
|
|
591
|
+
];
|
|
592
|
+
|
|
593
|
+
if (entry) {
|
|
594
|
+
packages[
|
|
595
|
+
packagePath
|
|
596
|
+
] =
|
|
597
|
+
entry;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return {
|
|
602
|
+
name:
|
|
603
|
+
normalizeApplicationName(
|
|
604
|
+
productionPackage.name
|
|
605
|
+
),
|
|
606
|
+
version:
|
|
607
|
+
normalizeApplicationVersion(
|
|
608
|
+
productionPackage.version
|
|
609
|
+
),
|
|
610
|
+
lockfileVersion: 3,
|
|
611
|
+
requires:
|
|
612
|
+
sourceLock.requires ??
|
|
613
|
+
true,
|
|
614
|
+
packages,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
export function parseEnvironmentExampleKeys(
|
|
619
|
+
content: string
|
|
620
|
+
): string[] {
|
|
621
|
+
const keys =
|
|
622
|
+
new Set<string>();
|
|
623
|
+
|
|
624
|
+
for (
|
|
625
|
+
const line
|
|
626
|
+
of content.split(/\r?\n/)
|
|
627
|
+
) {
|
|
628
|
+
const trimmed =
|
|
629
|
+
line.trim();
|
|
630
|
+
|
|
631
|
+
if (
|
|
632
|
+
trimmed === "" ||
|
|
633
|
+
trimmed.startsWith("#")
|
|
634
|
+
) {
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const match =
|
|
639
|
+
/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(
|
|
640
|
+
trimmed
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
if (match) {
|
|
644
|
+
keys.add(
|
|
645
|
+
match[1]
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return Array.from(
|
|
651
|
+
keys
|
|
652
|
+
).sort();
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function writeProductionLockfile(
|
|
656
|
+
rootDirectory: string,
|
|
657
|
+
outputDirectory: string,
|
|
658
|
+
productionPackage: ApplicationPackageJson,
|
|
659
|
+
warnings: string[]
|
|
660
|
+
): boolean {
|
|
661
|
+
const sourceFile =
|
|
662
|
+
path.join(
|
|
663
|
+
rootDirectory,
|
|
664
|
+
"package-lock.json"
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
if (!fs.existsSync(sourceFile)) {
|
|
668
|
+
warnings.push(
|
|
669
|
+
"package-lock.json was not found; deployment will use npm install --omit=dev."
|
|
670
|
+
);
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const sourceLock =
|
|
675
|
+
readJson<PackageLockJson>(
|
|
676
|
+
sourceFile
|
|
677
|
+
);
|
|
678
|
+
const productionLock =
|
|
679
|
+
createProductionLockfile(
|
|
680
|
+
sourceLock,
|
|
681
|
+
productionPackage
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
if (!productionLock) {
|
|
685
|
+
warnings.push(
|
|
686
|
+
"package-lock.json could not be safely pruned (npm lockfile v3 without workspace links is required); deployment will use npm install --omit=dev."
|
|
687
|
+
);
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
writeJson(
|
|
692
|
+
path.join(
|
|
693
|
+
outputDirectory,
|
|
694
|
+
"package-lock.json"
|
|
695
|
+
),
|
|
696
|
+
productionLock
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function createEnvironmentManifest(
|
|
703
|
+
rootDirectory: string
|
|
704
|
+
): JsonObject {
|
|
705
|
+
const exampleFiles = [
|
|
706
|
+
".env.example",
|
|
707
|
+
".env.production.example",
|
|
708
|
+
".env.production.local.example",
|
|
709
|
+
];
|
|
710
|
+
const declared =
|
|
711
|
+
new Set<string>();
|
|
712
|
+
const discoveredFiles:
|
|
713
|
+
string[] = [];
|
|
714
|
+
|
|
715
|
+
for (
|
|
716
|
+
const fileName
|
|
717
|
+
of exampleFiles
|
|
718
|
+
) {
|
|
719
|
+
const filePath =
|
|
720
|
+
path.join(
|
|
721
|
+
rootDirectory,
|
|
722
|
+
fileName
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
if (!fs.existsSync(filePath)) {
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
discoveredFiles.push(
|
|
730
|
+
fileName
|
|
731
|
+
);
|
|
732
|
+
|
|
733
|
+
const keys =
|
|
734
|
+
parseEnvironmentExampleKeys(
|
|
735
|
+
fs.readFileSync(
|
|
736
|
+
filePath,
|
|
737
|
+
"utf8"
|
|
738
|
+
)
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
for (const key of keys) {
|
|
742
|
+
declared.add(key);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return {
|
|
747
|
+
schemaVersion:
|
|
748
|
+
APPLICATION_PACKAGE_SCHEMA_VERSION,
|
|
749
|
+
valuesIncluded:
|
|
750
|
+
false,
|
|
751
|
+
environmentFilesIncluded:
|
|
752
|
+
false,
|
|
753
|
+
sourceExamples:
|
|
754
|
+
discoveredFiles.sort(),
|
|
755
|
+
declaredKeys:
|
|
756
|
+
Array.from(
|
|
757
|
+
declared
|
|
758
|
+
).sort(),
|
|
759
|
+
runtimeOverrides: [
|
|
760
|
+
"BCP_HOSTNAME",
|
|
761
|
+
"BCP_PORT",
|
|
762
|
+
],
|
|
763
|
+
note:
|
|
764
|
+
"Provide environment values at deployment/runtime. BCP application packages never copy .env files or secret values.",
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function collectPackageFiles(
|
|
769
|
+
directory: string,
|
|
770
|
+
excluded: Set<string>
|
|
771
|
+
): ApplicationPackageFile[] {
|
|
772
|
+
const files:
|
|
773
|
+
ApplicationPackageFile[] = [];
|
|
774
|
+
|
|
775
|
+
walkDirectory(
|
|
776
|
+
directory,
|
|
777
|
+
directory,
|
|
778
|
+
excluded,
|
|
779
|
+
files
|
|
780
|
+
);
|
|
781
|
+
|
|
782
|
+
return files.sort(
|
|
783
|
+
(left, right) =>
|
|
784
|
+
left.path.localeCompare(
|
|
785
|
+
right.path
|
|
786
|
+
)
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function walkDirectory(
|
|
791
|
+
rootDirectory: string,
|
|
792
|
+
currentDirectory: string,
|
|
793
|
+
excluded: Set<string>,
|
|
794
|
+
files: ApplicationPackageFile[]
|
|
795
|
+
): void {
|
|
796
|
+
const entries =
|
|
797
|
+
fs.readdirSync(
|
|
798
|
+
currentDirectory,
|
|
799
|
+
{
|
|
800
|
+
withFileTypes: true,
|
|
801
|
+
}
|
|
802
|
+
).sort(
|
|
803
|
+
(left, right) =>
|
|
804
|
+
left.name.localeCompare(
|
|
805
|
+
right.name
|
|
806
|
+
)
|
|
807
|
+
);
|
|
808
|
+
|
|
809
|
+
for (const entry of entries) {
|
|
810
|
+
const absolutePath =
|
|
811
|
+
path.join(
|
|
812
|
+
currentDirectory,
|
|
813
|
+
entry.name
|
|
814
|
+
);
|
|
815
|
+
const relativePath =
|
|
816
|
+
normalizePath(
|
|
817
|
+
path.relative(
|
|
818
|
+
rootDirectory,
|
|
819
|
+
absolutePath
|
|
820
|
+
)
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
if (
|
|
824
|
+
excluded.has(
|
|
825
|
+
relativePath
|
|
826
|
+
)
|
|
827
|
+
) {
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (entry.isDirectory()) {
|
|
832
|
+
walkDirectory(
|
|
833
|
+
rootDirectory,
|
|
834
|
+
absolutePath,
|
|
835
|
+
excluded,
|
|
836
|
+
files
|
|
837
|
+
);
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (!entry.isFile()) {
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const content =
|
|
846
|
+
fs.readFileSync(
|
|
847
|
+
absolutePath
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
files.push({
|
|
851
|
+
path:
|
|
852
|
+
relativePath,
|
|
853
|
+
bytes:
|
|
854
|
+
content.length,
|
|
855
|
+
sha256:
|
|
856
|
+
createHash("sha256")
|
|
857
|
+
.update(content)
|
|
858
|
+
.digest("hex"),
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function createRootLockPackage(
|
|
864
|
+
source: PackageLockPackage,
|
|
865
|
+
productionPackage: ApplicationPackageJson
|
|
866
|
+
): PackageLockPackage {
|
|
867
|
+
const root:
|
|
868
|
+
PackageLockPackage = {
|
|
869
|
+
...source,
|
|
870
|
+
name:
|
|
871
|
+
normalizeApplicationName(
|
|
872
|
+
productionPackage.name
|
|
873
|
+
),
|
|
874
|
+
version:
|
|
875
|
+
normalizeApplicationVersion(
|
|
876
|
+
productionPackage.version
|
|
877
|
+
),
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
delete root.devDependencies;
|
|
881
|
+
delete root.workspaces;
|
|
882
|
+
|
|
883
|
+
syncLockDependencyMap(
|
|
884
|
+
root,
|
|
885
|
+
productionPackage,
|
|
886
|
+
"dependencies"
|
|
887
|
+
);
|
|
888
|
+
syncLockDependencyMap(
|
|
889
|
+
root,
|
|
890
|
+
productionPackage,
|
|
891
|
+
"optionalDependencies"
|
|
892
|
+
);
|
|
893
|
+
syncLockDependencyMap(
|
|
894
|
+
root,
|
|
895
|
+
productionPackage,
|
|
896
|
+
"peerDependencies"
|
|
897
|
+
);
|
|
898
|
+
|
|
899
|
+
return root;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function syncLockDependencyMap(
|
|
903
|
+
target: PackageLockPackage,
|
|
904
|
+
source: ApplicationPackageJson,
|
|
905
|
+
key:
|
|
906
|
+
"dependencies" |
|
|
907
|
+
"optionalDependencies" |
|
|
908
|
+
"peerDependencies"
|
|
909
|
+
): void {
|
|
910
|
+
const value =
|
|
911
|
+
source[key];
|
|
912
|
+
|
|
913
|
+
if (
|
|
914
|
+
value &&
|
|
915
|
+
Object.keys(value).length > 0
|
|
916
|
+
) {
|
|
917
|
+
target[key] =
|
|
918
|
+
value;
|
|
919
|
+
} else {
|
|
920
|
+
delete target[key];
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function collectDependencyNames(
|
|
925
|
+
entry: PackageLockPackage
|
|
926
|
+
): string[] {
|
|
927
|
+
return Array.from(
|
|
928
|
+
new Set([
|
|
929
|
+
...Object.keys(
|
|
930
|
+
entry.dependencies ?? {}
|
|
931
|
+
),
|
|
932
|
+
...Object.keys(
|
|
933
|
+
entry.optionalDependencies ?? {}
|
|
934
|
+
),
|
|
935
|
+
...Object.keys(
|
|
936
|
+
entry.peerDependencies ?? {}
|
|
937
|
+
),
|
|
938
|
+
])
|
|
939
|
+
).sort();
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function resolveLockPackagePath(
|
|
943
|
+
packages:
|
|
944
|
+
Record<
|
|
945
|
+
string,
|
|
946
|
+
PackageLockPackage
|
|
947
|
+
>,
|
|
948
|
+
requesterPath: string,
|
|
949
|
+
dependencyName: string
|
|
950
|
+
): string | null {
|
|
951
|
+
const requesterSegments =
|
|
952
|
+
requesterPath === ""
|
|
953
|
+
? []
|
|
954
|
+
: normalizePath(
|
|
955
|
+
requesterPath
|
|
956
|
+
).split("/");
|
|
957
|
+
const candidates:
|
|
958
|
+
string[] = [];
|
|
959
|
+
|
|
960
|
+
for (
|
|
961
|
+
let length =
|
|
962
|
+
requesterSegments.length;
|
|
963
|
+
length >= 0;
|
|
964
|
+
length--
|
|
965
|
+
) {
|
|
966
|
+
const prefix =
|
|
967
|
+
requesterSegments
|
|
968
|
+
.slice(
|
|
969
|
+
0,
|
|
970
|
+
length
|
|
971
|
+
)
|
|
972
|
+
.join("/");
|
|
973
|
+
const candidate =
|
|
974
|
+
prefix === ""
|
|
975
|
+
? `node_modules/${dependencyName}`
|
|
976
|
+
: `${prefix}/node_modules/${dependencyName}`;
|
|
977
|
+
|
|
978
|
+
candidates.push(
|
|
979
|
+
candidate
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
for (const candidate of candidates) {
|
|
984
|
+
if (
|
|
985
|
+
packages[
|
|
986
|
+
candidate
|
|
987
|
+
]
|
|
988
|
+
) {
|
|
989
|
+
return candidate;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function copyBuildDirectory(
|
|
997
|
+
buildDirectory: string,
|
|
998
|
+
outputDirectory: string,
|
|
999
|
+
name: string,
|
|
1000
|
+
required: boolean
|
|
1001
|
+
): void {
|
|
1002
|
+
const source =
|
|
1003
|
+
path.join(
|
|
1004
|
+
buildDirectory,
|
|
1005
|
+
name
|
|
1006
|
+
);
|
|
1007
|
+
|
|
1008
|
+
if (!fs.existsSync(source)) {
|
|
1009
|
+
if (required) {
|
|
1010
|
+
throw new Error(
|
|
1011
|
+
`BCP Framework: production build directory "${name}" was not found.`
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
fs.cpSync(
|
|
1018
|
+
source,
|
|
1019
|
+
path.join(
|
|
1020
|
+
outputDirectory,
|
|
1021
|
+
name
|
|
1022
|
+
),
|
|
1023
|
+
{
|
|
1024
|
+
recursive: true,
|
|
1025
|
+
}
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function copySortedStringMap(
|
|
1030
|
+
source: ApplicationPackageJson,
|
|
1031
|
+
target: ApplicationPackageJson,
|
|
1032
|
+
key:
|
|
1033
|
+
"dependencies" |
|
|
1034
|
+
"optionalDependencies" |
|
|
1035
|
+
"peerDependencies"
|
|
1036
|
+
): void {
|
|
1037
|
+
const value =
|
|
1038
|
+
source[key];
|
|
1039
|
+
|
|
1040
|
+
if (
|
|
1041
|
+
value &&
|
|
1042
|
+
Object.keys(value).length > 0
|
|
1043
|
+
) {
|
|
1044
|
+
target[key] =
|
|
1045
|
+
sortStringMap(
|
|
1046
|
+
value
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function sortStringMap(
|
|
1052
|
+
value: Record<string, string>
|
|
1053
|
+
): Record<string, string> {
|
|
1054
|
+
return Object.fromEntries(
|
|
1055
|
+
Object.entries(value)
|
|
1056
|
+
.sort(
|
|
1057
|
+
([left], [right]) =>
|
|
1058
|
+
left.localeCompare(
|
|
1059
|
+
right
|
|
1060
|
+
)
|
|
1061
|
+
)
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function sortObject(
|
|
1066
|
+
value: JsonObject
|
|
1067
|
+
): JsonObject {
|
|
1068
|
+
return Object.fromEntries(
|
|
1069
|
+
Object.entries(value)
|
|
1070
|
+
.sort(
|
|
1071
|
+
([left], [right]) =>
|
|
1072
|
+
left.localeCompare(
|
|
1073
|
+
right
|
|
1074
|
+
)
|
|
1075
|
+
)
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function createDockerfile(): string {
|
|
1080
|
+
return `FROM node:24-alpine\n\nWORKDIR /app\nENV NODE_ENV=production\n\nCOPY package*.json ./\nRUN if [ -f package-lock.json ]; then npm ci --omit=dev; else npm install --omit=dev; fi\n\nCOPY . .\n\nEXPOSE 3000\nCMD ["node", "server/server.mjs"]\n`;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function createDockerIgnore(): string {
|
|
1084
|
+
return `node_modules\nnpm-debug.log*\n.env\n.env.*\n!.env.example\n.git\n.gitignore\n`;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function createPackageReadme(
|
|
1088
|
+
installCommand: string
|
|
1089
|
+
): string {
|
|
1090
|
+
return `# BCP Application Package\n\nTarget: \`${APPLICATION_PACKAGE_TARGET}\`\n\nInstall production dependencies:\n\n\`\`\`bash\n${installCommand}\n\`\`\`\n\nStart the application:\n\n\`\`\`bash\nnpm start\n\`\`\`\n\nRuntime configuration and secrets must be supplied by the deployment environment. This package intentionally excludes project \`.env\` files and devDependencies.\n\nFor containers:\n\n\`\`\`bash\ndocker build -t bcp-app .\ndocker run --rm -p 3000:3000 bcp-app\n\`\`\`\n`;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function normalizeApplicationName(
|
|
1094
|
+
value: unknown
|
|
1095
|
+
): string {
|
|
1096
|
+
if (
|
|
1097
|
+
typeof value === "string" &&
|
|
1098
|
+
value.trim() !== ""
|
|
1099
|
+
) {
|
|
1100
|
+
return value.trim();
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
return "bcp-application";
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function normalizeApplicationVersion(
|
|
1107
|
+
value: unknown
|
|
1108
|
+
): string {
|
|
1109
|
+
if (
|
|
1110
|
+
typeof value === "string" &&
|
|
1111
|
+
value.trim() !== ""
|
|
1112
|
+
) {
|
|
1113
|
+
return value.trim();
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
return "0.0.0";
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function normalizePath(
|
|
1120
|
+
value: string
|
|
1121
|
+
): string {
|
|
1122
|
+
return value.replace(
|
|
1123
|
+
/\\/g,
|
|
1124
|
+
"/"
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function readJson<T>(
|
|
1129
|
+
filePath: string
|
|
1130
|
+
): T {
|
|
1131
|
+
return JSON.parse(
|
|
1132
|
+
fs.readFileSync(
|
|
1133
|
+
filePath,
|
|
1134
|
+
"utf8"
|
|
1135
|
+
)
|
|
1136
|
+
) as T;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function writeJson(
|
|
1140
|
+
filePath: string,
|
|
1141
|
+
value: unknown
|
|
1142
|
+
): void {
|
|
1143
|
+
fs.writeFileSync(
|
|
1144
|
+
filePath,
|
|
1145
|
+
`${JSON.stringify(
|
|
1146
|
+
value,
|
|
1147
|
+
null,
|
|
1148
|
+
2
|
|
1149
|
+
)}\n`,
|
|
1150
|
+
"utf8"
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function isPlainObject(
|
|
1155
|
+
value: unknown
|
|
1156
|
+
): value is JsonObject {
|
|
1157
|
+
return Boolean(
|
|
1158
|
+
value &&
|
|
1159
|
+
typeof value === "object" &&
|
|
1160
|
+
!Array.isArray(value)
|
|
1161
|
+
);
|
|
1162
|
+
}
|