@sb-codex/create-sb-app 0.0.1 → 0.0.3
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 +26 -14
- package/dist/index.js +112 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,40 +1,52 @@
|
|
|
1
1
|
# @sb-codex/create-sb-app
|
|
2
2
|
|
|
3
|
-
Scaffold a new multi-tenant SaaS project from the [sb-codex starter](https://github.com/SB-SLIM/react-app-starter).
|
|
3
|
+
Scaffold a new multi-tenant SaaS project from the [sb-codex starter](https://github.com/SB-SLIM/react-app-starter).
|
|
4
|
+
|
|
5
|
+
It generates an **apps-only** project: you get `apps/` (admin, server, web, e2e) and the `@sb-codex/*` plugins are pulled from **npm** at their published versions — there is **no `packages/` folder** to maintain. The CLI also injects each plugin's peer dependencies into the consuming apps so the project installs cleanly.
|
|
4
6
|
|
|
5
7
|
## Usage
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
|
-
pnpm create @sb-codex/sb-app my-saas
|
|
9
|
-
# or: npm create @sb-codex/sb-app my-saas
|
|
10
|
+
pnpm create @sb-codex/sb-app@latest my-saas
|
|
11
|
+
# or: npm create @sb-codex/sb-app@latest my-saas
|
|
10
12
|
```
|
|
11
13
|
|
|
14
|
+
> The `@latest` tag forces the newest version and avoids npx serving a cached older release.
|
|
15
|
+
|
|
12
16
|
Prompts:
|
|
13
17
|
|
|
14
18
|
- **Project directory** — where to create the project
|
|
15
19
|
- **Project name** — root `package.json` name
|
|
16
|
-
- **Production domain** — replaces the reference domain
|
|
20
|
+
- **Production domain** — replaces the reference domain. **Defaults to `localhost`** for local dev (e.g. `hub.localhost`)
|
|
17
21
|
- **git init** — start with fresh git history
|
|
18
22
|
|
|
19
23
|
Then:
|
|
20
24
|
|
|
21
25
|
```bash
|
|
22
26
|
cd my-saas
|
|
23
|
-
pnpm install
|
|
27
|
+
pnpm install # plugins resolved from npm — no local build
|
|
24
28
|
pnpm dev
|
|
25
29
|
```
|
|
26
30
|
|
|
27
|
-
## What it
|
|
31
|
+
## What it does
|
|
32
|
+
|
|
33
|
+
- Pulls `apps/` from the starter; rewrites each `@sb-codex/*` dependency from `workspace:^` to the **published npm version** and injects that plugin's peer dependencies
|
|
34
|
+
- Removes the `packages/` source, trims `pnpm-workspace.yaml` to `apps/*`, strips `@sb-codex/*` from `pnpm.overrides`, drops the changeset config
|
|
35
|
+
- Copies `.env.example` → `.env`
|
|
36
|
+
- Replaces project-specific tokens:
|
|
28
37
|
|
|
29
|
-
| Token | Replaced with
|
|
30
|
-
| ----------------------------------- |
|
|
31
|
-
| `slimbouchoucha.tn` | your domain
|
|
32
|
-
| `ghcr.io/sb-slim/react-app-starter` | `ghcr.io/your-gh-org/<name>`
|
|
33
|
-
| `SB-SLIM/react-app-starter` | `your-gh-org/<name>`
|
|
34
|
-
|
|
|
35
|
-
| Notion page id | `YOUR_NOTION_PAGE_ID` |
|
|
38
|
+
| Token | Replaced with |
|
|
39
|
+
| ----------------------------------- | --------------------------------- |
|
|
40
|
+
| `slimbouchoucha.tn` | your domain (default `localhost`) |
|
|
41
|
+
| `ghcr.io/sb-slim/react-app-starter` | `ghcr.io/your-gh-org/<name>` |
|
|
42
|
+
| `SB-SLIM/react-app-starter` | `your-gh-org/<name>` |
|
|
43
|
+
| Notion page id | `YOUR_NOTION_PAGE_ID` |
|
|
36
44
|
|
|
37
|
-
|
|
45
|
+
## Non-interactive flags
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx @sb-codex/create-sb-app my-saas --name my-saas --domain localhost --no-git
|
|
49
|
+
```
|
|
38
50
|
|
|
39
51
|
## Local usage (from the monorepo)
|
|
40
52
|
|
package/dist/index.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import {
|
|
5
|
-
existsSync,
|
|
6
|
-
readdirSync,
|
|
7
|
-
readFileSync,
|
|
8
|
-
writeFileSync,
|
|
9
|
-
rmSync,
|
|
5
|
+
existsSync as existsSync2,
|
|
6
|
+
readdirSync as readdirSync2,
|
|
7
|
+
readFileSync as readFileSync2,
|
|
8
|
+
writeFileSync as writeFileSync2,
|
|
9
|
+
rmSync as rmSync2,
|
|
10
10
|
copyFileSync
|
|
11
11
|
} from "fs";
|
|
12
|
-
import { join, extname, basename } from "path";
|
|
12
|
+
import { join as join2, extname, basename } from "path";
|
|
13
13
|
import { execSync } from "child_process";
|
|
14
14
|
import {
|
|
15
15
|
intro,
|
|
@@ -44,8 +44,6 @@ function buildReplacements(answers) {
|
|
|
44
44
|
},
|
|
45
45
|
// Remaining bare repo name (URLs, image names)
|
|
46
46
|
{ find: /react-app-starter/g, replace: name },
|
|
47
|
-
// VPS IP
|
|
48
|
-
{ find: /152\.53\.187\.54/g, replace: "YOUR_VPS_IP" },
|
|
49
47
|
// Notion page id (mirror of architecture docs — project specific)
|
|
50
48
|
{ find: NOTION_PAGE_ID_DASHED, replace: "YOUR_NOTION_PAGE_ID" },
|
|
51
49
|
{ find: NOTION_PAGE_ID_COMPACT, replace: "YOUR_NOTION_PAGE_ID" }
|
|
@@ -76,6 +74,87 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
76
74
|
"build"
|
|
77
75
|
]);
|
|
78
76
|
|
|
77
|
+
// src/transform.ts
|
|
78
|
+
import {
|
|
79
|
+
existsSync,
|
|
80
|
+
readFileSync,
|
|
81
|
+
writeFileSync,
|
|
82
|
+
rmSync,
|
|
83
|
+
readdirSync
|
|
84
|
+
} from "fs";
|
|
85
|
+
import { join } from "path";
|
|
86
|
+
function readJson(path) {
|
|
87
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
88
|
+
}
|
|
89
|
+
function writeJson(path, data) {
|
|
90
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
|
|
91
|
+
}
|
|
92
|
+
function applyAppsOnly(targetDir) {
|
|
93
|
+
const packagesDir = join(targetDir, "packages");
|
|
94
|
+
const pkgMap = /* @__PURE__ */ new Map();
|
|
95
|
+
if (existsSync(packagesDir)) {
|
|
96
|
+
for (const entry of readdirSync(packagesDir, { withFileTypes: true })) {
|
|
97
|
+
if (!entry.isDirectory()) continue;
|
|
98
|
+
const pj = join(packagesDir, entry.name, "package.json");
|
|
99
|
+
if (!existsSync(pj)) continue;
|
|
100
|
+
const pkg = readJson(pj);
|
|
101
|
+
const name = pkg.name;
|
|
102
|
+
if (typeof name === "string" && name.startsWith("@sb-codex/")) {
|
|
103
|
+
pkgMap.set(name, {
|
|
104
|
+
version: String(pkg.version ?? "0.0.0"),
|
|
105
|
+
peerDependencies: pkg.peerDependencies ?? {}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const appsDir = join(targetDir, "apps");
|
|
111
|
+
if (existsSync(appsDir)) {
|
|
112
|
+
for (const entry of readdirSync(appsDir, { withFileTypes: true })) {
|
|
113
|
+
if (!entry.isDirectory()) continue;
|
|
114
|
+
const pj = join(appsDir, entry.name, "package.json");
|
|
115
|
+
if (!existsSync(pj)) continue;
|
|
116
|
+
const pkg = readJson(pj);
|
|
117
|
+
const deps = pkg.dependencies ?? {};
|
|
118
|
+
const devDeps = pkg.devDependencies ?? {};
|
|
119
|
+
let changed = false;
|
|
120
|
+
for (const depName of Object.keys(deps)) {
|
|
121
|
+
const info = pkgMap.get(depName);
|
|
122
|
+
if (!info) continue;
|
|
123
|
+
deps[depName] = `^${info.version}`;
|
|
124
|
+
changed = true;
|
|
125
|
+
for (const [peer, range] of Object.entries(info.peerDependencies)) {
|
|
126
|
+
if (!deps[peer] && !devDeps[peer]) deps[peer] = range;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (changed) {
|
|
130
|
+
pkg.dependencies = deps;
|
|
131
|
+
writeJson(pj, pkg);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (existsSync(packagesDir))
|
|
136
|
+
rmSync(packagesDir, { recursive: true, force: true });
|
|
137
|
+
const wsPath = join(targetDir, "pnpm-workspace.yaml");
|
|
138
|
+
if (existsSync(wsPath)) {
|
|
139
|
+
const kept = readFileSync(wsPath, "utf8").split(/\r?\n/).filter((line) => !/['"]packages\/\*['"]/.test(line)).join("\n");
|
|
140
|
+
writeFileSync(wsPath, kept);
|
|
141
|
+
}
|
|
142
|
+
const rootPj = join(targetDir, "package.json");
|
|
143
|
+
if (existsSync(rootPj)) {
|
|
144
|
+
const pkg = readJson(rootPj);
|
|
145
|
+
const pnpm = pkg.pnpm;
|
|
146
|
+
if (pnpm?.overrides) {
|
|
147
|
+
for (const key of Object.keys(pnpm.overrides)) {
|
|
148
|
+
if (key.startsWith("@sb-codex/")) delete pnpm.overrides[key];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
writeJson(rootPj, pkg);
|
|
152
|
+
}
|
|
153
|
+
const changesetDir = join(targetDir, ".changeset");
|
|
154
|
+
if (existsSync(changesetDir))
|
|
155
|
+
rmSync(changesetDir, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
|
|
79
158
|
// src/index.ts
|
|
80
159
|
var TEMPLATE = "github:SB-SLIM/react-app-starter";
|
|
81
160
|
function bail(message) {
|
|
@@ -107,14 +186,14 @@ function parseFlags(argv) {
|
|
|
107
186
|
return flags;
|
|
108
187
|
}
|
|
109
188
|
function collectTextFiles(dir, acc = []) {
|
|
110
|
-
for (const entry of
|
|
189
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
111
190
|
if (entry.isDirectory()) {
|
|
112
191
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
113
|
-
collectTextFiles(
|
|
192
|
+
collectTextFiles(join2(dir, entry.name), acc);
|
|
114
193
|
} else if (entry.isFile()) {
|
|
115
194
|
const name = entry.name;
|
|
116
195
|
if (TEXT_EXTENSIONS.has(extname(name)) || name.startsWith(".env")) {
|
|
117
|
-
acc.push(
|
|
196
|
+
acc.push(join2(dir, name));
|
|
118
197
|
}
|
|
119
198
|
}
|
|
120
199
|
}
|
|
@@ -135,8 +214,8 @@ async function main() {
|
|
|
135
214
|
if (isCancel(answer)) bail("Cancelled.");
|
|
136
215
|
target = answer;
|
|
137
216
|
}
|
|
138
|
-
const targetDir =
|
|
139
|
-
if (
|
|
217
|
+
const targetDir = join2(process.cwd(), target);
|
|
218
|
+
if (existsSync2(targetDir) && readdirSync2(targetDir).length > 0) {
|
|
140
219
|
bail(`Directory "${target}" already exists and is not empty.`);
|
|
141
220
|
}
|
|
142
221
|
let name;
|
|
@@ -156,12 +235,12 @@ async function main() {
|
|
|
156
235
|
domain = flags.domain;
|
|
157
236
|
} else {
|
|
158
237
|
const domainAnswer = await text({
|
|
159
|
-
message: "Production domain?",
|
|
160
|
-
placeholder: "
|
|
161
|
-
defaultValue: "
|
|
238
|
+
message: "Production domain? (defaults to localhost for local dev)",
|
|
239
|
+
placeholder: "localhost",
|
|
240
|
+
defaultValue: "localhost"
|
|
162
241
|
});
|
|
163
242
|
if (isCancel(domainAnswer)) bail("Cancelled.");
|
|
164
|
-
domain = domainAnswer || "
|
|
243
|
+
domain = domainAnswer || "localhost";
|
|
165
244
|
}
|
|
166
245
|
let doGit;
|
|
167
246
|
if (flags.git === true || flags["no-git"] === true) {
|
|
@@ -184,16 +263,15 @@ async function main() {
|
|
|
184
263
|
bail(err instanceof Error ? err.message : String(err));
|
|
185
264
|
}
|
|
186
265
|
s.stop("Template downloaded");
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (existsSync(lockfile)) rmSync(lockfile, { force: true });
|
|
266
|
+
applyAppsOnly(targetDir);
|
|
267
|
+
const gitDir = join2(targetDir, ".git");
|
|
268
|
+
if (existsSync2(gitDir)) rmSync2(gitDir, { recursive: true, force: true });
|
|
269
|
+
const lockfile = join2(targetDir, "pnpm-lock.yaml");
|
|
270
|
+
if (existsSync2(lockfile)) rmSync2(lockfile, { force: true });
|
|
193
271
|
s.start("Personalizing project");
|
|
194
272
|
const replacements = buildReplacements(answers);
|
|
195
273
|
for (const file of collectTextFiles(targetDir)) {
|
|
196
|
-
let content =
|
|
274
|
+
let content = readFileSync2(file, "utf8");
|
|
197
275
|
let changed = false;
|
|
198
276
|
for (const { find, replace } of replacements) {
|
|
199
277
|
if (find.test(content)) {
|
|
@@ -201,17 +279,17 @@ async function main() {
|
|
|
201
279
|
changed = true;
|
|
202
280
|
}
|
|
203
281
|
}
|
|
204
|
-
if (changed)
|
|
282
|
+
if (changed) writeFileSync2(file, content);
|
|
205
283
|
}
|
|
206
|
-
const rootPkgPath =
|
|
207
|
-
if (
|
|
208
|
-
const pkg = JSON.parse(
|
|
284
|
+
const rootPkgPath = join2(targetDir, "package.json");
|
|
285
|
+
if (existsSync2(rootPkgPath)) {
|
|
286
|
+
const pkg = JSON.parse(readFileSync2(rootPkgPath, "utf8"));
|
|
209
287
|
pkg.name = answers.name;
|
|
210
|
-
|
|
288
|
+
writeFileSync2(rootPkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
211
289
|
}
|
|
212
|
-
const envExample =
|
|
213
|
-
const envFile =
|
|
214
|
-
if (
|
|
290
|
+
const envExample = join2(targetDir, ".env.example");
|
|
291
|
+
const envFile = join2(targetDir, ".env");
|
|
292
|
+
if (existsSync2(envExample) && !existsSync2(envFile)) {
|
|
215
293
|
copyFileSync(envExample, envFile);
|
|
216
294
|
}
|
|
217
295
|
s.stop("Project personalized");
|
|
@@ -229,13 +307,13 @@ async function main() {
|
|
|
229
307
|
" server Fastify 5 + tRPC v11 + Pino (stateless API)",
|
|
230
308
|
" web Next.js 15 marketing site",
|
|
231
309
|
" e2e Playwright",
|
|
232
|
-
pc.bold("
|
|
310
|
+
pc.bold("@sb-codex/* plugins (from npm)"),
|
|
233
311
|
" core \xB7 config \xB7 db \xB7 auth \xB7 api-contracts \xB7 jobs \xB7 ui-components",
|
|
234
312
|
pc.bold("infra/"),
|
|
235
313
|
" docker \xB7 compose \xB7 traefik",
|
|
236
314
|
"",
|
|
237
315
|
pc.dim("Multi-tenant: Postgres + RLS, x-workspace-slug header."),
|
|
238
|
-
pc.dim("
|
|
316
|
+
pc.dim("Plugins resolved from npm \u2014 no packages/ folder.")
|
|
239
317
|
].join("\n"),
|
|
240
318
|
"Architecture"
|
|
241
319
|
);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@sb-codex/create-sb-app",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.3",
|
|
5
5
|
"description": "Scaffold a new multi-tenant SaaS project from the sb-codex starter.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|