@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 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
@@ -1,8 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import process from "node:process";
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
- const exitCode = await runCli(process.argv.slice(2));
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.0",
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 PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
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
- if (entries.length > 0 && !force) {
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
- async function collectInteractiveOptions({ parsed, stdout = process.stdout, stderr = process.stderr, stdin = process.stdin }) {
339
- const readline = createInterface({
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 createServer() {
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, onMounted, ref } from "vue";
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 exists with strict core-only defaults", async () => {
91
- const manifestModule = await import(path.join(APP_ROOT, "framework/app.manifest.mjs"));
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: [vue()],
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
  },
@@ -0,0 +1,9 @@
1
+ function toPositiveInt(value, fallback) {
2
+ const parsed = Number.parseInt(String(value || "").trim(), 10);
3
+ if (Number.isInteger(parsed) && parsed > 0) {
4
+ return parsed;
5
+ }
6
+ return fallback;
7
+ }
8
+
9
+ export { toPositiveInt };
@@ -1,8 +0,0 @@
1
- export default Object.freeze({
2
- manifestVersion: 1,
3
- appId: "__APP_NAME__",
4
- profileId: "web-saas-default",
5
- mode: "strict",
6
- enforceProfileRequired: true,
7
- optionalModulePacks: ["core"]
8
- });