@pie-element/cli 0.1.3 → 0.1.4-next.20261011221311

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
@@ -58,7 +58,7 @@ bun run cli packages:enable-publishing
58
58
 
59
59
  #### `docs:generate`
60
60
 
61
- Generate framework-agnostic HTML docs artifacts for elements from per-element `docs.contract.json` descriptors.
61
+ Generate framework-agnostic HTML docs for elements: property rows from each element's JSON Schemas, defaults from the files its `docs.contract.json` points at.
62
62
  The element demo runs this during `prebuild`/`predev`; the generated `static/element-docs`
63
63
  directory is a local build artifact and is not committed.
64
64
 
@@ -2,92 +2,13 @@ import { existsSync } from 'node:fs';
2
2
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
3
  import { join, relative } from 'node:path';
4
4
  import { CONTRACT_FILENAME, inferViewsFromPackageExports } from './discovery.js';
5
+ import { loadPointerValue } from './source-extractor.js';
5
6
  const MODES = ['gather', 'view', 'evaluate'];
6
7
  const isObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
7
8
  const isSourcePointer = (value) => isObject(value) &&
8
9
  typeof value.file === 'string' &&
9
10
  (value.exportName === undefined || typeof value.exportName === 'string') &&
10
11
  (value.path === undefined || typeof value.path === 'string');
11
- const isPrimitiveEnumValue = (value) => value === null || ['string', 'number', 'boolean'].includes(typeof value);
12
- const isPropertyMetadata = (value) => {
13
- if (!isObject(value)) {
14
- return false;
15
- }
16
- const allowed = [
17
- 'title',
18
- 'description',
19
- 'notes',
20
- 'type',
21
- 'required',
22
- 'enum',
23
- 'minimum',
24
- 'maximum',
25
- 'minLength',
26
- 'maxLength',
27
- 'pattern',
28
- 'itemType',
29
- 'examples',
30
- ];
31
- for (const key of Object.keys(value)) {
32
- if (!allowed.includes(key)) {
33
- return false;
34
- }
35
- }
36
- if (value.title !== undefined && typeof value.title !== 'string')
37
- return false;
38
- if (value.description !== undefined && typeof value.description !== 'string')
39
- return false;
40
- if (value.notes !== undefined && typeof value.notes !== 'string')
41
- return false;
42
- if (value.type !== undefined && typeof value.type !== 'string')
43
- return false;
44
- if (value.required !== undefined && typeof value.required !== 'boolean')
45
- return false;
46
- if (value.enum !== undefined) {
47
- if (!Array.isArray(value.enum) || value.enum.some((entry) => !isPrimitiveEnumValue(entry))) {
48
- return false;
49
- }
50
- }
51
- for (const key of ['minimum', 'maximum', 'minLength', 'maxLength']) {
52
- if (value[key] !== undefined && typeof value[key] !== 'number')
53
- return false;
54
- }
55
- if (value.pattern !== undefined && typeof value.pattern !== 'string')
56
- return false;
57
- if (value.itemType !== undefined && typeof value.itemType !== 'string')
58
- return false;
59
- if (value.examples !== undefined && !Array.isArray(value.examples))
60
- return false;
61
- return true;
62
- };
63
- const normalizeMetadataMap = (map) => {
64
- if (!map) {
65
- return undefined;
66
- }
67
- return Object.fromEntries(Object.entries(map)
68
- .sort(([a], [b]) => a.localeCompare(b))
69
- .map(([path, metadata]) => [path, { ...metadata }]));
70
- };
71
- const isViewMetadata = (value) => {
72
- if (!isObject(value)) {
73
- return false;
74
- }
75
- for (const key of Object.keys(value)) {
76
- if (!['pie', 'config'].includes(key)) {
77
- return false;
78
- }
79
- const map = value[key];
80
- if (map === undefined)
81
- continue;
82
- if (!isObject(map))
83
- return false;
84
- for (const entry of Object.values(map)) {
85
- if (!isPropertyMetadata(entry))
86
- return false;
87
- }
88
- }
89
- return true;
90
- };
91
12
  const normalizePointer = (pointer) => ({
92
13
  file: pointer.file,
93
14
  ...(pointer.exportName ? { exportName: pointer.exportName } : {}),
@@ -105,45 +26,35 @@ const normalizeContract = (contract) => ({
105
26
  view: view.view,
106
27
  pie: normalizePointer(view.pie),
107
28
  config: normalizePointer(view.config),
108
- ...(view.description ? { description: view.description } : {}),
109
- ...(view.metadata
110
- ? {
111
- metadata: {
112
- ...(view.metadata.pie ? { pie: normalizeMetadataMap(view.metadata.pie) } : {}),
113
- ...(view.metadata.config
114
- ? { config: normalizeMetadataMap(view.metadata.config) }
115
- : {}),
116
- },
117
- }
118
- : {}),
119
29
  })),
120
30
  });
