@krapwoo/ds-viewer 0.6.10 → 0.6.11

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
@@ -72,6 +72,23 @@ loser then serves a workspace that no longer describes it, which Expo reports as
72
72
  with `--workspace-dir .ds-viewer-second`. A lock left behind by a crashed `dev` never blocks the
73
73
  next one.
74
74
 
75
+ `.ds-viewer/` belongs to the running server, and it repairs it. If something outside the server
76
+ deletes the generated files while it is serving, it says so and writes them back, rather than
77
+ leaving Expo to report an unresolvable `main` field — which names your project's `package.json`, a
78
+ file that is fine and that you never touched:
79
+
80
+ ```
81
+ Something outside this server deleted package.json, app.json, tsconfig.json, metro.config.js,
82
+ entry.tsx from .ds-viewer/. Rewriting them — your project is fine, this is the catalog's own
83
+ generated workspace. If the catalog looks broken, reload the page.
84
+ ```
85
+
86
+ Metro picks the files back up without a restart. The repair is capped at five per run: if
87
+ something is deleting continuously, repairing forever would only hide it, so past the cap the
88
+ server stops and keeps reporting. If you ever see that message, what else was running at the time
89
+ would be useful on [issue #41](https://github.com/krapwoo/design-system-viewer/issues/41) — the
90
+ cause is not yet identified.
91
+
75
92
  `ds-viewer.config.ts` fields: `name`, `logo` (optional — a path to an image asset, shown in the
76
93
  sidebar beside the name, or in place of it for a wide wordmark; validated at `sync`/`dev` time
77
94
  and bundled into the preview workspace), `components`, `exclude`, `tokens`, `pages`, `groupOrder`, `starterKit` (new-project only —
package/dist/cli/dev.js CHANGED
@@ -6,7 +6,7 @@ import { execFile } from 'node:child_process';
6
6
  import { promisify } from 'node:util';
7
7
  import { resolveConfig } from "./config.js";
8
8
  import { sync } from "./sync.js";
9
- import { clearDevLock, liveDevLock, workspacePath, writeDevLock, writeWorkspace } from "./workspace.js";
9
+ import { clearDevLock, liveDevLock, missingWorkspaceFiles, workspacePath, writeDevLock, writeWorkspace } from "./workspace.js";
10
10
  import { findFreePort } from "./port.js";
11
11
  import { globBaseFolder } from "./glob.js";
12
12
  import { buildVersionStatus, createVersionController, defaultPreferencesDir, readPersonalAutoCheck, resolveAutoCheck, runUpdateCheck, statusFromCheck, writeUpdateFile, writeVersionFile, } from "./updateCheck.js";
@@ -326,6 +326,42 @@ export async function performRestart(handoff, deps = {}) {
326
326
  * component/token/page changes to re-run `sync`. Metro's own HMR updates the rendered component
327
327
  * when its source changes, but not the generated props table or page list (Task 17 Step 4 relies
328
328
  * on this: editing `Button.tsx` must update its props table, not just hot-reload the component). */
329
+ /** How often a running `dev` checks that its own workspace still exists. Cheap (five `existsSync`
330
+ * calls), and slow enough to be invisible: the failure it covers is rare, and noticing within a
331
+ * few seconds is soon enough to repair before the next page load. */
332
+ const WORKSPACE_WATCH_INTERVAL_MS = 4000;
333
+ /** How many times one `dev` run will rewrite a workspace that keeps losing files. Past this,
334
+ * something is deleting continuously and repairing again would only hide it. */
335
+ const WORKSPACE_REPAIR_LIMIT = 5;
336
+ /** One pass of the workspace integrity check. Returns what it did, so the caller owns the repair
337
+ * budget and the tests can drive it without a timer.
338
+ *
339
+ * `updateEndpoint` is the caller's own, passed through unchanged: rebuilding it here from the
340
+ * Metro port would point the repaired catalog at the wrong server, silently breaking the update
341
+ * panel while telling the developer everything is fine. */
342
+ export function repairWorkspaceIfMissing(deps) {
343
+ const { config, updateEndpoint, versionStatus, sync, reclaimLock, log = console.warn } = deps;
344
+ const missing = missingWorkspaceFiles(config);
345
+ if (missing.length === 0)
346
+ return { missing, repaired: false };
347
+ log(`Something outside this server deleted ${missing.join(', ')} from ${config.workspaceDir ?? DEFAULT_WORKSPACE_DIR}/. ` +
348
+ "Rewriting them — your project is fine, this is the catalog's own generated workspace. " +
349
+ 'If the catalog looks broken, reload the page.');
350
+ writeWorkspace(config, updateEndpoint);
351
+ // `entry.tsx` imports both of these, so a rewrite that skips them resolves nothing. Both come
352
+ // from one status, the way `refreshUpdateFile` writes them, so a repair cannot leave
353
+ // `version.json` announcing an update that `update.json` denies.
354
+ const status = versionStatus();
355
+ if (!existsSync(workspacePath(config, 'update.json'))) {
356
+ const update = status.update ? { ...status.update, checkedAt: status.lastCheckedAt ?? new Date().toISOString() } : undefined;
357
+ writeUpdateFile(config.projectRoot, update, config.workspaceDir);
358
+ }
359
+ if (!existsSync(workspacePath(config, 'version.json')))
360
+ writeVersionFile(config.projectRoot, status, config.workspaceDir);
361
+ reclaimLock();
362
+ sync(config);
363
+ return { missing, repaired: true };
364
+ }
329
365
  export async function dev(initialConfig, devOptions = {}) {
330
366
  assertLocalInstall(initialConfig.projectRoot);
331
367
  const ownVersion = readOwnVersion(import.meta.dirname);
@@ -505,8 +541,60 @@ export async function dev(initialConfig, devOptions = {}) {
505
541
  shell: process.platform === 'win32',
506
542
  detached: true,
507
543
  });
544
+ // This process owns its workspace: nothing else should remove the files that define it. When
545
+ // they vanish anyway, Metro reports it as `Cannot resolve entry file: the 'main' field defined
546
+ // in your package.json points to an unresolvable or non-existent path` — which names the
547
+ // *project's* package.json, a file that is fine and that the developer did not touch. The cause
548
+ // is still unidentified (issue #41), so this does not pretend to prevent it; it notices, says
549
+ // plainly what happened, and writes the files back, because `writeWorkspace` is idempotent and
550
+ // already knows how.
551
+ //
552
+ // Capped rather than endless: if something is deleting continuously, rewriting forever would
553
+ // hide it behind a repair loop. After the cap this stops repairing and keeps saying so, which
554
+ // leaves the evidence visible.
555
+ let repairs = 0;
556
+ const workspaceWatch = setInterval(() => {
557
+ if (watchersPaused)
558
+ return; // an update is rewriting the workspace on purpose.
559
+ if (repairs >= WORKSPACE_REPAIR_LIMIT) {
560
+ const missing = missingWorkspaceFiles(config);
561
+ if (missing.length > 0) {
562
+ console.warn(`The catalog workspace is still losing files (missing ${missing.join(', ')}). Not rewriting them again — ` +
563
+ 'something outside this server is deleting them. Please report what else was running at https://github.com/krapwoo/design-system-viewer/issues/41');
564
+ }
565
+ return;
566
+ }
567
+ // Counted as an attempt, not a success: a workspace that cannot be rewritten (a permission
568
+ // error, a sync failure) would otherwise retry every four seconds forever.
569
+ if (missingWorkspaceFiles(config).length > 0)
570
+ repairs += 1;
571
+ try {
572
+ // `updateEndpoint` is passed through, never rebuilt: the endpoint listens on its own
573
+ // ephemeral port, so deriving one from Metro's port here would repair the files and break
574
+ // the update panel in the same breath.
575
+ repairWorkspaceIfMissing({
576
+ config, updateEndpoint, versionStatus: version.getVersionStatus, sync,
577
+ // Only if no one else took it meanwhile: between losing the lock and this tick, a second
578
+ // `dev` can legitimately have started and be serving the directory. Overwriting its lock
579
+ // would leave two servers each believing they own it.
580
+ reclaimLock: () => {
581
+ const other = liveDevLock(config);
582
+ if (other && other.pid !== process.pid) {
583
+ console.warn(`Not reclaiming ${config.workspaceDir ?? DEFAULT_WORKSPACE_DIR}/: pid ${other.pid} owns it now.`);
584
+ return;
585
+ }
586
+ writeDevLock(config, { pid: process.pid, port });
587
+ },
588
+ });
589
+ }
590
+ catch (error) {
591
+ console.warn(`Could not rewrite the workspace: ${error.message}`);
592
+ }
593
+ }, WORKSPACE_WATCH_INTERVAL_MS);
594
+ workspaceWatch.unref?.();
508
595
  child.on('error', (error) => console.warn(`Could not start Expo: ${error.message}`));
509
596
  child.on('exit', (code) => {
597
+ clearInterval(workspaceWatch);
510
598
  for (const watcher of watchers)
511
599
  watcher.close();
512
600
  endpointServer?.close();
@@ -151,6 +151,18 @@ export function liveDevLock(config, ownPid = process.pid) {
151
151
  return undefined;
152
152
  return lock;
153
153
  }
154
+ /** The files `writeWorkspace` always writes: the ones that define the workspace to Expo and Metro.
155
+ * `babel.config.js` is conditional and the update files are written separately, so neither counts
156
+ * as missing here. */
157
+ export const WORKSPACE_DEFINING_FILES = ['package.json', 'app.json', 'tsconfig.json', 'metro.config.js', 'entry.tsx'];
158
+ /** Which of those a workspace has lost. A running `dev` owns these files and nothing else should
159
+ * remove them, so a non-empty answer means something outside this process deleted them — see
160
+ * issue #41, where the cause is still unidentified. Reported rather than diagnosed: the point is
161
+ * that the server can say what happened instead of leaving Expo to blame the project's own
162
+ * `package.json` with `Cannot resolve entry file`. */
163
+ export function missingWorkspaceFiles(config) {
164
+ return WORKSPACE_DEFINING_FILES.filter((name) => !existsSync(workspacePath(config, name)));
165
+ }
154
166
  /** Writes the self-contained Expo web workspace at `.ds-viewer/` (design §2 "The preview
155
167
  * workspace and `npx ds-viewer dev`"). Rewritten freely on every `dev` run — nothing here is
156
168
  * user-owned.
@@ -158,9 +170,9 @@ export function liveDevLock(config, ownPid = process.pid) {
158
170
  * `updateEndpoint` is the caller's own, inlined into `entry.tsx`. Without one (a render check's
159
171
  * throwaway server, `--no-update-endpoint`) the catalog is served with no endpoint at all, so it
160
172
  * makes no calls: the shell polls `/version` from every page, so inlining a *different* server's
161
- * endpoint here would have every page call it from the wrong origin. Two `dev` servers share this
162
- * one directory, so the last writer decides — see the README on running a render check beside an
163
- * open catalog. */
173
+ * endpoint here would have every page call it from the wrong origin. Each `dev` owns its own
174
+ * workspace directory (`dev-lock.json`), so a render check beside an open catalog writes to
175
+ * `.ds-viewer-render/` rather than this one — see the README. */
164
176
  export function writeWorkspace(config, updateEndpoint) {
165
177
  const workspace = workspacePath(config);
166
178
  mkdirSync(workspace, { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krapwoo/ds-viewer",
3
- "version": "0.6.10",
3
+ "version": "0.6.11",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "A browsable, always-current catalog for an Expo / React Native design system.",