@cedarjs/cli 8.0.0-canary.3333 → 8.0.0-canary.3337

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.
@@ -14,6 +14,9 @@ const CONFIG_FILENAME = "deploy.toml";
14
14
  const SYMLINK_FLAGS = "-nsf";
15
15
  const CURRENT_RELEASE_SYMLINK_NAME = "current";
16
16
  const LIFECYCLE_HOOKS = ["before", "after"];
17
+ const WEB_DIST_DIRS = ["web/dist", "web/dist/browser"];
18
+ const BUILD_MANIFEST_FILENAME = "client-build-manifest.json";
19
+ const LINK_BATCH_SIZE = 100;
17
20
  const RELEASE_DIR_PATTERN = "^[0-9]{14}$";
18
21
  const releaseDirRegExp = new RegExp(RELEASE_DIR_PATTERN);
19
22
  const DEFAULT_SERVER_CONFIG = {
@@ -210,6 +213,135 @@ const commandWithLifecycleEvents = ({
210
213
  tasks.push(lifecycleTask("after", name, skip, config));
211
214
  return tasks.flat().filter((t) => Boolean(t));
212
215
  };
216
+ const manifestAssetFiles = (manifestJson) => {
217
+ const manifest = JSON.parse(manifestJson);
218
+ const files = /* @__PURE__ */ new Set();
219
+ if (!manifest || typeof manifest !== "object") {
220
+ return [];
221
+ }
222
+ for (const entry of Object.values(manifest)) {
223
+ if (!entry || typeof entry !== "object") {
224
+ continue;
225
+ }
226
+ if ("file" in entry && typeof entry.file === "string") {
227
+ files.add(entry.file);
228
+ }
229
+ for (const key of ["css", "assets"]) {
230
+ if (!(key in entry) || !Array.isArray(entry[key])) {
231
+ continue;
232
+ }
233
+ for (const item of entry[key]) {
234
+ if (typeof item === "string") {
235
+ files.add(item);
236
+ }
237
+ }
238
+ }
239
+ }
240
+ return [...files];
241
+ };
242
+ const shellQuote = (value) => `'${value.replaceAll("'", `'\\''`)}'`;
243
+ const readBuildManifest = async (ssh, releasePath) => {
244
+ for (const distDir of WEB_DIST_DIRS) {
245
+ let stdout;
246
+ try {
247
+ ;
248
+ ({ stdout } = await ssh.exec(pathJoin(releasePath, distDir), "cat", [
249
+ BUILD_MANIFEST_FILENAME
250
+ ]));
251
+ } catch {
252
+ continue;
253
+ }
254
+ const jsonStart = stdout.indexOf("{");
255
+ const json = jsonStart === -1 ? stdout : stdout.slice(jsonStart);
256
+ try {
257
+ return { distDir, files: manifestAssetFiles(json) };
258
+ } catch (e) {
259
+ const message = e instanceof Error ? e.message : String(e);
260
+ throw new Error(
261
+ `Could not parse ${pathJoin(releasePath, distDir, BUILD_MANIFEST_FILENAME)}: ${message}`
262
+ );
263
+ }
264
+ }
265
+ return void 0;
266
+ };
267
+ const keepPreviousAssets = async (yargs, ssh, serverConfig) => {
268
+ const newReleasePath = pathJoin(serverConfig.path, yargs.releaseDir);
269
+ let newRelease;
270
+ try {
271
+ newRelease = await readBuildManifest(ssh, newReleasePath);
272
+ } catch (e) {
273
+ const reason = e instanceof Error ? e.message : String(e);
274
+ return { status: "skipped", reason };
275
+ }
276
+ if (!newRelease) {
277
+ return {
278
+ status: "skipped",
279
+ reason: `No ${BUILD_MANIFEST_FILENAME} in ` + WEB_DIST_DIRS.map((dir) => pathJoin(yargs.releaseDir, dir)).join(
280
+ " or "
281
+ )
282
+ };
283
+ }
284
+ const { stdout } = await ssh.exec(serverConfig.path, "ls");
285
+ const previousReleases = stdout.split("\n").map((line) => line.trim()).filter((dir) => releaseDirRegExp.test(dir) && dir !== yargs.releaseDir).sort().reverse().slice(0, Math.max(serverConfig.keepReleases - 1, 0));
286
+ const presentFiles = new Set(newRelease.files);
287
+ const warnings = [];
288
+ let linkedCount = 0;
289
+ for (const release of previousReleases) {
290
+ const releasePath = pathJoin(serverConfig.path, release);
291
+ let previousRelease;
292
+ try {
293
+ previousRelease = await readBuildManifest(ssh, releasePath);
294
+ } catch (e) {
295
+ const message = e instanceof Error ? e.message : String(e);
296
+ warnings.push(`Skipping the web assets of release ${release}: ${message}`);
297
+ continue;
298
+ }
299
+ if (!previousRelease) {
300
+ warnings.push(
301
+ `Release ${release} has no web build manifest, skipping its assets`
302
+ );
303
+ continue;
304
+ }
305
+ const missingFiles = previousRelease.files.filter(
306
+ (file) => !presentFiles.has(file)
307
+ );
308
+ if (missingFiles.length === 0) {
309
+ continue;
310
+ }
311
+ const filesByDir = /* @__PURE__ */ new Map();
312
+ for (const file of missingFiles) {
313
+ const dir = path.posix.dirname(file);
314
+ filesByDir.set(dir, [...filesByDir.get(dir) ?? [], file]);
315
+ }
316
+ try {
317
+ for (const [dir, files] of filesByDir) {
318
+ const targetDir = pathJoin(newReleasePath, newRelease.distDir, dir);
319
+ await ssh.exec(newReleasePath, "mkdir", ["-p", shellQuote(targetDir)]);
320
+ for (let i = 0; i < files.length; i += LINK_BATCH_SIZE) {
321
+ const sources = files.slice(i, i + LINK_BATCH_SIZE).map(
322
+ (file) => shellQuote(pathJoin(releasePath, previousRelease.distDir, file))
323
+ );
324
+ await ssh.exec(newReleasePath, "ln", [
325
+ "-f",
326
+ ...sources,
327
+ shellQuote(targetDir)
328
+ ]);
329
+ }
330
+ }
331
+ } catch (e) {
332
+ const message = e instanceof Error ? e.message : String(e);
333
+ warnings.push(
334
+ `Could not keep the web assets of release ${release}: ${message}`
335
+ );
336
+ continue;
337
+ }
338
+ for (const file of missingFiles) {
339
+ presentFiles.add(file);
340
+ }
341
+ linkedCount += missingFiles.length;
342
+ }
343
+ return { status: "done", linkedCount, warnings };
344
+ };
213
345
  const deployTasks = (yargs, ssh, serverConfig, serverLifecycle) => {
214
346
  const cmdPath = pathJoin(serverConfig.path, yargs.releaseDir);
215
347
  const config = {
@@ -358,6 +490,30 @@ const deployTasks = (yargs, ssh, serverConfig, serverLifecycle) => {
358
490
  })
359
491
  );
360
492
  }
493
+ tasks.push(
494
+ ...commandWithLifecycleEvents({
495
+ name: "keepAssets",
496
+ config,
497
+ skip: !yargs.keepAssets || !yargs.build || !serverConfig.sides.includes("web"),
498
+ command: {
499
+ title: `Keeping web assets of previous releases...`,
500
+ task: async (_ctx, task) => {
501
+ const result = await keepPreviousAssets(yargs, ssh, serverConfig);
502
+ if (result.status === "skipped") {
503
+ return task.skip(
504
+ c.warning(
505
+ `Warning: ${result.reason}, not keeping the web assets of previous releases`
506
+ )
507
+ );
508
+ }
509
+ task.output = [
510
+ `Linked ${result.linkedCount} file(s) from previous releases`,
511
+ ...result.warnings.map((warning) => c.warning(warning))
512
+ ].join("\n");
513
+ }
514
+ }
515
+ })
516
+ );
361
517
  tasks.push(
362
518
  ...commandWithLifecycleEvents({
363
519
  name: "symlinkCurrent",
@@ -599,8 +755,10 @@ export {
599
755
  commands,
600
756
  deployTasks,
601
757
  handler,
758
+ keepPreviousAssets,
602
759
  lifecycleTask,
603
760
  maintenanceTasks,
761
+ manifestAssetFiles,
604
762
  parseConfig,
605
763
  rollbackTasks,
606
764
  serverConfigWithDefaults,
@@ -48,6 +48,11 @@ const builder = (yargs) => {
48
48
  default: true,
49
49
  type: "boolean"
50
50
  });
51
+ yargs.option("keep-assets", {
52
+ describe: "Hardlink previous releases' hashed web assets into the new release so open browser tabs keep working after the deploy",
53
+ default: true,
54
+ type: "boolean"
55
+ });
51
56
  yargs.option("releaseDir", {
52
57
  describe: "Directory to create for the latest release, defaults to timestamp",
53
58
  default: (/* @__PURE__ */ new Date()).toISOString().replace(/[:\-TZ]/g, "").replace(/\.\d+$/, ""),
@@ -95,6 +100,7 @@ async function handler(yargs) {
95
100
  build: yargs.build,
96
101
  restart: yargs.restart,
97
102
  cleanup: yargs.cleanup,
103
+ keepAssets: yargs.keepAssets,
98
104
  maintenance: yargs.maintenance,
99
105
  rollback: yargs.rollback,
100
106
  verbose: yargs.verbose,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cedarjs/cli",
3
- "version": "8.0.0-canary.3333",
3
+ "version": "8.0.0-canary.3337",
4
4
  "description": "The CedarJS Command Line",
5
5
  "repository": {
6
6
  "type": "git",
@@ -36,17 +36,17 @@
36
36
  "@babel/preset-typescript": "7.29.7",
37
37
  "@babel/traverse": "7.29.8",
38
38
  "@babel/types": "7.29.8",
39
- "@cedarjs/api-server": "8.0.0-canary.3333",
40
- "@cedarjs/babel-config": "8.0.0-canary.3333",
41
- "@cedarjs/cli-helpers": "8.0.0-canary.3333",
42
- "@cedarjs/internal": "8.0.0-canary.3333",
43
- "@cedarjs/prerender": "8.0.0-canary.3333",
44
- "@cedarjs/project-config": "8.0.0-canary.3333",
45
- "@cedarjs/structure": "8.0.0-canary.3333",
46
- "@cedarjs/telemetry": "8.0.0-canary.3333",
47
- "@cedarjs/utils": "8.0.0-canary.3333",
48
- "@cedarjs/vite": "8.0.0-canary.3333",
49
- "@cedarjs/web-server": "8.0.0-canary.3333",
39
+ "@cedarjs/api-server": "8.0.0-canary.3337",
40
+ "@cedarjs/babel-config": "8.0.0-canary.3337",
41
+ "@cedarjs/cli-helpers": "8.0.0-canary.3337",
42
+ "@cedarjs/internal": "8.0.0-canary.3337",
43
+ "@cedarjs/prerender": "8.0.0-canary.3337",
44
+ "@cedarjs/project-config": "8.0.0-canary.3337",
45
+ "@cedarjs/structure": "8.0.0-canary.3337",
46
+ "@cedarjs/telemetry": "8.0.0-canary.3337",
47
+ "@cedarjs/utils": "8.0.0-canary.3337",
48
+ "@cedarjs/vite": "8.0.0-canary.3337",
49
+ "@cedarjs/web-server": "8.0.0-canary.3337",
50
50
  "@listr2/prompt-adapter-enquirer": "4.3.3",
51
51
  "@opentelemetry/api": "1.9.1",
52
52
  "@opentelemetry/core": "2.11.0",
@@ -94,7 +94,7 @@
94
94
  "yargs": "17.7.3"
95
95
  },
96
96
  "devDependencies": {
97
- "@cedarjs/framework-tools": "8.0.0-canary.3333",
97
+ "@cedarjs/framework-tools": "8.0.0-canary.3337",
98
98
  "@prisma/dmmf": "7.10.0",
99
99
  "@types/archiver": "^7.0.0",
100
100
  "memfs": "4.80.0",