@krapwoo/ds-viewer 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -355,6 +355,17 @@ Set `starterKit.root` in `ds-viewer.config.ts` if you moved the kit after `init
355
355
  (default `src/ds`); a config from before 0.4 with no `root` field still works — it's inferred from
356
356
  your first `components` glob.
357
357
 
358
+ ### Updating: known limits
359
+
360
+ - **Updating needs the network.** If npm doesn't answer within 20 seconds, the update page shows
361
+ "Couldn’t prepare the update" and nothing changes. It doesn't fall back to npm's cache, because
362
+ the install that follows would need the network anyway.
363
+ - **A console error while installing is expected.** While npm replaces the package, Metro briefly
364
+ reports that it can't resolve `@krapwoo/ds-viewer`. The update page keeps going, and the viewer
365
+ restarts on the new version.
366
+ - **Windows hasn't been tested on a real machine yet.** Package-manager commands run through a
367
+ shell on Windows (needed for `npm.cmd`), and this is covered by unit tests only.
368
+
358
369
  ## What's included
359
370
 
360
371
  Tokens · Icons (45) · and generic components:
package/dist/cli/dev.js CHANGED
@@ -14,7 +14,7 @@ import { readOwnVersion } from "./packageVersion.js";
14
14
  import { createUpdateEndpoint } from "./endpoint.js";
15
15
  import { buildUpdatePlan } from "./updatePlan.js";
16
16
  import { lastSummaryLine } from "./doctor.js";
17
- import { detectPackageManager, installUpgradeCommand } from "./packageManager.js";
17
+ import { detectPackageManager, installUpgradeCommand, platformCommand } from "./packageManager.js";
18
18
  export class LocalInstallMissingError extends Error {
19
19
  }
20
20
  const IGNORED_WATCH_SEGMENTS = new Set(['.ds-viewer', 'node_modules', '.git']);
@@ -80,7 +80,10 @@ export async function refreshUpdateFile(config, ownVersion, options = {}) {
80
80
  // `/update/status` polls throughout (Critical finding, Fable correction pass: the synchronous
81
81
  // version blocked the entire event loop until the whole update finished).
82
82
  const execFileAsync = promisify(execFile);
83
- const defaultExecImpl = async (command, args, options) => (await execFileAsync(command, args, options)).stdout;
83
+ const defaultExecImpl = async (command, args, options) => {
84
+ const run = platformCommand(command, args);
85
+ return (await execFileAsync(run.command, run.args, { ...options, shell: run.shell })).stdout;
86
+ };
84
87
  function installedMainJs(projectRoot) {
85
88
  return path.join(projectRoot, 'node_modules', '@krapwoo', 'ds-viewer', 'dist', 'cli', 'main.js');
86
89
  }
@@ -27,3 +27,14 @@ export function installUpgradeCommand(pm, pkg, version) {
27
27
  return { command: 'pnpm', args: ['add', '--save-dev', spec] };
28
28
  return { command: 'yarn', args: ['add', '--dev', spec] };
29
29
  }
30
+ const WINDOWS_SHIMS = new Set(['npm', 'npx', 'pnpm', 'yarn']);
31
+ /** On Windows, `npm`, `npx`, `pnpm` and `yarn` are `.cmd` shims, which Node refuses to run
32
+ * without a shell (the CVE-2024-27980 fix). With a shell, Node joins the arguments with spaces,
33
+ * so any argument containing a space or a shell metacharacter (a temp folder under
34
+ * `C:\Users\Jane Doe\…`) is wrapped in double quotes. Real executables (`node`, `tar`, `git`)
35
+ * and every other platform keep running without a shell. */
36
+ export function platformCommand(command, args, platform = process.platform) {
37
+ if (platform !== 'win32' || !WINDOWS_SHIMS.has(command))
38
+ return { command, args, shell: false };
39
+ return { command, args: args.map((arg) => (/[\s"&|<>^()]/.test(arg) ? `"${arg.replace(/"/g, '""')}"` : arg)), shell: true };
40
+ }
@@ -3,8 +3,8 @@ import { execFileSync } from 'node:child_process';
3
3
  import { createInterface } from 'node:readline/promises';
4
4
  import path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { buildUpdatePlan } from "./updatePlan.js";
7
- import { detectPackageManager, installUpgradeCommand } from "./packageManager.js";
6
+ import { buildUpdatePlan, isAlreadyUpToDate } from "./updatePlan.js";
7
+ import { detectPackageManager, installUpgradeCommand, platformCommand } from "./packageManager.js";
8
8
  import { readOwnVersion } from "./packageVersion.js";
9
9
  import { lastSummaryLine } from "./doctor.js";
