@mondaydotcomorg/z2h-cli 0.19.0-feat-arielle-fixed-preview-id-d22bd88 → 0.19.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/commands/deploy.d.ts +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +2 -17
- package/dist/esm/commands/deploy.d.ts +1 -1
- package/dist/esm/commands/deploy.d.ts.map +1 -1
- package/dist/esm/commands/deploy.mjs +2 -17
- package/dist/esm/index.mjs +6 -3
- package/dist/esm/shadow/prepare.d.ts +0 -1
- package/dist/esm/shadow/prepare.d.ts.map +1 -1
- package/dist/esm/shadow/prepare.mjs +5 -13
- package/dist/esm/types.d.ts +0 -1
- package/dist/esm/types.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/shadow/prepare.d.ts +0 -1
- package/dist/shadow/prepare.d.ts.map +1 -1
- package/dist/shadow/prepare.js +4 -13
- package/dist/types.d.ts +0 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/commands/__tests__/deploy.test.ts +27 -48
- package/src/commands/deploy.ts +4 -22
- package/src/index.ts +9 -9
- package/src/shadow/prepare.ts +4 -16
- package/src/types.ts +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAE,KAAK,UAAU,EAA2C,MAAM,4BAA4B,CAAC;AAUtG,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IAGjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAWD,wBAAsB,aAAa,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3E"}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
2
|
|
|
3
3
|
const os = require('node:os');
|
|
4
|
+
const crypto = require('node:crypto');
|
|
4
5
|
const fs = require('fs-extra');
|
|
5
6
|
const util_logger = require('../util/logger.js');
|
|
6
7
|
const shadow_paths = require('../shadow/paths.js');
|
|
@@ -17,8 +18,6 @@ const util_s3_client = require('../util/s3/client.js');
|
|
|
17
18
|
const util_machineIdentity = require('../util/machine-identity.js');
|
|
18
19
|
const util_broker_app = require('../util/broker/app.js');
|
|
19
20
|
const util_openBrowser = require('../util/open-browser.js');
|
|
20
|
-
const shadow_metadata = require('../shadow/metadata.js');
|
|
21
|
-
const shadow_prepare = require('../shadow/prepare.js');
|
|
22
21
|
const commands_build = require('./build.js');
|
|
23
22
|
const commands_tag = require('./tag.js');
|
|
24
23
|
|
|
@@ -64,13 +63,7 @@ async function buildAndStamp(opts) {
|
|
|
64
63
|
};
|
|
65
64
|
}
|
|
66
65
|
async function deployPreview({ paths, consumerName, appName, token, opts }) {
|
|
67
|
-
const
|
|
68
|
-
const previewId = opts.snapshot ? shadow_prepare.generatePreviewId() : (meta?.previewId ?? shadow_prepare.generatePreviewId());
|
|
69
|
-
// Pre-stamp so prepareShadow (called inside buildAndStamp) preserves our previewId.
|
|
70
|
-
// Only possible when metadata already exists — otherwise prepareShadow creates it fresh.
|
|
71
|
-
if (!opts.snapshot && meta && meta.previewId !== previewId) {
|
|
72
|
-
await shadow_metadata.writeMetadata(paths, { ...meta, previewId });
|
|
73
|
-
}
|
|
66
|
+
const previewId = opts.previewId ?? crypto.randomUUID().slice(0, 8);
|
|
74
67
|
const previewSlug = util_appName.buildPreviewSlug(consumerName, previewId);
|
|
75
68
|
const previewAppName = util_appName.mfAppName(previewSlug);
|
|
76
69
|
const keyPrefix = `${constants.S3_PREVIEWS_FOLDER}/${previewAppName}/MF`;
|
|
@@ -80,14 +73,6 @@ async function deployPreview({ paths, consumerName, appName, token, opts }) {
|
|
|
80
73
|
const nextVersion = (await util_auth_brokerCredentialProvider.fetchDeployVersion(previewAppName)) + 1;
|
|
81
74
|
util_logger.info('building');
|
|
82
75
|
const entry = await buildAndStamp({ paths, publicUrl: url, nextVersion, gitRemote: '' });
|
|
83
|
-
// Post-stamp: if metadata didn't exist before build, prepareShadow created it with
|
|
84
|
-
// its own previewId. Overwrite with ours so subsequent runs reuse the same URL.
|
|
85
|
-
if (!opts.snapshot) {
|
|
86
|
-
const freshMeta = await shadow_metadata.readMetadata(paths);
|
|
87
|
-
if (freshMeta && freshMeta.previewId !== previewId) {
|
|
88
|
-
await shadow_metadata.writeMetadata(paths, { ...freshMeta, previewId });
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
76
|
const creds = await util_auth_brokerCredentialProvider.brokerCredentialProvider({ appName: previewAppName, previewId })();
|
|
92
77
|
util_s3_client.setupS3Client(creds);
|
|
93
78
|
util_logger.info(`uploading preview build → s3://${constants.BUCKET_WRITE_NAME}/${keyPrefix}/`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../../src/commands/deploy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../../src/commands/deploy.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAE,KAAK,UAAU,EAA2C,MAAM,4BAA4B,CAAC;AAUtG,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IAGjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAWD,wBAAsB,aAAa,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3E"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { readJson } from 'fs-extra';
|
|
3
4
|
import { info, output, getOutputMode, warn } from '../util/logger.mjs';
|
|
4
5
|
import { resolveConsumerPaths } from '../shadow/paths.mjs';
|
|
@@ -15,8 +16,6 @@ import { setupS3Client } from '../util/s3/client.mjs';
|
|
|
15
16
|
import { getMachineAppName } from '../util/machine-identity.mjs';
|
|
16
17
|
import { registerOrUpdateApp } from '../util/broker/app.mjs';
|
|
17
18
|
import { openInBrowser } from '../util/open-browser.mjs';
|
|
18
|
-
import { readMetadata, writeMetadata } from '../shadow/metadata.mjs';
|
|
19
|
-
import { generatePreviewId } from '../shadow/prepare.mjs';
|
|
20
19
|
import { buildCommand } from './build.mjs';
|
|
21
20
|
import { promptForTags } from './tag.mjs';
|
|
22
21
|
|
|
@@ -58,13 +57,7 @@ async function buildAndStamp(opts) {
|
|
|
58
57
|
};
|
|
59
58
|
}
|
|
60
59
|
async function deployPreview({ paths, consumerName, appName, token, opts }) {
|
|
61
|
-
const
|
|
62
|
-
const previewId = opts.snapshot ? generatePreviewId() : (meta?.previewId ?? generatePreviewId());
|
|
63
|
-
// Pre-stamp so prepareShadow (called inside buildAndStamp) preserves our previewId.
|
|
64
|
-
// Only possible when metadata already exists — otherwise prepareShadow creates it fresh.
|
|
65
|
-
if (!opts.snapshot && meta && meta.previewId !== previewId) {
|
|
66
|
-
await writeMetadata(paths, { ...meta, previewId });
|
|
67
|
-
}
|
|
60
|
+
const previewId = opts.previewId ?? randomUUID().slice(0, 8);
|
|
68
61
|
const previewSlug = buildPreviewSlug(consumerName, previewId);
|
|
69
62
|
const previewAppName = mfAppName(previewSlug);
|
|
70
63
|
const keyPrefix = `${S3_PREVIEWS_FOLDER}/${previewAppName}/MF`;
|
|
@@ -74,14 +67,6 @@ async function deployPreview({ paths, consumerName, appName, token, opts }) {
|
|
|
74
67
|
const nextVersion = (await fetchDeployVersion(previewAppName)) + 1;
|
|
75
68
|
info('building');
|
|
76
69
|
const entry = await buildAndStamp({ paths, publicUrl: url, nextVersion, gitRemote: '' });
|
|
77
|
-
// Post-stamp: if metadata didn't exist before build, prepareShadow created it with
|
|
78
|
-
// its own previewId. Overwrite with ours so subsequent runs reuse the same URL.
|
|
79
|
-
if (!opts.snapshot) {
|
|
80
|
-
const freshMeta = await readMetadata(paths);
|
|
81
|
-
if (freshMeta && freshMeta.previewId !== previewId) {
|
|
82
|
-
await writeMetadata(paths, { ...freshMeta, previewId });
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
70
|
const creds = await brokerCredentialProvider({ appName: previewAppName, previewId })();
|
|
86
71
|
setupS3Client(creds);
|
|
87
72
|
info(`uploading preview build → s3://${BUCKET_WRITE_NAME}/${keyPrefix}/`);
|
package/dist/esm/index.mjs
CHANGED
|
@@ -93,14 +93,17 @@ async function main() {
|
|
|
93
93
|
.command('deploy')
|
|
94
94
|
.description('Build, upload to S3, and register the app with the Z2H backend')
|
|
95
95
|
.option('--dry-run', 'print what would happen without uploading anything')
|
|
96
|
-
.option('--preview', 'build an ephemeral, shareable preview instead of deploying — no git required, no deploy lock, never touches the live version;
|
|
97
|
-
.option('--snapshot', 'with --preview: deploy to a fresh previewId without touching the sticky URL — produces a permanent point-in-time link')
|
|
96
|
+
.option('--preview [previewId]', 'build an ephemeral, shareable preview instead of deploying — no git required, no deploy lock, never touches the live version; pass an existing previewId to update it instead of minting a new one')
|
|
98
97
|
.option('--open', 'open the resulting app/preview URL in the browser on success')
|
|
99
98
|
.option('--remote <type>', 'First deploy only: source control remote to initialise — s3 (shared storage) or github (your repo). Error if the app already has a git remote.')
|
|
100
99
|
.option('--repo-url <url>', 'First deploy only: GitHub repo URL — required when --remote=github. Error if the app already has a git remote.')
|
|
101
100
|
.action(async (opts) => {
|
|
102
101
|
try {
|
|
103
|
-
await trackDuration(opts.preview ? 'z2h_app_preview_success' : 'z2h_app_deploy_success', () => deployCommand(
|
|
102
|
+
await trackDuration(opts.preview ? 'z2h_app_preview_success' : 'z2h_app_deploy_success', () => deployCommand({
|
|
103
|
+
...opts,
|
|
104
|
+
preview: Boolean(opts.preview),
|
|
105
|
+
previewId: typeof opts.preview === 'string' ? opts.preview : undefined,
|
|
106
|
+
}), {
|
|
104
107
|
app_name: getCwdAppName(),
|
|
105
108
|
});
|
|
106
109
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prepare.d.ts","sourceRoot":"","sources":["../../../src/shadow/prepare.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"prepare.d.ts","sourceRoot":"","sources":["../../../src/shadow/prepare.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAA8B,cAAc,EAAkB,MAAM,UAAU,CAAC;AAE3F,wBAAsB,aAAa,CAAC,WAAW,GAAE,MAAsB,GAAG,OAAO,CAAC,cAAc,CAAC,CAgChG"}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
1
|
import { pathExists, readJson, ensureDir } from 'fs-extra';
|
|
3
2
|
import { info } from '../util/logger.mjs';
|
|
4
3
|
import { cleanAppName } from '../util/app-name.mjs';
|
|
@@ -36,9 +35,6 @@ async function prepareShadow(consumerDir = process.cwd()) {
|
|
|
36
35
|
await syncShadow(paths, consumerPkg);
|
|
37
36
|
return { appName, port, paths };
|
|
38
37
|
}
|
|
39
|
-
function generatePreviewId() {
|
|
40
|
-
return randomUUID().slice(0, 8);
|
|
41
|
-
}
|
|
42
38
|
/**
|
|
43
39
|
* Resolve the dev server port. Prefer the persisted port if it's still free
|
|
44
40
|
* (URL stays stable across restarts). Otherwise pick a fresh one. The
|
|
@@ -47,20 +43,16 @@ function generatePreviewId() {
|
|
|
47
43
|
*/
|
|
48
44
|
async function resolvePortAndMetadata(paths, appName) {
|
|
49
45
|
const existing = await readMetadata(paths);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
port = await pickPort();
|
|
46
|
+
if (existing?.port && (await isPortFree(existing.port))) {
|
|
47
|
+
return existing;
|
|
53
48
|
}
|
|
54
49
|
const next = {
|
|
55
50
|
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
56
51
|
appName,
|
|
57
|
-
port,
|
|
58
|
-
previewId: existing?.previewId ?? generatePreviewId(),
|
|
52
|
+
port: await pickPort(),
|
|
59
53
|
};
|
|
60
|
-
|
|
61
|
-
await writeMetadata(paths, next);
|
|
62
|
-
}
|
|
54
|
+
await writeMetadata(paths, next);
|
|
63
55
|
return next;
|
|
64
56
|
}
|
|
65
57
|
|
|
66
|
-
export {
|
|
58
|
+
export { prepareShadow };
|
package/dist/esm/types.d.ts
CHANGED
package/dist/esm/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAChD,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAOD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,IAAI,CAAC;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC;AAGD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAChD,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAOD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,IAAI,CAAC;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC;AAGD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAGD,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;CACxB;AAGD,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,aAAa,CAAC;CACtB"}
|
package/dist/index.js
CHANGED
|
@@ -99,14 +99,17 @@ async function main() {
|
|
|
99
99
|
.command('deploy')
|
|
100
100
|
.description('Build, upload to S3, and register the app with the Z2H backend')
|
|
101
101
|
.option('--dry-run', 'print what would happen without uploading anything')
|
|
102
|
-
.option('--preview', 'build an ephemeral, shareable preview instead of deploying — no git required, no deploy lock, never touches the live version;
|
|
103
|
-
.option('--snapshot', 'with --preview: deploy to a fresh previewId without touching the sticky URL — produces a permanent point-in-time link')
|
|
102
|
+
.option('--preview [previewId]', 'build an ephemeral, shareable preview instead of deploying — no git required, no deploy lock, never touches the live version; pass an existing previewId to update it instead of minting a new one')
|
|
104
103
|
.option('--open', 'open the resulting app/preview URL in the browser on success')
|
|
105
104
|
.option('--remote <type>', 'First deploy only: source control remote to initialise — s3 (shared storage) or github (your repo). Error if the app already has a git remote.')
|
|
106
105
|
.option('--repo-url <url>', 'First deploy only: GitHub repo URL — required when --remote=github. Error if the app already has a git remote.')
|
|
107
106
|
.action(async (opts) => {
|
|
108
107
|
try {
|
|
109
|
-
await util_tracker.trackDuration(opts.preview ? 'z2h_app_preview_success' : 'z2h_app_deploy_success', () => commands_deploy.deployCommand(
|
|
108
|
+
await util_tracker.trackDuration(opts.preview ? 'z2h_app_preview_success' : 'z2h_app_deploy_success', () => commands_deploy.deployCommand({
|
|
109
|
+
...opts,
|
|
110
|
+
preview: Boolean(opts.preview),
|
|
111
|
+
previewId: typeof opts.preview === 'string' ? opts.preview : undefined,
|
|
112
|
+
}), {
|
|
110
113
|
app_name: util_appName.getCwdAppName(),
|
|
111
114
|
});
|
|
112
115
|
}
|
package/dist/shadow/prepare.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prepare.d.ts","sourceRoot":"","sources":["../../src/shadow/prepare.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"prepare.d.ts","sourceRoot":"","sources":["../../src/shadow/prepare.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAA8B,cAAc,EAAkB,MAAM,UAAU,CAAC;AAE3F,wBAAsB,aAAa,CAAC,WAAW,GAAE,MAAsB,GAAG,OAAO,CAAC,cAAc,CAAC,CAgChG"}
|
package/dist/shadow/prepare.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
2
|
|
|
3
|
-
const crypto = require('node:crypto');
|
|
4
3
|
const fs = require('fs-extra');
|
|
5
4
|
const util_logger = require('../util/logger.js');
|
|
6
5
|
const util_appName = require('../util/app-name.js');
|
|
@@ -38,9 +37,6 @@ async function prepareShadow(consumerDir = process.cwd()) {
|
|
|
38
37
|
await shadow_sync.syncShadow(paths, consumerPkg);
|
|
39
38
|
return { appName, port, paths };
|
|
40
39
|
}
|
|
41
|
-
function generatePreviewId() {
|
|
42
|
-
return crypto.randomUUID().slice(0, 8);
|
|
43
|
-
}
|
|
44
40
|
/**
|
|
45
41
|
* Resolve the dev server port. Prefer the persisted port if it's still free
|
|
46
42
|
* (URL stays stable across restarts). Otherwise pick a fresh one. The
|
|
@@ -49,21 +45,16 @@ function generatePreviewId() {
|
|
|
49
45
|
*/
|
|
50
46
|
async function resolvePortAndMetadata(paths, appName) {
|
|
51
47
|
const existing = await shadow_metadata.readMetadata(paths);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
port = await shadow_port.pickPort();
|
|
48
|
+
if (existing?.port && (await shadow_port.isPortFree(existing.port))) {
|
|
49
|
+
return existing;
|
|
55
50
|
}
|
|
56
51
|
const next = {
|
|
57
52
|
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
58
53
|
appName,
|
|
59
|
-
port,
|
|
60
|
-
previewId: existing?.previewId ?? generatePreviewId(),
|
|
54
|
+
port: await shadow_port.pickPort(),
|
|
61
55
|
};
|
|
62
|
-
|
|
63
|
-
await shadow_metadata.writeMetadata(paths, next);
|
|
64
|
-
}
|
|
56
|
+
await shadow_metadata.writeMetadata(paths, next);
|
|
65
57
|
return next;
|
|
66
58
|
}
|
|
67
59
|
|
|
68
|
-
exports.generatePreviewId = generatePreviewId;
|
|
69
60
|
exports.prepareShadow = prepareShadow;
|
package/dist/types.d.ts
CHANGED
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAChD,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAOD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,IAAI,CAAC;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC;AAGD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAChD,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAOD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,IAAI,CAAC;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC;AAGD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAGD,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;CACxB;AAGD,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,aAAa,CAAC;CACtB"}
|
package/package.json
CHANGED
|
@@ -57,8 +57,6 @@ const ASSET_MANIFEST = {
|
|
|
57
57
|
entrypoints: ['main.js'],
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
-
const STUB_PREVIEW_ID = 'existing-preview-id' as const;
|
|
61
|
-
|
|
62
60
|
describe('deploy --preview', () => {
|
|
63
61
|
let tmp: string;
|
|
64
62
|
let cwd: string;
|
|
@@ -101,14 +99,6 @@ describe('deploy --preview', () => {
|
|
|
101
99
|
await rm(tmp, { recursive: true, force: true });
|
|
102
100
|
});
|
|
103
101
|
|
|
104
|
-
const writeMetadata = (previewId: string) =>
|
|
105
|
-
writeJson(path.join(tmp, '.zth', 'metadata.json'), {
|
|
106
|
-
createdAt: '2026-01-01T00:00:00.000Z',
|
|
107
|
-
appName: 'my-app',
|
|
108
|
-
port: 13000,
|
|
109
|
-
previewId,
|
|
110
|
-
});
|
|
111
|
-
|
|
112
102
|
it('throws when the consumer package.json has no name', async () => {
|
|
113
103
|
await writeJson(path.join(tmp, 'package.json'), {});
|
|
114
104
|
await expect(deployCommand({ preview: true })).rejects.toThrow(/must have a "name" field/);
|
|
@@ -128,58 +118,49 @@ describe('deploy --preview', () => {
|
|
|
128
118
|
expect(uploadDir).toHaveBeenCalledWith(path.join(tmp, '.zth', 'build'), `previews/${previewAppName}/MF`);
|
|
129
119
|
});
|
|
130
120
|
|
|
131
|
-
it('
|
|
132
|
-
|
|
133
|
-
vi.mocked(brokerProvider.fetchDeployVersion).mockResolvedValue(2);
|
|
134
|
-
|
|
135
|
-
await deployCommand({ preview: true });
|
|
121
|
+
it('calls registerOrUpdateApp for a brand-new preview at version 1 with an empty gitRemote', async () => {
|
|
122
|
+
vi.mocked(brokerProvider.fetchDeployVersion).mockResolvedValue(0);
|
|
136
123
|
|
|
137
|
-
|
|
138
|
-
expect(call.previewId).toBe(STUB_PREVIEW_ID);
|
|
124
|
+
await deployCommand({ preview: true, previewId: 'existing-preview-id' });
|
|
139
125
|
|
|
140
|
-
const
|
|
141
|
-
|
|
126
|
+
const previewSlug = buildPreviewSlug('my-app', 'existing-preview-id');
|
|
127
|
+
const previewAppName = `mf-${previewSlug}`;
|
|
142
128
|
|
|
143
129
|
expect(brokerApp.registerOrUpdateApp).toHaveBeenCalledWith({
|
|
144
130
|
appName: previewAppName,
|
|
145
131
|
token: 'jwt-stub',
|
|
146
|
-
nextVersion:
|
|
147
|
-
entry: expect.objectContaining({ ...ASSET_MANIFEST, version:
|
|
132
|
+
nextVersion: 1,
|
|
133
|
+
entry: expect.objectContaining({ ...ASSET_MANIFEST, version: 1, gitRemote: '' }),
|
|
148
134
|
gitRemote: '',
|
|
149
135
|
});
|
|
150
136
|
});
|
|
151
137
|
|
|
152
|
-
it('
|
|
153
|
-
|
|
154
|
-
vi.mocked(brokerProvider.fetchDeployVersion).mockResolvedValue(0);
|
|
138
|
+
it('reuses a supplied previewId and calls registerOrUpdateApp at version+1', async () => {
|
|
139
|
+
vi.mocked(brokerProvider.fetchDeployVersion).mockResolvedValue(2);
|
|
155
140
|
|
|
156
|
-
await deployCommand({ preview: true });
|
|
141
|
+
await deployCommand({ preview: true, previewId: 'existing-preview-id' });
|
|
142
|
+
|
|
143
|
+
const call = vi.mocked(brokerProvider.brokerCredentialProvider).mock.calls[0][0];
|
|
144
|
+
expect(call.previewId).toBe('existing-preview-id');
|
|
145
|
+
|
|
146
|
+
const previewSlug = buildPreviewSlug('my-app', 'existing-preview-id');
|
|
147
|
+
const previewAppName = `mf-${previewSlug}`;
|
|
148
|
+
|
|
149
|
+
expect(uploadDir).toHaveBeenCalledWith(path.join(tmp, '.zth', 'build'), `previews/${previewAppName}/MF`);
|
|
157
150
|
|
|
158
|
-
const previewAppName = `mf-${buildPreviewSlug('my-app', STUB_PREVIEW_ID)}`;
|
|
159
151
|
expect(brokerApp.registerOrUpdateApp).toHaveBeenCalledWith({
|
|
160
152
|
appName: previewAppName,
|
|
161
153
|
token: 'jwt-stub',
|
|
162
|
-
nextVersion:
|
|
163
|
-
entry: expect.objectContaining({ ...ASSET_MANIFEST, version:
|
|
154
|
+
nextVersion: 3,
|
|
155
|
+
entry: expect.objectContaining({ ...ASSET_MANIFEST, version: 3, gitRemote: '' }),
|
|
164
156
|
gitRemote: '',
|
|
165
157
|
});
|
|
166
158
|
});
|
|
167
159
|
|
|
168
|
-
it('--snapshot mints a fresh previewId even when metadata has an existing one', async () => {
|
|
169
|
-
await writeMetadata(STUB_PREVIEW_ID);
|
|
170
|
-
|
|
171
|
-
await deployCommand({ preview: true, snapshot: true });
|
|
172
|
-
|
|
173
|
-
const call = vi.mocked(brokerProvider.brokerCredentialProvider).mock.calls[0][0];
|
|
174
|
-
expect(call.previewId).not.toBe(STUB_PREVIEW_ID);
|
|
175
|
-
expect(call.previewId).toMatch(/^[0-9a-f]{8}$/);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
160
|
it('passes a full CDN URL (not a relative path) as publicUrl to the build', async () => {
|
|
179
|
-
await
|
|
180
|
-
await deployCommand({ preview: true });
|
|
161
|
+
await deployCommand({ preview: true, previewId: 'existing-preview-id' });
|
|
181
162
|
|
|
182
|
-
const previewAppName = `mf-${buildPreviewSlug('my-app',
|
|
163
|
+
const previewAppName = `mf-${buildPreviewSlug('my-app', 'existing-preview-id')}`;
|
|
183
164
|
expect(buildCommand).toHaveBeenCalledWith(
|
|
184
165
|
expect.objectContaining({
|
|
185
166
|
publicUrl: `https://bigbrain-zth-mf-assets.bigbrain.me/previews/${previewAppName}/MF`,
|
|
@@ -197,10 +178,9 @@ describe('deploy --preview', () => {
|
|
|
197
178
|
});
|
|
198
179
|
|
|
199
180
|
it('--open opens the previewUrl in the browser on success', async () => {
|
|
200
|
-
await
|
|
201
|
-
await deployCommand({ preview: true, open: true });
|
|
181
|
+
await deployCommand({ preview: true, previewId: 'existing-preview-id', open: true });
|
|
202
182
|
|
|
203
|
-
const previewSlug = buildPreviewSlug('my-app',
|
|
183
|
+
const previewSlug = buildPreviewSlug('my-app', 'existing-preview-id');
|
|
204
184
|
expect(openInBrowser).toHaveBeenCalledWith(`https://bigbrain.me/bigbrain-vibe/${previewSlug}`);
|
|
205
185
|
});
|
|
206
186
|
|
|
@@ -215,20 +195,19 @@ describe('deploy --preview', () => {
|
|
|
215
195
|
});
|
|
216
196
|
|
|
217
197
|
it('--json emits a machine-readable payload with an app-shaped previewUrl (not ?z2h_preview=)', async () => {
|
|
218
|
-
await writeMetadata(STUB_PREVIEW_ID);
|
|
219
198
|
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
220
199
|
setOutputMode('json');
|
|
221
200
|
|
|
222
|
-
await deployCommand({ preview: true });
|
|
201
|
+
await deployCommand({ preview: true, previewId: 'existing-preview-id' });
|
|
223
202
|
|
|
224
203
|
const jsonLine = writeSpy.mock.calls.map(c => c[0]).find(line => typeof line === 'string' && line.includes('"ok"'));
|
|
225
204
|
expect(jsonLine).toBeDefined();
|
|
226
205
|
const payload = JSON.parse(jsonLine as string);
|
|
227
|
-
const previewSlug = buildPreviewSlug('my-app',
|
|
206
|
+
const previewSlug = buildPreviewSlug('my-app', 'existing-preview-id');
|
|
228
207
|
expect(payload).toMatchObject({
|
|
229
208
|
ok: true,
|
|
230
209
|
appName: 'mf-my-app',
|
|
231
|
-
previewId:
|
|
210
|
+
previewId: 'existing-preview-id',
|
|
232
211
|
previewAppName: `mf-${previewSlug}`,
|
|
233
212
|
previewUrl: `https://bigbrain.me/bigbrain-vibe/${previewSlug}`,
|
|
234
213
|
});
|
package/src/commands/deploy.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { readJson } from 'fs-extra';
|
|
3
4
|
import { info, warn, output, getOutputMode } from '../util/logger';
|
|
4
5
|
import { resolveConsumerPaths } from '../shadow/paths';
|
|
@@ -21,8 +22,6 @@ import { setupS3Client } from '../util/s3/client';
|
|
|
21
22
|
import { getMachineAppName } from '../util/machine-identity';
|
|
22
23
|
import { registerOrUpdateApp } from '../util/broker/app';
|
|
23
24
|
import { openInBrowser } from '../util/open-browser';
|
|
24
|
-
import { readMetadata, writeMetadata } from '../shadow/metadata';
|
|
25
|
-
import { generatePreviewId } from '../shadow/prepare';
|
|
26
25
|
import { buildCommand } from './build';
|
|
27
26
|
import { promptForTags } from './tag';
|
|
28
27
|
import type { ConsumerPaths } from '../types';
|
|
@@ -35,8 +34,8 @@ export interface DeployOptions {
|
|
|
35
34
|
// Registers a real, disposable `apps` row under a synthetic name instead of
|
|
36
35
|
// deploying — no git, no deploy lock (each previewId's slug is unique).
|
|
37
36
|
preview?: boolean;
|
|
38
|
-
// With --preview:
|
|
39
|
-
|
|
37
|
+
// With --preview: update an existing preview instead of minting a new one.
|
|
38
|
+
previewId?: string;
|
|
40
39
|
open?: boolean;
|
|
41
40
|
}
|
|
42
41
|
|
|
@@ -96,15 +95,7 @@ async function buildAndStamp(opts: {
|
|
|
96
95
|
}
|
|
97
96
|
|
|
98
97
|
async function deployPreview({ paths, consumerName, appName, token, opts }: DeployContext): Promise<void> {
|
|
99
|
-
const
|
|
100
|
-
const previewId = opts.snapshot ? generatePreviewId() : (meta?.previewId ?? generatePreviewId());
|
|
101
|
-
|
|
102
|
-
// Pre-stamp so prepareShadow (called inside buildAndStamp) preserves our previewId.
|
|
103
|
-
// Only possible when metadata already exists — otherwise prepareShadow creates it fresh.
|
|
104
|
-
if (!opts.snapshot && meta && meta.previewId !== previewId) {
|
|
105
|
-
await writeMetadata(paths, { ...meta, previewId });
|
|
106
|
-
}
|
|
107
|
-
|
|
98
|
+
const previewId = opts.previewId ?? randomUUID().slice(0, 8);
|
|
108
99
|
const previewSlug = buildPreviewSlug(consumerName, previewId);
|
|
109
100
|
const previewAppName = mfAppName(previewSlug);
|
|
110
101
|
const keyPrefix = `${S3_PREVIEWS_FOLDER}/${previewAppName}/MF`;
|
|
@@ -117,15 +108,6 @@ async function deployPreview({ paths, consumerName, appName, token, opts }: Depl
|
|
|
117
108
|
info('building');
|
|
118
109
|
const entry = await buildAndStamp({ paths, publicUrl: url, nextVersion, gitRemote: '' });
|
|
119
110
|
|
|
120
|
-
// Post-stamp: if metadata didn't exist before build, prepareShadow created it with
|
|
121
|
-
// its own previewId. Overwrite with ours so subsequent runs reuse the same URL.
|
|
122
|
-
if (!opts.snapshot) {
|
|
123
|
-
const freshMeta = await readMetadata(paths);
|
|
124
|
-
if (freshMeta && freshMeta.previewId !== previewId) {
|
|
125
|
-
await writeMetadata(paths, { ...freshMeta, previewId });
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
111
|
const creds = await brokerCredentialProvider({ appName: previewAppName, previewId })();
|
|
130
112
|
setupS3Client(creds);
|
|
131
113
|
|
package/src/index.ts
CHANGED
|
@@ -103,12 +103,8 @@ async function main(): Promise<void> {
|
|
|
103
103
|
.description('Build, upload to S3, and register the app with the Z2H backend')
|
|
104
104
|
.option('--dry-run', 'print what would happen without uploading anything')
|
|
105
105
|
.option(
|
|
106
|
-
'--preview',
|
|
107
|
-
'build an ephemeral, shareable preview instead of deploying — no git required, no deploy lock, never touches the live version;
|
|
108
|
-
)
|
|
109
|
-
.option(
|
|
110
|
-
'--snapshot',
|
|
111
|
-
'with --preview: deploy to a fresh previewId without touching the sticky URL — produces a permanent point-in-time link'
|
|
106
|
+
'--preview [previewId]',
|
|
107
|
+
'build an ephemeral, shareable preview instead of deploying — no git required, no deploy lock, never touches the live version; pass an existing previewId to update it instead of minting a new one'
|
|
112
108
|
)
|
|
113
109
|
.option('--open', 'open the resulting app/preview URL in the browser on success')
|
|
114
110
|
.option(
|
|
@@ -122,8 +118,7 @@ async function main(): Promise<void> {
|
|
|
122
118
|
.action(
|
|
123
119
|
async (opts: {
|
|
124
120
|
dryRun?: boolean;
|
|
125
|
-
preview?: boolean;
|
|
126
|
-
snapshot?: boolean;
|
|
121
|
+
preview?: boolean | string;
|
|
127
122
|
open?: boolean;
|
|
128
123
|
remote?: string;
|
|
129
124
|
repoUrl?: string;
|
|
@@ -131,7 +126,12 @@ async function main(): Promise<void> {
|
|
|
131
126
|
try {
|
|
132
127
|
await trackDuration(
|
|
133
128
|
opts.preview ? 'z2h_app_preview_success' : 'z2h_app_deploy_success',
|
|
134
|
-
() =>
|
|
129
|
+
() =>
|
|
130
|
+
deployCommand({
|
|
131
|
+
...opts,
|
|
132
|
+
preview: Boolean(opts.preview),
|
|
133
|
+
previewId: typeof opts.preview === 'string' ? opts.preview : undefined,
|
|
134
|
+
} as Parameters<typeof deployCommand>[0]),
|
|
135
135
|
{
|
|
136
136
|
app_name: getCwdAppName(),
|
|
137
137
|
}
|
package/src/shadow/prepare.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
1
|
import { ensureDir, pathExists, readJson } from 'fs-extra';
|
|
3
2
|
import { info } from '../util/logger';
|
|
4
3
|
import { cleanAppName } from '../util/app-name';
|
|
@@ -45,10 +44,6 @@ export async function prepareShadow(consumerDir: string = process.cwd()): Promis
|
|
|
45
44
|
return { appName, port, paths };
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
export function generatePreviewId(): string {
|
|
49
|
-
return randomUUID().slice(0, 8);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
47
|
/**
|
|
53
48
|
* Resolve the dev server port. Prefer the persisted port if it's still free
|
|
54
49
|
* (URL stays stable across restarts). Otherwise pick a fresh one. The
|
|
@@ -57,22 +52,15 @@ export function generatePreviewId(): string {
|
|
|
57
52
|
*/
|
|
58
53
|
async function resolvePortAndMetadata(paths: ConsumerPaths, appName: string): Promise<ShadowMetadata> {
|
|
59
54
|
const existing = await readMetadata(paths);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (!port || !(await isPortFree(port))) {
|
|
63
|
-
port = await pickPort();
|
|
55
|
+
if (existing?.port && (await isPortFree(existing.port))) {
|
|
56
|
+
return existing;
|
|
64
57
|
}
|
|
65
58
|
|
|
66
59
|
const next: ShadowMetadata = {
|
|
67
60
|
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
68
61
|
appName,
|
|
69
|
-
port,
|
|
70
|
-
previewId: existing?.previewId ?? generatePreviewId(),
|
|
62
|
+
port: await pickPort(),
|
|
71
63
|
};
|
|
72
|
-
|
|
73
|
-
if (JSON.stringify(next) !== JSON.stringify(existing)) {
|
|
74
|
-
await writeMetadata(paths, next);
|
|
75
|
-
}
|
|
76
|
-
|
|
64
|
+
await writeMetadata(paths, next);
|
|
77
65
|
return next;
|
|
78
66
|
}
|