@vanilla-bean/components 1.1.0 → 2.0.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/Component/Component.js +226 -40
- package/Component/Component.scenarios.js +2 -2
- package/Component/Component.test.js +427 -40
- package/Component/README.md +480 -0
- package/Component/observeElementConnection.js +1 -1
- package/Elem/README.md +373 -0
- package/README.md +2 -2
- package/components/BottomSheet/BottomSheet.js +8 -16
- package/components/BottomSheet/README.md +1 -1
- package/components/Button/Button.js +12 -18
- package/components/Button/Button.lld.md +1 -1
- package/components/Calendar/Calendar.js +139 -59
- package/components/Calendar/CalendarEvent.js +9 -4
- package/components/Calendar/Toolbar.js +28 -20
- package/components/Calendar/index.js +1 -0
- package/components/Code/Code.js +26 -30
- package/components/Code/Code.lld.md +1 -1
- package/components/ColorPicker/ColorPicker.js +58 -58
- package/components/Dialog/Dialog.js +79 -73
- package/components/Dialog/Dialog.lld.md +1 -1
- package/components/Dialog/README.md +10 -10
- package/components/Form/Form.js +21 -17
- package/components/Form/Form.lld.md +2 -2
- package/components/Form/README.md +3 -3
- package/components/Icon/Icon.js +19 -11
- package/components/Input/Input.js +85 -67
- package/components/Input/README.md +7 -9
- package/components/Keyboard/Key.js +7 -10
- package/components/Keyboard/Keyboard.js +38 -46
- package/components/Keyboard/Keyboard.lld.md +1 -1
- package/components/Label/Label.js +52 -50
- package/components/Link/Link.js +14 -20
- package/components/List/List.js +28 -30
- package/components/Menu/Menu.js +19 -10
- package/components/Menu/Menu.lld.md +2 -2
- package/components/Notify/Notify.js +29 -19
- package/components/Notify/Notify.lld.md +2 -2
- package/components/Page/Page.lld.md +1 -1
- package/components/Popover/Popover.js +49 -44
- package/components/RadioButton/RadioButton.js +33 -29
- package/components/RadioButton/RadioButton.lld.md +1 -1
- package/components/Router/README.md +12 -12
- package/components/Router/Router.js +19 -21
- package/components/Select/README.md +5 -5
- package/components/Select/Select.js +25 -31
- package/components/Table/README.md +4 -4
- package/components/Table/Table.js +55 -36
- package/components/Table/Table.lld.md +1 -1
- package/components/TagList/Tag.js +16 -14
- package/components/TagList/TagList.js +15 -8
- package/components/TagList/TagList.lld.md +1 -1
- package/components/Tooltip/Tooltip.js +12 -25
- package/components/TooltipWrapper/TooltipWrapper.js +55 -49
- package/components/TooltipWrapper/TooltipWrapper.lld.md +2 -2
- package/components/Whiteboard/Whiteboard.js +45 -43
- package/components/Whiteboard/Whiteboard.lld.md +1 -1
- package/devTools/build.js +43 -0
- package/devTools/buildTypes.js +322 -0
- package/devTools/createComponent.js +155 -0
- package/devTools/extractJSDoc.js +395 -0
- package/devTools/processTemplate.js +500 -0
- package/devTools/updateComponentIndex.js +16 -0
- package/devTools/updateDemoViewIndex.js +90 -0
- package/eslint.config.cjs +5 -2
- package/index.d.ts +120 -62
- package/package.json +23 -22
- package/spellcheck.config.cjs +3 -0
- package/styled/README.md +329 -0
- package/theme/.test.js +2 -2
- package/theme/README.md +8 -8
- package/theme/colors.js +16 -13
- package/theme/colors.test.js +28 -0
- package/utils/browser.js +1 -1
- package/utils/element.js +2 -2
- package/FontWithASyntaxHighlighter-Regular.woff2 +0 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
|
|
3
|
+
import { capitalize } from '../utils/string';
|
|
4
|
+
|
|
5
|
+
if (!process.argv[2]) {
|
|
6
|
+
console.error('Usage: bun run create:component <ComponentName> [optionsJSON]');
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let options = {};
|
|
11
|
+
try {
|
|
12
|
+
options = process.argv[3] ? JSON.parse(process.argv[3]) : {};
|
|
13
|
+
} catch {
|
|
14
|
+
console.error('Invalid JSON options:', process.argv[3]);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const name = capitalize(process.argv[2]);
|
|
19
|
+
const componentFolder = `components/${name}`;
|
|
20
|
+
|
|
21
|
+
if (fs.existsSync(componentFolder)) {
|
|
22
|
+
console.error(`Component '${name}' already exists at ${componentFolder}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fs.mkdirSync(componentFolder);
|
|
27
|
+
|
|
28
|
+
const demoOptions = Object.entries(options)
|
|
29
|
+
.map(
|
|
30
|
+
([key, value]) =>
|
|
31
|
+
`${key}: ${
|
|
32
|
+
typeof value === 'object'
|
|
33
|
+
? JSON.stringify(value)
|
|
34
|
+
.replaceAll(/"(\w+)":/g, ` $1: `)
|
|
35
|
+
.replaceAll('"', `'`)
|
|
36
|
+
: value
|
|
37
|
+
},`,
|
|
38
|
+
)
|
|
39
|
+
.join('\n');
|
|
40
|
+
|
|
41
|
+
const testFile = `import { ${name} } from '.';
|
|
42
|
+
|
|
43
|
+
describe('${name}', () => {
|
|
44
|
+
test('renders', () => {
|
|
45
|
+
new ${name}({ appendTo: container });
|
|
46
|
+
|
|
47
|
+
expect(container.firstElementChild).toBeDefined();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
const demoFile = `import DemoView from '../../demo/DemoView';
|
|
53
|
+
import { ${name} } from '.';
|
|
54
|
+
|
|
55
|
+
export default class Demo extends DemoView {
|
|
56
|
+
build() {
|
|
57
|
+
this.component = new ${name}({
|
|
58
|
+
${demoOptions}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
const readMeFile = `# ${name}
|
|
65
|
+
|
|
66
|
+
[[extract-description ${name}.js]]
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
\`\`\`js
|
|
71
|
+
[[import ./demo.js]]/^(\\t*).+component = new.+\\(\\s*{(.|\\n)+?\\1}?\\);/gm
|
|
72
|
+
\`\`\`
|
|
73
|
+
|
|
74
|
+
## Options
|
|
75
|
+
|
|
76
|
+
[[extract-options ${name}.js]]
|
|
77
|
+
|
|
78
|
+
## Methods
|
|
79
|
+
|
|
80
|
+
[[extract-methods ${name}.js]]
|
|
81
|
+
|
|
82
|
+
## Properties
|
|
83
|
+
|
|
84
|
+
[[extract-properties ${name}.js]]
|
|
85
|
+
|
|
86
|
+
## Events
|
|
87
|
+
|
|
88
|
+
[[extract-events ${name}.js]]
|
|
89
|
+
|
|
90
|
+
## Dependencies
|
|
91
|
+
|
|
92
|
+
[[extract-imports ${name}.js]]
|
|
93
|
+
|
|
94
|
+
## Design
|
|
95
|
+
|
|
96
|
+

|
|
97
|
+
`;
|
|
98
|
+
|
|
99
|
+
const componentFile = `import { Component } from '../../Component';
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* [Component description - describe what this component does and its primary purpose]
|
|
103
|
+
*
|
|
104
|
+
* [Optional: Additional context about when to use this component or special behaviors]
|
|
105
|
+
*
|
|
106
|
+
* @param {object} [options={}] - Component configuration options
|
|
107
|
+
* @param {...(Component|HTMLElement|string)} children - Child elements to append
|
|
108
|
+
* @returns {${name}} Component instance with reactive options
|
|
109
|
+
* @example
|
|
110
|
+
* // Basic usage
|
|
111
|
+
* new ${name}({
|
|
112
|
+
* // Add example options here
|
|
113
|
+
* });
|
|
114
|
+
*/
|
|
115
|
+
export default class ${name} extends Component {
|
|
116
|
+
/**
|
|
117
|
+
* Creates component structure before options are processed.
|
|
118
|
+
* Assign child elements to instance properties here so handlers can reference them.
|
|
119
|
+
*/
|
|
120
|
+
build() {
|
|
121
|
+
// Create child elements here - this runs before options are processed
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Declare the option schema once with static schema - what can exist, the defaults,
|
|
125
|
+
// and how each key routes. Descriptor fields:
|
|
126
|
+
// default - initial value, merged automatically (child classes override per-key)
|
|
127
|
+
// set(value, next) - change handler; omit next() to own the key, call it to continue
|
|
128
|
+
// enum: [...] - valid values; anything else throws
|
|
129
|
+
// attribute: true / priority: true - setAttribute routing / processed first
|
|
130
|
+
// Declared keys with no DOM match live in this.options silently - a bare {} declares
|
|
131
|
+
// a data-only key. data: true exists only to force store-only on DOM-name collisions.
|
|
132
|
+
//
|
|
133
|
+
// static schema = {
|
|
134
|
+
// label: {
|
|
135
|
+
// default: 'Untitled',
|
|
136
|
+
// set(value) {
|
|
137
|
+
// this.myChild.elem.textContent = value;
|
|
138
|
+
// },
|
|
139
|
+
// },
|
|
140
|
+
// items: {},
|
|
141
|
+
// };
|
|
142
|
+
//
|
|
143
|
+
// Custom events: static events = ['select'];
|
|
144
|
+
// Computed defaults (no constructor needed): static prepareOptions(options, children) { return options; }
|
|
145
|
+
}
|
|
146
|
+
`;
|
|
147
|
+
|
|
148
|
+
await Bun.write(`${componentFolder}/${name}.js`, componentFile);
|
|
149
|
+
await Bun.write(`${componentFolder}/.test.js`, testFile);
|
|
150
|
+
await Bun.write(`${componentFolder}/demo.js`, demoFile);
|
|
151
|
+
await Bun.write(`${componentFolder}/README.md`, readMeFile);
|
|
152
|
+
await Bun.write(`${componentFolder}/index.js`, `export { default as ${name} } from './${name}';`);
|
|
153
|
+
|
|
154
|
+
await Bun.spawn(['bun', 'run', 'build:index']).exited;
|
|
155
|
+
await Bun.spawn(['bun', 'run', 'format']).exited;
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extracts JSDoc information from JavaScript files for documentation generation.
|
|
5
|
+
*
|
|
6
|
+
* Parses JSDoc comments and extracts component descriptions, parameters, methods,
|
|
7
|
+
* properties, and events for automated README generation.
|
|
8
|
+
* @param {string} filePath - Path to JavaScript file to parse
|
|
9
|
+
* @returns {object} Extracted JSDoc information structured for documentation templates
|
|
10
|
+
*/
|
|
11
|
+
export function extractJSDoc(filePath) {
|
|
12
|
+
try {
|
|
13
|
+
const content = readFileSync(filePath, 'utf8');
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
description: extractDescription(content),
|
|
17
|
+
options: extractOptions(content),
|
|
18
|
+
methods: extractMethods(content),
|
|
19
|
+
properties: extractProperties(content),
|
|
20
|
+
events: extractEvents(content),
|
|
21
|
+
imports: extractImports(content),
|
|
22
|
+
examples: extractExamples(content),
|
|
23
|
+
};
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.warn(`Failed to extract JSDoc from ${filePath}:`, error.message);
|
|
26
|
+
return {
|
|
27
|
+
description: '',
|
|
28
|
+
options: [],
|
|
29
|
+
methods: [],
|
|
30
|
+
properties: [],
|
|
31
|
+
events: [],
|
|
32
|
+
imports: [],
|
|
33
|
+
examples: [],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extracts component description from class-level JSDoc comment.
|
|
40
|
+
* @param {string} content - File content to parse
|
|
41
|
+
* @returns {string} Component description text
|
|
42
|
+
*/
|
|
43
|
+
function extractDescription(content) {
|
|
44
|
+
// Match class JSDoc comment - handle both export default class and class patterns
|
|
45
|
+
const classMatches = [
|
|
46
|
+
content.match(/\/\*\*\s*\n([\s\S]*?)\*\/\s*export\s+default\s+class\s+\w+/),
|
|
47
|
+
content.match(/\/\*\*\s*\n([\s\S]*?)\*\/\s*(?:export\s+)?class\s+\w+/),
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const classMatch = classMatches.find(match => match);
|
|
51
|
+
if (!classMatch) return '';
|
|
52
|
+
|
|
53
|
+
const jsdoc = classMatch[1];
|
|
54
|
+
|
|
55
|
+
// Extract description (everything before first @tag)
|
|
56
|
+
const descMatch = jsdoc.match(/^\s*\*\s*(.+?)(?=\s*^\s*\*\s*@|\s*$)/ms);
|
|
57
|
+
|
|
58
|
+
if (!descMatch) return '';
|
|
59
|
+
|
|
60
|
+
// Clean up the description text - handle multi-line descriptions
|
|
61
|
+
const lines = descMatch[1].split('\n');
|
|
62
|
+
const cleanedLines = lines.map(line => line.replace(/^\s*\*\s?/, '')).filter(line => line.trim() !== '');
|
|
63
|
+
|
|
64
|
+
return cleanedLines.join(' ').replace(/\s+/g, ' ').trim();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Extracts constructor options from JSDoc `@param` tags.
|
|
69
|
+
* @param {string} content - File content to parse
|
|
70
|
+
* @returns {Array} Array of option objects with name, type, description, and default
|
|
71
|
+
*/
|
|
72
|
+
function extractOptions(content) {
|
|
73
|
+
// Find class-level JSDoc (which contains constructor info) or constructor JSDoc
|
|
74
|
+
const jsDocMatches = [
|
|
75
|
+
content.match(/\/\*\*\s*\n([\s\S]*?)\*\/\s*export\s+default\s+class\s+\w+/),
|
|
76
|
+
content.match(/\/\*\*\s*\n([\s\S]*?)\*\/\s*constructor\s*\(/),
|
|
77
|
+
content.match(/\/\*\*\s*\n([\s\S]*?)\*\/\s*(?:export\s+)?class\s+\w+/),
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const jsDocMatch = jsDocMatches.find(match => match);
|
|
81
|
+
if (!jsDocMatch) return [];
|
|
82
|
+
|
|
83
|
+
const jsdoc = jsDocMatch[1];
|
|
84
|
+
|
|
85
|
+
// Extract @param tags for options (handle both options.prop and direct param patterns)
|
|
86
|
+
const paramMatches = [
|
|
87
|
+
// options.property pattern
|
|
88
|
+
...jsdoc.matchAll(
|
|
89
|
+
/^\s*\*\s*@param\s+{([^}]+)}\s+(\[?options\.(\w+)(?:=([^\]]+))?\]?)\s*-\s*(.+?)(?=\s*^\s*\*\s*@|\s*^\s*\*\s*$|\s*\*\/)/gms,
|
|
90
|
+
),
|
|
91
|
+
// Direct parameter pattern (for constructor params like options={})
|
|
92
|
+
...jsdoc.matchAll(
|
|
93
|
+
/^\s*\*\s*@param\s+{([^}]+)}\s+(\[?(\w+)(?:=([^\]]+))?\]?)\s*-\s*(.+?)(?=\s*^\s*\*\s*@|\s*^\s*\*\s*$|\s*\*\/)/gms,
|
|
94
|
+
),
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
const codeDefaults = extractDefaultOptionsFromCode(content);
|
|
98
|
+
|
|
99
|
+
return paramMatches
|
|
100
|
+
.filter(match => match[2].includes('options.'))
|
|
101
|
+
.map(match => {
|
|
102
|
+
const [, type, fullParam, name, defaultValue, description] = match;
|
|
103
|
+
const optional = fullParam.startsWith('[') && fullParam.endsWith(']');
|
|
104
|
+
const optionName = name.replace('options.', '');
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
name: optionName,
|
|
108
|
+
type: type.trim(),
|
|
109
|
+
description: description.trim().replace(/\s+/g, ' '),
|
|
110
|
+
optional,
|
|
111
|
+
default: defaultValue || codeDefaults[optionName] || null,
|
|
112
|
+
isSubProperty: fullParam.includes('options.'),
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Reads the `defaultOptions` object literal from source and extracts simple key→value pairs.
|
|
119
|
+
* Only captures string, number, and boolean literals - skips getters and complex values.
|
|
120
|
+
* @param {string} content - File content
|
|
121
|
+
* @returns {object} Map of option name → default value string
|
|
122
|
+
*/
|
|
123
|
+
function extractDefaultOptionsFromCode(content) {
|
|
124
|
+
const defaults = {};
|
|
125
|
+
const block = content.match(/const\s+defaultOptions\s*=\s*\{([\s\S]*?)\};/);
|
|
126
|
+
if (!block) return defaults;
|
|
127
|
+
|
|
128
|
+
const pairs = [
|
|
129
|
+
...block[1].matchAll(/\b(\w+):\s*(?:'([^']*)'|"([^"]*)"|(`[^`]*`)|(-?\d+\.?\d*(?:e[+-]?\d+)?)|true|false)/g),
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
pairs.forEach(match => {
|
|
133
|
+
const [full, key, singleQuoted, doubleQuoted, template, number] = match;
|
|
134
|
+
if (singleQuoted !== undefined) defaults[key] = `'${singleQuoted}'`;
|
|
135
|
+
else if (doubleQuoted !== undefined) defaults[key] = `"${doubleQuoted}"`;
|
|
136
|
+
else if (template !== undefined) defaults[key] = template;
|
|
137
|
+
else if (number !== undefined) defaults[key] = number;
|
|
138
|
+
else if (full.endsWith('true')) defaults[key] = 'true';
|
|
139
|
+
else if (full.endsWith('false')) defaults[key] = 'false';
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return defaults;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Extracts method information from JSDoc comments.
|
|
147
|
+
* @param {string} content - File content to parse
|
|
148
|
+
* @returns {Array} Array of method objects with name, description, parameters, and return info
|
|
149
|
+
*/
|
|
150
|
+
function extractMethods(content) {
|
|
151
|
+
// Match method JSDoc comments (excluding constructor and private methods)
|
|
152
|
+
// Match JSDoc comments that are within the class body, not at class level
|
|
153
|
+
|
|
154
|
+
// Find class body content (everything between class declaration and closing brace)
|
|
155
|
+
const classBodyMatch = content.match(/class\s+\w+[^{]*\{([\s\S]*)\}[^}]*$/);
|
|
156
|
+
if (!classBodyMatch) return [];
|
|
157
|
+
|
|
158
|
+
const classBody = classBodyMatch[1];
|
|
159
|
+
|
|
160
|
+
// Match each JSDoc block with the method that IMMEDIATELY follows it.
|
|
161
|
+
// `(?:(?!\/\*\*)[\s\S])*?` - non-greedy content that cannot cross into a new `/**`,
|
|
162
|
+
// so the captured block can never span multiple JSDoc comments or method bodies.
|
|
163
|
+
// get/set accessors are excluded; underscore-prefixed names are excluded.
|
|
164
|
+
const matches = [
|
|
165
|
+
...classBody.matchAll(
|
|
166
|
+
/\/\*\*((?:(?!\/\*\*)[\s\S])*?)\*\/[ \t]*\n[ \t]*(?!constructor\b)(?!get\b)(?!set\b)(?!_)(\w+)\s*\([^)]*\)\s*\{/g,
|
|
167
|
+
),
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
return matches.map(match => {
|
|
171
|
+
const [, jsdoc, name] = match;
|
|
172
|
+
|
|
173
|
+
// Extract description
|
|
174
|
+
const descMatch = jsdoc.match(/^\s*\*\s*(.+?)(?=\s*^\s*\*\s*@|\s*$)/ms);
|
|
175
|
+
const description = descMatch
|
|
176
|
+
? descMatch[1]
|
|
177
|
+
.split('\n')
|
|
178
|
+
.map(line => line.replace(/^\s*\*\s?/, ''))
|
|
179
|
+
.join(' ')
|
|
180
|
+
.replace(/\s+/g, ' ')
|
|
181
|
+
.trim()
|
|
182
|
+
: '';
|
|
183
|
+
|
|
184
|
+
// Extract parameters
|
|
185
|
+
const paramMatches = [
|
|
186
|
+
...jsdoc.matchAll(
|
|
187
|
+
/^\s*\*\s*@param\s+{([^}]+)}\s+(\[?(\w+)(?:=([^\]]+))?\]?)\s*-\s*(.+?)(?=\s*^\s*\*\s*@|\s*^\s*\*\s*$)/gms,
|
|
188
|
+
),
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
const parameters = paramMatches.map(paramMatch => {
|
|
192
|
+
const [, type, fullParam, name, defaultValue, desc] = paramMatch;
|
|
193
|
+
const optional = fullParam.startsWith('[') && fullParam.endsWith(']');
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
name,
|
|
197
|
+
type: type.trim(),
|
|
198
|
+
description: desc.trim().replace(/\s+/g, ' '),
|
|
199
|
+
optional,
|
|
200
|
+
default: defaultValue || null,
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Extract return information - type only; description is not used in type generation.
|
|
205
|
+
const returnMatch = jsdoc.match(/^\s*\*\s*@returns?\s+\{([^}]+)\}/m);
|
|
206
|
+
const returnInfo = returnMatch ? { type: returnMatch[1].trim() } : null;
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
name,
|
|
210
|
+
description,
|
|
211
|
+
parameters,
|
|
212
|
+
returns: returnInfo,
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Extracts property information from JSDoc comments and getter methods.
|
|
219
|
+
* @param {string} content - File content to parse
|
|
220
|
+
* @returns {Array} Array of property objects with name, type, description, and access info
|
|
221
|
+
*/
|
|
222
|
+
function extractProperties(content) {
|
|
223
|
+
const properties = [];
|
|
224
|
+
|
|
225
|
+
// Extract getter properties - same anti-span pattern as extractMethods.
|
|
226
|
+
const getterMatches = [
|
|
227
|
+
...content.matchAll(/\/\*\*((?:(?!\/\*\*)[\s\S])*?)\*\/[ \t]*\n[ \t]*get\s+(\w+)\s*\(\)\s*\{/g),
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
getterMatches.forEach(match => {
|
|
231
|
+
const [, jsdoc, name] = match;
|
|
232
|
+
|
|
233
|
+
const descMatch = jsdoc.match(/^\s*\*\s*(.+?)(?=\s*^\s*\*\s*@|\s*$)/ms);
|
|
234
|
+
const description = descMatch
|
|
235
|
+
? descMatch[1]
|
|
236
|
+
.split('\n')
|
|
237
|
+
.map(line => line.replace(/^\s*\*\s?/, ''))
|
|
238
|
+
.join(' ')
|
|
239
|
+
.replace(/\s+/g, ' ')
|
|
240
|
+
.trim()
|
|
241
|
+
: '';
|
|
242
|
+
|
|
243
|
+
const returnMatch = jsdoc.match(/^\s*\*\s*@returns?\s+\{([^}]+)\}/m);
|
|
244
|
+
const type = returnMatch ? returnMatch[1].trim() : 'unknown';
|
|
245
|
+
|
|
246
|
+
properties.push({
|
|
247
|
+
name,
|
|
248
|
+
type,
|
|
249
|
+
description,
|
|
250
|
+
access: 'readonly',
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Extract documented properties (this.property assignments with JSDoc)
|
|
255
|
+
const propMatches = [...content.matchAll(/\/\*\*((?:(?!\/\*\*)[\s\S])*?)\*\/[ \t]*\n[ \t]*(?:this\.)?(\w+)\s*=/g)];
|
|
256
|
+
|
|
257
|
+
propMatches.forEach(match => {
|
|
258
|
+
const [, jsdoc, name] = match;
|
|
259
|
+
|
|
260
|
+
const descMatch = jsdoc.match(/^\s*\*\s*(.+?)(?=\s*^\s*\*\s*@|\s*$)/ms);
|
|
261
|
+
const description = descMatch
|
|
262
|
+
? descMatch[1]
|
|
263
|
+
.split('\n')
|
|
264
|
+
.map(line => line.replace(/^\s*\*\s?/, ''))
|
|
265
|
+
.join(' ')
|
|
266
|
+
.replace(/\s+/g, ' ')
|
|
267
|
+
.trim()
|
|
268
|
+
: '';
|
|
269
|
+
|
|
270
|
+
const typeMatch = jsdoc.match(/^\s*\*\s*@type\s+{([^}]+)}/m);
|
|
271
|
+
const type = typeMatch ? typeMatch[1].trim() : 'unknown';
|
|
272
|
+
|
|
273
|
+
properties.push({
|
|
274
|
+
name,
|
|
275
|
+
type,
|
|
276
|
+
description,
|
|
277
|
+
access: 'read-write',
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return properties;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Extracts event information from emit() calls and JSDoc `@fires` tags.
|
|
286
|
+
* @param {string} content - File content to parse
|
|
287
|
+
* @returns {Array} Array of event objects with name and description
|
|
288
|
+
*/
|
|
289
|
+
function extractEvents(content) {
|
|
290
|
+
const events = [];
|
|
291
|
+
|
|
292
|
+
// Extract from emit() calls
|
|
293
|
+
const emitMatches = [...content.matchAll(/\.emit\s*\(\s*['"`]([^'"`]+)['"`]/g)];
|
|
294
|
+
|
|
295
|
+
emitMatches.forEach(match => {
|
|
296
|
+
const [, eventName] = match;
|
|
297
|
+
events.push({
|
|
298
|
+
name: eventName,
|
|
299
|
+
description: `Emitted when ${eventName} occurs`,
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Extract from dispatchEvent calls
|
|
304
|
+
const dispatchMatches = [...content.matchAll(/\.dispatchEvent\s*\(\s*new\s+CustomEvent\s*\(\s*['"`]([^'"`]+)['"`]/g)];
|
|
305
|
+
|
|
306
|
+
dispatchMatches.forEach(match => {
|
|
307
|
+
const [, eventName] = match;
|
|
308
|
+
if (!events.find(e => e.name === eventName)) {
|
|
309
|
+
events.push({
|
|
310
|
+
name: eventName,
|
|
311
|
+
description: `Custom event: ${eventName}`,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
return events;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Extracts import statements to determine component dependencies.
|
|
321
|
+
* @param {string} content - File content to parse
|
|
322
|
+
* @returns {Array} Array of import objects with module and imported items
|
|
323
|
+
*/
|
|
324
|
+
function extractImports(content) {
|
|
325
|
+
const importMatches = [
|
|
326
|
+
...content.matchAll(/^import\s+(?:{([^}]+)}|\*\s+as\s+(\w+)|(\w+))\s+from\s+['"`]([^'"`]+)['"`];/gm),
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
return importMatches
|
|
330
|
+
.filter(([, , , , module]) => /^\.\.\//.test(module)) // external relative only, not same-dir ./
|
|
331
|
+
.map(match => {
|
|
332
|
+
const [, namedImports, namespaceImport, defaultImport, module] = match;
|
|
333
|
+
|
|
334
|
+
let imports = [];
|
|
335
|
+
|
|
336
|
+
if (namedImports) {
|
|
337
|
+
imports = namedImports.split(',').map(imp => imp.trim());
|
|
338
|
+
} else if (namespaceImport) {
|
|
339
|
+
imports = [`* as ${namespaceImport}`];
|
|
340
|
+
} else if (defaultImport) {
|
|
341
|
+
imports = [defaultImport];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
module: module.replace(/^(\.\.?\/)+/, ''),
|
|
346
|
+
imports,
|
|
347
|
+
};
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Extracts example code from JSDoc `@example` tags.
|
|
353
|
+
* @param {string} content - File content to parse
|
|
354
|
+
* @returns {Array} Array of example objects with code and optional description
|
|
355
|
+
*/
|
|
356
|
+
function extractExamples(content) {
|
|
357
|
+
const examples = [];
|
|
358
|
+
|
|
359
|
+
// Find all JSDoc blocks with @example tags
|
|
360
|
+
const jsdocBlocks = [...content.matchAll(/\/\*\*\s*\n([\s\S]*?)\*\//g)];
|
|
361
|
+
|
|
362
|
+
jsdocBlocks.forEach(([, jsdoc]) => {
|
|
363
|
+
const exampleMatches = [...jsdoc.matchAll(/^\s*\*\s*@example\s*\n([\s\S]*?)(?=^\s*\*\s*@|\s*$)/gms)];
|
|
364
|
+
|
|
365
|
+
exampleMatches.forEach(([, exampleContent]) => {
|
|
366
|
+
const code = exampleContent
|
|
367
|
+
.split('\n')
|
|
368
|
+
.map(line => line.replace(/^\s*\*\s?/, ''))
|
|
369
|
+
.join('\n')
|
|
370
|
+
.trim();
|
|
371
|
+
|
|
372
|
+
if (code) {
|
|
373
|
+
examples.push({
|
|
374
|
+
code,
|
|
375
|
+
description: '', // Could extract description if needed
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
return examples;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// CLI usage
|
|
385
|
+
if (import.meta.main) {
|
|
386
|
+
const filePath = process.argv[2];
|
|
387
|
+
|
|
388
|
+
if (!filePath) {
|
|
389
|
+
console.error('Usage: bun extractJSDoc.js <path-to-js-file>');
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const result = extractJSDoc(filePath);
|
|
394
|
+
console.log(JSON.stringify(result, null, 2));
|
|
395
|
+
}
|