Skip to content

Commit ee87966

Browse files
authored
feat: Add helper to write documentation files (#67)
1 parent a90c4e5 commit ee87966

File tree

8 files changed

+105
-38
lines changed

8 files changed

+105
-38
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
"*.js",
1616
"*.d.ts"
1717
],
18+
"exports": {
19+
".": "./index.js"
20+
},
1821
"scripts": {
1922
"lint": "eslint --ignore-path .gitignore --ext js,ts,tsx .",
2023
"prebuild": "rimraf lib",

src/components/index.ts

Lines changed: 54 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
3+
import fs from 'node:fs';
34
import { pascalCase } from 'change-case';
45
import pathe from 'pathe';
56
import { matcher } from 'micromatch';
@@ -21,46 +22,65 @@ export interface DocumenterOptions {
2122
extraExports?: Record<string, Array<string>>;
2223
}
2324

25+
export interface WriteOptions {
26+
outDir: string;
27+
}
28+
29+
export function writeComponentsDocumentation({ outDir, ...options }: WriteOptions & DocumenterOptions): void {
30+
const definitions = documentComponents(options);
31+
fs.mkdirSync(outDir, { recursive: true });
32+
for (const definition of definitions) {
33+
fs.writeFileSync(
34+
pathe.join(outDir, definition.dashCaseName + '.js'),
35+
`module.exports = ${JSON.stringify(definition, null, 2)};`
36+
);
37+
}
38+
const indexContent = `module.exports = {
39+
${definitions
40+
.map(definition => `${JSON.stringify(definition.dashCaseName)}:require('./${definition.dashCaseName}')`)
41+
.join(',\n')}
42+
}`;
43+
fs.writeFileSync(pathe.join(outDir, 'index.js'), indexContent);
44+
fs.copyFileSync(require.resolve('./interfaces.d.ts'), pathe.join(outDir, 'interfaces.d.ts'));
45+
fs.writeFileSync(
46+
pathe.join(outDir, 'index.d.ts'),
47+
`import { ComponentDefinition } from './interfaces';
48+
const definitions: Record<string, ComponentDefinition>;
49+
export default definitions;
50+
`
51+
);
52+
}
53+
2454
export function documentComponents(options: DocumenterOptions): Array<ComponentDefinition> {
2555
const program = bootstrapTypescriptProject(options.tsconfigPath);
2656
const checker = program.getTypeChecker();
2757

2858
const isMatch = matcher(pathe.resolve(options.publicFilesGlob));
2959

30-
return program
31-
.getSourceFiles()
32-
.filter(file => isMatch(file.fileName))
33-
.map(sourceFile => {
34-
const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
35-
const { name, dashCaseName } = componentNameFromPath(sourceFile.fileName);
60+
const sourceFiles = program.getSourceFiles().filter(file => isMatch(file.fileName));
61+
62+
if (sourceFiles.length === 0) {
63+
throw new Error(`No files found matching ${options.publicFilesGlob}`);
64+
}
65+
66+
return sourceFiles.map(sourceFile => {
67+
const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
68+
const { name, dashCaseName } = componentNameFromPath(sourceFile.fileName);
3669

37-
// istanbul ignore next
38-
if (!moduleSymbol) {
39-
throw new Error(`Unable to resolve module: ${sourceFile.fileName}`);
40-
}
41-
const exportSymbols = checker.getExportsOfModule(moduleSymbol);
42-
const { propsSymbol, componentSymbol } = extractExports(
43-
name,
44-
exportSymbols,
45-
checker,
46-
options?.extraExports ?? {}
47-
);
48-
const props = extractProps(propsSymbol, checker);
49-
const functions = extractFunctions(propsSymbol, checker);
50-
const defaultValues = extractDefaultValues(componentSymbol, checker);
51-
const componentDescription = getDescription(
52-
componentSymbol.getDocumentationComment(checker),
53-
extractDeclaration(componentSymbol)
54-
);
70+
// istanbul ignore next
71+
if (!moduleSymbol) {
72+
throw new Error(`Unable to resolve module: ${sourceFile.fileName}`);
73+
}
74+
const exportSymbols = checker.getExportsOfModule(moduleSymbol);
75+
const { propsSymbol, componentSymbol } = extractExports(name, exportSymbols, checker, options?.extraExports ?? {});
76+
const props = extractProps(propsSymbol, checker);
77+
const functions = extractFunctions(propsSymbol, checker);
78+
const defaultValues = extractDefaultValues(componentSymbol, checker);
79+
const componentDescription = getDescription(
80+
componentSymbol.getDocumentationComment(checker),
81+
extractDeclaration(componentSymbol)
82+
);
5583

56-
return buildComponentDefinition(
57-
name,
58-
dashCaseName,
59-
props,
60-
functions,
61-
defaultValues,
62-
componentDescription,
63-
checker
64-
);
65-
});
84+
return buildComponentDefinition(name, dashCaseName, props, functions, defaultValues, componentDescription, checker);
85+
});
6686
}

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33
export * from './components/interfaces';
4-
export { documentComponents } from './components';
4+
export { documentComponents, writeComponentsDocumentation } from './components';
55
export { documentTestUtils } from './test-utils';

test/components/errors.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ test('should throw in case of configuration errors', () => {
77
expect(() => buildProject('errors-config')).toThrow('Failed to parse tsconfig.json');
88
});
99

10+
test('should throw if tsconfig is not found', () => {
11+
expect(() => buildProject('fixture-does-not-exist')).toThrow('Failed to read tsconfig.json');
12+
});
13+
14+
test('should throw if no components in the output', () => {
15+
expect(() => buildProject('simple', { publicFilesGlob: 'does-not-exist' })).toThrow(
16+
'No files found matching does-not-exist'
17+
);
18+
});
19+
1020
test('should throw in case of type errors', () => {
1121
expect(() => buildProject('errors-types')).toThrow('Compilation failed');
1222
});

test/components/test-helpers.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
3+
import fs from 'node:fs';
4+
import os from 'node:os';
5+
import pathe from 'pathe';
36
import ts from 'typescript';
47
import { ProjectReflection } from 'typedoc';
58
import { documentComponents, documentTestUtils } from '../../src';
@@ -9,12 +12,16 @@ import { DocumenterOptions } from '../../src/components';
912

1013
export function buildProject(name: string, options?: Partial<DocumenterOptions>) {
1114
return documentComponents({
12-
tsconfigPath: require.resolve(`../../fixtures/components/${name}/tsconfig.json`),
15+
tsconfigPath: pathe.resolve(`fixtures/components/${name}/tsconfig.json`),
1316
publicFilesGlob: `fixtures/components/${name}/*/index.tsx`,
1417
...options,
1518
});
1619
}
1720

21+
export function getTemporaryDir() {
22+
return fs.mkdtempSync(pathe.join(os.tmpdir(), 'documenter-'));
23+
}
24+
1825
export function buildTestUtilsProject(name: string, testGlob?: string): TestUtilsDoc[] {
1926
return documentTestUtils(
2027
{

test/components/writer.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import { expect, test } from 'vitest';
4+
import fs from 'node:fs';
5+
import pathe from 'pathe';
6+
import { getTemporaryDir } from './test-helpers';
7+
// must use compiled artifacts, because the code relies on generated files
8+
import { writeComponentsDocumentation } from '../../lib';
9+
10+
test('should write documentation files into the outDir', async () => {
11+
const outDir = getTemporaryDir();
12+
expect(fs.readdirSync(outDir)).toHaveLength(0);
13+
14+
writeComponentsDocumentation({
15+
tsconfigPath: pathe.resolve('fixtures/components/simple/tsconfig.json'),
16+
publicFilesGlob: 'fixtures/components/simple/*/index.tsx',
17+
outDir,
18+
});
19+
20+
expect(fs.readdirSync(outDir)).toEqual(['index.d.ts', 'index.js', 'interfaces.d.ts', 'simple.js']);
21+
const { default: definitions } = await import(pathe.join(outDir, 'index.js'));
22+
expect(definitions).toEqual({
23+
simple: expect.objectContaining({ name: 'Simple', dashCaseName: 'simple' }),
24+
});
25+
});

tsconfig.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
"strict": true,
1010
"allowSyntheticDefaultImports": true,
1111
"esModuleInterop": true,
12-
"forceConsistentCasingInFileNames": true
12+
"forceConsistentCasingInFileNames": true,
13+
"sourceMap": true,
14+
"inlineSources": true
1315
},
1416
"include": ["src"]
1517
}

vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default defineConfig({
88
coverage: {
99
enabled: process.env.CI === 'true',
1010
provider: 'v8',
11-
include: ['src/**'],
11+
include: ['src/**', 'lib/**'],
1212
},
1313
},
1414
});

0 commit comments

Comments
 (0)