@jskit-ai/create-app 0.1.0 → 0.1.2
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 +3 -6
- package/package.json +3 -2
- package/src/shared/cliEntrypoint.js +25 -0
- package/src/{index.js → shared/index.js} +157 -18
- package/templates/base-shell/Procfile +1 -0
- package/templates/base-shell/README.md +7 -1
- package/templates/base-shell/package.json +6 -0
- package/templates/base-shell/package.json.ACTUAL_CORRECT +38 -0
- package/templates/base-shell/server.js +76 -4
- package/templates/base-shell/src/App.vue +24 -0
- package/templates/base-shell/src/main.js +2 -28
- package/templates/base-shell/tests/server/minimalShell.contract.test.js +9 -13
- package/templates/base-shell/tests/server/smoke.test.js +1 -1
- package/templates/base-shell/vite.config.mjs +23 -9
- package/templates/base-shell/vite.shared.mjs +9 -0
- 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
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
import { runCli } from "../src/index.js";
|
|
2
|
+
import { runCliEntrypoint } from "../src/shared/cliEntrypoint.js";
|
|
3
|
+
import { runCli } from "../src/shared/index.js";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
if (exitCode !== 0) {
|
|
7
|
-
process.exit(exitCode);
|
|
8
|
-
}
|
|
5
|
+
await runCliEntrypoint(runCli);
|
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.2",
|
|
4
4
|
"description": "Scaffold minimal JSKIT app shells.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -16,8 +16,9 @@
|
|
|
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
|
+
"dependencies": {},
|
|
21
22
|
"engines": {
|
|
22
23
|
"node": "20.x"
|
|
23
24
|
},
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
|
|
3
|
+
function shellQuote(value) {
|
|
4
|
+
const raw = String(value ?? "");
|
|
5
|
+
if (!raw) {
|
|
6
|
+
return "''";
|
|
7
|
+
}
|
|
8
|
+
if (/^[A-Za-z0-9_./:=+,-]+$/.test(raw)) {
|
|
9
|
+
return raw;
|
|
10
|
+
}
|
|
11
|
+
return `'${raw.replace(/'/g, "'\\''")}'`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function runCliEntrypoint(runCli, argv = process.argv.slice(2)) {
|
|
15
|
+
if (typeof runCli !== "function") {
|
|
16
|
+
throw new TypeError("runCliEntrypoint requires a runCli function");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const exitCode = await runCli(argv);
|
|
20
|
+
if (exitCode !== 0) {
|
|
21
|
+
process.exit(exitCode);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { shellQuote, runCliEntrypoint };
|
|
@@ -3,9 +3,15 @@ import path from "node:path";
|
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { shellQuote } from "./cliEntrypoint.js";
|
|
6
7
|
|
|
7
8
|
const DEFAULT_TEMPLATE = "base-shell";
|
|
8
|
-
const
|
|
9
|
+
const DEFAULT_INITIAL_BUNDLES = "none";
|
|
10
|
+
const DEFAULT_DB_PROVIDER = "mysql";
|
|
11
|
+
const INITIAL_BUNDLE_PRESETS = new Set(["none", "db", "db-auth"]);
|
|
12
|
+
const DB_PROVIDERS = new Set(["mysql", "postgres"]);
|
|
13
|
+
const ALLOWED_EXISTING_TARGET_ENTRIES = new Set([".git"]);
|
|
14
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
9
15
|
const TEMPLATES_ROOT = path.join(PACKAGE_ROOT, "templates");
|
|
10
16
|
|
|
11
17
|
function createCliError(message, { showUsage = false, exitCode = 1 } = {}) {
|
|
@@ -15,17 +21,6 @@ function createCliError(message, { showUsage = false, exitCode = 1 } = {}) {
|
|
|
15
21
|
return error;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
function shellQuote(value) {
|
|
19
|
-
const raw = String(value ?? "");
|
|
20
|
-
if (!raw) {
|
|
21
|
-
return "''";
|
|
22
|
-
}
|
|
23
|
-
if (/^[A-Za-z0-9_./:=+,-]+$/.test(raw)) {
|
|
24
|
-
return raw;
|
|
25
|
-
}
|
|
26
|
-
return `'${raw.replace(/'/g, "'\\''")}'`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
24
|
function toAppTitle(appName) {
|
|
30
25
|
const words = String(appName)
|
|
31
26
|
.trim()
|
|
@@ -36,6 +31,53 @@ function toAppTitle(appName) {
|
|
|
36
31
|
return words.length > 0 ? words.join(" ") : "App";
|
|
37
32
|
}
|
|
38
33
|
|
|
34
|
+
function normalizeInitialBundlesPreset(value, { showUsage = true } = {}) {
|
|
35
|
+
const normalized = String(value || DEFAULT_INITIAL_BUNDLES).trim().toLowerCase();
|
|
36
|
+
if (INITIAL_BUNDLE_PRESETS.has(normalized)) {
|
|
37
|
+
return normalized;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
throw createCliError(
|
|
41
|
+
`Invalid --initial-bundles value "${value}". Expected one of: none, db, db-auth.`,
|
|
42
|
+
{ showUsage }
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeDbProvider(value, { showUsage = true } = {}) {
|
|
47
|
+
const normalized = String(value || DEFAULT_DB_PROVIDER).trim().toLowerCase();
|
|
48
|
+
if (DB_PROVIDERS.has(normalized)) {
|
|
49
|
+
return normalized;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
throw createCliError(
|
|
53
|
+
`Invalid --db-provider value "${value}". Expected one of: mysql, postgres.`,
|
|
54
|
+
{ showUsage }
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildInitialBundleCommands(initialBundles, dbProvider) {
|
|
59
|
+
const normalizedPreset = normalizeInitialBundlesPreset(initialBundles, { showUsage: false });
|
|
60
|
+
const normalizedProvider = normalizeDbProvider(dbProvider, { showUsage: false });
|
|
61
|
+
|
|
62
|
+
const commands = [];
|
|
63
|
+
if (normalizedPreset === "db" || normalizedPreset === "db-auth") {
|
|
64
|
+
commands.push(`npx jskit add db --provider ${normalizedProvider} --no-install`);
|
|
65
|
+
}
|
|
66
|
+
if (normalizedPreset === "db-auth") {
|
|
67
|
+
commands.push("npx jskit add auth-base --no-install");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return commands;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildProgressiveBundleCommands(dbProvider) {
|
|
74
|
+
const normalizedProvider = normalizeDbProvider(dbProvider, { showUsage: false });
|
|
75
|
+
return [
|
|
76
|
+
`npx jskit add db --provider ${normalizedProvider} --no-install`,
|
|
77
|
+
"npx jskit add auth-base --no-install"
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
|
|
39
81
|
function validateAppName(appName, { showUsage = true } = {}) {
|
|
40
82
|
if (!appName || typeof appName !== "string") {
|
|
41
83
|
throw createCliError("Missing app name.", { showUsage });
|
|
@@ -69,6 +111,8 @@ function parseCliArgs(argv) {
|
|
|
69
111
|
appTitle: null,
|
|
70
112
|
template: DEFAULT_TEMPLATE,
|
|
71
113
|
target: null,
|
|
114
|
+
initialBundles: DEFAULT_INITIAL_BUNDLES,
|
|
115
|
+
dbProvider: DEFAULT_DB_PROVIDER,
|
|
72
116
|
force: false,
|
|
73
117
|
dryRun: false,
|
|
74
118
|
help: false,
|
|
@@ -136,6 +180,30 @@ function parseCliArgs(argv) {
|
|
|
136
180
|
continue;
|
|
137
181
|
}
|
|
138
182
|
|
|
183
|
+
if (arg === "--initial-bundles") {
|
|
184
|
+
const { value, nextIndex } = parseOptionWithValue(args, index, "--initial-bundles");
|
|
185
|
+
options.initialBundles = value;
|
|
186
|
+
index = nextIndex;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (arg.startsWith("--initial-bundles=")) {
|
|
191
|
+
options.initialBundles = arg.slice("--initial-bundles=".length);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (arg === "--db-provider") {
|
|
196
|
+
const { value, nextIndex } = parseOptionWithValue(args, index, "--db-provider");
|
|
197
|
+
options.dbProvider = value;
|
|
198
|
+
index = nextIndex;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (arg.startsWith("--db-provider=")) {
|
|
203
|
+
options.dbProvider = arg.slice("--db-provider=".length);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
139
207
|
if (arg.startsWith("-")) {
|
|
140
208
|
throw createCliError(`Unknown option: ${arg}`, {
|
|
141
209
|
showUsage: true
|
|
@@ -171,6 +239,8 @@ function printUsage(stream = process.stderr) {
|
|
|
171
239
|
stream.write(` --template <name> Template folder under templates/ (default: ${DEFAULT_TEMPLATE})\n`);
|
|
172
240
|
stream.write(" --title <text> App title used for template replacements\n");
|
|
173
241
|
stream.write(" --target <path> Target directory (default: ./<app-name>)\n");
|
|
242
|
+
stream.write(" --initial-bundles <preset> Optional bundle preset: none | db | db-auth (default: none)\n");
|
|
243
|
+
stream.write(" --db-provider <provider> Database provider for db presets: mysql | postgres (default: mysql)\n");
|
|
174
244
|
stream.write(" --force Allow writing into a non-empty target directory\n");
|
|
175
245
|
stream.write(" --dry-run Print planned writes without changing the filesystem\n");
|
|
176
246
|
stream.write(" --interactive Prompt for app values instead of passing all flags\n");
|
|
@@ -233,7 +303,8 @@ async function ensureTargetDirectoryState(targetDirectory, { force = false, dryR
|
|
|
233
303
|
}
|
|
234
304
|
|
|
235
305
|
const entries = await readdir(targetDirectory);
|
|
236
|
-
|
|
306
|
+
const blockingEntries = entries.filter((entry) => !ALLOWED_EXISTING_TARGET_ENTRIES.has(entry));
|
|
307
|
+
if (blockingEntries.length > 0 && !force) {
|
|
237
308
|
throw createCliError(
|
|
238
309
|
`Target directory is not empty: ${targetDirectory}. Use --force to allow writing into it.`
|
|
239
310
|
);
|
|
@@ -335,11 +406,24 @@ async function askYesNoQuestion(readline, label, defaultValue) {
|
|
|
335
406
|
}
|
|
336
407
|
}
|
|
337
408
|
|
|
338
|
-
|
|
339
|
-
|
|
409
|
+
function createReadlineInterface({ stdin = process.stdin, stdout = process.stdout } = {}) {
|
|
410
|
+
return createInterface({
|
|
340
411
|
input: stdin,
|
|
341
412
|
output: stdout
|
|
342
413
|
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function collectInteractiveOptions({
|
|
417
|
+
parsed,
|
|
418
|
+
stdout = process.stdout,
|
|
419
|
+
stderr = process.stderr,
|
|
420
|
+
stdin = process.stdin,
|
|
421
|
+
readlineFactory = createReadlineInterface
|
|
422
|
+
}) {
|
|
423
|
+
const readline = readlineFactory({
|
|
424
|
+
stdin,
|
|
425
|
+
stdout
|
|
426
|
+
});
|
|
343
427
|
|
|
344
428
|
try {
|
|
345
429
|
let appName = String(parsed.appName || "").trim();
|
|
@@ -368,12 +452,42 @@ async function collectInteractiveOptions({ parsed, stdout = process.stdout, stde
|
|
|
368
452
|
Boolean(parsed.force)
|
|
369
453
|
);
|
|
370
454
|
|
|
455
|
+
let initialBundles = normalizeInitialBundlesPreset(parsed.initialBundles, { showUsage: false });
|
|
456
|
+
while (true) {
|
|
457
|
+
const candidate = await askQuestion(
|
|
458
|
+
readline,
|
|
459
|
+
"Initial bundle preset (none|db|db-auth)",
|
|
460
|
+
initialBundles
|
|
461
|
+
);
|
|
462
|
+
try {
|
|
463
|
+
initialBundles = normalizeInitialBundlesPreset(candidate, { showUsage: false });
|
|
464
|
+
break;
|
|
465
|
+
} catch (error) {
|
|
466
|
+
stderr.write(`Error: ${error?.message || String(error)}\n`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
let dbProvider = normalizeDbProvider(parsed.dbProvider, { showUsage: false });
|
|
471
|
+
if (initialBundles === "db" || initialBundles === "db-auth") {
|
|
472
|
+
while (true) {
|
|
473
|
+
const candidate = await askQuestion(readline, "DB provider (mysql|postgres)", dbProvider);
|
|
474
|
+
try {
|
|
475
|
+
dbProvider = normalizeDbProvider(candidate, { showUsage: false });
|
|
476
|
+
break;
|
|
477
|
+
} catch (error) {
|
|
478
|
+
stderr.write(`Error: ${error?.message || String(error)}\n`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
371
483
|
return {
|
|
372
484
|
appName,
|
|
373
485
|
appTitle,
|
|
374
486
|
target,
|
|
375
487
|
template,
|
|
376
|
-
force
|
|
488
|
+
force,
|
|
489
|
+
initialBundles,
|
|
490
|
+
dbProvider
|
|
377
491
|
};
|
|
378
492
|
} finally {
|
|
379
493
|
readline.close();
|
|
@@ -385,6 +499,8 @@ export async function createApp({
|
|
|
385
499
|
appTitle = null,
|
|
386
500
|
template = DEFAULT_TEMPLATE,
|
|
387
501
|
target = null,
|
|
502
|
+
initialBundles = DEFAULT_INITIAL_BUNDLES,
|
|
503
|
+
dbProvider = DEFAULT_DB_PROVIDER,
|
|
388
504
|
force = false,
|
|
389
505
|
dryRun = false,
|
|
390
506
|
cwd = process.cwd()
|
|
@@ -393,6 +509,8 @@ export async function createApp({
|
|
|
393
509
|
validateAppName(resolvedAppName);
|
|
394
510
|
|
|
395
511
|
const resolvedAppTitle = String(appTitle || "").trim() || toAppTitle(resolvedAppName);
|
|
512
|
+
const resolvedInitialBundles = normalizeInitialBundlesPreset(initialBundles);
|
|
513
|
+
const resolvedDbProvider = normalizeDbProvider(dbProvider);
|
|
396
514
|
|
|
397
515
|
const resolvedCwd = path.resolve(cwd);
|
|
398
516
|
const targetDirectory = path.resolve(resolvedCwd, target ? String(target) : resolvedAppName);
|
|
@@ -419,6 +537,10 @@ export async function createApp({
|
|
|
419
537
|
appName: resolvedAppName,
|
|
420
538
|
appTitle: resolvedAppTitle,
|
|
421
539
|
template: String(template),
|
|
540
|
+
initialBundles: resolvedInitialBundles,
|
|
541
|
+
dbProvider: resolvedDbProvider,
|
|
542
|
+
selectedBundleCommands: buildInitialBundleCommands(resolvedInitialBundles, resolvedDbProvider),
|
|
543
|
+
progressiveBundleCommands: buildProgressiveBundleCommands(resolvedDbProvider),
|
|
422
544
|
targetDirectory,
|
|
423
545
|
dryRun,
|
|
424
546
|
touchedFiles
|
|
@@ -431,7 +553,8 @@ export async function runCli(
|
|
|
431
553
|
stdout = process.stdout,
|
|
432
554
|
stderr = process.stderr,
|
|
433
555
|
stdin = process.stdin,
|
|
434
|
-
cwd = process.cwd()
|
|
556
|
+
cwd = process.cwd(),
|
|
557
|
+
readlineFactory = createReadlineInterface
|
|
435
558
|
} = {}
|
|
436
559
|
) {
|
|
437
560
|
try {
|
|
@@ -449,7 +572,8 @@ export async function runCli(
|
|
|
449
572
|
parsed,
|
|
450
573
|
stdout,
|
|
451
574
|
stderr,
|
|
452
|
-
stdin
|
|
575
|
+
stdin,
|
|
576
|
+
readlineFactory
|
|
453
577
|
}))
|
|
454
578
|
}
|
|
455
579
|
: parsed;
|
|
@@ -459,6 +583,8 @@ export async function runCli(
|
|
|
459
583
|
appTitle: resolvedOptions.appTitle,
|
|
460
584
|
template: resolvedOptions.template,
|
|
461
585
|
target: resolvedOptions.target,
|
|
586
|
+
initialBundles: resolvedOptions.initialBundles,
|
|
587
|
+
dbProvider: resolvedOptions.dbProvider,
|
|
462
588
|
force: resolvedOptions.force,
|
|
463
589
|
dryRun: resolvedOptions.dryRun,
|
|
464
590
|
cwd
|
|
@@ -484,6 +610,19 @@ export async function runCli(
|
|
|
484
610
|
stdout.write(`- cd ${shellQuote(targetLabel)}\n`);
|
|
485
611
|
stdout.write("- npm install\n");
|
|
486
612
|
stdout.write("- npm run dev\n");
|
|
613
|
+
|
|
614
|
+
stdout.write("\n");
|
|
615
|
+
if (result.selectedBundleCommands.length > 0) {
|
|
616
|
+
stdout.write(`Initial framework bundle commands (${result.initialBundles}):\n`);
|
|
617
|
+
for (const command of result.selectedBundleCommands) {
|
|
618
|
+
stdout.write(`- ${command}\n`);
|
|
619
|
+
}
|
|
620
|
+
} else {
|
|
621
|
+
stdout.write("Add framework capabilities when ready:\n");
|
|
622
|
+
for (const command of result.progressiveBundleCommands) {
|
|
623
|
+
stdout.write(`- ${command}\n`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
487
626
|
}
|
|
488
627
|
|
|
489
628
|
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",
|
|
@@ -20,11 +23,14 @@
|
|
|
20
23
|
},
|
|
21
24
|
"dependencies": {
|
|
22
25
|
"@jskit-ai/app-scripts": "0.1.0",
|
|
26
|
+
"@jskit-ai/server-runtime-core": "file:node_modules/@jskit-ai/jskit/packages/runtime/server-runtime-core",
|
|
27
|
+
"@jskit-ai/surface-routing": "file:node_modules/@jskit-ai/jskit/packages/surface-routing",
|
|
23
28
|
"fastify": "^5.7.4",
|
|
24
29
|
"vue": "^3.5.13"
|
|
25
30
|
},
|
|
26
31
|
"devDependencies": {
|
|
27
32
|
"@jskit-ai/config-eslint": "0.1.0",
|
|
33
|
+
"@jskit-ai/jskit": "github:mobily-enterprises/jskit-ai",
|
|
28
34
|
"@vitejs/plugin-vue": "^5.2.1",
|
|
29
35
|
"eslint": "^9.39.1",
|
|
30
36
|
"vite": "^6.1.0",
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__APP_NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Minimal JSKIT base app (Fastify + Vue)",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": "20.x"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"jskit": "node_modules/@jskit-ai/jskit/packages/tooling/jskit/bin/jskit.js"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"server": "jskit-app-scripts server",
|
|
15
|
+
"start": "jskit-app-scripts start",
|
|
16
|
+
"dev": "jskit-app-scripts dev",
|
|
17
|
+
"build": "jskit-app-scripts build",
|
|
18
|
+
"preview": "jskit-app-scripts preview",
|
|
19
|
+
"lint": "jskit-app-scripts lint",
|
|
20
|
+
"lint:process-env": "jskit-app-scripts lint:process-env",
|
|
21
|
+
"test": "jskit-app-scripts test",
|
|
22
|
+
"test:client": "jskit-app-scripts test:client"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@jskit-ai/app-scripts": "0.1.0",
|
|
26
|
+
"@jskit-ai/server-runtime-core": "file:node_modules/@jskit-ai/jskit/packages/runtime/server-runtime-core",
|
|
27
|
+
"fastify": "^5.7.4",
|
|
28
|
+
"vue": "^3.5.13"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@jskit-ai/config-eslint": "0.1.0",
|
|
32
|
+
"@jskit-ai/jskit": "github:mobily-enterprises/jskit-ai",
|
|
33
|
+
"@vitejs/plugin-vue": "^5.2.1",
|
|
34
|
+
"eslint": "^9.39.1",
|
|
35
|
+
"vite": "^6.1.0",
|
|
36
|
+
"vitest": "^4.0.18"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -1,15 +1,87 @@
|
|
|
1
1
|
import Fastify from "fastify";
|
|
2
2
|
import { resolveRuntimeEnv } from "./server/lib/runtimeEnv.js";
|
|
3
|
+
import { registerApiRouteDefinitions } from "@jskit-ai/server-runtime-core/apiRouteRegistration";
|
|
4
|
+
import { createServerRuntimeFromApp, applyContributedRuntimeLifecycle } from "@jskit-ai/server-runtime-core/serverContributions";
|
|
5
|
+
import path from "node:path";
|
|
3
6
|
|
|
4
|
-
function
|
|
5
|
-
const app = Fastify({ logger: true });
|
|
6
|
-
|
|
7
|
+
function registerFallbackHealthRoute(app) {
|
|
7
8
|
app.get("/api/v1/health", async () => {
|
|
8
9
|
return {
|
|
9
10
|
ok: true,
|
|
10
11
|
app: "__APP_NAME__"
|
|
11
12
|
};
|
|
12
13
|
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function registerContributedRuntime(app, { appRoot, runtimeEnv }) {
|
|
17
|
+
try {
|
|
18
|
+
const composed = await createServerRuntimeFromApp({
|
|
19
|
+
appRoot,
|
|
20
|
+
strict: false,
|
|
21
|
+
dependencies: {
|
|
22
|
+
env: runtimeEnv,
|
|
23
|
+
logger: app.log
|
|
24
|
+
},
|
|
25
|
+
routeConfig: {}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const routeCount = Array.isArray(composed.routes) ? composed.routes.length : 0;
|
|
29
|
+
if (routeCount > 0) {
|
|
30
|
+
registerApiRouteDefinitions(app, {
|
|
31
|
+
routes: composed.routes
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const lifecycleResult = await applyContributedRuntimeLifecycle({
|
|
36
|
+
app,
|
|
37
|
+
runtimeResult: composed,
|
|
38
|
+
dependencies: {
|
|
39
|
+
env: runtimeEnv,
|
|
40
|
+
logger: app.log
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
app.log.info(
|
|
45
|
+
{
|
|
46
|
+
routeCount,
|
|
47
|
+
pluginCount: lifecycleResult.pluginCount,
|
|
48
|
+
workerCount: lifecycleResult.workerCount,
|
|
49
|
+
onBootCount: lifecycleResult.onBootCount,
|
|
50
|
+
packageOrder: composed.packageOrder
|
|
51
|
+
},
|
|
52
|
+
"Registered JSKIT contributed server runtime."
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
enabled: true,
|
|
57
|
+
routeCount,
|
|
58
|
+
pluginCount: lifecycleResult.pluginCount,
|
|
59
|
+
workerCount: lifecycleResult.workerCount,
|
|
60
|
+
onBootCount: lifecycleResult.onBootCount
|
|
61
|
+
};
|
|
62
|
+
} catch (error) {
|
|
63
|
+
const message = String(error?.message || "");
|
|
64
|
+
if (message.includes("Lock file not found:")) {
|
|
65
|
+
return {
|
|
66
|
+
enabled: false,
|
|
67
|
+
routeCount: 0
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function createServer() {
|
|
75
|
+
const app = Fastify({ logger: true });
|
|
76
|
+
const runtimeEnv = resolveRuntimeEnv();
|
|
77
|
+
const appRoot = path.resolve(process.cwd());
|
|
78
|
+
const contributed = await registerContributedRuntime(app, {
|
|
79
|
+
appRoot,
|
|
80
|
+
runtimeEnv
|
|
81
|
+
});
|
|
82
|
+
if (!contributed.enabled || contributed.routeCount < 1) {
|
|
83
|
+
registerFallbackHealthRoute(app);
|
|
84
|
+
}
|
|
13
85
|
|
|
14
86
|
return app;
|
|
15
87
|
}
|
|
@@ -18,7 +90,7 @@ async function startServer(options = {}) {
|
|
|
18
90
|
const runtimeEnv = resolveRuntimeEnv();
|
|
19
91
|
const port = Number(options?.port) || runtimeEnv.PORT;
|
|
20
92
|
const host = String(options?.host || "").trim() || runtimeEnv.HOST;
|
|
21
|
-
const app = createServer();
|
|
93
|
+
const app = await createServer();
|
|
22
94
|
await app.listen({ port, host });
|
|
23
95
|
return app;
|
|
24
96
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { onMounted, ref } from "vue";
|
|
3
|
+
|
|
4
|
+
const appTitle = "__APP_TITLE__";
|
|
5
|
+
const health = ref("loading...");
|
|
6
|
+
|
|
7
|
+
onMounted(async () => {
|
|
8
|
+
try {
|
|
9
|
+
const response = await fetch("/api/v1/health");
|
|
10
|
+
const payload = await response.json();
|
|
11
|
+
health.value = payload?.ok ? "ok" : "unhealthy";
|
|
12
|
+
} catch {
|
|
13
|
+
health.value = "unreachable";
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<main style="font-family: sans-serif; max-width: 48rem; margin: 3rem auto; padding: 0 1rem;">
|
|
20
|
+
<h1>{{ appTitle }}</h1>
|
|
21
|
+
<p>Minimal starter shell is running.</p>
|
|
22
|
+
<p><strong>Health:</strong> {{ health }}</p>
|
|
23
|
+
</main>
|
|
24
|
+
</template>
|
|
@@ -1,30 +1,4 @@
|
|
|
1
|
-
import { createApp
|
|
2
|
-
|
|
3
|
-
const App = {
|
|
4
|
-
setup() {
|
|
5
|
-
const health = ref("loading...");
|
|
6
|
-
|
|
7
|
-
onMounted(async () => {
|
|
8
|
-
try {
|
|
9
|
-
const response = await fetch("/api/v1/health");
|
|
10
|
-
const payload = await response.json();
|
|
11
|
-
health.value = payload?.ok ? "ok" : "unhealthy";
|
|
12
|
-
} catch {
|
|
13
|
-
health.value = "unreachable";
|
|
14
|
-
}
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
return {
|
|
18
|
-
health
|
|
19
|
-
};
|
|
20
|
-
},
|
|
21
|
-
template: `
|
|
22
|
-
<main style="font-family: sans-serif; max-width: 48rem; margin: 3rem auto; padding: 0 1rem;">
|
|
23
|
-
<h1>__APP_TITLE__</h1>
|
|
24
|
-
<p>Minimal starter shell is running.</p>
|
|
25
|
-
<p><strong>Health:</strong> {{ health }}</p>
|
|
26
|
-
</main>
|
|
27
|
-
`
|
|
28
|
-
};
|
|
1
|
+
import { createApp } from "vue";
|
|
2
|
+
import App from "./App.vue";
|
|
29
3
|
|
|
30
4
|
createApp(App).mount("#app");
|
|
@@ -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);
|
|
@@ -10,12 +10,14 @@ const APP_ROOT = path.resolve(__dirname, "../..");
|
|
|
10
10
|
|
|
11
11
|
const EXPECTED_RUNTIME_DEPENDENCIES = Object.freeze([
|
|
12
12
|
"@jskit-ai/app-scripts",
|
|
13
|
+
"@jskit-ai/server-runtime-core",
|
|
13
14
|
"fastify",
|
|
14
15
|
"vue"
|
|
15
16
|
]);
|
|
16
17
|
|
|
17
18
|
const EXPECTED_DEV_DEPENDENCIES = Object.freeze([
|
|
18
19
|
"@jskit-ai/config-eslint",
|
|
20
|
+
"@jskit-ai/jskit",
|
|
19
21
|
"@vitejs/plugin-vue",
|
|
20
22
|
"eslint",
|
|
21
23
|
"vite",
|
|
@@ -23,20 +25,22 @@ const EXPECTED_DEV_DEPENDENCIES = Object.freeze([
|
|
|
23
25
|
]);
|
|
24
26
|
|
|
25
27
|
const EXPECTED_TOP_LEVEL_ENTRIES = Object.freeze([
|
|
28
|
+
"Procfile",
|
|
26
29
|
"README.md",
|
|
27
30
|
"app.scripts.config.mjs",
|
|
28
31
|
"bin",
|
|
29
32
|
"eslint.config.mjs",
|
|
30
33
|
"favicon.svg",
|
|
31
|
-
"framework",
|
|
32
34
|
"gitignore",
|
|
33
35
|
"index.html",
|
|
34
36
|
"package.json",
|
|
37
|
+
"package.json.ACTUAL_CORRECT",
|
|
35
38
|
"server.js",
|
|
36
39
|
"server",
|
|
37
40
|
"src",
|
|
38
41
|
"tests",
|
|
39
|
-
"vite.config.mjs"
|
|
42
|
+
"vite.config.mjs",
|
|
43
|
+
"vite.shared.mjs"
|
|
40
44
|
]);
|
|
41
45
|
|
|
42
46
|
async function readPackageJson() {
|
|
@@ -87,14 +91,6 @@ test("starter shell keeps a strict top-level footprint", async () => {
|
|
|
87
91
|
assert.deepEqual(sortStrings(entries), sortStrings(EXPECTED_TOP_LEVEL_ENTRIES));
|
|
88
92
|
});
|
|
89
93
|
|
|
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"]);
|
|
94
|
+
test("legacy app.manifest scaffold is removed from starter shell", async () => {
|
|
95
|
+
await assert.rejects(access(path.join(APP_ROOT, "framework/app.manifest.mjs")), /ENOENT/);
|
|
100
96
|
});
|
|
@@ -3,7 +3,7 @@ import test from "node:test";
|
|
|
3
3
|
import { createServer } from "../../server.js";
|
|
4
4
|
|
|
5
5
|
test("GET /api/v1/health returns ok payload", async () => {
|
|
6
|
-
const app = createServer();
|
|
6
|
+
const app = await createServer();
|
|
7
7
|
const response = await app.inject({
|
|
8
8
|
method: "GET",
|
|
9
9
|
url: "/api/v1/health"
|
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
import { defineConfig } from "vite";
|
|
2
2
|
import vue from "@vitejs/plugin-vue";
|
|
3
|
-
|
|
4
|
-
function toPositiveInt(value, fallback) {
|
|
5
|
-
const parsed = Number.parseInt(String(value || "").trim(), 10);
|
|
6
|
-
if (Number.isInteger(parsed) && parsed > 0) {
|
|
7
|
-
return parsed;
|
|
8
|
-
}
|
|
9
|
-
return fallback;
|
|
10
|
-
}
|
|
3
|
+
import { toPositiveInt } from "./vite.shared.mjs";
|
|
11
4
|
|
|
12
5
|
const devPort = toPositiveInt(process.env.VITE_DEV_PORT, 5173);
|
|
13
6
|
const apiProxyTarget = String(process.env.VITE_API_PROXY_TARGET || "").trim() || "http://localhost:3000";
|
|
7
|
+
const clientEntry = (() => {
|
|
8
|
+
const normalized = String(process.env.VITE_CLIENT_ENTRY || "").trim();
|
|
9
|
+
if (!normalized) {
|
|
10
|
+
return "/src/main.js";
|
|
11
|
+
}
|
|
12
|
+
if (normalized.startsWith("/")) {
|
|
13
|
+
return normalized;
|
|
14
|
+
}
|
|
15
|
+
if (normalized.startsWith("src/")) {
|
|
16
|
+
return `/${normalized}`;
|
|
17
|
+
}
|
|
18
|
+
return `/src/${normalized}`;
|
|
19
|
+
})();
|
|
14
20
|
|
|
15
21
|
export default defineConfig({
|
|
16
|
-
plugins: [
|
|
22
|
+
plugins: [
|
|
23
|
+
vue(),
|
|
24
|
+
{
|
|
25
|
+
name: "jskit-client-entry",
|
|
26
|
+
transformIndexHtml(source) {
|
|
27
|
+
return String(source || "").replace(/\/src\/main\.js/g, clientEntry);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
],
|
|
17
31
|
test: {
|
|
18
32
|
include: ["tests/client/**/*.vitest.js"]
|
|
19
33
|
},
|