partweave 0.2.0 → 0.3.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.
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/commands/create.ts
|
|
4
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
4
5
|
import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
|
|
5
6
|
import { basename, resolve as resolve3 } from "path";
|
|
6
|
-
import { intro, log, note, outro, spinner } from "@clack/prompts";
|
|
7
|
+
import { confirm as confirm3, intro, isCancel as isCancel3, log as log2, note, outro, spinner as spinner2 } from "@clack/prompts";
|
|
7
8
|
import pc2 from "picocolors";
|
|
8
9
|
|
|
9
10
|
// src/compose.ts
|
|
@@ -42,6 +43,13 @@ function parseDep(dep) {
|
|
|
42
43
|
}
|
|
43
44
|
return { name: dep, version: "latest" };
|
|
44
45
|
}
|
|
46
|
+
function normalizeWorkspaceDeps(deps, workspaceRange) {
|
|
47
|
+
return deps.map((dep) => {
|
|
48
|
+
const { name, version } = parseDep(dep);
|
|
49
|
+
if (version.startsWith("workspace:")) return `${name}@${workspaceRange}`;
|
|
50
|
+
return dep;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
45
53
|
function mergePackageJsonDeps(pkgJson, deps, field = "dependencies") {
|
|
46
54
|
if (deps.length === 0) return pkgJson;
|
|
47
55
|
const pkg = JSON.parse(pkgJson);
|
|
@@ -180,7 +188,8 @@ function jsPmProfile(name) {
|
|
|
180
188
|
runAll: (script) => `npm run ${script} --workspaces --if-present`,
|
|
181
189
|
packageManagerField: null,
|
|
182
190
|
usesWorkspaceYaml: false,
|
|
183
|
-
needsHoistNpmrc: false
|
|
191
|
+
needsHoistNpmrc: false,
|
|
192
|
+
workspaceRange: "*"
|
|
184
193
|
};
|
|
185
194
|
}
|
|
186
195
|
return {
|
|
@@ -190,7 +199,8 @@ function jsPmProfile(name) {
|
|
|
190
199
|
runAll: (script) => `pnpm -r ${script}`,
|
|
191
200
|
packageManagerField: PNPM_VERSION,
|
|
192
201
|
usesWorkspaceYaml: true,
|
|
193
|
-
needsHoistNpmrc: true
|
|
202
|
+
needsHoistNpmrc: true,
|
|
203
|
+
workspaceRange: "workspace:*"
|
|
194
204
|
};
|
|
195
205
|
}
|
|
196
206
|
var PIP_SYNC = "python3 -m venv .venv && .venv/bin/python -m pip install -U pip && .venv/bin/python scripts/sync_deps.py";
|
|
@@ -210,6 +220,34 @@ function pyPmProfile(name) {
|
|
|
210
220
|
needsSyncScript: false
|
|
211
221
|
};
|
|
212
222
|
}
|
|
223
|
+
function jsPmInstallPlan(pm) {
|
|
224
|
+
if (pm === "npm") return null;
|
|
225
|
+
return {
|
|
226
|
+
label: "Enabling pnpm via corepack",
|
|
227
|
+
cmd: "corepack",
|
|
228
|
+
args: ["enable", "pnpm"],
|
|
229
|
+
hint: "corepack enable pnpm (or: npm i -g pnpm)"
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function pyPmInstallPlan(pm) {
|
|
233
|
+
if (pm === "pip") return null;
|
|
234
|
+
if (process.platform === "win32") {
|
|
235
|
+
const ps = "irm https://astral.sh/uv/install.ps1 | iex";
|
|
236
|
+
return {
|
|
237
|
+
label: "Installing uv",
|
|
238
|
+
cmd: "powershell",
|
|
239
|
+
args: ["-ExecutionPolicy", "Bypass", "-c", ps],
|
|
240
|
+
hint: `powershell -ExecutionPolicy Bypass -c "${ps}"`
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const sh = "curl -LsSf https://astral.sh/uv/install.sh | sh";
|
|
244
|
+
return {
|
|
245
|
+
label: "Installing uv",
|
|
246
|
+
cmd: "sh",
|
|
247
|
+
args: ["-c", sh],
|
|
248
|
+
hint: sh
|
|
249
|
+
};
|
|
250
|
+
}
|
|
213
251
|
function hasCommand(cmd) {
|
|
214
252
|
try {
|
|
215
253
|
const probe = process.platform === "win32" ? spawnSync("where", [cmd], { stdio: "ignore" }) : spawnSync("sh", ["-c", `command -v ${cmd}`], { stdio: "ignore" });
|
|
@@ -241,73 +279,206 @@ function buildJsWorkspace(ctx) {
|
|
|
241
279
|
if (members.length === 0) return null;
|
|
242
280
|
return "packages:\n" + members.map((m) => ` - "${m}"`).join("\n") + "\n";
|
|
243
281
|
}
|
|
244
|
-
function buildRootPackageJson(ctx) {
|
|
282
|
+
function buildRootPackageJson(ctx, hasDocker) {
|
|
245
283
|
const anyJs = ctx.hasWeb || ctx.hasMobile || ctx.hasShared;
|
|
246
|
-
if (!anyJs) return null;
|
|
284
|
+
if (!anyJs && !ctx.hasServer) return null;
|
|
247
285
|
const pm = jsPmProfile(ctx.jsPm);
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
if (ctx.
|
|
251
|
-
if (ctx.
|
|
252
|
-
|
|
286
|
+
const t = (task) => `node scripts/run.mjs ${task}`;
|
|
287
|
+
const scripts = { bootstrap: t("bootstrap"), dev: t("dev") };
|
|
288
|
+
if (ctx.hasWeb) scripts["web"] = t("web");
|
|
289
|
+
if (ctx.hasMobile) scripts["mobile"] = t("mobile");
|
|
290
|
+
if (ctx.hasServer) {
|
|
291
|
+
scripts["server"] = t("server");
|
|
292
|
+
scripts["migrate"] = t("migrate");
|
|
293
|
+
scripts["superuser"] = t("superuser");
|
|
294
|
+
if (hasDocker) {
|
|
295
|
+
scripts["db:up"] = t("db:up");
|
|
296
|
+
scripts["db:down"] = t("db:down");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (ctx.hasApiClient) scripts["gen:api"] = t("gen:api");
|
|
300
|
+
if (anyJs) scripts["typecheck"] = pm.runAll("typecheck");
|
|
253
301
|
const pkg = {
|
|
254
302
|
name: ctx.projectSlug,
|
|
255
303
|
version: "0.1.0",
|
|
256
304
|
private: true
|
|
257
305
|
};
|
|
258
|
-
if (pm.packageManagerField) pkg.packageManager = pm.packageManagerField;
|
|
259
|
-
if (!pm.usesWorkspaceYaml) pkg.workspaces = jsMembers(ctx);
|
|
306
|
+
if (anyJs && pm.packageManagerField) pkg.packageManager = pm.packageManagerField;
|
|
307
|
+
if (anyJs && !pm.usesWorkspaceYaml) pkg.workspaces = jsMembers(ctx);
|
|
260
308
|
pkg.scripts = scripts;
|
|
261
|
-
pkg.devDependencies = { typescript: "^5.7.2" };
|
|
309
|
+
if (anyJs) pkg.devDependencies = { typescript: "^5.7.2" };
|
|
262
310
|
return JSON.stringify(pkg, null, 2) + "\n";
|
|
263
311
|
}
|
|
312
|
+
function buildTaskRunner(ctx, hasDocker) {
|
|
313
|
+
const anyJs = ctx.hasWeb || ctx.hasMobile || ctx.hasShared;
|
|
314
|
+
const npm = ctx.jsPm === "npm";
|
|
315
|
+
const pip = ctx.pyPm === "pip";
|
|
316
|
+
const jsInstall = npm ? `run("npm", ["install"])` : `run("pnpm", ["install"])`;
|
|
317
|
+
const jsRun = (pkg, script) => npm ? `run("npm", ["run", "${script}", "-w", "${pkg}"])` : `run("pnpm", ["--filter", "${pkg}", "${script}"])`;
|
|
318
|
+
const pyManage = (...sub) => {
|
|
319
|
+
const args = sub.map((s) => `"${s}"`).join(", ");
|
|
320
|
+
return ctx.pyPm === "uv" ? `run("uv", ["run", "python", "manage.py", ${args}], SERVER)` : `run(venvPy, ["manage.py", ${args}], SERVER)`;
|
|
321
|
+
};
|
|
322
|
+
const serverDev = ctx.pyPm === "uv" ? `["uv", ["run", "python", "manage.py", "runserver", "0.0.0.0:8000"], SERVER]` : `[venvPy, ["manage.py", "runserver", "0.0.0.0:8000"], SERVER]`;
|
|
323
|
+
const jsDevTuple = (pkg, script) => npm ? `["npm", ["run", "${script}", "-w", "${pkg}"]]` : `["pnpm", ["--filter", "${pkg}", "${script}"]]`;
|
|
324
|
+
const devTuples = [];
|
|
325
|
+
if (ctx.hasServer) devTuples.push(serverDev);
|
|
326
|
+
if (ctx.hasWeb) devTuples.push(jsDevTuple("web", "dev"));
|
|
327
|
+
if (ctx.hasMobile) devTuples.push(jsDevTuple("mobile", "start"));
|
|
328
|
+
const needsParallel = devTuples.length > 1;
|
|
329
|
+
const L = [];
|
|
330
|
+
L.push(
|
|
331
|
+
`#!/usr/bin/env node`,
|
|
332
|
+
`// ${ctx.projectName} \u2014 cross-platform task runner (generated by partweave).`,
|
|
333
|
+
`// Run a task on any OS: node scripts/run.mjs <task>`,
|
|
334
|
+
`// Also available as: npm run <task> \xB7 make <task> (on macOS/Linux)`,
|
|
335
|
+
needsParallel ? `import { spawn, spawnSync } from "node:child_process";` : `import { spawnSync } from "node:child_process";`,
|
|
336
|
+
`import { existsSync } from "node:fs";`,
|
|
337
|
+
``,
|
|
338
|
+
`const isWin = process.platform === "win32";`,
|
|
339
|
+
`const SERVER = { cwd: "apps/server" };`
|
|
340
|
+
);
|
|
341
|
+
if (ctx.hasServer && pip) {
|
|
342
|
+
L.push(`const venvPy = isWin ? ".venv\\\\Scripts\\\\python.exe" : ".venv/bin/python";`);
|
|
343
|
+
}
|
|
344
|
+
L.push(
|
|
345
|
+
``,
|
|
346
|
+
`// Windows must spawn Node's .cmd shims through a shell; POSIX doesn't.`,
|
|
347
|
+
`function run(cmd, args = [], opts = {}) {`,
|
|
348
|
+
` const r = spawnSync(cmd, args, { stdio: "inherit", shell: isWin, ...opts });`,
|
|
349
|
+
` if (r.error) { console.error("\\u2716 " + cmd + ": " + r.error.message); process.exit(1); }`,
|
|
350
|
+
` if (r.status) process.exit(r.status);`,
|
|
351
|
+
`}`,
|
|
352
|
+
`function has(cmd) {`,
|
|
353
|
+
` const p = isWin`,
|
|
354
|
+
` ? spawnSync("where", [cmd], { stdio: "ignore" })`,
|
|
355
|
+
` : spawnSync("sh", ["-c", "command -v " + cmd], { stdio: "ignore" });`,
|
|
356
|
+
` return p.status === 0;`,
|
|
357
|
+
`}`,
|
|
358
|
+
``
|
|
359
|
+
);
|
|
360
|
+
if (needsParallel) {
|
|
361
|
+
L.push(
|
|
362
|
+
`// Run several long-lived dev servers at once; Ctrl-C stops them all.`,
|
|
363
|
+
`function parallel(specs) {`,
|
|
364
|
+
` const kids = specs.map(([cmd, args, opts = {}]) =>`,
|
|
365
|
+
` spawn(cmd, args, { stdio: "inherit", shell: isWin, ...opts }));`,
|
|
366
|
+
` const stop = () => kids.forEach((k) => k.kill());`,
|
|
367
|
+
` process.on("SIGINT", stop);`,
|
|
368
|
+
` process.on("SIGTERM", stop);`,
|
|
369
|
+
` kids.forEach((k) => k.on("exit", (code) => { if (code) { stop(); process.exit(code); } }));`,
|
|
370
|
+
`}`,
|
|
371
|
+
``
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
const ensure = [];
|
|
375
|
+
if (anyJs && ctx.jsPm === "pnpm") {
|
|
376
|
+
ensure.push(
|
|
377
|
+
` if (!has("pnpm")) {`,
|
|
378
|
+
` console.log("pnpm not found \\u2014 enabling it via corepack\\u2026");`,
|
|
379
|
+
` spawnSync("corepack", ["enable", "pnpm"], { stdio: "inherit", shell: isWin });`,
|
|
380
|
+
` if (!has("pnpm")) { console.error("Could not enable pnpm. Install it with: npm i -g pnpm"); process.exit(1); }`,
|
|
381
|
+
` }`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
if (ctx.hasServer && ctx.pyPm === "uv") {
|
|
385
|
+
ensure.push(
|
|
386
|
+
` if (!has("uv")) {`,
|
|
387
|
+
` console.error("uv not found. Install it, then re-run bootstrap:\\n " + (isWin`,
|
|
388
|
+
` ? 'powershell -ExecutionPolicy Bypass -c "irm https://astral.sh/uv/install.ps1 | iex"'`,
|
|
389
|
+
` : "curl -LsSf https://astral.sh/uv/install.sh | sh"));`,
|
|
390
|
+
` process.exit(1);`,
|
|
391
|
+
` }`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
if (ctx.hasServer && pip) {
|
|
395
|
+
ensure.push(
|
|
396
|
+
` if (!has("python3") && !has("python")) { console.error("Python 3 not found. Install Python 3.12+ and re-run."); process.exit(1); }`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
L.push(
|
|
400
|
+
`function ensureTools() {`,
|
|
401
|
+
...ensure.length ? ensure : [` // npm and pip ship with Node and Python respectively.`],
|
|
402
|
+
`}`,
|
|
403
|
+
``
|
|
404
|
+
);
|
|
405
|
+
if (ctx.hasServer && pip) {
|
|
406
|
+
L.push(
|
|
407
|
+
`function pipSync() {`,
|
|
408
|
+
` const py = has("python3") ? "python3" : "python";`,
|
|
409
|
+
` run(py, ["-m", "venv", ".venv"], SERVER);`,
|
|
410
|
+
` run(venvPy, ["-m", "pip", "install", "-U", "pip"], SERVER);`,
|
|
411
|
+
` run(venvPy, ["scripts/sync_deps.py"], SERVER);`,
|
|
412
|
+
`}`,
|
|
413
|
+
``
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
L.push(`const tasks = {`);
|
|
417
|
+
const boot = [` ensureTools();`];
|
|
418
|
+
if (anyJs) boot.push(` ${jsInstall};`);
|
|
419
|
+
if (ctx.hasServer) boot.push(pip ? ` pipSync();` : ` run("uv", ["sync"], SERVER);`);
|
|
420
|
+
L.push(` bootstrap() {`, ...boot, ` },`);
|
|
421
|
+
if (devTuples.length === 1) {
|
|
422
|
+
L.push(` dev() { const [c, a, o = {}] = ${devTuples[0]}; run(c, a, o); },`);
|
|
423
|
+
} else if (devTuples.length > 1) {
|
|
424
|
+
L.push(` dev() { parallel([${devTuples.join(", ")}]); },`);
|
|
425
|
+
}
|
|
426
|
+
if (ctx.hasWeb) L.push(` web() { ${jsRun("web", "dev")}; },`);
|
|
427
|
+
if (ctx.hasMobile) L.push(` mobile() { ${jsRun("mobile", "start")}; },`);
|
|
428
|
+
if (ctx.hasServer) {
|
|
429
|
+
L.push(` server() { ${pyManage("runserver", "0.0.0.0:8000")}; },`);
|
|
430
|
+
L.push(` migrate() { ${pyManage("migrate")}; },`);
|
|
431
|
+
L.push(` superuser() { ${pyManage("createsuperuser")}; },`);
|
|
432
|
+
if (hasDocker) {
|
|
433
|
+
L.push(
|
|
434
|
+
` "db:up"() {`,
|
|
435
|
+
` if (spawnSync("docker", ["info"], { stdio: "ignore", shell: isWin }).status) {`,
|
|
436
|
+
` console.error("Docker isn't running \\u2014 start Docker Desktop and retry."); process.exit(1);`,
|
|
437
|
+
` }`,
|
|
438
|
+
` const env = existsSync(".env") ? ["--env-file", ".env"] : [];`,
|
|
439
|
+
` run("docker", ["compose", ...env, "-f", "infra/docker-compose.yml", "up", "-d", "db"]);`,
|
|
440
|
+
` },`,
|
|
441
|
+
` "db:down"() { run("docker", ["compose", "-f", "infra/docker-compose.yml", "down"]); },`
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (ctx.hasApiClient) L.push(` "gen:api"() { ${jsRun("@app/api-client", "generate")}; },`);
|
|
446
|
+
L.push(`};`, ``);
|
|
447
|
+
L.push(
|
|
448
|
+
`const task = process.argv[2];`,
|
|
449
|
+
`if (!task || !Object.hasOwn(tasks, task)) {`,
|
|
450
|
+
` console.log("Tasks: " + Object.keys(tasks).join(", "));`,
|
|
451
|
+
` process.exit(task ? 1 : 0);`,
|
|
452
|
+
`}`,
|
|
453
|
+
`tasks[task]();`,
|
|
454
|
+
``
|
|
455
|
+
);
|
|
456
|
+
return L.join("\n");
|
|
457
|
+
}
|
|
264
458
|
function buildMakefile(ctx, hasDocker) {
|
|
265
|
-
const js = jsPmProfile(ctx.jsPm);
|
|
266
|
-
const py = pyPmProfile(ctx.pyPm);
|
|
267
459
|
const L = [];
|
|
268
460
|
const phony = [];
|
|
269
|
-
const add = (name,
|
|
461
|
+
const add = (name, task, help) => {
|
|
270
462
|
phony.push(name);
|
|
271
|
-
L.push(`${name}: ## ${help}`);
|
|
272
|
-
for (const line of body) L.push(` ${line}`);
|
|
273
|
-
L.push("");
|
|
463
|
+
L.push(`${name}: ## ${help}`, ` node scripts/run.mjs ${task}`, "");
|
|
274
464
|
};
|
|
275
|
-
L.push("# " + ctx.projectName + " \u2014 dev tasks");
|
|
465
|
+
L.push("# " + ctx.projectName + " \u2014 dev tasks (wrappers around scripts/run.mjs)");
|
|
276
466
|
L.push(".DEFAULT_GOAL := help");
|
|
277
467
|
L.push("");
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (ctx.hasServer) bootstrap.push(`cd apps/server && ${py.syncInServer}`);
|
|
281
|
-
if (bootstrap.length) add("bootstrap", bootstrap, "install all dependencies");
|
|
468
|
+
add("bootstrap", "bootstrap", "install all dependencies");
|
|
469
|
+
add("dev", "dev", "run all dev servers");
|
|
282
470
|
if (ctx.hasServer) {
|
|
283
471
|
if (hasDocker) {
|
|
284
|
-
add(
|
|
285
|
-
|
|
286
|
-
[
|
|
287
|
-
`@docker info >/dev/null 2>&1 || { echo "Docker isn't running \u2014 start Docker Desktop and wait for it to be ready, then retry."; exit 1; }`,
|
|
288
|
-
// Load the project's .env (if present) so POSTGRES_* there drive the container.
|
|
289
|
-
"docker compose $$([ -f .env ] && echo --env-file .env) -f infra/docker-compose.yml up -d db"
|
|
290
|
-
],
|
|
291
|
-
"start Postgres (needs Docker running)"
|
|
292
|
-
);
|
|
293
|
-
add("db-down", ["docker compose -f infra/docker-compose.yml down"], "stop Postgres");
|
|
472
|
+
add("db-up", "db:up", "start Postgres (needs Docker running)");
|
|
473
|
+
add("db-down", "db:down", "stop Postgres");
|
|
294
474
|
}
|
|
295
|
-
add("migrate",
|
|
296
|
-
add(
|
|
297
|
-
|
|
298
|
-
[`cd apps/server && ${py.run("python manage.py runserver 0.0.0.0:8000")}`],
|
|
299
|
-
"run the Django dev server"
|
|
300
|
-
);
|
|
301
|
-
add(
|
|
302
|
-
"superuser",
|
|
303
|
-
[`cd apps/server && ${py.run("python manage.py createsuperuser")}`],
|
|
304
|
-
"create an admin user"
|
|
305
|
-
);
|
|
475
|
+
add("migrate", "migrate", "run migrations");
|
|
476
|
+
add("server", "server", "run the Django dev server");
|
|
477
|
+
add("superuser", "superuser", "create an admin user");
|
|
306
478
|
}
|
|
307
|
-
if (ctx.hasWeb) add("web",
|
|
308
|
-
if (ctx.hasMobile) add("mobile",
|
|
309
|
-
if (ctx.hasApiClient)
|
|
310
|
-
add("gen-api", [js.run("@app/api-client", "generate")], "regenerate the typed API client");
|
|
479
|
+
if (ctx.hasWeb) add("web", "web", "run the Next.js dev server");
|
|
480
|
+
if (ctx.hasMobile) add("mobile", "mobile", "run the Expo dev server");
|
|
481
|
+
if (ctx.hasApiClient) add("gen-api", "gen:api", "regenerate the typed API client");
|
|
311
482
|
L.push(
|
|
312
483
|
"help: ## show this help",
|
|
313
484
|
` @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \\033[36m%-12s\\033[0m %s\\n", $$1, $$2}'`,
|
|
@@ -507,10 +678,22 @@ function buildReadme(ctx, modules) {
|
|
|
507
678
|
for (const m of modules) parts.push(`- **${m.manifest.title}** \u2014 ${m.manifest.description ?? ""}`);
|
|
508
679
|
parts.push("");
|
|
509
680
|
}
|
|
510
|
-
parts.push(
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
681
|
+
parts.push(
|
|
682
|
+
"## Getting started",
|
|
683
|
+
"",
|
|
684
|
+
"These `npm run` tasks work the same on macOS, Linux, and Windows (they wrap",
|
|
685
|
+
"`scripts/run.mjs`). On macOS/Linux you can also use `make <task>`.",
|
|
686
|
+
"",
|
|
687
|
+
"```sh",
|
|
688
|
+
"npm run bootstrap"
|
|
689
|
+
);
|
|
690
|
+
const hasDocker = modules.some((m) => m.manifest.id === "docker");
|
|
691
|
+
if (ctx.hasServer) {
|
|
692
|
+
if (hasDocker) parts.push("npm run db:up # start Postgres (needs Docker)");
|
|
693
|
+
parts.push("npm run migrate", "npm run server # http://localhost:8000");
|
|
694
|
+
}
|
|
695
|
+
if (ctx.hasWeb) parts.push("npm run web # http://localhost:3000");
|
|
696
|
+
if (ctx.hasMobile) parts.push("npm run mobile # Expo dev server");
|
|
514
697
|
parts.push("```", "");
|
|
515
698
|
parts.push("Copy `.env.example` to `.env` and fill in values before running.", "");
|
|
516
699
|
return parts.join("\n");
|
|
@@ -729,7 +912,7 @@ function injectIntoFiles(index, anchorId, lines, targetLabel) {
|
|
|
729
912
|
writeFileSync2(file, next);
|
|
730
913
|
}
|
|
731
914
|
}
|
|
732
|
-
function applyWiring(outDir, targets, modules) {
|
|
915
|
+
function applyWiring(outDir, targets, modules, workspaceRange) {
|
|
733
916
|
const indexes = /* @__PURE__ */ new Map();
|
|
734
917
|
for (const t of targets) {
|
|
735
918
|
indexes.set(t, buildAnchorIndex(join2(outDir, TARGET_DEST[t])));
|
|
@@ -753,7 +936,7 @@ function applyWiring(outDir, targets, modules) {
|
|
|
753
936
|
if (existsSync2(pkgPath)) {
|
|
754
937
|
const merged = mergePackageJsonDeps(
|
|
755
938
|
readFileSync2(pkgPath, "utf8"),
|
|
756
|
-
wiring.deps
|
|
939
|
+
normalizeWorkspaceDeps(wiring.deps, workspaceRange)
|
|
757
940
|
);
|
|
758
941
|
writeFileSync2(pkgPath, merged);
|
|
759
942
|
}
|
|
@@ -774,7 +957,8 @@ function applyEnv(outDir, modules) {
|
|
|
774
957
|
function writeStructuralRootFiles(outDir, ctx, modules) {
|
|
775
958
|
const workspace = buildJsWorkspace(ctx);
|
|
776
959
|
if (workspace) writeFileEnsured(join2(outDir, "pnpm-workspace.yaml"), workspace);
|
|
777
|
-
const
|
|
960
|
+
const hasDocker = modules.some((m) => m.manifest.id === "docker");
|
|
961
|
+
const rootPkg = buildRootPackageJson(ctx, hasDocker);
|
|
778
962
|
if (rootPkg) writeFileEnsured(join2(outDir, "package.json"), rootPkg);
|
|
779
963
|
const turbo = buildTurboJson(ctx);
|
|
780
964
|
if (turbo) writeFileEnsured(join2(outDir, "turbo.json"), turbo);
|
|
@@ -784,7 +968,7 @@ function writeStructuralRootFiles(outDir, ctx, modules) {
|
|
|
784
968
|
if (npmrc) writeFileEnsured(join2(outDir, ".npmrc"), npmrc);
|
|
785
969
|
const pipSync = buildPipSyncScript(ctx);
|
|
786
970
|
if (pipSync) writeFileEnsured(join2(outDir, "apps/server/scripts/sync_deps.py"), pipSync);
|
|
787
|
-
|
|
971
|
+
writeFileEnsured(join2(outDir, "scripts/run.mjs"), buildTaskRunner(ctx, hasDocker));
|
|
788
972
|
writeFileEnsured(join2(outDir, "Makefile"), buildMakefile(ctx, hasDocker));
|
|
789
973
|
writeFileEnsured(join2(outDir, ".env.example"), buildBaseEnv(ctx));
|
|
790
974
|
}
|
|
@@ -814,7 +998,7 @@ function compose(opts) {
|
|
|
814
998
|
}
|
|
815
999
|
}
|
|
816
1000
|
}
|
|
817
|
-
applyWiring(outDir, wireTargets, modules);
|
|
1001
|
+
applyWiring(outDir, wireTargets, modules, jsPmProfile(ctx.jsPm).workspaceRange);
|
|
818
1002
|
applyEnv(outDir, modules);
|
|
819
1003
|
if (selection.modules.includes("ci")) {
|
|
820
1004
|
for (const [rel, content] of Object.entries(buildCiWorkflows(ctx))) {
|
|
@@ -831,6 +1015,43 @@ function compose(opts) {
|
|
|
831
1015
|
return { written, notes };
|
|
832
1016
|
}
|
|
833
1017
|
|
|
1018
|
+
// src/preflight.ts
|
|
1019
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1020
|
+
import { confirm, isCancel, log, spinner } from "@clack/prompts";
|
|
1021
|
+
function runInstall(plan, probe) {
|
|
1022
|
+
const s = spinner();
|
|
1023
|
+
s.start(plan.label);
|
|
1024
|
+
const r = spawnSync2(plan.cmd, plan.args, {
|
|
1025
|
+
stdio: ["ignore", "ignore", "inherit"],
|
|
1026
|
+
shell: process.platform === "win32"
|
|
1027
|
+
});
|
|
1028
|
+
const ok = r.status === 0 && hasCommand(probe);
|
|
1029
|
+
s.stop(ok ? `${probe} is ready` : `Could not install ${probe}`);
|
|
1030
|
+
return ok;
|
|
1031
|
+
}
|
|
1032
|
+
async function ensurePm(chosen, fallback, plan, opts) {
|
|
1033
|
+
if (hasCommand(chosen)) return chosen;
|
|
1034
|
+
if (!plan) return chosen;
|
|
1035
|
+
let doInstall = opts.install === true;
|
|
1036
|
+
if (opts.interactive) {
|
|
1037
|
+
const answer = await confirm({
|
|
1038
|
+
message: `${chosen} isn't installed. Install it now? (${plan.hint})`
|
|
1039
|
+
});
|
|
1040
|
+
doInstall = !isCancel(answer) && answer === true;
|
|
1041
|
+
}
|
|
1042
|
+
if (doInstall && runInstall(plan, chosen)) return chosen;
|
|
1043
|
+
log.warn(
|
|
1044
|
+
`Using ${fallback} instead of ${chosen}. To use ${chosen} later, run: ${plan.hint}`
|
|
1045
|
+
);
|
|
1046
|
+
return fallback;
|
|
1047
|
+
}
|
|
1048
|
+
function ensureJsPm(pm, opts) {
|
|
1049
|
+
return ensurePm(pm, "npm", jsPmInstallPlan(pm), opts);
|
|
1050
|
+
}
|
|
1051
|
+
function ensurePyPm(pm, opts) {
|
|
1052
|
+
return ensurePm(pm, "pip", pyPmInstallPlan(pm), opts);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
834
1055
|
// src/registry.ts
|
|
835
1056
|
import { readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
836
1057
|
import { join as join4 } from "path";
|
|
@@ -1004,8 +1225,8 @@ Enable the required app(s) or drop the component(s).`
|
|
|
1004
1225
|
// src/prompts.ts
|
|
1005
1226
|
import {
|
|
1006
1227
|
cancel,
|
|
1007
|
-
confirm,
|
|
1008
|
-
isCancel,
|
|
1228
|
+
confirm as confirm2,
|
|
1229
|
+
isCancel as isCancel2,
|
|
1009
1230
|
multiselect,
|
|
1010
1231
|
select,
|
|
1011
1232
|
text
|
|
@@ -1014,7 +1235,7 @@ import pc from "picocolors";
|
|
|
1014
1235
|
import { resolve as resolve2 } from "path";
|
|
1015
1236
|
var MULTI_HINT = pc.dim("\u2191/\u2193 move \xB7 space toggle \xB7 a all \xB7 enter confirm");
|
|
1016
1237
|
function bail(v) {
|
|
1017
|
-
if (
|
|
1238
|
+
if (isCancel2(v)) {
|
|
1018
1239
|
cancel("Cancelled.");
|
|
1019
1240
|
process.exit(0);
|
|
1020
1241
|
}
|
|
@@ -1092,7 +1313,7 @@ async function promptCreate(registry, defaults) {
|
|
|
1092
1313
|
bail(pyRaw);
|
|
1093
1314
|
pyPm = pyRaw;
|
|
1094
1315
|
}
|
|
1095
|
-
const ok = await
|
|
1316
|
+
const ok = await confirm2({
|
|
1096
1317
|
message: `Scaffold ${apps.join(" + ")}${modules.length ? " with " + modules.join(", ") : ""} into ${outDir}?`
|
|
1097
1318
|
});
|
|
1098
1319
|
bail(ok);
|
|
@@ -1107,7 +1328,7 @@ async function promptCreate(registry, defaults) {
|
|
|
1107
1328
|
function resolvePm(value, allowed, detect, flag) {
|
|
1108
1329
|
if (value === void 0) return detect();
|
|
1109
1330
|
if (allowed.includes(value)) return value;
|
|
1110
|
-
|
|
1331
|
+
log2.error(`Invalid ${flag} "${value}". Choose one of: ${allowed.join(", ")}.`);
|
|
1111
1332
|
process.exit(1);
|
|
1112
1333
|
}
|
|
1113
1334
|
function appsFromFlags(flags) {
|
|
@@ -1149,26 +1370,33 @@ async function runCreate(flags) {
|
|
|
1149
1370
|
});
|
|
1150
1371
|
}
|
|
1151
1372
|
if (existsSync5(choices.outDir) && readdirSync3(choices.outDir).length > 0 && !flags.force) {
|
|
1152
|
-
|
|
1373
|
+
log2.error(`${choices.outDir} exists and is not empty. Use --force to override.`);
|
|
1153
1374
|
process.exit(1);
|
|
1154
1375
|
}
|
|
1376
|
+
const ensureOpts = { interactive: !nonInteractive, install: flags.install };
|
|
1377
|
+
if (choices.apps.includes("web") || choices.apps.includes("mobile")) {
|
|
1378
|
+
choices.jsPm = await ensureJsPm(choices.jsPm, ensureOpts);
|
|
1379
|
+
}
|
|
1380
|
+
if (choices.apps.includes("server")) {
|
|
1381
|
+
choices.pyPm = await ensurePyPm(choices.pyPm, ensureOpts);
|
|
1382
|
+
}
|
|
1155
1383
|
let resolved;
|
|
1156
1384
|
try {
|
|
1157
1385
|
resolved = resolveModules(registry, choices.modules);
|
|
1158
1386
|
} catch (err) {
|
|
1159
|
-
|
|
1387
|
+
log2.error(err.message);
|
|
1160
1388
|
process.exit(1);
|
|
1161
1389
|
}
|
|
1162
1390
|
if (resolved.autoAdded.length) {
|
|
1163
|
-
|
|
1391
|
+
log2.info(`Added required components: ${resolved.autoAdded.join(", ")}`);
|
|
1164
1392
|
}
|
|
1165
1393
|
try {
|
|
1166
1394
|
validateApps(registry, resolved.modules, choices.apps);
|
|
1167
1395
|
} catch (err) {
|
|
1168
|
-
|
|
1396
|
+
log2.error(err.message);
|
|
1169
1397
|
process.exit(1);
|
|
1170
1398
|
}
|
|
1171
|
-
const s =
|
|
1399
|
+
const s = spinner2();
|
|
1172
1400
|
s.start("Scaffolding");
|
|
1173
1401
|
let result;
|
|
1174
1402
|
try {
|
|
@@ -1190,7 +1418,7 @@ async function runCreate(flags) {
|
|
|
1190
1418
|
});
|
|
1191
1419
|
} catch (err) {
|
|
1192
1420
|
s.stop("Failed");
|
|
1193
|
-
|
|
1421
|
+
log2.error(err.message);
|
|
1194
1422
|
process.exit(1);
|
|
1195
1423
|
}
|
|
1196
1424
|
s.stop(`Created ${result.written.length} files`);
|
|
@@ -1201,25 +1429,52 @@ async function runCreate(flags) {
|
|
|
1201
1429
|
jsPm: choices.jsPm,
|
|
1202
1430
|
pyPm: choices.pyPm
|
|
1203
1431
|
});
|
|
1432
|
+
let installed = false;
|
|
1433
|
+
if (nonInteractive) {
|
|
1434
|
+
installed = flags.install === true;
|
|
1435
|
+
} else {
|
|
1436
|
+
const ans = await confirm3({ message: "Install dependencies now?" });
|
|
1437
|
+
installed = !isCancel3(ans) && ans === true;
|
|
1438
|
+
}
|
|
1439
|
+
if (installed) {
|
|
1440
|
+
log2.step("Installing dependencies (npm run bootstrap)\u2026");
|
|
1441
|
+
const r = spawnSync3("node", ["scripts/run.mjs", "bootstrap"], {
|
|
1442
|
+
cwd: choices.outDir,
|
|
1443
|
+
stdio: "inherit"
|
|
1444
|
+
});
|
|
1445
|
+
if (r.status) {
|
|
1446
|
+
log2.warn("bootstrap didn't finish cleanly \u2014 run `npm run bootstrap` in the project to retry.");
|
|
1447
|
+
installed = false;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1204
1450
|
const rel = basename(choices.outDir);
|
|
1205
|
-
const
|
|
1206
|
-
|
|
1207
|
-
if (
|
|
1208
|
-
if (choices.apps.includes("
|
|
1209
|
-
|
|
1451
|
+
const hasDocker = resolved.modules.includes("docker");
|
|
1452
|
+
const steps = [`cd ${rel}`];
|
|
1453
|
+
if (!installed) steps.push("npm run bootstrap");
|
|
1454
|
+
if (choices.apps.includes("server")) {
|
|
1455
|
+
steps.push(hasDocker ? "npm run db:up && npm run migrate" : "npm run migrate");
|
|
1456
|
+
steps.push("npm run server");
|
|
1457
|
+
}
|
|
1458
|
+
if (choices.apps.includes("web")) steps.push("npm run web");
|
|
1459
|
+
if (choices.apps.includes("mobile")) steps.push("npm run mobile");
|
|
1460
|
+
note(
|
|
1461
|
+
steps.join("\n") + "\n\n" + pc2.dim("These work on macOS, Linux & Windows. On macOS/Linux, `make <task>` works too."),
|
|
1462
|
+
"Next steps"
|
|
1463
|
+
);
|
|
1210
1464
|
if (result.notes.length) note(result.notes.join("\n"), "Notes");
|
|
1211
1465
|
outro(pc2.green("Done."));
|
|
1212
1466
|
}
|
|
1213
1467
|
|
|
1214
1468
|
export {
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
pyPmProfile,
|
|
1469
|
+
jsPmInstallPlan,
|
|
1470
|
+
pyPmInstallPlan,
|
|
1471
|
+
hasCommand,
|
|
1219
1472
|
APPS,
|
|
1220
1473
|
buildContext,
|
|
1221
1474
|
selectedTargets,
|
|
1222
1475
|
compose,
|
|
1476
|
+
ensureJsPm,
|
|
1477
|
+
ensurePyPm,
|
|
1223
1478
|
Registry,
|
|
1224
1479
|
readProjectManifest,
|
|
1225
1480
|
writeProjectManifest,
|
|
@@ -1227,4 +1482,4 @@ export {
|
|
|
1227
1482
|
validateApps,
|
|
1228
1483
|
runCreate
|
|
1229
1484
|
};
|
|
1230
|
-
//# sourceMappingURL=chunk-
|
|
1485
|
+
//# sourceMappingURL=chunk-XB7H4V4J.js.map
|
package/dist/create-bin.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
APPS,
|
|
4
|
-
DEFAULT_JS_PM,
|
|
5
|
-
DEFAULT_PY_PM,
|
|
6
4
|
Registry,
|
|
7
5
|
buildContext,
|
|
8
6
|
compose,
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
ensureJsPm,
|
|
8
|
+
ensurePyPm,
|
|
9
|
+
hasCommand,
|
|
10
|
+
jsPmInstallPlan,
|
|
11
|
+
pyPmInstallPlan,
|
|
11
12
|
readProjectManifest,
|
|
12
13
|
resolveModules,
|
|
13
14
|
runCreate,
|
|
14
15
|
selectedTargets,
|
|
15
16
|
validateApps,
|
|
16
17
|
writeProjectManifest
|
|
17
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-XB7H4V4J.js";
|
|
18
19
|
|
|
19
20
|
// src/index.ts
|
|
20
21
|
import { Command } from "commander";
|
|
@@ -54,13 +55,9 @@ async function runAdd(ids, flags) {
|
|
|
54
55
|
let manifest = pm;
|
|
55
56
|
if (appIds.length) manifest = addApps(registry, dir, manifest, appIds);
|
|
56
57
|
if (moduleIds.length) manifest = addModules(registry, dir, manifest, moduleIds);
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (manifest.apps.some((a) => a === "web" || a === "mobile")) reinstall.push(js.install);
|
|
61
|
-
if (manifest.apps.includes("server"))
|
|
62
|
-
reinstall.push(`cd apps/server && ${py.syncInServer} && ${py.run("python manage.py migrate")}`);
|
|
63
|
-
if (reinstall.length) note(reinstall.join("\n"), "Sync deps");
|
|
58
|
+
const reinstall = ["npm run bootstrap"];
|
|
59
|
+
if (manifest.apps.includes("server")) reinstall.push("npm run migrate");
|
|
60
|
+
note(reinstall.join("\n"), "Sync deps");
|
|
64
61
|
outro(pc.green("Done."));
|
|
65
62
|
}
|
|
66
63
|
function addApps(registry, dir, pm, appIds) {
|
|
@@ -135,17 +132,62 @@ function addModules(registry, dir, pm, moduleIds) {
|
|
|
135
132
|
return updated;
|
|
136
133
|
}
|
|
137
134
|
|
|
135
|
+
// src/commands/doctor.ts
|
|
136
|
+
import { spawnSync } from "child_process";
|
|
137
|
+
import { resolve as resolve2 } from "path";
|
|
138
|
+
import { intro as intro2, note as note2, outro as outro2 } from "@clack/prompts";
|
|
139
|
+
import pc2 from "picocolors";
|
|
140
|
+
function version(cmd) {
|
|
141
|
+
const r = spawnSync(cmd, ["--version"], { encoding: "utf8" });
|
|
142
|
+
if (r.error || r.status !== 0) return null;
|
|
143
|
+
return (r.stdout || r.stderr || "").trim().split("\n")[0] ?? "";
|
|
144
|
+
}
|
|
145
|
+
async function runDoctor(flags) {
|
|
146
|
+
intro2(pc2.bgCyan(pc2.black(" partweave ")) + pc2.dim(" doctor"));
|
|
147
|
+
const dir = resolve2(flags.dir ?? process.cwd());
|
|
148
|
+
const project = readProjectManifest(dir);
|
|
149
|
+
const rows = [];
|
|
150
|
+
const check = (label, cmd, optional = false) => {
|
|
151
|
+
const v = version(cmd);
|
|
152
|
+
const ok = v !== null;
|
|
153
|
+
const mark = ok ? pc2.green("\u2713") : optional ? pc2.yellow("\u25CB") : pc2.red("\u2717");
|
|
154
|
+
const detail = ok ? pc2.dim(v) : pc2.dim(optional ? "not found (optional)" : "not found");
|
|
155
|
+
rows.push(`${mark} ${label.padEnd(9)} ${detail}`);
|
|
156
|
+
return ok;
|
|
157
|
+
};
|
|
158
|
+
check("node", "node");
|
|
159
|
+
check("npm", "npm");
|
|
160
|
+
check("pnpm", "pnpm", true);
|
|
161
|
+
check("uv", "uv", true);
|
|
162
|
+
check("python", process.platform === "win32" ? "python" : "python3", true);
|
|
163
|
+
check("docker", "docker", true);
|
|
164
|
+
check("make", "make", true);
|
|
165
|
+
note2(rows.join("\n"), project ? `Environment for ${project.name}` : "Environment");
|
|
166
|
+
const jsPm = project?.jsPm ?? "pnpm";
|
|
167
|
+
const pyPm = project?.pyPm ?? "uv";
|
|
168
|
+
if (jsPmInstallPlan(jsPm) && !hasCommand(jsPm)) {
|
|
169
|
+
await ensureJsPm(jsPm, { interactive: true });
|
|
170
|
+
}
|
|
171
|
+
if (pyPmInstallPlan(pyPm) && !hasCommand(pyPm)) {
|
|
172
|
+
await ensurePyPm(pyPm, { interactive: true });
|
|
173
|
+
}
|
|
174
|
+
outro2(pc2.green("Done."));
|
|
175
|
+
}
|
|
176
|
+
|
|
138
177
|
// src/index.ts
|
|
139
178
|
function buildProgram() {
|
|
140
179
|
const program = new Command();
|
|
141
180
|
program.name("partweave").description("A modular full-stack scaffolder \u2014 pick parts, generate only that code.").version("0.1.0");
|
|
142
|
-
program.command("create", { isDefault: true }).argument("[name]", "project name").description("Scaffold a new project").option("-d, --dir <path>", "target directory").option("--server", "include the Django server").option("--no-server", "exclude the server").option("--web", "include the Next.js web app").option("--no-web", "exclude the web app").option("--mobile", "include the Expo mobile app").option("--no-mobile", "exclude the mobile app").option("--with <ids>", "comma-separated component ids (e.g. auth,docker,ci)").option("--js-pm <pm>", "JS package manager: pnpm | npm (default: auto-detect)").option("--py-pm <pm>", "Python package manager: uv | pip (default: auto-detect)").option("-y, --yes", "skip prompts; use flags/defaults").option("-f, --force", "write into a non-empty directory").action(async (name, opts) => {
|
|
181
|
+
program.command("create", { isDefault: true }).argument("[name]", "project name").description("Scaffold a new project").option("-d, --dir <path>", "target directory").option("--server", "include the Django server").option("--no-server", "exclude the server").option("--web", "include the Next.js web app").option("--no-web", "exclude the web app").option("--mobile", "include the Expo mobile app").option("--no-mobile", "exclude the mobile app").option("--with <ids>", "comma-separated component ids (e.g. auth,docker,ci)").option("--js-pm <pm>", "JS package manager: pnpm | npm (default: auto-detect)").option("--py-pm <pm>", "Python package manager: uv | pip (default: auto-detect)").option("-y, --yes", "skip prompts; use flags/defaults").option("-f, --force", "write into a non-empty directory").option("--install", "install dependencies after scaffolding (default: ask, off with --yes)").action(async (name, opts) => {
|
|
143
182
|
const flags = { ...opts, name: name ?? opts.name };
|
|
144
183
|
await runCreate(flags);
|
|
145
184
|
});
|
|
146
185
|
program.command("add").argument("<items...>", "apps (server/web/mobile) or component ids to add").description("Add an app or component to an existing generated project").option("-d, --dir <path>", "project directory (default: cwd)").action(async (items, opts) => {
|
|
147
186
|
await runAdd(items, opts);
|
|
148
187
|
});
|
|
188
|
+
program.command("doctor").description("Check your environment and install any missing package managers").option("-d, --dir <path>", "project directory (default: cwd)").action(async (opts) => {
|
|
189
|
+
await runDoctor(opts);
|
|
190
|
+
});
|
|
149
191
|
return program;
|
|
150
192
|
}
|
|
151
193
|
async function main(argv = process.argv) {
|
|
@@ -159,6 +201,7 @@ export {
|
|
|
159
201
|
buildProgram,
|
|
160
202
|
main,
|
|
161
203
|
runAdd,
|
|
162
|
-
runCreate
|
|
204
|
+
runCreate,
|
|
205
|
+
runDoctor
|
|
163
206
|
};
|
|
164
207
|
//# sourceMappingURL=index.js.map
|
|
@@ -13,6 +13,8 @@ config.resolver.nodeModulesPaths = [
|
|
|
13
13
|
path.resolve(projectRoot, "node_modules"),
|
|
14
14
|
path.resolve(workspaceRoot, "node_modules"),
|
|
15
15
|
];
|
|
16
|
-
|
|
16
|
+
// Keep Metro's default hierarchical node_modules lookup ON: under npm (and any
|
|
17
|
+
// non-hoisted install) some Expo sub-deps (e.g. expo-asset) resolve from
|
|
18
|
+
// expo/node_modules, and disabling the walk-up breaks bundling them.
|
|
17
19
|
|
|
18
20
|
module.exports = config;
|
|
@@ -29,8 +29,10 @@
|
|
|
29
29
|
"@testing-library/react-native": "^13.2.0",
|
|
30
30
|
"@types/jest": "^29.5.14",
|
|
31
31
|
"@types/react": "~19.1.10",
|
|
32
|
+
"babel-preset-expo": "~54.0.7",
|
|
32
33
|
"jest": "^29.7.0",
|
|
33
34
|
"jest-expo": "~54.0.0",
|
|
35
|
+
"react-test-renderer": "19.1.0",
|
|
34
36
|
"typescript": "~5.9.2"
|
|
35
37
|
}
|
|
36
38
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "partweave",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A modular full-stack scaffolder — pick the parts you need (Django server, Next.js web, Expo mobile) and generate only that code, wired together.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|