@mondaydotcomorg/z2h-cli 0.1.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/README.md +16 -0
- package/bin/z2h-cli.js +2 -0
- package/dist/commands/build.d.ts +5 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +37 -0
- package/dist/commands/clean.d.ts +5 -0
- package/dist/commands/clean.d.ts.map +1 -0
- package/dist/commands/clean.js +24 -0
- package/dist/commands/deploy.d.ts +6 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +92 -0
- package/dist/commands/dev.d.ts +5 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +58 -0
- package/dist/constants.d.ts +25 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +67 -0
- package/dist/esm/commands/build.d.ts +5 -0
- package/dist/esm/commands/build.d.ts.map +1 -0
- package/dist/esm/commands/build.mjs +35 -0
- package/dist/esm/commands/clean.d.ts +5 -0
- package/dist/esm/commands/clean.d.ts.map +1 -0
- package/dist/esm/commands/clean.mjs +22 -0
- package/dist/esm/commands/deploy.d.ts +6 -0
- package/dist/esm/commands/deploy.d.ts.map +1 -0
- package/dist/esm/commands/deploy.mjs +86 -0
- package/dist/esm/commands/dev.d.ts +5 -0
- package/dist/esm/commands/dev.d.ts.map +1 -0
- package/dist/esm/commands/dev.mjs +56 -0
- package/dist/esm/constants.d.ts +25 -0
- package/dist/esm/constants.d.ts.map +1 -0
- package/dist/esm/constants.mjs +57 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.mjs +86 -0
- package/dist/esm/shadow/consumer-pkg.d.ts +24 -0
- package/dist/esm/shadow/consumer-pkg.d.ts.map +1 -0
- package/dist/esm/shadow/consumer-pkg.mjs +81 -0
- package/dist/esm/shadow/gitignore.d.ts +7 -0
- package/dist/esm/shadow/gitignore.d.ts.map +1 -0
- package/dist/esm/shadow/gitignore.mjs +29 -0
- package/dist/esm/shadow/merge-package-json.d.ts +16 -0
- package/dist/esm/shadow/merge-package-json.d.ts.map +1 -0
- package/dist/esm/shadow/merge-package-json.mjs +58 -0
- package/dist/esm/shadow/metadata.d.ts +4 -0
- package/dist/esm/shadow/metadata.d.ts.map +1 -0
- package/dist/esm/shadow/metadata.mjs +18 -0
- package/dist/esm/shadow/paths.d.ts +3 -0
- package/dist/esm/shadow/paths.d.ts.map +1 -0
- package/dist/esm/shadow/paths.mjs +25 -0
- package/dist/esm/shadow/port.d.ts +7 -0
- package/dist/esm/shadow/port.d.ts.map +1 -0
- package/dist/esm/shadow/port.mjs +34 -0
- package/dist/esm/shadow/prepare.d.ts +3 -0
- package/dist/esm/shadow/prepare.d.ts.map +1 -0
- package/dist/esm/shadow/prepare.mjs +60 -0
- package/dist/esm/shadow/scaffold.d.ts +7 -0
- package/dist/esm/shadow/scaffold.d.ts.map +1 -0
- package/dist/esm/shadow/scaffold.mjs +112 -0
- package/dist/esm/shadow/sync.d.ts +9 -0
- package/dist/esm/shadow/sync.d.ts.map +1 -0
- package/dist/esm/shadow/sync.mjs +29 -0
- package/dist/esm/shadow/yarnrc.d.ts +10 -0
- package/dist/esm/shadow/yarnrc.d.ts.map +1 -0
- package/dist/esm/shadow/yarnrc.mjs +27 -0
- package/dist/esm/types.d.ts +53 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.mjs +1 -0
- package/dist/esm/util/app-name.d.ts +12 -0
- package/dist/esm/util/app-name.d.ts.map +1 -0
- package/dist/esm/util/app-name.mjs +17 -0
- package/dist/esm/util/logger.d.ts +22 -0
- package/dist/esm/util/logger.d.ts.map +1 -0
- package/dist/esm/util/logger.mjs +59 -0
- package/dist/esm/util/manifest.d.ts +31 -0
- package/dist/esm/util/manifest.d.ts.map +1 -0
- package/dist/esm/util/manifest.mjs +31 -0
- package/dist/esm/util/s3/client.d.ts +11 -0
- package/dist/esm/util/s3/client.d.ts.map +1 -0
- package/dist/esm/util/s3/client.mjs +38 -0
- package/dist/esm/util/s3/upload.d.ts +2 -0
- package/dist/esm/util/s3/upload.d.ts.map +1 -0
- package/dist/esm/util/s3/upload.mjs +32 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -0
- package/dist/shadow/consumer-pkg.d.ts +24 -0
- package/dist/shadow/consumer-pkg.d.ts.map +1 -0
- package/dist/shadow/consumer-pkg.js +84 -0
- package/dist/shadow/gitignore.d.ts +7 -0
- package/dist/shadow/gitignore.d.ts.map +1 -0
- package/dist/shadow/gitignore.js +35 -0
- package/dist/shadow/merge-package-json.d.ts +16 -0
- package/dist/shadow/merge-package-json.d.ts.map +1 -0
- package/dist/shadow/merge-package-json.js +66 -0
- package/dist/shadow/metadata.d.ts +4 -0
- package/dist/shadow/metadata.d.ts.map +1 -0
- package/dist/shadow/metadata.js +21 -0
- package/dist/shadow/paths.d.ts +3 -0
- package/dist/shadow/paths.d.ts.map +1 -0
- package/dist/shadow/paths.js +31 -0
- package/dist/shadow/port.d.ts +7 -0
- package/dist/shadow/port.d.ts.map +1 -0
- package/dist/shadow/port.js +41 -0
- package/dist/shadow/prepare.d.ts +3 -0
- package/dist/shadow/prepare.d.ts.map +1 -0
- package/dist/shadow/prepare.js +62 -0
- package/dist/shadow/scaffold.d.ts +7 -0
- package/dist/shadow/scaffold.d.ts.map +1 -0
- package/dist/shadow/scaffold.js +118 -0
- package/dist/shadow/sync.d.ts +9 -0
- package/dist/shadow/sync.d.ts.map +1 -0
- package/dist/shadow/sync.js +31 -0
- package/dist/shadow/yarnrc.d.ts +10 -0
- package/dist/shadow/yarnrc.d.ts.map +1 -0
- package/dist/shadow/yarnrc.js +33 -0
- package/dist/types.d.ts +53 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/util/app-name.d.ts +12 -0
- package/dist/util/app-name.d.ts.map +1 -0
- package/dist/util/app-name.js +20 -0
- package/dist/util/logger.d.ts +22 -0
- package/dist/util/logger.d.ts.map +1 -0
- package/dist/util/logger.js +71 -0
- package/dist/util/manifest.d.ts +31 -0
- package/dist/util/manifest.d.ts.map +1 -0
- package/dist/util/manifest.js +36 -0
- package/dist/util/s3/client.d.ts +11 -0
- package/dist/util/s3/client.d.ts.map +1 -0
- package/dist/util/s3/client.js +48 -0
- package/dist/util/s3/upload.d.ts +2 -0
- package/dist/util/s3/upload.d.ts.map +1 -0
- package/dist/util/s3/upload.js +39 -0
- package/package.json +56 -0
- package/src/commands/build.ts +42 -0
- package/src/commands/clean.ts +26 -0
- package/src/commands/deploy.ts +114 -0
- package/src/commands/dev.ts +64 -0
- package/src/constants.ts +63 -0
- package/src/index.ts +90 -0
- package/src/shadow/consumer-pkg.ts +100 -0
- package/src/shadow/gitignore.ts +30 -0
- package/src/shadow/merge-package-json.ts +62 -0
- package/src/shadow/metadata.ts +17 -0
- package/src/shadow/paths.ts +24 -0
- package/src/shadow/port.ts +34 -0
- package/src/shadow/prepare.ts +68 -0
- package/src/shadow/scaffold.ts +120 -0
- package/src/shadow/sync.ts +30 -0
- package/src/shadow/yarnrc.ts +29 -0
- package/src/types.ts +60 -0
- package/src/util/app-name.ts +16 -0
- package/src/util/logger.ts +64 -0
- package/src/util/manifest.ts +65 -0
- package/src/util/s3/client.ts +40 -0
- package/src/util/s3/upload.ts +41 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-file constants. Narrowly-used literals (templates, prefixes used in
|
|
3
|
+
* one place) stay local to their file to keep this index small.
|
|
4
|
+
*/
|
|
5
|
+
// ─────────── Shadow dir layout ───────────
|
|
6
|
+
const SHADOW_DIR_NAME = '.zth';
|
|
7
|
+
// ─────────── Consumer pkg enforcement ──────
|
|
8
|
+
/**
|
|
9
|
+
* Protected runtime deps that MUST resolve to a single instance across the
|
|
10
|
+
* wrapper (.zth) and the consumer's src. Declared as peerDependencies in
|
|
11
|
+
* the shadow pkg so yarn hoists them from the workspace root — which is
|
|
12
|
+
* why we enforce their presence in the consumer's own package.json.
|
|
13
|
+
*/
|
|
14
|
+
const PROTECTED_DEPS = {
|
|
15
|
+
react: '^18.2.0',
|
|
16
|
+
'react-dom': '^18.2.0',
|
|
17
|
+
'react-error-boundary': '^3.1.3',
|
|
18
|
+
'@mondaydotcomorg/trident-runtime': '^1.24.1',
|
|
19
|
+
'@vibe/core': '^4.0.1',
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Protected dev deps. Same idempotent treatment as PROTECTED_DEPS but live
|
|
23
|
+
* in the consumer's `devDependencies`. These give the LLM-authored consumer
|
|
24
|
+
* code access to monday.com platform types out of the box.
|
|
25
|
+
*/
|
|
26
|
+
const PROTECTED_DEV_DEPS = {
|
|
27
|
+
'@mondaydotcomorg/mf-api-types': '^0.1.1',
|
|
28
|
+
'@mondaydotcomorg/bigbrain-types': '^0.3.3',
|
|
29
|
+
};
|
|
30
|
+
// ─────────── Shadow pkg template ───────────
|
|
31
|
+
// TODO — Add trident mf-types / bigbrain-types opt-in.
|
|
32
|
+
const WORKSPACE_DEV_DEPS = {
|
|
33
|
+
'@mondaydotcomorg/trident-toolkit': '^2.9.25',
|
|
34
|
+
'@types/react': '^18.0.14',
|
|
35
|
+
'@types/react-dom': '^18.0.5',
|
|
36
|
+
typescript: '^5.7.2',
|
|
37
|
+
};
|
|
38
|
+
const SHADOW_SCRIPTS = {
|
|
39
|
+
start: 'trident start',
|
|
40
|
+
build: 'trident build',
|
|
41
|
+
};
|
|
42
|
+
// ─────────── Auto-allocated dev port range ────
|
|
43
|
+
// High enough to dodge common dev defaults (3000 Next, 5173 Vite, 8080).
|
|
44
|
+
// Collisions within the 7k-port pool are rare for POC; if trident can't
|
|
45
|
+
// bind, the user sees a clear bind error from the dev server and re-runs.
|
|
46
|
+
const MIN_PORT = 13000;
|
|
47
|
+
const MAX_PORT = 19999;
|
|
48
|
+
// ─────────── Gitignore entries auto-appended to the consumer's file ────
|
|
49
|
+
const GITIGNORE_ENTRIES = ['.zth/'];
|
|
50
|
+
// ─────────── .yarnrc.yml written to new consumer apps ────
|
|
51
|
+
const YARNRC_LINES = [
|
|
52
|
+
'nodeLinker: node-modules',
|
|
53
|
+
"npmAuthToken: '${NPM_TOKEN}'",
|
|
54
|
+
"npmRegistryServer: 'https://registry.npmjs.org/'",
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
export { GITIGNORE_ENTRIES, MAX_PORT, MIN_PORT, PROTECTED_DEPS, PROTECTED_DEV_DEPS, SHADOW_DIR_NAME, SHADOW_SCRIPTS, WORKSPACE_DEV_DEPS, YARNRC_LINES };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { devCommand } from './commands/dev.mjs';
|
|
3
|
+
import { buildCommand } from './commands/build.mjs';
|
|
4
|
+
import { cleanCommand } from './commands/clean.mjs';
|
|
5
|
+
import { deployCommand } from './commands/deploy.mjs';
|
|
6
|
+
import { errorLog, fail } from './util/logger.mjs';
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program
|
|
11
|
+
.name('z2h-cli')
|
|
12
|
+
.description([
|
|
13
|
+
'Zero-to-Hero CLI — ship LLM-generated React apps as Trident MFs.',
|
|
14
|
+
'',
|
|
15
|
+
'Each consumer dir is self-contained: a shadow at <consumer>/.zth/',
|
|
16
|
+
'wraps the consumer source as a Trident MF. Run from inside any',
|
|
17
|
+
'consumer dir (the one with your src/index.tsx).',
|
|
18
|
+
'',
|
|
19
|
+
'Exit codes:',
|
|
20
|
+
' 0 success',
|
|
21
|
+
' 1 user-resolvable error (bad cwd, invalid package.json, lock held)',
|
|
22
|
+
' 2 system error (yarn install or trident build failed)',
|
|
23
|
+
].join('\n'))
|
|
24
|
+
.version('0.0.0');
|
|
25
|
+
program
|
|
26
|
+
.command('build')
|
|
27
|
+
.description('Build the consumer into its .zth/ shadow and copy artifacts to ./build/')
|
|
28
|
+
.option('--json', 'emit structured JSON on stdout')
|
|
29
|
+
.action(async (opts) => {
|
|
30
|
+
try {
|
|
31
|
+
await buildCommand(opts);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
// fail() emits a structured JSON error on stdout in --json mode and
|
|
35
|
+
// a red line on stderr in human mode, then exits. errorLog alone is
|
|
36
|
+
// silent in JSON mode, which leaves machine consumers with no signal.
|
|
37
|
+
fail(`build failed: ${err.message}`, { command: 'build' }, 2);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
program
|
|
41
|
+
.command('dev')
|
|
42
|
+
.description('Start the Trident dev server against .zth/ with native Vite HMR')
|
|
43
|
+
.option('--json', 'emit structured JSON on stdout')
|
|
44
|
+
.action(async (opts) => {
|
|
45
|
+
try {
|
|
46
|
+
await devCommand(opts);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
fail(`dev failed: ${err.message}`, { command: 'dev' }, 2);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
program
|
|
53
|
+
.command('clean')
|
|
54
|
+
.description('Delete <consumer>/.zth/ and <consumer>/build/')
|
|
55
|
+
.option('--json', 'emit structured JSON on stdout')
|
|
56
|
+
.action(async (opts) => {
|
|
57
|
+
try {
|
|
58
|
+
await cleanCommand(opts);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
fail(`clean failed: ${err.message}`, { command: 'clean' }, 2);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
program
|
|
65
|
+
.command('deploy')
|
|
66
|
+
.description('Build, upload artifacts to S3, and update the Z2H manifest')
|
|
67
|
+
.option('--dry-run', 'print what would happen without uploading anything')
|
|
68
|
+
.option('--json', 'emit structured JSON on stdout')
|
|
69
|
+
.action(async (opts) => {
|
|
70
|
+
try {
|
|
71
|
+
await deployCommand(opts);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
fail(`deploy failed: ${err.message}`, { command: 'deploy' }, 2);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
await program.parseAsync(process.argv);
|
|
78
|
+
}
|
|
79
|
+
void main()
|
|
80
|
+
.then(() => {
|
|
81
|
+
process.exit(0);
|
|
82
|
+
})
|
|
83
|
+
.catch((err) => {
|
|
84
|
+
errorLog(`main failed: ${err.message}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ConsumerPkg } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Return the consumer's `workspaces` field extended with `.zth`. Handles
|
|
4
|
+
* all three shapes: missing, array form, or object form (`{ packages }`).
|
|
5
|
+
* Always extends — never replaces the user's existing entries.
|
|
6
|
+
*/
|
|
7
|
+
export declare function ensureConsumerWorkspaces(pkg: ConsumerPkg): {
|
|
8
|
+
changed: boolean;
|
|
9
|
+
workspaces: ConsumerPkg['workspaces'];
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Idempotently reshape the consumer's package.json so that:
|
|
13
|
+
* - it is marked `"private": true`
|
|
14
|
+
* - `.zth` is declared as a yarn workspace member (yarn will hoist)
|
|
15
|
+
* - the protected runtime deps are present at compatible versions
|
|
16
|
+
* - the protected dev deps are present at compatible versions
|
|
17
|
+
*
|
|
18
|
+
* Always extends — never overrides existing fields.
|
|
19
|
+
*/
|
|
20
|
+
export declare function ensureConsumerPkg(pkgPath: string, pkg: ConsumerPkg): Promise<{
|
|
21
|
+
changed: boolean;
|
|
22
|
+
pkg: ConsumerPkg;
|
|
23
|
+
}>;
|
|
24
|
+
//# sourceMappingURL=consumer-pkg.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consumer-pkg.d.ts","sourceRoot":"","sources":["../../../src/shadow/consumer-pkg.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,WAAW,GAAG;IAC1D,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;CACvC,CAsBA;AAqBD;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,WAAW,GACf,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,WAAW,CAAA;CAAE,CAAC,CAgCjD"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { writeJson } from 'fs-extra';
|
|
2
|
+
import { SHADOW_DIR_NAME, PROTECTED_DEPS, PROTECTED_DEV_DEPS } from '../constants.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Return the consumer's `workspaces` field extended with `.zth`. Handles
|
|
6
|
+
* all three shapes: missing, array form, or object form (`{ packages }`).
|
|
7
|
+
* Always extends — never replaces the user's existing entries.
|
|
8
|
+
*/
|
|
9
|
+
function ensureConsumerWorkspaces(pkg) {
|
|
10
|
+
const { workspaces } = pkg;
|
|
11
|
+
if (Array.isArray(workspaces)) {
|
|
12
|
+
if (workspaces.includes(SHADOW_DIR_NAME)) {
|
|
13
|
+
return { changed: false, workspaces };
|
|
14
|
+
}
|
|
15
|
+
return { changed: true, workspaces: [...workspaces, SHADOW_DIR_NAME] };
|
|
16
|
+
}
|
|
17
|
+
if (workspaces && typeof workspaces === 'object') {
|
|
18
|
+
const pkgs = workspaces.packages ?? [];
|
|
19
|
+
if (pkgs.includes(SHADOW_DIR_NAME)) {
|
|
20
|
+
return { changed: false, workspaces };
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
changed: true,
|
|
24
|
+
workspaces: { ...workspaces, packages: [...pkgs, SHADOW_DIR_NAME] },
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return { changed: true, workspaces: [SHADOW_DIR_NAME] };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Extend a deps map with the protected entries; only fills in missing ones,
|
|
31
|
+
* never overrides versions the consumer already pinned.
|
|
32
|
+
*/
|
|
33
|
+
function applyProtected(current, protectedMap) {
|
|
34
|
+
const next = { ...(current ?? {}) };
|
|
35
|
+
let changed = false;
|
|
36
|
+
for (const [name, version] of Object.entries(protectedMap)) {
|
|
37
|
+
if (!next[name]) {
|
|
38
|
+
next[name] = version;
|
|
39
|
+
changed = true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { changed, next };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Idempotently reshape the consumer's package.json so that:
|
|
46
|
+
* - it is marked `"private": true`
|
|
47
|
+
* - `.zth` is declared as a yarn workspace member (yarn will hoist)
|
|
48
|
+
* - the protected runtime deps are present at compatible versions
|
|
49
|
+
* - the protected dev deps are present at compatible versions
|
|
50
|
+
*
|
|
51
|
+
* Always extends — never overrides existing fields.
|
|
52
|
+
*/
|
|
53
|
+
async function ensureConsumerPkg(pkgPath, pkg) {
|
|
54
|
+
let changed = false;
|
|
55
|
+
const next = { ...pkg };
|
|
56
|
+
if (next.private !== true) {
|
|
57
|
+
next.private = true;
|
|
58
|
+
changed = true;
|
|
59
|
+
}
|
|
60
|
+
const ws = ensureConsumerWorkspaces(next);
|
|
61
|
+
if (ws.changed) {
|
|
62
|
+
next.workspaces = ws.workspaces;
|
|
63
|
+
changed = true;
|
|
64
|
+
}
|
|
65
|
+
const deps = applyProtected(next.dependencies, PROTECTED_DEPS);
|
|
66
|
+
if (deps.changed) {
|
|
67
|
+
next.dependencies = deps.next;
|
|
68
|
+
changed = true;
|
|
69
|
+
}
|
|
70
|
+
const devDeps = applyProtected(next.devDependencies, PROTECTED_DEV_DEPS);
|
|
71
|
+
if (devDeps.changed) {
|
|
72
|
+
next.devDependencies = devDeps.next;
|
|
73
|
+
changed = true;
|
|
74
|
+
}
|
|
75
|
+
if (changed) {
|
|
76
|
+
await writeJson(pkgPath, next, { spaces: 2 });
|
|
77
|
+
}
|
|
78
|
+
return { changed, pkg: next };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { ensureConsumerPkg, ensureConsumerWorkspaces };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotently append our shadow-dir entries to the consumer's .gitignore.
|
|
3
|
+
* Never modifies existing entries; only appends missing ones. Creates the
|
|
4
|
+
* file if absent.
|
|
5
|
+
*/
|
|
6
|
+
export declare function ensureGitignore(consumerDir: string): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=gitignore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitignore.d.ts","sourceRoot":"","sources":["../../../src/shadow/gitignore.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBxE"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { pathExists, readFile, writeFile } from 'fs-extra';
|
|
3
|
+
import { GITIGNORE_ENTRIES } from '../constants.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Idempotently append our shadow-dir entries to the consumer's .gitignore.
|
|
7
|
+
* Never modifies existing entries; only appends missing ones. Creates the
|
|
8
|
+
* file if absent.
|
|
9
|
+
*/
|
|
10
|
+
async function ensureGitignore(consumerDir) {
|
|
11
|
+
const gitignorePath = path.join(consumerDir, '.gitignore');
|
|
12
|
+
let body = '';
|
|
13
|
+
if (await pathExists(gitignorePath)) {
|
|
14
|
+
body = await readFile(gitignorePath, 'utf8');
|
|
15
|
+
}
|
|
16
|
+
const existing = new Set(body
|
|
17
|
+
.split('\n')
|
|
18
|
+
.map(l => l.trim())
|
|
19
|
+
.filter(Boolean));
|
|
20
|
+
const missing = GITIGNORE_ENTRIES.filter(e => !existing.has(e));
|
|
21
|
+
if (missing.length === 0) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const prefix = body.length && !body.endsWith('\n') ? '\n' : '';
|
|
25
|
+
const block = `${prefix}\n# z2h-cli\n${missing.join('\n')}\n`;
|
|
26
|
+
await writeFile(gitignorePath, body + block);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { ensureGitignore };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ConsumerPkg, ShadowPkg } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Build the shadow package.json. Declares the shared runtime deps as
|
|
4
|
+
* peerDependencies — yarn resolves them from the workspace root (the
|
|
5
|
+
* consumer's package.json), guaranteeing a single React instance across
|
|
6
|
+
* the wrapper and consumer src.
|
|
7
|
+
*/
|
|
8
|
+
export declare function buildShadowPkg(consumerPkg: ConsumerPkg): ShadowPkg;
|
|
9
|
+
/**
|
|
10
|
+
* Hash the consumer's declared deps. This is the sole input that decides
|
|
11
|
+
* whether `yarn install` needs to run — the shadow package.json is
|
|
12
|
+
* regenerated from a CLI-owned template every run and doesn't participate.
|
|
13
|
+
*/
|
|
14
|
+
export declare function hashConsumerDeps(consumer: ConsumerPkg): string;
|
|
15
|
+
export declare function writeShadowPkg(shadowPkgPath: string, shadow: ShadowPkg): Promise<void>;
|
|
16
|
+
//# sourceMappingURL=merge-package-json.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge-package-json.d.ts","sourceRoot":"","sources":["../../../src/shadow/merge-package-json.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAEvD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,WAAW,GAAG,SAAS,CAalE;AAgBD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,WAAW,GAAG,MAAM,CAW9D;AAED,wBAAsB,cAAc,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5F"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { writeJson } from 'fs-extra';
|
|
3
|
+
import { SHADOW_SCRIPTS, PROTECTED_DEPS, WORKSPACE_DEV_DEPS } from '../constants.mjs';
|
|
4
|
+
import { mfAppName } from '../util/app-name.mjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Build the shadow package.json. Declares the shared runtime deps as
|
|
8
|
+
* peerDependencies — yarn resolves them from the workspace root (the
|
|
9
|
+
* consumer's package.json), guaranteeing a single React instance across
|
|
10
|
+
* the wrapper and consumer src.
|
|
11
|
+
*/
|
|
12
|
+
function buildShadowPkg(consumerPkg) {
|
|
13
|
+
// Use the `@trident/` scope so trident-toolkit strips it and the emitted
|
|
14
|
+
// bundle becomes `MF-mf-<appName>`, matching the URL the host MF routes to.
|
|
15
|
+
// The scope also keeps the shadow's name distinct from the consumer's
|
|
16
|
+
// package.json name (same workspace, different identifiers).
|
|
17
|
+
return {
|
|
18
|
+
name: `@trident/${mfAppName(consumerPkg.name)}`,
|
|
19
|
+
version: '0.0.1',
|
|
20
|
+
private: true,
|
|
21
|
+
scripts: SHADOW_SCRIPTS,
|
|
22
|
+
peerDependencies: { ...PROTECTED_DEPS },
|
|
23
|
+
devDependencies: WORKSPACE_DEV_DEPS,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Stable, key-sorted serialization. Two objects with the same keys/values
|
|
28
|
+
* hash identically regardless of insertion order — so re-orderings from
|
|
29
|
+
* yarn's install or ensureConsumerPkg don't trigger spurious reinstalls.
|
|
30
|
+
*/
|
|
31
|
+
function stableStringify(value) {
|
|
32
|
+
return JSON.stringify(value, (_key, v) => {
|
|
33
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
34
|
+
return Object.fromEntries(Object.entries(v).sort(([a], [b]) => a.localeCompare(b)));
|
|
35
|
+
}
|
|
36
|
+
return v;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Hash the consumer's declared deps. This is the sole input that decides
|
|
41
|
+
* whether `yarn install` needs to run — the shadow package.json is
|
|
42
|
+
* regenerated from a CLI-owned template every run and doesn't participate.
|
|
43
|
+
*/
|
|
44
|
+
function hashConsumerDeps(consumer) {
|
|
45
|
+
return crypto
|
|
46
|
+
.createHash('sha256')
|
|
47
|
+
.update(stableStringify({
|
|
48
|
+
deps: consumer.dependencies ?? {},
|
|
49
|
+
devDeps: consumer.devDependencies ?? {},
|
|
50
|
+
peerDeps: consumer.peerDependencies ?? {},
|
|
51
|
+
}))
|
|
52
|
+
.digest('hex');
|
|
53
|
+
}
|
|
54
|
+
async function writeShadowPkg(shadowPkgPath, shadow) {
|
|
55
|
+
await writeJson(shadowPkgPath, shadow, { spaces: 2 });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { buildShadowPkg, hashConsumerDeps, writeShadowPkg };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ConsumerPaths, ShadowMetadata } from '../types';
|
|
2
|
+
export declare function readMetadata(paths: ConsumerPaths): Promise<ShadowMetadata | null>;
|
|
3
|
+
export declare function writeMetadata(paths: ConsumerPaths, meta: ShadowMetadata): Promise<void>;
|
|
4
|
+
//# sourceMappingURL=metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../../../src/shadow/metadata.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE9D,wBAAsB,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CASvF;AAED,wBAAsB,aAAa,CAAC,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAE7F"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { pathExists, readJson, writeJson } from 'fs-extra';
|
|
2
|
+
|
|
3
|
+
async function readMetadata(paths) {
|
|
4
|
+
if (!(await pathExists(paths.shadowMetadata))) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
try {
|
|
8
|
+
return (await readJson(paths.shadowMetadata));
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async function writeMetadata(paths, meta) {
|
|
15
|
+
await writeJson(paths.shadowMetadata, meta, { spaces: 2 });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { readMetadata, writeMetadata };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../../src/shadow/paths.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,CAmBvE"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { SHADOW_DIR_NAME } from '../constants.mjs';
|
|
3
|
+
|
|
4
|
+
function resolveConsumerPaths(consumerDir) {
|
|
5
|
+
const shadowDir = path.join(consumerDir, SHADOW_DIR_NAME);
|
|
6
|
+
return {
|
|
7
|
+
consumerDir,
|
|
8
|
+
consumerPkg: path.join(consumerDir, 'package.json'),
|
|
9
|
+
consumerSrc: path.join(consumerDir, 'src'),
|
|
10
|
+
consumerSrcEntry: path.join(consumerDir, 'src', 'index.tsx'),
|
|
11
|
+
shadowDir,
|
|
12
|
+
shadowPkg: path.join(shadowDir, 'package.json'),
|
|
13
|
+
shadowSrc: path.join(shadowDir, 'src'),
|
|
14
|
+
shadowWrapper: path.join(shadowDir, 'src', 'index.tsx'),
|
|
15
|
+
shadowTridentrc: path.join(shadowDir, '.tridentrc.js'),
|
|
16
|
+
shadowNvmrc: path.join(shadowDir, '.nvmrc'),
|
|
17
|
+
shadowDts: path.join(shadowDir, 'src', 'declaration.d.ts'),
|
|
18
|
+
shadowTsconfig: path.join(shadowDir, 'tsconfig.json'),
|
|
19
|
+
shadowBuild: path.join(shadowDir, 'build'),
|
|
20
|
+
shadowDepsHash: path.join(shadowDir, '.deps-hash'),
|
|
21
|
+
shadowMetadata: path.join(shadowDir, 'metadata.json'),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { resolveConsumerPaths };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tries to bind to `port` on localhost; resolves true if the bind succeeds
|
|
3
|
+
* (port is free) and the listener was cleanly closed.
|
|
4
|
+
*/
|
|
5
|
+
export declare function isPortFree(port: number): Promise<boolean>;
|
|
6
|
+
export declare function pickPort(): Promise<number>;
|
|
7
|
+
//# sourceMappingURL=port.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"port.d.ts","sourceRoot":"","sources":["../../../src/shadow/port.ts"],"names":[],"mappings":"AAOA;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOzD;AAOD,wBAAsB,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,CAQhD"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
import { MIN_PORT, MAX_PORT } from '../constants.mjs';
|
|
3
|
+
|
|
4
|
+
function randomPort() {
|
|
5
|
+
return MIN_PORT + Math.floor(Math.random() * (MAX_PORT - MIN_PORT + 1));
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Tries to bind to `port` on localhost; resolves true if the bind succeeds
|
|
9
|
+
* (port is free) and the listener was cleanly closed.
|
|
10
|
+
*/
|
|
11
|
+
function isPortFree(port) {
|
|
12
|
+
return new Promise(resolve => {
|
|
13
|
+
const server = net.createServer();
|
|
14
|
+
server.once('error', () => resolve(false));
|
|
15
|
+
server.once('listening', () => server.close(() => resolve(true)));
|
|
16
|
+
server.listen(port, '127.0.0.1');
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Picks a random port from the configured range and verifies it's actually
|
|
21
|
+
* free before returning. Tries up to `MAX_PORT_TRIES` candidates.
|
|
22
|
+
*/
|
|
23
|
+
const MAX_PORT_TRIES = 50;
|
|
24
|
+
async function pickPort() {
|
|
25
|
+
for (let i = 0; i < MAX_PORT_TRIES; i++) {
|
|
26
|
+
const p = randomPort();
|
|
27
|
+
if (await isPortFree(p)) {
|
|
28
|
+
return p;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`Could not find a free port in range ${MIN_PORT}-${MAX_PORT} after ${MAX_PORT_TRIES} tries`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { isPortFree, pickPort };
|
|
@@ -0,0 +1 @@
|
|
|
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,IAAI,OAAO,CAAC,cAAc,CAAC,CAiC7D"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { pathExists, readJson, ensureDir } from 'fs-extra';
|
|
2
|
+
import { info } from '../util/logger.mjs';
|
|
3
|
+
import { cleanAppName } from '../util/app-name.mjs';
|
|
4
|
+
import { resolveConsumerPaths } from './paths.mjs';
|
|
5
|
+
import { ensureGitignore } from './gitignore.mjs';
|
|
6
|
+
import { ensureYarnRc } from './yarnrc.mjs';
|
|
7
|
+
import { ensureConsumerPkg } from './consumer-pkg.mjs';
|
|
8
|
+
import { scaffoldShadow } from './scaffold.mjs';
|
|
9
|
+
import { readMetadata, writeMetadata } from './metadata.mjs';
|
|
10
|
+
import { isPortFree, pickPort } from './port.mjs';
|
|
11
|
+
import { syncShadow } from './sync.mjs';
|
|
12
|
+
|
|
13
|
+
async function prepareShadow() {
|
|
14
|
+
const paths = resolveConsumerPaths(process.cwd());
|
|
15
|
+
if (!(await pathExists(paths.consumerPkg))) {
|
|
16
|
+
throw new Error(`No package.json found in ${paths.consumerDir}`);
|
|
17
|
+
}
|
|
18
|
+
if (!(await pathExists(paths.consumerSrcEntry))) {
|
|
19
|
+
throw new Error(`Consumer must have src/index.tsx (in ${paths.consumerDir})`);
|
|
20
|
+
}
|
|
21
|
+
const rawConsumerPkg = (await readJson(paths.consumerPkg));
|
|
22
|
+
if (!rawConsumerPkg.name) {
|
|
23
|
+
throw new Error('Consumer package.json must have a "name" field');
|
|
24
|
+
}
|
|
25
|
+
const appName = cleanAppName(rawConsumerPkg.name);
|
|
26
|
+
const { pkg: consumerPkg, changed: consumerChanged } = await ensureConsumerPkg(paths.consumerPkg, rawConsumerPkg);
|
|
27
|
+
if (consumerChanged) {
|
|
28
|
+
info(`ensured consumer pkg shape at ${paths.consumerPkg}`);
|
|
29
|
+
}
|
|
30
|
+
await ensureGitignore(paths.consumerDir);
|
|
31
|
+
await ensureYarnRc(paths.consumerDir);
|
|
32
|
+
await ensureDir(paths.shadowDir);
|
|
33
|
+
const metadata = await resolvePortAndMetadata(paths, appName);
|
|
34
|
+
const port = metadata.port;
|
|
35
|
+
info(`preparing shadow at ${paths.shadowDir} (port ${port})`);
|
|
36
|
+
await scaffoldShadow(paths, appName, port);
|
|
37
|
+
await syncShadow(paths, consumerPkg);
|
|
38
|
+
return { appName, port, paths };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the dev server port. Prefer the persisted port if it's still free
|
|
42
|
+
* (URL stays stable across restarts). Otherwise pick a fresh one. The
|
|
43
|
+
* orphaned process — if there is one — keeps running on its old port,
|
|
44
|
+
* harmless. No process killing, no PID files.
|
|
45
|
+
*/
|
|
46
|
+
async function resolvePortAndMetadata(paths, appName) {
|
|
47
|
+
const existing = await readMetadata(paths);
|
|
48
|
+
if (existing?.port && (await isPortFree(existing.port))) {
|
|
49
|
+
return existing;
|
|
50
|
+
}
|
|
51
|
+
const next = {
|
|
52
|
+
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
53
|
+
appName,
|
|
54
|
+
port: await pickPort(),
|
|
55
|
+
};
|
|
56
|
+
await writeMetadata(paths, next);
|
|
57
|
+
return next;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { prepareShadow };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ConsumerPaths } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Write every generated file under .zth/. Overwrites on every run so the
|
|
4
|
+
* shadow dir is a deterministic function of the CLI version — no drift.
|
|
5
|
+
*/
|
|
6
|
+
export declare function scaffoldShadow(paths: ConsumerPaths, appName: string, port: number): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=scaffold.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../../../src/shadow/scaffold.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAqG9C;;;GAGG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAYvG"}
|