@mondaydotcomorg/z2h-cli 0.18.1 → 0.19.0-feat-arielle-fixed-preview-id-b98ab39
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 +7 -2
- 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 +7 -2
- package/dist/esm/index.mjs +3 -6
- package/dist/esm/migrations/007-proxy-iframe-src.d.ts +3 -0
- package/dist/esm/migrations/007-proxy-iframe-src.d.ts.map +1 -0
- package/dist/esm/migrations/007-proxy-iframe-src.mjs +49 -0
- package/dist/esm/migrations/__tests__/007-proxy-iframe-src.test.d.ts +2 -0
- package/dist/esm/migrations/__tests__/007-proxy-iframe-src.test.d.ts.map +1 -0
- 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 +3 -6
- package/dist/migrations/007-proxy-iframe-src.d.ts +3 -0
- package/dist/migrations/007-proxy-iframe-src.d.ts.map +1 -0
- package/dist/migrations/007-proxy-iframe-src.js +51 -0
- package/dist/migrations/__tests__/007-proxy-iframe-src.test.d.ts +2 -0
- package/dist/migrations/__tests__/007-proxy-iframe-src.test.d.ts.map +1 -0
- 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/migrations.json +5 -0
- package/package.json +1 -1
- package/src/commands/__tests__/deploy.test.ts +48 -27
- package/src/commands/deploy.ts +10 -4
- package/src/index.ts +5 -11
- package/src/migrations/007-proxy-iframe-src.ts +58 -0
- package/src/migrations/__tests__/007-proxy-iframe-src.test.ts +112 -0
- package/src/shadow/prepare.ts +16 -4
- package/src/templates/app/html-index.tsx.tmpl +9 -1
- 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;IAElB,QAAQ,CAAC,EAAE,OAAO,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,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');
|
|
@@ -20,6 +19,8 @@ const util_broker_app = require('../util/broker/app.js');
|
|
|
20
19
|
const util_openBrowser = require('../util/open-browser.js');
|
|
21
20
|
const commands_build = require('./build.js');
|
|
22
21
|
const commands_tag = require('./tag.js');
|
|
22
|
+
const shadow_metadata = require('../shadow/metadata.js');
|
|
23
|
+
const shadow_prepare = require('../shadow/prepare.js');
|
|
23
24
|
|
|
24
25
|
const _interopDefault = e => e && e.__esModule ? e : { default: e };
|
|
25
26
|
|
|
@@ -63,7 +64,11 @@ 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
|
+
let previewId = meta?.previewId;
|
|
69
|
+
if (!previewId || opts.snapshot) {
|
|
70
|
+
previewId = shadow_prepare.generatePreviewId();
|
|
71
|
+
}
|
|
67
72
|
const previewSlug = util_appName.buildPreviewSlug(consumerName, previewId);
|
|
68
73
|
const previewAppName = util_appName.mfAppName(previewSlug);
|
|
69
74
|
const keyPrefix = `${constants.S3_PREVIEWS_FOLDER}/${previewAppName}/MF`;
|
|
@@ -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;IAElB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,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';
|
|
@@ -18,6 +17,8 @@ import { registerOrUpdateApp } from '../util/broker/app.mjs';
|
|
|
18
17
|
import { openInBrowser } from '../util/open-browser.mjs';
|
|
19
18
|
import { buildCommand } from './build.mjs';
|
|
20
19
|
import { promptForTags } from './tag.mjs';
|
|
20
|
+
import { readMetadata } from '../shadow/metadata.mjs';
|
|
21
|
+
import { generatePreviewId } from '../shadow/prepare.mjs';
|
|
21
22
|
|
|
22
23
|
// Should be run from the consumer app directory — uses process.cwd() to locate it.
|
|
23
24
|
async function deployCommand(opts = {}) {
|
|
@@ -57,7 +58,11 @@ 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
|
+
let previewId = meta?.previewId;
|
|
63
|
+
if (!previewId || opts.snapshot) {
|
|
64
|
+
previewId = generatePreviewId();
|
|
65
|
+
}
|
|
61
66
|
const previewSlug = buildPreviewSlug(consumerName, previewId);
|
|
62
67
|
const previewAppName = mfAppName(previewSlug);
|
|
63
68
|
const keyPrefix = `${S3_PREVIEWS_FOLDER}/${previewAppName}/MF`;
|
package/dist/esm/index.mjs
CHANGED
|
@@ -93,17 +93,14 @@ 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
|
+
.option('--snapshot', 'with --preview: deploy to a fresh previewId without touching the sticky URL — produces a permanent point-in-time link')
|
|
97
98
|
.option('--open', 'open the resulting app/preview URL in the browser on success')
|
|
98
99
|
.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
100
|
.option('--repo-url <url>', 'First deploy only: GitHub repo URL — required when --remote=github. Error if the app already has a git remote.')
|
|
100
101
|
.action(async (opts) => {
|
|
101
102
|
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
|
-
}), {
|
|
103
|
+
await trackDuration(opts.preview ? 'z2h_app_preview_success' : 'z2h_app_deploy_success', () => deployCommand(opts), {
|
|
107
104
|
app_name: getCwdAppName(),
|
|
108
105
|
});
|
|
109
106
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"007-proxy-iframe-src.d.ts","sourceRoot":"","sources":["../../../src/migrations/007-proxy-iframe-src.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAqBvC,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAoCvD"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Backfills the html-embed template's `toProxiedUrl` fix onto apps scaffolded before it
|
|
2
|
+
// existed. Matches any relative `.html?url` import, not just `./page.html` — apps are
|
|
3
|
+
// free to name/nest their embedded HTML file however they like (e.g. `./pages/foo.html`).
|
|
4
|
+
const IFRAME_URL_IMPORT = /import\s+(\w+)\s+from\s+(['"])(\.\/[^'"]+?\.html)\?url\2;?/;
|
|
5
|
+
function toProxiedUrlBlock() {
|
|
6
|
+
return [
|
|
7
|
+
'',
|
|
8
|
+
"const Z2H_MFS_BASE_URL = 'https://bigbrain-zth-mf-assets.bigbrain.me';",
|
|
9
|
+
"const Z2H_ASSETS_PROXY_PREFIX = '/z2h-assets';",
|
|
10
|
+
'',
|
|
11
|
+
'function toProxiedUrl(url: string): string {',
|
|
12
|
+
' if (!url.startsWith(Z2H_MFS_BASE_URL)) return url;',
|
|
13
|
+
' return `${window.location.origin}${Z2H_ASSETS_PROXY_PREFIX}${url.slice(Z2H_MFS_BASE_URL.length)}`;',
|
|
14
|
+
'}',
|
|
15
|
+
].join('\n');
|
|
16
|
+
}
|
|
17
|
+
function proxyIframeSrc(tree) {
|
|
18
|
+
for (const entry of tree.children('.')) {
|
|
19
|
+
if (entry.startsWith('.')) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const indexPath = `${entry}/src/index.tsx`;
|
|
23
|
+
if (!tree.exists(indexPath)) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const source = tree.read(indexPath, 'utf-8');
|
|
27
|
+
if (!source.includes('<iframe') || source.includes('toProxiedUrl')) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const importMatch = source.match(IFRAME_URL_IMPORT);
|
|
31
|
+
if (!importMatch) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const [importLine, varName, , relativePath] = importMatch;
|
|
35
|
+
const htmlPath = `${entry}/src/${relativePath.replace(/^\.\//, '')}`;
|
|
36
|
+
if (!tree.exists(htmlPath)) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const srcAttr = `src={${varName}}`;
|
|
40
|
+
if (!source.includes(srcAttr)) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const withBlock = source.replace(importLine, `${importLine}\n${toProxiedUrlBlock()}`);
|
|
44
|
+
const rewritten = withBlock.replace(srcAttr, `src={toProxiedUrl(${varName})}`);
|
|
45
|
+
tree.write(indexPath, rewritten);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { proxyIframeSrc as default };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"007-proxy-iframe-src.test.d.ts","sourceRoot":"","sources":["../../../../src/migrations/__tests__/007-proxy-iframe-src.test.ts"],"names":[],"mappings":""}
|
|
@@ -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,14 @@ 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
|
+
.option('--snapshot', 'with --preview: deploy to a fresh previewId without touching the sticky URL — produces a permanent point-in-time link')
|
|
103
104
|
.option('--open', 'open the resulting app/preview URL in the browser on success')
|
|
104
105
|
.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
106
|
.option('--repo-url <url>', 'First deploy only: GitHub repo URL — required when --remote=github. Error if the app already has a git remote.')
|
|
106
107
|
.action(async (opts) => {
|
|
107
108
|
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
|
-
}), {
|
|
109
|
+
await util_tracker.trackDuration(opts.preview ? 'z2h_app_preview_success' : 'z2h_app_deploy_success', () => commands_deploy.deployCommand(opts), {
|
|
113
110
|
app_name: util_appName.getCwdAppName(),
|
|
114
111
|
});
|
|
115
112
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"007-proxy-iframe-src.d.ts","sourceRoot":"","sources":["../../src/migrations/007-proxy-iframe-src.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAqBvC,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAoCvD"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: 'Module' } });
|
|
2
|
+
|
|
3
|
+
// Backfills the html-embed template's `toProxiedUrl` fix onto apps scaffolded before it
|
|
4
|
+
// existed. Matches any relative `.html?url` import, not just `./page.html` — apps are
|
|
5
|
+
// free to name/nest their embedded HTML file however they like (e.g. `./pages/foo.html`).
|
|
6
|
+
const IFRAME_URL_IMPORT = /import\s+(\w+)\s+from\s+(['"])(\.\/[^'"]+?\.html)\?url\2;?/;
|
|
7
|
+
function toProxiedUrlBlock() {
|
|
8
|
+
return [
|
|
9
|
+
'',
|
|
10
|
+
"const Z2H_MFS_BASE_URL = 'https://bigbrain-zth-mf-assets.bigbrain.me';",
|
|
11
|
+
"const Z2H_ASSETS_PROXY_PREFIX = '/z2h-assets';",
|
|
12
|
+
'',
|
|
13
|
+
'function toProxiedUrl(url: string): string {',
|
|
14
|
+
' if (!url.startsWith(Z2H_MFS_BASE_URL)) return url;',
|
|
15
|
+
' return `${window.location.origin}${Z2H_ASSETS_PROXY_PREFIX}${url.slice(Z2H_MFS_BASE_URL.length)}`;',
|
|
16
|
+
'}',
|
|
17
|
+
].join('\n');
|
|
18
|
+
}
|
|
19
|
+
function proxyIframeSrc(tree) {
|
|
20
|
+
for (const entry of tree.children('.')) {
|
|
21
|
+
if (entry.startsWith('.')) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const indexPath = `${entry}/src/index.tsx`;
|
|
25
|
+
if (!tree.exists(indexPath)) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const source = tree.read(indexPath, 'utf-8');
|
|
29
|
+
if (!source.includes('<iframe') || source.includes('toProxiedUrl')) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const importMatch = source.match(IFRAME_URL_IMPORT);
|
|
33
|
+
if (!importMatch) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const [importLine, varName, , relativePath] = importMatch;
|
|
37
|
+
const htmlPath = `${entry}/src/${relativePath.replace(/^\.\//, '')}`;
|
|
38
|
+
if (!tree.exists(htmlPath)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const srcAttr = `src={${varName}}`;
|
|
42
|
+
if (!source.includes(srcAttr)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const withBlock = source.replace(importLine, `${importLine}\n${toProxiedUrlBlock()}`);
|
|
46
|
+
const rewritten = withBlock.replace(srcAttr, `src={toProxiedUrl(${varName})}`);
|
|
47
|
+
tree.write(indexPath, rewritten);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
exports.default = proxyIframeSrc;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"007-proxy-iframe-src.test.d.ts","sourceRoot":"","sources":["../../../src/migrations/__tests__/007-proxy-iframe-src.test.ts"],"names":[],"mappings":""}
|
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/migrations.json
CHANGED
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
"version": "0.15.3",
|
|
30
30
|
"description": "Remove the start script (z2h-cli dev) from consumer package.json — dev is gone, superseded by deploy --preview",
|
|
31
31
|
"migrate": "./dist/migrations/006-remove-start-script"
|
|
32
|
+
},
|
|
33
|
+
"proxy-iframe-src": {
|
|
34
|
+
"version": "0.19.0",
|
|
35
|
+
"description": "Rewrite html-embed apps' iframe src to go through the host's same-origin /z2h-assets proxy instead of the raw CDN bucket URL",
|
|
36
|
+
"migrate": "./dist/migrations/007-proxy-iframe-src"
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
39
|
}
|
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,58 @@ 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);
|
|
134
|
+
|
|
135
|
+
await deployCommand({ preview: true });
|
|
123
136
|
|
|
124
|
-
|
|
137
|
+
const call = vi.mocked(brokerProvider.brokerCredentialProvider).mock.calls[0][0];
|
|
138
|
+
expect(call.previewId).toBe(STUB_PREVIEW_ID);
|
|
125
139
|
|
|
126
|
-
const
|
|
127
|
-
|
|
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
|
|
|
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
|
+
|
|
160
178
|
it('passes a full CDN URL (not a relative path) as publicUrl to the build', async () => {
|
|
161
|
-
await
|
|
179
|
+
await writeMetadata(STUB_PREVIEW_ID);
|
|
180
|
+
await deployCommand({ preview: true });
|
|
162
181
|
|
|
163
|
-
const previewAppName = `mf-${buildPreviewSlug('my-app',
|
|
182
|
+
const previewAppName = `mf-${buildPreviewSlug('my-app', STUB_PREVIEW_ID)}`;
|
|
164
183
|
expect(buildCommand).toHaveBeenCalledWith(
|
|
165
184
|
expect.objectContaining({
|
|
166
185
|
publicUrl: `https://bigbrain-zth-mf-assets.bigbrain.me/previews/${previewAppName}/MF`,
|
|
@@ -178,9 +197,10 @@ describe('deploy --preview', () => {
|
|
|
178
197
|
});
|
|
179
198
|
|
|
180
199
|
it('--open opens the previewUrl in the browser on success', async () => {
|
|
181
|
-
await
|
|
200
|
+
await writeMetadata(STUB_PREVIEW_ID);
|
|
201
|
+
await deployCommand({ preview: true, open: true });
|
|
182
202
|
|
|
183
|
-
const previewSlug = buildPreviewSlug('my-app',
|
|
203
|
+
const previewSlug = buildPreviewSlug('my-app', STUB_PREVIEW_ID);
|
|
184
204
|
expect(openInBrowser).toHaveBeenCalledWith(`https://bigbrain.me/bigbrain-vibe/${previewSlug}`);
|
|
185
205
|
});
|
|
186
206
|
|
|
@@ -195,19 +215,20 @@ describe('deploy --preview', () => {
|
|
|
195
215
|
});
|
|
196
216
|
|
|
197
217
|
it('--json emits a machine-readable payload with an app-shaped previewUrl (not ?z2h_preview=)', async () => {
|
|
218
|
+
await writeMetadata(STUB_PREVIEW_ID);
|
|
198
219
|
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
199
220
|
setOutputMode('json');
|
|
200
221
|
|
|
201
|
-
await deployCommand({ preview: true
|
|
222
|
+
await deployCommand({ preview: true });
|
|
202
223
|
|
|
203
224
|
const jsonLine = writeSpy.mock.calls.map(c => c[0]).find(line => typeof line === 'string' && line.includes('"ok"'));
|
|
204
225
|
expect(jsonLine).toBeDefined();
|
|
205
226
|
const payload = JSON.parse(jsonLine as string);
|
|
206
|
-
const previewSlug = buildPreviewSlug('my-app',
|
|
227
|
+
const previewSlug = buildPreviewSlug('my-app', STUB_PREVIEW_ID);
|
|
207
228
|
expect(payload).toMatchObject({
|
|
208
229
|
ok: true,
|
|
209
230
|
appName: 'mf-my-app',
|
|
210
|
-
previewId:
|
|
231
|
+
previewId: STUB_PREVIEW_ID,
|
|
211
232
|
previewAppName: `mf-${previewSlug}`,
|
|
212
233
|
previewUrl: `https://bigbrain.me/bigbrain-vibe/${previewSlug}`,
|
|
213
234
|
});
|
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';
|
|
@@ -24,8 +23,10 @@ import { registerOrUpdateApp } from '../util/broker/app';
|
|
|
24
23
|
import { openInBrowser } from '../util/open-browser';
|
|
25
24
|
import { buildCommand } from './build';
|
|
26
25
|
import { promptForTags } from './tag';
|
|
26
|
+
import { readMetadata } from '../shadow/metadata';
|
|
27
27
|
import type { ConsumerPaths } from '../types';
|
|
28
28
|
import type { ManifestEntry } from '../util/manifest';
|
|
29
|
+
import { generatePreviewId } from '../shadow/prepare';
|
|
29
30
|
|
|
30
31
|
export interface DeployOptions {
|
|
31
32
|
dryRun?: boolean;
|
|
@@ -34,8 +35,8 @@ 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:
|
|
38
|
-
|
|
38
|
+
// With --preview: deploy to a new previewId without touching metadata — the sticky --preview URL stays unchanged.
|
|
39
|
+
snapshot?: boolean;
|
|
39
40
|
open?: boolean;
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -95,7 +96,12 @@ async function buildAndStamp(opts: {
|
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
async function deployPreview({ paths, consumerName, appName, token, opts }: DeployContext): Promise<void> {
|
|
98
|
-
const
|
|
99
|
+
const meta = await readMetadata(paths);
|
|
100
|
+
let previewId = meta?.previewId;
|
|
101
|
+
if (!previewId || opts.snapshot) {
|
|
102
|
+
previewId = generatePreviewId();
|
|
103
|
+
}
|
|
104
|
+
|
|
99
105
|
const previewSlug = buildPreviewSlug(consumerName, previewId);
|
|
100
106
|
const previewAppName = mfAppName(previewSlug);
|
|
101
107
|
const keyPrefix = `${S3_PREVIEWS_FOLDER}/${previewAppName}/MF`;
|
package/src/index.ts
CHANGED
|
@@ -102,10 +102,8 @@ async function main(): Promise<void> {
|
|
|
102
102
|
.command('deploy')
|
|
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
|
-
.option(
|
|
106
|
-
|
|
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'
|
|
108
|
-
)
|
|
105
|
+
.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)')
|
|
106
|
+
.option('--snapshot', 'with --preview: deploy to a fresh previewId without touching the sticky URL — produces a permanent point-in-time link')
|
|
109
107
|
.option('--open', 'open the resulting app/preview URL in the browser on success')
|
|
110
108
|
.option(
|
|
111
109
|
'--remote <type>',
|
|
@@ -118,7 +116,8 @@ async function main(): Promise<void> {
|
|
|
118
116
|
.action(
|
|
119
117
|
async (opts: {
|
|
120
118
|
dryRun?: boolean;
|
|
121
|
-
preview?: boolean
|
|
119
|
+
preview?: boolean;
|
|
120
|
+
snapshot?: boolean;
|
|
122
121
|
open?: boolean;
|
|
123
122
|
remote?: string;
|
|
124
123
|
repoUrl?: string;
|
|
@@ -126,12 +125,7 @@ async function main(): Promise<void> {
|
|
|
126
125
|
try {
|
|
127
126
|
await trackDuration(
|
|
128
127
|
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]),
|
|
128
|
+
() => deployCommand(opts as Parameters<typeof deployCommand>[0]),
|
|
135
129
|
{
|
|
136
130
|
app_name: getCwdAppName(),
|
|
137
131
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Tree } from '@nx/devkit';
|
|
2
|
+
|
|
3
|
+
// Backfills the html-embed template's `toProxiedUrl` fix onto apps scaffolded before it
|
|
4
|
+
// existed. Matches any relative `.html?url` import, not just `./page.html` — apps are
|
|
5
|
+
// free to name/nest their embedded HTML file however they like (e.g. `./pages/foo.html`).
|
|
6
|
+
|
|
7
|
+
const IFRAME_URL_IMPORT = /import\s+(\w+)\s+from\s+(['"])(\.\/[^'"]+?\.html)\?url\2;?/;
|
|
8
|
+
|
|
9
|
+
function toProxiedUrlBlock(): string {
|
|
10
|
+
return [
|
|
11
|
+
'',
|
|
12
|
+
"const Z2H_MFS_BASE_URL = 'https://bigbrain-zth-mf-assets.bigbrain.me';",
|
|
13
|
+
"const Z2H_ASSETS_PROXY_PREFIX = '/z2h-assets';",
|
|
14
|
+
'',
|
|
15
|
+
'function toProxiedUrl(url: string): string {',
|
|
16
|
+
' if (!url.startsWith(Z2H_MFS_BASE_URL)) return url;',
|
|
17
|
+
' return `${window.location.origin}${Z2H_ASSETS_PROXY_PREFIX}${url.slice(Z2H_MFS_BASE_URL.length)}`;',
|
|
18
|
+
'}',
|
|
19
|
+
].join('\n');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function proxyIframeSrc(tree: Tree): void {
|
|
23
|
+
for (const entry of tree.children('.')) {
|
|
24
|
+
if (entry.startsWith('.')) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const indexPath = `${entry}/src/index.tsx`;
|
|
29
|
+
if (!tree.exists(indexPath)) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const source = tree.read(indexPath, 'utf-8')!;
|
|
34
|
+
if (!source.includes('<iframe') || source.includes('toProxiedUrl')) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const importMatch = source.match(IFRAME_URL_IMPORT);
|
|
39
|
+
if (!importMatch) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const [importLine, varName, , relativePath] = importMatch;
|
|
44
|
+
const htmlPath = `${entry}/src/${relativePath.replace(/^\.\//, '')}`;
|
|
45
|
+
if (!tree.exists(htmlPath)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const srcAttr = `src={${varName}}`;
|
|
50
|
+
if (!source.includes(srcAttr)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const withBlock = source.replace(importLine, `${importLine}\n${toProxiedUrlBlock()}`);
|
|
55
|
+
const rewritten = withBlock.replace(srcAttr, `src={toProxiedUrl(${varName})}`);
|
|
56
|
+
tree.write(indexPath, rewritten);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { createTree } from '@nx/devkit/testing';
|
|
3
|
+
import proxyIframeSrc from '../007-proxy-iframe-src';
|
|
4
|
+
import type { Tree } from '@nx/devkit';
|
|
5
|
+
|
|
6
|
+
const UNMIGRATED_INDEX = [
|
|
7
|
+
"import React, { useRef } from 'react';",
|
|
8
|
+
"import pageUrl from './page.html?url';",
|
|
9
|
+
'',
|
|
10
|
+
'export default function App() {',
|
|
11
|
+
' const iframeRef = useRef<HTMLIFrameElement>(null);',
|
|
12
|
+
' return <iframe ref={iframeRef} src={pageUrl} title="App" />;',
|
|
13
|
+
'}',
|
|
14
|
+
'',
|
|
15
|
+
].join('\n');
|
|
16
|
+
|
|
17
|
+
function makeTree(): Tree {
|
|
18
|
+
return createTree();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function writeHtmlEmbedApp(tree: Tree, dir: string, indexSource: string): void {
|
|
22
|
+
tree.write(`${dir}/package.json`, `{"name":"${dir}"}`);
|
|
23
|
+
tree.write(`${dir}/src/page.html`, '<!doctype html><html></html>');
|
|
24
|
+
tree.write(`${dir}/src/index.tsx`, indexSource);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('migration 007 — proxy iframe src', () => {
|
|
28
|
+
it('wraps the iframe src and adds the proxy helper', () => {
|
|
29
|
+
const tree = makeTree();
|
|
30
|
+
writeHtmlEmbedApp(tree, 'my-app', UNMIGRATED_INDEX);
|
|
31
|
+
|
|
32
|
+
proxyIframeSrc(tree);
|
|
33
|
+
|
|
34
|
+
const out = tree.read('my-app/src/index.tsx', 'utf-8')!;
|
|
35
|
+
expect(out).toContain("const Z2H_MFS_BASE_URL = 'https://bigbrain-zth-mf-assets.bigbrain.me';");
|
|
36
|
+
expect(out).toContain("const Z2H_ASSETS_PROXY_PREFIX = '/z2h-assets';");
|
|
37
|
+
expect(out).toContain('function toProxiedUrl(url: string): string {');
|
|
38
|
+
expect(out).toContain('src={toProxiedUrl(pageUrl)}');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('handles a page-url variable named something other than pageUrl', () => {
|
|
42
|
+
const tree = makeTree();
|
|
43
|
+
const source = UNMIGRATED_INDEX.replace(/pageUrl/g, 'businessReviewUrl');
|
|
44
|
+
writeHtmlEmbedApp(tree, 'latam-business-review', source);
|
|
45
|
+
|
|
46
|
+
proxyIframeSrc(tree);
|
|
47
|
+
|
|
48
|
+
const out = tree.read('latam-business-review/src/index.tsx', 'utf-8')!;
|
|
49
|
+
expect(out).toContain('src={toProxiedUrl(businessReviewUrl)}');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('handles an HTML file nested in a subdirectory, not just src/page.html', () => {
|
|
53
|
+
const tree = createTree();
|
|
54
|
+
const source = [
|
|
55
|
+
"import React, { useRef } from 'react';",
|
|
56
|
+
"import businessReviewUrl from './pages/business-review.html?url';",
|
|
57
|
+
'',
|
|
58
|
+
'export default function App() {',
|
|
59
|
+
' const iframeRef = useRef<HTMLIFrameElement>(null);',
|
|
60
|
+
' return <iframe ref={iframeRef} src={businessReviewUrl} title="App" />;',
|
|
61
|
+
'}',
|
|
62
|
+
'',
|
|
63
|
+
].join('\n');
|
|
64
|
+
tree.write('latam-business-review/package.json', '{"name":"latam-business-review"}');
|
|
65
|
+
tree.write('latam-business-review/src/pages/business-review.html', '<!doctype html><html></html>');
|
|
66
|
+
tree.write('latam-business-review/src/index.tsx', source);
|
|
67
|
+
|
|
68
|
+
proxyIframeSrc(tree);
|
|
69
|
+
|
|
70
|
+
const out = tree.read('latam-business-review/src/index.tsx', 'utf-8')!;
|
|
71
|
+
expect(out).toContain('src={toProxiedUrl(businessReviewUrl)}');
|
|
72
|
+
expect(out).toContain("const Z2H_MFS_BASE_URL = 'https://bigbrain-zth-mf-assets.bigbrain.me';");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('is idempotent — a second run on an already-migrated app is a no-op', () => {
|
|
76
|
+
const tree = makeTree();
|
|
77
|
+
writeHtmlEmbedApp(tree, 'my-app', UNMIGRATED_INDEX);
|
|
78
|
+
|
|
79
|
+
proxyIframeSrc(tree);
|
|
80
|
+
const afterFirst = tree.read('my-app/src/index.tsx', 'utf-8')!;
|
|
81
|
+
|
|
82
|
+
proxyIframeSrc(tree);
|
|
83
|
+
expect(tree.read('my-app/src/index.tsx', 'utf-8')!).toBe(afterFirst);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('leaves a default (non-embed) app untouched — no page.html', () => {
|
|
87
|
+
const tree = makeTree();
|
|
88
|
+
tree.write('default-app/package.json', '{"name":"default-app"}');
|
|
89
|
+
tree.write('default-app/src/index.tsx', 'export default function App() { return <div />; }\n');
|
|
90
|
+
|
|
91
|
+
proxyIframeSrc(tree);
|
|
92
|
+
|
|
93
|
+
expect(tree.read('default-app/src/index.tsx', 'utf-8')!).toBe(
|
|
94
|
+
'export default function App() { return <div />; }\n'
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('leaves an html-embed app untouched if it has no iframe tag', () => {
|
|
99
|
+
const tree = makeTree();
|
|
100
|
+
const source = "import pageUrl from './page.html?url';\nexport default () => null;\n";
|
|
101
|
+
writeHtmlEmbedApp(tree, 'weird-app', source);
|
|
102
|
+
|
|
103
|
+
proxyIframeSrc(tree);
|
|
104
|
+
|
|
105
|
+
expect(tree.read('weird-app/src/index.tsx', 'utf-8')!).toBe(source);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('does nothing when the workspace has no apps', () => {
|
|
109
|
+
const tree = makeTree();
|
|
110
|
+
expect(() => proxyIframeSrc(tree)).not.toThrow();
|
|
111
|
+
});
|
|
112
|
+
});
|
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
|
}
|
|
@@ -9,6 +9,14 @@ function getBackendBaseUrl(): string | null {
|
|
|
9
9
|
return base ? base.replace('//', `//${BACKEND_SUBDOMAIN}.`) : null;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
const Z2H_MFS_BASE_URL = 'https://bigbrain-zth-mf-assets.bigbrain.me';
|
|
13
|
+
const Z2H_ASSETS_PROXY_PREFIX = '/z2h-assets';
|
|
14
|
+
|
|
15
|
+
function toProxiedUrl(url: string): string {
|
|
16
|
+
if (!url.startsWith(Z2H_MFS_BASE_URL)) return url;
|
|
17
|
+
return `${window.location.origin}${Z2H_ASSETS_PROXY_PREFIX}${url.slice(Z2H_MFS_BASE_URL.length)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
export default function __APP_COMPONENT__() {
|
|
13
21
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
14
22
|
useEffect(() => {
|
|
@@ -25,7 +33,7 @@ export default function __APP_COMPONENT__() {
|
|
|
25
33
|
return (
|
|
26
34
|
<iframe
|
|
27
35
|
ref={iframeRef}
|
|
28
|
-
src={pageUrl}
|
|
36
|
+
src={toProxiedUrl(pageUrl)}
|
|
29
37
|
title="App"
|
|
30
38
|
style={{ width: '100vw', height: '100vh', border: 'none', display: 'block' }}
|
|
31
39
|
/>
|