@ankhorage/paradox 0.1.17 → 0.1.18

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.18
4
+
5
+ ### Patch Changes
6
+
7
+ - eaa56ad: Render README Configuration examples from the actual tagged Paradox config file instead of synthesized boilerplate.
8
+
3
9
  ## 0.1.17
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  # @ankhorage/paradox
5
5
 
6
- ![license: MIT](./paradox/badges/license.svg) ![npm: v0.1.17](./paradox/badges/npm.svg) ![runtime: bun](./paradox/badges/runtime.svg) ![typescript: strict](./paradox/badges/typescript.svg) ![eslint: checked](./paradox/badges/eslint.svg) ![prettier: checked](./paradox/badges/prettier.svg) ![build: checked](./paradox/badges/build.svg) ![tests: checked](./paradox/badges/tests.svg) ![docs: paradox](./paradox/badges/docs.svg)
6
+ ![license: MIT](./paradox/badges/license.svg) ![npm: v0.1.18](./paradox/badges/npm.svg) ![runtime: bun](./paradox/badges/runtime.svg) ![typescript: strict](./paradox/badges/typescript.svg) ![eslint: checked](./paradox/badges/eslint.svg) ![prettier: checked](./paradox/badges/prettier.svg) ![build: checked](./paradox/badges/build.svg) ![tests: checked](./paradox/badges/tests.svg) ![docs: paradox](./paradox/badges/docs.svg)
7
7
 
8
8
  Deterministic documentation generator for TypeScript packages.
9
9
 
@@ -66,13 +66,26 @@ sequenceDiagram
66
66
 
67
67
  ## Configuration
68
68
 
69
- Create a `paradox.config.ts` file:
69
+ Canonical Paradox configuration for this package.
70
70
 
71
71
  ```ts
72
- import { defineParadoxConfig } from '@ankhorage/paradox';
72
+ import { defineParadoxConfig } from './src/config/defineParadoxConfig.js';
73
73
 
74
74
  export default defineParadoxConfig({
75
- // ...
75
+ mode: 'write',
76
+
77
+ docs: {
78
+ title: '@ankhorage/paradox',
79
+ description: 'Deterministic documentation generator for TypeScript packages.',
80
+ },
81
+
82
+ package: {
83
+ entrypoints: ['src/index.ts'],
84
+ },
85
+
86
+ output: {
87
+ dir: 'paradox',
88
+ },
76
89
  });
77
90
  ```
78
91
 
@@ -5,4 +5,5 @@ import type { AnalysisResult } from './types.js';
5
5
  */
6
6
  export declare function analyze(config: ParadoxConfig, runtime: {
7
7
  packageRoot: string;
8
+ configFilePath?: string;
8
9
  }): Promise<AnalysisResult>;
@@ -5,6 +5,7 @@ import { analyzeComponents } from './components.js';
5
5
  import { analyzeExports } from './exports.js';
6
6
  import { analyzeModules } from './modules.js';
7
7
  import { createProject } from './project.js';
8
+ import { analyzeReadmeConfig } from './readmeConfig.js';
8
9
  import { analyzeReadmeUsage } from './readmeUsage.js';
9
10
  import { createTypeScriptProgram } from './semantic/createTypeScriptProgram.js';
10
11
  import { collectTypeMembers, resolveTypeReference } from './semantic/exports.js';
