@jskit-ai/jskit-cli 0.2.136 → 0.2.139

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/jskit-cli",
3
- "version": "0.2.136",
3
+ "version": "0.2.139",
4
4
  "description": "Bundle and package orchestration CLI for JSKIT apps.",
5
5
  "type": "module",
6
6
  "files": [
@@ -14,15 +14,16 @@
14
14
  "exports": {
15
15
  ".": "./src/index.js",
16
16
  "./server": "./src/server/index.js",
17
- "./client": "./src/client/index.js"
17
+ "./client": "./src/client/index.js",
18
+ "./test/playwright": "./src/test/playwright.js"
18
19
  },
19
20
  "scripts": {
20
21
  "test": "node --test"
21
22
  },
22
23
  "dependencies": {
23
- "@jskit-ai/jskit-catalog": "0.1.131",
24
- "@jskit-ai/kernel": "0.1.121",
25
- "@jskit-ai/shell-web": "0.1.120",
24
+ "@jskit-ai/jskit-catalog": "0.1.134",
25
+ "@jskit-ai/kernel": "0.1.122",
26
+ "@jskit-ai/shell-web": "0.1.123",
26
27
  "@vue/compiler-sfc": "^3.5.29",
27
28
  "ts-morph": "^28.0.0",
28
29
  "yaml": "^2.8.3"
@@ -18,11 +18,10 @@ import {
18
18
  } from "../core/commandCatalog.js";
19
19
 
20
20
  const WRAPPER_COMMANDS = new Set(["npx", "jsx"]);
21
- const KNOWN_GENERATE_FLAG_OPTIONS = Object.freeze(["dry-run", "run-npm-install", "devlinks", "json", "verbose"]);
21
+ const KNOWN_GENERATE_FLAG_OPTIONS = Object.freeze(["dry-run", "run-npm-install", "json", "verbose"]);
22
22
  const BOOLEAN_OPTION_NAMES = new Set([
23
23
  "dry-run",
24
24
  "run-npm-install",
25
- "devlinks",
26
25
  "full",
27
26
  "expanded",
28
27
  "details",
@@ -784,7 +783,6 @@ function buildCommandOptionMeta(command = "", catalogModule) {
784
783
  const labels = {
785
784
  dryRun: "dry-run",
786
785
  runNpmInstall: "run-npm-install",
787
- devlinks: "devlinks",
788
786
  full: "full",
789
787
  expanded: "expanded",
790
788
  details: "details",
@@ -9,7 +9,6 @@ import {
9
9
  resolveAppCommandDefinition
10
10
  } from "./appCommandCatalog.js";
11
11
  import { runAppAdoptManagedScriptsCommand } from "./appCommands/adoptManagedScripts.js";
12
- import { runAppLinkLocalPackagesCommand } from "./appCommands/linkLocalPackages.js";
13
12
  import { runAppMigrateSourceMutationsCommand } from "./appCommands/migrateSourceMutations.js";
14
13
  import { runAppReleaseCommand } from "./appCommands/release.js";
15
14
  import { runAppSyncCiCommand } from "./appCommands/syncCi.js";
@@ -147,9 +146,6 @@ function createAppCommands(ctx = {}) {
147
146
  if (definition.name === "sync-ci") {
148
147
  return runAppSyncCiCommand(ctx, { appRoot, options, stdout, stderr });
149
148
  }
150
- if (definition.name === "link-local-packages") {
151
- return runAppLinkLocalPackagesCommand(ctx, { appRoot, options, stdout, stderr });
152
- }
153
149
  if (definition.name === "release") {
154
150
  return runAppReleaseCommand(ctx, { appRoot, options, stdout, stderr });
155
151
  }
@@ -1,7 +1,6 @@
1
1
  const APP_SCRIPT_WRAPPERS = Object.freeze({
2
2
  verify: "jskit app verify && npm run --if-present verify:app",
3
3
  "jskit:update": "jskit app update-packages",
4
- devlinks: "jskit app link-local-packages",
5
4
  release: "jskit app release"
6
5
  });
7
6
 
@@ -12,9 +11,6 @@ const COPIED_APP_SCRIPT_VALUES = Object.freeze({
12
11
  "jskit:update": Object.freeze([
13
12
  "bash ./scripts/update-jskit-packages.sh"
14
13
  ]),
15
- devlinks: Object.freeze([
16
- "bash ./scripts/link-local-jskit-packages.sh"
17
- ]),
18
14
  release: Object.freeze([
19
15
  "bash ./scripts/release.sh"
20
16
  ])
@@ -22,7 +18,6 @@ const COPIED_APP_SCRIPT_VALUES = Object.freeze({
22
18
 
23
19
  const COPIED_APP_SCRIPT_FILES = Object.freeze([
24
20
  "scripts/update-jskit-packages.sh",
25
- "scripts/link-local-jskit-packages.sh",
26
21
  "scripts/release.sh"
27
22
  ]);
28
23
 
@@ -110,22 +105,6 @@ const APP_COMMAND_DEFINITIONS = Object.freeze({
110
105
  "Never claims an unrecorded jskit-verify.yml or removes a customized legacy verify.yml."
111
106
  ])
112
107
  }),
113
- "link-local-packages": Object.freeze({
114
- name: "link-local-packages",
115
- summary: "Link local @jskit-ai workspace packages into the current app for live development.",
116
- usage: "jskit app link-local-packages [--repo-root <path>]",
117
- options: Object.freeze([
118
- Object.freeze({
119
- label: "--repo-root <path>",
120
- description: "Explicit jskit-ai monorepo checkout to link from. If omitted, JSKIT_REPO_ROOT or nearby jskit-ai directories are used."
121
- })
122
- ]),
123
- defaults: Object.freeze([
124
- "Links packages from both the monorepo packages/ and tooling/ directories.",
125
- "Refreshes node_modules/.bin entries for linked packages that publish binaries.",
126
- "Clears node_modules/.vite so Vite does not keep stale prebundled paths."
127
- ])
128
- }),
129
108
  release: Object.freeze({
130
109
  name: "release",
131
110
  summary: "Run the JSKIT release helper for an app repository.",
@@ -232,9 +211,6 @@ function buildAppCommandOptionMeta(subcommandName = "") {
232
211
  optionMeta.feature = { inputType: "text" };
233
212
  optionMeta["auth-mode"] = { inputType: "text" };
234
213
  }
235
- if (definition.name === "link-local-packages") {
236
- optionMeta["repo-root"] = { inputType: "text" };
237
- }
238
214
  return optionMeta;
239
215
  }
240
216
 
@@ -1,6 +1,6 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
2
  import path from "node:path";
3
- import { access, mkdir, readFile, readdir, rm, symlink } from "node:fs/promises";
3
+ import { access } from "node:fs/promises";
4
4
 
5
5
  function normalizeText(value = "") {
6
6
  return String(value || "").trim();
@@ -247,6 +247,7 @@ async function runLocalJskitAsync(
247
247
  appRoot,
248
248
  args = [],
249
249
  {
250
+ env = {},
250
251
  stdout,
251
252
  stderr,
252
253
  createCliError,
@@ -262,6 +263,7 @@ async function runLocalJskitAsync(
262
263
 
263
264
  return runExternalCommandAsync(localJskitBin, args, {
264
265
  cwd: appRoot,
266
+ env,
265
267
  stdout,
266
268
  stderr,
267
269
  quiet,
@@ -269,141 +271,6 @@ async function runLocalJskitAsync(
269
271
  });
270
272
  }
271
273
 
272
- async function resolveLocalRepoRoot({ appRoot = "", explicitRepoRoot = "" } = {}) {
273
- const explicit = normalizeText(explicitRepoRoot);
274
- if (explicit) {
275
- return explicit;
276
- }
277
-
278
- const envRepoRoot = normalizeText(process.env.JSKIT_REPO_ROOT);
279
- if (envRepoRoot) {
280
- return envRepoRoot;
281
- }
282
-
283
- let currentDirectory = path.dirname(appRoot);
284
- while (true) {
285
- const candidateRoot = path.join(currentDirectory, "jskit-ai");
286
- if (await fileExists(path.join(candidateRoot, "packages")) && await fileExists(path.join(candidateRoot, "tooling"))) {
287
- return candidateRoot;
288
- }
289
- const parentDirectory = path.dirname(currentDirectory);
290
- if (parentDirectory === currentDirectory) {
291
- return "";
292
- }
293
- currentDirectory = parentDirectory;
294
- }
295
- }
296
-
297
- async function discoverLocalPackageMap(repoRoot = "") {
298
- const packageMap = new Map();
299
- const parentDirectories = [
300
- path.join(repoRoot, "packages"),
301
- path.join(repoRoot, "tooling")
302
- ];
303
-
304
- for (const parentDirectory of parentDirectories) {
305
- if (!(await fileExists(parentDirectory))) {
306
- continue;
307
- }
308
-
309
- const entries = await readdir(parentDirectory, { withFileTypes: true });
310
- entries.sort((left, right) => left.name.localeCompare(right.name));
311
-
312
- for (const entry of entries) {
313
- if (!entry.isDirectory() || entry.name.startsWith(".")) {
314
- continue;
315
- }
316
-
317
- const packageRoot = path.join(parentDirectory, entry.name);
318
- const packageJsonPath = path.join(packageRoot, "package.json");
319
- if (!(await fileExists(packageJsonPath))) {
320
- continue;
321
- }
322
-
323
- let packageJson = {};
324
- try {
325
- packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
326
- } catch {
327
- continue;
328
- }
329
-
330
- const packageId = normalizeText(packageJson?.name);
331
- if (!packageId.startsWith("@jskit-ai/")) {
332
- continue;
333
- }
334
-
335
- const packageDirName = packageId.slice("@jskit-ai/".length);
336
- if (!packageDirName || packageDirName.includes("/")) {
337
- continue;
338
- }
339
-
340
- if (!packageMap.has(packageDirName)) {
341
- packageMap.set(packageDirName, packageRoot);
342
- }
343
- }
344
- }
345
-
346
- return packageMap;
347
- }
348
-
349
- async function linkPackageBinEntries({
350
- appRoot,
351
- packageDirName,
352
- sourceDir,
353
- stdout
354
- } = {}) {
355
- const packageJsonPath = path.join(sourceDir, "package.json");
356
- if (!(await fileExists(packageJsonPath))) {
357
- return;
358
- }
359
-
360
- let packageJson = {};
361
- try {
362
- packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
363
- } catch {
364
- return;
365
- }
366
-
367
- const rawBin = packageJson?.bin;
368
- let binEntries = [];
369
- if (typeof rawBin === "string") {
370
- binEntries = [[packageDirName, rawBin]];
371
- } else if (rawBin && typeof rawBin === "object" && !Array.isArray(rawBin)) {
372
- binEntries = Object.entries(rawBin);
373
- }
374
-
375
- if (binEntries.length < 1) {
376
- return;
377
- }
378
-
379
- const binDirectory = path.join(appRoot, "node_modules", ".bin");
380
- const packageRoot = path.join(appRoot, "node_modules", "@jskit-ai", packageDirName);
381
- await mkdir(binDirectory, { recursive: true });
382
-
383
- for (const [rawBinName, rawBinTarget] of binEntries) {
384
- const binName = normalizeText(rawBinName);
385
- const binTarget = normalizeText(rawBinTarget);
386
- if (!binName || !binTarget) {
387
- continue;
388
- }
389
-
390
- const absoluteTarget = path.join(packageRoot, binTarget);
391
- if (!(await fileExists(absoluteTarget))) {
392
- continue;
393
- }
394
-
395
- const binPath = path.join(binDirectory, binName);
396
- await rm(binPath, { recursive: true, force: true });
397
- const relativeTarget = path.relative(binDirectory, absoluteTarget) || absoluteTarget;
398
- await symlink(relativeTarget, binPath);
399
- stdout?.write(`[link-local] linked bin ${binName} -> ${relativeTarget}\n`);
400
- }
401
- }
402
-
403
- function resolveSymlinkType() {
404
- return process.platform === "win32" ? "junction" : "dir";
405
- }
406
-
407
274
  export {
408
275
  fileExists,
409
276
  normalizeText,
@@ -414,9 +281,5 @@ export {
414
281
  formatUtcReleaseTimestamp,
415
282
  resolveLocalJskitBin,
416
283
  runLocalJskit,
417
- runLocalJskitAsync,
418
- resolveLocalRepoRoot,
419
- discoverLocalPackageMap,
420
- linkPackageBinEntries,
421
- resolveSymlinkType
284
+ runLocalJskitAsync
422
285
  };
@@ -33,6 +33,8 @@ const DEPENDENCY_SECTIONS = Object.freeze([
33
33
  })
34
34
  ]);
35
35
  const JSKIT_PACKAGE_PATTERN = /^@jskit-ai\/[a-z0-9._-]+$/iu;
36
+ const JSKIT_CLI_PACKAGE = "@jskit-ai/jskit-cli";
37
+ const UPDATE_PACKAGES_BOOTSTRAPPED_ENV = "JSKIT_UPDATE_PACKAGES_BOOTSTRAPPED";
36
38
  const PROGRESS_INTERVAL_MS = 5_000;
37
39
 
38
40
  function collectJskitPackageNames(packageMap = {}) {
@@ -513,6 +515,68 @@ async function runAppUpdatePackagesCommand(ctx = {}, { appRoot = "", options = {
513
515
  stdout?.write("[jskit:update] dry-run mode enabled.\n");
514
516
  }
515
517
 
518
+ const cliSection = DEPENDENCY_SECTIONS.find((section) =>
519
+ Object.prototype.hasOwnProperty.call(packageJson?.[section.name] || {}, JSKIT_CLI_PACKAGE)
520
+ );
521
+ if (
522
+ !dryRun &&
523
+ cliSection &&
524
+ process.env[UPDATE_PACKAGES_BOOTSTRAPPED_ENV] !== "1"
525
+ ) {
526
+ await resolveLatestVersions([JSKIT_CLI_PACKAGE], latestVersions, {
527
+ appRoot,
528
+ createCliError,
529
+ registryArgs,
530
+ stderr,
531
+ stdout
532
+ });
533
+ const latestCliVersion = latestVersions.get(JSKIT_CLI_PACKAGE);
534
+ let installedCliVersion = "";
535
+ try {
536
+ installedCliVersion = String(
537
+ (await readJson(path.join(appRoot, "node_modules", "@jskit-ai", "jskit-cli", "package.json")))?.version || ""
538
+ ).trim();
539
+ } catch (error) {
540
+ if (error?.code !== "ENOENT") {
541
+ throw error;
542
+ }
543
+ }
544
+
545
+ if (installedCliVersion !== latestCliVersion) {
546
+ stdout?.write(`[jskit:update] bootstrapping ${JSKIT_CLI_PACKAGE}@${latestCliVersion}.\n`);
547
+ await runExternalCommandAsync(
548
+ "npm",
549
+ [
550
+ "install",
551
+ ...cliSection.installArgs,
552
+ ...registryArgs,
553
+ `${JSKIT_CLI_PACKAGE}@${latestCliVersion}`
554
+ ],
555
+ {
556
+ cwd: appRoot,
557
+ stdout,
558
+ stderr,
559
+ createCliError
560
+ }
561
+ );
562
+ }
563
+
564
+ stdout?.write("[jskit:update] handing the update to the current local CLI.\n");
565
+ await runLocalJskitAsync(
566
+ appRoot,
567
+ ["app", "update-packages", ...registryArgs],
568
+ {
569
+ env: {
570
+ [UPDATE_PACKAGES_BOOTSTRAPPED_ENV]: "1"
571
+ },
572
+ stdout,
573
+ stderr,
574
+ createCliError
575
+ }
576
+ );
577
+ return 0;
578
+ }
579
+
516
580
  await runWithProgress(
517
581
  async () => {
518
582
  const updatedRootPackages = await updateRootPackages({
@@ -2,12 +2,8 @@ const MOBILE_COMMAND_DEFINITIONS = Object.freeze({
2
2
  sync: Object.freeze({
3
3
  name: "sync",
4
4
  summary: "Build the JSKIT web client and sync the Android Capacitor shell.",
5
- usage: "jskit mobile android sync [--dry-run] [--devlinks]",
5
+ usage: "jskit mobile android sync [--dry-run]",
6
6
  options: Object.freeze([
7
- Object.freeze({
8
- label: "--devlinks",
9
- description: "If sync needs to run npm install first, also run npm run --if-present devlinks afterward."
10
- }),
11
7
  Object.freeze({
12
8
  label: "--dry-run",
13
9
  description: "Preview the build and Capacitor sync commands without writing dist output or mutating the Android shell."
@@ -134,18 +134,11 @@ async function runLocalProjectBinary(binaryName, args = [], {
134
134
 
135
135
  async function installAppDependenciesForHook({
136
136
  appRoot,
137
- appPackageJson,
138
137
  io,
139
138
  pathModule = path,
140
139
  createCliError,
141
- dryRun = false,
142
- runDevlinks = false
140
+ dryRun = false
143
141
  } = {}) {
144
- const packageScripts =
145
- appPackageJson?.scripts && typeof appPackageJson.scripts === "object"
146
- ? appPackageJson.scripts
147
- : {};
148
-
149
142
  await runLocalProjectBinary("npm", ["install"], {
150
143
  appRoot,
151
144
  io,
@@ -154,17 +147,6 @@ async function installAppDependenciesForHook({
154
147
  explanation: "[mobile] Installing app dependencies for the mobile shell:",
155
148
  dryRun
156
149
  });
157
-
158
- if (runDevlinks === true && Object.prototype.hasOwnProperty.call(packageScripts, "devlinks")) {
159
- await runLocalProjectBinary("npm", ["run", "--if-present", "devlinks"], {
160
- appRoot,
161
- io,
162
- pathModule,
163
- createCliError,
164
- explanation: "[mobile] Refreshing local JSKIT package links:",
165
- dryRun
166
- });
167
- }
168
150
  }
169
151
 
170
152
  async function resolvePackageOptionInputForInstall({
@@ -248,8 +230,7 @@ function createInstallHookHelpers({
248
230
  ctx,
249
231
  appRoot,
250
232
  io,
251
- appPackageJson,
252
- commandOptions = {}
233
+ appPackageJson
253
234
  } = {}) {
254
235
  return Object.freeze({
255
236
  ensureManagedMobileConfig: async ({ dryRun = false } = {}) =>
@@ -263,12 +244,10 @@ function createInstallHookHelpers({
263
244
  installAppDependencies: async ({ dryRun = false } = {}) =>
264
245
  await installAppDependenciesForHook({
265
246
  appRoot,
266
- appPackageJson,
267
247
  io,
268
248
  pathModule: ctx.path,
269
249
  createCliError: ctx.createCliError,
270
- dryRun,
271
- runDevlinks: commandOptions.devlinks === true
250
+ dryRun
272
251
  }),
273
252
  runProjectBinary: async (binaryName, args = [], { dryRun = false, explanation = "" } = {}) =>
274
253
  await runLocalProjectBinary(binaryName, args, {
@@ -649,8 +628,7 @@ async function runPackageAddCommand(ctx = {}, { positional, options, cwd, io })
649
628
  ctx,
650
629
  appRoot,
651
630
  io,
652
- appPackageJson: packageJson,
653
- commandOptions: options
631
+ appPackageJson: packageJson
654
632
  });
655
633
 
656
634
  for (const packageId of packagesToInstall) {
@@ -14,7 +14,6 @@ function parseArgs(argv, { createCliError } = {}) {
14
14
  options: {
15
15
  dryRun: false,
16
16
  runNpmInstall: false,
17
- devlinks: false,
18
17
  full: false,
19
18
  expanded: false,
20
19
  details: false,
@@ -41,7 +40,6 @@ function parseArgs(argv, { createCliError } = {}) {
41
40
  const options = {
42
41
  dryRun: false,
43
42
  runNpmInstall: false,
44
- devlinks: false,
45
43
  full: false,
46
44
  expanded: false,
47
45
  details: false,
@@ -73,10 +71,6 @@ function parseArgs(argv, { createCliError } = {}) {
73
71
  options.runNpmInstall = true;
74
72
  continue;
75
73
  }
76
- if (token === "--devlinks") {
77
- options.devlinks = true;
78
- continue;
79
- }
80
74
  if (token === "--full") {
81
75
  options.full = true;
82
76
  continue;
@@ -1,7 +1,6 @@
1
1
  const OPTION_FLAG_LABELS = Object.freeze({
2
2
  dryRun: "--dry-run",
3
3
  runNpmInstall: "--run-npm-install",
4
- devlinks: "--devlinks",
5
4
  full: "--full",
6
5
  expanded: "--expanded",
7
6
  details: "--details",
@@ -136,7 +135,7 @@ const COMMAND_DESCRIPTORS = Object.freeze({
136
135
  parameters: Object.freeze([
137
136
  Object.freeze({
138
137
  name: "<subcommand>",
139
- description: "verify | update-packages | link-local-packages | release | adopt-managed-scripts | migrate-source-mutations."
138
+ description: "verify | update-packages | release | adopt-managed-scripts | migrate-source-mutations."
140
139
  })
141
140
  ]),
142
141
  defaults: Object.freeze([
@@ -189,13 +188,12 @@ const COMMAND_DESCRIPTORS = Object.freeze({
189
188
  defaults: Object.freeze([
190
189
  "Install the shell first with jskit add package @jskit-ai/mobile-capacitor.",
191
190
  "Use jskit mobile <platform> help for platform-specific usage.",
192
- "--dry-run is accepted by jskit mobile android sync/run/build.",
193
- "--devlinks runs npm run --if-present devlinks after jskit mobile android sync maintenance for development-only relinking."
191
+ "--dry-run is accepted by jskit mobile android sync/run/build."
194
192
  ]),
195
193
  fullUse: "jskit mobile <platform> <subcommand> [help] [--dry-run] [--<option> <value>...]",
196
194
  showHelpOnBareInvocation: true,
197
195
  handlerName: "commandMobile",
198
- allowedFlagKeys: Object.freeze(["dryRun", "devlinks"]),
196
+ allowedFlagKeys: Object.freeze(["dryRun"]),
199
197
  inlineOptionMode: "delegate",
200
198
  allowedValueOptionNames: Object.freeze([]),
201
199
  canDelegateInlineOptions: (positional = []) => Array.isArray(positional) && positional.length > 0
@@ -298,14 +296,13 @@ const COMMAND_DESCRIPTORS = Object.freeze({
298
296
  "No npm install runs unless --run-npm-install is passed.",
299
297
  "Short ids resolve to @jskit-ai/<id> when available.",
300
298
  "Running without args lists bundles and runtime packages.",
301
- "Existing matching version is skipped unless options force reapply.",
302
- "--devlinks runs npm run --if-present devlinks after install when the app defines that script."
299
+ "Existing matching version is skipped unless options force reapply."
303
300
  ]),
304
301
  fullUse:
305
- "jskit add <package|bundle> <id> [--<option> <value>...] [--dry-run] [--run-npm-install] [--devlinks] [--json] [--verbose]",
302
+ "jskit add <package|bundle> <id> [--<option> <value>...] [--dry-run] [--run-npm-install] [--json] [--verbose]",
306
303
  showHelpOnBareInvocation: false,
307
304
  handlerName: "commandAdd",
308
- allowedFlagKeys: Object.freeze(["dryRun", "runNpmInstall", "devlinks", "json", "verbose"]),
305
+ allowedFlagKeys: Object.freeze(["dryRun", "runNpmInstall", "json", "verbose"]),
309
306
  inlineOptionMode: "delegate",
310
307
  allowedValueOptionNames: Object.freeze([]),
311
308
  canDelegateInlineOptions: canDelegateAddInlineOptions
@@ -0,0 +1,95 @@
1
+ const DEFAULT_LOCAL_BASE_URL = "http://127.0.0.1:4173";
2
+ const DEFAULT_SMOKE_PATH = "/home";
3
+ const DEFAULT_VIEWPORTS = Object.freeze([
4
+ Object.freeze({ name: "compact", width: 390, height: 844 }),
5
+ Object.freeze({ name: "medium", width: 768, height: 1024 }),
6
+ Object.freeze({ name: "expanded", width: 1280, height: 900 })
7
+ ]);
8
+
9
+ function normalizeBaseUrl(value) {
10
+ return String(value || "").trim().replace(/\/+$/u, "");
11
+ }
12
+
13
+ function createJskitPlaywrightConfig({ env = process.env } = {}) {
14
+ const managedBaseUrl = normalizeBaseUrl(env.PLAYWRIGHT_BASE_URL);
15
+ const baseURL = managedBaseUrl || DEFAULT_LOCAL_BASE_URL;
16
+ const storageState = String(env.JSKIT_PLAYWRIGHT_STORAGE_STATE || "").trim();
17
+
18
+ return {
19
+ testDir: "./tests/e2e",
20
+ timeout: 60_000,
21
+ expect: {
22
+ timeout: 10_000
23
+ },
24
+ use: {
25
+ baseURL,
26
+ headless: true,
27
+ ...(storageState ? { storageState } : {})
28
+ },
29
+ ...(managedBaseUrl ? {} : {
30
+ webServer: {
31
+ command: "npm run build && node ./bin/server.js",
32
+ env: {
33
+ PORT: "4173"
34
+ },
35
+ url: `${baseURL}/api/health`,
36
+ reuseExistingServer: true,
37
+ timeout: 180_000
38
+ }
39
+ })
40
+ };
41
+ }
42
+
43
+ async function expectNoHorizontalOverflow(page, expect) {
44
+ const metrics = await page.evaluate(() => ({
45
+ clientWidth: document.documentElement.clientWidth,
46
+ scrollWidth: document.documentElement.scrollWidth
47
+ }));
48
+
49
+ expect(metrics.scrollWidth).toBeLessThanOrEqual(metrics.clientWidth + 1);
50
+ }
51
+
52
+ async function expectVisibleTapTargets(page, expect) {
53
+ const heights = await page.locator("a[href], button, [role='button'], .v-btn, .v-list-item").evaluateAll(
54
+ (elements) => elements
55
+ .filter((element) => {
56
+ const rect = element.getBoundingClientRect();
57
+ const style = window.getComputedStyle(element);
58
+ return style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0;
59
+ })
60
+ .map((element) => element.getBoundingClientRect().height)
61
+ );
62
+
63
+ for (const height of heights) {
64
+ expect(height).toBeGreaterThanOrEqual(48);
65
+ }
66
+ }
67
+
68
+ function runGeneratedAppSmoke({ test, expect, expectedText } = {}) {
69
+ if (!test || !expect || !String(expectedText || "").trim()) {
70
+ throw new Error("runGeneratedAppSmoke requires Playwright test, expect, and expectedText.");
71
+ }
72
+
73
+ const smokePath = String(process.env.JSKIT_PLAYWRIGHT_SMOKE_PATH || DEFAULT_SMOKE_PATH);
74
+
75
+ test.describe("generated app responsive smoke", () => {
76
+ for (const viewport of DEFAULT_VIEWPORTS) {
77
+ test(`${viewport.name} home route renders without horizontal overflow`, async ({ page }) => {
78
+ await page.setViewportSize({ width: viewport.width, height: viewport.height });
79
+ await page.goto(smokePath);
80
+ await expect(page.locator("body")).toBeVisible();
81
+ await expect(page.getByText(expectedText)).toBeVisible();
82
+ await expect(page.locator(".generated-ui-screen").first()).toBeVisible();
83
+ await expectVisibleTapTargets(page, expect);
84
+ await expectNoHorizontalOverflow(page, expect);
85
+ });
86
+ }
87
+ });
88
+ }
89
+
90
+ export {
91
+ createJskitPlaywrightConfig,
92
+ DEFAULT_LOCAL_BASE_URL,
93
+ DEFAULT_VIEWPORTS,
94
+ runGeneratedAppSmoke
95
+ };
@@ -1,197 +0,0 @@
1
- import path from "node:path";
2
- import { lstat, mkdir, readFile, readlink, rm, symlink } from "node:fs/promises";
3
- import {
4
- discoverLocalPackageMap,
5
- fileExists,
6
- linkPackageBinEntries,
7
- resolveLocalRepoRoot,
8
- resolveSymlinkType
9
- } from "./shared.js";
10
-
11
- const COMPANION_PACKAGES = Object.freeze([
12
- Object.freeze({
13
- packageName: "json-rest-schema",
14
- repoDirName: "json-rest-schema"
15
- }),
16
- Object.freeze({
17
- packageName: "json-rest-stores",
18
- repoDirName: "json-rest-stores"
19
- })
20
- ]);
21
-
22
- function collectDeclaredPackageNames(packageJson = {}) {
23
- const names = new Set();
24
- const sections = [
25
- packageJson?.dependencies,
26
- packageJson?.devDependencies,
27
- packageJson?.optionalDependencies,
28
- packageJson?.peerDependencies
29
- ];
30
-
31
- for (const section of sections) {
32
- if (!section || typeof section !== "object" || Array.isArray(section)) {
33
- continue;
34
- }
35
-
36
- for (const packageName of Object.keys(section)) {
37
- const normalizedPackageName = String(packageName || "").trim();
38
- if (normalizedPackageName) {
39
- names.add(normalizedPackageName);
40
- }
41
- }
42
- }
43
-
44
- return names;
45
- }
46
-
47
- async function verifySymlinkTarget(targetPath = "", sourceDir = "", {
48
- packageName = ""
49
- } = {}) {
50
- const stats = await lstat(targetPath);
51
- if (!stats.isSymbolicLink()) {
52
- throw new Error(`[link-local] expected ${packageName || targetPath} to be a symlink after linking.`);
53
- }
54
-
55
- const linkedTarget = await readlink(targetPath);
56
- if (linkedTarget !== sourceDir) {
57
- throw new Error(
58
- `[link-local] expected ${packageName || targetPath} to link to ${sourceDir}, got ${linkedTarget}.`
59
- );
60
- }
61
- }
62
-
63
- async function replaceWithSymlink(targetPath = "", sourceDir = "", {
64
- packageName = ""
65
- } = {}) {
66
- await mkdir(path.dirname(targetPath), { recursive: true });
67
- await rm(targetPath, { recursive: true, force: true });
68
- await symlink(sourceDir, targetPath, resolveSymlinkType());
69
- await verifySymlinkTarget(targetPath, sourceDir, {
70
- packageName
71
- });
72
- }
73
-
74
- async function maybeLinkCompanionPackages({
75
- appRoot = "",
76
- repoRoot = "",
77
- stdout,
78
- createCliError
79
- }) {
80
- const companionRoot = path.dirname(repoRoot);
81
- const appPackageJsonPath = path.join(appRoot, "package.json");
82
- let appPackageJson = {};
83
- try {
84
- appPackageJson = JSON.parse(await readFile(appPackageJsonPath, "utf8"));
85
- } catch {
86
- appPackageJson = {};
87
- }
88
- const declaredPackageNames = collectDeclaredPackageNames(appPackageJson);
89
- let linkedCount = 0;
90
-
91
- for (const companion of COMPANION_PACKAGES) {
92
- if (!declaredPackageNames.has(companion.packageName)) {
93
- continue;
94
- }
95
-
96
- const sourceDir = path.join(companionRoot, companion.repoDirName);
97
- const packageJsonPath = path.join(sourceDir, "package.json");
98
- if (!(await fileExists(packageJsonPath))) {
99
- throw createCliError(
100
- `[link-local] companion package ${companion.packageName} is declared by the app but local source was not found at ${sourceDir}.`,
101
- { exitCode: 1 }
102
- );
103
- }
104
-
105
- let packageJson = {};
106
- try {
107
- packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
108
- } catch {
109
- continue;
110
- }
111
-
112
- if (String(packageJson?.name || "").trim() !== companion.packageName) {
113
- throw createCliError(
114
- `[link-local] companion source at ${sourceDir} does not match expected package ${companion.packageName}.`,
115
- { exitCode: 1 }
116
- );
117
- }
118
-
119
- const appTargetPath = path.join(appRoot, "node_modules", companion.packageName);
120
- await replaceWithSymlink(appTargetPath, sourceDir, {
121
- packageName: companion.packageName
122
- });
123
- stdout.write(`[link-local] linked ${companion.packageName} -> ${sourceDir}\n`);
124
- linkedCount += 1;
125
-
126
- const repoTargetPath = path.join(repoRoot, "node_modules", companion.packageName);
127
- await replaceWithSymlink(repoTargetPath, sourceDir, {
128
- packageName: `${companion.packageName} (repo root)`
129
- });
130
- stdout.write(`[link-local] linked repo companion ${companion.packageName} -> ${sourceDir}\n`);
131
- linkedCount += 1;
132
- }
133
-
134
- return linkedCount;
135
- }
136
-
137
- async function runAppLinkLocalPackagesCommand(ctx = {}, { appRoot = "", options = {}, stdout }) {
138
- const { createCliError } = ctx;
139
- const explicitRepoRoot = String(options?.inlineOptions?.["repo-root"] || "").trim();
140
- const scopeDirectory = path.join(appRoot, "node_modules", "@jskit-ai");
141
- const viteCacheDirectory = path.join(appRoot, "node_modules", ".vite");
142
-
143
- if (!(await fileExists(scopeDirectory))) {
144
- throw createCliError(`[link-local] @jskit-ai scope not found at ${scopeDirectory} (run npm install first).`, {
145
- exitCode: 1
146
- });
147
- }
148
-
149
- const repoRoot = await resolveLocalRepoRoot({ appRoot, explicitRepoRoot });
150
- if (!repoRoot) {
151
- throw createCliError("[link-local] no JSKIT repository found. Set JSKIT_REPO_ROOT or use --repo-root.", {
152
- exitCode: 1
153
- });
154
- }
155
-
156
- if (!(await fileExists(path.join(repoRoot, "packages"))) || !(await fileExists(path.join(repoRoot, "tooling")))) {
157
- throw createCliError(`[link-local] JSKIT repo root is not valid: ${repoRoot}`, {
158
- exitCode: 1
159
- });
160
- }
161
-
162
- const packageMap = await discoverLocalPackageMap(repoRoot);
163
- let linkedCount = 0;
164
- await mkdir(scopeDirectory, { recursive: true });
165
-
166
- for (const [packageDirName, sourceDir] of [...packageMap.entries()].sort((left, right) => left[0].localeCompare(right[0]))) {
167
- const targetPath = path.join(scopeDirectory, packageDirName);
168
- await replaceWithSymlink(targetPath, sourceDir, {
169
- packageName: `@jskit-ai/${packageDirName}`
170
- });
171
- stdout.write(`[link-local] linked @jskit-ai/${packageDirName} -> ${sourceDir}\n`);
172
- await linkPackageBinEntries({
173
- appRoot,
174
- packageDirName,
175
- sourceDir,
176
- stdout
177
- });
178
- linkedCount += 1;
179
- }
180
-
181
- linkedCount += await maybeLinkCompanionPackages({
182
- appRoot,
183
- repoRoot,
184
- stdout,
185
- createCliError
186
- });
187
-
188
- if (await fileExists(viteCacheDirectory)) {
189
- await rm(viteCacheDirectory, { recursive: true, force: true });
190
- stdout.write(`[link-local] cleared Vite cache at ${viteCacheDirectory}\n`);
191
- }
192
-
193
- stdout.write(`[link-local] done. linked=${linkedCount}\n`);
194
- return 0;
195
- }
196
-
197
- export { runAppLinkLocalPackagesCommand };