@krapwoo/ds-viewer 0.4.5 → 0.4.7
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 +111 -18
- package/dist/cli/dev.js +31 -6
- package/dist/cli/doctor.js +137 -0
- package/dist/cli/endpoint.js +76 -1
- package/dist/cli/explain.js +44 -6
- package/dist/cli/main.js +4 -2
- package/dist/cli/props.js +80 -1
- package/dist/cli/staticPage.js +228 -6
- package/dist/cli/updateCheck.js +174 -43
- package/dist/cli/workspace.js +2 -1
- package/dist/native/catalog/catalogNavigation.js +11 -0
- package/dist/native/catalog/comparison.js +29 -4
- package/native/catalog/BoundedOverlayViewport.tsx +112 -0
- package/native/catalog/CatalogShell.tsx +69 -12
- package/native/catalog/CatalogSidebar.tsx +8 -7
- package/native/catalog/ComparisonGrid.tsx +2 -2
- package/native/catalog/ComparisonGroups.tsx +2 -2
- package/native/catalog/ComparisonList.tsx +2 -2
- package/native/catalog/PhoneFrame.tsx +6 -0
- package/native/catalog/ReferenceDetails.tsx +75 -20
- package/native/catalog/SectionBlock.tsx +66 -4
- package/native/catalog/SpecimenSurface.tsx +18 -10
- package/native/catalog/UpdatePanel.tsx +161 -5
- package/native/catalog/catalogNavigation.ts +14 -1
- package/native/catalog/comparison.ts +39 -5
- package/native/catalog/index.ts +4 -0
- package/native/catalog/overlayViewport.ts +156 -0
- package/native/catalog/pageApi.ts +6 -0
- package/native/catalog/specimenSurfaceStyle.ts +70 -1
- package/native/catalog/types.ts +62 -12
- package/native/catalog/updatePanelState.ts +72 -0
- package/package.json +1 -1
- package/starter-kit/components/Banner/Banner.catalog.tsx +3 -0
- package/starter-kit/components/Button/Button.catalog.tsx +3 -0
- package/starter-kit/components/Dropdown/Dropdown.catalog.tsx +6 -0
- package/starter-kit/components/EmptyState/EmptyState.catalog.tsx +5 -0
- package/starter-kit/components/InputField/InputField.catalog.tsx +3 -0
- package/starter-kit/components/List/List.catalog.tsx +3 -0
- package/starter-kit/components/Pill/Pill.catalog.tsx +3 -0
- package/starter-kit/components/PillRow/PillRow.catalog.tsx +3 -0
- package/starter-kit/components/SearchField/SearchField.catalog.tsx +4 -0
- package/starter-kit/components/SectionHeader/SectionHeader.catalog.tsx +3 -0
- package/starter-kit/components/TextArea/TextArea.catalog.tsx +3 -0
- package/starter-kit/components/Toast/Toast.catalog.tsx +7 -0
package/dist/cli/props.js
CHANGED
|
@@ -185,7 +185,86 @@ function readComponentRecord(checker, name, type, declaration, optionRoots, comp
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
const inheritedFrom = propsType ? nodeModulesHeritageNames(checker, propsType.isUnion() ? propsType.types : [propsType]) : [];
|
|
188
|
-
|
|
188
|
+
const composedOfCandidates = readComposedOfCandidates(checker, declaration);
|
|
189
|
+
return {
|
|
190
|
+
name,
|
|
191
|
+
file: path.relative(process.cwd(), sourceFile.fileName),
|
|
192
|
+
props,
|
|
193
|
+
inheritedFrom,
|
|
194
|
+
...(composedOfCandidates.length > 0 ? { composedOfCandidates } : {}),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/** Optional, additive source evidence (guided-intelligence design §4, plan Task 3): every other
|
|
198
|
+
* known, project-local component this component's own implementation renders as a literal JSX
|
|
199
|
+
* tag. Scoped to exactly the implementation function's own body (the same function
|
|
200
|
+
* `defaultsFromImplementation` already unwraps through `React.memo`/`forwardRef`) — a same-file
|
|
201
|
+
* helper's own JSX, or a different export's JSX, is never walked at all, since traversal never
|
|
202
|
+
* starts there. Only a JSX tag that is a bare identifier resolving (through import aliases and
|
|
203
|
+
* barrel re-exports) to an exported symbol satisfying `isComponentType` becomes a candidate; a
|
|
204
|
+
* type-only import can never reach here (it cannot be used as a JSX tag at all), an import that is
|
|
205
|
+
* never written as a JSX tag is never visited, and a locally-computed/conditional tag identifier
|
|
206
|
+
* (not itself an import alias) is left uncertain rather than guessed at. A `node_modules`
|
|
207
|
+
* declaration (a framework primitive, not a project component) is excluded — "known canonical
|
|
208
|
+
* component export" means part of this project's own component surface. */
|
|
209
|
+
function readComposedOfCandidates(checker, declaration) {
|
|
210
|
+
const startNode = ts.isVariableDeclaration(declaration) && declaration.initializer ? declaration.initializer : declaration;
|
|
211
|
+
const fn = resolveFunctionLike(startNode, new Set());
|
|
212
|
+
if (!fn?.body)
|
|
213
|
+
return [];
|
|
214
|
+
const candidates = [];
|
|
215
|
+
const visit = (node) => {
|
|
216
|
+
// A nested named helper (a local `function foo() {}` declaration, or a `const foo = () => {}`/
|
|
217
|
+
// `function expression` assignment) defines its own scope: its JSX belongs to it, not to this
|
|
218
|
+
// component, even when this component calls it as an ordinary function — so its body is never
|
|
219
|
+
// walked at all, matching the top-level sibling-helper behavior this already preserved.
|
|
220
|
+
if (isNestedNamedHelper(node))
|
|
221
|
+
return;
|
|
222
|
+
if ((ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) && ts.isIdentifier(node.tagName)) {
|
|
223
|
+
const candidate = resolveComposedOfCandidate(checker, node.tagName);
|
|
224
|
+
if (candidate)
|
|
225
|
+
candidates.push(candidate);
|
|
226
|
+
}
|
|
227
|
+
ts.forEachChild(node, visit);
|
|
228
|
+
};
|
|
229
|
+
visit(fn.body);
|
|
230
|
+
return candidates;
|
|
231
|
+
}
|
|
232
|
+
function isNestedNamedHelper(node) {
|
|
233
|
+
if (ts.isFunctionDeclaration(node) && node.name)
|
|
234
|
+
return true;
|
|
235
|
+
return (ts.isVariableDeclaration(node) &&
|
|
236
|
+
ts.isIdentifier(node.name) &&
|
|
237
|
+
node.initializer !== undefined &&
|
|
238
|
+
(ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)));
|
|
239
|
+
}
|
|
240
|
+
/** Resolves one JSX tag identifier to a composedOfCandidate, or undefined when it isn't a known,
|
|
241
|
+
* project-local component export reached through an import alias (directly or through one or more
|
|
242
|
+
* barrel re-exports). */
|
|
243
|
+
function resolveComposedOfCandidate(checker, tagName) {
|
|
244
|
+
let symbol = checker.getSymbolAtLocation(tagName);
|
|
245
|
+
if (!symbol)
|
|
246
|
+
return undefined;
|
|
247
|
+
// Only a tag identifier that is itself bound to an import (an Alias symbol) counts — a locally
|
|
248
|
+
// computed/conditional identifier (e.g. `const Comp = cond ? A : B`) is left uncertain even if its
|
|
249
|
+
// inferred type happens to satisfy `isComponentType`.
|
|
250
|
+
if (!(symbol.flags & ts.SymbolFlags.Alias))
|
|
251
|
+
return undefined;
|
|
252
|
+
const seen = new Set();
|
|
253
|
+
while (symbol.flags & ts.SymbolFlags.Alias && !seen.has(symbol)) {
|
|
254
|
+
seen.add(symbol);
|
|
255
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
256
|
+
}
|
|
257
|
+
const resolvedDeclaration = symbol.valueDeclaration;
|
|
258
|
+
if (!resolvedDeclaration)
|
|
259
|
+
return undefined;
|
|
260
|
+
const declarationFile = resolvedDeclaration.getSourceFile().fileName;
|
|
261
|
+
if (declarationFile.includes('node_modules'))
|
|
262
|
+
return undefined;
|
|
263
|
+
const type = checker.getTypeOfSymbolAtLocation(symbol, resolvedDeclaration);
|
|
264
|
+
if (!isComponentType(checker, type))
|
|
265
|
+
return undefined;
|
|
266
|
+
const { line } = tagName.getSourceFile().getLineAndCharacterOfPosition(tagName.getStart());
|
|
267
|
+
return { component: symbol.getName(), file: path.relative(process.cwd(), tagName.getSourceFile().fileName), line: line + 1 };
|
|
189
268
|
}
|
|
190
269
|
/** Design §3 — "Type column: keeps alias names, not expanded unions." An optional prop's type
|
|
191
270
|
* (as read via its symbol) includes `undefined` — printed as `"T | undefined"` when `T` has no
|
package/dist/cli/staticPage.js
CHANGED
|
@@ -140,7 +140,7 @@ function readStaticPage(file) {
|
|
|
140
140
|
else
|
|
141
141
|
page.checkable = false;
|
|
142
142
|
}
|
|
143
|
-
const variants = readSlotItems(findProp(pageObject, 'variants'), consts);
|
|
143
|
+
const variants = readSlotItems(findProp(pageObject, 'variants'), consts, page.component, sourceFile);
|
|
144
144
|
if (!variants.checkable)
|
|
145
145
|
page.checkable = false;
|
|
146
146
|
else {
|
|
@@ -148,7 +148,7 @@ function readStaticPage(file) {
|
|
|
148
148
|
page.variantsItemsFill = variants.itemsFill;
|
|
149
149
|
page.variantsMaxColumns = variants.maxColumns;
|
|
150
150
|
}
|
|
151
|
-
const states = readSlotItems(findProp(pageObject, 'states'), consts);
|
|
151
|
+
const states = readSlotItems(findProp(pageObject, 'states'), consts, page.component, sourceFile);
|
|
152
152
|
if (!states.checkable)
|
|
153
153
|
page.checkable = false;
|
|
154
154
|
else {
|
|
@@ -156,8 +156,42 @@ function readStaticPage(file) {
|
|
|
156
156
|
page.statesItemsFill = states.itemsFill;
|
|
157
157
|
page.statesMaxColumns = states.maxColumns;
|
|
158
158
|
}
|
|
159
|
+
const composedOfProp = findProp(pageObject, 'composedOf');
|
|
160
|
+
page.hasComposedOf = composedOfProp !== undefined;
|
|
161
|
+
if (composedOfProp)
|
|
162
|
+
page.composedOf = readComposedOf(composedOfProp.initializer, consts);
|
|
163
|
+
const intentionalStaticPreviewProp = findProp(pageObject, 'intentionalStaticPreview');
|
|
164
|
+
if (intentionalStaticPreviewProp) {
|
|
165
|
+
const resolved = resolve(intentionalStaticPreviewProp.initializer, consts, new Set());
|
|
166
|
+
if (ts.isObjectLiteralExpression(resolved)) {
|
|
167
|
+
const reasonProp = findProp(resolved, 'reason');
|
|
168
|
+
if (reasonProp)
|
|
169
|
+
page.intentionalStaticPreviewReason = stringLiteral(resolve(reasonProp.initializer, consts, new Set()));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
159
172
|
return page;
|
|
160
173
|
}
|
|
174
|
+
/** A literal array of `ComposedOfEntry`-shaped object literals, read down to just `component`
|
|
175
|
+
* (design §4: role/relationship are authored prose `doctor` never needs for its missing-
|
|
176
|
+
* composition comparison). Undefined — not an empty array — for anything that isn't itself a
|
|
177
|
+
* literal array, or whose element isn't an object literal with a literal `component`: `doctor`
|
|
178
|
+
* must never mistake "couldn't read this" for "author declared no composition here". */
|
|
179
|
+
function readComposedOf(node, consts) {
|
|
180
|
+
const resolved = resolve(node, consts, new Set());
|
|
181
|
+
if (!ts.isArrayLiteralExpression(resolved))
|
|
182
|
+
return undefined;
|
|
183
|
+
const entries = [];
|
|
184
|
+
for (const element of resolved.elements) {
|
|
185
|
+
const resolvedElement = resolve(element, consts, new Set());
|
|
186
|
+
if (!ts.isObjectLiteralExpression(resolvedElement))
|
|
187
|
+
return undefined;
|
|
188
|
+
const component = propertyStringLiteral(resolvedElement, 'component', consts);
|
|
189
|
+
if (component === undefined)
|
|
190
|
+
return undefined;
|
|
191
|
+
entries.push({ component });
|
|
192
|
+
}
|
|
193
|
+
return entries;
|
|
194
|
+
}
|
|
161
195
|
/** Local name -> imported name, for every named import from `moduleSpecifier`. */
|
|
162
196
|
function importedNames(sourceFile, moduleSpecifier) {
|
|
163
197
|
const names = new Map();
|
|
@@ -219,6 +253,20 @@ function resolve(node, consts, seen) {
|
|
|
219
253
|
function stringLiteral(node) {
|
|
220
254
|
return ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) ? node.text : undefined;
|
|
221
255
|
}
|
|
256
|
+
/** `true`/`false` only from a known `true`/`false` literal; any other expression (a variable, a
|
|
257
|
+
* computed value) stays `undefined` rather than being misread as `false`. Unlike the page's own
|
|
258
|
+
* `hide.*`/`tokenGallery`/`itemsFill`/`wide` booleans — which intentionally treat "not literally
|
|
259
|
+
* `true`" as `false`, since those already default to `false` when omitted — `fill`'s own default
|
|
260
|
+
* is inherited (`SpecimenPresentation.fill` defaults to the slot's `itemsFill`, not to `false`),
|
|
261
|
+
* so a non-literal value here must stay honestly unknown instead of claiming "not filling". */
|
|
262
|
+
function literalBooleanOrUnknown(node, consts) {
|
|
263
|
+
const resolved = resolve(node, consts, new Set());
|
|
264
|
+
if (resolved.kind === ts.SyntaxKind.TrueKeyword)
|
|
265
|
+
return true;
|
|
266
|
+
if (resolved.kind === ts.SyntaxKind.FalseKeyword)
|
|
267
|
+
return false;
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
222
270
|
function propertyName(name) {
|
|
223
271
|
return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : name.getText();
|
|
224
272
|
}
|
|
@@ -271,6 +319,34 @@ function readAxis(node, consts) {
|
|
|
271
319
|
}
|
|
272
320
|
return undefined;
|
|
273
321
|
}
|
|
322
|
+
/** A literal array of `ComparisonCell`-shaped object literals: `rowKey`/`columnKey` required
|
|
323
|
+
* (string literals only), `surface`/`align`/`fill` read the same per-field-lenient way
|
|
324
|
+
* `readListItems` reads them. `node`/`unavailableReason` are never inspected — design §4 never
|
|
325
|
+
* evaluates a cell's specimen node. Undefined (no cell metadata at all, not "every field unset")
|
|
326
|
+
* for anything that isn't itself a literal array, or whose element isn't itself an object literal
|
|
327
|
+
* with literal `rowKey`/`columnKey` — a `.flatMap()`-built `cells` array (the starter kit's own
|
|
328
|
+
* convention for a `grid()`-free literal comparison) stays unrepresented rather than guessed at. */
|
|
329
|
+
function readComparisonCells(node, consts) {
|
|
330
|
+
const resolved = resolve(node, consts, new Set());
|
|
331
|
+
if (!ts.isArrayLiteralExpression(resolved))
|
|
332
|
+
return undefined;
|
|
333
|
+
const cells = [];
|
|
334
|
+
for (const element of resolved.elements) {
|
|
335
|
+
const resolvedElement = resolve(element, consts, new Set());
|
|
336
|
+
if (!ts.isObjectLiteralExpression(resolvedElement))
|
|
337
|
+
return undefined;
|
|
338
|
+
const rowKey = propertyStringLiteral(resolvedElement, 'rowKey', consts);
|
|
339
|
+
const columnKey = propertyStringLiteral(resolvedElement, 'columnKey', consts);
|
|
340
|
+
if (rowKey === undefined || columnKey === undefined)
|
|
341
|
+
return undefined;
|
|
342
|
+
const surface = propertyStringLiteral(resolvedElement, 'surface', consts);
|
|
343
|
+
const align = propertyStringLiteral(resolvedElement, 'align', consts);
|
|
344
|
+
const fillProp = findProp(resolvedElement, 'fill');
|
|
345
|
+
const fill = fillProp ? literalBooleanOrUnknown(fillProp.initializer, consts) : undefined;
|
|
346
|
+
cells.push({ rowKey, columnKey, surface, align, fill });
|
|
347
|
+
}
|
|
348
|
+
return cells;
|
|
349
|
+
}
|
|
274
350
|
/** A page's `comparison` field: either a direct `ComparisonDef`-shaped object literal, or (the
|
|
275
351
|
* starter kit's own convention since Task 1/2) a call to this package's own `grid()` helper — the
|
|
276
352
|
* one call shape this reader specially recognizes, reading its `rows`/`columns` arguments and
|
|
@@ -305,12 +381,14 @@ function readComparison(node, consts, gridNames) {
|
|
|
305
381
|
const rowLabelProp = findProp(resolved, 'rowLabel');
|
|
306
382
|
const columnLabelProp = findProp(resolved, 'columnLabel');
|
|
307
383
|
const sizeProp = findProp(resolved, 'size');
|
|
384
|
+
const cellsProp = findProp(resolved, 'cells');
|
|
308
385
|
return {
|
|
309
386
|
rowLabel: rowLabelProp ? stringLiteral(resolve(rowLabelProp.initializer, consts, new Set())) : undefined,
|
|
310
387
|
columnLabel: columnLabelProp ? stringLiteral(resolve(columnLabelProp.initializer, consts, new Set())) : undefined,
|
|
311
388
|
rows,
|
|
312
389
|
columns,
|
|
313
390
|
size: sizeProp ? stringLiteral(resolve(sizeProp.initializer, consts, new Set())) : undefined,
|
|
391
|
+
cells: cellsProp ? readComparisonCells(cellsProp.initializer, consts) : undefined,
|
|
314
392
|
};
|
|
315
393
|
}
|
|
316
394
|
return undefined;
|
|
@@ -318,7 +396,7 @@ function readComparison(node, consts, gridNames) {
|
|
|
318
396
|
/** A literal array of list-item object literals — `key`/`name` required; `props` (read as a flat
|
|
319
397
|
* literal map, non-literal values simply omitted) and `group` optional. Each item's `node` field
|
|
320
398
|
* (always present, always JSX) is never inspected — reading the sibling fields never depends on it. */
|
|
321
|
-
function readListItems(node, consts) {
|
|
399
|
+
function readListItems(node, consts, componentName, sourceFile) {
|
|
322
400
|
const resolved = resolve(node, consts, new Set());
|
|
323
401
|
if (!ts.isArrayLiteralExpression(resolved))
|
|
324
402
|
return undefined;
|
|
@@ -333,10 +411,154 @@ function readListItems(node, consts) {
|
|
|
333
411
|
return undefined;
|
|
334
412
|
const group = propertyStringLiteral(resolvedElement, 'group', consts);
|
|
335
413
|
const propsProp = findProp(resolvedElement, 'props');
|
|
336
|
-
|
|
414
|
+
const fillProp = findProp(resolvedElement, 'fill');
|
|
415
|
+
const fill = fillProp ? literalBooleanOrUnknown(fillProp.initializer, consts) : undefined;
|
|
416
|
+
const surface = propertyStringLiteral(resolvedElement, 'surface', consts);
|
|
417
|
+
const align = propertyStringLiteral(resolvedElement, 'align', consts);
|
|
418
|
+
const nodeProp = findProp(resolvedElement, 'node');
|
|
419
|
+
const nodeExample = nodeProp ? readNodeExample(nodeProp.initializer, consts, componentName, sourceFile) : {};
|
|
420
|
+
items.push({
|
|
421
|
+
key, name, group, props: propsProp ? readLiteralProps(propsProp.initializer, consts) : undefined, fill, surface, align,
|
|
422
|
+
...nodeExample,
|
|
423
|
+
});
|
|
337
424
|
}
|
|
338
425
|
return items;
|
|
339
426
|
}
|
|
427
|
+
/** Generic, additive reading of one item's own `node:` JSX (design §3, plan Task 3) — used only by
|
|
428
|
+
* `cli/doctor.ts`'s interactive-preview advisory. Never evaluates `node`; only reads its literal
|
|
429
|
+
* AST shape. `node` matching `componentName` by plain tag-name text (not module resolution — this
|
|
430
|
+
* reader never resolves imports) means "a direct instance of the documented component": its own
|
|
431
|
+
* JSX attributes are read generically (`nodeAttributes`). Any other JSX tag is checked only for
|
|
432
|
+
* whether it's a confidently-identified, genuinely stateful same-file wrapper
|
|
433
|
+
* (`nodeStatefulWrapper`) — never asserted one way or the other when that can't be confirmed.
|
|
434
|
+
* Anything that isn't a direct JSX element at all (`null`, a conditional, a function call, …)
|
|
435
|
+
* leaves every field unset. */
|
|
436
|
+
function readNodeExample(node, consts, componentName, sourceFile) {
|
|
437
|
+
const resolved = resolve(node, consts, new Set());
|
|
438
|
+
let element;
|
|
439
|
+
if (ts.isJsxSelfClosingElement(resolved))
|
|
440
|
+
element = resolved;
|
|
441
|
+
else if (ts.isJsxElement(resolved))
|
|
442
|
+
element = resolved.openingElement;
|
|
443
|
+
if (!element || !ts.isIdentifier(element.tagName))
|
|
444
|
+
return {};
|
|
445
|
+
const tagText = element.tagName.text;
|
|
446
|
+
if (componentName !== undefined && tagText === componentName) {
|
|
447
|
+
const nodeAttributes = {};
|
|
448
|
+
for (const attribute of element.attributes.properties) {
|
|
449
|
+
// Skips a spread attribute ({...rest}) and a namespaced name (xml:lang) — unreadable,
|
|
450
|
+
// never a reason to reject the whole item.
|
|
451
|
+
if (!ts.isJsxAttribute(attribute) || !ts.isIdentifier(attribute.name))
|
|
452
|
+
continue;
|
|
453
|
+
nodeAttributes[attribute.name.text] = readJsxAttributeValue(attribute, consts);
|
|
454
|
+
}
|
|
455
|
+
return { nodeDirectComponent: tagText, nodeAttributes };
|
|
456
|
+
}
|
|
457
|
+
return { nodeStatefulWrapper: sameFileWrapperHasUseState(sourceFile, tagText) ? true : undefined };
|
|
458
|
+
}
|
|
459
|
+
/** One JSX attribute's value, as a `StaticAttributeValue`. A shorthand attribute (`disabled`, no
|
|
460
|
+
* `initializer` at all — equivalent to `disabled={true}`) is a literal `true`. */
|
|
461
|
+
function readJsxAttributeValue(attribute, consts) {
|
|
462
|
+
if (!attribute.initializer)
|
|
463
|
+
return { kind: 'literal', value: true };
|
|
464
|
+
if (ts.isStringLiteral(attribute.initializer))
|
|
465
|
+
return { kind: 'literal', value: attribute.initializer.text };
|
|
466
|
+
if (!ts.isJsxExpression(attribute.initializer) || !attribute.initializer.expression)
|
|
467
|
+
return { kind: 'unknown' };
|
|
468
|
+
const resolved = resolve(attribute.initializer.expression, consts, new Set());
|
|
469
|
+
if (ts.isStringLiteral(resolved) || ts.isNoSubstitutionTemplateLiteral(resolved))
|
|
470
|
+
return { kind: 'literal', value: resolved.text };
|
|
471
|
+
if (ts.isNumericLiteral(resolved))
|
|
472
|
+
return { kind: 'literal', value: Number(resolved.text) };
|
|
473
|
+
if (resolved.kind === ts.SyntaxKind.TrueKeyword)
|
|
474
|
+
return { kind: 'literal', value: true };
|
|
475
|
+
if (resolved.kind === ts.SyntaxKind.FalseKeyword)
|
|
476
|
+
return { kind: 'literal', value: false };
|
|
477
|
+
if (ts.isIdentifier(resolved) && resolved.text === 'undefined')
|
|
478
|
+
return { kind: 'no-op-callback' };
|
|
479
|
+
if ((ts.isArrowFunction(resolved) || ts.isFunctionExpression(resolved)) && isEmptyFunctionBody(resolved.body)) {
|
|
480
|
+
return { kind: 'no-op-callback' };
|
|
481
|
+
}
|
|
482
|
+
return { kind: 'unknown' };
|
|
483
|
+
}
|
|
484
|
+
function isEmptyFunctionBody(body) {
|
|
485
|
+
return ts.isBlock(body) && body.statements.length === 0;
|
|
486
|
+
}
|
|
487
|
+
/** True only when `sourceFile` declares a top-level function (declaration, or a `const`/`let`
|
|
488
|
+
* initialized with a function/arrow expression) named `tagName` whose own body contains a call to
|
|
489
|
+
* a *source-backed* React `useState`/`React.useState` — a confidently-identified, genuinely
|
|
490
|
+
* stateful wrapper. Walking only that one function's own body (never the whole file) keeps this
|
|
491
|
+
* scoped the same way `cli/props.ts`'s composedOfCandidate reader scopes its own JSX walk to one
|
|
492
|
+
* implementation. */
|
|
493
|
+
function sameFileWrapperHasUseState(sourceFile, tagName) {
|
|
494
|
+
const bindings = reactUseStateBindings(sourceFile);
|
|
495
|
+
for (const statement of sourceFile.statements) {
|
|
496
|
+
let body;
|
|
497
|
+
if (ts.isFunctionDeclaration(statement) && statement.name?.text === tagName)
|
|
498
|
+
body = statement.body;
|
|
499
|
+
else if (ts.isVariableStatement(statement)) {
|
|
500
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
501
|
+
if (ts.isIdentifier(declaration.name) && declaration.name.text === tagName && declaration.initializer &&
|
|
502
|
+
(ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) {
|
|
503
|
+
body = declaration.initializer.body;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (body && containsUseStateCall(body, bindings))
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
/** This file's own top-level `import ... from 'react'` bindings relevant to recognizing a genuine
|
|
513
|
+
* `useState` call — never a type-checker resolution, only the same-file import syntax, matching
|
|
514
|
+
* this reader's existing "never resolves imports across files" contract. `namedUseState` holds
|
|
515
|
+
* every local name bound to the real named `useState` export (an alias, e.g. `import { useState as
|
|
516
|
+
* useReactState }`, included); `reactNamespace` holds every local name bound to the whole `react`
|
|
517
|
+
* module (a default or `* as` import) that a `React.useState(...)` call could be written through.
|
|
518
|
+
* A locally-defined `useState` (no matching import) or an unrelated object's own `.useState` method
|
|
519
|
+
* binds to neither set and so is never counted — left uncheckable, never asserted stateful. */
|
|
520
|
+
function reactUseStateBindings(sourceFile) {
|
|
521
|
+
const namedUseState = new Set();
|
|
522
|
+
const reactNamespace = new Set();
|
|
523
|
+
for (const statement of sourceFile.statements) {
|
|
524
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== 'react')
|
|
525
|
+
continue;
|
|
526
|
+
const clause = statement.importClause;
|
|
527
|
+
if (!clause)
|
|
528
|
+
continue;
|
|
529
|
+
if (clause.name)
|
|
530
|
+
reactNamespace.add(clause.name.text);
|
|
531
|
+
if (clause.namedBindings && ts.isNamespaceImport(clause.namedBindings))
|
|
532
|
+
reactNamespace.add(clause.namedBindings.name.text);
|
|
533
|
+
else if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
|
534
|
+
for (const element of clause.namedBindings.elements) {
|
|
535
|
+
if ((element.propertyName ?? element.name).text === 'useState')
|
|
536
|
+
namedUseState.add(element.name.text);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return { namedUseState, reactNamespace };
|
|
541
|
+
}
|
|
542
|
+
function containsUseStateCall(node, bindings) {
|
|
543
|
+
let found = false;
|
|
544
|
+
const visit = (n) => {
|
|
545
|
+
if (found)
|
|
546
|
+
return;
|
|
547
|
+
if (ts.isCallExpression(n)) {
|
|
548
|
+
const callee = n.expression;
|
|
549
|
+
const isNamedUseState = ts.isIdentifier(callee) && bindings.namedUseState.has(callee.text);
|
|
550
|
+
const isReactNamespaceUseState = ts.isPropertyAccessExpression(callee) && callee.name.text === 'useState' &&
|
|
551
|
+
ts.isIdentifier(callee.expression) && bindings.reactNamespace.has(callee.expression.text);
|
|
552
|
+
if (isNamedUseState || isReactNamespaceUseState) {
|
|
553
|
+
found = true;
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
ts.forEachChild(n, visit);
|
|
558
|
+
};
|
|
559
|
+
visit(node);
|
|
560
|
+
return found;
|
|
561
|
+
}
|
|
340
562
|
function readLiteralProps(node, consts) {
|
|
341
563
|
const resolved = resolve(node, consts, new Set());
|
|
342
564
|
if (!ts.isObjectLiteralExpression(resolved))
|
|
@@ -365,14 +587,14 @@ function readLiteralProps(node, consts) {
|
|
|
365
587
|
* literal — like `previewWidths`/`fullWidthLabel`, it isn't in the original literal-checkable
|
|
366
588
|
* list, so a non-literal `itemsFill` is simply left unset. `maxColumns` does affect the geometry
|
|
367
589
|
* `explain` reports, so it must be a literal integer from 1–5 or the page is not checkable. */
|
|
368
|
-
function readSlotItems(prop, consts) {
|
|
590
|
+
function readSlotItems(prop, consts, componentName, sourceFile) {
|
|
369
591
|
if (!prop)
|
|
370
592
|
return { checkable: true };
|
|
371
593
|
const resolved = resolve(prop.initializer, consts, new Set());
|
|
372
594
|
const itemsProp = ts.isObjectLiteralExpression(resolved) ? findProp(resolved, 'items') : undefined;
|
|
373
595
|
if (!itemsProp)
|
|
374
596
|
return { checkable: false };
|
|
375
|
-
const items = readListItems(itemsProp.initializer, consts);
|
|
597
|
+
const items = readListItems(itemsProp.initializer, consts, componentName, sourceFile);
|
|
376
598
|
if (!items)
|
|
377
599
|
return { checkable: false };
|
|
378
600
|
const itemsFillProp = ts.isObjectLiteralExpression(resolved) ? findProp(resolved, 'itemsFill') : undefined;
|
package/dist/cli/updateCheck.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// cli/updateCheck.ts
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { compareVersions, isBreakingUpgrade, parseVersion } from "./semver.js";
|
|
5
5
|
const CACHE_FILE_NAME = 'update-check.json';
|
|
@@ -79,59 +79,163 @@ export async function fetchWithTimeout(url, fetchImpl, timeoutMs) {
|
|
|
79
79
|
clearTimeout(timer);
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
+
/** One update check, with whether npm actually answered — `checkForUpdate` alone can't tell "no
|
|
83
|
+
* newer version" from "couldn't reach npm", which the update page must say differently. Never
|
|
84
|
+
* throws. `cached` is the cache as it stands afterwards (the last successful check on this
|
|
85
|
+
* computer), when there is one. */
|
|
86
|
+
export async function runUpdateCheck(currentVersion, options = {}) {
|
|
87
|
+
const now = options.now ?? Date.now;
|
|
88
|
+
const cacheDir = options.cacheDir ?? cacheDirFor(process.platform, process.env, process.env.HOME ?? process.env.USERPROFILE ?? '');
|
|
89
|
+
const cached = readCache(cacheDir);
|
|
90
|
+
// `parseVersion` (inside `toResultOrUndefined`) throws on a non-semver `latest`, whether from a
|
|
91
|
+
// malformed cache file or an unexpected registry body; this function must never throw.
|
|
92
|
+
const safeResult = (entry) => {
|
|
93
|
+
try {
|
|
94
|
+
return toResultOrUndefined(entry, currentVersion);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
if (cached && !options.force && isCacheFresh(cached, now()))
|
|
101
|
+
return { result: safeResult(cached), reached: true, cached };
|
|
102
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
103
|
+
const timeoutMs = options.timeoutMs ?? 900;
|
|
104
|
+
let latest;
|
|
105
|
+
try {
|
|
106
|
+
const registryResponse = (await fetchWithTimeout(REGISTRY_URL, fetchImpl, timeoutMs));
|
|
107
|
+
latest = registryResponse.version;
|
|
108
|
+
parseVersion(latest);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// A stale cache is still better than nothing once offline — still run through the same
|
|
112
|
+
// not-actually-newer guard, never a raw passthrough.
|
|
113
|
+
return { result: cached ? safeResult(cached) : undefined, reached: false, cached };
|
|
114
|
+
}
|
|
115
|
+
let summary = [];
|
|
116
|
+
let releasedAt;
|
|
117
|
+
try {
|
|
118
|
+
const release = (await fetchWithTimeout(`https://api.github.com/repos/${GITHUB_REPO}/releases/tags/v${latest}`, fetchImpl, timeoutMs));
|
|
119
|
+
summary = release.body ? extractSummaryBullets(release.body) : [];
|
|
120
|
+
releasedAt = release.published_at;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// The version comparison is the half that matters for the footer/banner; a missing release
|
|
124
|
+
// summary (private repo hiccup, a tag pushed slightly before its release note) degrades to an
|
|
125
|
+
// empty "What's new" list, never a failed check.
|
|
126
|
+
}
|
|
127
|
+
const fresh = { latest, summary, releasedAt, checkedAt: new Date(now()).toISOString() };
|
|
128
|
+
// A registry that (briefly, during its own propagation) reports an older "latest" than what is
|
|
129
|
+
// already cached never regresses the cache's version, but the check time still moves forward.
|
|
130
|
+
let stored = fresh;
|
|
131
|
+
try {
|
|
132
|
+
if (cached && compareVersions(parseVersion(fresh.latest), parseVersion(cached.latest)) < 0)
|
|
133
|
+
stored = { ...cached, checkedAt: fresh.checkedAt };
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// An unreadable cached version is simply replaced.
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
writeCache(cacheDir, stored);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// A read-only cache folder must not turn a successful check into a failure.
|
|
143
|
+
}
|
|
144
|
+
return { result: safeResult(fresh), reached: true, cached: stored };
|
|
145
|
+
}
|
|
82
146
|
/** Design §5 "Update check" in full. Never throws — every failure (a stale/missing cache with no
|
|
83
147
|
* reachable registry, a GitHub release that 404s, a request that times out) resolves to either a
|
|
84
148
|
* usable result or `undefined`, so every caller can treat "no update known" as the one, uniform
|
|
85
149
|
* outcome of being offline, blocked, or genuinely up to date with nothing cached yet. */
|
|
86
150
|
export async function checkForUpdate(currentVersion, options = {}) {
|
|
87
|
-
// M5 (Minor, Fable correction pass): this function is documented — and relied on by `dev()`,
|
|
88
|
-
// which `await`s it before starting anything else — as never throwing. `parseVersion` (inside
|
|
89
|
-
// `toResultOrUndefined`) throws on a non-semver `latest`, whether from a parseable-but-malformed
|
|
90
|
-
// cache file or an unexpected 200 body with no real `version`; either would otherwise stop `dev`
|
|
91
|
-
// from starting at all.
|
|
92
151
|
try {
|
|
93
|
-
|
|
94
|
-
const cacheDir = options.cacheDir ?? cacheDirFor(process.platform, process.env, process.env.HOME ?? process.env.USERPROFILE ?? '');
|
|
95
|
-
const cached = readCache(cacheDir);
|
|
96
|
-
if (cached && isCacheFresh(cached, now()))
|
|
97
|
-
return toResultOrUndefined(cached, currentVersion);
|
|
98
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
99
|
-
const timeoutMs = options.timeoutMs ?? 900;
|
|
100
|
-
let latest;
|
|
101
|
-
try {
|
|
102
|
-
const registryResponse = (await fetchWithTimeout(REGISTRY_URL, fetchImpl, timeoutMs));
|
|
103
|
-
latest = registryResponse.version;
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
// A stale cache is still better than nothing once offline — still run through the same
|
|
107
|
-
// not-actually-newer guard, never a raw passthrough.
|
|
108
|
-
return cached ? toResultOrUndefined(cached, currentVersion) : undefined;
|
|
109
|
-
}
|
|
110
|
-
let summary = [];
|
|
111
|
-
let releasedAt;
|
|
112
|
-
try {
|
|
113
|
-
const release = (await fetchWithTimeout(`https://api.github.com/repos/${GITHUB_REPO}/releases/tags/v${latest}`, fetchImpl, timeoutMs));
|
|
114
|
-
summary = release.body ? extractSummaryBullets(release.body) : [];
|
|
115
|
-
releasedAt = release.published_at;
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
// The version comparison is the half that matters for the footer/banner; a missing release
|
|
119
|
-
// summary (private repo hiccup, a tag pushed slightly before its release note) degrades to an
|
|
120
|
-
// empty "What's new" list, never a failed check.
|
|
121
|
-
}
|
|
122
|
-
const cachedResult = { latest, summary, releasedAt, checkedAt: new Date(now()).toISOString() };
|
|
123
|
-
// compareVersions is used only to decide *whether* this is worth caching as "the latest" at all
|
|
124
|
-
// — a registry that (briefly, during its own propagation) reports an older "latest" than what
|
|
125
|
-
// is already cached never regresses the cache.
|
|
126
|
-
if (!cached || compareVersions(parseVersion(cachedResult.latest), parseVersion(cached.latest)) >= 0) {
|
|
127
|
-
writeCache(cacheDir, cachedResult);
|
|
128
|
-
}
|
|
129
|
-
return toResultOrUndefined(cachedResult, currentVersion);
|
|
152
|
+
return (await runUpdateCheck(currentVersion, options)).result;
|
|
130
153
|
}
|
|
131
154
|
catch {
|
|
132
155
|
return undefined;
|
|
133
156
|
}
|
|
134
157
|
}
|
|
158
|
+
/** A settings folder, not the cache folder (which the OS may clear): the personal switch must
|
|
159
|
+
* survive. */
|
|
160
|
+
export function preferencesDirFor(platform, env, homedir) {
|
|
161
|
+
if (platform === 'darwin')
|
|
162
|
+
return path.posix.join(homedir, 'Library', 'Application Support', 'ds-viewer');
|
|
163
|
+
if (platform === 'win32')
|
|
164
|
+
return path.win32.join(env.APPDATA ?? path.win32.join(homedir, 'AppData', 'Roaming'), 'ds-viewer');
|
|
165
|
+
return path.posix.join(env.XDG_CONFIG_HOME ?? path.posix.join(homedir, '.config'), 'ds-viewer');
|
|
166
|
+
}
|
|
167
|
+
export function defaultPreferencesDir() {
|
|
168
|
+
return preferencesDirFor(process.platform, process.env, process.env.HOME ?? process.env.USERPROFILE ?? '');
|
|
169
|
+
}
|
|
170
|
+
const PREFERENCES_FILE_NAME = 'preferences.json';
|
|
171
|
+
function readPreferences(dir) {
|
|
172
|
+
try {
|
|
173
|
+
const parsed = JSON.parse(readFileSync(path.join(dir, PREFERENCES_FILE_NAME), 'utf8'));
|
|
174
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return {};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** Your own "check automatically" switch for one project on this computer, or `undefined` when you
|
|
181
|
+
* haven't set it (the project config then decides). */
|
|
182
|
+
export function readPersonalAutoCheck(dir, projectRoot) {
|
|
183
|
+
const value = readPreferences(dir).projects?.[path.resolve(projectRoot)]?.autoCheck;
|
|
184
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
185
|
+
}
|
|
186
|
+
export function writePersonalAutoCheck(dir, projectRoot, enabled) {
|
|
187
|
+
const prefs = readPreferences(dir);
|
|
188
|
+
const projects = { ...(prefs.projects ?? {}) };
|
|
189
|
+
projects[path.resolve(projectRoot)] = { ...projects[path.resolve(projectRoot)], autoCheck: enabled };
|
|
190
|
+
mkdirSync(dir, { recursive: true });
|
|
191
|
+
// Write then rename, so a crash mid-write never leaves a half file that would read as "no
|
|
192
|
+
// settings" and silently reset every project's switch.
|
|
193
|
+
const target = path.join(dir, PREFERENCES_FILE_NAME);
|
|
194
|
+
const temp = `${target}.${process.pid}.tmp`;
|
|
195
|
+
writeFileSync(temp, `${JSON.stringify({ ...prefs, projects }, null, 2)}\n`);
|
|
196
|
+
renameSync(temp, target);
|
|
197
|
+
}
|
|
198
|
+
/** Which setting decides automatic checks, strongest first: CI's environment variable, your own
|
|
199
|
+
* switch, the project config, then on by default. */
|
|
200
|
+
export function resolveAutoCheck(config, env, personal) {
|
|
201
|
+
if (env.DS_VIEWER_NO_UPDATE_CHECK === '1')
|
|
202
|
+
return { enabled: false, source: 'env' };
|
|
203
|
+
if (personal !== undefined)
|
|
204
|
+
return { enabled: personal, source: 'personal' };
|
|
205
|
+
if (config.updateCheck !== undefined)
|
|
206
|
+
return { enabled: config.updateCheck, source: 'project' };
|
|
207
|
+
return { enabled: true, source: 'default' };
|
|
208
|
+
}
|
|
209
|
+
function toNotice(result) {
|
|
210
|
+
if (!result)
|
|
211
|
+
return null;
|
|
212
|
+
return { current: result.current, latest: result.latest, breaking: result.breaking, summary: result.summary, releasedAt: result.releasedAt };
|
|
213
|
+
}
|
|
214
|
+
/** The version status `dev` starts with: runs the (cached) check only when automatic checks are on. */
|
|
215
|
+
export async function buildVersionStatus(currentVersion, autoCheck, options = {}) {
|
|
216
|
+
if (!autoCheck.enabled) {
|
|
217
|
+
const cacheDir = options.cacheDir ?? cacheDirFor(process.platform, process.env, process.env.HOME ?? process.env.USERPROFILE ?? '');
|
|
218
|
+
return { current: currentVersion, autoCheck, lastCheckedAt: readCache(cacheDir)?.checkedAt, lastOutcome: 'not-run', update: null };
|
|
219
|
+
}
|
|
220
|
+
return statusFromCheck(currentVersion, autoCheck, await runUpdateCheck(currentVersion, options));
|
|
221
|
+
}
|
|
222
|
+
/** Folds one check's outcome into a status (startup, or **Check now**). */
|
|
223
|
+
export function statusFromCheck(currentVersion, autoCheck, outcome) {
|
|
224
|
+
return {
|
|
225
|
+
current: currentVersion,
|
|
226
|
+
autoCheck,
|
|
227
|
+
lastCheckedAt: outcome.cached?.checkedAt,
|
|
228
|
+
lastOutcome: outcome.reached ? 'ok' : 'unreachable',
|
|
229
|
+
update: toNotice(outcome.result),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/** `dev` writes this next to `update.json`; the generated entry file imports it. */
|
|
233
|
+
export function writeVersionFile(projectRoot, status) {
|
|
234
|
+
const file = path.join(projectRoot, '.ds-viewer', 'version.json');
|
|
235
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
236
|
+
writeFileSync(file, JSON.stringify(status));
|
|
237
|
+
return file;
|
|
238
|
+
}
|
|
135
239
|
/** Design §5: "`dev` writes the result to `.ds-viewer/update.json`; the viewer reads it." Always
|
|
136
240
|
* writes something — `null` when there is no result — so the generated entry file (Task 17) can
|
|
137
241
|
* unconditionally `import updateNotice from './update.json'` without ever hitting a missing file. */
|
|
@@ -141,3 +245,30 @@ export function writeUpdateFile(projectRoot, result) {
|
|
|
141
245
|
writeFileSync(file, JSON.stringify(result ?? null));
|
|
142
246
|
return file;
|
|
143
247
|
}
|
|
248
|
+
/** The live version status behind the local endpoint (`GET /version`, **Check now**, the switch),
|
|
249
|
+
* kept outside `dev()` so its ordering rules are testable. */
|
|
250
|
+
export function createVersionController(deps) {
|
|
251
|
+
let status = deps.initial;
|
|
252
|
+
const resolveNow = () => resolveAutoCheck(deps.getConfig(), deps.env, readPersonalAutoCheck(deps.prefsDir, deps.projectRoot));
|
|
253
|
+
return {
|
|
254
|
+
getVersionStatus: () => status,
|
|
255
|
+
/** **Check now**: asks npm ignoring the cache. Reads the switch *after* the check, so a change
|
|
256
|
+
* made while it ran is kept. */
|
|
257
|
+
checkNow: async () => {
|
|
258
|
+
const outcome = await deps.runCheck();
|
|
259
|
+
status = statusFromCheck(deps.ownVersion, status.autoCheck, outcome);
|
|
260
|
+
return status;
|
|
261
|
+
},
|
|
262
|
+
/** The switch: your own setting for this project on this computer. Takes effect for automatic
|
|
263
|
+
* checks from the next start; the current status keeps any update already found. */
|
|
264
|
+
setAutoCheck: (enabled) => {
|
|
265
|
+
writePersonalAutoCheck(deps.prefsDir, deps.projectRoot, enabled);
|
|
266
|
+
status = { ...status, autoCheck: resolveNow() };
|
|
267
|
+
return status;
|
|
268
|
+
},
|
|
269
|
+
/** After `ds-viewer.config.ts` changes while `dev` runs. */
|
|
270
|
+
refreshAutoCheck: () => {
|
|
271
|
+
status = { ...status, autoCheck: resolveNow() };
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
package/dist/cli/workspace.js
CHANGED
|
@@ -65,13 +65,14 @@ function entryTsx(name, hasSafeArea, groupOrder, logoRequirePath, updateEndpoint
|
|
|
65
65
|
// (no reserved words in either key) — the same reasoning the existing `logoRequirePath` baking
|
|
66
66
|
// already relies on, extended to a whole object instead of one string.
|
|
67
67
|
const endpointConst = `const updateEndpoint = ${updateEndpoint ? JSON.stringify(updateEndpoint) : 'undefined'};\n`;
|
|
68
|
-
const viewer = `<CatalogShell appName={${JSON.stringify(name)}} title="Component Catalog" groups={groups} sections={sections}${logoProp} update={updateNotice} updateEndpoint={updateEndpoint} />`;
|
|
68
|
+
const viewer = `<CatalogShell appName={${JSON.stringify(name)}} title="Component Catalog" groups={groups} sections={sections}${logoProp} update={updateNotice} versionStatus={versionStatus} updateEndpoint={updateEndpoint} />`;
|
|
69
69
|
const root = hasSafeArea ? `<SafeAreaProvider>${viewer}</SafeAreaProvider>` : viewer;
|
|
70
70
|
return `import { registerRootComponent } from 'expo';
|
|
71
71
|
import { CatalogShell, buildCatalogSections } from '@krapwoo/ds-viewer';
|
|
72
72
|
${safeAreaImport}${logoImport}import pages from './generated/pages';
|
|
73
73
|
import components from './generated/components.json';
|
|
74
74
|
import updateNotice from './update.json';
|
|
75
|
+
import versionStatus from './version.json';
|
|
75
76
|
${endpointConst}
|
|
76
77
|
function App() {
|
|
77
78
|
const { sections, groups } = buildCatalogSections(pages, components, ${JSON.stringify(groupOrder)});
|