@@ -25,6 +26,10 @@ export async function analyze(config, runtime) {
25
26
  const readmeUsageDescription = config.docs?.usage?.description ?? null;
26
27
  const usageEntryPoints = config.docs?.usage?.entrypoints ?? [];
27
28
  const readmeUsage = await analyzeReadmeUsage({ root, entrypoints: usageEntryPoints });
29
+ const readmeConfig = await analyzeReadmeConfig({
30
+ root,
31
+ configFilePath: runtime.configFilePath ?? null,
32
+ });
28
33
  const program = createTypeScriptProgram({ root, entrypoints, project });
29
34
  const { config: configMetadata, exports } = analyzeExports(project, { root, entrypoints });
30
35
  const components = analyzeComponents(exports, { program });
@@ -70,6 +75,7 @@ export async function analyze(config, runtime) {
70
75
  usage,
71
76
  readmeUsageDescription,
72
77
  readmeUsage,
78
+ readmeConfig,
73
79
  config: configMetadata
74
80
  ? {
75
81
  exportName: configMetadata.exportName,
@@ -0,0 +1,14 @@
1
+ export interface AnalysisReadmeConfig {
2
+ description: string | null;
3
+ language: string;
4
+ code: string;
5
+ sourcePath: string;
6
+ }
7
+ /***
8
+ * Collects a README configuration example from the actual Paradox config file when its
9
+ * leading Paradox comment is marked with both @config and @readme.
10
+ */
11
+ export declare function analyzeReadmeConfig(options: {
12
+ root: string;
13
+ configFilePath: string | null;
14
+ }): Promise<AnalysisReadmeConfig | null>;
@@ -0,0 +1,57 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { extname, relative } from 'node:path';
3
+ import { parseParadoxComment } from './utils/parseParadoxComment.js';
4
+ /***
5
+ * Collects a README configuration example from the actual Paradox config file when its
6
+ * leading Paradox comment is marked with both @config and @readme.
7
+ */
8
+ export async function analyzeReadmeConfig(options) {
9
+ if (options.configFilePath === null)
10
+ return null;
11
+ const source = await readFile(options.configFilePath, 'utf-8');
12
+ const match = findLeadingParadoxComment(source);
13
+ if (match === null)
14
+ return null;
15
+ const parsed = parseParadoxComment(match.comment);
16
+ if (!parsed.isConfig || !parsed.isReadme)
17
+ return null;
18
+ const sourcePath = toPosixPath(relative(options.root, options.configFilePath));
19
+ return {
20
+ description: parsed.description,
21
+ language: getLanguage(sourcePath),
22
+ code: removeRange(source, match.start, match.end).trim(),
23
+ sourcePath,
24
+ };
25
+ }
26
+ function findLeadingParadoxComment(source) {
27
+ const match = /^\s*(\/\*\*\*[\s\S]*?\*\/)/.exec(source);
28
+ const comment = match?.[1];
29
+ if (match === null || comment === undefined)
30
+ return null;
31
+ const start = match[0].indexOf(comment);
32
+ return {
33
+ comment,
34
+ start,
35
+ end: start + comment.length,
36
+ };
37
+ }
38
+ function removeRange(source, start, end) {
39
+ const before = source.slice(0, start).trimEnd();
40
+ const after = source.slice(end).trimStart();
41
+ if (before.length === 0)
42
+ return after;
43
+ if (after.length === 0)
44
+ return before;
45
+ return `${before}\n\n${after}`;
46
+ }
47
+ function getLanguage(sourcePath) {
48
+ const extension = extname(sourcePath).toLowerCase();
49
+ if (extension === '.ts')
50
+ return 'ts';
51
+ if (extension === '.js' || extension === '.mjs' || extension === '.cjs')
52
+ return 'js';
53
+ return '';
54
+ }
55
+ function toPosixPath(path) {
56
+ return path.replaceAll('\\', '/');
57
+ }
@@ -86,6 +86,12 @@ interface AnalysisReadmeUsage {
86
86
  code: string;
87
87
  sourcePath: string;
88
88
  }
89
+ interface AnalysisReadmeConfig {
90
+ description: string | null;
91
+ language: string;
92
+ code: string;
93
+ sourcePath: string;
94
+ }
89
95
  export interface AnalysisBadge {
90
96
  id: string;
91
97
  label: string;
@@ -165,6 +171,7 @@ export interface AnalysisResult {
165
171
  usage: AnalysisUsage | null;
166
172
  readmeUsageDescription: string | null;
167
173
  readmeUsage: AnalysisReadmeUsage[];
174
+ readmeConfig: AnalysisReadmeConfig | null;
168
175
  config: {
169
176
  exportName: string;
170
177
  isReadme: boolean;
@@ -24,7 +24,7 @@ async function main() {
24
24
  const config = await loadParadoxConfig(configFilePath);
25
25
  const packageRoot = await resolvePackageRoot(config, configDir);
26
26
  const { outputDir, outputRoot } = resolveOutputRoot(config, packageRoot);
27
- const analysis = await analyze(config, { packageRoot });
27
+ const analysis = await analyze(config, { packageRoot, configFilePath });
28
28
  const model = buildModel(analysis);
29
29
  const result = render(model, { outputDir });
30
30
  await write(result, config, { packageRoot, outputRoot });
@@ -8,8 +8,8 @@ export declare const PARADOX_DOC_TAGS: readonly [{
8
8
  }, {
9
9
  readonly name: "config";
10
10
  readonly syntax: "@config";
11
- readonly description: "Marks a type or interface as part of the Paradox configuration model. @config alone does not imply README inclusion; use @config plus @readme for README output.";
12
- readonly appliesTo: readonly ["interface", "type"];
11
+ readonly description: "Marks a configuration type, interface, or source block. Pair with @readme to include the schema or actual config source in README Configuration output.";
12
+ readonly appliesTo: readonly ["block", "interface", "type"];
13
13
  readonly repeatable: false;
14
14
  readonly handler: "markConfig";
15
15
  }, {
@@ -19,8 +19,8 @@ export const PARADOX_DOC_TAGS = [
19
19
  {
20
20
  name: 'config',
21
21
  syntax: '@config',
22
- description: 'Marks a type or interface as part of the Paradox configuration model. @config alone does not imply README inclusion; use @config plus @readme for README output.',
23
- appliesTo: ['interface', 'type'],
22
+ description: 'Marks a configuration type, interface, or source block. Pair with @readme to include the schema or actual config source in README Configuration output.',
23
+ appliesTo: ['block', 'interface', 'type'],
24
24
  repeatable: false,
25
25
  handler: 'markConfig',
26
26
  },
@@ -115,6 +115,12 @@ interface BuildModelInput {
115
115
  code: string;
116
116
  sourcePath: string;
117
117
  }[];
118
+ readmeConfig: {
119
+ description: string | null;
120
+ language: string;
121
+ code: string;
122
+ sourcePath: string;
123
+ } | null;
118
124
  config: {
119
125
  exportName: string;
120
126
  isReadme: boolean;
@@ -34,14 +34,18 @@ export function buildModel(analysis) {
34
34
  sourcePath: usageEntry.sourcePath,
35
35
  }))
36
36
  .sort((left, right) => left.sourcePath.localeCompare(right.sourcePath)),
37
+ readmeConfig: analysis.readmeConfig !== null
38
+ ? {
39
+ description: analysis.readmeConfig.description,
40
+ language: analysis.readmeConfig.language,
41
+ code: analysis.readmeConfig.code,
42
+ sourcePath: analysis.readmeConfig.sourcePath,
43
+ }
44
+ : null,
37
45
  config: analysis.config !== null
38
46
  ? {
39
47
  exportName: analysis.config.exportName,
40
48
  isReadme: analysis.config.isReadme,
41
- configFile: getDefaultConfigFileName(analysis.packageId),
42
- factoryName: findConfigFactoryName(analysis.config.exportName, [
43
- ...exportsByName.keys(),
44
- ]),
45
49
  members: analysis.config.members,
46
50
  }
47
51
  : null,
@@ -150,23 +154,6 @@ function mapComponent(component, exportModel) {
150
154
  }))),
151
155
  };
152
156
  }
153
- /***
154
- * Finds the conventional config factory export for a config type when present.
155
- */
156
- function findConfigFactoryName(configExportName, exportNames) {
157
- const prefix = configExportName.endsWith('Config')
158
- ? configExportName.slice(0, -'Config'.length)
159
- : configExportName;
160
- const expectedFactoryName = `define${prefix}Config`;
161
- return exportNames.includes(expectedFactoryName) ? expectedFactoryName : null;
162
- }
163
- /***
164
- * Derives the default config file name from a package id.
165
- */
166
- function getDefaultConfigFileName(packageId) {
167
- const packageBaseName = packageId.split('/').pop() ?? packageId;
168
- return `${packageBaseName}.config.ts`;
169
- }
170
157
  /***
171
158
  * Returns a copy of items sorted by their `name` property.
172
159
  */
@@ -9,6 +9,7 @@ export interface DocumentationModel {
9
9
  usage: UsageModel | null;
10
10
  readmeUsageDescription: string | null;
11
11
  readmeUsage: ReadmeUsageModel[];
12
+ readmeConfig: ReadmeConfigModel | null;
12
13
  config: ConfigModel | null;
13
14
  entrypoints: string[];
14
15
  modules: ModuleModel[];
@@ -39,11 +40,15 @@ interface ReadmeUsageModel {
39
40
  code: string;
40
41
  sourcePath: string;
41
42
  }
43
+ interface ReadmeConfigModel {
44
+ description: string | null;
45
+ language: string;
46
+ code: string;
47
+ sourcePath: string;
48
+ }
42
49
  interface ConfigModel {
43
50
  exportName: string;
44
51
  isReadme: boolean;
45
- configFile: string;
46
- factoryName: string | null;
47
52
  members: ConfigMemberModel[];
48
53
  }
49
54
  export interface ExportModel {
@@ -31,8 +31,7 @@ function renderReadme(model, outputDir, badges, diagrams) {
31
31
  lines.push('```', '');
32
32
  }
33
33
  renderCliScenarios(lines, model, outputDir, diagrams);
34
- if (model.config?.isReadme)
35
- renderConfiguration(lines, model);
34
+ renderConfiguration(lines, model);
36
35
  renderGeneratedDocumentation(lines, outputDir, diagrams);
37
36
  renderReadmeApi(lines, model);
38
37
  return `${lines.join('\n').trimEnd()}\n`;
@@ -88,30 +87,19 @@ function findScenarioDiagram(diagrams, scenario) {
88
87
  return diagrams.find((diagram) => diagram.path === `diagrams/sequences/${toFileStem(scenario.name)}.mmd`);
89
88
  }
90
89
  function renderConfiguration(lines, model) {
91
- const { config } = model;
92
- if (config === null)
90
+ const config = model.config?.isReadme ? model.config : null;
91
+ const example = model.readmeConfig;
92
+ if (config === null && example === null)
93
93
  return;
94
94
  lines.push('## Configuration', '');
95
- lines.push(`Create a \`${config.configFile}\` file:`, '');
96
- lines.push('```ts');
97
- if (config.factoryName !== null) {
98
- lines.push(`import { ${config.factoryName} } from '${model.packageId}';`);
99
- lines.push('');
100
- lines.push(`export default ${config.factoryName}({`);
101
- lines.push(' // ...');
102
- lines.push('});');
103
- }
104
- else {
105
- lines.push(`import type { ${config.exportName} } from '${model.packageId}';`);
106
- lines.push('');
107
- lines.push('const config = {');
108
- lines.push(' // ...');
109
- lines.push(`} satisfies ${config.exportName};`);
110
- lines.push('');
111
- lines.push('export default config;');
95
+ if (example !== null) {
96
+ if (example.description !== null)
97
+ lines.push(example.description, '');
98
+ lines.push(`\`\`\`${example.language}`);
99
+ lines.push(example.code);
100
+ lines.push('```', '');
112
101
  }
113
- lines.push('```', '');
114
- if (config.members.length === 0)
102
+ if (config === null || config.members.length === 0)
115
103
  return;
116
104
  lines.push('<details>');
117
105
  lines.push('<summary>Configuration options</summary>', '');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ankhorage/paradox",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Deterministic documentation generator for TypeScript packages.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {