@krapwoo/ds-viewer 0.2.0 → 0.4.0
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 +151 -0
- package/config/index.ts +4 -2
- package/dist/cli/agentsFile.js +31 -0
- package/dist/cli/dev.js +312 -15
- package/dist/cli/doctor.js +502 -0
- package/dist/cli/endpoint.js +142 -0
- package/dist/cli/explain.js +151 -0
- package/dist/cli/githubAction.js +65 -0
- package/dist/cli/init.js +9 -5
- package/dist/cli/kitDiff.js +132 -0
- package/dist/cli/kitRoot.js +20 -0
- package/dist/cli/main.js +123 -15
- package/dist/cli/migrate.js +30 -0
- package/dist/cli/migrateEntry.js +19 -0
- package/dist/cli/migrations/index.js +14 -0
- package/dist/cli/migrations/types.js +9 -0
- package/dist/cli/packageManager.js +29 -0
- package/dist/cli/packageVersion.js +24 -0
- package/dist/cli/pageIndex.js +27 -1
- package/dist/cli/props.js +4 -3
- package/dist/cli/semver.js +26 -0
- package/dist/cli/staticPage.js +356 -0
- package/dist/cli/sync.js +11 -4
- package/dist/cli/update.js +93 -0
- package/dist/cli/updateCheck.js +143 -0
- package/dist/cli/updatePlan.js +202 -0
- package/dist/cli/workspace.js +10 -5
- package/dist/native/catalog/catalogNavigation.js +129 -0
- package/dist/native/catalog/comparison.js +283 -0
- package/dist/native/catalog/tokens.js +134 -0
- package/dist/native/catalog/types.js +1 -0
- package/native/catalog/CatalogShell.tsx +105 -11
- package/native/catalog/CatalogSidebar.tsx +58 -2
- package/native/catalog/ComparisonGrid.tsx +9 -7
- package/native/catalog/SectionBlock.tsx +5 -0
- package/native/catalog/SpacingScaleGallery.tsx +1 -1
- package/native/catalog/UpdatePanel.tsx +556 -0
- package/native/catalog/catalogNavigation.ts +70 -1
- package/native/catalog/comparison.ts +63 -5
- package/native/catalog/index.ts +5 -1
- package/native/catalog/pageApi.ts +11 -2
- package/native/catalog/tokens.ts +15 -0
- package/native/catalog/types.ts +19 -5
- package/native/catalog/updatePanelState.ts +74 -0
- package/package.json +5 -1
- package/starter-kit/components/Avatar/Avatar.catalog.tsx +4 -22
- package/starter-kit/components/Badge/Badge.catalog.tsx +15 -23
- package/starter-kit/components/Banner/Banner.tsx +1 -1
- package/starter-kit/components/BottomSheet/BottomSheet.tsx +2 -2
- package/starter-kit/components/Button/Button.catalog.tsx +15 -29
- package/starter-kit/components/Dialog/Dialog.tsx +2 -2
- package/starter-kit/components/Pill/Pill.catalog.tsx +12 -26
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// cli/migrations/index.ts
|
|
2
|
+
import { compareVersions, parseVersion } from "../semver.js";
|
|
3
|
+
/** The real, production migration registry. Empty in 0.4 — design §5 "Versioning": "From 0.4,
|
|
4
|
+
* every breaking change ships an automatic migration" describes the *mechanism* this plan builds,
|
|
5
|
+
* not a requirement to invent a breaking change 0.4 does not have. The framework itself is proven
|
|
6
|
+
* by a test-only fixture migration (`cli/__tests__/migrate.test.ts`) that is never added here. */
|
|
7
|
+
export const MIGRATIONS = [];
|
|
8
|
+
/** Every migration after `fromVersion`, oldest first. */
|
|
9
|
+
export function migrationsAfter(fromVersion) {
|
|
10
|
+
const from = parseVersion(fromVersion);
|
|
11
|
+
return MIGRATIONS
|
|
12
|
+
.filter((migration) => compareVersions(parseVersion(migration.version), from) > 0)
|
|
13
|
+
.sort((a, b) => compareVersions(parseVersion(a.version), parseVersion(b.version)));
|
|
14
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// cli/migrations/types.ts
|
|
2
|
+
//
|
|
3
|
+
// This file, and every file `cli/migrateEntry.ts` transitively imports through `Migration` values
|
|
4
|
+
// placed in `cli/migrations/index.ts` (`migrate.ts`, `migrations/index.ts`, `semver.ts`), must
|
|
5
|
+
// never import the `typescript` npm package (directly or transitively) — `migrateEntry.ts` is the
|
|
6
|
+
// one thing this plan runs from inside an `npm pack`-extracted tarball that has no `node_modules`
|
|
7
|
+
// of its own, and `typescript` is a real `dependencies` entry (Task 9's own `buildUpdatePlan`,
|
|
8
|
+
// spiked for real in `spikes/migrate-entry/`).
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// cli/packageManager.ts
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
/** The same lockfile-sniffing `cli/githubAction.ts`'s own `installStep` already did inline (0.3)
|
|
5
|
+
* — promoted here so `cli/update.ts` (Task 10) can detect the same thing without duplicating it.
|
|
6
|
+
* A v1 yarn.lock's own first line is always exactly `# yarn lockfile v1`; anything else (Berry's
|
|
7
|
+
* own header/`__metadata`) is Berry. */
|
|
8
|
+
export function detectPackageManager(projectRoot) {
|
|
9
|
+
if (existsSync(path.join(projectRoot, 'pnpm-lock.yaml')))
|
|
10
|
+
return 'pnpm';
|
|
11
|
+
const yarnLockPath = path.join(projectRoot, 'yarn.lock');
|
|
12
|
+
if (existsSync(yarnLockPath)) {
|
|
13
|
+
const firstLine = readFileSync(yarnLockPath, 'utf8').split('\n', 1)[0];
|
|
14
|
+
return firstLine === '# yarn lockfile v1' ? 'yarn-classic' : 'yarn-berry';
|
|
15
|
+
}
|
|
16
|
+
return 'npm'; // package-lock.json, or no lockfile at all (npm's own documented default either way).
|
|
17
|
+
}
|
|
18
|
+
/** The one command `cli/update.ts`'s "Upgrades with the project's package manager" step (design
|
|
19
|
+
* §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) {
|
|
23
|
+
const spec = `${pkg}@${version}`;
|
|
24
|
+
if (pm === 'npm')
|
|
25
|
+
return { command: 'npm', args: ['install', '--save-dev', spec] };
|
|
26
|
+
if (pm === 'pnpm')
|
|
27
|
+
return { command: 'pnpm', args: ['add', '--save-dev', spec] };
|
|
28
|
+
return { command: 'yarn', args: ['add', '--dev', spec] };
|
|
29
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// cli/packageVersion.ts
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
/** Walks up from `startDir` looking for the `package.json` named `@krapwoo/ds-viewer` — needed
|
|
5
|
+
* because this file sits one level under the repo root as `cli/packageVersion.ts` (how tests and
|
|
6
|
+
* `init`/`main` run it from source) but two levels under it once compiled, as
|
|
7
|
+
* `dist/cli/packageVersion.js`. Throws when it reaches the filesystem root without finding one,
|
|
8
|
+
* which should never happen for a real install (`readOwnVersion` is only ever called from code
|
|
9
|
+
* that is itself inside the installed/running package). */
|
|
10
|
+
export function readOwnVersion(startDir) {
|
|
11
|
+
let dir = startDir;
|
|
12
|
+
while (true) {
|
|
13
|
+
const candidate = path.join(dir, 'package.json');
|
|
14
|
+
if (existsSync(candidate)) {
|
|
15
|
+
const pkg = JSON.parse(readFileSync(candidate, 'utf8'));
|
|
16
|
+
if (pkg.name === '@krapwoo/ds-viewer' && pkg.version)
|
|
17
|
+
return pkg.version;
|
|
18
|
+
}
|
|
19
|
+
const parent = path.dirname(dir);
|
|
20
|
+
if (parent === dir)
|
|
21
|
+
throw new Error("Could not find @krapwoo/ds-viewer's own package.json to read its version.");
|
|
22
|
+
dir = parent;
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/cli/pageIndex.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { resolveGlob } from "./glob.js";
|
|
4
4
|
/** Component pages: `<Export>.catalog.tsx` beside the component's entry file (design §2). A
|
|
@@ -10,6 +10,25 @@ export function discoverPages(input) {
|
|
|
10
10
|
const standalonePages = input.standalonePageGlobs.flatMap((pattern) => resolveGlob(input.projectRoot, pattern));
|
|
11
11
|
return [...componentPages, ...standalonePages].sort();
|
|
12
12
|
}
|
|
13
|
+
/** Every `*.catalog.tsx`/`.catalog.ts` file `doctor` should read — unlike `discoverPages`, this
|
|
14
|
+
* also finds a page beside a component whose folder still exists but whose export was renamed or
|
|
15
|
+
* removed (that page's file never changed, so it's still sitting right there). `componentFolders`
|
|
16
|
+
* is every distinct folder containing a currently-resolved component entry file — `cli/doctor.ts`
|
|
17
|
+
* derives it the same way `cli/sync.ts` does, by resolving `config.components` and taking each
|
|
18
|
+
* match's `path.dirname`. A folder that was deleted entirely (not just renamed within) has nothing
|
|
19
|
+
* left to find here, which is correct: there's no stale page file left on disk to warn about. */
|
|
20
|
+
export function discoverAllPageFiles(input) {
|
|
21
|
+
const componentPages = input.componentFolders.flatMap((folder) => {
|
|
22
|
+
try {
|
|
23
|
+
return readdirSync(folder).filter((name) => /\.catalog\.tsx?$/.test(name)).map((name) => path.join(folder, name));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
const standalonePages = input.standalonePageGlobs.flatMap((pattern) => resolveGlob(input.projectRoot, pattern));
|
|
30
|
+
return [...new Set([...componentPages, ...standalonePages])].sort();
|
|
31
|
+
}
|
|
13
32
|
/** Writes `.ds-viewer/generated/pages.ts`: imports every discovered page's default export,
|
|
14
33
|
* resolves each one's id (design §2 "Page id": `id ?? component ?? <file stem>` — a component
|
|
15
34
|
* page defaults to its export name, a standalone page to its own file's stem) and `file` (the
|
|
@@ -25,6 +44,13 @@ export function writePageIndex(generatedDir, pageFiles) {
|
|
|
25
44
|
writeFileSync(outFile, `${importLines.join('\n')}\n\n${resolveLines.join('\n')}\n\n${exportLine}\n`);
|
|
26
45
|
return outFile;
|
|
27
46
|
}
|
|
47
|
+
/** `page.id ?? page.component ?? <file's own stem>` (design §2 "Page id") — factored out of
|
|
48
|
+
* `writePageIndex`'s own inline logic above so `cli/doctor.ts` resolves a page's id exactly the
|
|
49
|
+
* same way, never a second, drifting copy of this rule. Takes the same two fields `StaticPage`
|
|
50
|
+
* (Task 3) and the generated page index both already carry. */
|
|
51
|
+
export function resolvePageId(page, file) {
|
|
52
|
+
return page.id ?? page.component ?? fileStem(file);
|
|
53
|
+
}
|
|
28
54
|
function toImportPath(fromDir, toFile) {
|
|
29
55
|
const withoutExt = toFile.replace(/\.tsx?$/, '');
|
|
30
56
|
const relativePath = path.relative(fromDir, withoutExt).split(path.sep).join('/');
|
package/dist/cli/props.js
CHANGED
|
@@ -11,15 +11,16 @@ const COMPONENT_TYPE_HINT = /MemoExoticComponent|ForwardRefExoticComponent|Named
|
|
|
11
11
|
* fixtures have no tsconfig of their own, so lookup reaches this repository's root
|
|
12
12
|
* `tsconfig.json` (Task 1); it declares no `paths`, `baseUrl`, or `jsx`, so `options.paths`
|
|
13
13
|
* alone decides resolution in tests. */
|
|
14
|
-
function readHostTsconfigOptions(entryFile) {
|
|
14
|
+
export function readHostTsconfigOptions(entryFile) {
|
|
15
15
|
const tsconfigPath = ts.findConfigFile(path.dirname(entryFile), ts.sys.fileExists);
|
|
16
16
|
if (!tsconfigPath)
|
|
17
|
-
return {};
|
|
17
|
+
return { options: {}, declarationFiles: [] };
|
|
18
18
|
const { config } = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
|
|
19
19
|
const parsed = ts.parseJsonConfigFileContent(config, ts.sys, path.dirname(tsconfigPath));
|
|
20
20
|
const { baseUrl, paths, jsx } = parsed.options;
|
|
21
21
|
const pathsBasePath = parsed.options.pathsBasePath;
|
|
22
|
-
|
|
22
|
+
const declarationFiles = parsed.fileNames.filter((file) => file.endsWith('.d.ts'));
|
|
23
|
+
return { baseUrl, paths, jsx, pathsBasePath, options: parsed.options, declarationFiles };
|
|
23
24
|
}
|
|
24
25
|
/** What is a component (design §3): an export whose type has a call signature returning
|
|
25
26
|
* JSX/ReactNode, or is typed as a Memo/ForwardRef/NamedExotic/ComponentType wrapper.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// cli/semver.ts
|
|
2
|
+
const VERSION_RE = /^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/;
|
|
3
|
+
export function parseVersion(raw) {
|
|
4
|
+
const match = VERSION_RE.exec(raw.trim());
|
|
5
|
+
if (!match)
|
|
6
|
+
throw new Error(`Not a version: "${raw}"`);
|
|
7
|
+
const [, major, minor, patch, prerelease] = match;
|
|
8
|
+
return { major: Number(major), minor: Number(minor), patch: Number(patch), prerelease };
|
|
9
|
+
}
|
|
10
|
+
/** -1 when `a` is older than `b`, 1 when newer, 0 when equal — prerelease tags are ignored for
|
|
11
|
+
* ordering (this package only ever compares a real installed version against a real registry
|
|
12
|
+
* "latest", which is never a prerelease; the throwaway spike tag above is test-only). */
|
|
13
|
+
export function compareVersions(a, b) {
|
|
14
|
+
for (const key of ['major', 'minor', 'patch']) {
|
|
15
|
+
if (a[key] !== b[key])
|
|
16
|
+
return a[key] < b[key] ? -1 : 1;
|
|
17
|
+
}
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
/** Design §5 "Releasing" — "From 1.0, breaking changes only in major versions"; before that, this
|
|
21
|
+
* plan still needs one consistent rule to decide the banner vs. the quiet footer line (design
|
|
22
|
+
* §5 "Viewer notice"), and a major-version bump is the only rule that needs no per-release
|
|
23
|
+
* judgment call. */
|
|
24
|
+
export function isBreakingUpgrade(current, latest) {
|
|
25
|
+
return latest.major > current.major;
|
|
26
|
+
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
const TRANSPILE_OPTIONS = {
|
|
4
|
+
compilerOptions: { jsx: ts.JsxEmit.ReactJSX, module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
|
|
5
|
+
reportDiagnostics: true,
|
|
6
|
+
};
|
|
7
|
+
/** Reads every page file's literal-checkable fields (design §4 "Static checking"). Never imports
|
|
8
|
+
* the file and never builds a `ts.Program` across files — no module resolution happens at all, so
|
|
9
|
+
* this cannot fail because of an unresolved import, a missing `node_modules`, or a stale type.
|
|
10
|
+
* One broken file never stops the rest (Global Constraints, Error-handling rule): each file is
|
|
11
|
+
* read independently. */
|
|
12
|
+
export function readStaticPages(pageFiles) {
|
|
13
|
+
// Error-handling rule (design, Global Constraints): one broken file never stops the rest of a
|
|
14
|
+
// read. `readStaticPage` already guards its own `readFileSync` call, but an unexpected throw
|
|
15
|
+
// from deeper in its AST walk (e.g. a pathological same-file `const` chain overflowing the call
|
|
16
|
+
// stack) must be caught per file too, not just per-read, or it aborts every other page in the
|
|
17
|
+
// same `doctor`/`explain` run.
|
|
18
|
+
return pageFiles.map((file) => {
|
|
19
|
+
try {
|
|
20
|
+
return readStaticPage(file);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return { file, checkable: false, parseError: error.message };
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function readStaticPage(file) {
|
|
28
|
+
let text;
|
|
29
|
+
try {
|
|
30
|
+
text = readFileSync(file, 'utf8');
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
return { file, checkable: false, parseError: error.message };
|
|
34
|
+
}
|
|
35
|
+
// `transpileModule` parses a single file with no module resolution at all — its `diagnostics`
|
|
36
|
+
// are syntactic only (it never type-checks), which is exactly "fails to parse" (design §4's
|
|
37
|
+
// error rule) and nothing more; a semantic/type error in a page file is out of this reader's
|
|
38
|
+
// scope (it never had enough information — no resolved `node_modules` — to find one anyway).
|
|
39
|
+
const { diagnostics } = ts.transpileModule(text, TRANSPILE_OPTIONS);
|
|
40
|
+
if (diagnostics && diagnostics.length > 0) {
|
|
41
|
+
return { file, checkable: false, parseError: ts.flattenDiagnosticMessageText(diagnostics[0].messageText, '\n') };
|
|
42
|
+
}
|
|
43
|
+
const sourceFile = ts.createSourceFile(file, text, ts.ScriptTarget.ES2022, true, ts.ScriptKind.TSX);
|
|
44
|
+
const imports = importedNames(sourceFile, '@krapwoo/ds-viewer');
|
|
45
|
+
const defineCatalogPageNames = localNamesFor(imports, 'defineCatalogPage');
|
|
46
|
+
const gridNames = localNamesFor(imports, 'grid');
|
|
47
|
+
const call = defaultExportCall(sourceFile);
|
|
48
|
+
if (!call || !ts.isIdentifier(call.expression) || !defineCatalogPageNames.has(call.expression.text) || call.arguments.length !== 1) {
|
|
49
|
+
// Not a recognized `defineCatalogPage(...)` default export — e.g. a hand-written object, or a
|
|
50
|
+
// shadowed/renamed import this reader can't trace. Not a parse error: the file is valid
|
|
51
|
+
// TypeScript, just not literal-checkable.
|
|
52
|
+
return { file, checkable: false };
|
|
53
|
+
}
|
|
54
|
+
const consts = topLevelConsts(sourceFile);
|
|
55
|
+
const pageObject = resolve(call.arguments[0], consts, new Set());
|
|
56
|
+
if (!ts.isObjectLiteralExpression(pageObject))
|
|
57
|
+
return { file, checkable: false };
|
|
58
|
+
const page = { file, checkable: true };
|
|
59
|
+
const idProp = findProp(pageObject, 'id');
|
|
60
|
+
const componentProp = findProp(pageObject, 'component');
|
|
61
|
+
const groupProp = findProp(pageObject, 'group');
|
|
62
|
+
const specimenSizeProp = findProp(pageObject, 'specimenSize');
|
|
63
|
+
const tokenGalleryProp = findProp(pageObject, 'tokenGallery');
|
|
64
|
+
if (idProp)
|
|
65
|
+
page.id = stringLiteral(resolve(idProp.initializer, consts, new Set()));
|
|
66
|
+
if (componentProp)
|
|
67
|
+
page.component = stringLiteral(resolve(componentProp.initializer, consts, new Set()));
|
|
68
|
+
if (groupProp)
|
|
69
|
+
page.group = stringLiteral(resolve(groupProp.initializer, consts, new Set()));
|
|
70
|
+
if (specimenSizeProp)
|
|
71
|
+
page.specimenSize = stringLiteral(resolve(specimenSizeProp.initializer, consts, new Set()));
|
|
72
|
+
if (tokenGalleryProp)
|
|
73
|
+
page.tokenGallery = resolve(tokenGalleryProp.initializer, consts, new Set()).kind === ts.SyntaxKind.TrueKeyword;
|
|
74
|
+
page.hasWhenToUse = findProp(pageObject, 'whenToUse') !== undefined;
|
|
75
|
+
page.hasA11y = findProp(pageObject, 'a11y') !== undefined;
|
|
76
|
+
page.hasRender = findProp(pageObject, 'render') !== undefined;
|
|
77
|
+
const hideProp = findProp(pageObject, 'hide');
|
|
78
|
+
if (hideProp) {
|
|
79
|
+
const hideObject = resolve(hideProp.initializer, consts, new Set());
|
|
80
|
+
const literalBool = (name) => {
|
|
81
|
+
const prop = ts.isObjectLiteralExpression(hideObject) ? findProp(hideObject, name) : undefined;
|
|
82
|
+
return prop ? resolve(prop.initializer, consts, new Set()).kind === ts.SyntaxKind.TrueKeyword : false;
|
|
83
|
+
};
|
|
84
|
+
page.hidesAccessibility = literalBool('accessibility');
|
|
85
|
+
page.hideVariants = literalBool('variants');
|
|
86
|
+
page.hideStates = literalBool('states');
|
|
87
|
+
}
|
|
88
|
+
const previewWidthsProp = findProp(pageObject, 'previewWidths');
|
|
89
|
+
if (previewWidthsProp) {
|
|
90
|
+
const resolved = resolve(previewWidthsProp.initializer, consts, new Set());
|
|
91
|
+
if (ts.isStringLiteral(resolved) && resolved.text === 'full') {
|
|
92
|
+
page.previewWidths = 'full';
|
|
93
|
+
}
|
|
94
|
+
else if (ts.isArrayLiteralExpression(resolved) && resolved.elements.every(ts.isNumericLiteral)) {
|
|
95
|
+
page.previewWidths = resolved.elements.map((element) => Number(element.text));
|
|
96
|
+
}
|
|
97
|
+
// Anything else (a function call, a non-literal expression) is simply left unset — `previewWidths`
|
|
98
|
+
// isn't in design §4's literal-checkable list, so it's never a reason to flip `checkable`.
|
|
99
|
+
}
|
|
100
|
+
const fullWidthLabelProp = findProp(pageObject, 'fullWidthLabel');
|
|
101
|
+
if (fullWidthLabelProp)
|
|
102
|
+
page.fullWidthLabel = stringLiteral(resolve(fullWidthLabelProp.initializer, consts, new Set()));
|
|
103
|
+
const propNotesProp = findProp(pageObject, 'propNotes');
|
|
104
|
+
if (propNotesProp) {
|
|
105
|
+
const resolved = resolve(propNotesProp.initializer, consts, new Set());
|
|
106
|
+
if (ts.isObjectLiteralExpression(resolved)) {
|
|
107
|
+
page.propNoteKeys = resolved.properties.filter(ts.isPropertyAssignment).map((p) => propertyName(p.name));
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
page.checkable = false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const comparisonProp = findProp(pageObject, 'comparison');
|
|
114
|
+
if (comparisonProp) {
|
|
115
|
+
const comparison = readComparison(comparisonProp.initializer, consts, gridNames);
|
|
116
|
+
if (comparison)
|
|
117
|
+
page.comparison = comparison;
|
|
118
|
+
else
|
|
119
|
+
page.checkable = false;
|
|
120
|
+
}
|
|
121
|
+
const variants = readSlotItems(findProp(pageObject, 'variants'), consts);
|
|
122
|
+
if (!variants.checkable)
|
|
123
|
+
page.checkable = false;
|
|
124
|
+
else {
|
|
125
|
+
page.variantsItems = variants.items;
|
|
126
|
+
page.variantsItemsFill = variants.itemsFill;
|
|
127
|
+
}
|
|
128
|
+
const states = readSlotItems(findProp(pageObject, 'states'), consts);
|
|
129
|
+
if (!states.checkable)
|
|
130
|
+
page.checkable = false;
|
|
131
|
+
else {
|
|
132
|
+
page.statesItems = states.items;
|
|
133
|
+
page.statesItemsFill = states.itemsFill;
|
|
134
|
+
}
|
|
135
|
+
return page;
|
|
136
|
+
}
|
|
137
|
+
/** Local name -> imported name, for every named import from `moduleSpecifier`. */
|
|
138
|
+
function importedNames(sourceFile, moduleSpecifier) {
|
|
139
|
+
const names = new Map();
|
|
140
|
+
for (const statement of sourceFile.statements) {
|
|
141
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== moduleSpecifier) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const bindings = statement.importClause?.namedBindings;
|
|
145
|
+
if (!bindings || !ts.isNamedImports(bindings))
|
|
146
|
+
continue;
|
|
147
|
+
for (const element of bindings.elements) {
|
|
148
|
+
names.set(element.name.text, element.propertyName ? element.propertyName.text : element.name.text);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return names;
|
|
152
|
+
}
|
|
153
|
+
function localNamesFor(imports, importedName) {
|
|
154
|
+
const locals = new Set();
|
|
155
|
+
for (const [local, imported] of imports)
|
|
156
|
+
if (imported === importedName)
|
|
157
|
+
locals.add(local);
|
|
158
|
+
return locals;
|
|
159
|
+
}
|
|
160
|
+
function defaultExportCall(sourceFile) {
|
|
161
|
+
for (const statement of sourceFile.statements) {
|
|
162
|
+
if (ts.isExportAssignment(statement) && !statement.isExportEquals && ts.isCallExpression(statement.expression)) {
|
|
163
|
+
return statement.expression;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
function topLevelConsts(sourceFile) {
|
|
169
|
+
const consts = new Map();
|
|
170
|
+
for (const statement of sourceFile.statements) {
|
|
171
|
+
if (!ts.isVariableStatement(statement))
|
|
172
|
+
continue;
|
|
173
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
174
|
+
if (ts.isIdentifier(declaration.name) && declaration.initializer)
|
|
175
|
+
consts.set(declaration.name.text, declaration.initializer);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return consts;
|
|
179
|
+
}
|
|
180
|
+
/** Unwraps `as`/parenthesized/`satisfies` expressions and chases a same-file top-level `const`
|
|
181
|
+
* reference to its own initializer (same pattern as `cli/tokens.ts`'s `readExpression`) — returns
|
|
182
|
+
* the resolved expression node itself, not a value, so callers can pattern-match on its kind
|
|
183
|
+
* (array/object literal, call expression, …). `seen` guards a reference cycle. */
|
|
184
|
+
function resolve(node, consts, seen) {
|
|
185
|
+
if (ts.isAsExpression(node) || ts.isParenthesizedExpression(node) || ts.isSatisfiesExpression(node)) {
|
|
186
|
+
return resolve(node.expression, consts, seen);
|
|
187
|
+
}
|
|
188
|
+
if (ts.isIdentifier(node) && !seen.has(node.text)) {
|
|
189
|
+
const target = consts.get(node.text);
|
|
190
|
+
if (target)
|
|
191
|
+
return resolve(target, consts, new Set(seen).add(node.text));
|
|
192
|
+
}
|
|
193
|
+
return node;
|
|
194
|
+
}
|
|
195
|
+
function stringLiteral(node) {
|
|
196
|
+
return ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) ? node.text : undefined;
|
|
197
|
+
}
|
|
198
|
+
function propertyName(name) {
|
|
199
|
+
return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : name.getText();
|
|
200
|
+
}
|
|
201
|
+
function findProp(obj, name) {
|
|
202
|
+
const prop = obj.properties.find((p) => ts.isPropertyAssignment(p) && propertyName(p.name) === name);
|
|
203
|
+
return prop;
|
|
204
|
+
}
|
|
205
|
+
function propertyStringLiteral(obj, name, consts) {
|
|
206
|
+
const prop = findProp(obj, name);
|
|
207
|
+
return prop ? stringLiteral(resolve(prop.initializer, consts, new Set())) : undefined;
|
|
208
|
+
}
|
|
209
|
+
/** A literal array of `{ key, label }` object literals — each element may itself be a same-file
|
|
210
|
+
* const reference. Undefined (not literal) for anything else: a `.map()`/other call, a spread, a
|
|
211
|
+
* cross-file import. */
|
|
212
|
+
function readAxisItemArray(node, consts) {
|
|
213
|
+
const resolved = resolve(node, consts, new Set());
|
|
214
|
+
if (!ts.isArrayLiteralExpression(resolved))
|
|
215
|
+
return undefined;
|
|
216
|
+
const items = [];
|
|
217
|
+
for (const element of resolved.elements) {
|
|
218
|
+
const resolvedElement = resolve(element, consts, new Set());
|
|
219
|
+
if (!ts.isObjectLiteralExpression(resolvedElement))
|
|
220
|
+
return undefined;
|
|
221
|
+
const key = propertyStringLiteral(resolvedElement, 'key', consts);
|
|
222
|
+
const label = propertyStringLiteral(resolvedElement, 'label', consts);
|
|
223
|
+
if (key === undefined || label === undefined)
|
|
224
|
+
return undefined;
|
|
225
|
+
items.push({ key, label });
|
|
226
|
+
}
|
|
227
|
+
return items;
|
|
228
|
+
}
|
|
229
|
+
/** A `GridAxis` (design §4): a plain literal array, or `{ prop, items }` (`items` itself read by
|
|
230
|
+
* `readAxisItemArray`). Undefined for anything else. */
|
|
231
|
+
function readAxis(node, consts) {
|
|
232
|
+
const resolved = resolve(node, consts, new Set());
|
|
233
|
+
if (ts.isArrayLiteralExpression(resolved)) {
|
|
234
|
+
const items = readAxisItemArray(resolved, consts);
|
|
235
|
+
return items ? { items } : undefined;
|
|
236
|
+
}
|
|
237
|
+
if (ts.isObjectLiteralExpression(resolved)) {
|
|
238
|
+
const propProp = findProp(resolved, 'prop');
|
|
239
|
+
const itemsProp = findProp(resolved, 'items');
|
|
240
|
+
if (!propProp || !itemsProp)
|
|
241
|
+
return undefined;
|
|
242
|
+
const prop = stringLiteral(resolve(propProp.initializer, consts, new Set()));
|
|
243
|
+
const items = readAxisItemArray(itemsProp.initializer, consts);
|
|
244
|
+
if (prop === undefined || !items)
|
|
245
|
+
return undefined;
|
|
246
|
+
return { prop, items };
|
|
247
|
+
}
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
/** A page's `comparison` field: either a direct `ComparisonDef`-shaped object literal, or (the
|
|
251
|
+
* starter kit's own convention since Task 1/2) a call to this package's own `grid()` helper — the
|
|
252
|
+
* one call shape this reader specially recognizes, reading its `rows`/`columns` arguments and
|
|
253
|
+
* skipping its `cell` argument entirely (design §4: "Specimen nodes and `cell(...)` functions are
|
|
254
|
+
* not evaluated"). `gridNames` is the set of local identifiers that import resolved to `grid` from
|
|
255
|
+
* `@krapwoo/ds-viewer` — a page's own unrelated local function named `grid` is never matched. */
|
|
256
|
+
function readComparison(node, consts, gridNames) {
|
|
257
|
+
const resolved = resolve(node, consts, new Set());
|
|
258
|
+
if (ts.isCallExpression(resolved) && ts.isIdentifier(resolved.expression) && gridNames.has(resolved.expression.text)) {
|
|
259
|
+
const [rowLabelArg, columnLabelArg, rowsArg, columnsArg, , sizeArg] = resolved.arguments;
|
|
260
|
+
const rows = rowsArg && readAxis(rowsArg, consts);
|
|
261
|
+
const columns = columnsArg && readAxis(columnsArg, consts);
|
|
262
|
+
if (!rows || !columns)
|
|
263
|
+
return undefined;
|
|
264
|
+
return {
|
|
265
|
+
rowLabel: rowLabelArg ? stringLiteral(resolve(rowLabelArg, consts, new Set())) : undefined,
|
|
266
|
+
columnLabel: columnLabelArg ? stringLiteral(resolve(columnLabelArg, consts, new Set())) : undefined,
|
|
267
|
+
rows,
|
|
268
|
+
columns,
|
|
269
|
+
size: sizeArg ? stringLiteral(resolve(sizeArg, consts, new Set())) : undefined,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
if (ts.isObjectLiteralExpression(resolved)) {
|
|
273
|
+
const rowsProp = findProp(resolved, 'rows');
|
|
274
|
+
const columnsProp = findProp(resolved, 'columns');
|
|
275
|
+
if (!rowsProp || !columnsProp)
|
|
276
|
+
return undefined;
|
|
277
|
+
const rows = readAxis(rowsProp.initializer, consts);
|
|
278
|
+
const columns = readAxis(columnsProp.initializer, consts);
|
|
279
|
+
if (!rows || !columns)
|
|
280
|
+
return undefined;
|
|
281
|
+
const rowLabelProp = findProp(resolved, 'rowLabel');
|
|
282
|
+
const columnLabelProp = findProp(resolved, 'columnLabel');
|
|
283
|
+
const sizeProp = findProp(resolved, 'size');
|
|
284
|
+
return {
|
|
285
|
+
rowLabel: rowLabelProp ? stringLiteral(resolve(rowLabelProp.initializer, consts, new Set())) : undefined,
|
|
286
|
+
columnLabel: columnLabelProp ? stringLiteral(resolve(columnLabelProp.initializer, consts, new Set())) : undefined,
|
|
287
|
+
rows,
|
|
288
|
+
columns,
|
|
289
|
+
size: sizeProp ? stringLiteral(resolve(sizeProp.initializer, consts, new Set())) : undefined,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
/** A literal array of list-item object literals — `key`/`name` required; `props` (read as a flat
|
|
295
|
+
* literal map, non-literal values simply omitted) and `group` optional. Each item's `node` field
|
|
296
|
+
* (always present, always JSX) is never inspected — reading the sibling fields never depends on it. */
|
|
297
|
+
function readListItems(node, consts) {
|
|
298
|
+
const resolved = resolve(node, consts, new Set());
|
|
299
|
+
if (!ts.isArrayLiteralExpression(resolved))
|
|
300
|
+
return undefined;
|
|
301
|
+
const items = [];
|
|
302
|
+
for (const element of resolved.elements) {
|
|
303
|
+
const resolvedElement = resolve(element, consts, new Set());
|
|
304
|
+
if (!ts.isObjectLiteralExpression(resolvedElement))
|
|
305
|
+
return undefined;
|
|
306
|
+
const key = propertyStringLiteral(resolvedElement, 'key', consts);
|
|
307
|
+
const name = propertyStringLiteral(resolvedElement, 'name', consts);
|
|
308
|
+
if (key === undefined || name === undefined)
|
|
309
|
+
return undefined;
|
|
310
|
+
const group = propertyStringLiteral(resolvedElement, 'group', consts);
|
|
311
|
+
const propsProp = findProp(resolvedElement, 'props');
|
|
312
|
+
items.push({ key, name, group, props: propsProp ? readLiteralProps(propsProp.initializer, consts) : undefined });
|
|
313
|
+
}
|
|
314
|
+
return items;
|
|
315
|
+
}
|
|
316
|
+
function readLiteralProps(node, consts) {
|
|
317
|
+
const resolved = resolve(node, consts, new Set());
|
|
318
|
+
if (!ts.isObjectLiteralExpression(resolved))
|
|
319
|
+
return undefined;
|
|
320
|
+
const result = {};
|
|
321
|
+
for (const property of resolved.properties) {
|
|
322
|
+
if (!ts.isPropertyAssignment(property))
|
|
323
|
+
continue;
|
|
324
|
+
const value = resolve(property.initializer, consts, new Set());
|
|
325
|
+
if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value))
|
|
326
|
+
result[propertyName(property.name)] = value.text;
|
|
327
|
+
else if (ts.isNumericLiteral(value))
|
|
328
|
+
result[propertyName(property.name)] = Number(value.text);
|
|
329
|
+
else if (value.kind === ts.SyntaxKind.TrueKeyword)
|
|
330
|
+
result[propertyName(property.name)] = true;
|
|
331
|
+
else if (value.kind === ts.SyntaxKind.FalseKeyword)
|
|
332
|
+
result[propertyName(property.name)] = false;
|
|
333
|
+
// Anything else (e.g. a token reference) is skipped, not a reason to reject the whole item —
|
|
334
|
+
// `props` tags are only ever compared against a prop's string options (design §4).
|
|
335
|
+
}
|
|
336
|
+
return result;
|
|
337
|
+
}
|
|
338
|
+
/** A `variants`/`states` field: `{ itemsFill?, items: [...] }`, `items` read by `readListItems`.
|
|
339
|
+
* Distinguishes "field absent" (always checkable, everything undefined) from "field present but
|
|
340
|
+
* not literal" (not checkable). `itemsFill` is read only if present and literal — like
|
|
341
|
+
* `previewWidths`/`fullWidthLabel`, it isn't in design §4's literal-checkable list, so a
|
|
342
|
+
* non-literal `itemsFill` is simply left unset, never a reason to reject the whole slot. */
|
|
343
|
+
function readSlotItems(prop, consts) {
|
|
344
|
+
if (!prop)
|
|
345
|
+
return { checkable: true };
|
|
346
|
+
const resolved = resolve(prop.initializer, consts, new Set());
|
|
347
|
+
const itemsProp = ts.isObjectLiteralExpression(resolved) ? findProp(resolved, 'items') : undefined;
|
|
348
|
+
if (!itemsProp)
|
|
349
|
+
return { checkable: false };
|
|
350
|
+
const items = readListItems(itemsProp.initializer, consts);
|
|
351
|
+
if (!items)
|
|
352
|
+
return { checkable: false };
|
|
353
|
+
const itemsFillProp = ts.isObjectLiteralExpression(resolved) ? findProp(resolved, 'itemsFill') : undefined;
|
|
354
|
+
const itemsFill = itemsFillProp ? resolve(itemsFillProp.initializer, consts, new Set()).kind === ts.SyntaxKind.TrueKeyword : undefined;
|
|
355
|
+
return { items, itemsFill, checkable: true };
|
|
356
|
+
}
|
package/dist/cli/sync.js
CHANGED
|
@@ -50,11 +50,14 @@ export function sync(config, resolveOptions) {
|
|
|
50
50
|
? { '*': [path.join(config.projectRoot, 'node_modules', '@types', '*'), path.join(config.projectRoot, 'node_modules', '*')] }
|
|
51
51
|
: undefined;
|
|
52
52
|
const readComponent = createComponentReader(componentEntryFiles, { ...resolveOptions, optionRoots, fallbackPaths });
|
|
53
|
+
const warnings = [];
|
|
53
54
|
for (const entryFile of componentEntryFiles) {
|
|
54
55
|
try {
|
|
55
56
|
for (const record of readComponent(entryFile)) {
|
|
56
57
|
if (seenNames.has(record.name)) {
|
|
57
|
-
|
|
58
|
+
const message = `Duplicate component name "${record.name}" (${path.relative(config.projectRoot, entryFile)}); the catalog will show only one.`;
|
|
59
|
+
console.warn(message);
|
|
60
|
+
warnings.push(message);
|
|
58
61
|
}
|
|
59
62
|
seenNames.add(record.name);
|
|
60
63
|
components.push(record);
|
|
@@ -62,7 +65,9 @@ export function sync(config, resolveOptions) {
|
|
|
62
65
|
}
|
|
63
66
|
}
|
|
64
67
|
catch (error) {
|
|
65
|
-
|
|
68
|
+
const message = `Could not read ${path.relative(config.projectRoot, entryFile)}: ${error.message}`;
|
|
69
|
+
console.warn(message);
|
|
70
|
+
warnings.push(message);
|
|
66
71
|
}
|
|
67
72
|
}
|
|
68
73
|
components.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -73,12 +78,14 @@ export function sync(config, resolveOptions) {
|
|
|
73
78
|
tokenRecords.push(...readTokenModules([tokenFile]));
|
|
74
79
|
}
|
|
75
80
|
catch (error) {
|
|
76
|
-
|
|
81
|
+
const message = `Could not read ${path.relative(config.projectRoot, tokenFile)}: ${error.message}`;
|
|
82
|
+
console.warn(message);
|
|
83
|
+
warnings.push(message);
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
86
|
writeFileSync(path.join(generatedDir, 'tokens.json'), `${JSON.stringify(tokenRecords, null, 2)}\n`);
|
|
80
87
|
writeFileSync(path.join(generatedDir, 'index.ts'), GENERATED_INDEX);
|
|
81
88
|
const pageFiles = discoverPages({ components: componentRefs, standalonePageGlobs: config.pages ?? [], projectRoot: config.projectRoot });
|
|
82
89
|
writePageIndex(generatedDir, pageFiles);
|
|
83
|
-
return { componentCount: components.length, pageCount: pageFiles.length, generatedDir };
|
|
90
|
+
return { componentCount: components.length, pageCount: pageFiles.length, generatedDir, warnings };
|
|
84
91
|
}
|