react-lib-tools 0.0.1
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/LICENSE.md +21 -0
- package/dist/react-lib-tools.cjs +8 -0
- package/dist/react-lib-tools.cjs.map +1 -0
- package/dist/react-lib-tools.css +1 -0
- package/dist/react-lib-tools.d.ts +177 -0
- package/dist/react-lib-tools.js +9373 -0
- package/dist/react-lib-tools.js.map +1 -0
- package/package.json +110 -0
- package/scripts/compile-docs.ts +22 -0
- package/scripts/compile-examples.ts +71 -0
- package/scripts/utils/docs/compileComponent.ts +116 -0
- package/scripts/utils/docs/compileComponents.ts +74 -0
- package/scripts/utils/docs/compileImperativeHandle.ts +48 -0
- package/scripts/utils/docs/compileImperativeHandles.ts +45 -0
- package/scripts/utils/docs/formatDescriptionText.ts +11 -0
- package/scripts/utils/docs/getPropTypeText.ts +28 -0
- package/scripts/utils/docs/insertPropsMarkdown.ts +29 -0
- package/scripts/utils/docs/parseDescription.ts +59 -0
- package/scripts/utils/docs/propsToTable.ts +48 -0
- package/scripts/utils/examples/trimExcludedText.ts +13 -0
- package/scripts/utils/getFilesWithExtensions.ts +31 -0
- package/scripts/utils/initialize.ts +34 -0
- package/scripts/utils/rmFilesWithExtensions.ts +13 -0
- package/scripts/utils/syntax-highlight.ts +255 -0
- package/styles.css +127 -0
package/package.json
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-lib-tools",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"author": "Brian Vaughn <brian.david.vaughn@gmail.com> (https://github.com/bvaughn/)",
|
|
6
|
+
"contributors": [
|
|
7
|
+
"Brian Vaughn <brian.david.vaughn@gmail.com> (https://github.com/bvaughn/)"
|
|
8
|
+
],
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/bvaughn/react-lib-tools.git"
|
|
13
|
+
},
|
|
14
|
+
"main": "dist/react-lib-tools.cjs",
|
|
15
|
+
"module": "dist/react-lib-tools.js",
|
|
16
|
+
"types": "dist/react-lib-tools.d.ts",
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"scripts",
|
|
20
|
+
"styles.css"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"dev": "vite",
|
|
24
|
+
"build": "pnpm run build:lib && pnpm run build:docs",
|
|
25
|
+
"build:docs": "TARGET=docs vite build",
|
|
26
|
+
"build:lib": "TARGET=lib vite build",
|
|
27
|
+
"lint": "eslint .",
|
|
28
|
+
"prerelease": "rm -rf dist && pnpm run build:lib",
|
|
29
|
+
"prettier": "prettier --write \"**/*.{css,html,js,json,jsx,ts,tsx}\"",
|
|
30
|
+
"prettier:ci": "prettier --check \"**/*.{css,html,js,json,jsx,ts,tsx}\"",
|
|
31
|
+
"preview": "vite preview",
|
|
32
|
+
"test": "vitest",
|
|
33
|
+
"test:ci": "vitest run",
|
|
34
|
+
"test:debug": "vitest --inspect-brk=127.0.0.1:3000 --no-file-parallelism",
|
|
35
|
+
"tsc": "tsc -b"
|
|
36
|
+
},
|
|
37
|
+
"lint-staged": {
|
|
38
|
+
"**/*": "prettier --write --ignore-unknown"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
42
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@codemirror/lang-css": "latest",
|
|
46
|
+
"@codemirror/lang-html": "latest",
|
|
47
|
+
"@codemirror/lang-javascript": "latest",
|
|
48
|
+
"@codemirror/lang-markdown": "latest",
|
|
49
|
+
"@codemirror/language": "latest",
|
|
50
|
+
"@codemirror/state": "latest",
|
|
51
|
+
"@csstools/postcss-oklab-function": "^4.0.11",
|
|
52
|
+
"@eslint/js": "^9.30.1",
|
|
53
|
+
"@headlessui/react": "^2.2.4",
|
|
54
|
+
"@headlessui/tailwindcss": "^0.2.2",
|
|
55
|
+
"@heroicons/react": "^2.2.0",
|
|
56
|
+
"@lezer/highlight": "latest",
|
|
57
|
+
"@tailwindcss/vite": "^4.1.11",
|
|
58
|
+
"@tailwindplus/elements": "^1.0.5",
|
|
59
|
+
"@testing-library/jest-dom": "^6.6.4",
|
|
60
|
+
"@testing-library/react": "^16.3.0",
|
|
61
|
+
"@testing-library/user-event": "^14.6.1",
|
|
62
|
+
"@ts-ast-parser/core": "^0.8.0",
|
|
63
|
+
"@types/compression": "^1.8.1",
|
|
64
|
+
"@types/express": "^5.0.5",
|
|
65
|
+
"@types/markdown-it": "^14.1.2",
|
|
66
|
+
"@types/node": "^24.2.0",
|
|
67
|
+
"@types/react": "^19.1.8",
|
|
68
|
+
"@types/react-dom": "^19.2.3",
|
|
69
|
+
"@vitejs/plugin-react-swc": "^3.10.2",
|
|
70
|
+
"clsx": "^2.1.1",
|
|
71
|
+
"compression": "^1.8.1",
|
|
72
|
+
"csstype": "^3.1.3",
|
|
73
|
+
"eslint": "^9.30.1",
|
|
74
|
+
"eslint-plugin-react-hooks": "^5.2.0",
|
|
75
|
+
"eslint-plugin-react-refresh": "^0.4.20",
|
|
76
|
+
"express": "^5.1.0",
|
|
77
|
+
"globals": "^16.3.0",
|
|
78
|
+
"husky": "^9.1.7",
|
|
79
|
+
"jsdom": "^26.1.0",
|
|
80
|
+
"lint-staged": "^16.1.4",
|
|
81
|
+
"markdown-it": "^14.1.0",
|
|
82
|
+
"marked": "^16.4.1",
|
|
83
|
+
"postcss": "^8.5.6",
|
|
84
|
+
"prettier": "3.6.2",
|
|
85
|
+
"prettier-plugin-tailwindcss": "^0.7.1",
|
|
86
|
+
"react": "^19.2.3",
|
|
87
|
+
"react-docgen-typescript": "^2.4.0",
|
|
88
|
+
"react-dom": "^19.2.3",
|
|
89
|
+
"react-error-boundary": "^6.0.0",
|
|
90
|
+
"react-router-dom": "^7.6.3",
|
|
91
|
+
"rollup-plugin-terser": "^7.0.2",
|
|
92
|
+
"rollup-plugin-visualizer": "^6.0.3",
|
|
93
|
+
"rollup-preserve-directives": "^1.1.3",
|
|
94
|
+
"sirv": "^3.0.2",
|
|
95
|
+
"tailwind-merge": "^3.3.1",
|
|
96
|
+
"tailwindcss": "^4.1.11",
|
|
97
|
+
"terser": "^5.43.1",
|
|
98
|
+
"ts-blank-space": "^0.6.2",
|
|
99
|
+
"ts-node": "^10.9.2",
|
|
100
|
+
"typescript": "~5.8.3",
|
|
101
|
+
"typescript-eslint": "^8.35.1",
|
|
102
|
+
"typescript-json-schema": "^0.65.1",
|
|
103
|
+
"vite": "^7.0.4",
|
|
104
|
+
"vitest-fail-on-console": "^0.10.1",
|
|
105
|
+
"vite-plugin-dts": "^4.5.4",
|
|
106
|
+
"vite-plugin-svgr": "^4.3.0",
|
|
107
|
+
"vitest": "^3.2.4",
|
|
108
|
+
"zustand": "^5.0.7"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { compileComponents } from "./utils/docs/compileComponents.ts";
|
|
2
|
+
import { compileImperativeHandles } from "./utils/docs/compileImperativeHandles.ts";
|
|
3
|
+
|
|
4
|
+
export async function compileDocs({
|
|
5
|
+
componentNames,
|
|
6
|
+
imperativeHandleNames,
|
|
7
|
+
outputDirName = "docs"
|
|
8
|
+
}: {
|
|
9
|
+
componentNames: string[];
|
|
10
|
+
imperativeHandleNames: string[];
|
|
11
|
+
outputDirName?: string | undefined;
|
|
12
|
+
}) {
|
|
13
|
+
await compileComponents({
|
|
14
|
+
componentNames,
|
|
15
|
+
outputDirName
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
await compileImperativeHandles({
|
|
19
|
+
names: imperativeHandleNames,
|
|
20
|
+
outputDirName
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { initialize } from "./utils/initialize.ts";
|
|
4
|
+
import { syntaxHighlight } from "./utils/syntax-highlight.ts";
|
|
5
|
+
import { trimExcludedText } from "./utils/examples/trimExcludedText.ts";
|
|
6
|
+
|
|
7
|
+
export async function compileExamples({
|
|
8
|
+
fileExtensions = [".html", ".ts", ".tsx"],
|
|
9
|
+
inputPath = ["src", "routes"],
|
|
10
|
+
outputDirName = "examples"
|
|
11
|
+
}: {
|
|
12
|
+
fileExtensions?: string[] | undefined;
|
|
13
|
+
inputPath?: string[] | undefined;
|
|
14
|
+
outputDirName?: string | undefined;
|
|
15
|
+
} = {}) {
|
|
16
|
+
const { files, outputDir } = await initialize({
|
|
17
|
+
fileExtensions,
|
|
18
|
+
inputPath,
|
|
19
|
+
outputDirName
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
for (const file of files) {
|
|
23
|
+
const buffer = await readFile(file);
|
|
24
|
+
|
|
25
|
+
let rawText = buffer.toString();
|
|
26
|
+
|
|
27
|
+
{
|
|
28
|
+
// Remove special comments and directives before syntax highlighting
|
|
29
|
+
rawText = trimExcludedText(rawText);
|
|
30
|
+
|
|
31
|
+
rawText = rawText
|
|
32
|
+
.split("\n")
|
|
33
|
+
.filter(
|
|
34
|
+
(line) =>
|
|
35
|
+
!line.includes("prettier-ignore") &&
|
|
36
|
+
!line.includes("eslint-disable-next-line") &&
|
|
37
|
+
!line.includes("@ts-expect-error") &&
|
|
38
|
+
!line.includes("// hidden")
|
|
39
|
+
)
|
|
40
|
+
.join("\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let html;
|
|
44
|
+
if (file.endsWith(".html")) {
|
|
45
|
+
html = await syntaxHighlight(rawText, "HTML");
|
|
46
|
+
} else if (file.endsWith(".js") || file.endsWith(".jsx")) {
|
|
47
|
+
html = await syntaxHighlight(
|
|
48
|
+
rawText,
|
|
49
|
+
file.endsWith("jsx") ? "JSX" : "JS"
|
|
50
|
+
);
|
|
51
|
+
} else if (file.endsWith(".ts") || file.endsWith(".tsx")) {
|
|
52
|
+
html = await syntaxHighlight(
|
|
53
|
+
rawText,
|
|
54
|
+
file.endsWith("tsx") ? "TSX" : "TS"
|
|
55
|
+
);
|
|
56
|
+
} else {
|
|
57
|
+
throw Error(`Unsupported file type: ${file}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const fileName = basename(file);
|
|
61
|
+
|
|
62
|
+
const outputFile = join(
|
|
63
|
+
outputDir,
|
|
64
|
+
fileName.replace(/\.example\..+$/, ".json")
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
console.debug("Writing to", outputFile);
|
|
68
|
+
|
|
69
|
+
await writeFile(outputFile, JSON.stringify({ html }, null, 2));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { cwd } from "node:process";
|
|
4
|
+
import { type FileParser, type PropItem } from "react-docgen-typescript";
|
|
5
|
+
import type { ComponentMetadata } from "../../../lib/types.ts";
|
|
6
|
+
import { assert } from "../../../lib/utils/assert.ts";
|
|
7
|
+
import { syntaxHighlight } from "../syntax-highlight.ts";
|
|
8
|
+
import { getPropTypeText } from "./getPropTypeText.ts";
|
|
9
|
+
import { parseDescription } from "./parseDescription.ts";
|
|
10
|
+
import { propsToTable } from "./propsToTable.ts";
|
|
11
|
+
|
|
12
|
+
const TOKEN_TO_REPLACE = "TOKEN_TO_REPLACE";
|
|
13
|
+
|
|
14
|
+
export async function compileComponent({
|
|
15
|
+
filePath,
|
|
16
|
+
outputDir,
|
|
17
|
+
parser
|
|
18
|
+
}: {
|
|
19
|
+
filePath: string;
|
|
20
|
+
outputDir: string;
|
|
21
|
+
parser: FileParser;
|
|
22
|
+
}) {
|
|
23
|
+
const parsed = parser.parse(filePath);
|
|
24
|
+
assert(
|
|
25
|
+
parsed.length === 1,
|
|
26
|
+
`Expected 1 parsed component but found ${parsed.length}`
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const component = parsed[0];
|
|
30
|
+
|
|
31
|
+
// Convert to local paths
|
|
32
|
+
component.filePath = relative(cwd(), filePath);
|
|
33
|
+
|
|
34
|
+
// Filter inherited HTML attributes
|
|
35
|
+
for (const key in component.props) {
|
|
36
|
+
const prop = component.props[key];
|
|
37
|
+
if (
|
|
38
|
+
prop.declarations?.filter(
|
|
39
|
+
(declaration) => !declaration.fileName.includes("node_modules")
|
|
40
|
+
).length === 0
|
|
41
|
+
) {
|
|
42
|
+
delete component.props[key];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Generate syntax highlighted HTML for prop types
|
|
47
|
+
{
|
|
48
|
+
const componentMetadata: ComponentMetadata = {
|
|
49
|
+
description: await parseDescription(component.description),
|
|
50
|
+
filePath: component.filePath,
|
|
51
|
+
name: component.displayName,
|
|
52
|
+
props: {}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
for (const name in component.props) {
|
|
56
|
+
const prop = component.props[name];
|
|
57
|
+
|
|
58
|
+
let textToFormat = getPropTypeText(prop);
|
|
59
|
+
|
|
60
|
+
if (prop.defaultValue?.value) {
|
|
61
|
+
const formattedValue =
|
|
62
|
+
typeof prop.defaultValue.value === "string"
|
|
63
|
+
? `"${prop.defaultValue.value}"`
|
|
64
|
+
: prop.defaultValue.value;
|
|
65
|
+
|
|
66
|
+
textToFormat = `${textToFormat} = ${formattedValue}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Format with a placeholder token so we can replace it with a formatted string
|
|
70
|
+
textToFormat = `${TOKEN_TO_REPLACE}${prop.required ? "" : "?"}: ${textToFormat}`;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
let html = await syntaxHighlight(textToFormat, "TS");
|
|
74
|
+
html = html.replace(
|
|
75
|
+
TOKEN_TO_REPLACE,
|
|
76
|
+
`<span class="tok-propertyName">${name}</span>`
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
componentMetadata.props[name] = {
|
|
80
|
+
description: await parseDescription(prop.description),
|
|
81
|
+
html,
|
|
82
|
+
name,
|
|
83
|
+
required: prop.required
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error(error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const outputFile = join(outputDir, `${component.displayName}.json`);
|
|
91
|
+
|
|
92
|
+
console.debug("Writing to", outputFile);
|
|
93
|
+
|
|
94
|
+
await writeFile(outputFile, JSON.stringify(componentMetadata, null, 2));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Generate markdown for prop types
|
|
98
|
+
const requiredProps: PropItem[] = [];
|
|
99
|
+
const optionalProps: PropItem[] = [];
|
|
100
|
+
|
|
101
|
+
for (const propName in component.props) {
|
|
102
|
+
const prop = component.props[propName];
|
|
103
|
+
if (prop.required) {
|
|
104
|
+
requiredProps.push(prop);
|
|
105
|
+
} else {
|
|
106
|
+
optionalProps.push(prop);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
componentName: component.displayName,
|
|
112
|
+
description: component.description,
|
|
113
|
+
optionalPropsTable: await propsToTable(optionalProps),
|
|
114
|
+
requiredPropsTable: await propsToTable(requiredProps)
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { cwd } from "node:process";
|
|
4
|
+
import { withCustomConfig } from "react-docgen-typescript";
|
|
5
|
+
import { initialize } from "../initialize.ts";
|
|
6
|
+
import { compileComponent } from "./compileComponent.ts";
|
|
7
|
+
import { insertPropsMarkdown } from "./insertPropsMarkdown.ts";
|
|
8
|
+
|
|
9
|
+
export async function compileComponents({
|
|
10
|
+
componentNames,
|
|
11
|
+
outputDirName
|
|
12
|
+
}: {
|
|
13
|
+
componentNames: string[];
|
|
14
|
+
outputDirName: string;
|
|
15
|
+
}) {
|
|
16
|
+
const parser = withCustomConfig("./tsconfig.json", {
|
|
17
|
+
savePropValueAsString: true,
|
|
18
|
+
shouldExtractLiteralValuesFromEnum: true,
|
|
19
|
+
shouldExtractValuesFromUnion: true,
|
|
20
|
+
shouldRemoveUndefinedFromOptional: true
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const { files, outputDir } = await initialize({
|
|
24
|
+
fileExtensions: [".ts", ".tsx"],
|
|
25
|
+
fileFilter: (file) =>
|
|
26
|
+
componentNames.some((componentName) => file.endsWith(componentName)),
|
|
27
|
+
inputPath: ["lib", "components"],
|
|
28
|
+
outputDirName
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const markdownPath = join(cwd(), "README.md");
|
|
32
|
+
|
|
33
|
+
let markdown = await readFile(markdownPath, { encoding: "utf-8" });
|
|
34
|
+
|
|
35
|
+
await Promise.all(
|
|
36
|
+
files.map((filePath) =>
|
|
37
|
+
compileComponent({
|
|
38
|
+
filePath,
|
|
39
|
+
outputDir,
|
|
40
|
+
parser
|
|
41
|
+
}).then(
|
|
42
|
+
({
|
|
43
|
+
componentName,
|
|
44
|
+
description,
|
|
45
|
+
optionalPropsTable,
|
|
46
|
+
requiredPropsTable
|
|
47
|
+
}) => {
|
|
48
|
+
markdown = insertPropsMarkdown({
|
|
49
|
+
componentMarkdown: description,
|
|
50
|
+
componentName,
|
|
51
|
+
markdown,
|
|
52
|
+
section: "description"
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
markdown = insertPropsMarkdown({
|
|
56
|
+
componentMarkdown: requiredPropsTable,
|
|
57
|
+
componentName,
|
|
58
|
+
markdown,
|
|
59
|
+
section: "required-props"
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
markdown = insertPropsMarkdown({
|
|
63
|
+
componentMarkdown: optionalPropsTable,
|
|
64
|
+
componentName,
|
|
65
|
+
markdown,
|
|
66
|
+
section: "optional-props"
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
await writeFile(markdownPath, markdown);
|
|
74
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { InterfaceNode } from "@ts-ast-parser/core";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import type { ImperativeHandleMetadata } from "../../../lib/types.ts";
|
|
6
|
+
import { syntaxHighlight } from "../syntax-highlight.ts";
|
|
7
|
+
import { parseDescription } from "./parseDescription.ts";
|
|
8
|
+
|
|
9
|
+
export async function compileImperativeHandle({
|
|
10
|
+
filePath,
|
|
11
|
+
interfaceNode,
|
|
12
|
+
outputDir
|
|
13
|
+
}: {
|
|
14
|
+
filePath: string;
|
|
15
|
+
interfaceNode: InterfaceNode;
|
|
16
|
+
outputDir: string;
|
|
17
|
+
}) {
|
|
18
|
+
const name = interfaceNode.getName();
|
|
19
|
+
|
|
20
|
+
const json: ImperativeHandleMetadata = {
|
|
21
|
+
description: await parseDescription(
|
|
22
|
+
"" + interfaceNode.getJSDoc().getTag("description")?.text
|
|
23
|
+
),
|
|
24
|
+
filePath,
|
|
25
|
+
methods: [],
|
|
26
|
+
name
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const methods = interfaceNode.getMethods();
|
|
30
|
+
for (const method of methods) {
|
|
31
|
+
const jsDoc = method.getJSDoc();
|
|
32
|
+
assert(jsDoc);
|
|
33
|
+
|
|
34
|
+
json.methods.push({
|
|
35
|
+
description: await parseDescription(
|
|
36
|
+
"" + jsDoc.getTag("description")?.text
|
|
37
|
+
),
|
|
38
|
+
html: await syntaxHighlight(method.getTSNode().getText(), "TS"),
|
|
39
|
+
name: method.getName()
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const outputFile = join(outputDir, `${name}.json`);
|
|
44
|
+
|
|
45
|
+
console.debug("Writing to", outputFile);
|
|
46
|
+
|
|
47
|
+
await writeFile(outputFile, JSON.stringify(json, null, 2));
|
|
48
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { parseFromProject, type InterfaceNode } from "@ts-ast-parser/core";
|
|
2
|
+
import tsConfig from "../../../tsconfig.json" with { type: "json" };
|
|
3
|
+
import { compileImperativeHandle } from "./compileImperativeHandle.ts";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { cwd } from "node:process";
|
|
6
|
+
|
|
7
|
+
export async function compileImperativeHandles({
|
|
8
|
+
names,
|
|
9
|
+
outputDirName
|
|
10
|
+
}: {
|
|
11
|
+
names: string[];
|
|
12
|
+
outputDirName: string;
|
|
13
|
+
}) {
|
|
14
|
+
const outputDir = join(cwd(), "public", "generated", outputDirName);
|
|
15
|
+
|
|
16
|
+
const result = await parseFromProject(tsConfig);
|
|
17
|
+
const reflectedModules = result.project?.getModules() ?? [];
|
|
18
|
+
|
|
19
|
+
const nodes: {
|
|
20
|
+
filePath: string;
|
|
21
|
+
node: InterfaceNode;
|
|
22
|
+
}[] = [];
|
|
23
|
+
|
|
24
|
+
names.forEach((name) => {
|
|
25
|
+
reflectedModules.forEach((reflectedModule) => {
|
|
26
|
+
const node = reflectedModule.getDeclarationByName(name);
|
|
27
|
+
if (node) {
|
|
28
|
+
nodes.push({
|
|
29
|
+
filePath: reflectedModule.getSourcePath(),
|
|
30
|
+
node: node as unknown as InterfaceNode
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await Promise.all(
|
|
37
|
+
nodes.map(({ filePath, node }) =>
|
|
38
|
+
compileImperativeHandle({
|
|
39
|
+
filePath,
|
|
40
|
+
interfaceNode: node,
|
|
41
|
+
outputDir
|
|
42
|
+
})
|
|
43
|
+
)
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { PropItem } from "react-docgen-typescript";
|
|
2
|
+
|
|
3
|
+
export function getPropTypeText(prop: PropItem) {
|
|
4
|
+
let textToFormat = prop.type.raw;
|
|
5
|
+
if (!textToFormat && prop.type.name.includes(":")) {
|
|
6
|
+
// Edge case where some prop types aren't registered as containing raw TS
|
|
7
|
+
textToFormat = prop.type.name;
|
|
8
|
+
|
|
9
|
+
// List/Grid and rowComponent/cellComponent are annotated with a return type of ReactElement instead of ReactNode
|
|
10
|
+
// As a result of this change the generated docs are significantly less readable, so tidy them up here
|
|
11
|
+
// See github.com/bvaughn/react-window/issues/875
|
|
12
|
+
textToFormat = textToFormat.replace(
|
|
13
|
+
"ReactElement<unknown, string | JSXElementConstructor<...>>",
|
|
14
|
+
"ReactNode"
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!textToFormat) {
|
|
19
|
+
textToFormat = `${prop.type.name}`;
|
|
20
|
+
|
|
21
|
+
const match = textToFormat.match(/ExcludeForbiddenKeys<([^>]+)>/);
|
|
22
|
+
if (match) {
|
|
23
|
+
textToFormat = match[1];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return textToFormat;
|
|
28
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function insertPropsMarkdown({
|
|
2
|
+
componentMarkdown,
|
|
3
|
+
componentName,
|
|
4
|
+
markdown,
|
|
5
|
+
section
|
|
6
|
+
}: {
|
|
7
|
+
componentMarkdown: string;
|
|
8
|
+
componentName: string;
|
|
9
|
+
markdown: string;
|
|
10
|
+
section: string;
|
|
11
|
+
}) {
|
|
12
|
+
const flag = `${componentName}:${section}`;
|
|
13
|
+
const startToken = `<!-- ${flag}:begin -->`;
|
|
14
|
+
const stopToken = `<!-- ${flag}:end -->`;
|
|
15
|
+
|
|
16
|
+
const startIndex = markdown.indexOf(startToken) + startToken.length;
|
|
17
|
+
const stopIndex = markdown.indexOf(stopToken);
|
|
18
|
+
if (startIndex < 0 || stopIndex < 0) {
|
|
19
|
+
throw Error("Parsing README failed");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
markdown.substring(0, startIndex) +
|
|
24
|
+
"\n" +
|
|
25
|
+
(componentMarkdown || "None") +
|
|
26
|
+
"\n" +
|
|
27
|
+
markdown.substring(stopIndex)
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Intent, Section } from "../../../lib/types.ts";
|
|
2
|
+
import { formatDescriptionText } from "./formatDescriptionText.ts";
|
|
3
|
+
import { syntaxHighlight, type Language } from "../syntax-highlight.ts";
|
|
4
|
+
|
|
5
|
+
export async function parseDescription(rawText: string) {
|
|
6
|
+
const sections: Section[] = [];
|
|
7
|
+
|
|
8
|
+
// Paper over differences between "@ts-ast-parser/core" and "react-docgen-typescript"
|
|
9
|
+
let text = rawText;
|
|
10
|
+
Object.keys(INTENT_FLAGS).forEach((flag) => {
|
|
11
|
+
text = text
|
|
12
|
+
.split(`\n${flag}`)
|
|
13
|
+
.join(`\n\n${flag}`)
|
|
14
|
+
.replaceAll("\n\n\n", "\n\n");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
for (const chunk of text.split("\n\n")) {
|
|
18
|
+
let content = "";
|
|
19
|
+
let intent: Intent | undefined = undefined;
|
|
20
|
+
|
|
21
|
+
if (chunk.startsWith("```")) {
|
|
22
|
+
const match = chunk.match(/^```([a-z]+)/)!;
|
|
23
|
+
const language = match[1].toUpperCase() as Language;
|
|
24
|
+
|
|
25
|
+
content = await syntaxHighlight(
|
|
26
|
+
chunk.substring(language.length + 3, chunk.length - 3).trim(),
|
|
27
|
+
language
|
|
28
|
+
);
|
|
29
|
+
} else {
|
|
30
|
+
content = formatDescriptionText(chunk.trim());
|
|
31
|
+
|
|
32
|
+
for (const char in INTENT_FLAGS) {
|
|
33
|
+
if (content.startsWith(`<p>${char} `)) {
|
|
34
|
+
intent = INTENT_FLAGS[char as keyof typeof INTENT_FLAGS] as Intent;
|
|
35
|
+
content = content.replace(`<p>${char} `, "<p>");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Strip TSDoc comments
|
|
41
|
+
content = content.replace(/\n@param.+/, "");
|
|
42
|
+
content = content.replace(/\n@return.+/, "");
|
|
43
|
+
|
|
44
|
+
sections.push({
|
|
45
|
+
content,
|
|
46
|
+
intent
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return sections;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const INTENT_FLAGS = {
|
|
54
|
+
"❌": "danger",
|
|
55
|
+
"NOTE:": "none",
|
|
56
|
+
ℹ️: "primary",
|
|
57
|
+
"✅": "success",
|
|
58
|
+
"⚠️": "warning"
|
|
59
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { marked } from "marked";
|
|
2
|
+
import type { PropItem } from "react-docgen-typescript";
|
|
3
|
+
import { getPropTypeText } from "./getPropTypeText.ts";
|
|
4
|
+
|
|
5
|
+
const TABLE_TAG_START = `
|
|
6
|
+
<table>
|
|
7
|
+
<thead>
|
|
8
|
+
<tr>
|
|
9
|
+
<th>Name</th>
|
|
10
|
+
<th>Description</th>
|
|
11
|
+
</tr>
|
|
12
|
+
</thead>
|
|
13
|
+
<tbody>`;
|
|
14
|
+
|
|
15
|
+
const PROP_ROW = `
|
|
16
|
+
<tr>
|
|
17
|
+
<td>[[name]]</td>
|
|
18
|
+
<td>[[description]]</td>
|
|
19
|
+
</tr>`;
|
|
20
|
+
|
|
21
|
+
const TABLE_TAG_STOP = `
|
|
22
|
+
</tbody>
|
|
23
|
+
</table>
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
export async function propsToTable(props: PropItem[]) {
|
|
27
|
+
const htmlStrings = [];
|
|
28
|
+
|
|
29
|
+
if (props.length > 0) {
|
|
30
|
+
htmlStrings.push(TABLE_TAG_START);
|
|
31
|
+
|
|
32
|
+
for (const prop of props) {
|
|
33
|
+
const type = getPropTypeText(prop);
|
|
34
|
+
|
|
35
|
+
const description = await marked(prop.description);
|
|
36
|
+
|
|
37
|
+
htmlStrings.push(
|
|
38
|
+
PROP_ROW.replace("[[name]]", prop.name)
|
|
39
|
+
.replace("[[type]]", type)
|
|
40
|
+
.replace("[[description]]", description)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
htmlStrings.push(TABLE_TAG_STOP);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return htmlStrings.join("");
|
|
48
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function trimExcludedText(rawText: string) {
|
|
2
|
+
{
|
|
3
|
+
const pieces = rawText.split("// <begin>");
|
|
4
|
+
rawText = pieces[pieces.length - 1].trim();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
{
|
|
8
|
+
const pieces = rawText.split("// <end>");
|
|
9
|
+
rawText = pieces[0].trim();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return rawText;
|
|
13
|
+
}
|