@mondaydotcomorg/z2h-cli 0.20.0 → 0.21.0-feat-arielle-fixed-preview-id-3537677
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 +0 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +10 -2
- package/dist/esm/commands/deploy.d.ts +0 -1
- package/dist/esm/commands/deploy.d.ts.map +1 -1
- package/dist/esm/commands/deploy.mjs +10 -2
- package/dist/esm/index.mjs +2 -6
- package/dist/esm/shadow/prepare.d.ts +1 -0
- package/dist/esm/shadow/prepare.d.ts.map +1 -1
- package/dist/esm/shadow/prepare.mjs +13 -5
- package/dist/esm/types.d.ts +1 -0
- package/dist/esm/types.d.ts.map +1 -1
- package/dist/index.js +2 -6
- package/dist/shadow/prepare.d.ts +1 -0
- package/dist/shadow/prepare.d.ts.map +1 -1
- package/dist/shadow/prepare.js +13 -4
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/commands/__tests__/deploy.test.ts +38 -27
- package/src/commands/deploy.ts +11 -4
- package/src/index.ts +4 -15
- package/src/shadow/prepare.ts +16 -4
- package/src/types.ts +1 -0
|
@@ -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":"AAkBA,OAAO,EAAE,KAAK,UAAU,EAA2C,MAAM,4BAA4B,CAAC;AAYtG,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;IAClB,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,7 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
2
|
|
|
3
3
|
const os = require('node:os');
|
|
4
|
-
const crypto = require('node:crypto');
|
|
5
4
|
const fs = require('fs-extra');
|
|
6
5
|
const util_logger = require('../util/logger.js');
|
|
7
6
|
const shadow_paths = require('../shadow/paths.js');
|
|
@@ -18,6 +17,8 @@ const util_s3_client = require('../util/s3/client.js');
|
|
|
18
17
|
const util_machineIdentity = require('../util/machine-identity.js');
|
|
19
18
|
const util_broker_app = require('../util/broker/app.js');
|
|
20
19
|
const util_openBrowser = require('../util/open-browser.js');
|
|
20
|
+
const shadow_metadata = require('../shadow/metadata.js');
|
|
21
|
+
const shadow_prepare = require('../shadow/prepare.js');
|
|
21
22
|
const commands_build = require('./build.js');
|
|
22
23
|
const commands_tag = require('./tag.js');
|
|
23
24
|
|
|
@@ -63,7 +64,8 @@ async function buildAndStamp(opts) {
|
|
|
63
64
|
};
|
|
64
65
|
}
|
|
65
66
|
async function deployPreview({ paths, consumerName, appName, token, opts }) {
|
|
66
|
-
const
|
|
67
|
+
const meta = await shadow_metadata.readMetadata(paths);
|
|
68
|
+
const previewId = meta?.previewId ?? shadow_prepare.generatePreviewId();
|
|
67
69
|
const previewSlug = util_appName.buildPreviewSlug(consumerName, previewId);
|
|
68
70
|
const previewAppName = util_appName.mfAppName(previewSlug);
|
|
69
71
|
const keyPrefix = `${constants.S3_PREVIEWS_FOLDER}/${previewAppName}/MF`;
|
|
@@ -73,10 +75,12 @@ async function deployPreview({ paths, consumerName, appName, token, opts }) {
|
|
|
73
75
|
const nextVersion = (await util_auth_brokerCredentialProvider.fetchDeployVersion(previewAppName)) + 1;
|
|
74
76
|
util_logger.info('building');
|
|
75
77
|
const entry = await buildAndStamp({ paths, publicUrl: url, nextVersion, gitRemote: '' });
|
|
78
|
+
util_logger.info('getting sts credentials');
|
|
76
79
|
const creds = await util_auth_brokerCredentialProvider.brokerCredentialProvider({ appName: previewAppName, previewId })();
|
|
77
80
|
util_s3_client.setupS3Client(creds);
|
|
78
81
|
util_logger.info(`uploading preview build → s3://${constants.BUCKET_WRITE_NAME}/${keyPrefix}/`);
|
|
79
82
|
await util_s3_upload.uploadDir(paths.shadowBuild, keyPrefix);
|
|
83
|
+
util_logger.info('updating manifest');
|
|
80
84
|
await util_broker_app.registerOrUpdateApp({ appName: previewAppName, token, nextVersion, entry, gitRemote: '' });
|
|
81
85
|
const previewUrl = `https://bigbrain.me/bigbrain-vibe/${previewSlug}`;
|
|
82
86
|
util_logger.output(`[z2h-cli] preview ready ✅\n app: ${appName}\n previewAppName: ${previewAppName}\n open: ${previewUrl}`, {
|
|
@@ -151,8 +155,10 @@ async function deployLive({ paths, consumerName, appName, token, opts }) {
|
|
|
151
155
|
// Acquire the deploy lock explicitly. Done after the build
|
|
152
156
|
// so the lock hold stays tiny — it wraps only push → upload → manifest.
|
|
153
157
|
// A 409 here means someone else is mid-deploy;
|
|
158
|
+
util_logger.info('getting sts credentials');
|
|
154
159
|
const creds = await util_auth_brokerCredentialProvider.brokerCredentialProvider({ appName })();
|
|
155
160
|
util_s3_client.setupS3Client(creds);
|
|
161
|
+
util_logger.info('acquiring deploy lock');
|
|
156
162
|
await acquireDeployLock(appName, token);
|
|
157
163
|
// Push before any S3 write so a rejected push (someone deployed during our
|
|
158
164
|
// build) stops the deploy before the bundle/manifest go live
|
|
@@ -162,9 +168,11 @@ async function deployLive({ paths, consumerName, appName, token, opts }) {
|
|
|
162
168
|
}
|
|
163
169
|
util_logger.info(`uploading build → s3://${constants.BUCKET_WRITE_NAME}/${keyPrefix}/`);
|
|
164
170
|
await util_s3_upload.uploadDir(paths.shadowBuild, keyPrefix);
|
|
171
|
+
util_logger.info('updating manifest');
|
|
165
172
|
await util_broker_app.registerOrUpdateApp({ appName, token, nextVersion, entry, gitRemote });
|
|
166
173
|
}
|
|
167
174
|
finally {
|
|
175
|
+
util_logger.info('releasing deploy lock');
|
|
168
176
|
await releaseLockBestEffort(appName, token);
|
|
169
177
|
}
|
|
170
178
|
// Tags are a post-deploy concern: the S3 upload has already succeeded, so a
|
|
@@ -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":"AAkBA,OAAO,EAAE,KAAK,UAAU,EAA2C,MAAM,4BAA4B,CAAC;AAYtG,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;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAWD,wBAAsB,aAAa,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3E"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
3
2
|
import { readJson } from 'fs-extra';
|
|
4
3
|
import { info, output, getOutputMode, warn } from '../util/logger.mjs';
|
|
5
4
|
import { resolveConsumerPaths } from '../shadow/paths.mjs';
|
|
@@ -16,6 +15,8 @@ import { setupS3Client } from '../util/s3/client.mjs';
|
|
|
16
15
|
import { getMachineAppName } from '../util/machine-identity.mjs';
|
|
17
16
|
import { registerOrUpdateApp } from '../util/broker/app.mjs';
|
|
18
17
|
import { openInBrowser } from '../util/open-browser.mjs';
|
|
18
|
+
import { readMetadata } from '../shadow/metadata.mjs';
|
|
19
|
+
import { generatePreviewId } from '../shadow/prepare.mjs';
|
|
19
20
|
import { buildCommand } from './build.mjs';
|
|
20
21
|
import { promptForTags } from './tag.mjs';
|
|
21
22
|
|
|
@@ -57,7 +58,8 @@ async function buildAndStamp(opts) {
|
|
|
57
58
|
};
|
|
58
59
|
}
|
|
59
60
|
async function deployPreview({ paths, consumerName, appName, token, opts }) {
|
|
60
|
-
const
|
|
61
|
+
const meta = await readMetadata(paths);
|
|
62
|
+
const previewId = meta?.previewId ?? generatePreviewId();
|
|
61
63
|
const previewSlug = buildPreviewSlug(consumerName, previewId);
|
|
62
64
|
const previewAppName = mfAppName(previewSlug);
|
|
63
65
|
const keyPrefix = `${S3_PREVIEWS_FOLDER}/${previewAppName}/MF`;
|
|
@@ -67,10 +69,12 @@ async function deployPreview({ paths, consumerName, appName, token, opts }) {
|
|
|
67
69
|
const nextVersion = (await fetchDeployVersion(previewAppName)) + 1;
|
|
68
70
|
info('building');
|
|
69
71
|
const entry = await buildAndStamp({ paths, publicUrl: url, nextVersion, gitRemote: '' });
|
|
72
|
+
info('getting sts credentials');
|
|
70
73
|
const creds = await brokerCredentialProvider({ appName: previewAppName, previewId })();
|
|
71
74
|
setupS3Client(creds);
|
|
72
75
|
info(`uploading preview build → s3://${BUCKET_WRITE_NAME}/${keyPrefix}/`);
|
|
73
76
|
await uploadDir(paths.shadowBuild, keyPrefix);
|
|
77
|
+
info('updating manifest');
|
|
74
78
|
await registerOrUpdateApp({ appName: previewAppName, token, nextVersion, entry, gitRemote: '' });
|
|
75
79
|
const previewUrl = `https://bigbrain.me/bigbrain-vibe/${previewSlug}`;
|
|
76
80
|
output(`[z2h-cli] preview ready ✅\n app: ${appName}\n previewAppName: ${previewAppName}\n open: ${previewUrl}`, {
|
|
@@ -145,8 +149,10 @@ async function deployLive({ paths, consumerName, appName, token, opts }) {
|
|
|
145
149
|
// Acquire the deploy lock explicitly. Done after the build
|
|
146
150
|
// so the lock hold stays tiny — it wraps only push → upload → manifest.
|
|
147
151
|
// A 409 here means someone else is mid-deploy;
|
|
152
|
+
info('getting sts credentials');
|
|
148
153
|
const creds = await brokerCredentialProvider({ appName })();
|
|
149
154
|
setupS3Client(creds);
|
|
155
|
+
info('acquiring deploy lock');
|
|
150
156
|
await acquireDeployLock(appName, token);
|
|
151
157
|
// Push before any S3 write so a rejected push (someone deployed during our
|
|
152
158
|
// build) stops the deploy before the bundle/manifest go live
|
|
@@ -156,9 +162,11 @@ async function deployLive({ paths, consumerName, appName, token, opts }) {
|
|
|
156
162
|
}
|
|
157
163
|
info(`uploading build → s3://${BUCKET_WRITE_NAME}/${keyPrefix}/`);
|
|
158
164
|
await uploadDir(paths.shadowBuild, keyPrefix);
|
|
165
|
+
info('updating manifest');
|
|
159
166
|
await registerOrUpdateApp({ appName, token, nextVersion, entry, gitRemote });
|
|
160
167
|
}
|
|
161
168
|
finally {
|
|
169
|
+
info('releasing deploy lock');
|
|
162
170
|
await releaseLockBestEffort(appName, token);
|
|
163
171
|
}
|
|
164
172
|
// Tags are a post-deploy concern: the S3 upload has already succeeded, so a
|
package/dist/esm/index.mjs
CHANGED
|
@@ -93,17 +93,13 @@ 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
|
|
96
|
+
.option('--preview', 'build an ephemeral, shareable preview instead of deploying — no git required, no deploy lock, never touches the live version; reuses the same preview URL across runs (stable link)')
|
|
97
97
|
.option('--open', 'open the resulting app/preview URL in the browser on success')
|
|
98
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.')
|
|
99
99
|
.option('--repo-url <url>', 'First deploy only: GitHub repo URL — required when --remote=github. Error if the app already has a git remote.')
|
|
100
100
|
.action(async (opts) => {
|
|
101
101
|
try {
|
|
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
|
-
}), {
|
|
102
|
+
await trackDuration(opts.preview ? 'z2h_app_preview_success' : 'z2h_app_deploy_success', () => deployCommand(opts), {
|
|
107
103
|
app_name: getCwdAppName(),
|
|
108
104
|
});
|
|
109
105
|
}
|
|
@@ -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":"AAWA,OAAO,KAAK,EAA8B,cAAc,EAAkB,MAAM,UAAU,CAAC;AAE3F,wBAAsB,aAAa,CAAC,WAAW,GAAE,MAAsB,GAAG,OAAO,CAAC,cAAc,CAAC,CAgChG;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { pathExists, readJson, ensureDir } from 'fs-extra';
|
|
2
3
|
import { info } from '../util/logger.mjs';
|
|
3
4
|
import { cleanAppName } from '../util/app-name.mjs';
|
|
@@ -35,6 +36,9 @@ async function prepareShadow(consumerDir = process.cwd()) {
|
|
|
35
36
|
await syncShadow(paths, consumerPkg);
|
|
36
37
|
return { appName, port, paths };
|
|
37
38
|
}
|
|
39
|
+
function generatePreviewId() {
|
|
40
|
+
return randomUUID().slice(0, 8);
|
|
41
|
+
}
|
|
38
42
|
/**
|
|
39
43
|
* Resolve the dev server port. Prefer the persisted port if it's still free
|
|
40
44
|
* (URL stays stable across restarts). Otherwise pick a fresh one. The
|
|
@@ -43,16 +47,20 @@ async function prepareShadow(consumerDir = process.cwd()) {
|
|
|
43
47
|
*/
|
|
44
48
|
async function resolvePortAndMetadata(paths, appName) {
|
|
45
49
|
const existing = await readMetadata(paths);
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
let port = existing?.port;
|
|
51
|
+
if (!port || !(await isPortFree(port))) {
|
|
52
|
+
port = await pickPort();
|
|
48
53
|
}
|
|
49
54
|
const next = {
|
|
50
55
|
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
51
56
|
appName,
|
|
52
|
-
port
|
|
57
|
+
port,
|
|
58
|
+
previewId: existing?.previewId ?? generatePreviewId(),
|
|
53
59
|
};
|
|
54
|
-
|
|
60
|
+
if (JSON.stringify(next) !== JSON.stringify(existing)) {
|
|
61
|
+
await writeMetadata(paths, next);
|
|
62
|
+
}
|
|
55
63
|
return next;
|
|
56
64
|
}
|
|
57
65
|
|
|
58
|
-
export { prepareShadow };
|
|
66
|
+
export { generatePreviewId, 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;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;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,17 +99,13 @@ 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
|
|
102
|
+
.option('--preview', 'build an ephemeral, shareable preview instead of deploying — no git required, no deploy lock, never touches the live version; reuses the same preview URL across runs (stable link)')
|
|
103
103
|
.option('--open', 'open the resulting app/preview URL in the browser on success')
|
|
104
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.')
|
|
105
105
|
.option('--repo-url <url>', 'First deploy only: GitHub repo URL — required when --remote=github. Error if the app already has a git remote.')
|
|
106
106
|
.action(async (opts) => {
|
|
107
107
|
try {
|
|
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
|
-
}), {
|
|
108
|
+
await util_tracker.trackDuration(opts.preview ? 'z2h_app_preview_success' : 'z2h_app_deploy_success', () => commands_deploy.deployCommand(opts), {
|
|
113
109
|
app_name: util_appName.getCwdAppName(),
|
|
114
110
|
});
|
|
115
111
|
}
|
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":"AAWA,OAAO,KAAK,EAA8B,cAAc,EAAkB,MAAM,UAAU,CAAC;AAE3F,wBAAsB,aAAa,CAAC,WAAW,GAAE,MAAsB,GAAG,OAAO,CAAC,cAAc,CAAC,CAgChG;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C"}
|
package/dist/shadow/prepare.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
2
|
|
|
3
|
+
const crypto = require('node:crypto');
|
|
3
4
|
const fs = require('fs-extra');
|
|
4
5
|
const util_logger = require('../util/logger.js');
|
|
5
6
|
const util_appName = require('../util/app-name.js');
|
|
@@ -37,6 +38,9 @@ async function prepareShadow(consumerDir = process.cwd()) {
|
|
|
37
38
|
await shadow_sync.syncShadow(paths, consumerPkg);
|
|
38
39
|
return { appName, port, paths };
|
|
39
40
|
}
|
|
41
|
+
function generatePreviewId() {
|
|
42
|
+
return crypto.randomUUID().slice(0, 8);
|
|
43
|
+
}
|
|
40
44
|
/**
|
|
41
45
|
* Resolve the dev server port. Prefer the persisted port if it's still free
|
|
42
46
|
* (URL stays stable across restarts). Otherwise pick a fresh one. The
|
|
@@ -45,16 +49,21 @@ async function prepareShadow(consumerDir = process.cwd()) {
|
|
|
45
49
|
*/
|
|
46
50
|
async function resolvePortAndMetadata(paths, appName) {
|
|
47
51
|
const existing = await shadow_metadata.readMetadata(paths);
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
let port = existing?.port;
|
|
53
|
+
if (!port || !(await shadow_port.isPortFree(port))) {
|
|
54
|
+
port = await shadow_port.pickPort();
|
|
50
55
|
}
|
|
51
56
|
const next = {
|
|
52
57
|
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
53
58
|
appName,
|
|
54
|
-
port
|
|
59
|
+
port,
|
|
60
|
+
previewId: existing?.previewId ?? generatePreviewId(),
|
|
55
61
|
};
|
|
56
|
-
|
|
62
|
+
if (JSON.stringify(next) !== JSON.stringify(existing)) {
|
|
63
|
+
await shadow_metadata.writeMetadata(paths, next);
|
|
64
|
+
}
|
|
57
65
|
return next;
|
|
58
66
|
}
|
|
59
67
|
|
|
68
|
+
exports.generatePreviewId = generatePreviewId;
|
|
60
69
|
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;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;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,6 +57,8 @@ const ASSET_MANIFEST = {
|
|
|
57
57
|
entrypoints: ['main.js'],
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
+
const STUB_PREVIEW_ID = 'existing-preview-id' as const;
|
|
61
|
+
|
|
60
62
|
describe('deploy --preview', () => {
|
|
61
63
|
let tmp: string;
|
|
62
64
|
let cwd: string;
|
|
@@ -99,6 +101,14 @@ describe('deploy --preview', () => {
|
|
|
99
101
|
await rm(tmp, { recursive: true, force: true });
|
|
100
102
|
});
|
|
101
103
|
|
|
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
|
+
|
|
102
112
|
it('throws when the consumer package.json has no name', async () => {
|
|
103
113
|
await writeJson(path.join(tmp, 'package.json'), {});
|
|
104
114
|
await expect(deployCommand({ preview: true })).rejects.toThrow(/must have a "name" field/);
|
|
@@ -118,49 +128,48 @@ describe('deploy --preview', () => {
|
|
|
118
128
|
expect(uploadDir).toHaveBeenCalledWith(path.join(tmp, '.zth', 'build'), `previews/${previewAppName}/MF`);
|
|
119
129
|
});
|
|
120
130
|
|
|
121
|
-
it('
|
|
122
|
-
|
|
131
|
+
it('reuses the sticky previewId from metadata', async () => {
|
|
132
|
+
await writeMetadata(STUB_PREVIEW_ID);
|
|
133
|
+
vi.mocked(brokerProvider.fetchDeployVersion).mockResolvedValue(2);
|
|
123
134
|
|
|
124
|
-
await deployCommand({ preview: true
|
|
135
|
+
await deployCommand({ preview: true });
|
|
125
136
|
|
|
126
|
-
const
|
|
127
|
-
|
|
137
|
+
const call = vi.mocked(brokerProvider.brokerCredentialProvider).mock.calls[0][0];
|
|
138
|
+
expect(call.previewId).toBe(STUB_PREVIEW_ID);
|
|
139
|
+
|
|
140
|
+
const previewAppName = `mf-${buildPreviewSlug('my-app', STUB_PREVIEW_ID)}`;
|
|
141
|
+
expect(uploadDir).toHaveBeenCalledWith(path.join(tmp, '.zth', 'build'), `previews/${previewAppName}/MF`);
|
|
128
142
|
|
|
129
143
|
expect(brokerApp.registerOrUpdateApp).toHaveBeenCalledWith({
|
|
130
144
|
appName: previewAppName,
|
|
131
145
|
token: 'jwt-stub',
|
|
132
|
-
nextVersion:
|
|
133
|
-
entry: expect.objectContaining({ ...ASSET_MANIFEST, version:
|
|
146
|
+
nextVersion: 3,
|
|
147
|
+
entry: expect.objectContaining({ ...ASSET_MANIFEST, version: 3, gitRemote: '' }),
|
|
134
148
|
gitRemote: '',
|
|
135
149
|
});
|
|
136
150
|
});
|
|
137
151
|
|
|
138
|
-
it('
|
|
139
|
-
|
|
140
|
-
|
|
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}`;
|
|
152
|
+
it('calls registerOrUpdateApp for a brand-new preview at version 1 with an empty gitRemote', async () => {
|
|
153
|
+
await writeMetadata(STUB_PREVIEW_ID);
|
|
154
|
+
vi.mocked(brokerProvider.fetchDeployVersion).mockResolvedValue(0);
|
|
148
155
|
|
|
149
|
-
|
|
156
|
+
await deployCommand({ preview: true });
|
|
150
157
|
|
|
158
|
+
const previewAppName = `mf-${buildPreviewSlug('my-app', STUB_PREVIEW_ID)}`;
|
|
151
159
|
expect(brokerApp.registerOrUpdateApp).toHaveBeenCalledWith({
|
|
152
160
|
appName: previewAppName,
|
|
153
161
|
token: 'jwt-stub',
|
|
154
|
-
nextVersion:
|
|
155
|
-
entry: expect.objectContaining({ ...ASSET_MANIFEST, version:
|
|
162
|
+
nextVersion: 1,
|
|
163
|
+
entry: expect.objectContaining({ ...ASSET_MANIFEST, version: 1, gitRemote: '' }),
|
|
156
164
|
gitRemote: '',
|
|
157
165
|
});
|
|
158
166
|
});
|
|
159
167
|
|
|
160
168
|
it('passes a full CDN URL (not a relative path) as publicUrl to the build', async () => {
|
|
161
|
-
await
|
|
169
|
+
await writeMetadata(STUB_PREVIEW_ID);
|
|
170
|
+
await deployCommand({ preview: true });
|
|
162
171
|
|
|
163
|
-
const previewAppName = `mf-${buildPreviewSlug('my-app',
|
|
172
|
+
const previewAppName = `mf-${buildPreviewSlug('my-app', STUB_PREVIEW_ID)}`;
|
|
164
173
|
expect(buildCommand).toHaveBeenCalledWith(
|
|
165
174
|
expect.objectContaining({
|
|
166
175
|
publicUrl: `https://bigbrain-zth-mf-assets.bigbrain.me/previews/${previewAppName}/MF`,
|
|
@@ -178,9 +187,10 @@ describe('deploy --preview', () => {
|
|
|
178
187
|
});
|
|
179
188
|
|
|
180
189
|
it('--open opens the previewUrl in the browser on success', async () => {
|
|
181
|
-
await
|
|
190
|
+
await writeMetadata(STUB_PREVIEW_ID);
|
|
191
|
+
await deployCommand({ preview: true, open: true });
|
|
182
192
|
|
|
183
|
-
const previewSlug = buildPreviewSlug('my-app',
|
|
193
|
+
const previewSlug = buildPreviewSlug('my-app', STUB_PREVIEW_ID);
|
|
184
194
|
expect(openInBrowser).toHaveBeenCalledWith(`https://bigbrain.me/bigbrain-vibe/${previewSlug}`);
|
|
185
195
|
});
|
|
186
196
|
|
|
@@ -195,19 +205,20 @@ describe('deploy --preview', () => {
|
|
|
195
205
|
});
|
|
196
206
|
|
|
197
207
|
it('--json emits a machine-readable payload with an app-shaped previewUrl (not ?z2h_preview=)', async () => {
|
|
208
|
+
await writeMetadata(STUB_PREVIEW_ID);
|
|
198
209
|
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
199
210
|
setOutputMode('json');
|
|
200
211
|
|
|
201
|
-
await deployCommand({ preview: true
|
|
212
|
+
await deployCommand({ preview: true });
|
|
202
213
|
|
|
203
214
|
const jsonLine = writeSpy.mock.calls.map(c => c[0]).find(line => typeof line === 'string' && line.includes('"ok"'));
|
|
204
215
|
expect(jsonLine).toBeDefined();
|
|
205
216
|
const payload = JSON.parse(jsonLine as string);
|
|
206
|
-
const previewSlug = buildPreviewSlug('my-app',
|
|
217
|
+
const previewSlug = buildPreviewSlug('my-app', STUB_PREVIEW_ID);
|
|
207
218
|
expect(payload).toMatchObject({
|
|
208
219
|
ok: true,
|
|
209
220
|
appName: 'mf-my-app',
|
|
210
|
-
previewId:
|
|
221
|
+
previewId: STUB_PREVIEW_ID,
|
|
211
222
|
previewAppName: `mf-${previewSlug}`,
|
|
212
223
|
previewUrl: `https://bigbrain.me/bigbrain-vibe/${previewSlug}`,
|
|
213
224
|
});
|
package/src/commands/deploy.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
3
2
|
import { readJson } from 'fs-extra';
|
|
4
3
|
import { info, warn, output, getOutputMode } from '../util/logger';
|
|
5
4
|
import { resolveConsumerPaths } from '../shadow/paths';
|
|
@@ -22,6 +21,8 @@ import { setupS3Client } from '../util/s3/client';
|
|
|
22
21
|
import { getMachineAppName } from '../util/machine-identity';
|
|
23
22
|
import { registerOrUpdateApp } from '../util/broker/app';
|
|
24
23
|
import { openInBrowser } from '../util/open-browser';
|
|
24
|
+
import { readMetadata } from '../shadow/metadata';
|
|
25
|
+
import { generatePreviewId } from '../shadow/prepare';
|
|
25
26
|
import { buildCommand } from './build';
|
|
26
27
|
import { promptForTags } from './tag';
|
|
27
28
|
import type { ConsumerPaths } from '../types';
|
|
@@ -34,8 +35,6 @@ export interface DeployOptions {
|
|
|
34
35
|
// Registers a real, disposable `apps` row under a synthetic name instead of
|
|
35
36
|
// deploying — no git, no deploy lock (each previewId's slug is unique).
|
|
36
37
|
preview?: boolean;
|
|
37
|
-
// With --preview: update an existing preview instead of minting a new one.
|
|
38
|
-
previewId?: string;
|
|
39
38
|
open?: boolean;
|
|
40
39
|
}
|
|
41
40
|
|
|
@@ -95,7 +94,9 @@ async function buildAndStamp(opts: {
|
|
|
95
94
|
}
|
|
96
95
|
|
|
97
96
|
async function deployPreview({ paths, consumerName, appName, token, opts }: DeployContext): Promise<void> {
|
|
98
|
-
const
|
|
97
|
+
const meta = await readMetadata(paths);
|
|
98
|
+
const previewId = meta?.previewId ?? generatePreviewId();
|
|
99
|
+
|
|
99
100
|
const previewSlug = buildPreviewSlug(consumerName, previewId);
|
|
100
101
|
const previewAppName = mfAppName(previewSlug);
|
|
101
102
|
const keyPrefix = `${S3_PREVIEWS_FOLDER}/${previewAppName}/MF`;
|
|
@@ -108,12 +109,14 @@ async function deployPreview({ paths, consumerName, appName, token, opts }: Depl
|
|
|
108
109
|
info('building');
|
|
109
110
|
const entry = await buildAndStamp({ paths, publicUrl: url, nextVersion, gitRemote: '' });
|
|
110
111
|
|
|
112
|
+
info('getting sts credentials');
|
|
111
113
|
const creds = await brokerCredentialProvider({ appName: previewAppName, previewId })();
|
|
112
114
|
setupS3Client(creds);
|
|
113
115
|
|
|
114
116
|
info(`uploading preview build → s3://${BUCKET_WRITE_NAME}/${keyPrefix}/`);
|
|
115
117
|
await uploadDir(paths.shadowBuild, keyPrefix);
|
|
116
118
|
|
|
119
|
+
info('updating manifest');
|
|
117
120
|
await registerOrUpdateApp({ appName: previewAppName, token, nextVersion, entry, gitRemote: '' });
|
|
118
121
|
|
|
119
122
|
const previewUrl = `https://bigbrain.me/bigbrain-vibe/${previewSlug}`;
|
|
@@ -205,8 +208,10 @@ async function deployLive({ paths, consumerName, appName, token, opts }: DeployC
|
|
|
205
208
|
// Acquire the deploy lock explicitly. Done after the build
|
|
206
209
|
// so the lock hold stays tiny — it wraps only push → upload → manifest.
|
|
207
210
|
// A 409 here means someone else is mid-deploy;
|
|
211
|
+
info('getting sts credentials');
|
|
208
212
|
const creds = await brokerCredentialProvider({ appName })();
|
|
209
213
|
setupS3Client(creds);
|
|
214
|
+
info('acquiring deploy lock');
|
|
210
215
|
await acquireDeployLock(appName, token);
|
|
211
216
|
|
|
212
217
|
// Push before any S3 write so a rejected push (someone deployed during our
|
|
@@ -219,8 +224,10 @@ async function deployLive({ paths, consumerName, appName, token, opts }: DeployC
|
|
|
219
224
|
info(`uploading build → s3://${BUCKET_WRITE_NAME}/${keyPrefix}/`);
|
|
220
225
|
await uploadDir(paths.shadowBuild, keyPrefix);
|
|
221
226
|
|
|
227
|
+
info('updating manifest');
|
|
222
228
|
await registerOrUpdateApp({ appName, token, nextVersion, entry, gitRemote });
|
|
223
229
|
} finally {
|
|
230
|
+
info('releasing deploy lock');
|
|
224
231
|
await releaseLockBestEffort(appName, token);
|
|
225
232
|
}
|
|
226
233
|
|
package/src/index.ts
CHANGED
|
@@ -103,8 +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;
|
|
106
|
+
'--preview',
|
|
107
|
+
'build an ephemeral, shareable preview instead of deploying — no git required, no deploy lock, never touches the live version; reuses the same preview URL across runs (stable link)'
|
|
108
108
|
)
|
|
109
109
|
.option('--open', 'open the resulting app/preview URL in the browser on success')
|
|
110
110
|
.option(
|
|
@@ -116,22 +116,11 @@ async function main(): Promise<void> {
|
|
|
116
116
|
'First deploy only: GitHub repo URL — required when --remote=github. Error if the app already has a git remote.'
|
|
117
117
|
)
|
|
118
118
|
.action(
|
|
119
|
-
async (opts: {
|
|
120
|
-
dryRun?: boolean;
|
|
121
|
-
preview?: boolean | string;
|
|
122
|
-
open?: boolean;
|
|
123
|
-
remote?: string;
|
|
124
|
-
repoUrl?: string;
|
|
125
|
-
}) => {
|
|
119
|
+
async (opts: { dryRun?: boolean; preview?: boolean; open?: boolean; remote?: string; repoUrl?: string }) => {
|
|
126
120
|
try {
|
|
127
121
|
await trackDuration(
|
|
128
122
|
opts.preview ? 'z2h_app_preview_success' : 'z2h_app_deploy_success',
|
|
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]),
|
|
123
|
+
() => deployCommand(opts as Parameters<typeof deployCommand>[0]),
|
|
135
124
|
{
|
|
136
125
|
app_name: getCwdAppName(),
|
|
137
126
|
}
|
package/src/shadow/prepare.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { ensureDir, pathExists, readJson } from 'fs-extra';
|
|
2
3
|
import { info } from '../util/logger';
|
|
3
4
|
import { cleanAppName } from '../util/app-name';
|
|
@@ -44,6 +45,10 @@ export async function prepareShadow(consumerDir: string = process.cwd()): Promis
|
|
|
44
45
|
return { appName, port, paths };
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
export function generatePreviewId(): string {
|
|
49
|
+
return randomUUID().slice(0, 8);
|
|
50
|
+
}
|
|
51
|
+
|
|
47
52
|
/**
|
|
48
53
|
* Resolve the dev server port. Prefer the persisted port if it's still free
|
|
49
54
|
* (URL stays stable across restarts). Otherwise pick a fresh one. The
|
|
@@ -52,15 +57,22 @@ export async function prepareShadow(consumerDir: string = process.cwd()): Promis
|
|
|
52
57
|
*/
|
|
53
58
|
async function resolvePortAndMetadata(paths: ConsumerPaths, appName: string): Promise<ShadowMetadata> {
|
|
54
59
|
const existing = await readMetadata(paths);
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
|
|
61
|
+
let port = existing?.port;
|
|
62
|
+
if (!port || !(await isPortFree(port))) {
|
|
63
|
+
port = await pickPort();
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
const next: ShadowMetadata = {
|
|
60
67
|
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
61
68
|
appName,
|
|
62
|
-
port
|
|
69
|
+
port,
|
|
70
|
+
previewId: existing?.previewId ?? generatePreviewId(),
|
|
63
71
|
};
|
|
64
|
-
|
|
72
|
+
|
|
73
|
+
if (JSON.stringify(next) !== JSON.stringify(existing)) {
|
|
74
|
+
await writeMetadata(paths, next);
|
|
75
|
+
}
|
|
76
|
+
|
|
65
77
|
return next;
|
|
66
78
|
}
|