@multiplatform.one/cli 6.1.0 → 6.4.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 +72 -0
- package/bin/createMultiplatformApp.mjs +77 -0
- package/lib/bin/multiplatformOne.mjs +35 -10
- package/lib/commands/e2e.mjs +3 -3
- package/lib/commands/init.mjs +16 -12
- package/lib/commands/initApp.mjs +175 -0
- package/lib/commands/updateApp.mjs +260 -0
- package/lib/generateVscode.mjs +27 -10
- package/package.json +23 -12
- package/src/bin/multiplatformOne.ts +83 -20
- package/src/commands/init.ts +7 -18
- package/src/commands/initApp.spec.ts +210 -0
- package/src/commands/initApp.ts +274 -0
- package/src/commands/updateApp.ts +277 -0
- package/src/generateVscode.ts +31 -9
- package/templates/app/AGENTS.md +142 -0
- package/templates/app/README.md +62 -0
- package/templates/app/apps/__NAME__/index.html +36 -0
- package/templates/app/apps/__NAME__/package.json +30 -0
- package/templates/app/apps/__NAME__/public/fonts/inter-400.woff2 +0 -0
- package/templates/app/apps/__NAME__/public/fonts/inter-700.woff2 +0 -0
- package/templates/app/apps/__NAME__/src/App.tsx +9 -0
- package/templates/app/apps/__NAME__/src/main.tsx +14 -0
- package/templates/app/apps/__NAME__/tsconfig.json +8 -0
- package/templates/app/apps/__NAME__/vite.config.ts +17 -0
- package/templates/app/features/__NAME__/package.json +34 -0
- package/templates/app/features/__NAME__/src/HomeScreen.tsx +70 -0
- package/templates/app/features/__NAME__/src/index.ts +1 -0
- package/templates/app/features/__NAME__/tsconfig.json +5 -0
- package/templates/app/gitignore +8 -0
- package/templates/app/npmrc +2 -0
- package/templates/app/oxlintrc.json +39 -0
- package/templates/app/package.json +20 -0
- package/templates/app/packages/config/config.json +4 -0
- package/templates/app/packages/config/index.ts +5 -0
- package/templates/app/packages/config/package.json +17 -0
- package/templates/app/packages/config/tsconfig.json +5 -0
- package/templates/app/pnpm-workspace.yaml +4 -0
- package/templates/app/tsconfig.base.json +16 -0
- package/templates/universal/AGENTS.md +142 -0
- package/templates/universal/README.md +97 -0
- package/templates/universal/apps/__NAME__/app.config.js +31 -0
- package/templates/universal/apps/__NAME__/config/tamagui.config.ts +4 -0
- package/templates/universal/apps/__NAME__/package.json +61 -0
- package/templates/universal/apps/__NAME__/polyfills.native.ts +23 -0
- package/templates/universal/apps/__NAME__/polyfills.ts +3 -0
- package/templates/universal/apps/__NAME__/public/fonts/inter-400.woff2 +0 -0
- package/templates/universal/apps/__NAME__/public/fonts/inter-700.woff2 +0 -0
- package/templates/universal/apps/__NAME__/routes/_layout.tsx +22 -0
- package/templates/universal/apps/__NAME__/routes/_layout.web.tsx +21 -0
- package/templates/universal/apps/__NAME__/routes/about.tsx +3 -0
- package/templates/universal/apps/__NAME__/routes/index.tsx +3 -0
- package/templates/universal/apps/__NAME__/src/app.ts +10 -0
- package/templates/universal/apps/__NAME__/tamagui.css +3 -0
- package/templates/universal/apps/__NAME__/tsconfig.json +8 -0
- package/templates/universal/apps/__NAME__/types.d.ts +9 -0
- package/templates/universal/apps/__NAME__/vite.config.ts +66 -0
- package/templates/universal/features/__NAME__/package.json +35 -0
- package/templates/universal/features/__NAME__/src/AboutScreen.tsx +27 -0
- package/templates/universal/features/__NAME__/src/AppLayout.tsx +50 -0
- package/templates/universal/features/__NAME__/src/ContactForm.spec.tsx +22 -0
- package/templates/universal/features/__NAME__/src/ContactForm.tsx +55 -0
- package/templates/universal/features/__NAME__/src/HomeScreen.tsx +40 -0
- package/templates/universal/features/__NAME__/src/index.ts +4 -0
- package/templates/universal/features/__NAME__/src/screens.spec.ts +12 -0
- package/templates/universal/features/__NAME__/tsconfig.json +5 -0
- package/templates/universal/features/__NAME__/vitest.config.mjs +7 -0
- package/templates/universal/gitignore +12 -0
- package/templates/universal/npmrc +3 -0
- package/templates/universal/oxlintrc.json +39 -0
- package/templates/universal/package.json +27 -0
- package/templates/universal/packages/config/config.json +4 -0
- package/templates/universal/packages/config/index.ts +5 -0
- package/templates/universal/packages/config/package.json +17 -0
- package/templates/universal/packages/config/tsconfig.json +5 -0
- package/templates/universal/packages/i18n/en/common.json +30 -0
- package/templates/universal/packages/i18n/es/common.json +30 -0
- package/templates/universal/packages/i18n/i18n.d.ts +16 -0
- package/templates/universal/packages/i18n/index.ts +26 -0
- package/templates/universal/packages/i18n/package.json +20 -0
- package/templates/universal/packages/i18n/resources.ts +13 -0
- package/templates/universal/packages/i18n/tsconfig.json +5 -0
- package/templates/universal/packages/themes/accent.ts +32 -0
- package/templates/universal/packages/themes/base.ts +50 -0
- package/templates/universal/packages/themes/fonts.ts +8 -0
- package/templates/universal/packages/themes/index.ts +33 -0
- package/templates/universal/packages/themes/package.json +23 -0
- package/templates/universal/packages/themes/tamagui.config.ts +14 -0
- package/templates/universal/packages/themes/tsconfig.json +5 -0
- package/templates/universal/pnpm-workspace.yaml +4 -0
- package/templates/universal/tsconfig.base.json +21 -0
- package/templates/universal/types/env.d.ts +9 -0
- package/types/bin/multiplatformOne.d.ts +11 -0
- package/types/bin/multiplatformOne.d.ts.map +1 -0
- package/types/commands/e2e.d.ts +14 -0
- package/types/commands/e2e.d.ts.map +1 -0
- package/types/commands/init.d.ts +16 -0
- package/types/commands/init.d.ts.map +1 -0
- package/types/commands/initApp.d.ts +41 -0
- package/types/commands/initApp.d.ts.map +1 -0
- package/types/commands/updateApp.d.ts +24 -0
- package/types/commands/updateApp.d.ts.map +1 -0
- package/types/generateVscode.d.ts +7 -0
- package/types/generateVscode.d.ts.map +1 -0
- package/types/index.d.ts +2 -0
- package/types/index.d.ts.map +1 -0
- package/types/types.d.ts +6 -0
- package/types/types.d.ts.map +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# @multiplatform.one/cli
|
|
2
|
+
|
|
3
|
+
The multiplatform.one command line: scaffold new multiplatform projects
|
|
4
|
+
(`mpo init` / `create-multiplatform-app`), manage the monorepo dev
|
|
5
|
+
environment, and drive Frappe + E2E workflows.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add -g @multiplatform.one/cli
|
|
11
|
+
# or run the scaffolder without installing:
|
|
12
|
+
npx --package=@multiplatform.one/cli create-multiplatform-app my-app
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Two binaries: **`mpo`** (full CLI) and **`create-multiplatform-app`**
|
|
16
|
+
(scaffolder only).
|
|
17
|
+
|
|
18
|
+
## Scaffold a project (recommended)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
mpo init my-app # universal: web (SSR) + iOS + Android via one
|
|
22
|
+
cd my-app
|
|
23
|
+
pnpm install
|
|
24
|
+
pnpm dev # web + native dev server
|
|
25
|
+
pnpm ios # build & run the iOS dev client (pnpm dev in another terminal)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`mpo init` creates a **consumer** project — not a monorepo fork. It prompts for
|
|
29
|
+
anything omitted (name; platforms, defaulting to universal); pass `--yes` for
|
|
30
|
+
CI. `@multiplatform.one/*` dependencies come from npm at semver ranges, never
|
|
31
|
+
`workspace:*`.
|
|
32
|
+
|
|
33
|
+
Universal layout: `apps/<name>` (routes + providers + vite/expo config),
|
|
34
|
+
`features/<name>` (starter Home/About screens, forms demo, vitest),
|
|
35
|
+
`packages/config`, `packages/i18n` (en/es), `packages/themes` (Tamagui theme).
|
|
36
|
+
|
|
37
|
+
Want web-only? `mpo init --web my-app` scaffolds the Vite + React variant
|
|
38
|
+
(`--app` is a back-compat alias; `create-multiplatform-app` without flags also
|
|
39
|
+
produces it — pass `--universal` there for the full project).
|
|
40
|
+
|
|
41
|
+
Options: `--universal`, `--web`/`--app`, `--skip-install`,
|
|
42
|
+
`--mpo-version <range>` (defaults to `^<cli version>`), `-y/--yes`.
|
|
43
|
+
|
|
44
|
+
## Other commands
|
|
45
|
+
|
|
46
|
+
| Command | Purpose |
|
|
47
|
+
| ---------------------------- | -------------------------------------------------------------------- |
|
|
48
|
+
| `mpo init --monorepo [name]` | DEPRECATED: clone/modify the full monorepo (framework dev forks) |
|
|
49
|
+
| `mpo update` | Merge upstream and re-apply workspace config |
|
|
50
|
+
| `mpo wait` | Wait for a service (frappe, postgres, mariadb, keycloak) to be ready |
|
|
51
|
+
| `mpo frappe bootstrap` | Bootstrap the Frappe development environment |
|
|
52
|
+
| `mpo frappe dev` | Start the Frappe development server |
|
|
53
|
+
| `mpo frappe bench -- <args>` | Run a bench command |
|
|
54
|
+
| `mpo frappe clean` | Clean Frappe bench artifacts |
|
|
55
|
+
| `mpo build` | Run the build command |
|
|
56
|
+
| `mpo test` | Run tests (E2E flags: `--up-only`, `--down`, `--filter`, …) |
|
|
57
|
+
|
|
58
|
+
## Development
|
|
59
|
+
|
|
60
|
+
Templates live in `templates/universal/` and `templates/app/`. Generator unit
|
|
61
|
+
specs: `pnpm --filter @multiplatform.one/cli test`
|
|
62
|
+
(`src/commands/initApp.spec.ts` — tree + package.json shape for both templates).
|
|
63
|
+
|
|
64
|
+
## What it must not do
|
|
65
|
+
|
|
66
|
+
- The project templates never ship `workspace:*` dependencies on
|
|
67
|
+
`@multiplatform.one/*` or copies of the monorepo's `public/` packages —
|
|
68
|
+
consumers upgrade via npm (`pnpm update "@multiplatform.one/*"`)
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
Apache-2.0
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* create-multiplatform-app — npm create / npx entry for consumer scaffolds.
|
|
5
|
+
* Delegates to the same generator as `mpo init`.
|
|
6
|
+
*
|
|
7
|
+
* Defaults to the web-only app template (back-compat). Pass --universal for
|
|
8
|
+
* the full web + iOS + Android project (same as `mpo init <name>`).
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* npx --package=@multiplatform.one/cli create-multiplatform-app my-app
|
|
12
|
+
* npx --package=@multiplatform.one/cli create-multiplatform-app my-app --universal
|
|
13
|
+
* mpo init my-app # universal
|
|
14
|
+
* mpo init --web my-app # web-only
|
|
15
|
+
*/
|
|
16
|
+
import { createRequire } from "node:module";
|
|
17
|
+
import { dirname, resolve } from "node:path";
|
|
18
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
19
|
+
|
|
20
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
|
|
23
|
+
function flagValue(argv, name) {
|
|
24
|
+
const idx = argv.indexOf(name);
|
|
25
|
+
if (idx < 0) return undefined;
|
|
26
|
+
return argv[idx + 1];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function main() {
|
|
30
|
+
const argv = process.argv.slice(2);
|
|
31
|
+
if (argv[0] === "--help" || argv[0] === "-h") {
|
|
32
|
+
console.log(`create-multiplatform-app [name] [--universal] [--skip-install] [--mpo-version <range>] [--yes]
|
|
33
|
+
|
|
34
|
+
Scaffold a consumer project with @multiplatform.one/* at semver ranges
|
|
35
|
+
(not workspace:*):
|
|
36
|
+
|
|
37
|
+
(default) apps/<name> + features/<name> + packages/config (web-only)
|
|
38
|
+
--universal one-based web + iOS + Android app
|
|
39
|
+
(adds packages/i18n + packages/themes)
|
|
40
|
+
|
|
41
|
+
Same as: mpo init --web [name] / mpo init [name]
|
|
42
|
+
`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (argv[0] === "--version" || argv[0] === "-V") {
|
|
46
|
+
console.log(require("../package.json").version);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const skipInstall = argv.includes("--skip-install");
|
|
51
|
+
const yes = argv.includes("--yes") || argv.includes("-y");
|
|
52
|
+
const universal = argv.includes("--universal");
|
|
53
|
+
const version = flagValue(argv, "--mpo-version");
|
|
54
|
+
const name = argv.find((a) => !a.startsWith("-"));
|
|
55
|
+
|
|
56
|
+
const libPath = resolve(here, "../lib/commands/initApp.mjs");
|
|
57
|
+
let initApp;
|
|
58
|
+
try {
|
|
59
|
+
({ initApp } = await import(pathToFileURL(libPath).href));
|
|
60
|
+
} catch {
|
|
61
|
+
throw new Error(
|
|
62
|
+
"Could not load initApp. Build @multiplatform.one/cli first (pnpm --filter @multiplatform.one/cli build).",
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await initApp(name, {
|
|
67
|
+
skipInstall,
|
|
68
|
+
version,
|
|
69
|
+
template: universal ? "universal" : "app",
|
|
70
|
+
yes,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
main().catch((err) => {
|
|
75
|
+
console.error(err instanceof Error ? err.message : err);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
});
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { generateVscodeConfig } from "../generateVscode.mjs";
|
|
2
2
|
import { discoverE2EApp, runE2ESession } from "../commands/e2e.mjs";
|
|
3
3
|
import { init, runModifyStep } from "../commands/init.mjs";
|
|
4
|
+
import { initApp, readProvenance } from "../commands/initApp.mjs";
|
|
5
|
+
import { updateApp } from "../commands/updateApp.mjs";
|
|
4
6
|
import { createRequire } from "node:module";
|
|
5
7
|
import fs from "node:fs/promises";
|
|
6
8
|
import path from "node:path";
|
|
7
|
-
import spawn from "nano-spawn";
|
|
8
|
-
import yoctoSpinner from "yocto-spinner";
|
|
9
|
-
import YAML from "yaml";
|
|
10
9
|
import fsSync from "node:fs";
|
|
11
10
|
import { fileURLToPath } from "node:url";
|
|
12
11
|
import { formatServiceList, lookupProjectRoot, waitForFrappe, waitForKeycloak, waitForPostgres, waitServices } from "@multiplatform.one/utils/dev";
|
|
13
12
|
import { Command } from "commander";
|
|
13
|
+
import spawn from "nano-spawn";
|
|
14
|
+
import YAML from "yaml";
|
|
15
|
+
import yoctoSpinner from "yocto-spinner";
|
|
14
16
|
|
|
15
17
|
//#region \0rolldown/runtime.js
|
|
16
18
|
var __create = Object.create;
|
|
@@ -469,16 +471,39 @@ async function getFrappeApps(root) {
|
|
|
469
471
|
const program = new Command();
|
|
470
472
|
program.name("mpo");
|
|
471
473
|
program.version(JSON.parse(fsSync.readFileSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json"), "utf8"))?.version);
|
|
472
|
-
program.command("init").option("-c, --checkout <branch>", "branch, tag or commit to checkout", "main").option("-a, --apps <apps>", "comma-separated apps to include").option("-s, --services <services>", "comma-separated services to include").argument("[name]", "the name of the project").description("
|
|
473
|
-
if (
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
474
|
+
program.command("init").option("--universal", "scaffold the universal template (web + iOS + Android; default)").option("--web", "scaffold the web-only template (Vite + React)").option("--app", "alias of --web (back-compat)").option("--skip-install", "skip pnpm install after scaffolding").option("--mpo-version <range>", "semver range for @multiplatform.one/* (default: ^<cli version>)").option("-y, --yes", "non-interactive: accept defaults instead of prompting").option("--monorepo", "DEPRECATED: clone/modify the full multiplatform.one monorepo instead of scaffolding a consumer project").option("-c, --checkout <branch>", "branch, tag or commit to checkout (--monorepo only)", "main").option("-a, --apps <apps>", "comma-separated apps to include (--monorepo only)").option("-s, --services <services>", "comma-separated services to include (--monorepo only)").argument("[name]", "the name of the project").description("scaffold a new multiplatform project (universal web + native by default; --web for web-only; semver @multiplatform.one/* deps)").action(async (name, options) => {
|
|
475
|
+
if (options.monorepo) {
|
|
476
|
+
console.warn("⚠️ `mpo init --monorepo` clones the whole multiplatform.one monorepo (legacy). New projects should use the default consumer scaffold instead.");
|
|
477
|
+
if (await spawn("git", ["rev-parse", "--is-inside-work-tree"]).then(() => true, () => false)) throw new Error("mpo cannot be initialized inside a git repository");
|
|
478
|
+
const cloneScript = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../scripts/clone.sh");
|
|
479
|
+
await init(name, {
|
|
480
|
+
...options,
|
|
481
|
+
cloneScript
|
|
482
|
+
});
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (options.apps || options.services) throw new Error("--apps/--services select monorepo pieces and require --monorepo (legacy clone mode)");
|
|
486
|
+
const webOnly = Boolean(options.web || options.app);
|
|
487
|
+
if (webOnly && options.universal) throw new Error("Pass either --web/--app or --universal, not both");
|
|
488
|
+
await initApp(name, {
|
|
489
|
+
skipInstall: Boolean(options.skipInstall),
|
|
490
|
+
version: options.mpoVersion,
|
|
491
|
+
template: webOnly ? "app" : options.universal ? "universal" : void 0,
|
|
492
|
+
yes: Boolean(options.yes)
|
|
478
493
|
});
|
|
479
494
|
});
|
|
480
|
-
program.command("update").option("-c, --checkout <branch>", "branch, tag or commit to merge from upstream", "main").option("-r, --remote <url>", "upstream remote URL", "https://gitlab.com/bitspur/multiplatform.one/multiplatform.one.git").description("
|
|
495
|
+
program.command("update").option("-c, --checkout <branch>", "branch, tag or commit to merge from upstream (monorepo forks)", "main").option("-r, --remote <url>", "upstream remote URL (monorepo forks)", "https://gitlab.com/bitspur/multiplatform.one/multiplatform.one.git").option("--skip-install", "skip pnpm install after the update").option("--mpo-version <range>", "semver range for @multiplatform.one/* (default: ^<cli version>)").description("update a scaffolded project to the current template (three-way merge via .mpo.json provenance); monorepo forks fall back to the upstream merge flow").action(async (options) => {
|
|
481
496
|
if (await spawn("git", ["rev-parse", "--is-inside-work-tree"]).then(() => false, () => true)) throw new Error("mpo cannot be updated outside of a git repository");
|
|
497
|
+
if (readProvenance(projectRoot)) {
|
|
498
|
+
await updateApp({
|
|
499
|
+
skipInstall: options.skipInstall,
|
|
500
|
+
version: options.mpoVersion
|
|
501
|
+
});
|
|
502
|
+
try {
|
|
503
|
+
await generateVscodeConfig(projectRoot);
|
|
504
|
+
} catch {}
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
482
507
|
if (await spawn("git", [
|
|
483
508
|
"diff",
|
|
484
509
|
"--cached",
|
package/lib/commands/e2e.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import crypto from "node:crypto";
|
|
4
|
-
import os from "node:os";
|
|
5
3
|
import spawn from "nano-spawn";
|
|
6
|
-
import yoctoSpinner from "yocto-spinner";
|
|
7
4
|
import YAML from "yaml";
|
|
5
|
+
import yoctoSpinner from "yocto-spinner";
|
|
6
|
+
import crypto from "node:crypto";
|
|
7
|
+
import os from "node:os";
|
|
8
8
|
|
|
9
9
|
//#region src/commands/e2e.ts
|
|
10
10
|
function sessionFileName(portOffset) {
|
package/lib/commands/init.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join, resolve } from "node:path";
|
|
2
|
-
import { execSync } from "node:child_process";
|
|
3
2
|
import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
4
|
import inquirer from "inquirer";
|
|
5
5
|
|
|
6
6
|
//#region src/commands/init.ts
|
|
@@ -20,8 +20,6 @@ const apps = [
|
|
|
20
20
|
"keycloak",
|
|
21
21
|
"storybook",
|
|
22
22
|
"storybook-expo",
|
|
23
|
-
"vscode",
|
|
24
|
-
"webext",
|
|
25
23
|
"vocs"
|
|
26
24
|
];
|
|
27
25
|
/** env variable prefixes to remove per service */
|
|
@@ -33,12 +31,14 @@ const serviceEnvPrefixes = {
|
|
|
33
31
|
};
|
|
34
32
|
/** env variable prefixes to remove per app */
|
|
35
33
|
const appEnvPrefixes = {
|
|
36
|
-
one: [
|
|
34
|
+
one: [
|
|
35
|
+
"ONE_",
|
|
36
|
+
"VSCODE_",
|
|
37
|
+
"WEBEXT_"
|
|
38
|
+
],
|
|
37
39
|
keycloak: ["KEYCLOAK_"],
|
|
38
40
|
storybook: ["STORYBOOK_", "VR_DIFFING_ENGINE="],
|
|
39
41
|
"storybook-expo": [],
|
|
40
|
-
vscode: ["VSCODE_"],
|
|
41
|
-
webext: ["WEBEXT_"],
|
|
42
42
|
vocs: ["VOCS_"]
|
|
43
43
|
};
|
|
44
44
|
/** env section headers (comments) to remove per app/service */
|
|
@@ -49,12 +49,14 @@ const serviceEnvSections = {
|
|
|
49
49
|
sui: []
|
|
50
50
|
};
|
|
51
51
|
const appEnvSections = {
|
|
52
|
-
one: [
|
|
52
|
+
one: [
|
|
53
|
+
"# one",
|
|
54
|
+
"# vscode",
|
|
55
|
+
"# webext"
|
|
56
|
+
],
|
|
53
57
|
keycloak: ["# keycloak"],
|
|
54
58
|
storybook: ["# storybook"],
|
|
55
59
|
"storybook-expo": [],
|
|
56
|
-
vscode: ["# vscode"],
|
|
57
|
-
webext: ["# webext"],
|
|
58
60
|
vocs: ["# vocs"]
|
|
59
61
|
};
|
|
60
62
|
/** launch.json configuration names per service/app */
|
|
@@ -65,12 +67,14 @@ const serviceLaunchNames = {
|
|
|
65
67
|
sui: ["sui dev"]
|
|
66
68
|
};
|
|
67
69
|
const appLaunchNames = {
|
|
68
|
-
one: [
|
|
70
|
+
one: [
|
|
71
|
+
"one dev",
|
|
72
|
+
"vscode dev",
|
|
73
|
+
"webext dev"
|
|
74
|
+
],
|
|
69
75
|
keycloak: ["keycloak dev", "keycloak storybook"],
|
|
70
76
|
storybook: ["storybook dev"],
|
|
71
77
|
"storybook-expo": ["storybook-expo dev"],
|
|
72
|
-
vscode: ["vscode dev"],
|
|
73
|
-
webext: ["webext dev"],
|
|
74
78
|
vocs: ["vocs dev"]
|
|
75
79
|
};
|
|
76
80
|
/**
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { dirname, extname, join, resolve } from "node:path";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import spawn from "nano-spawn";
|
|
5
|
+
import inquirer from "inquirer";
|
|
6
|
+
|
|
7
|
+
//#region src/commands/initApp.ts
|
|
8
|
+
function toPascalCase(name) {
|
|
9
|
+
return name.split(/[-_\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
10
|
+
}
|
|
11
|
+
function validateName(name) {
|
|
12
|
+
const trimmed = name.trim();
|
|
13
|
+
if (!trimmed) throw new Error("Project name is required");
|
|
14
|
+
if (!/^[a-z][a-z0-9-]*$/.test(trimmed)) throw new Error(`Invalid project name "${trimmed}". Use lowercase letters, numbers, and hyphens (e.g. my-app).`);
|
|
15
|
+
return trimmed;
|
|
16
|
+
}
|
|
17
|
+
function resolveMpoVersion(explicit) {
|
|
18
|
+
if (explicit) return explicit.startsWith("^") || explicit.startsWith("~") || explicit === "*" ? explicit : `^${explicit}`;
|
|
19
|
+
const version = cliVersion();
|
|
20
|
+
return version ? `^${version}` : "^6.1.0";
|
|
21
|
+
}
|
|
22
|
+
function cliVersion() {
|
|
23
|
+
try {
|
|
24
|
+
const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), "../../package.json");
|
|
25
|
+
return JSON.parse(readFileSync(pkgPath, "utf-8")).version;
|
|
26
|
+
} catch {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const PROVENANCE_FILE = ".mpo.json";
|
|
31
|
+
function writeProvenance(targetDir, provenance) {
|
|
32
|
+
writeFileSync(join(targetDir, PROVENANCE_FILE), `${JSON.stringify(provenance, null, 2)}\n`);
|
|
33
|
+
}
|
|
34
|
+
function readProvenance(projectDir) {
|
|
35
|
+
const file = join(projectDir, PROVENANCE_FILE);
|
|
36
|
+
if (!existsSync(file)) return void 0;
|
|
37
|
+
try {
|
|
38
|
+
const raw = JSON.parse(readFileSync(file, "utf-8"));
|
|
39
|
+
if (!raw.template || !raw.cliVersion || !raw.name) return void 0;
|
|
40
|
+
return {
|
|
41
|
+
template: raw.template,
|
|
42
|
+
cliVersion: raw.cliVersion,
|
|
43
|
+
name: raw.name,
|
|
44
|
+
mpoVersion: raw.mpoVersion ?? `^${raw.cliVersion}`
|
|
45
|
+
};
|
|
46
|
+
} catch {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function templatesRoot(template) {
|
|
51
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), "../../templates", template);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* npm always strips .gitignore / .npmrc from published tarballs, so the
|
|
55
|
+
* template stores them without the leading dot; restore it at scaffold time.
|
|
56
|
+
*/
|
|
57
|
+
const DOTFILE_NAMES = {
|
|
58
|
+
gitignore: ".gitignore",
|
|
59
|
+
npmrc: ".npmrc"
|
|
60
|
+
};
|
|
61
|
+
function render(content, vars) {
|
|
62
|
+
return content.replaceAll("__NAME_PASCAL__", vars.NAME_PASCAL).replaceAll("__NAME_APPID__", vars.NAME_APPID).replaceAll("__NAME__", vars.NAME).replaceAll("__MPO_VERSION__", vars.MPO_VERSION).replaceAll("__YEAR__", vars.YEAR);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Assets copied byte-for-byte — running them through the utf-8 render pass
|
|
66
|
+
* would corrupt them (fonts ship in the templates for LC-33 stable mount).
|
|
67
|
+
*/
|
|
68
|
+
const BINARY_EXTENSIONS = new Set([
|
|
69
|
+
".woff2",
|
|
70
|
+
".woff",
|
|
71
|
+
".ttf",
|
|
72
|
+
".otf",
|
|
73
|
+
".png",
|
|
74
|
+
".jpg",
|
|
75
|
+
".ico"
|
|
76
|
+
]);
|
|
77
|
+
function writeTree(srcDir, destDir, vars) {
|
|
78
|
+
if (!existsSync(srcDir)) throw new Error(`Project template missing at ${srcDir}`);
|
|
79
|
+
mkdirSync(destDir, { recursive: true });
|
|
80
|
+
for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
|
|
81
|
+
const from = join(srcDir, entry.name);
|
|
82
|
+
const to = join(destDir, render(DOTFILE_NAMES[entry.name] ?? entry.name, vars));
|
|
83
|
+
if (entry.isDirectory()) {
|
|
84
|
+
writeTree(from, to, vars);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (BINARY_EXTENSIONS.has(extname(entry.name).toLowerCase())) {
|
|
88
|
+
copyFileSync(from, to);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
writeFileSync(to, render(readFileSync(from, "utf-8"), vars));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function resolveTemplate(options) {
|
|
95
|
+
if (options.template) return options.template;
|
|
96
|
+
if (options.yes || !process.stdout.isTTY) return "universal";
|
|
97
|
+
return (await inquirer.prompt([{
|
|
98
|
+
message: "Which platforms should this project target?",
|
|
99
|
+
name: "template",
|
|
100
|
+
type: "list",
|
|
101
|
+
choices: [{
|
|
102
|
+
name: "Universal — web + iOS + Android (one)",
|
|
103
|
+
value: "universal"
|
|
104
|
+
}, {
|
|
105
|
+
name: "Web only — Vite + React",
|
|
106
|
+
value: "app"
|
|
107
|
+
}],
|
|
108
|
+
default: "universal"
|
|
109
|
+
}])).template;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Scaffold a consumer project (universal by default, web-only with
|
|
113
|
+
* template: "app") that depends on @multiplatform.one/* at semver ranges
|
|
114
|
+
* (never workspace:*). This is NOT a monorepo fork.
|
|
115
|
+
*/
|
|
116
|
+
async function initApp(nameArg, options = {}) {
|
|
117
|
+
let projectName = nameArg;
|
|
118
|
+
if (!projectName) if (options.yes || !process.stdout.isTTY) projectName = "my-app";
|
|
119
|
+
else projectName = (await inquirer.prompt([{
|
|
120
|
+
message: "What is the app name?",
|
|
121
|
+
name: "name",
|
|
122
|
+
type: "input",
|
|
123
|
+
default: "my-app"
|
|
124
|
+
}])).name;
|
|
125
|
+
const name = validateName(projectName);
|
|
126
|
+
const template = await resolveTemplate(options);
|
|
127
|
+
const targetDir = resolve(name);
|
|
128
|
+
if (existsSync(targetDir)) {
|
|
129
|
+
if (readdirSync(targetDir).length > 0) throw new Error(`Directory "${targetDir}" already exists and is not empty`);
|
|
130
|
+
}
|
|
131
|
+
const vars = {
|
|
132
|
+
NAME: name,
|
|
133
|
+
NAME_PASCAL: toPascalCase(name),
|
|
134
|
+
NAME_APPID: name.replaceAll("-", ""),
|
|
135
|
+
MPO_VERSION: resolveMpoVersion(options.version),
|
|
136
|
+
YEAR: String((/* @__PURE__ */ new Date()).getFullYear())
|
|
137
|
+
};
|
|
138
|
+
console.log(`\nScaffolding ${template === "universal" ? "universal (web + iOS + Android)" : "web-only"} project at ${targetDir}`);
|
|
139
|
+
console.log(` apps/${name}`);
|
|
140
|
+
console.log(` features/${name}`);
|
|
141
|
+
console.log(` packages/config`);
|
|
142
|
+
if (template === "universal") {
|
|
143
|
+
console.log(` packages/i18n`);
|
|
144
|
+
console.log(` packages/themes`);
|
|
145
|
+
}
|
|
146
|
+
console.log(` @multiplatform.one/* → ${vars.MPO_VERSION}\n`);
|
|
147
|
+
writeTree(templatesRoot(template), targetDir, vars);
|
|
148
|
+
writeProvenance(targetDir, {
|
|
149
|
+
template,
|
|
150
|
+
cliVersion: cliVersion() ?? "0.0.0",
|
|
151
|
+
name,
|
|
152
|
+
mpoVersion: vars.MPO_VERSION
|
|
153
|
+
});
|
|
154
|
+
if (!options.skipInstall) {
|
|
155
|
+
console.log("Installing dependencies...");
|
|
156
|
+
await spawn("pnpm", ["install"], {
|
|
157
|
+
cwd: targetDir,
|
|
158
|
+
stdio: "inherit"
|
|
159
|
+
});
|
|
160
|
+
} else console.log("Skipped pnpm install (--skip-install).");
|
|
161
|
+
console.log(`\n✅ Project created at ${targetDir}`);
|
|
162
|
+
console.log("\nNext steps:");
|
|
163
|
+
console.log(` cd ${name}`);
|
|
164
|
+
if (options.skipInstall) console.log(" pnpm install");
|
|
165
|
+
console.log(" pnpm dev # web dev server");
|
|
166
|
+
if (template === "universal") {
|
|
167
|
+
console.log(" pnpm ios # iOS (requires pnpm dev in another terminal)");
|
|
168
|
+
console.log(" pnpm android # Android");
|
|
169
|
+
}
|
|
170
|
+
console.log("\nThis project consumes published @multiplatform.one/* packages.");
|
|
171
|
+
console.log("It is not a fork of the multiplatform.one monorepo.");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
//#endregion
|
|
175
|
+
export { PROVENANCE_FILE, cliVersion, initApp, readProvenance, writeProvenance };
|