@jskit-ai/create-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 +2 -0
- package/bin/jskit-create-app.js +1 -1
- package/package.json +2 -2
- package/src/{index.js → shared/index.js} +156 -7
- package/templates/base-shell/Procfile +1 -0
- package/templates/base-shell/README.md +7 -1
- package/templates/base-shell/package.json +4 -0
- package/templates/base-shell/tests/server/minimalShell.contract.test.js +5 -12
- package/templates/base-shell/framework/app.manifest.mjs +0 -8
package/README.md
CHANGED
|
@@ -17,6 +17,8 @@ jskit-create-app --interactive
|
|
|
17
17
|
- `--template <name>` template name under `templates/` (default `base-shell`)
|
|
18
18
|
- `--title <text>` override the generated app title placeholder
|
|
19
19
|
- `--target <path>` output directory (default `./<app-name>`)
|
|
20
|
+
- `--initial-bundles <preset>` optional framework preset: `none`, `db`, or `db-auth`
|
|
21
|
+
- `--db-provider <provider>` provider for `db` presets: `mysql` or `postgres`
|
|
20
22
|
- `--force` allow writes into non-empty target directories
|
|
21
23
|
- `--dry-run` preview writes only
|
|
22
24
|
- `--interactive` prompt for app values
|
package/bin/jskit-create-app.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/create-app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Scaffold minimal JSKIT app shells.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"jskit-create-app": "bin/jskit-create-app.js"
|
|
17
17
|
},
|
|
18
18
|
"exports": {
|
|
19
|
-
".": "./src/index.js"
|
|
19
|
+
".": "./src/shared/index.js"
|
|
20
20
|
},
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": "20.x"
|
|
@@ -5,7 +5,12 @@ import { createInterface } from "node:readline/promises";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
7
|
const DEFAULT_TEMPLATE = "base-shell";
|
|
8
|
-
const
|
|
8
|
+
const DEFAULT_INITIAL_BUNDLES = "none";
|
|
9
|
+
const DEFAULT_DB_PROVIDER = "mysql";
|
|
10
|
+
const INITIAL_BUNDLE_PRESETS = new Set(["none", "db", "db-auth"]);
|
|
11
|
+
const DB_PROVIDERS = new Set(["mysql", "postgres"]);
|
|
12
|
+
const ALLOWED_EXISTING_TARGET_ENTRIES = new Set([".git"]);
|
|
13
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
9
14
|
const TEMPLATES_ROOT = path.join(PACKAGE_ROOT, "templates");
|
|
10
15
|
|
|
11
16
|
function createCliError(message, { showUsage = false, exitCode = 1 } = {}) {
|
|
@@ -36,6 +41,53 @@ function toAppTitle(appName) {
|
|
|
36
41
|
return words.length > 0 ? words.join(" ") : "App";
|
|
37
42
|
}
|
|
38
43
|
|
|
44
|
+
function normalizeInitialBundlesPreset(value, { showUsage = true } = {}) {
|
|
45
|
+
const normalized = String(value || DEFAULT_INITIAL_BUNDLES).trim().toLowerCase();
|
|
46
|
+
if (INITIAL_BUNDLE_PRESETS.has(normalized)) {
|
|
47
|
+
return normalized;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
throw createCliError(
|
|
51
|
+
`Invalid --initial-bundles value "${value}". Expected one of: none, db, db-auth.`,
|
|
52
|
+
{ showUsage }
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeDbProvider(value, { showUsage = true } = {}) {
|
|
57
|
+
const normalized = String(value || DEFAULT_DB_PROVIDER).trim().toLowerCase();
|
|
58
|
+
if (DB_PROVIDERS.has(normalized)) {
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
throw createCliError(
|
|
63
|
+
`Invalid --db-provider value "${value}". Expected one of: mysql, postgres.`,
|
|
64
|
+
{ showUsage }
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildInitialBundleCommands(initialBundles, dbProvider) {
|
|
69
|
+
const normalizedPreset = normalizeInitialBundlesPreset(initialBundles, { showUsage: false });
|
|
70
|
+
const normalizedProvider = normalizeDbProvider(dbProvider, { showUsage: false });
|
|
71
|
+
|
|
72
|
+
const commands = [];
|
|
73
|
+
if (normalizedPreset === "db" || normalizedPreset === "db-auth") {
|
|
74
|
+
commands.push(`npx jskit add db --provider ${normalizedProvider} --no-install`);
|
|
75
|
+
}
|
|
76
|
+
if (normalizedPreset === "db-auth") {
|
|
77
|
+
commands.push("npx jskit add auth-base --no-install");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return commands;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildProgressiveBundleCommands(dbProvider) {
|
|
84
|
+
const normalizedProvider = normalizeDbProvider(dbProvider, { showUsage: false });
|
|
85
|
+
return [
|
|
86
|
+
`npx jskit add db --provider ${normalizedProvider} --no-install`,
|
|
87
|
+
"npx jskit add auth-base --no-install"
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
|
|
39
91
|
function validateAppName(appName, { showUsage = true } = {}) {
|
|
40
92
|
if (!appName || typeof appName !== "string") {
|
|
41
93
|
throw createCliError("Missing app name.", { showUsage });
|
|
@@ -69,6 +121,8 @@ function parseCliArgs(argv) {
|
|
|
69
121
|
appTitle: null,
|
|
70
122
|
template: DEFAULT_TEMPLATE,
|
|
71
123
|
target: null,
|
|
124
|
+
initialBundles: DEFAULT_INITIAL_BUNDLES,
|
|
125
|
+
dbProvider: DEFAULT_DB_PROVIDER,
|
|
72
126
|
force: false,
|
|
73
127
|
dryRun: false,
|
|
74
128
|
help: false,
|
|
@@ -136,6 +190,30 @@ function parseCliArgs(argv) {
|
|
|
136
190
|
continue;
|
|
137
191
|
}
|
|
138
192
|
|
|
193
|
+
if (arg === "--initial-bundles") {
|
|
194
|
+
const { value, nextIndex } = parseOptionWithValue(args, index, "--initial-bundles");
|
|
195
|
+
options.initialBundles = value;
|
|
196
|
+
index = nextIndex;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (arg.startsWith("--initial-bundles=")) {
|
|
201
|
+
options.initialBundles = arg.slice("--initial-bundles=".length);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (arg === "--db-provider") {
|
|
206
|
+
const { value, nextIndex } = parseOptionWithValue(args, index, "--db-provider");
|
|
207
|
+
options.dbProvider = value;
|
|
208
|
+
index = nextIndex;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (arg.startsWith("--db-provider=")) {
|
|
213
|
+
options.dbProvider = arg.slice("--db-provider=".length);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
139
217
|
if (arg.startsWith("-")) {
|
|
140
218
|
throw createCliError(`Unknown option: ${arg}`, {
|
|
141
219
|
showUsage: true
|
|
@@ -171,6 +249,8 @@ function printUsage(stream = process.stderr) {
|
|
|
171
249
|
stream.write(` --template <name> Template folder under templates/ (default: ${DEFAULT_TEMPLATE})\n`);
|
|
172
250
|
stream.write(" --title <text> App title used for template replacements\n");
|
|
173
251
|
stream.write(" --target <path> Target directory (default: ./<app-name>)\n");
|
|
252
|
+
stream.write(" --initial-bundles <preset> Optional bundle preset: none | db | db-auth (default: none)\n");
|
|
253
|
+
stream.write(" --db-provider <provider> Database provider for db presets: mysql | postgres (default: mysql)\n");
|
|
174
254
|
stream.write(" --force Allow writing into a non-empty target directory\n");
|
|
175
255
|
stream.write(" --dry-run Print planned writes without changing the filesystem\n");
|
|
176
256
|
stream.write(" --interactive Prompt for app values instead of passing all flags\n");
|
|
@@ -233,7 +313,8 @@ async function ensureTargetDirectoryState(targetDirectory, { force = false, dryR
|
|
|
233
313
|
}
|
|
234
314
|
|
|
235
315
|
const entries = await readdir(targetDirectory);
|
|
236
|
-
|
|
316
|
+
const blockingEntries = entries.filter((entry) => !ALLOWED_EXISTING_TARGET_ENTRIES.has(entry));
|
|
317
|
+
if (blockingEntries.length > 0 && !force) {
|
|
237
318
|
throw createCliError(
|
|
238
319
|
`Target directory is not empty: ${targetDirectory}. Use --force to allow writing into it.`
|
|
239
320
|
);
|
|
@@ -335,11 +416,24 @@ async function askYesNoQuestion(readline, label, defaultValue) {
|
|
|
335
416
|
}
|
|
336
417
|
}
|
|
337
418
|
|
|
338
|
-
|
|
339
|
-
|
|
419
|
+
function createReadlineInterface({ stdin = process.stdin, stdout = process.stdout } = {}) {
|
|
420
|
+
return createInterface({
|
|
340
421
|
input: stdin,
|
|
341
422
|
output: stdout
|
|
342
423
|
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function collectInteractiveOptions({
|
|
427
|
+
parsed,
|
|
428
|
+
stdout = process.stdout,
|
|
429
|
+
stderr = process.stderr,
|
|
430
|
+
stdin = process.stdin,
|
|
431
|
+
readlineFactory = createReadlineInterface
|
|
432
|
+
}) {
|
|
433
|
+
const readline = readlineFactory({
|
|
434
|
+
stdin,
|
|
435
|
+
stdout
|
|
436
|
+
});
|
|
343
437
|
|
|
344
438
|
try {
|
|
345
439
|
let appName = String(parsed.appName || "").trim();
|
|
@@ -368,12 +462,42 @@ async function collectInteractiveOptions({ parsed, stdout = process.stdout, stde
|
|
|
368
462
|
Boolean(parsed.force)
|
|
369
463
|
);
|
|
370
464
|
|
|
465
|
+
let initialBundles = normalizeInitialBundlesPreset(parsed.initialBundles, { showUsage: false });
|
|
466
|
+
while (true) {
|
|
467
|
+
const candidate = await askQuestion(
|
|
468
|
+
readline,
|
|
469
|
+
"Initial bundle preset (none|db|db-auth)",
|
|
470
|
+
initialBundles
|
|
471
|
+
);
|
|
472
|
+
try {
|
|
473
|
+
initialBundles = normalizeInitialBundlesPreset(candidate, { showUsage: false });
|
|
474
|
+
break;
|
|
475
|
+
} catch (error) {
|
|
476
|
+
stderr.write(`Error: ${error?.message || String(error)}\n`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
let dbProvider = normalizeDbProvider(parsed.dbProvider, { showUsage: false });
|
|
481
|
+
if (initialBundles === "db" || initialBundles === "db-auth") {
|
|
482
|
+
while (true) {
|
|
483
|
+
const candidate = await askQuestion(readline, "DB provider (mysql|postgres)", dbProvider);
|
|
484
|
+
try {
|
|
485
|
+
dbProvider = normalizeDbProvider(candidate, { showUsage: false });
|
|
486
|
+
break;
|
|
487
|
+
} catch (error) {
|
|
488
|
+
stderr.write(`Error: ${error?.message || String(error)}\n`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
371
493
|
return {
|
|
372
494
|
appName,
|
|
373
495
|
appTitle,
|
|
374
496
|
target,
|
|
375
497
|
template,
|
|
376
|
-
force
|
|
498
|
+
force,
|
|
499
|
+
initialBundles,
|
|
500
|
+
dbProvider
|
|
377
501
|
};
|
|
378
502
|
} finally {
|
|
379
503
|
readline.close();
|
|
@@ -385,6 +509,8 @@ export async function createApp({
|
|
|
385
509
|
appTitle = null,
|
|
386
510
|
template = DEFAULT_TEMPLATE,
|
|
387
511
|
target = null,
|
|
512
|
+
initialBundles = DEFAULT_INITIAL_BUNDLES,
|
|
513
|
+
dbProvider = DEFAULT_DB_PROVIDER,
|
|
388
514
|
force = false,
|
|
389
515
|
dryRun = false,
|
|
390
516
|
cwd = process.cwd()
|
|
@@ -393,6 +519,8 @@ export async function createApp({
|
|
|
393
519
|
validateAppName(resolvedAppName);
|
|
394
520
|
|
|
395
521
|
const resolvedAppTitle = String(appTitle || "").trim() || toAppTitle(resolvedAppName);
|
|
522
|
+
const resolvedInitialBundles = normalizeInitialBundlesPreset(initialBundles);
|
|
523
|
+
const resolvedDbProvider = normalizeDbProvider(dbProvider);
|
|
396
524
|
|
|
397
525
|
const resolvedCwd = path.resolve(cwd);
|
|
398
526
|
const targetDirectory = path.resolve(resolvedCwd, target ? String(target) : resolvedAppName);
|
|
@@ -419,6 +547,10 @@ export async function createApp({
|
|
|
419
547
|
appName: resolvedAppName,
|
|
420
548
|
appTitle: resolvedAppTitle,
|
|
421
549
|
template: String(template),
|
|
550
|
+
initialBundles: resolvedInitialBundles,
|
|
551
|
+
dbProvider: resolvedDbProvider,
|
|
552
|
+
selectedBundleCommands: buildInitialBundleCommands(resolvedInitialBundles, resolvedDbProvider),
|
|
553
|
+
progressiveBundleCommands: buildProgressiveBundleCommands(resolvedDbProvider),
|
|
422
554
|
targetDirectory,
|
|
423
555
|
dryRun,
|
|
424
556
|
touchedFiles
|
|
@@ -431,7 +563,8 @@ export async function runCli(
|
|
|
431
563
|
stdout = process.stdout,
|
|
432
564
|
stderr = process.stderr,
|
|
433
565
|
stdin = process.stdin,
|
|
434
|
-
cwd = process.cwd()
|
|
566
|
+
cwd = process.cwd(),
|
|
567
|
+
readlineFactory = createReadlineInterface
|
|
435
568
|
} = {}
|
|
436
569
|
) {
|
|
437
570
|
try {
|
|
@@ -449,7 +582,8 @@ export async function runCli(
|
|
|
449
582
|
parsed,
|
|
450
583
|
stdout,
|
|
451
584
|
stderr,
|
|
452
|
-
stdin
|
|
585
|
+
stdin,
|
|
586
|
+
readlineFactory
|
|
453
587
|
}))
|
|
454
588
|
}
|
|
455
589
|
: parsed;
|
|
@@ -459,6 +593,8 @@ export async function runCli(
|
|
|
459
593
|
appTitle: resolvedOptions.appTitle,
|
|
460
594
|
template: resolvedOptions.template,
|
|
461
595
|
target: resolvedOptions.target,
|
|
596
|
+
initialBundles: resolvedOptions.initialBundles,
|
|
597
|
+
dbProvider: resolvedOptions.dbProvider,
|
|
462
598
|
force: resolvedOptions.force,
|
|
463
599
|
dryRun: resolvedOptions.dryRun,
|
|
464
600
|
cwd
|
|
@@ -484,6 +620,19 @@ export async function runCli(
|
|
|
484
620
|
stdout.write(`- cd ${shellQuote(targetLabel)}\n`);
|
|
485
621
|
stdout.write("- npm install\n");
|
|
486
622
|
stdout.write("- npm run dev\n");
|
|
623
|
+
|
|
624
|
+
stdout.write("\n");
|
|
625
|
+
if (result.selectedBundleCommands.length > 0) {
|
|
626
|
+
stdout.write(`Initial framework bundle commands (${result.initialBundles}):\n`);
|
|
627
|
+
for (const command of result.selectedBundleCommands) {
|
|
628
|
+
stdout.write(`- ${command}\n`);
|
|
629
|
+
}
|
|
630
|
+
} else {
|
|
631
|
+
stdout.write("Add framework capabilities when ready:\n");
|
|
632
|
+
for (const command of result.progressiveBundleCommands) {
|
|
633
|
+
stdout.write(`- ${command}\n`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
487
636
|
}
|
|
488
637
|
|
|
489
638
|
return 0;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
web: npm run start
|
|
@@ -9,7 +9,6 @@ This is the smallest practical JSKIT app host:
|
|
|
9
9
|
- tiny Fastify server (`/api/v1/health`)
|
|
10
10
|
- tiny Vue client shell
|
|
11
11
|
- standardized scripts via `@jskit-ai/app-scripts`
|
|
12
|
-
- framework composition seed file: `framework/app.manifest.mjs`
|
|
13
12
|
|
|
14
13
|
## What This Is Not
|
|
15
14
|
|
|
@@ -31,3 +30,10 @@ npm run server
|
|
|
31
30
|
npm run test
|
|
32
31
|
npm run test:client
|
|
33
32
|
```
|
|
33
|
+
|
|
34
|
+
## Progressive Bundles
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx jskit add db --provider mysql --no-install
|
|
38
|
+
npx jskit add auth-base --no-install
|
|
39
|
+
```
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": "20.x"
|
|
9
9
|
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"jskit": "node_modules/@jskit-ai/jskit/packages/tooling/jskit/bin/jskit.js"
|
|
12
|
+
},
|
|
10
13
|
"scripts": {
|
|
11
14
|
"server": "jskit-app-scripts server",
|
|
12
15
|
"start": "jskit-app-scripts start",
|
|
@@ -25,6 +28,7 @@
|
|
|
25
28
|
},
|
|
26
29
|
"devDependencies": {
|
|
27
30
|
"@jskit-ai/config-eslint": "0.1.0",
|
|
31
|
+
"@jskit-ai/jskit": "github:mobily-enterprises/jskit-ai",
|
|
28
32
|
"@vitejs/plugin-vue": "^5.2.1",
|
|
29
33
|
"eslint": "^9.39.1",
|
|
30
34
|
"vite": "^6.1.0",
|
|
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
5
|
+
import { access, readdir, readFile } from "node:fs/promises";
|
|
6
6
|
|
|
7
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
8
|
const __dirname = path.dirname(__filename);
|
|
@@ -16,6 +16,7 @@ const EXPECTED_RUNTIME_DEPENDENCIES = Object.freeze([
|
|
|
16
16
|
|
|
17
17
|
const EXPECTED_DEV_DEPENDENCIES = Object.freeze([
|
|
18
18
|
"@jskit-ai/config-eslint",
|
|
19
|
+
"@jskit-ai/jskit",
|
|
19
20
|
"@vitejs/plugin-vue",
|
|
20
21
|
"eslint",
|
|
21
22
|
"vite",
|
|
@@ -23,12 +24,12 @@ const EXPECTED_DEV_DEPENDENCIES = Object.freeze([
|
|
|
23
24
|
]);
|
|
24
25
|
|
|
25
26
|
const EXPECTED_TOP_LEVEL_ENTRIES = Object.freeze([
|
|
27
|
+
"Procfile",
|
|
26
28
|
"README.md",
|
|
27
29
|
"app.scripts.config.mjs",
|
|
28
30
|
"bin",
|
|
29
31
|
"eslint.config.mjs",
|
|
30
32
|
"favicon.svg",
|
|
31
|
-
"framework",
|
|
32
33
|
"gitignore",
|
|
33
34
|
"index.html",
|
|
34
35
|
"package.json",
|
|
@@ -87,14 +88,6 @@ test("starter shell keeps a strict top-level footprint", async () => {
|
|
|
87
88
|
assert.deepEqual(sortStrings(entries), sortStrings(EXPECTED_TOP_LEVEL_ENTRIES));
|
|
88
89
|
});
|
|
89
90
|
|
|
90
|
-
test("manifest scaffold
|
|
91
|
-
|
|
92
|
-
const manifest = manifestModule?.default;
|
|
93
|
-
|
|
94
|
-
assert.equal(manifest.manifestVersion, 1);
|
|
95
|
-
assert.equal(manifest.appId, "__APP_NAME__");
|
|
96
|
-
assert.equal(manifest.profileId, "web-saas-default");
|
|
97
|
-
assert.equal(manifest.mode, "strict");
|
|
98
|
-
assert.equal(manifest.enforceProfileRequired, true);
|
|
99
|
-
assert.deepEqual(manifest.optionalModulePacks, ["core"]);
|
|
91
|
+
test("legacy app.manifest scaffold is removed from starter shell", async () => {
|
|
92
|
+
await assert.rejects(access(path.join(APP_ROOT, "framework/app.manifest.mjs")), /ENOENT/);
|
|
100
93
|
});
|