create-du-app 0.1.0 → 0.1.1
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 +53 -17
- package/package.json +2 -2
- package/src/generate.js +18 -18
- package/src/index.js +44 -44
- package/src/prompts.js +13 -13
- package/src/registry.js +11 -11
- package/templates/be/nestjs/README.md +6 -6
- package/templates/be/nestjs/_package.json +1 -1
- package/templates/be/nodejs/README.md +6 -6
- package/templates/be/nodejs/_package.json +1 -1
- package/templates/be/php/README.md +9 -9
- package/templates/fe/nextjs/README.md +6 -6
- package/templates/fe/nextjs/_package.json +1 -1
- package/templates/fe/reactjs/README.md +6 -6
- package/templates/fe/reactjs/_package.json +1 -1
- package/templates/mobile/expo/README.md +6 -6
- package/templates/mobile/expo/_package.json +1 -1
- package/templates/mobile/flutter/README.md +9 -9
- package/templates/mobile/rn/README.md +6 -6
- package/templates/mobile/rn/_package.json +1 -1
- package/templates/shared/README.md +6 -6
- package/templates/shared/_package.json +1 -1
- package/templates/shared/index.js +2 -2
package/README.md
CHANGED
|
@@ -1,28 +1,64 @@
|
|
|
1
|
-
#
|
|
1
|
+
# create-du-app
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
một monorepo sản phẩm độc lập.
|
|
3
|
+
> The `create-app` CLI — scaffolds a standalone product monorepo from company templates.
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
Prompts for a project name and a selection of stacks (Mobile / Frontend / Backend), then copies
|
|
6
|
+
the matching templates, renames manifests, substitutes the project name, and emits a ready-to-run
|
|
7
|
+
pnpm workspace.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
7
10
|
|
|
8
11
|
```bash
|
|
9
12
|
cd packages/cli
|
|
10
13
|
pnpm install
|
|
11
|
-
pnpm link --global #
|
|
12
|
-
|
|
14
|
+
pnpm link --global # registers the global `create-app` command
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or run without linking:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
node src/index.js
|
|
13
21
|
```
|
|
14
22
|
|
|
15
|
-
##
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
**Interactive** (requires a real terminal):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
create-app
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Non-interactive** (CI or any terminal):
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
create-app --name my-shop --mobile expo --fe nextjs --be nestjs
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Options
|
|
38
|
+
|
|
39
|
+
| Flag | Description |
|
|
40
|
+
|----------------------|------------------------------------------|
|
|
41
|
+
| `-n`, `--name <name>`| Project name |
|
|
42
|
+
| `--mobile <id>` | `rn` · `expo` · `flutter` |
|
|
43
|
+
| `--fe <id>` | `nextjs` · `reactjs` |
|
|
44
|
+
| `--be <id>` | `nodejs` · `nestjs` · `php` |
|
|
45
|
+
| `-h`, `--help` | Print help |
|
|
46
|
+
| `-v`, `--version` | Print version |
|
|
47
|
+
|
|
48
|
+
## How it works
|
|
49
|
+
|
|
50
|
+
| File | Responsibility |
|
|
51
|
+
|---------------|----------------|
|
|
52
|
+
| `index.js` | Entry point. Parses flags, decides interactive vs. non-interactive, runs the generator. |
|
|
53
|
+
| `registry.js` | **Data declaration** of groups, technologies, `templatePath`, and `lang`. The only file to edit when adding or removing a stack. |
|
|
54
|
+
| `prompts.js` | Interactive prompts (`@clack/prompts`): project name → multi-select groups → select one technology per group. |
|
|
55
|
+
| `generate.js` | Copies templates, renames `_package.json` → `package.json`, generates the root `package.json` + `pnpm-workspace.yaml`, conditionally creates `packages/shared`, and substitutes `{{PROJECT_NAME}}`. |
|
|
16
56
|
|
|
17
|
-
|
|
18
|
-
|---------------|---------|
|
|
19
|
-
| `index.js` | entry point, parse args (`--help`, `--version`), gọi prompt rồi generate |
|
|
20
|
-
| `registry.js` | **khai báo dạng data**: các nhóm + tech + `templatePath` + `lang`. Thêm/bớt template chỉ sửa file này |
|
|
21
|
-
| `prompts.js` | logic hỏi tương tác (`@clack/prompts`): tên project → multiselect nhóm → select tech mỗi nhóm |
|
|
22
|
-
| `generate.js` | copy template, rename `_package.json`→`package.json`, sinh `package.json` + `pnpm-workspace.yaml` gốc động, tạo `packages/shared` có điều kiện, thay `{{PROJECT_NAME}}` |
|
|
57
|
+
## Template conventions
|
|
23
58
|
|
|
24
|
-
|
|
59
|
+
- A manifest named `_package.json` is renamed to `package.json` on generate. Likewise
|
|
60
|
+
`_gitignore` → `.gitignore` and `_npmrc` → `.npmrc`.
|
|
61
|
+
- Any text file containing `{{PROJECT_NAME}}` has it replaced with the real project name.
|
|
62
|
+
- `packages/shared` is created only when **≥ 2** selections have `lang === 'js'`.
|
|
25
63
|
|
|
26
|
-
|
|
27
|
-
- Mọi file text chứa `{{PROJECT_NAME}}` sẽ được thay bằng tên project thật.
|
|
28
|
-
- `packages/shared` chỉ được tạo khi có **>= 2** lựa chọn `lang === 'js'`.
|
|
64
|
+
See **[CONTRIBUTING.md](../../CONTRIBUTING.md)** for the full workflow of adding real source.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-du-app",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "CLI generator:
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "CLI generator: pick templates (Mobile/FE/BE) and scaffold a product monorepo",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"create-app": "src/index.js"
|
package/src/generate.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// generate.js — copy
|
|
1
|
+
// generate.js — copy templates + rename + generate dynamic package.json + replace placeholders.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
3
|
+
// Input: the plan from prompts.js → { projectName, selections: [{group, option}] }
|
|
4
|
+
// and `repoRoot` (the company-starter root that contains templates/).
|
|
5
5
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import fse from 'fs-extra';
|
|
@@ -12,15 +12,15 @@ import {
|
|
|
12
12
|
|
|
13
13
|
const PLACEHOLDER = /\{\{PROJECT_NAME\}\}/g;
|
|
14
14
|
|
|
15
|
-
//
|
|
16
|
-
//
|
|
15
|
+
// Files inside a template are given "safe" names so the root repo's npm/tooling does not
|
|
16
|
+
// pick them up by mistake. When generating the real product, they are renamed back.
|
|
17
17
|
const RENAME_MAP = {
|
|
18
18
|
'_package.json': 'package.json',
|
|
19
19
|
'_gitignore': '.gitignore',
|
|
20
20
|
'_npmrc': '.npmrc',
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
//
|
|
23
|
+
// Extensions treated as "text" → {{PROJECT_NAME}} gets replaced.
|
|
24
24
|
const TEXT_EXT = new Set([
|
|
25
25
|
'.json', '.js', '.jsx', '.ts', '.tsx', '.md', '.txt', '.yml', '.yaml',
|
|
26
26
|
'.env', '.html', '.css', '.scss', '.gitignore', '.npmrc', '.dart', '.php',
|
|
@@ -36,11 +36,11 @@ function renameBasename(name) {
|
|
|
36
36
|
return RENAME_MAP[name] ?? name;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
// Copy
|
|
39
|
+
// Copy one template folder → destination, renaming files and replacing {{PROJECT_NAME}}.
|
|
40
40
|
async function copyTemplate(srcDir, destDir, projectName) {
|
|
41
|
-
//
|
|
41
|
+
// An empty template (real source not dropped in yet) must still work.
|
|
42
42
|
if (!(await fse.pathExists(srcDir))) {
|
|
43
|
-
throw new Error(`
|
|
43
|
+
throw new Error(`Template not found: ${srcDir}`);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
await fse.ensureDir(destDir);
|
|
@@ -66,7 +66,7 @@ async function copyTemplate(srcDir, destDir, projectName) {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
//
|
|
69
|
+
// Build the project's root package.json dynamically (pnpm: NO "workspaces" field).
|
|
70
70
|
function buildRootPackageJson(projectName) {
|
|
71
71
|
return {
|
|
72
72
|
name: projectName,
|
|
@@ -77,8 +77,8 @@ function buildRootPackageJson(projectName) {
|
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
//
|
|
81
|
-
// packages:
|
|
80
|
+
// Build the contents of pnpm-workspace.yaml.
|
|
81
|
+
// packages: only the apps/* actually created + always packages/*.
|
|
82
82
|
function buildPnpmWorkspaceYaml(createdAppDirs) {
|
|
83
83
|
const entries = [...createdAppDirs, 'packages/*'];
|
|
84
84
|
const lines = entries.map((p) => ` - '${p}'`);
|
|
@@ -91,12 +91,12 @@ export async function generate(plan, repoRoot, cwd = process.cwd()) {
|
|
|
91
91
|
const logs = [];
|
|
92
92
|
|
|
93
93
|
if (await fse.pathExists(projectRoot)) {
|
|
94
|
-
throw new Error(`
|
|
94
|
+
throw new Error(`Directory "${projectName}" already exists at ${projectRoot}.`);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
await fse.ensureDir(projectRoot);
|
|
98
98
|
|
|
99
|
-
// 1) Copy
|
|
99
|
+
// 1) Copy each selected app.
|
|
100
100
|
const createdAppDirs = [];
|
|
101
101
|
for (const { group, option } of selections) {
|
|
102
102
|
const srcDir = path.resolve(repoRoot, option.templatePath);
|
|
@@ -106,18 +106,18 @@ export async function generate(plan, repoRoot, cwd = process.cwd()) {
|
|
|
106
106
|
logs.push(`✓ ${group.label} (${option.label}) → ${group.outputDir}`);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
// 2) packages/shared
|
|
109
|
+
// 2) packages/shared is CONDITIONAL: >= 2 selections with lang === 'js'.
|
|
110
110
|
const jsCount = selections.filter((s) => s.option.lang === 'js').length;
|
|
111
111
|
if (jsCount >= 2) {
|
|
112
112
|
const srcDir = path.resolve(repoRoot, SHARED_TEMPLATE_PATH);
|
|
113
113
|
const destDir = path.resolve(projectRoot, SHARED_OUTPUT_DIR);
|
|
114
114
|
await copyTemplate(srcDir, destDir, projectName);
|
|
115
|
-
logs.push(`✓ shared → ${SHARED_OUTPUT_DIR} (
|
|
115
|
+
logs.push(`✓ shared → ${SHARED_OUTPUT_DIR} (${jsCount} JS/TS apps)`);
|
|
116
116
|
} else {
|
|
117
|
-
logs.push(`•
|
|
117
|
+
logs.push(`• Skipping shared: not enough JS/TS apps (need >= 2, have ${jsCount}).`);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
// 3)
|
|
120
|
+
// 3) Generate the root package.json + pnpm-workspace.yaml dynamically.
|
|
121
121
|
const rootPkg = buildRootPackageJson(projectName);
|
|
122
122
|
await fse.writeJson(path.join(projectRoot, 'package.json'), rootPkg, { spaces: 2 });
|
|
123
123
|
logs.push('✓ package.json');
|
package/src/index.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// index.js — entry point. Parse args +
|
|
2
|
+
// index.js — entry point. Parse args + run prompts (TTY) or generate directly (flags).
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
// create-app
|
|
6
|
-
// create-app --name my-shop --be nestjs non-interactive (
|
|
4
|
+
// Usage:
|
|
5
|
+
// create-app interactive (requires a real terminal)
|
|
6
|
+
// create-app --name my-shop --be nestjs non-interactive (runs anywhere)
|
|
7
7
|
// node src/index.js
|
|
8
8
|
//
|
|
9
9
|
// Flags:
|
|
10
|
-
// -h, --help
|
|
11
|
-
// -v, --version
|
|
12
|
-
// -n, --name <name>
|
|
10
|
+
// -h, --help print help
|
|
11
|
+
// -v, --version print version
|
|
12
|
+
// -n, --name <name> project name (non-interactive)
|
|
13
13
|
// --mobile <id> rn | expo | flutter
|
|
14
14
|
// --fe <id> nextjs | reactjs
|
|
15
15
|
// --be <id> nodejs | nestjs | php
|
|
16
|
-
// -y, --yes
|
|
16
|
+
// -y, --yes generate without prompting (needs --name + >=1 group)
|
|
17
17
|
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import { fileURLToPath } from 'node:url';
|
|
@@ -24,11 +24,11 @@ import { GROUPS, findOption } from './registry.js';
|
|
|
24
24
|
|
|
25
25
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
26
|
|
|
27
|
-
//
|
|
28
|
-
// 1)
|
|
29
|
-
// 2)
|
|
30
|
-
//
|
|
31
|
-
//
|
|
27
|
+
// Resolve the root folder that contains templates/. Supports 2 scenarios:
|
|
28
|
+
// 1) Running from the repo (clone): templates at ../../../templates (company-starter root)
|
|
29
|
+
// 2) Installed via npm (published): templates are bundled INSIDE the CLI package,
|
|
30
|
+
// at packages/cli/templates → ../templates
|
|
31
|
+
// Pick whichever location actually has a templates/ folder.
|
|
32
32
|
function resolveTemplatesRoot() {
|
|
33
33
|
const candidates = [
|
|
34
34
|
path.resolve(__dirname, '..'), // npm publish: cli/templates
|
|
@@ -37,12 +37,12 @@ function resolveTemplatesRoot() {
|
|
|
37
37
|
for (const root of candidates) {
|
|
38
38
|
if (fse.pathExistsSync(path.join(root, 'templates'))) return root;
|
|
39
39
|
}
|
|
40
|
-
// fallback: repo root (
|
|
40
|
+
// fallback: repo root (so generate.js can emit a clear error)
|
|
41
41
|
return path.resolve(__dirname, '..', '..', '..');
|
|
42
42
|
}
|
|
43
43
|
const REPO_ROOT = resolveTemplatesRoot();
|
|
44
44
|
|
|
45
|
-
//
|
|
45
|
+
// Map flag → groupId (for concise error hints).
|
|
46
46
|
const GROUP_FLAGS = { mobile: 'mobile', fe: 'fe', be: 'be' };
|
|
47
47
|
|
|
48
48
|
async function readVersion() {
|
|
@@ -56,30 +56,30 @@ async function readVersion() {
|
|
|
56
56
|
|
|
57
57
|
function printHelp() {
|
|
58
58
|
console.log(`
|
|
59
|
-
create-app —
|
|
59
|
+
create-app — scaffold a product monorepo from company templates
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
create-app
|
|
63
|
-
create-app --name my-shop --be nestjs non-interactive (
|
|
64
|
-
node src/index.js
|
|
61
|
+
Usage:
|
|
62
|
+
create-app interactive (requires a real terminal)
|
|
63
|
+
create-app --name my-shop --be nestjs non-interactive (runs anywhere)
|
|
64
|
+
node src/index.js equivalent (when not linked)
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
-h, --help
|
|
68
|
-
-v, --version
|
|
69
|
-
-n, --name <name>
|
|
66
|
+
Options:
|
|
67
|
+
-h, --help print this help
|
|
68
|
+
-v, --version print version
|
|
69
|
+
-n, --name <name> project name
|
|
70
70
|
--mobile <id> rn | expo | flutter
|
|
71
71
|
--fe <id> nextjs | reactjs
|
|
72
72
|
--be <id> nodejs | nestjs | php
|
|
73
|
-
-y, --yes generate
|
|
73
|
+
-y, --yes generate without prompting (needs --name + >=1 group)
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
Example:
|
|
76
76
|
create-app --name my-shop --mobile expo --fe nextjs --be nestjs
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
Add/remove templates: edit packages/cli/src/registry.js
|
|
79
79
|
`);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
// Parse argv
|
|
82
|
+
// Parse argv as "--key value" / "-k value" + boolean flags.
|
|
83
83
|
function parseArgs(argv) {
|
|
84
84
|
const out = { _flags: new Set() };
|
|
85
85
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -93,25 +93,25 @@ function parseArgs(argv) {
|
|
|
93
93
|
case '--fe': out.fe = argv[++i]; break;
|
|
94
94
|
case '--be': out.be = argv[++i]; break;
|
|
95
95
|
default:
|
|
96
|
-
throw new Error(`
|
|
96
|
+
throw new Error(`Invalid argument: ${a}. See: create-app --help`);
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
return out;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
//
|
|
102
|
+
// Any template-selection flag present → go the non-interactive route.
|
|
103
103
|
function hasSelectionFlags(args) {
|
|
104
104
|
return Boolean(args.name || args.mobile || args.fe || args.be || args._flags.has('yes'));
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
//
|
|
107
|
+
// Build a plan from flags, validating each id against the registry.
|
|
108
108
|
function planFromFlags(args) {
|
|
109
109
|
const name = (args.name ?? '').trim();
|
|
110
110
|
if (!name) {
|
|
111
|
-
throw new Error('
|
|
111
|
+
throw new Error('Missing --name <project-name>.');
|
|
112
112
|
}
|
|
113
113
|
if (!/^[a-z0-9][a-z0-9-_]*$/i.test(name)) {
|
|
114
|
-
throw new Error('
|
|
114
|
+
throw new Error('Project name may only contain letters/numbers/-/_ and must start with a letter/number.');
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
const selections = [];
|
|
@@ -121,13 +121,13 @@ function planFromFlags(args) {
|
|
|
121
121
|
const found = findOption(groupId, optionId);
|
|
122
122
|
if (!found) {
|
|
123
123
|
const valid = GROUPS.find((g) => g.id === groupId).options.map((o) => o.id).join(', ');
|
|
124
|
-
throw new Error(`--${flag} "${optionId}"
|
|
124
|
+
throw new Error(`--${flag} "${optionId}" is invalid. Valid: ${valid}.`);
|
|
125
125
|
}
|
|
126
126
|
selections.push(found);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
if (selections.length === 0) {
|
|
130
|
-
throw new Error('
|
|
130
|
+
throw new Error('At least one group is required: --mobile / --fe / --be.');
|
|
131
131
|
}
|
|
132
132
|
return { projectName: name, selections };
|
|
133
133
|
}
|
|
@@ -144,17 +144,17 @@ async function main() {
|
|
|
144
144
|
return;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
//
|
|
148
|
-
// -
|
|
149
|
-
// -
|
|
150
|
-
// -
|
|
147
|
+
// Decide the flow:
|
|
148
|
+
// - selection flags present → non-interactive
|
|
149
|
+
// - no flags BUT stdin is not a TTY → fail with a clear error instead of hanging
|
|
150
|
+
// - otherwise → interactive
|
|
151
151
|
let plan;
|
|
152
152
|
if (hasSelectionFlags(args)) {
|
|
153
153
|
plan = planFromFlags(args);
|
|
154
154
|
} else if (!process.stdin.isTTY) {
|
|
155
155
|
throw new Error(
|
|
156
|
-
'
|
|
157
|
-
' →
|
|
156
|
+
'This terminal does not support interactive mode (stdin is not a TTY).\n' +
|
|
157
|
+
' → Run it in Terminal.app/iTerm, OR use flags:\n' +
|
|
158
158
|
' create-app --name my-shop --mobile expo --fe nextjs --be nestjs',
|
|
159
159
|
);
|
|
160
160
|
} else {
|
|
@@ -168,13 +168,13 @@ async function main() {
|
|
|
168
168
|
console.log('');
|
|
169
169
|
|
|
170
170
|
const rel = path.relative(process.cwd(), projectRoot) || projectRoot;
|
|
171
|
-
const msg = `
|
|
172
|
-
// outro() (clack)
|
|
171
|
+
const msg = `Done! Next:\n\n cd ${rel} && pnpm install\n`;
|
|
172
|
+
// outro() (clack) only looks good in a TTY; print plainly in non-interactive mode.
|
|
173
173
|
if (process.stdout.isTTY && !hasSelectionFlags(args)) outro(msg);
|
|
174
174
|
else console.log(msg);
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
main().catch((err) => {
|
|
178
|
-
console.error('\n✗
|
|
178
|
+
console.error('\n✗ Error:', err.message);
|
|
179
179
|
process.exit(1);
|
|
180
180
|
});
|
package/src/prompts.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// prompts.js —
|
|
1
|
+
// prompts.js — interactive prompt logic.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// Returns a pure-data "plan" for generate.js to execute:
|
|
4
4
|
// { projectName, selections: [{ group, option }, ...] }
|
|
5
|
-
//
|
|
5
|
+
// No disk I/O happens here — this only asks and validates.
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
intro,
|
|
@@ -17,47 +17,47 @@ import { GROUPS, findOption } from './registry.js';
|
|
|
17
17
|
|
|
18
18
|
function ensureNotCancelled(value) {
|
|
19
19
|
if (isCancel(value)) {
|
|
20
|
-
cancel('
|
|
20
|
+
cancel('Cancelled. Nothing was created.');
|
|
21
21
|
process.exit(0);
|
|
22
22
|
}
|
|
23
23
|
return value;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export async function runPrompts() {
|
|
27
|
-
intro('create-app —
|
|
27
|
+
intro('create-app — scaffold a product monorepo from company templates');
|
|
28
28
|
|
|
29
|
-
// 1)
|
|
29
|
+
// 1) Project name
|
|
30
30
|
const projectName = ensureNotCancelled(
|
|
31
31
|
await text({
|
|
32
|
-
message: '
|
|
32
|
+
message: 'Project name?',
|
|
33
33
|
placeholder: 'my-shop',
|
|
34
34
|
validate(value) {
|
|
35
35
|
const v = (value ?? '').trim();
|
|
36
|
-
if (!v) return '
|
|
36
|
+
if (!v) return 'Project name must not be empty.';
|
|
37
37
|
if (!/^[a-z0-9][a-z0-9-_]*$/i.test(v)) {
|
|
38
|
-
return '
|
|
38
|
+
return 'Use only letters, numbers, "-" or "_", and start with a letter/number.';
|
|
39
39
|
}
|
|
40
40
|
return undefined;
|
|
41
41
|
},
|
|
42
42
|
}),
|
|
43
43
|
);
|
|
44
44
|
|
|
45
|
-
// 2) Multi-select
|
|
45
|
+
// 2) Multi-select the groups to include (you can pick several)
|
|
46
46
|
const groupIds = ensureNotCancelled(
|
|
47
47
|
await multiselect({
|
|
48
|
-
message: '
|
|
48
|
+
message: 'Select the groups you need (Space to select, Enter to confirm):',
|
|
49
49
|
options: GROUPS.map((g) => ({ value: g.id, label: g.label })),
|
|
50
50
|
required: true,
|
|
51
51
|
}),
|
|
52
52
|
);
|
|
53
53
|
|
|
54
|
-
// 3)
|
|
54
|
+
// 3) For each selected group → single-select one technology
|
|
55
55
|
const selections = [];
|
|
56
56
|
for (const groupId of groupIds) {
|
|
57
57
|
const group = GROUPS.find((g) => g.id === groupId);
|
|
58
58
|
const optionId = ensureNotCancelled(
|
|
59
59
|
await select({
|
|
60
|
-
message: `${group.label}:
|
|
60
|
+
message: `${group.label}: pick one technology`,
|
|
61
61
|
options: group.options.map((o) => ({ value: o.id, label: o.label })),
|
|
62
62
|
}),
|
|
63
63
|
);
|
package/src/registry.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
// registry.js —
|
|
1
|
+
// registry.js — declares all groups + technologies + template paths.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// This is the single source of truth for adding/removing templates. To add a new
|
|
4
|
+
// technology, just add one object to the relevant group's `options` (or add a whole
|
|
5
|
+
// new group). You do NOT need to touch the logic in prompts.js / generate.js.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
// id :
|
|
9
|
-
// label :
|
|
10
|
-
// templatePath :
|
|
7
|
+
// Each option:
|
|
8
|
+
// id : short identifier (used internally)
|
|
9
|
+
// label : display label shown when prompting
|
|
10
|
+
// templatePath : path to the template, RELATIVE TO the company-starter repo root
|
|
11
11
|
// lang : 'js' | 'dart' | 'php' ...
|
|
12
|
-
//
|
|
12
|
+
// Only options with lang === 'js' are eligible to use packages/shared.
|
|
13
13
|
|
|
14
14
|
export const GROUPS = [
|
|
15
15
|
{
|
|
@@ -43,11 +43,11 @@ export const GROUPS = [
|
|
|
43
43
|
},
|
|
44
44
|
];
|
|
45
45
|
|
|
46
|
-
//
|
|
46
|
+
// Path to the shared template folder (only copied when >= 2 selections have lang === 'js').
|
|
47
47
|
export const SHARED_TEMPLATE_PATH = 'templates/shared';
|
|
48
48
|
export const SHARED_OUTPUT_DIR = 'packages/shared';
|
|
49
49
|
|
|
50
|
-
//
|
|
50
|
+
// Quickly look up a single option by (groupId, optionId).
|
|
51
51
|
export function findOption(groupId, optionId) {
|
|
52
52
|
const group = GROUPS.find((g) => g.id === groupId);
|
|
53
53
|
if (!group) return null;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Placeholder: NestJS (be/nestjs)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This is a **placeholder** for the **NestJS** template.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Drop the standard NestJS source here. Conventions:
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
7
|
+
- Name the template's `package.json` as **`_package.json`** (the CLI renames it on generate).
|
|
8
|
+
- Name `.gitignore` as **`_gitignore`**, `.npmrc` as **`_npmrc`** if present.
|
|
9
|
+
- Wherever you need the project name, write `{{PROJECT_NAME}}` — the CLI replaces it with the real name.
|
|
10
10
|
|
|
11
|
-
`lang: js` → app
|
|
11
|
+
`lang: js` → this app counts toward the condition for creating `packages/shared`.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Placeholder: Node.js (be/nodejs)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This is a **placeholder** for the **Node.js** template.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Drop the standard Node.js backend source here. Conventions:
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
7
|
+
- Name the template's `package.json` as **`_package.json`** (the CLI renames it on generate).
|
|
8
|
+
- Name `.gitignore` as **`_gitignore`**, `.npmrc` as **`_npmrc`** if present.
|
|
9
|
+
- Wherever you need the project name, write `{{PROJECT_NAME}}` — the CLI replaces it with the real name.
|
|
10
10
|
|
|
11
|
-
`lang: js` → app
|
|
11
|
+
`lang: js` → this app counts toward the condition for creating `packages/shared`.
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# Placeholder: PHP (be/php)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This is a **placeholder** for the **PHP** template.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Drop the standard PHP backend source here.
|
|
6
6
|
|
|
7
|
-
> **
|
|
8
|
-
> - **
|
|
9
|
-
> - **
|
|
10
|
-
> - **
|
|
7
|
+
> **Notes specific to PHP (`lang: php`):**
|
|
8
|
+
> - **No** `_package.json` (PHP uses `composer.json`, not npm).
|
|
9
|
+
> - **Not** part of the project's npm/pnpm workspaces.
|
|
10
|
+
> - **Does not** use `packages/shared` (shared is JS/TS only).
|
|
11
11
|
>
|
|
12
|
-
>
|
|
13
|
-
>
|
|
12
|
+
> On generate, the CLI still copies this whole folder into `apps/backend` and replaces
|
|
13
|
+
> `{{PROJECT_NAME}}` in text files (e.g. `composer.json`, `README.md`).
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
You can put `{{PROJECT_NAME}}` in `composer.json` and the CLI will fill in the project name.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Placeholder: Next.js (fe/nextjs)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This is a **placeholder** for the **Next.js** template.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Drop the standard Next.js source here. Conventions:
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
7
|
+
- Name the template's `package.json` as **`_package.json`** (the CLI renames it on generate).
|
|
8
|
+
- Name `.gitignore` as **`_gitignore`**, `.npmrc` as **`_npmrc`** if present.
|
|
9
|
+
- Wherever you need the project name, write `{{PROJECT_NAME}}` — the CLI replaces it with the real name.
|
|
10
10
|
|
|
11
|
-
`lang: js` → app
|
|
11
|
+
`lang: js` → this app counts toward the condition for creating `packages/shared`.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Placeholder: ReactJS (fe/reactjs)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This is a **placeholder** for the **ReactJS** template.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Drop the standard ReactJS source here. Conventions:
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
7
|
+
- Name the template's `package.json` as **`_package.json`** (the CLI renames it on generate).
|
|
8
|
+
- Name `.gitignore` as **`_gitignore`**, `.npmrc` as **`_npmrc`** if present.
|
|
9
|
+
- Wherever you need the project name, write `{{PROJECT_NAME}}` — the CLI replaces it with the real name.
|
|
10
10
|
|
|
11
|
-
`lang: js` → app
|
|
11
|
+
`lang: js` → this app counts toward the condition for creating `packages/shared`.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Placeholder: Expo (mobile/expo)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This is a **placeholder** for the **Expo** template.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Drop the standard Expo source here. Conventions:
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
7
|
+
- Name the template's `package.json` as **`_package.json`** (the CLI renames it on generate).
|
|
8
|
+
- Name `.gitignore` as **`_gitignore`**, `.npmrc` as **`_npmrc`** if present.
|
|
9
|
+
- Wherever you need the project name, write `{{PROJECT_NAME}}` — the CLI replaces it with the real name.
|
|
10
10
|
|
|
11
|
-
`lang: js` → app
|
|
11
|
+
`lang: js` → this app counts toward the condition for creating `packages/shared`.
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# Placeholder: Flutter (mobile/flutter)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This is a **placeholder** for the **Flutter** template.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Drop the standard Flutter (Dart) source here.
|
|
6
6
|
|
|
7
|
-
> **
|
|
8
|
-
> - **
|
|
9
|
-
> - **
|
|
10
|
-
> - **
|
|
7
|
+
> **Notes specific to Flutter (`lang: dart`):**
|
|
8
|
+
> - **No** `_package.json` (Flutter uses `pubspec.yaml`, not npm).
|
|
9
|
+
> - **Not** part of the project's npm/pnpm workspaces.
|
|
10
|
+
> - **Does not** use `packages/shared` (shared is JS/TS only).
|
|
11
11
|
>
|
|
12
|
-
>
|
|
13
|
-
>
|
|
12
|
+
> On generate, the CLI still copies this whole folder into `apps/mobile` and replaces
|
|
13
|
+
> `{{PROJECT_NAME}}` in text files (e.g. `pubspec.yaml`, `README.md`).
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
You can put `{{PROJECT_NAME}}` in `pubspec.yaml` and the CLI will fill in the project name.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Placeholder: React Native (mobile/rn)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This is a **placeholder** for the **React Native** template.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Drop the standard React Native source here. Conventions:
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
7
|
+
- Name the template's `package.json` as **`_package.json`** (the CLI renames it on generate).
|
|
8
|
+
- Name `.gitignore` as **`_gitignore`**, `.npmrc` as **`_npmrc`** if present.
|
|
9
|
+
- Wherever you need the project name, write `{{PROJECT_NAME}}` — the CLI replaces it with the real name.
|
|
10
10
|
|
|
11
|
-
`lang: js` → app
|
|
11
|
+
`lang: js` → this app counts toward the condition for creating `packages/shared`.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Placeholder: shared (packages/shared)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This is a **placeholder** for the shared **JS/TS** package: shared types + api client.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Drop the standard shared source (types, api client, ...) for the JS/TS apps here. Conventions:
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
7
|
+
- Name the template's `package.json` as **`_package.json`** (the CLI renames it on generate).
|
|
8
|
+
- Wherever you need the project name, write `{{PROJECT_NAME}}` — the CLI replaces it with the real name.
|
|
9
9
|
|
|
10
|
-
>
|
|
11
|
-
>
|
|
10
|
+
> This package is **created only** when the project has **>= 2** selections with `lang === 'js'`.
|
|
11
|
+
> Otherwise the CLI skips it and prints a notice.
|