@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.
Files changed (157) hide show
  1. package/README.md +16 -0
  2. package/bin/z2h-cli.js +2 -0
  3. package/dist/commands/build.d.ts +5 -0
  4. package/dist/commands/build.d.ts.map +1 -0
  5. package/dist/commands/build.js +37 -0
  6. package/dist/commands/clean.d.ts +5 -0
  7. package/dist/commands/clean.d.ts.map +1 -0
  8. package/dist/commands/clean.js +24 -0
  9. package/dist/commands/deploy.d.ts +6 -0
  10. package/dist/commands/deploy.d.ts.map +1 -0
  11. package/dist/commands/deploy.js +92 -0
  12. package/dist/commands/dev.d.ts +5 -0
  13. package/dist/commands/dev.d.ts.map +1 -0
  14. package/dist/commands/dev.js +58 -0
  15. package/dist/constants.d.ts +25 -0
  16. package/dist/constants.d.ts.map +1 -0
  17. package/dist/constants.js +67 -0
  18. package/dist/esm/commands/build.d.ts +5 -0
  19. package/dist/esm/commands/build.d.ts.map +1 -0
  20. package/dist/esm/commands/build.mjs +35 -0
  21. package/dist/esm/commands/clean.d.ts +5 -0
  22. package/dist/esm/commands/clean.d.ts.map +1 -0
  23. package/dist/esm/commands/clean.mjs +22 -0
  24. package/dist/esm/commands/deploy.d.ts +6 -0
  25. package/dist/esm/commands/deploy.d.ts.map +1 -0
  26. package/dist/esm/commands/deploy.mjs +86 -0
  27. package/dist/esm/commands/dev.d.ts +5 -0
  28. package/dist/esm/commands/dev.d.ts.map +1 -0
  29. package/dist/esm/commands/dev.mjs +56 -0
  30. package/dist/esm/constants.d.ts +25 -0
  31. package/dist/esm/constants.d.ts.map +1 -0
  32. package/dist/esm/constants.mjs +57 -0
  33. package/dist/esm/index.d.ts +2 -0
  34. package/dist/esm/index.d.ts.map +1 -0
  35. package/dist/esm/index.mjs +86 -0
  36. package/dist/esm/shadow/consumer-pkg.d.ts +24 -0
  37. package/dist/esm/shadow/consumer-pkg.d.ts.map +1 -0
  38. package/dist/esm/shadow/consumer-pkg.mjs +81 -0
  39. package/dist/esm/shadow/gitignore.d.ts +7 -0
  40. package/dist/esm/shadow/gitignore.d.ts.map +1 -0
  41. package/dist/esm/shadow/gitignore.mjs +29 -0
  42. package/dist/esm/shadow/merge-package-json.d.ts +16 -0
  43. package/dist/esm/shadow/merge-package-json.d.ts.map +1 -0
  44. package/dist/esm/shadow/merge-package-json.mjs +58 -0
  45. package/dist/esm/shadow/metadata.d.ts +4 -0
  46. package/dist/esm/shadow/metadata.d.ts.map +1 -0
  47. package/dist/esm/shadow/metadata.mjs +18 -0
  48. package/dist/esm/shadow/paths.d.ts +3 -0
  49. package/dist/esm/shadow/paths.d.ts.map +1 -0
  50. package/dist/esm/shadow/paths.mjs +25 -0
  51. package/dist/esm/shadow/port.d.ts +7 -0
  52. package/dist/esm/shadow/port.d.ts.map +1 -0
  53. package/dist/esm/shadow/port.mjs +34 -0
  54. package/dist/esm/shadow/prepare.d.ts +3 -0
  55. package/dist/esm/shadow/prepare.d.ts.map +1 -0
  56. package/dist/esm/shadow/prepare.mjs +60 -0
  57. package/dist/esm/shadow/scaffold.d.ts +7 -0
  58. package/dist/esm/shadow/scaffold.d.ts.map +1 -0
  59. package/dist/esm/shadow/scaffold.mjs +112 -0
  60. package/dist/esm/shadow/sync.d.ts +9 -0
  61. package/dist/esm/shadow/sync.d.ts.map +1 -0
  62. package/dist/esm/shadow/sync.mjs +29 -0
  63. package/dist/esm/shadow/yarnrc.d.ts +10 -0
  64. package/dist/esm/shadow/yarnrc.d.ts.map +1 -0
  65. package/dist/esm/shadow/yarnrc.mjs +27 -0
  66. package/dist/esm/types.d.ts +53 -0
  67. package/dist/esm/types.d.ts.map +1 -0
  68. package/dist/esm/types.mjs +1 -0
  69. package/dist/esm/util/app-name.d.ts +12 -0
  70. package/dist/esm/util/app-name.d.ts.map +1 -0
  71. package/dist/esm/util/app-name.mjs +17 -0
  72. package/dist/esm/util/logger.d.ts +22 -0
  73. package/dist/esm/util/logger.d.ts.map +1 -0
  74. package/dist/esm/util/logger.mjs +59 -0
  75. package/dist/esm/util/manifest.d.ts +31 -0
  76. package/dist/esm/util/manifest.d.ts.map +1 -0
  77. package/dist/esm/util/manifest.mjs +31 -0
  78. package/dist/esm/util/s3/client.d.ts +11 -0
  79. package/dist/esm/util/s3/client.d.ts.map +1 -0
  80. package/dist/esm/util/s3/client.mjs +38 -0
  81. package/dist/esm/util/s3/upload.d.ts +2 -0
  82. package/dist/esm/util/s3/upload.d.ts.map +1 -0
  83. package/dist/esm/util/s3/upload.mjs +32 -0
  84. package/dist/index.d.ts +2 -0
  85. package/dist/index.d.ts.map +1 -0
  86. package/dist/index.js +86 -0
  87. package/dist/shadow/consumer-pkg.d.ts +24 -0
  88. package/dist/shadow/consumer-pkg.d.ts.map +1 -0
  89. package/dist/shadow/consumer-pkg.js +84 -0
  90. package/dist/shadow/gitignore.d.ts +7 -0
  91. package/dist/shadow/gitignore.d.ts.map +1 -0
  92. package/dist/shadow/gitignore.js +35 -0
  93. package/dist/shadow/merge-package-json.d.ts +16 -0
  94. package/dist/shadow/merge-package-json.d.ts.map +1 -0
  95. package/dist/shadow/merge-package-json.js +66 -0
  96. package/dist/shadow/metadata.d.ts +4 -0
  97. package/dist/shadow/metadata.d.ts.map +1 -0
  98. package/dist/shadow/metadata.js +21 -0
  99. package/dist/shadow/paths.d.ts +3 -0
  100. package/dist/shadow/paths.d.ts.map +1 -0
  101. package/dist/shadow/paths.js +31 -0
  102. package/dist/shadow/port.d.ts +7 -0
  103. package/dist/shadow/port.d.ts.map +1 -0
  104. package/dist/shadow/port.js +41 -0
  105. package/dist/shadow/prepare.d.ts +3 -0
  106. package/dist/shadow/prepare.d.ts.map +1 -0
  107. package/dist/shadow/prepare.js +62 -0
  108. package/dist/shadow/scaffold.d.ts +7 -0
  109. package/dist/shadow/scaffold.d.ts.map +1 -0
  110. package/dist/shadow/scaffold.js +118 -0
  111. package/dist/shadow/sync.d.ts +9 -0
  112. package/dist/shadow/sync.d.ts.map +1 -0
  113. package/dist/shadow/sync.js +31 -0
  114. package/dist/shadow/yarnrc.d.ts +10 -0
  115. package/dist/shadow/yarnrc.d.ts.map +1 -0
  116. package/dist/shadow/yarnrc.js +33 -0
  117. package/dist/types.d.ts +53 -0
  118. package/dist/types.d.ts.map +1 -0
  119. package/dist/types.js +1 -0
  120. package/dist/util/app-name.d.ts +12 -0
  121. package/dist/util/app-name.d.ts.map +1 -0
  122. package/dist/util/app-name.js +20 -0
  123. package/dist/util/logger.d.ts +22 -0
  124. package/dist/util/logger.d.ts.map +1 -0
  125. package/dist/util/logger.js +71 -0
  126. package/dist/util/manifest.d.ts +31 -0
  127. package/dist/util/manifest.d.ts.map +1 -0
  128. package/dist/util/manifest.js +36 -0
  129. package/dist/util/s3/client.d.ts +11 -0
  130. package/dist/util/s3/client.d.ts.map +1 -0
  131. package/dist/util/s3/client.js +48 -0
  132. package/dist/util/s3/upload.d.ts +2 -0
  133. package/dist/util/s3/upload.d.ts.map +1 -0
  134. package/dist/util/s3/upload.js +39 -0
  135. package/package.json +56 -0
  136. package/src/commands/build.ts +42 -0
  137. package/src/commands/clean.ts +26 -0
  138. package/src/commands/deploy.ts +114 -0
  139. package/src/commands/dev.ts +64 -0
  140. package/src/constants.ts +63 -0
  141. package/src/index.ts +90 -0
  142. package/src/shadow/consumer-pkg.ts +100 -0
  143. package/src/shadow/gitignore.ts +30 -0
  144. package/src/shadow/merge-package-json.ts +62 -0
  145. package/src/shadow/metadata.ts +17 -0
  146. package/src/shadow/paths.ts +24 -0
  147. package/src/shadow/port.ts +34 -0
  148. package/src/shadow/prepare.ts +68 -0
  149. package/src/shadow/scaffold.ts +120 -0
  150. package/src/shadow/sync.ts +30 -0
  151. package/src/shadow/yarnrc.ts +29 -0
  152. package/src/types.ts +60 -0
  153. package/src/util/app-name.ts +16 -0
  154. package/src/util/logger.ts +64 -0
  155. package/src/util/manifest.ts +65 -0
  156. package/src/util/s3/client.ts +40 -0
  157. 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.d.ts.map
@@ -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,3 @@
1
+ import type { ConsumerPaths } from '../types';
2
+ export declare function resolveConsumerPaths(consumerDir: string): ConsumerPaths;
3
+ //# sourceMappingURL=paths.d.ts.map
@@ -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,3 @@
1
+ import type { PreparedShadow } from '../types';
2
+ export declare function prepareShadow(): Promise<PreparedShadow>;
3
+ //# sourceMappingURL=prepare.d.ts.map
@@ -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"}