@krapwoo/ds-viewer 0.6.6 → 0.6.8

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
@@ -622,6 +622,16 @@ Downloads the target version, runs its own `migrate --from <your version>`, upgr
622
622
  whichever package manager your lockfile names, re-runs `migrate` for real, then `doctor`. Changes
623
623
  are left uncommitted — review them like any other change.
624
624
 
625
+ The upgrade keeps the package in whichever section your `package.json` already declares it in,
626
+ rather than always writing a devDependency.
627
+
628
+ If that spec is a local one — `file:`, `link:`, `workspace:`, `portal:`, or a bare path — `update`
629
+ refuses and tells you how to proceed, because installing a published version would replace the
630
+ checkout your project deliberately points at. The catalog would keep running while no longer
631
+ reflecting the source you are editing, which is hard to notice. `--force` does not override this:
632
+ it exists for uncommitted files, not for changing where the package comes from. The update page's
633
+ **Update now** refuses the same way, reporting it as a failed install step.
634
+
625
635
  ### `npx ds-viewer migrate --from <version>`
626
636
 
627
637
  Runs every migration introduced after `<version>` — `update` calls this for you; run it directly
package/dist/cli/dev.js CHANGED
@@ -14,7 +14,8 @@ 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, platformCommand } from "./packageManager.js";
17
+ import { declaredDependency, detectPackageManager, installUpgradeCommand, isLocalSpec, platformCommand } from "./packageManager.js";
18
+ import { localInstallRefusal } from "./update.js";
18
19
  import { DEFAULT_WORKSPACE_DIR, RENDER_WORKSPACE_DIR } from "./types.js";
19
20
  export class LocalInstallMissingError extends Error {
20
21
  }
@@ -142,7 +143,22 @@ export async function performUpdate(config, currentVersion, plan, deps) {
142
143
  });
143
144
  report(startAt);
144
145
  if (startAt <= 1) {
145
- const { command, args } = installUpgradeCommand(pm, '@krapwoo/ds-viewer', plan.latest);
146
+ // The same refusal the CLI gives: installing a registry version over a `file:`/`link:`/
147
+ // `workspace:` spec would replace the local checkout the project points at, and the catalog
148
+ // would keep serving while no longer reflecting the source being edited. Reported as a
149
+ // failure of the install step, so the page offers its normal recovery rather than appearing
150
+ // to succeed.
151
+ const declared = declaredDependency(config.projectRoot, '@krapwoo/ds-viewer');
152
+ if (declared && isLocalSpec(declared.spec)) {
153
+ deps.onStatus({
154
+ phase: 'failure',
155
+ log: localInstallRefusal(declared.spec, plan.latest),
156
+ failedStep: stepDefs[1].failLabel ?? stepDefs[1].label,
157
+ step: 'install',
158
+ });
159
+ return;
160
+ }
161
+ const { command, args } = installUpgradeCommand(pm, '@krapwoo/ds-viewer', plan.latest, declared?.section);
146
162
  try {
147
163
  await deps.execImpl(command, args, { cwd: config.projectRoot, encoding: 'utf8' });
148
164
  }
@@ -15,17 +15,52 @@ export function detectPackageManager(projectRoot) {
15
15
  }
16
16
  return 'npm'; // package-lock.json, or no lockfile at all (npm's own documented default either way).
17
17
  }
18
+ /** How `pkg` is declared in this project, or undefined when it is not declared at all.
19
+ * `dependencies` wins when both sections name it, matching npm's own resolution. */
20
+ export function declaredDependency(projectRoot, pkg) {
21
+ const packageJsonPath = path.join(projectRoot, 'package.json');
22
+ if (!existsSync(packageJsonPath))
23
+ return undefined;
24
+ let parsed;
25
+ try {
26
+ parsed = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
27
+ }
28
+ catch {
29
+ return undefined; // An unreadable package.json is not this function's error to report.
30
+ }
31
+ const fromDeps = parsed.dependencies?.[pkg];
32
+ if (typeof fromDeps === 'string')
33
+ return { section: 'dependencies', spec: fromDeps };
34
+ const fromDevDeps = parsed.devDependencies?.[pkg];
35
+ if (typeof fromDevDeps === 'string')
36
+ return { section: 'devDependencies', spec: fromDevDeps };
37
+ return undefined;
38
+ }
39
+ /** A spec that resolves to something other than a published version: the local source tree, a
40
+ * linked folder, or another package in the same workspace. Installing a registry version over one
41
+ * of these replaces the thing the project is deliberately pointing at, so `update` refuses rather
42
+ * than doing it silently. `npm:` aliases and plain ranges are registry-backed and not local. */
43
+ export function isLocalSpec(spec) {
44
+ const value = spec.trim();
45
+ if (/^(file|link|workspace|portal):/i.test(value))
46
+ return true;
47
+ // A bare relative or absolute path is a file dependency too — npm accepts `../pkg` unprefixed.
48
+ return value.startsWith('./') || value.startsWith('../') || value.startsWith('/') || value.startsWith('~/');
49
+ }
18
50
  /** The one command `cli/update.ts`'s "Upgrades with the project's package manager" step (design
19
51
  * §5 "`npx ds-viewer update`" step 3) runs, for whichever manager `detectPackageManager` found.
20
- * Yarn (classic and Berry both) use the same `yarn add --dev` syntax for upgrading a single
21
- * package to an exact version. */
22
- export function installUpgradeCommand(pm, pkg, version) {
52
+ * Yarn (classic and Berry both) use the same `yarn add` syntax for upgrading a single package to
53
+ * an exact version. `section` keeps the package in the section it is already declared in; it
54
+ * defaults to `devDependencies`, which is where `init` puts it. */
55
+ export function installUpgradeCommand(pm, pkg, version, section = 'devDependencies') {
23
56
  const spec = `${pkg}@${version}`;
57
+ const dev = section === 'devDependencies';
24
58
  if (pm === 'npm')
25
- return { command: 'npm', args: ['install', '--save-dev', spec] };
59
+ return { command: 'npm', args: ['install', dev ? '--save-dev' : '--save-prod', spec] };
26
60
  if (pm === 'pnpm')
27
- return { command: 'pnpm', args: ['add', '--save-dev', spec] };
28
- return { command: 'yarn', args: ['add', '--dev', spec] };
61
+ return { command: 'pnpm', args: ['add', dev ? '--save-dev' : '--save-prod', spec] };
62
+ // Yarn has no --save-prod: a plain `yarn add` is already a production dependency.
63
+ return { command: 'yarn', args: dev ? ['add', '--dev', spec] : ['add', spec] };
29
64
  }
30
65
  const WINDOWS_SHIMS = new Set(['npm', 'npx', 'pnpm', 'yarn']);
31
66
  /** On Windows, `npm`, `npx`, `pnpm` and `yarn` are `.cmd` shims, which Node refuses to run
@@ -4,7 +4,7 @@ import { createInterface } from 'node:readline/promises';
4
4
  import path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { buildUpdatePlan, isAlreadyUpToDate } from "./updatePlan.js";
7
- import { detectPackageManager, installUpgradeCommand, platformCommand } from "./packageManager.js";
7
+ import { declaredDependency, detectPackageManager, installUpgradeCommand, isLocalSpec, platformCommand } from "./packageManager.js";
8
8
  import { readOwnVersion } from "./packageVersion.js";
9
9
  import { lastSummaryLine } from "./doctor.js";
10
10
  function formatPlanHuman(plan) {
@@ -32,6 +32,22 @@ async function defaultConfirm(message) {
32
32
  function installedMainJs(projectRoot) {
33
33
  return path.join(projectRoot, 'node_modules', '@krapwoo', 'ds-viewer', 'dist', 'cli', 'main.js');
34
34
  }
35
+ /** Why an update cannot run over a local checkout, and what to do instead. Shared by the CLI and
36
+ * the update page so both refuse in the same words. */
37
+ export function localInstallRefusal(spec, latest) {
38
+ return [
39
+ `Refusing: @krapwoo/ds-viewer is installed from "${spec}", not from the registry.`,
40
+ '',
41
+ 'Updating would install the published package over it, replacing the local checkout this',
42
+ 'project deliberately points at — the catalog would keep running while no longer reflecting',
43
+ 'the source being edited.',
44
+ '',
45
+ `To move this project onto the published ${latest}, change the dependency yourself:`,
46
+ ` npm install --save-dev @krapwoo/ds-viewer@${latest}`,
47
+ '',
48
+ 'To stay on local source, pull and rebuild it instead.',
49
+ ].join('\n');
50
+ }
35
51
  /** Design §5 "`npx ds-viewer update`", steps 1-6. */
36
52
  export async function runUpdate(config, options = {}) {
37
53
  const currentVersion = options.currentVersion ?? readOwnVersion(path.dirname(fileURLToPath(import.meta.url)));
@@ -41,6 +57,14 @@ export async function runUpdate(config, options = {}) {
41
57
  const planText = formatPlanHuman(plan);
42
58
  if (options.dryRun)
43
59
  return { output: planText, exitCode: 0 };
60
+ // Refused before the dirty-file check and before any prompt: a `--yes --force` script must not
61
+ // be able to swap a local checkout for a registry copy either. `--force` deliberately does not
62
+ // override this — it exists for uncommitted files, not for changing where the package comes
63
+ // from, which is a decision the project's own package.json already recorded.
64
+ const declared = declaredDependency(config.projectRoot, '@krapwoo/ds-viewer');
65
+ if (declared && isLocalSpec(declared.spec)) {
66
+ return { output: `${planText}\n\n${localInstallRefusal(declared.spec, plan.latest)}`, exitCode: 1 };
67
+ }
44
68
  // Step 2: refuses on any dirty planned file, unless --force — checked before the confirmation
45
69
  // prompt, so a script passing --yes without --force still gets refused instead of silently
46
70
  // overwriting uncommitted work.
@@ -59,7 +83,9 @@ export async function runUpdate(config, options = {}) {
59
83
  const run = platformCommand(cmd, args);
60
84
  return execFileSync(run.command, run.args, { ...opts, encoding: 'utf8', shell: run.shell });
61
85
  });
62
- const { command, args } = installUpgradeCommand(detectPackageManager(config.projectRoot), '@krapwoo/ds-viewer', plan.latest);
86
+ const { command, args } = installUpgradeCommand(detectPackageManager(config.projectRoot), '@krapwoo/ds-viewer', plan.latest,
87
+ // Keep it in the section the project already declared, instead of forcing devDependencies.
88
+ declared?.section);
63
89
  try {
64
90
  execImpl(command, args, { cwd: config.projectRoot, encoding: 'utf8' });
65
91
  }
@@ -113,22 +113,21 @@ function Preview({ render, widths, surface, layout }: { render: () => React.Reac
113
113
  if (widths === 'full') return <SpecimenSurface surface={kind} style={styles.previewCard}>{render()}</SpecimenSurface>;
114
114
  if (layout === 'table') return <TablePreview render={render} widths={widths} surface={kind} />;
115
115
  return (
116
- <View style={styles.previewCard}>
116
+ <SpecimenSurface surface={kind} style={styles.previewCard}>
117
117
  <View style={styles.frames}>
118
118
  {widths.map((width, i) => (
119
- <SpecimenSurface
119
+ <View
120
120
  key={`${width}-${i}`}
121
- surface={kind}
122
121
  // Two live instances of the same component: name each so assistive tech can tell them apart.
123
122
  {...(widths.length > 1 ? ({ role: 'group', 'aria-label': frameLabel(width) } as Record<string, unknown>) : null)}
124
123
  style={[styles.frame, { width }]}
125
124
  >
126
125
  {widths.length > 1 && <Text style={styles.frameLabel}>{frameLabel(width)}</Text>}
127
126
  {render()}
128
- </SpecimenSurface>
127
+ </View>
129
128
  ))}
130
129
  </View>
131
- </View>
130
+ </SpecimenSurface>
132
131
  );
133
132
  }
134
133
 
@@ -116,25 +116,30 @@ function readTree(root: MeasurableNode, browser: Browser): LayoutBox {
116
116
  }
117
117
 
118
118
  export function SpecimenContent({ fill, align, capped = true, children }: { fill?: boolean; align?: SpecimenAlign; /** Phone-width cap for a filled example; off for page-width cells. */ capped?: boolean; children: React.ReactNode }) {
119
- // After layout (browser only): a `fill` example whose visible content is narrower than the cell
120
- // (a fixed-width skeleton, a tooltip around a small trigger) is centred instead of sitting
121
- // against the leading edge; an explicit `align` always wins. And in development, an example
119
+ // After layout (browser only): a genuinely narrow `fill` root (a fixed-width skeleton) is
120
+ // centred. Narrow text/icons inside a full-width layout do not collapse that layout's root.
121
+ // An explicit `align` always wins. And in development, an example
122
122
  // wider than its cell logs a warning naming a wider `specimenSize`.
123
123
  const address = useSpecimenAddress();
124
124
  const ref = useRef<View>(null);
125
125
  const [centerNarrow, setCenterNarrow] = useState(false);
126
126
  const warned = useRef(false);
127
+ useEffect(() => {
128
+ setCenterNarrow(false);
129
+ }, [fill, capped, align]);
127
130
  const measure = useCallback(() => {
128
131
  const browser = globalThis as unknown as Browser;
129
132
  const node = ref.current as unknown as MeasurableNode | null;
130
133
  if (!browser.Element || !browser.getComputedStyle || !node?.getBoundingClientRect) return;
131
134
  const wrapperWidth = node.getBoundingClientRect().width;
132
- const visible = paintedExtent(visibleBoxes(readTree(node, browser)));
135
+ const tree = readTree(node, browser);
136
+ const visible = paintedExtent(visibleBoxes(tree));
137
+ const rootWidth = Math.max(0, ...tree.children.map((child) => child.right - child.left));
133
138
  // Only for a phone-width cell. Centring exists for a small control adrift in a wide cell (a
134
139
  // tooltip, a fixed-width skeleton); a 'page' cell holds a layout, whose own content is
135
140
  // supposed to sit at the left of its own measure, and centring it collapses it to the width of
136
141
  // whatever it happens to have painted.
137
- if (capped && fill && align === undefined && !centerNarrow && shouldCenterFilledContent(visible, wrapperWidth)) setCenterNarrow(true);
142
+ if (capped && fill && align === undefined && !centerNarrow && shouldCenterFilledContent(visible, wrapperWidth, rootWidth)) setCenterNarrow(true);
138
143
  if ((globalThis as { __DEV__?: boolean }).__DEV__ && !warned.current && overflowsCell(visible, wrapperWidth)) {
139
144
  warned.current = true;
140
145
  console.warn(
@@ -143,7 +148,7 @@ export function SpecimenContent({ fill, align, capped = true, children }: { fill
143
148
  }
144
149
  }, [capped, fill, align, centerNarrow, address]);
145
150
  if (fill) {
146
- const alignItems = centerNarrow ? 'center' : specimenContentAlignItems(fill, align);
151
+ const alignItems = capped && align === undefined && centerNarrow ? 'center' : specimenContentAlignItems(fill, align);
147
152
  return <View ref={ref} onLayout={measure} style={[SPECIMEN_SURFACE_WIDTH_STYLE, filledContentStyle(capped), { alignItems }]}>{children}</View>;
148
153
  }
149
154
  return <View ref={ref} onLayout={measure} style={specimenContentBoundedStyle(align)}>{children}</View>;
@@ -89,12 +89,12 @@ export function specimenContentBoundedStyle(align: SpecimenAlign | undefined) {
89
89
  * `specimenFill.ts`). A page can force a fill with `specimenSurface`. */
90
90
  export const DEFAULT_SPECIMEN_SURFACE: SpecimenSurfaceKind = 'auto';
91
91
 
92
- /** A `fill` example stretches to its cell, but one with its own fixed width (a 160-point skeleton
93
- * bar) stays that width; it should then sit in the middle of the cell, not against its leading
94
- * edge. True when the measured example is narrower than its wrapper (by more than rounding). */
95
- export function shouldCenterFilledContent(contentWidth: number, wrapperWidth: number): boolean {
96
- if (!(contentWidth > 0) || !(wrapperWidth > 0)) return false;
97
- return contentWidth < wrapperWidth - 1;
92
+ /** Center genuinely narrow filled examples, not narrow paint inside a full-width fluid layout.
93
+ * The latter must retain stretch: centering its root would collapse it to its intrinsic width.
94
+ * Callers without a separate root measurement retain the established fixed-width behavior. */
95
+ export function shouldCenterFilledContent(contentWidth: number, wrapperWidth: number, rootWidth = contentWidth): boolean {
96
+ if (!(contentWidth > 0) || !(wrapperWidth > 0) || !(rootWidth > 0)) return false;
97
+ return contentWidth < wrapperWidth - 1 && rootWidth < wrapperWidth - 1;
98
98
  }
99
99
 
100
100
  /** The width of what's actually visible in an example: from the leftmost to the rightmost painted
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krapwoo/ds-viewer",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "A browsable, always-current catalog for an Expo / React Native design system.",