@mondaydotcomorg/z2h-cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/__tests__/create-workspace.test.d.ts +2 -0
- package/dist/commands/__tests__/create-workspace.test.d.ts.map +1 -0
- package/dist/commands/__tests__/create.test.d.ts +2 -0
- package/dist/commands/__tests__/create.test.d.ts.map +1 -0
- package/dist/commands/build.d.ts +1 -0
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +5 -1
- package/dist/commands/create-workspace.d.ts +5 -0
- package/dist/commands/create-workspace.d.ts.map +1 -0
- package/dist/commands/create-workspace.js +81 -0
- package/dist/commands/create.d.ts +7 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +104 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +13 -9
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +29 -0
- package/dist/constants.d.ts +1 -14
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +11 -34
- package/dist/esm/commands/__tests__/create-workspace.test.d.ts +2 -0
- package/dist/esm/commands/__tests__/create-workspace.test.d.ts.map +1 -0
- package/dist/esm/commands/__tests__/create.test.d.ts +2 -0
- package/dist/esm/commands/__tests__/create.test.d.ts.map +1 -0
- package/dist/esm/commands/build.d.ts +1 -0
- package/dist/esm/commands/build.d.ts.map +1 -1
- package/dist/esm/commands/build.mjs +5 -1
- package/dist/esm/commands/create-workspace.d.ts +5 -0
- package/dist/esm/commands/create-workspace.d.ts.map +1 -0
- package/dist/esm/commands/create-workspace.mjs +75 -0
- package/dist/esm/commands/create.d.ts +7 -0
- package/dist/esm/commands/create.d.ts.map +1 -0
- package/dist/esm/commands/create.mjs +98 -0
- package/dist/esm/commands/deploy.d.ts.map +1 -1
- package/dist/esm/commands/deploy.mjs +12 -9
- package/dist/esm/commands/dev.d.ts.map +1 -1
- package/dist/esm/commands/dev.mjs +25 -1
- package/dist/esm/constants.d.ts +1 -14
- package/dist/esm/constants.d.ts.map +1 -1
- package/dist/esm/constants.mjs +11 -32
- package/dist/esm/index.mjs +28 -0
- package/dist/esm/shadow/__tests__/consumer-pkg.test.d.ts +2 -0
- package/dist/esm/shadow/__tests__/consumer-pkg.test.d.ts.map +1 -0
- package/dist/esm/shadow/__tests__/merge-package-json.test.d.ts +2 -0
- package/dist/esm/shadow/__tests__/merge-package-json.test.d.ts.map +1 -0
- package/dist/esm/shadow/consumer-pkg.d.ts +5 -15
- package/dist/esm/shadow/consumer-pkg.d.ts.map +1 -1
- package/dist/esm/shadow/consumer-pkg.mjs +11 -72
- package/dist/esm/shadow/merge-package-json.d.ts +10 -4
- package/dist/esm/shadow/merge-package-json.d.ts.map +1 -1
- package/dist/esm/shadow/merge-package-json.mjs +12 -11
- package/dist/esm/shadow/scaffold.d.ts.map +1 -1
- package/dist/esm/shadow/scaffold.mjs +5 -0
- package/dist/esm/shadow/sync.mjs +2 -2
- package/dist/esm/templates/app/claude-settings.mjs +23 -0
- package/dist/esm/types.d.ts +0 -1
- package/dist/esm/types.d.ts.map +1 -1
- package/dist/esm/util/__tests__/templates.test.d.ts +2 -0
- package/dist/esm/util/__tests__/templates.test.d.ts.map +1 -0
- package/dist/esm/util/__tests__/workspace-templates-shipped.test.d.ts +2 -0
- package/dist/esm/util/__tests__/workspace-templates-shipped.test.d.ts.map +1 -0
- package/dist/esm/util/__tests__/workspace.test.d.ts +2 -0
- package/dist/esm/util/__tests__/workspace.test.d.ts.map +1 -0
- package/dist/esm/util/manifest.d.ts +1 -0
- package/dist/esm/util/manifest.d.ts.map +1 -1
- package/dist/esm/util/s3/upload.mjs +1 -1
- package/dist/esm/util/templates.d.ts +22 -0
- package/dist/esm/util/templates.d.ts.map +1 -0
- package/dist/esm/util/templates.mjs +36 -0
- package/dist/esm/util/workspace.d.ts +20 -0
- package/dist/esm/util/workspace.d.ts.map +1 -0
- package/dist/esm/util/workspace.mjs +46 -0
- package/dist/index.js +28 -0
- package/dist/shadow/__tests__/consumer-pkg.test.d.ts +2 -0
- package/dist/shadow/__tests__/consumer-pkg.test.d.ts.map +1 -0
- package/dist/shadow/__tests__/merge-package-json.test.d.ts +2 -0
- package/dist/shadow/__tests__/merge-package-json.test.d.ts.map +1 -0
- package/dist/shadow/consumer-pkg.d.ts +5 -15
- package/dist/shadow/consumer-pkg.d.ts.map +1 -1
- package/dist/shadow/consumer-pkg.js +10 -72
- package/dist/shadow/merge-package-json.d.ts +10 -4
- package/dist/shadow/merge-package-json.d.ts.map +1 -1
- package/dist/shadow/merge-package-json.js +11 -10
- package/dist/shadow/scaffold.d.ts.map +1 -1
- package/dist/shadow/scaffold.js +5 -0
- package/dist/shadow/sync.js +2 -2
- package/dist/templates/app/claude-settings.js +27 -0
- package/dist/types.d.ts +0 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/util/__tests__/templates.test.d.ts +2 -0
- package/dist/util/__tests__/templates.test.d.ts.map +1 -0
- package/dist/util/__tests__/workspace-templates-shipped.test.d.ts +2 -0
- package/dist/util/__tests__/workspace-templates-shipped.test.d.ts.map +1 -0
- package/dist/util/__tests__/workspace.test.d.ts +2 -0
- package/dist/util/__tests__/workspace.test.d.ts.map +1 -0
- package/dist/util/manifest.d.ts +1 -0
- package/dist/util/manifest.d.ts.map +1 -1
- package/dist/util/s3/upload.js +1 -1
- package/dist/util/templates.d.ts +22 -0
- package/dist/util/templates.d.ts.map +1 -0
- package/dist/util/templates.js +45 -0
- package/dist/util/workspace.d.ts +20 -0
- package/dist/util/workspace.d.ts.map +1 -0
- package/dist/util/workspace.js +57 -0
- package/package.json +5 -3
- package/src/commands/__tests__/create-workspace.test.ts +78 -0
- package/src/commands/__tests__/create.test.ts +114 -0
- package/src/commands/build.ts +6 -1
- package/src/commands/create-workspace.ts +86 -0
- package/src/commands/create.ts +121 -0
- package/src/commands/deploy.ts +15 -11
- package/src/commands/dev.ts +26 -1
- package/src/constants.ts +11 -34
- package/src/index.ts +28 -0
- package/src/shadow/__tests__/consumer-pkg.test.ts +56 -0
- package/src/shadow/__tests__/merge-package-json.test.ts +44 -0
- package/src/shadow/consumer-pkg.ts +10 -88
- package/src/shadow/merge-package-json.ts +12 -11
- package/src/shadow/scaffold.ts +5 -0
- package/src/shadow/sync.ts +2 -2
- package/src/templates/app/CLAUDE.md +72 -0
- package/src/templates/app/claude-settings.json +19 -0
- package/src/templates/app/default-index.tsx.tmpl +21 -0
- package/src/templates/app/gitignore +4 -0
- package/src/templates/app/html-index.tsx.tmpl +21 -0
- package/src/templates/app/package.json.tmpl +13 -0
- package/src/templates/workspace/README.md +39 -0
- package/src/templates/workspace/gitignore +23 -0
- package/src/templates/workspace/npmrc +3 -0
- package/src/templates/workspace/nvmrc +1 -0
- package/src/templates/workspace/package.json.tmpl +23 -0
- package/src/templates/workspace/yarn-releases/yarn-4.5.1.cjs +934 -0
- package/src/templates/workspace/yarnlock +0 -0
- package/src/templates/workspace/yarnrc.yml +7 -0
- package/src/types.ts +4 -1
- package/src/util/__tests__/templates.test.ts +27 -0
- package/src/util/__tests__/workspace-templates-shipped.test.ts +35 -0
- package/src/util/__tests__/workspace.test.ts +65 -0
- package/src/util/manifest.ts +1 -0
- package/src/util/s3/upload.ts +1 -1
- package/src/util/templates.ts +37 -0
- package/src/util/workspace.ts +52 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const WORKSPACE_DIR_NAME = "zth-projects";
|
|
2
|
+
export declare const WORKSPACE_MARKER_FIELD = "z2hWorkspace";
|
|
3
|
+
/**
|
|
4
|
+
* The Z2H workspace home is hardcoded to `~/zth-projects/` for users.
|
|
5
|
+
* Tests override via the `Z2H_WORKSPACE_HOME` env var so they can use a
|
|
6
|
+
* tmp dir without touching the user's real home.
|
|
7
|
+
*/
|
|
8
|
+
export declare function getWorkspaceHome(): string;
|
|
9
|
+
/**
|
|
10
|
+
* Returns true iff `<dir>/package.json` exists and has `"z2hWorkspace": true`.
|
|
11
|
+
* Any read or parse error returns false (defensive — callers route to a
|
|
12
|
+
* helpful message via assertWorkspace).
|
|
13
|
+
*/
|
|
14
|
+
export declare function isZ2hWorkspace(dir: string): Promise<boolean>;
|
|
15
|
+
/**
|
|
16
|
+
* Resolves to the workspace home path if the marker is present; otherwise
|
|
17
|
+
* throws an Error pointing the user at `z2h-cli create-workspace`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function assertWorkspace(): Promise<string>;
|
|
20
|
+
//# sourceMappingURL=workspace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../src/util/workspace.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AACjD,eAAO,MAAM,sBAAsB,iBAAiB,CAAC;AAErD;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAMD;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAWlE;AAED;;;GAGG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CASvD"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const fsExtra = require('fs-extra');
|
|
6
|
+
|
|
7
|
+
const _interopDefault = e => e && e.__esModule ? e : { default: e };
|
|
8
|
+
|
|
9
|
+
const path__default = /*#__PURE__*/_interopDefault(path);
|
|
10
|
+
const os__default = /*#__PURE__*/_interopDefault(os);
|
|
11
|
+
|
|
12
|
+
const WORKSPACE_DIR_NAME = 'zth-projects';
|
|
13
|
+
const WORKSPACE_MARKER_FIELD = 'z2hWorkspace';
|
|
14
|
+
/**
|
|
15
|
+
* The Z2H workspace home is hardcoded to `~/zth-projects/` for users.
|
|
16
|
+
* Tests override via the `Z2H_WORKSPACE_HOME` env var so they can use a
|
|
17
|
+
* tmp dir without touching the user's real home.
|
|
18
|
+
*/
|
|
19
|
+
function getWorkspaceHome() {
|
|
20
|
+
return process.env.Z2H_WORKSPACE_HOME ?? path__default.default.join(os__default.default.homedir(), WORKSPACE_DIR_NAME);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Returns true iff `<dir>/package.json` exists and has `"z2hWorkspace": true`.
|
|
24
|
+
* Any read or parse error returns false (defensive — callers route to a
|
|
25
|
+
* helpful message via assertWorkspace).
|
|
26
|
+
*/
|
|
27
|
+
async function isZ2hWorkspace(dir) {
|
|
28
|
+
const pkgPath = path__default.default.join(dir, 'package.json');
|
|
29
|
+
if (!(await fsExtra.pathExists(pkgPath))) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const pkg = (await fsExtra.readJson(pkgPath));
|
|
34
|
+
return pkg[WORKSPACE_MARKER_FIELD] === true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolves to the workspace home path if the marker is present; otherwise
|
|
42
|
+
* throws an Error pointing the user at `z2h-cli create-workspace`.
|
|
43
|
+
*/
|
|
44
|
+
async function assertWorkspace() {
|
|
45
|
+
const home = getWorkspaceHome();
|
|
46
|
+
if (await isZ2hWorkspace(home)) {
|
|
47
|
+
return home;
|
|
48
|
+
}
|
|
49
|
+
throw new Error(`No Z2H workspace at ${home}. Run "z2h-cli create-workspace" first ` +
|
|
50
|
+
`(or set Z2H_WORKSPACE_HOME if you keep the workspace elsewhere).`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
exports.WORKSPACE_DIR_NAME = WORKSPACE_DIR_NAME;
|
|
54
|
+
exports.WORKSPACE_MARKER_FIELD = WORKSPACE_MARKER_FIELD;
|
|
55
|
+
exports.assertWorkspace = assertWorkspace;
|
|
56
|
+
exports.getWorkspaceHome = getWorkspaceHome;
|
|
57
|
+
exports.isZ2hWorkspace = isZ2hWorkspace;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mondaydotcomorg/z2h-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"bin": "./bin/z2h-cli.js",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
]
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
|
-
"test": "
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest",
|
|
24
25
|
"lint": "trident-library lint",
|
|
25
26
|
"build": "trident-library build",
|
|
26
27
|
"watch": "trident-library build -w"
|
|
@@ -46,7 +47,8 @@
|
|
|
46
47
|
"@types/fs-extra": "^11.0.4",
|
|
47
48
|
"@types/mime-types": "^2.1.4",
|
|
48
49
|
"@types/node": "^22.19.0",
|
|
49
|
-
"typescript": "^5.7.2"
|
|
50
|
+
"typescript": "^5.7.2",
|
|
51
|
+
"vitest": "^2.1.4"
|
|
50
52
|
},
|
|
51
53
|
"repository": {
|
|
52
54
|
"type": "git",
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { mkdtemp, rm, writeJson, readJson, ensureDir, pathExists } from 'fs-extra';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { execa } from 'execa';
|
|
7
|
+
import { createWorkspaceCommand } from '../create-workspace';
|
|
8
|
+
|
|
9
|
+
// Mock execa so `yarn install` doesn't actually run.
|
|
10
|
+
vi.mock('execa', () => ({
|
|
11
|
+
execa: vi.fn().mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 }),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
describe('create-workspace', () => {
|
|
15
|
+
let tmp: string;
|
|
16
|
+
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
tmp = await mkdtemp(path.join(os.tmpdir(), 'z2h-cw-'));
|
|
19
|
+
process.env.Z2H_WORKSPACE_HOME = tmp;
|
|
20
|
+
vi.mocked(execa).mockClear();
|
|
21
|
+
// After mkdtemp the dir exists and is empty; we want absent for the
|
|
22
|
+
// "happy path" tests, so remove it.
|
|
23
|
+
await rm(tmp, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
delete process.env.Z2H_WORKSPACE_HOME;
|
|
28
|
+
await rm(tmp, { recursive: true, force: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('creates package.json with the marker, gitignore, README, then runs yarn install', async () => {
|
|
32
|
+
await createWorkspaceCommand({});
|
|
33
|
+
|
|
34
|
+
const pkgPath = path.join(tmp, 'package.json');
|
|
35
|
+
expect(await pathExists(pkgPath)).toBe(true);
|
|
36
|
+
const pkg = await readJson(pkgPath);
|
|
37
|
+
expect(pkg.z2hWorkspace).toBe(true);
|
|
38
|
+
expect(pkg.workspaces).toEqual(['*', '*/.zth']);
|
|
39
|
+
expect(pkg.private).toBe(true);
|
|
40
|
+
expect(pkg.dependencies).toBeDefined();
|
|
41
|
+
expect(pkg.devDependencies).toBeDefined();
|
|
42
|
+
|
|
43
|
+
expect(await pathExists(path.join(tmp, '.gitignore'))).toBe(true);
|
|
44
|
+
expect(await pathExists(path.join(tmp, '.yarnrc.yml'))).toBe(true);
|
|
45
|
+
expect(await pathExists(path.join(tmp, '.nvmrc'))).toBe(true);
|
|
46
|
+
expect(await pathExists(path.join(tmp, '.npmrc'))).toBe(true);
|
|
47
|
+
expect(await pathExists(path.join(tmp, 'README.md'))).toBe(true);
|
|
48
|
+
|
|
49
|
+
expect(execa).toHaveBeenCalledWith(
|
|
50
|
+
'yarn',
|
|
51
|
+
expect.arrayContaining(['install']),
|
|
52
|
+
expect.objectContaining({ cwd: tmp })
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('refuses if the target dir is non-empty', async () => {
|
|
57
|
+
await ensureDir(tmp);
|
|
58
|
+
await writeJson(path.join(tmp, 'somefile.json'), { hello: 'world' });
|
|
59
|
+
|
|
60
|
+
await expect(createWorkspaceCommand({})).rejects.toThrow(/non-empty/);
|
|
61
|
+
expect(execa).not.toHaveBeenCalled();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('substitutes the current z2h-cli version into the workspace template', async () => {
|
|
65
|
+
await createWorkspaceCommand({});
|
|
66
|
+
|
|
67
|
+
const pkg = await readJson(path.join(tmp, 'package.json'));
|
|
68
|
+
const cliDep = pkg.devDependencies['@mondaydotcomorg/z2h-cli'];
|
|
69
|
+
expect(cliDep).toBeTruthy();
|
|
70
|
+
expect(cliDep).not.toBe('__Z2H_CLI_VERSION__'); // should be substituted
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('ships the yarn berry release inline so any `yarn` invocation runs 4.x without corepack', async () => {
|
|
74
|
+
await createWorkspaceCommand({});
|
|
75
|
+
|
|
76
|
+
expect(await pathExists(path.join(tmp, '.yarn', 'releases', 'yarn-4.5.1.cjs'))).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { mkdtemp, rm, writeJson, readJson, ensureDir, pathExists, writeFile } from 'fs-extra';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { execa } from 'execa';
|
|
7
|
+
import { createCommand } from '../create';
|
|
8
|
+
import * as manifest from '../../util/manifest';
|
|
9
|
+
|
|
10
|
+
vi.mock('execa', () => ({
|
|
11
|
+
execa: vi.fn().mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 }),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.spyOn(manifest, 'readManifest').mockResolvedValue({});
|
|
15
|
+
|
|
16
|
+
async function makeWorkspace(tmp: string): Promise<void> {
|
|
17
|
+
await ensureDir(tmp);
|
|
18
|
+
await writeJson(path.join(tmp, 'package.json'), {
|
|
19
|
+
name: '@mondaydotcomorg/zth-projects-workspace',
|
|
20
|
+
private: true,
|
|
21
|
+
z2hWorkspace: true,
|
|
22
|
+
workspaces: ['*', '*/.zth'],
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('create', () => {
|
|
27
|
+
let tmp: string;
|
|
28
|
+
|
|
29
|
+
beforeEach(async () => {
|
|
30
|
+
tmp = await mkdtemp(path.join(os.tmpdir(), 'z2h-c-'));
|
|
31
|
+
process.env.Z2H_WORKSPACE_HOME = tmp;
|
|
32
|
+
vi.mocked(execa).mockClear();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(async () => {
|
|
36
|
+
delete process.env.Z2H_WORKSPACE_HOME;
|
|
37
|
+
await rm(tmp, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('refuses when there is no workspace marker', async () => {
|
|
41
|
+
await expect(createCommand('demo-app', {})).rejects.toThrow(/create-workspace/);
|
|
42
|
+
expect(execa).not.toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('rejects invalid app names', async () => {
|
|
46
|
+
await makeWorkspace(tmp);
|
|
47
|
+
await expect(createCommand('Invalid_Name', {})).rejects.toThrow(/lowercase kebab-case/);
|
|
48
|
+
await expect(createCommand('1starts-with-digit', {})).rejects.toThrow(/lowercase kebab-case/);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('refuses when the target app dir already exists and is non-empty', async () => {
|
|
52
|
+
await makeWorkspace(tmp);
|
|
53
|
+
const appDir = path.join(tmp, 'demo-app');
|
|
54
|
+
await ensureDir(appDir);
|
|
55
|
+
await writeFile(path.join(appDir, 'something.txt'), 'hi');
|
|
56
|
+
await expect(createCommand('demo-app', {})).rejects.toThrow(/non-empty/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('scaffolds an app with the default React template', async () => {
|
|
60
|
+
await makeWorkspace(tmp);
|
|
61
|
+
await createCommand('demo-app', {});
|
|
62
|
+
|
|
63
|
+
const appDir = path.join(tmp, 'demo-app');
|
|
64
|
+
expect(await pathExists(path.join(appDir, 'package.json'))).toBe(true);
|
|
65
|
+
expect(await pathExists(path.join(appDir, 'src', 'index.tsx'))).toBe(true);
|
|
66
|
+
expect(await pathExists(path.join(appDir, '.gitignore'))).toBe(true);
|
|
67
|
+
expect(await pathExists(path.join(appDir, '.claude', 'settings.json'))).toBe(true);
|
|
68
|
+
expect(await pathExists(path.join(appDir, 'CLAUDE.md'))).toBe(true);
|
|
69
|
+
|
|
70
|
+
const pkg = await readJson(path.join(appDir, 'package.json'));
|
|
71
|
+
expect(pkg.name).toBe('demo-app');
|
|
72
|
+
expect(pkg.private).toBe(true);
|
|
73
|
+
expect(pkg.scripts.start).toBe('z2h-cli dev');
|
|
74
|
+
|
|
75
|
+
const indexSrc = (await import('node:fs/promises')).readFile;
|
|
76
|
+
const content = await indexSrc(path.join(appDir, 'src', 'index.tsx'), 'utf8');
|
|
77
|
+
expect(content).toContain('export default function DemoApp()');
|
|
78
|
+
expect(content).not.toContain('__APP_COMPONENT__');
|
|
79
|
+
|
|
80
|
+
expect(execa).toHaveBeenCalledWith(
|
|
81
|
+
'yarn',
|
|
82
|
+
expect.arrayContaining(['install']),
|
|
83
|
+
expect.objectContaining({ cwd: tmp })
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('scaffolds an html-embed app and copies the html file', async () => {
|
|
88
|
+
await makeWorkspace(tmp);
|
|
89
|
+
const htmlPath = path.join(tmp, 'page.html');
|
|
90
|
+
await writeFile(htmlPath, '<html><body>Hi</body></html>');
|
|
91
|
+
|
|
92
|
+
await createCommand('html-app', { template: 'html-embed', htmlFile: htmlPath });
|
|
93
|
+
|
|
94
|
+
const appDir = path.join(tmp, 'html-app');
|
|
95
|
+
expect(await pathExists(path.join(appDir, 'src', 'page.html'))).toBe(true);
|
|
96
|
+
const indexContent = await (
|
|
97
|
+
await import('node:fs/promises')
|
|
98
|
+
).readFile(path.join(appDir, 'src', 'index.tsx'), 'utf8');
|
|
99
|
+
expect(indexContent).toContain("import pageUrl from './page.html?url'");
|
|
100
|
+
expect(indexContent).toContain('export default function HtmlApp()');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('rejects unknown templates', async () => {
|
|
104
|
+
await makeWorkspace(tmp);
|
|
105
|
+
await expect(createCommand('demo', { template: 'nonexistent' })).rejects.toThrow(/unknown template/i);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('rejects --html-file when the file does not exist', async () => {
|
|
109
|
+
await makeWorkspace(tmp);
|
|
110
|
+
await expect(createCommand('demo', { template: 'html-embed', htmlFile: '/no/such/file.html' })).rejects.toThrow(
|
|
111
|
+
/html file/i
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
});
|
package/src/commands/build.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { prepareShadow } from '../shadow/prepare';
|
|
|
5
5
|
|
|
6
6
|
export interface BuildOptions {
|
|
7
7
|
json?: boolean;
|
|
8
|
+
publicUrl?: string;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export async function buildCommand(opts: BuildOptions): Promise<void> {
|
|
@@ -27,7 +28,11 @@ export async function buildCommand(opts: BuildOptions): Promise<void> {
|
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
info(`building "${ctx.appName}" via trident`);
|
|
30
|
-
await execa('yarn', ['build'], {
|
|
31
|
+
await execa('yarn', ['build'], {
|
|
32
|
+
cwd: ctx.paths.shadowDir,
|
|
33
|
+
stdio: 'inherit',
|
|
34
|
+
env: { ...process.env, ...(opts.publicUrl ? { PUBLIC_URL: opts.publicUrl } : {}) },
|
|
35
|
+
});
|
|
31
36
|
|
|
32
37
|
if (!(await pathExists(ctx.paths.shadowBuild))) {
|
|
33
38
|
throw new Error(`Expected build output at ${ctx.paths.shadowBuild} — not found.`);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { ensureDir, readJson, writeFile, pathExists, readdir, copy } from 'fs-extra';
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
import { info, output, setOutputMode } from '../util/logger';
|
|
6
|
+
import { getWorkspaceHome } from '../util/workspace';
|
|
7
|
+
import { getTemplatesDir, renderTemplate } from '../util/templates';
|
|
8
|
+
|
|
9
|
+
export interface CreateWorkspaceOptions {
|
|
10
|
+
json?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Read the current z2h-cli version from its own package.json — the version
|
|
15
|
+
* that gets pinned into the workspace's devDependencies. That way the
|
|
16
|
+
* workspace's `@mondaydotcomorg/z2h-cli` always matches the version that
|
|
17
|
+
* bootstrapped it.
|
|
18
|
+
*/
|
|
19
|
+
async function readCurrentCliVersion(): Promise<string> {
|
|
20
|
+
// util/templates.ts already locates `<package-root>/src/templates`. The
|
|
21
|
+
// package's own package.json sits two levels above templates/.
|
|
22
|
+
const templatesDir = getTemplatesDir();
|
|
23
|
+
const pkgPath = path.resolve(templatesDir, '..', '..', 'package.json');
|
|
24
|
+
const pkg = (await readJson(pkgPath)) as { version: string };
|
|
25
|
+
return `^${pkg.version}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function createWorkspaceCommand(opts: CreateWorkspaceOptions): Promise<void> {
|
|
29
|
+
if (opts.json) {
|
|
30
|
+
setOutputMode('json');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const home = getWorkspaceHome();
|
|
34
|
+
const templatesDir = getTemplatesDir();
|
|
35
|
+
const wsTemplatesDir = path.join(templatesDir, 'workspace');
|
|
36
|
+
|
|
37
|
+
// Refuse non-empty target.
|
|
38
|
+
if (await pathExists(home)) {
|
|
39
|
+
const entries = await readdir(home);
|
|
40
|
+
if (entries.length > 0) {
|
|
41
|
+
throw new Error(`${home} is non-empty. Wipe it before running create-workspace.`);
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
await ensureDir(home);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const [cliVersion, pkgRaw] = await Promise.all([
|
|
48
|
+
readCurrentCliVersion(),
|
|
49
|
+
readFile(path.join(wsTemplatesDir, 'package.json.tmpl'), 'utf8'),
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
// Sources have plain names (gitignore, yarnrc.yml, nvmrc, npmrc) so they
|
|
53
|
+
// aren't interpreted as the active config files of *this* package; we
|
|
54
|
+
// rename to dotfiles at the destination.
|
|
55
|
+
//
|
|
56
|
+
// Yarn berry ships inline at .yarn/releases/. The workspace's .yarnrc.yml
|
|
57
|
+
// sets `yarnPath` to point here, so any `yarn` invocation (system classic
|
|
58
|
+
// or otherwise) reads the rc, delegates to this binary, and runs as 4.5.1
|
|
59
|
+
// — no corepack, no global pin needed. Mirrors how bigbrain-z2h ships its
|
|
60
|
+
// own yarn release.
|
|
61
|
+
await Promise.all([
|
|
62
|
+
writeFile(path.join(home, 'package.json'), renderTemplate(pkgRaw, { __Z2H_CLI_VERSION__: cliVersion })),
|
|
63
|
+
copy(path.join(wsTemplatesDir, 'gitignore'), path.join(home, '.gitignore')),
|
|
64
|
+
copy(path.join(wsTemplatesDir, 'yarnrc.yml'), path.join(home, '.yarnrc.yml')),
|
|
65
|
+
copy(path.join(wsTemplatesDir, 'yarnlock'), path.join(home, 'yarn.lock')),
|
|
66
|
+
copy(path.join(wsTemplatesDir, 'nvmrc'), path.join(home, '.nvmrc')),
|
|
67
|
+
copy(path.join(wsTemplatesDir, 'npmrc'), path.join(home, '.npmrc')),
|
|
68
|
+
copy(path.join(wsTemplatesDir, 'README.md'), path.join(home, 'README.md')),
|
|
69
|
+
copy(
|
|
70
|
+
path.join(wsTemplatesDir, 'yarn-releases', 'yarn-4.5.1.cjs'),
|
|
71
|
+
path.join(home, '.yarn', 'releases', 'yarn-4.5.1.cjs')
|
|
72
|
+
),
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
info(`bootstrapping workspace at ${home}`);
|
|
76
|
+
await execa('yarn', ['install'], {
|
|
77
|
+
cwd: home,
|
|
78
|
+
stdio: 'inherit',
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
output(`[z2h-cli] workspace ready at ${home}`, {
|
|
82
|
+
ok: true,
|
|
83
|
+
workspace: home,
|
|
84
|
+
z2hCliVersion: cliVersion,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { ensureDir, pathExists, readdir, writeFile, copy } from 'fs-extra';
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
import { info, output, setOutputMode } from '../util/logger';
|
|
6
|
+
import { assertWorkspace } from '../util/workspace';
|
|
7
|
+
import { getTemplatesDir, renderTemplate, APP_TEMPLATES, type AppTemplate } from '../util/templates';
|
|
8
|
+
import { readManifest } from '../util/manifest';
|
|
9
|
+
import { mfAppName } from '../util/app-name';
|
|
10
|
+
|
|
11
|
+
export interface CreateOptions {
|
|
12
|
+
template?: string;
|
|
13
|
+
htmlFile?: string;
|
|
14
|
+
json?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const APP_NAME_RE = /^[a-z][a-z0-9-]{1,49}$/;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Convert kebab-case to PascalCase. `demo-app` → `DemoApp`.
|
|
21
|
+
*/
|
|
22
|
+
function pascalCase(input: string): string {
|
|
23
|
+
return input
|
|
24
|
+
.split('-')
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
27
|
+
.join('');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns true iff `dir` either does not exist or is an empty directory.
|
|
32
|
+
*/
|
|
33
|
+
async function isDirEmpty(dir: string): Promise<boolean> {
|
|
34
|
+
if (!(await pathExists(dir))) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
const entries = await readdir(dir);
|
|
38
|
+
return entries.length === 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function createCommand(appName: string, opts: CreateOptions): Promise<void> {
|
|
42
|
+
if (opts.json) {
|
|
43
|
+
setOutputMode('json');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Validate workspace.
|
|
47
|
+
const workspace = await assertWorkspace();
|
|
48
|
+
|
|
49
|
+
// Validate app name.
|
|
50
|
+
if (!APP_NAME_RE.test(appName)) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`App name "${appName}" is invalid. Use lowercase kebab-case (a-z, 0-9, -), ` +
|
|
53
|
+
`2-50 chars, starting with a letter.`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Resolve template.
|
|
58
|
+
// `--html-file` implies `--template html-embed` unless the user already picked one.
|
|
59
|
+
const template: AppTemplate = (opts.template ?? (opts.htmlFile ? 'html-embed' : 'default')) as AppTemplate;
|
|
60
|
+
if (!APP_TEMPLATES.includes(template)) {
|
|
61
|
+
throw new Error(`Unknown template "${template}". Available: ${APP_TEMPLATES.join(', ')}.`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (opts.htmlFile && !(await pathExists(opts.htmlFile))) {
|
|
65
|
+
throw new Error(`HTML file does not exist: ${opts.htmlFile}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Validate target dir.
|
|
69
|
+
const appDir = path.join(workspace, appName);
|
|
70
|
+
if (!(await isDirEmpty(appDir))) {
|
|
71
|
+
throw new Error(`${appDir} is non-empty. Pick a different name or wipe it first.`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check for manifest collision (network call — after all local checks).
|
|
75
|
+
const manifest = await readManifest();
|
|
76
|
+
if (manifest[mfAppName(appName)]) {
|
|
77
|
+
throw new Error(`An app named "${appName}" already exists in the Z2H registry. Pick a different name.`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Compute substitutions.
|
|
81
|
+
const vars = {
|
|
82
|
+
__APP_NAME__: appName,
|
|
83
|
+
__APP_COMPONENT__: pascalCase(appName),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const appTemplatesDir = path.join(getTemplatesDir(), 'app');
|
|
87
|
+
await ensureDir(path.join(appDir, 'src'));
|
|
88
|
+
await ensureDir(path.join(appDir, '.claude'));
|
|
89
|
+
|
|
90
|
+
// package.json (templated).
|
|
91
|
+
const pkgRaw = await readFile(path.join(appTemplatesDir, 'package.json.tmpl'), 'utf8');
|
|
92
|
+
await writeFile(path.join(appDir, 'package.json'), renderTemplate(pkgRaw, vars));
|
|
93
|
+
|
|
94
|
+
// src/index.tsx (templated; choice driven by template).
|
|
95
|
+
const indexTemplate = template === 'html-embed' ? 'html-index.tsx.tmpl' : 'default-index.tsx.tmpl';
|
|
96
|
+
const indexRaw = await readFile(path.join(appTemplatesDir, indexTemplate), 'utf8');
|
|
97
|
+
await writeFile(path.join(appDir, 'src', 'index.tsx'), renderTemplate(indexRaw, vars));
|
|
98
|
+
|
|
99
|
+
// Copy verbatim files.
|
|
100
|
+
await copy(path.join(appTemplatesDir, 'gitignore'), path.join(appDir, '.gitignore'));
|
|
101
|
+
await copy(path.join(appTemplatesDir, 'claude-settings.json'), path.join(appDir, '.claude', 'settings.json'));
|
|
102
|
+
await copy(path.join(appTemplatesDir, 'CLAUDE.md'), path.join(appDir, 'CLAUDE.md'));
|
|
103
|
+
|
|
104
|
+
// html-embed: also copy the source HTML.
|
|
105
|
+
if (template === 'html-embed' && opts.htmlFile) {
|
|
106
|
+
await copy(opts.htmlFile, path.join(appDir, 'src', 'page.html'));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
info(`installing into workspace at ${workspace}`);
|
|
110
|
+
await execa('yarn', ['install'], {
|
|
111
|
+
cwd: workspace,
|
|
112
|
+
stdio: 'inherit',
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
output(`[z2h-cli] app ready at ${appDir}`, {
|
|
116
|
+
ok: true,
|
|
117
|
+
appDir,
|
|
118
|
+
appName,
|
|
119
|
+
template,
|
|
120
|
+
});
|
|
121
|
+
}
|
package/src/commands/deploy.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
2
3
|
import { pathExists, readJson } from 'fs-extra';
|
|
3
4
|
import { execa } from 'execa';
|
|
4
5
|
import { info, warn, output, setOutputMode } from '../util/logger';
|
|
@@ -42,9 +43,6 @@ export async function deployCommand(opts: DeployOptions): Promise<void> {
|
|
|
42
43
|
info('checking S3 bucket');
|
|
43
44
|
await assertBucketExists();
|
|
44
45
|
|
|
45
|
-
info('building');
|
|
46
|
-
await buildCommand({ json: opts.json });
|
|
47
|
-
|
|
48
46
|
const paths = resolveConsumerPaths(process.cwd());
|
|
49
47
|
const consumerPkg = (await readJson(paths.consumerPkg)) as { name?: string };
|
|
50
48
|
if (!consumerPkg.name) {
|
|
@@ -52,6 +50,15 @@ export async function deployCommand(opts: DeployOptions): Promise<void> {
|
|
|
52
50
|
}
|
|
53
51
|
const appName = mfAppName(consumerPkg.name);
|
|
54
52
|
|
|
53
|
+
info('reading current Z2H manifest');
|
|
54
|
+
const manifest = await readManifest();
|
|
55
|
+
const version = nextVersion(manifest, appName);
|
|
56
|
+
const keyPrefix = `${appName}/${version}`;
|
|
57
|
+
const url = `${BUCKET_READ_DNS_URL}/${keyPrefix}`;
|
|
58
|
+
|
|
59
|
+
info('building');
|
|
60
|
+
await buildCommand({ json: opts.json, publicUrl: url });
|
|
61
|
+
|
|
55
62
|
if (!(await pathExists(paths.shadowBuild))) {
|
|
56
63
|
throw new Error(`No build output at ${paths.shadowBuild} after build step.`);
|
|
57
64
|
}
|
|
@@ -63,12 +70,6 @@ export async function deployCommand(opts: DeployOptions): Promise<void> {
|
|
|
63
70
|
}
|
|
64
71
|
const assetManifest = (await readJson(assetManifestPath)) as AssetManifestFile;
|
|
65
72
|
|
|
66
|
-
info('reading current Z2H manifest');
|
|
67
|
-
const manifest = await readManifest();
|
|
68
|
-
const version = nextVersion(manifest, appName);
|
|
69
|
-
const keyPrefix = `${appName}/${version}`;
|
|
70
|
-
const url = `${BUCKET_READ_DNS_URL}/${keyPrefix}`;
|
|
71
|
-
|
|
72
73
|
if (opts.dryRun) {
|
|
73
74
|
output(`[z2h-cli] dry-run: would upload to s3://${BUCKET_WRITE_NAME}/${keyPrefix}/ as v${version}`, {
|
|
74
75
|
ok: true,
|
|
@@ -85,15 +86,18 @@ export async function deployCommand(opts: DeployOptions): Promise<void> {
|
|
|
85
86
|
|
|
86
87
|
await requestAponoAccess();
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
await uploadDir(paths.shadowBuild, keyPrefix);
|
|
89
|
+
const deployedBy = os.userInfo().username;
|
|
90
90
|
|
|
91
91
|
const entry: ManifestEntry = {
|
|
92
92
|
...assetManifest,
|
|
93
93
|
version,
|
|
94
94
|
url,
|
|
95
|
+
deployedBy,
|
|
95
96
|
};
|
|
96
97
|
|
|
98
|
+
info(`uploading build → s3://${BUCKET_WRITE_NAME}/${keyPrefix}/`);
|
|
99
|
+
await uploadDir(paths.shadowBuild, keyPrefix);
|
|
100
|
+
|
|
97
101
|
info(`updating manifest entry "${appName}" → v${version}`);
|
|
98
102
|
await writeManifest(upsertEntry(manifest, appName, entry));
|
|
99
103
|
|
package/src/commands/dev.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { readJson, writeFile } from 'fs-extra';
|
|
2
4
|
import { watch } from 'chokidar';
|
|
3
5
|
import { execa } from 'execa';
|
|
4
6
|
import { info, warn, errorLog, output, setOutputMode } from '../util/logger';
|
|
@@ -31,6 +33,29 @@ export async function devCommand(opts: DevOptions): Promise<void> {
|
|
|
31
33
|
stdio: ['inherit', 'pipe', 'inherit'],
|
|
32
34
|
});
|
|
33
35
|
|
|
36
|
+
const pidFile = path.join(ctx.paths.shadowDir, 'dev.pid');
|
|
37
|
+
if (server.pid) {
|
|
38
|
+
await writeFile(pidFile, String(server.pid));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const cleanupPid = () => {
|
|
42
|
+
try {
|
|
43
|
+
fs.unlinkSync(pidFile);
|
|
44
|
+
} catch (_) {
|
|
45
|
+
// file already gone — ignore
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
server.on('exit', cleanupPid);
|
|
49
|
+
process.on('exit', cleanupPid);
|
|
50
|
+
process.on('SIGINT', () => {
|
|
51
|
+
server.kill();
|
|
52
|
+
process.exit(0);
|
|
53
|
+
});
|
|
54
|
+
process.on('SIGTERM', () => {
|
|
55
|
+
server.kill();
|
|
56
|
+
process.exit(0);
|
|
57
|
+
});
|
|
58
|
+
|
|
34
59
|
let readyPrinted = false;
|
|
35
60
|
server.stdout?.on('data', (chunk: Buffer) => {
|
|
36
61
|
process.stdout.write(chunk);
|