121
- const inferPiePointer = (packageDir) => {
122
- const candidates = [
123
- { file: 'src/author/defaultConfiguration.ts', path: 'model' },
124
- { file: 'src/author/defaults.ts', path: 'model' },
125
- { file: 'src/controller/defaults.ts' },
126
- ];
31
+ // The first candidate whose file exists and whose path resolves in it.
32
+ const firstResolving = async (packageDir, candidates) => {
127
33
  for (const candidate of candidates) {
128
- if (existsSync(join(packageDir, candidate.file))) {
34
+ if (!existsSync(join(packageDir, candidate.file)))
35
+ continue;
36
+ try {
37
+ await loadPointerValue(packageDir, candidate);
129
38
  return candidate;
130
39
  }
131
- }
132
- return { file: 'src/controller/defaults.ts' };
133
- };
134
- const inferConfigPointer = (packageDir) => {
135
- const candidates = [
136
- { file: 'src/author/defaultConfiguration.ts', path: 'configuration' },
137
- { file: 'src/author/defaults.ts', path: 'configuration' },
138
- { file: 'src/controller/defaults.ts' },
139
- ];
140
- for (const candidate of candidates) {
141
- if (existsSync(join(packageDir, candidate.file))) {
142
- return candidate;
40
+ catch {
41
+ // The path is absent from this file; try the next candidate.
143
42
  }
144
43
  }
145
- return { file: 'src/controller/defaults.ts', path: 'configuration' };
44
+ return undefined;
146
45
  };
46
+ // Controller defaults export either `{ model, configuration }` or the model itself.
47
+ const inferPiePointer = async (packageDir) => (await firstResolving(packageDir, [
48
+ { file: 'src/author/defaultConfiguration.ts', path: 'model' },
49
+ { file: 'src/author/defaults.ts', path: 'model' },
50
+ { file: 'src/controller/defaults.ts', path: 'model' },
51
+ ])) ?? { file: 'src/controller/defaults.ts' };
52
+ const inferConfigPointer = async (packageDir) => (await firstResolving(packageDir, [
53
+ { file: 'src/author/defaultConfiguration.ts', path: 'configuration' },
54
+ { file: 'src/author/defaults.ts', path: 'configuration' },
55
+ { file: 'src/author/defaults.tsx', path: 'configuration' },
56
+ { file: 'src/controller/defaults.ts', path: 'configuration' },
57
+ ])) ?? { file: 'src/controller/defaults.ts', path: 'configuration' };
147
58
  export const parseContract = (raw, location) => {
148
59
  if (!isObject(raw)) {
149
60
  throw new Error(`Invalid docs contract in ${location}: expected object`);
@@ -173,16 +84,7 @@ export const parseContract = (raw, location) => {
173
84
  if (!isSourcePointer(view.config)) {
174
85
  throw new Error(`Invalid docs contract in ${location}: views[${index}].config is invalid`);
175
86
  }
176
- if (view.metadata !== undefined && !isViewMetadata(view.metadata)) {
177
- throw new Error(`Invalid docs contract in ${location}: views[${index}].metadata is invalid`);
178
- }
179
- return {
180
- view: view.view,
181
- pie: view.pie,
182
- config: view.config,
183
- ...(typeof view.description === 'string' ? { description: view.description } : {}),
184
- ...(view.metadata ? { metadata: view.metadata } : {}),
185
- };
87
+ return { view: view.view, pie: view.pie, config: view.config };
186
88
  });
187
89
  const supportedModes = Array.isArray(raw.supportedModes)
188
90
  ? raw.supportedModes.filter((mode) => typeof mode === 'string')
@@ -216,8 +118,8 @@ export const seedContractForElement = async (element, rootDir, options) => {
216
118
  if (existing && !options?.refresh) {
217
119
  return { wrote: false, contractPath, contract: existing };
218
120
  }
219
- const piePointer = inferPiePointer(element.packageDir);
220
- const configPointer = inferConfigPointer(element.packageDir);
121
+ const piePointer = await inferPiePointer(element.packageDir);
122
+ const configPointer = await inferConfigPointer(element.packageDir);
221
123
  const views = inferViewsFromPackageExports(element.exportsMap).map((view) => ({
222
124
  view,
223
125
  pie: piePointer,
@@ -1 +1 @@
1
- {"version":3,"file":"contracts.js","sourceRoot":"","sources":["../../../src/lib/docs/contracts.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from 'node:fs';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { join, relative } from 'node:path';\nimport { CONTRACT_FILENAME, inferViewsFromPackageExports } from './discovery.js';\nimport type {\n ElementPackageInfo,\n PieDocsContract,\n PieDocsPropertyMetadata,\n PieDocsSourcePointer,\n PieDocsViewMetadata,\n} from './types.js';\n\nconst MODES = ['gather', 'view', 'evaluate'];\n\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n !!value && typeof value === 'object' && !Array.isArray(value);\n\nconst isSourcePointer = (value: unknown): value is PieDocsSourcePointer =>\n isObject(value) &&\n typeof value.file === 'string' &&\n (value.exportName === undefined || typeof value.exportName === 'string') &&\n (value.path === undefined || typeof value.path === 'string');\n\nconst isPrimitiveEnumValue = (value: unknown): value is string | number | boolean | null =>\n value === null || ['string', 'number', 'boolean'].includes(typeof value);\n\nconst isPropertyMetadata = (value: unknown): value is PieDocsPropertyMetadata => {\n if (!isObject(value)) {\n return false;\n }\n const allowed = [\n 'title',\n 'description',\n 'notes',\n 'type',\n 'required',\n 'enum',\n 'minimum',\n 'maximum',\n 'minLength',\n 'maxLength',\n 'pattern',\n 'itemType',\n 'examples',\n ];\n for (const key of Object.keys(value)) {\n if (!allowed.includes(key)) {\n return false;\n }\n }\n if (value.title !== undefined && typeof value.title !== 'string') return false;\n if (value.description !== undefined && typeof value.description !== 'string') return false;\n if (value.notes !== undefined && typeof value.notes !== 'string') return false;\n if (value.type !== undefined && typeof value.type !== 'string') return false;\n if (value.required !== undefined && typeof value.required !== 'boolean') return false;\n if (value.enum !== undefined) {\n if (!Array.isArray(value.enum) || value.enum.some((entry) => !isPrimitiveEnumValue(entry))) {\n return false;\n }\n }\n for (const key of ['minimum', 'maximum', 'minLength', 'maxLength'] as const) {\n if (value[key] !== undefined && typeof value[key] !== 'number') return false;\n }\n if (value.pattern !== undefined && typeof value.pattern !== 'string') return false;\n if (value.itemType !== undefined && typeof value.itemType !== 'string') return false;\n if (value.examples !== undefined && !Array.isArray(value.examples)) return false;\n return true;\n};\n\nconst normalizeMetadataMap = (\n map?: Record<string, PieDocsPropertyMetadata>\n): Record<string, PieDocsPropertyMetadata> | undefined => {\n if (!map) {\n return undefined;\n }\n return Object.fromEntries(\n Object.entries(map)\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([path, metadata]) => [path, { ...metadata }])\n );\n};\n\nconst isViewMetadata = (value: unknown): value is PieDocsViewMetadata => {\n if (!isObject(value)) {\n return false;\n }\n for (const key of Object.keys(value)) {\n if (!['pie', 'config'].includes(key)) {\n return false;\n }\n const map = value[key];\n if (map === undefined) continue;\n if (!isObject(map)) return false;\n for (const entry of Object.values(map)) {\n if (!isPropertyMetadata(entry)) return false;\n }\n }\n return true;\n};\n\nconst normalizePointer = (pointer: PieDocsSourcePointer): PieDocsSourcePointer => ({\n file: pointer.file,\n ...(pointer.exportName ? { exportName: pointer.exportName } : {}),\n ...(pointer.path ? { path: pointer.path } : {}),\n});\n\nconst normalizeContract = (contract: PieDocsContract): PieDocsContract => ({\n elementName: contract.elementName,\n packageName: contract.packageName,\n framework: contract.framework,\n ...(contract.summary ? { summary: contract.summary } : {}),\n supportedModes: contract.supportedModes?.length ? [...contract.supportedModes] : MODES,\n views: [...contract.views]\n .sort((a, b) => a.view.localeCompare(b.view))\n .map((view) => ({\n view: view.view,\n pie: normalizePointer(view.pie),\n config: normalizePointer(view.config),\n ...(view.description ? { description: view.description } : {}),\n ...(view.metadata\n ? {\n metadata: {\n ...(view.metadata.pie ? { pie: normalizeMetadataMap(view.metadata.pie) } : {}),\n ...(view.metadata.config\n ? { config: normalizeMetadataMap(view.metadata.config) }\n : {}),\n },\n }\n : {}),\n })),\n});\n\nconst inferPiePointer = (packageDir: string): PieDocsSourcePointer => {\n const candidates: PieDocsSourcePointer[] = [\n { file: 'src/author/defaultConfiguration.ts', path: 'model' },\n { file: 'src/author/defaults.ts', path: 'model' },\n { file: 'src/controller/defaults.ts' },\n ];\n\n for (const candidate of candidates) {\n if (existsSync(join(packageDir, candidate.file))) {\n return candidate;\n }\n }\n\n return { file: 'src/controller/defaults.ts' };\n};\n\nconst inferConfigPointer = (packageDir: string): PieDocsSourcePointer => {\n const candidates: PieDocsSourcePointer[] = [\n { file: 'src/author/defaultConfiguration.ts', path: 'configuration' },\n { file: 'src/author/defaults.ts', path: 'configuration' },\n { file: 'src/controller/defaults.ts' },\n ];\n\n for (const candidate of candidates) {\n if (existsSync(join(packageDir, candidate.file))) {\n return candidate;\n }\n }\n\n return { file: 'src/controller/defaults.ts', path: 'configuration' };\n};\n\nexport const parseContract = (raw: unknown, location: string): PieDocsContract => {\n if (!isObject(raw)) {\n throw new Error(`Invalid docs contract in ${location}: expected object`);\n }\n if (typeof raw.elementName !== 'string' || !raw.elementName.trim()) {\n throw new Error(`Invalid docs contract in ${location}: elementName is required`);\n }\n if (typeof raw.packageName !== 'string' || !raw.packageName.startsWith('@pie-element/')) {\n throw new Error(`Invalid docs contract in ${location}: packageName is invalid`);\n }\n if (raw.framework !== 'react' && raw.framework !== 'svelte') {\n throw new Error(`Invalid docs contract in ${location}: framework must be react|svelte`);\n }\n if (!Array.isArray(raw.views) || raw.views.length === 0) {\n throw new Error(`Invalid docs contract in ${location}: at least one view is required`);\n }\n\n const views = raw.views.map((view, index) => {\n if (!isObject(view)) {\n throw new Error(`Invalid docs contract in ${location}: views[${index}] must be an object`);\n }\n if (typeof view.view !== 'string' || !view.view.trim()) {\n throw new Error(`Invalid docs contract in ${location}: views[${index}].view is required`);\n }\n if (!isSourcePointer(view.pie)) {\n throw new Error(`Invalid docs contract in ${location}: views[${index}].pie is invalid`);\n }\n if (!isSourcePointer(view.config)) {\n throw new Error(`Invalid docs contract in ${location}: views[${index}].config is invalid`);\n }\n if (view.metadata !== undefined && !isViewMetadata(view.metadata)) {\n throw new Error(`Invalid docs contract in ${location}: views[${index}].metadata is invalid`);\n }\n\n return {\n view: view.view,\n pie: view.pie,\n config: view.config,\n ...(typeof view.description === 'string' ? { description: view.description } : {}),\n ...(view.metadata ? { metadata: view.metadata } : {}),\n };\n });\n\n const supportedModes = Array.isArray(raw.supportedModes)\n ? raw.supportedModes.filter((mode): mode is string => typeof mode === 'string')\n : undefined;\n\n return normalizeContract({\n elementName: raw.elementName,\n packageName: raw.packageName,\n framework: raw.framework,\n summary: typeof raw.summary === 'string' ? raw.summary : undefined,\n supportedModes: supportedModes?.length ? supportedModes : MODES,\n views,\n });\n};\n\nexport const loadContract = async (packageDir: string): Promise<PieDocsContract | null> => {\n const contractPath = join(packageDir, CONTRACT_FILENAME);\n if (!existsSync(contractPath)) {\n return null;\n }\n const raw = JSON.parse(await readFile(contractPath, 'utf-8')) as unknown;\n return parseContract(raw, contractPath);\n};\n\nexport const writeContract = async (\n packageDir: string,\n contract: PieDocsContract\n): Promise<void> => {\n const contractPath = join(packageDir, CONTRACT_FILENAME);\n const normalized = normalizeContract(contract);\n await mkdir(packageDir, { recursive: true });\n await writeFile(contractPath, `${JSON.stringify(normalized, null, 2)}\\n`, 'utf-8');\n};\n\nexport const seedContractForElement = async (\n element: ElementPackageInfo,\n rootDir: string,\n options?: { refresh?: boolean }\n): Promise<{ wrote: boolean; contractPath: string; contract: PieDocsContract }> => {\n const contractPath = join(element.packageDir, CONTRACT_FILENAME);\n const existing = await loadContract(element.packageDir);\n if (existing && !options?.refresh) {\n return { wrote: false, contractPath, contract: existing };\n }\n\n const piePointer = inferPiePointer(element.packageDir);\n const configPointer = inferConfigPointer(element.packageDir);\n const views = inferViewsFromPackageExports(element.exportsMap).map((view) => ({\n view,\n pie: piePointer,\n config: configPointer,\n }));\n\n const contract: PieDocsContract = normalizeContract({\n elementName: element.elementName,\n packageName: element.packageName,\n framework: element.framework,\n summary: existing?.summary || element.packageDescription,\n supportedModes: existing?.supportedModes?.length ? existing.supportedModes : MODES,\n views,\n });\n\n await writeContract(element.packageDir, contract);\n\n return {\n wrote: true,\n contractPath: relative(rootDir, contractPath),\n contract,\n };\n};\n"]}
1
+ {"version":3,"file":"contracts.js","sourceRoot":"","sources":["../../../src/lib/docs/contracts.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from 'node:fs';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { join, relative } from 'node:path';\nimport { CONTRACT_FILENAME, inferViewsFromPackageExports } from './discovery.js';\nimport { loadPointerValue } from './source-extractor.js';\nimport type { ElementPackageInfo, PieDocsContract, PieDocsSourcePointer } from './types.js';\n\nconst MODES = ['gather', 'view', 'evaluate'];\n\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n !!value && typeof value === 'object' && !Array.isArray(value);\n\nconst isSourcePointer = (value: unknown): value is PieDocsSourcePointer =>\n isObject(value) &&\n typeof value.file === 'string' &&\n (value.exportName === undefined || typeof value.exportName === 'string') &&\n (value.path === undefined || typeof value.path === 'string');\n\nconst normalizePointer = (pointer: PieDocsSourcePointer): PieDocsSourcePointer => ({\n file: pointer.file,\n ...(pointer.exportName ? { exportName: pointer.exportName } : {}),\n ...(pointer.path ? { path: pointer.path } : {}),\n});\n\nconst normalizeContract = (contract: PieDocsContract): PieDocsContract => ({\n elementName: contract.elementName,\n packageName: contract.packageName,\n framework: contract.framework,\n ...(contract.summary ? { summary: contract.summary } : {}),\n supportedModes: contract.supportedModes?.length ? [...contract.supportedModes] : MODES,\n views: [...contract.views]\n .sort((a, b) => a.view.localeCompare(b.view))\n .map((view) => ({\n view: view.view,\n pie: normalizePointer(view.pie),\n config: normalizePointer(view.config),\n })),\n});\n\n// The first candidate whose file exists and whose path resolves in it.\nconst firstResolving = async (\n packageDir: string,\n candidates: PieDocsSourcePointer[]\n): Promise<PieDocsSourcePointer | undefined> => {\n for (const candidate of candidates) {\n if (!existsSync(join(packageDir, candidate.file))) continue;\n try {\n await loadPointerValue(packageDir, candidate);\n return candidate;\n } catch {\n // The path is absent from this file; try the next candidate.\n }\n }\n return undefined;\n};\n\n// Controller defaults export either `{ model, configuration }` or the model itself.\nconst inferPiePointer = async (packageDir: string): Promise<PieDocsSourcePointer> =>\n (await firstResolving(packageDir, [\n { file: 'src/author/defaultConfiguration.ts', path: 'model' },\n { file: 'src/author/defaults.ts', path: 'model' },\n { file: 'src/controller/defaults.ts', path: 'model' },\n ])) ?? { file: 'src/controller/defaults.ts' };\n\nconst inferConfigPointer = async (packageDir: string): Promise<PieDocsSourcePointer> =>\n (await firstResolving(packageDir, [\n { file: 'src/author/defaultConfiguration.ts', path: 'configuration' },\n { file: 'src/author/defaults.ts', path: 'configuration' },\n { file: 'src/author/defaults.tsx', path: 'configuration' },\n { file: 'src/controller/defaults.ts', path: 'configuration' },\n ])) ?? { file: 'src/controller/defaults.ts', path: 'configuration' };\n\nexport const parseContract = (raw: unknown, location: string): PieDocsContract => {\n if (!isObject(raw)) {\n throw new Error(`Invalid docs contract in ${location}: expected object`);\n }\n if (typeof raw.elementName !== 'string' || !raw.elementName.trim()) {\n throw new Error(`Invalid docs contract in ${location}: elementName is required`);\n }\n if (typeof raw.packageName !== 'string' || !raw.packageName.startsWith('@pie-element/')) {\n throw new Error(`Invalid docs contract in ${location}: packageName is invalid`);\n }\n if (raw.framework !== 'react' && raw.framework !== 'svelte') {\n throw new Error(`Invalid docs contract in ${location}: framework must be react|svelte`);\n }\n if (!Array.isArray(raw.views) || raw.views.length === 0) {\n throw new Error(`Invalid docs contract in ${location}: at least one view is required`);\n }\n\n const views = raw.views.map((view, index) => {\n if (!isObject(view)) {\n throw new Error(`Invalid docs contract in ${location}: views[${index}] must be an object`);\n }\n if (typeof view.view !== 'string' || !view.view.trim()) {\n throw new Error(`Invalid docs contract in ${location}: views[${index}].view is required`);\n }\n if (!isSourcePointer(view.pie)) {\n throw new Error(`Invalid docs contract in ${location}: views[${index}].pie is invalid`);\n }\n if (!isSourcePointer(view.config)) {\n throw new Error(`Invalid docs contract in ${location}: views[${index}].config is invalid`);\n }\n\n return { view: view.view, pie: view.pie, config: view.config };\n });\n\n const supportedModes = Array.isArray(raw.supportedModes)\n ? raw.supportedModes.filter((mode): mode is string => typeof mode === 'string')\n : undefined;\n\n return normalizeContract({\n elementName: raw.elementName,\n packageName: raw.packageName,\n framework: raw.framework,\n summary: typeof raw.summary === 'string' ? raw.summary : undefined,\n supportedModes: supportedModes?.length ? supportedModes : MODES,\n views,\n });\n};\n\nexport const loadContract = async (packageDir: string): Promise<PieDocsContract | null> => {\n const contractPath = join(packageDir, CONTRACT_FILENAME);\n if (!existsSync(contractPath)) {\n return null;\n }\n const raw = JSON.parse(await readFile(contractPath, 'utf-8')) as unknown;\n return parseContract(raw, contractPath);\n};\n\nexport const writeContract = async (\n packageDir: string,\n contract: PieDocsContract\n): Promise<void> => {\n const contractPath = join(packageDir, CONTRACT_FILENAME);\n const normalized = normalizeContract(contract);\n await mkdir(packageDir, { recursive: true });\n await writeFile(contractPath, `${JSON.stringify(normalized, null, 2)}\\n`, 'utf-8');\n};\n\nexport const seedContractForElement = async (\n element: ElementPackageInfo,\n rootDir: string,\n options?: { refresh?: boolean }\n): Promise<{ wrote: boolean; contractPath: string; contract: PieDocsContract }> => {\n const contractPath = join(element.packageDir, CONTRACT_FILENAME);\n const existing = await loadContract(element.packageDir);\n if (existing && !options?.refresh) {\n return { wrote: false, contractPath, contract: existing };\n }\n\n const piePointer = await inferPiePointer(element.packageDir);\n const configPointer = await inferConfigPointer(element.packageDir);\n const views = inferViewsFromPackageExports(element.exportsMap).map((view) => ({\n view,\n pie: piePointer,\n config: configPointer,\n }));\n\n const contract: PieDocsContract = normalizeContract({\n elementName: element.elementName,\n packageName: element.packageName,\n framework: element.framework,\n summary: existing?.summary || element.packageDescription,\n supportedModes: existing?.supportedModes?.length ? existing.supportedModes : MODES,\n views,\n });\n\n await writeContract(element.packageDir, contract);\n\n return {\n wrote: true,\n contractPath: relative(rootDir, contractPath),\n contract,\n };\n};\n"]}
@@ -1,56 +1,9 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
3
  import { join, relative } from 'node:path';
4
+ import { generateElementSchemas } from '../schemas/generate.js';
5
+ import { schemaDocs } from './schema-properties.js';
4
6
  import { loadPointerValue } from './source-extractor.js';
5
- const isRecord = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
6
- const inferType = (value) => {
7
- if (value === null)
8
- return 'null';
9
- if (Array.isArray(value))
10
- return 'array';
11
- switch (typeof value) {
12
- case 'string':
13
- case 'number':
14
- case 'boolean':
15
- return typeof value;
16
- case 'object':
17
- return 'object';
18
- default:
19
- return 'unknown';
20
- }
21
- };
22
- const flattenProperties = (value, basePath = '') => {
23
- if (isRecord(value)) {
24
- const keys = Object.keys(value).sort();
25
- if (keys.length === 0 && basePath) {
26
- return [{ path: basePath, type: 'object', required: false, defaultValue: {} }];
27
- }
28
- const flattened = [];
29
- for (const key of keys) {
30
- const childPath = basePath ? `${basePath}.${key}` : key;
31
- flattened.push(...flattenProperties(value[key], childPath));
32
- }
33
- return flattened;
34
- }
35
- if (Array.isArray(value)) {
36
- return [
37
- {
38
- path: basePath,
39
- type: 'array',
40
- required: false,
41
- defaultValue: value,
42
- },
43
- ];
44
- }
45
- return [
46
- {
47
- path: basePath,
48
- type: inferType(value),
49
- required: false,
50
- defaultValue: value,
51
- },
52
- ];
53
- };
54
7
  const escapeHtml = (value) => value
55
8
  .replaceAll('&', '&amp;')
56
9
  .replaceAll('<', '&lt;')
@@ -66,7 +19,7 @@ const formatInline = (value) => {
66
19
  };
67
20
  const formatValue = (value) => {
68
21
  if (typeof value === 'string') {
69
- return `<code>${escapeHtml(value)}</code>`;
22
+ return `<code>${value === '' ? '""' : escapeHtml(value)}</code>`;
70
23
  }
71
24
  if (typeof value === 'number' || typeof value === 'boolean') {
72
25
  return `<code>${String(value)}</code>`;
@@ -123,8 +76,8 @@ const loadValueWithFallback = async (packageDir, pointer, kind) => {
123
76
  pointer,
124
77
  { file: 'src/author/defaultConfiguration.ts', path: 'configuration' },
125
78
  { file: 'src/author/defaults.ts', path: 'configuration' },
79
+ { file: 'src/author/defaults.tsx', path: 'configuration' },
126
80
  { file: 'src/controller/defaults.ts', path: 'configuration' },
127
- { file: 'src/controller/defaults.ts' },
128
81
  ];
129
82
  let lastError;
130
83
  for (const candidate of fallbackPointers) {
@@ -147,49 +100,27 @@ const loadValueWithFallback = async (packageDir, pointer, kind) => {
147
100
  }
148
101
  throw lastError || new Error(`Unable to load ${kind} defaults from ${pointer.file}`);
149
102
  };
150
- const applyMetadata = (properties, metadata) => {
151
- if (!metadata || Object.keys(metadata).length === 0) {
152
- return properties;
153
- }
154
- const byPath = new Map(properties.map((p) => [p.path, { ...p }]));
155
- for (const [path, meta] of Object.entries(metadata)) {
156
- const current = byPath.get(path) || {
157
- path,
158
- type: meta.type || 'unknown',
159
- required: meta.required ?? false,
160
- };
161
- const merged = {
162
- ...current,
163
- ...(meta.type ? { type: meta.type } : {}),
164
- ...(meta.required !== undefined ? { required: meta.required } : {}),
165
- ...(meta.title ? { title: meta.title } : {}),
166
- ...(meta.description ? { description: meta.description } : {}),
167
- ...(meta.notes ? { notes: meta.notes } : {}),
168
- ...(meta.enum ? { enum: [...meta.enum] } : {}),
169
- ...(meta.minimum !== undefined ? { minimum: meta.minimum } : {}),
170
- ...(meta.maximum !== undefined ? { maximum: meta.maximum } : {}),
171
- ...(meta.minLength !== undefined ? { minLength: meta.minLength } : {}),
172
- ...(meta.maxLength !== undefined ? { maxLength: meta.maxLength } : {}),
173
- ...(meta.pattern ? { pattern: meta.pattern } : {}),
174
- ...(meta.itemType ? { itemType: meta.itemType } : {}),
175
- ...(meta.examples ? { examples: [...meta.examples] } : {}),
176
- };
177
- byPath.set(path, merged);
178
- }
179
- return Array.from(byPath.values()).sort((a, b) => a.path.localeCompare(b.path));
180
- };
181
103
  const propertyFingerprint = (properties) => JSON.stringify(properties.map((p) => ({
182
104
  path: p.path,
183
105
  type: p.type,
184
106
  required: p.required,
185
107
  defaultValue: p.defaultValue,
186
108
  })));
187
- const renderPropertyHtml = (prop) => {
109
+ // A type label names a documented type by its definition name; link those names to it.
110
+ const renderType = (type, anchor) => type
111
+ .split(' | ')
112
+ .map((member) => {
113
+ const id = anchor(member);
114
+ const code = `<code>${escapeHtml(member)}</code>`;
115
+ return id ? `<a href="#${id}">${code}</a>` : code;
116
+ })
117
+ .join(' | ');
118
+ const renderPropertyHtml = (prop, heading, anchor) => {
188
119
  const rows = [];
189
- rows.push(`<li><strong>Type:</strong> <code>${escapeHtml(prop.type)}</code></li>`);
120
+ rows.push(`<li><strong>Type:</strong> ${renderType(prop.type, anchor)}</li>`);
190
121
  rows.push(`<li><strong>Required:</strong> <code>${String(prop.required)}</code></li>`);
191
122
  if (prop.itemType) {
192
- rows.push(`<li><strong>Item Type:</strong> <code>${escapeHtml(prop.itemType)}</code></li>`);
123
+ rows.push(`<li><strong>Item Type:</strong> ${renderType(prop.itemType, anchor)}</li>`);
193
124
  }
194
125
  if (prop.description) {
195
126
  rows.push(`<li><strong>Description:</strong> ${escapeHtml(prop.description)}</li>`);
@@ -214,36 +145,40 @@ const renderPropertyHtml = (prop) => {
214
145
  rows.push(`<li><strong>Constraints:</strong> ${constraints.join(', ')}</li>`);
215
146
  }
216
147
  rows.push(`<li><strong>Default:</strong> ${prop.defaultValue !== undefined ? formatValue(prop.defaultValue) : '<em>none</em>'}</li>`);
217
- if (prop.examples && prop.examples.length > 0) {
218
- rows.push(`<li><strong>Examples:</strong> ${prop.examples
219
- .map((v) => `<code>${escapeHtml(formatInline(v))}</code>`)
220
- .join(', ')}</li>`);
221
- }
222
- if (prop.notes) {
223
- rows.push(`<li><strong>Notes:</strong> ${escapeHtml(prop.notes)}</li>`);
224
- }
225
148
  return `
226
149
  <section class="doc-property">
227
- <h3><code>${escapeHtml(prop.path)}</code></h3>
150
+ <${heading}><code>${escapeHtml(prop.path)}</code></${heading}>
228
151
  <ul>
229
152
  ${rows.join('\n ')}
230
153
  </ul>
231
154
  </section>`;
232
155
  };
233
- const renderPropertiesSectionHtml = (title, source, properties) => {
234
- if (properties.length === 0) {
235
- return `
236
- <section class="doc-section">
237
- <h2>${escapeHtml(title)}</h2>
238
- <p><strong>Source:</strong> <code>${escapeHtml(source)}</code></p>
239
- <p>No properties found.</p>
240
- </section>`;
241
- }
156
+ const renderPropertiesSectionHtml = (opts) => {
157
+ const sources = [
158
+ opts.schema && `<strong>Schema:</strong> <code>${escapeHtml(opts.schema)}</code>`,
159
+ opts.defaults && `<strong>Defaults:</strong> <code>${escapeHtml(opts.defaults)}</code>`,
160
+ ].filter(Boolean);
161
+ // Each schema carries its own definitions, so anchors are scoped to the section.
162
+ const prefix = opts.title.toLowerCase();
163
+ const names = new Set(opts.docs.definitions.map((definition) => definition.name));
164
+ const anchor = (name) => names.has(name) ? `${prefix}-${encodeURIComponent(name)}` : undefined;
165
+ const body = opts.docs.properties.length > 0
166
+ ? opts.docs.properties.map((prop) => renderPropertyHtml(prop, 'h3', anchor)).join('\n')
167
+ : '<p>No properties found.</p>';
168
+ const definitions = opts.docs.definitions
169
+ .map((definition) => `
170
+ <section class="doc-definition" id="${anchor(definition.name)}">
171
+ <h3>Type <code>${escapeHtml(definition.name)}</code></h3>
172
+ ${definition.description ? `<p>${escapeHtml(definition.description)}</p>` : ''}
173
+ ${definition.properties.map((prop) => renderPropertyHtml(prop, 'h4', anchor)).join('\n')}
174
+ </section>`)
175
+ .join('\n');
242
176
  return `
243
177
  <section class="doc-section">
244
- <h2>${escapeHtml(title)}</h2>
245
- <p><strong>Source:</strong> <code>${escapeHtml(source)}</code></p>
246
- ${properties.map((prop) => renderPropertyHtml(prop)).join('\n')}
178
+ <h2>${escapeHtml(opts.title)}</h2>
179
+ ${sources.length > 0 ? `<p>${sources.join(' · ')}</p>` : ''}
180
+ ${body}
181
+ ${definitions}
247
182
  </section>`;
248
183
  };
249
184
  const wrapHtmlPage = (title, body) => `
@@ -261,6 +196,7 @@ const wrapHtmlPage = (title, body) => `
261
196
  code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
262
197
  .doc-section { margin-bottom: 2rem; }
263
198
  .doc-property { margin: 0 0 1.25rem 0; padding-bottom: 0.5rem; border-bottom: 1px solid rgba(128,128,128,.25); }
199
+ .doc-definition { margin: 1.5rem 0 0 0; padding-left: 1rem; border-left: 3px solid rgba(128,128,128,.35); }
264
200
  ul { padding-left: 1.25rem; }
265
201
  .meta-list li { margin-bottom: .25rem; }
266
202
  </style>
@@ -300,10 +236,28 @@ const renderElementIndexHtml = (opts) => wrapHtmlPage(`${opts.contract.elementNa
300
236
  </ul>
301
237
  </section>
302
238
  `);
303
- const renderViewHtml = (opts) => wrapHtmlPage(`${opts.contract.elementName} ${opts.view} docs`, `
304
- <h1>${escapeHtml(opts.contract.elementName)} / ${escapeHtml(opts.view)}</h1>
305
- ${renderPropertiesSectionHtml('Model Defaults', opts.pieSource, opts.pieProperties)}
306
- ${renderPropertiesSectionHtml('Authoring/Configuration Defaults', opts.configSource, opts.configProperties)}
239
+ const schemaSpecifier = (contract, kind) => `${contract.packageName}/schemas/${kind}.schema.json`;
240
+ const renderViewHtml = (opts) => wrapHtmlPage(`${opts.contract.elementName} ${opts.view.view} docs`, `
241
+ <h1>${escapeHtml(opts.contract.elementName)} / ${escapeHtml(opts.view.view)}</h1>
242
+ ${renderPropertiesSectionHtml({
243
+ title: 'Model',
244
+ schema: schemaSpecifier(opts.contract, 'model'),
245
+ defaults: opts.view.pieSource,
246
+ docs: opts.view.pieDocs,
247
+ })}
248
+ ${opts.kinds.includes('configuration')
249
+ ? renderPropertiesSectionHtml({
250
+ title: 'Configuration',
251
+ schema: schemaSpecifier(opts.contract, 'configuration'),
252
+ defaults: opts.view.configSource,
253
+ docs: opts.view.configDocs,
254
+ })
255
+ : ''}
256
+ ${renderPropertiesSectionHtml({
257
+ title: 'Session',
258
+ schema: schemaSpecifier(opts.contract, 'session'),
259
+ docs: opts.sessionDocs,
260
+ })}
307
261
  `);
308
262
  const chooseViewDocs = (resolvedViews) => {
309
263
  const map = new Map(resolvedViews.map((v) => [v.view, v]));
@@ -319,9 +273,9 @@ const chooseViewDocs = (resolvedViews) => {
319
273
  }
320
274
  if (print) {
321
275
  const deliveryFingerprint = delivery
322
- ? `${propertyFingerprint(delivery.pieProperties)}|${propertyFingerprint(delivery.configProperties)}`
276
+ ? `${propertyFingerprint(delivery.pieDocs.properties)}|${propertyFingerprint(delivery.configDocs.properties)}`
323
277
  : '';
324
- const printFingerprint = `${propertyFingerprint(print.pieProperties)}|${propertyFingerprint(print.configProperties)}`;
278
+ const printFingerprint = `${propertyFingerprint(print.pieDocs.properties)}|${propertyFingerprint(print.configDocs.properties)}`;
325
279
  const sourceDiffFromDelivery = delivery
326
280
  ? print.pieSource !== delivery.pieSource || print.configSource !== delivery.configSource
327
281
  : true;
@@ -355,16 +309,22 @@ export const generateDocsForContract = async (packageDir, contract, options) =>
355
309
  views: [],
356
310
  sourcesUsed: [],
357
311
  };
312
+ // Property rows come from the schemas the package publishes, generated here from source so the
313
+ // docs do not depend on the element having been built.
314
+ const schemas = generateElementSchemas(packageDir);
315
+ if (!schemas.model || !schemas.session) {
316
+ throw new Error(`${contract.packageName} publishes no model or session schema to document`);
317
+ }
318
+ const kinds = Object.keys(schemas);
319
+ const sessionDocs = schemaDocs(schemas.session);
358
320
  const resolvedViews = [];
359
321
  for (const view of contract.views) {
360
322
  const pieResolved = await loadValueWithFallback(packageDir, view.pie, 'pie');
361
323
  const configResolved = await loadValueWithFallback(packageDir, view.config, 'config');
362
- const pieProperties = applyMetadata(flattenProperties(pieResolved.value).sort((a, b) => a.path.localeCompare(b.path)), view.metadata?.pie);
363
- const configProperties = applyMetadata(flattenProperties(configResolved.value).sort((a, b) => a.path.localeCompare(b.path)), view.metadata?.config);
364
324
  resolvedViews.push({
365
325
  view: view.view,
366
- pieProperties,
367
- configProperties,
326
+ pieDocs: schemaDocs(schemas.model, pieResolved.value),
327
+ configDocs: schemaDocs(schemas.configuration, configResolved.value),
368
328
  pieSource: pieResolved.sourceLabel,
369
329
  configSource: configResolved.sourceLabel,
370
330
  });
@@ -390,8 +350,8 @@ export const generateDocsForContract = async (packageDir, contract, options) =>
390
350
  file: `${view.view}.html`,
391
351
  pieSource: view.pieSource,
392
352
  configSource: view.configSource,
393
- hasModelDefaults: view.pieProperties.length > 0,
394
- hasConfigDefaults: view.configProperties.length > 0,
353
+ hasModelDefaults: view.pieDocs.properties.length > 0,
354
+ hasConfigDefaults: view.configDocs.properties.length > 0,
395
355
  })),
396
356
  };
397
357
  const candidates = [
@@ -402,14 +362,7 @@ export const generateDocsForContract = async (packageDir, contract, options) =>
402
362
  },
403
363
  ...emittedViews.map((view) => ({
404
364
  path: join(elementOutputDir, `${view.view}.html`),
405
- content: renderViewHtml({
406
- contract,
407
- view: view.view,
408
- pieSource: view.pieSource,
409
- configSource: view.configSource,
410
- pieProperties: view.pieProperties,
411
- configProperties: view.configProperties,
412
- }),
365
+ content: renderViewHtml({ contract, view, kinds, sessionDocs }),
413
366
  })),
414
367
  ];
415
368
  for (const candidate of candidates) {
@@ -434,8 +387,8 @@ export const generateDocsForContract = async (packageDir, contract, options) =>
434
387
  sourcesUsed.add(view.configSource);
435
388
  outputs.views.push({
436
389
  view: view.view,
437
- pieProperties: view.pieProperties,
438
- configProperties: view.configProperties,
390
+ pieProperties: view.pieDocs.properties,
391
+ configProperties: view.configDocs.properties,
439
392
  });
440
393
  }
441
394
  outputs.sourcesUsed = Array.from(sourcesUsed).sort();