10
10
  function formatPlanHuman(plan) {
@@ -37,7 +37,7 @@ export async function runUpdate(config, options = {}) {
37
37
  const currentVersion = options.currentVersion ?? readOwnVersion(path.dirname(fileURLToPath(import.meta.url)));
38
38
  const plan = await (options.buildPlan ?? buildUpdatePlan)(config, currentVersion);
39
39
  if ('error' in plan)
40
- return { output: plan.error, exitCode: 1 };
40
+ return { output: plan.error, exitCode: isAlreadyUpToDate(plan.error) ? 0 : 1 };
41
41
  const planText = formatPlanHuman(plan);
42
42
  if (options.dryRun)
43
43
  return { output: planText, exitCode: 0 };
@@ -55,7 +55,10 @@ export async function runUpdate(config, options = {}) {
55
55
  if (!proceed)
56
56
  return { output: 'Aborted: nothing was changed.', exitCode: 0 };
57
57
  }
58
- const execImpl = options.execImpl ?? ((cmd, args, opts) => execFileSync(cmd, args, { ...opts, encoding: 'utf8' }));
58
+ const execImpl = options.execImpl ?? ((cmd, args, opts) => {
59
+ const run = platformCommand(cmd, args);
60
+ return execFileSync(run.command, run.args, { ...opts, encoding: 'utf8', shell: run.shell });
61
+ });
59
62
  const { command, args } = installUpgradeCommand(detectPackageManager(config.projectRoot), '@krapwoo/ds-viewer', plan.latest);
60
63
  try {
61
64
  execImpl(command, args, { cwd: config.projectRoot, encoding: 'utf8' });
@@ -4,7 +4,7 @@ import { mkdtempSync, rmSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
5
  import path from 'node:path';
6
6
  import { promisify } from 'node:util';
7
- import { detectPackageManager } from "./packageManager.js";
7
+ import { detectPackageManager, platformCommand } from "./packageManager.js";
8
8
  import { listKitFileDiffs } from "./kitDiff.js";
9
9
  import { extractSummaryBullets, fetchWithTimeout } from "./updateCheck.js";
10
10
  import { compareVersions, isBreakingUpgrade, parseVersion } from "./semver.js";
@@ -88,12 +88,21 @@ function defaultGitStatus(projectRoot, plannedPaths) {
88
88
  throw new GitStatusError(`git status failed: ${error.message}`);
89
89
  }
90
90
  }
91
+ /** "Already on the latest version" is reported through the plan's `{ error }` channel, but it is not a
92
+ * failure: `update` exits 0 for it, and the viewer shows it as up to date (`isAlreadyUpToDateError`). */
93
+ export const ALREADY_UP_TO_DATE = 'Already on the latest version';
94
+ export function isAlreadyUpToDate(error) {
95
+ return error.startsWith(ALREADY_UP_TO_DATE);
96
+ }
91
97
  /** Design §5 "Update plan", steps 1-3, in full. Every external effect is injected (Global
92
98
  * Constraints) — the real defaults are a real `npm view`/`npm pack`/`tar`/`node migrate` and a
93
99
  * real `git status --porcelain`, exactly what Task 20's own Verdaccio spike already exercised. */
94
100
  export async function buildUpdatePlan(config, currentVersion, options = {}) {
95
101
  const execFileAsync = promisify(execFile);
96
- const execImpl = options.execImpl ?? (async (cmd, args, opts) => (await execFileAsync(cmd, args, opts)).stdout);
102
+ const execImpl = options.execImpl ?? (async (cmd, args, opts) => {
103
+ const run = platformCommand(cmd, args);
104
+ return (await execFileAsync(run.command, run.args, { ...opts, shell: run.shell })).stdout;
105
+ });
97
106
  const fetchImpl = options.fetchImpl ?? fetch;
98
107
  const tmpDirImpl = options.tmpDirImpl ?? (() => mkdtempSync(path.join(tmpdir(), 'ds-viewer-update-')));
99
108
  const gitStatusImpl = options.gitStatusImpl ?? defaultGitStatus;
@@ -114,7 +123,7 @@ export async function buildUpdatePlan(config, currentVersion, options = {}) {
114
123
  // installed version ahead of the registry's own `npm view` result (e.g. a local prerelease) is
115
124
  // not something `update` should try to "downgrade" into.
116
125
  if (compareVersions(parseVersion(targetVersion), parseVersion(currentVersion)) <= 0) {
117
- return { error: `Already on the latest version (${currentVersion}).` };
126
+ return { error: `${ALREADY_UP_TO_DATE} (${currentVersion}).` };
118
127
  }
119
128
  let migrateResult;
120
129
  // M4 (Minor, Fable correction pass): compared against the *downloaded target version's* own
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krapwoo/ds-viewer",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "A browsable, always-current catalog for an Expo / React Native design system.",