@@ -8,16 +8,10 @@ import MagicString from "magic-string";
88 * Automatically discovers components and provides IDE autocomplete + bundling
99 */
1010export function openScriptComponentPlugin ( options = { } ) {
11- const {
12- componentsDir = "src/components" ,
13- autoRegister = true ,
14- generateTypes = true ,
15- } = options ;
11+ const { componentsDir = "src/components" , generateTypes = true } = options ;
1612
1713 let config ;
1814 let components = [ ] ;
19- const virtualModuleId = "virtual:openscript-components" ;
20- const resolvedVirtualModuleId = "\0" + virtualModuleId ;
2115
2216 return {
2317 name : "openscript-component-plugin" ,
@@ -32,7 +26,7 @@ export function openScriptComponentPlugin(options = {}) {
3226
3327 if ( ! fs . existsSync ( componentsPath ) ) {
3428 console . warn (
35- `[OpenScript] Components directory not found: ${ componentsPath } `
29+ `[OpenScript] Components directory not found: ${ componentsPath } ` ,
3630 ) ;
3731 return ;
3832 }
@@ -42,7 +36,7 @@ export function openScriptComponentPlugin(options = {}) {
4236
4337 console . log (
4438 `[OpenScript] Found ${ components . length } components:` ,
45- components . map ( ( c ) => c . name ) . join ( ", " )
39+ components . map ( ( c ) => c . name ) . join ( ", " ) ,
4640 ) ;
4741
4842 // Generate TypeScript definitions if enabled
@@ -51,83 +45,185 @@ export function openScriptComponentPlugin(options = {}) {
5145 }
5246 } ,
5347
54- resolveId ( id ) {
55- if ( id === virtualModuleId ) {
56- return resolvedVirtualModuleId ;
57- }
58- } ,
59-
60- load ( id ) {
61- if ( id === resolvedVirtualModuleId ) {
62- // Generate virtual module that imports all components
63- return generateVirtualModule (
64- config . root ,
65- componentsDir ,
66- components ,
67- autoRegister
68- ) ;
69- }
70- } ,
71-
7248 transform ( code , id ) {
49+ // Normalize path for Windows compatibility
50+ const normalizedId = normalizePath ( id ) ;
51+
7352 // Only transform files in components directory
74- if ( ! id . includes ( componentsDir ) || ! id . endsWith ( ".js" ) ) return ;
53+ if (
54+ ! normalizedId . includes ( componentsDir ) ||
55+ ! normalizedId . endsWith ( ".js" )
56+ )
57+ return ;
7558
76- // Find class definition
77- const classMatch = code . match ( / c l a s s \s + ( \w + ) \s + e x t e n d s \s + C o m p o n e n t / ) ;
78- if ( ! classMatch ) return ;
59+ const s = new MagicString ( code ) ;
60+ let hasChanged = false ;
61+
62+ // 1. Check for Functional Components: function MyComp(...) { ... }
63+ // Regex matches: 1=export_modifier, 2=Name, 3=Args
64+ const funcRegex =
65+ / ( e x p o r t \s + d e f a u l t \s + | e x p o r t \s + ) ? f u n c t i o n \s + ( [ A - Z ] \w * ) \s * \( ( [ ^ ) ] * ) \) \s * \{ / g;
66+ let match ;
67+
68+ // We need to handle multiple components in one file, though rare for default exports
69+ // But let's loop to be safe and use a while loop with exec
70+ while ( ( match = funcRegex . exec ( code ) ) !== null ) {
71+ const [ fullMatch , exportModifier = "" , name , args ] = match ;
72+ const start = match . index ;
73+ const bodyStart = start + fullMatch . length - 1 ; // pointing to {
74+
75+ // Find closing brace
76+ let braceCount = 1 ;
77+ let end = - 1 ;
78+ let i = bodyStart + 1 ;
79+
80+ while ( i < code . length ) {
81+ if ( code [ i ] === "{" ) braceCount ++ ;
82+ else if ( code [ i ] === "}" ) braceCount -- ;
83+
84+ if ( braceCount === 0 ) {
85+ end = i ;
86+ break ;
87+ }
88+ i ++ ;
89+ }
7990
80- const className = classMatch [ 1 ] ;
91+ if ( end !== - 1 ) {
92+ // Found the component body
93+ hasChanged = true ;
94+
95+ // Replace header
96+ // "export default function Name(args) {"
97+ // ->
98+ // "export default class Name extends Component { constructor() { super(); this.name = "Name"; } render(args) {"
99+ const replacement = `${ exportModifier } class ${ name } extends Component {
100+ constructor() {
101+ super();
102+ this.name = "${ name } ";
103+ }
81104
82- // If code already sets this.name explicitly, skip (simple check)
83- if ( code . includes ( `this.name = "${ className } "` ) ) return ;
105+ render(${ args } ) {` ;
84106
85- const s = new MagicString ( code ) ;
107+ s . overwrite ( start , bodyStart + 1 , replacement ) ;
86108
87- if ( code . includes ( "constructor" ) ) {
88- // Inject after super()
89- const superMatch = code . match ( / ( s u p e r \s * \( [ ^ ) ] * \) \s * ; ? ) / ) ;
90- if ( superMatch ) {
91- const index = superMatch . index + superMatch [ 0 ] . length ;
92- s . appendRight (
93- index ,
94- `\n if (!this.name) this.name = "${ className } ";`
95- ) ;
109+ // Append closing brace for the class
110+ s . appendRight ( end + 1 , "\n}" ) ;
96111 }
97- } else {
98- // No constructor, inject one
99- const classDef = classMatch [ 0 ] ;
100- const openBraceIndex = code . indexOf ( "{" , code . indexOf ( classDef ) ) ;
112+ }
101113
102- if ( openBraceIndex !== - 1 ) {
114+ // 2. Existing logic: Class Component name injection
115+ // Only run this if we haven't just converted it (though regex below checks for class)
116+ // If we converted, we already injected the name.
117+ // But there might be other classes in the file.
118+
119+ // Note: If we just converted, s.toString() inside loop?
120+ // MagicString handles edits on original string.
121+ // But our regex 'code.match' works on original code.
122+ // If we have mixed content, we should be careful.
123+ // The original logic scanned for "class ... extends Component".
124+ // Our new logic creates that string in output, but original code didn't have it.
125+ // So checks against 'code' for class will only find ORIGINAL classes.
126+
127+ const classRegex = / c l a s s \s + ( \w + ) \s + e x t e n d s \s + C o m p o n e n t / g;
128+ while ( ( match = classRegex . exec ( code ) ) !== null ) {
129+ const className = match [ 1 ] ;
130+
131+ // Check if this class already has name set in ORIGINAL code
132+ // (If we synthesized it, we don't need to do this, and regex won't find synthesized one)
133+ const classStart = match . index ;
134+ // Simple check range for existing name assignment is hard with regex loop
135+ // But we can check globally if the code has it for this class
136+ if ( code . includes ( `this.name = "${ className } "` ) ) continue ;
137+
138+ // Find constructor or inject it
139+ // We need to look inside the class body.
140+ // Find the opening brace for this class
141+ const openBraceIndex = code . indexOf ( "{" , classStart ) ;
142+ if ( openBraceIndex === - 1 ) continue ;
143+
144+ // Check if constructor exists within reasonable distance/scope?
145+ // This is tricky with simple string searching on the whole file.
146+ // But let's assume standard structure or rely on previous logic's robustness
147+ // The previous logic used 'if (code.includes("constructor"))' which is global!
148+ // That was capable of false positives if multiple classes existed or comments.
149+ // But let's stick to the previous logic style for consistency but apply it carefully.
150+
151+ // Actually, since I'm rewriting the transform function, I can try to improve it slightly
152+ // or just keep it as is for existing classes.
153+
154+ // Given constraints, I will preserve the logic's INTENT:
155+ // Ensure `this.name` is set.
156+
157+ // For the *functional* transformation I just added, I *know* I added the constructor.
158+ // So I don't need to process those.
159+ // So I only process matches from `classRegex` on the `code` string.
160+ // Since `code` is original source, it won't include my functional-to-class transforms.
161+ // So I am only processing originally-written classes. Perfect.
162+
163+ // Re-implementing existing logic for original classes:
164+
165+ // Try to find constructor in the class body?
166+ // We'll search for `constructor` keyword after class def.
167+ const nextClassIndex = code . indexOf ( "class " , openBraceIndex ) ;
168+ const searchEnd = nextClassIndex === - 1 ? code . length : nextClassIndex ;
169+ const constructorMatch = code
170+ . substring ( openBraceIndex , searchEnd )
171+ . match ( / c o n s t r u c t o r \s * \( / ) ;
172+
173+ if ( constructorMatch ) {
174+ // Constructor exists
175+ // Find super() call relative to class start
176+ const constructorAbsIndex = openBraceIndex + constructorMatch . index ;
177+ // Search for super() after constructor
178+ const superMatch = code
179+ . substring ( constructorAbsIndex , searchEnd )
180+ . match ( / s u p e r \s * \( [ ^ ) ] * \) \s * ; ? / ) ;
181+ if ( superMatch ) {
182+ const insertAt =
183+ constructorAbsIndex + superMatch . index + superMatch [ 0 ] . length ;
184+ s . appendRight (
185+ insertAt ,
186+ `\n if (!this.name) this.name = "${ className } ";` ,
187+ ) ;
188+ hasChanged = true ;
189+ }
190+ } else {
191+ // No constructor, inject one at start of class
103192 s . appendRight (
104193 openBraceIndex + 1 ,
105- `\n constructor() { super(); this.name = "${ className } "; }`
194+ `\n constructor() { super(); this.name = "${ className } "; }` ,
106195 ) ;
196+ hasChanged = true ;
107197 }
108198 }
109199
110- if ( s . hasChanged ( ) ) {
200+ // 3. Inject Component import if needed
201+ // If we did any transformation (functional or existing class), we might need Component.
202+ // Especially for functional -> class.
203+ if ( hasChanged ) {
204+ // Check if Component is imported
205+ // Matches: import { Component } ... or import ... Component ...
206+ // Simple check:
207+ if ( ! code . includes ( "import" ) || ! code . match ( / i m p o r t \s + .* C o m p o n e n t / ) ) {
208+ // Need to import Component.
209+ // Check if existing import from "modular-openscriptjs" exists
210+ if ( code . includes ( "modular-openscriptjs" ) ) {
211+ // Try to add to existing import? Hard with regex.
212+ // Easier to just add a new line.
213+ s . prepend ( `import { Component } from "modular-openscriptjs";\n` ) ;
214+ } else {
215+ s . prepend ( `import { Component } from "modular-openscriptjs";\n` ) ;
216+ }
217+ }
218+ }
219+
220+ if ( hasChanged ) {
111221 return {
112222 code : s . toString ( ) ,
113223 map : s . generateMap ( { source : id , includeContent : true } ) ,
114224 } ;
115225 }
116226 } ,
117-
118- // HMR support
119- handleHotUpdate ( { file, server } ) {
120- if ( file . includes ( componentsDir ) ) {
121- // Reload virtual module when components change
122- const module = server . moduleGraph . getModuleById (
123- resolvedVirtualModuleId
124- ) ;
125- if ( module ) {
126- server . moduleGraph . invalidateModule ( module ) ;
127- return [ module ] ;
128- }
129- }
130- } ,
131227 } ;
132228}
133229
@@ -217,45 +313,3 @@ export {};
217313 fs . writeFileSync ( dtsPath , content , "utf-8" ) ;
218314 console . log ( `[OpenScript] Generated type definitions: ${ dtsPath } ` ) ;
219315}
220-
221- /**
222- * Generate virtual module content that imports and registers all components
223- */
224- function generateVirtualModule ( root , componentsDir , components , autoRegister ) {
225- const imports = components
226- . map ( ( c ) => {
227- const absolutePath = normalizePath (
228- path . resolve ( root , componentsDir , c . path )
229- ) ;
230- return `import ${ c . name } from '${ absolutePath } ';` ;
231- } )
232- . join ( "\n" ) ;
233-
234- const exports = components . map ( ( c ) => c . name ) . join ( ", " ) ;
235-
236- const registration = autoRegister
237- ? `
238- // Auto-register all components
239- const components = { ${ exports } };
240-
241- export async function registerAllComponents() {
242- for (const [name, Component] of Object.entries(components)) {
243- const instance = new Component();
244- await instance.mount();
245- }
246- }
247- `
248- : "" ;
249-
250- return `// Auto-generated by OpenScript Component Plugin
251- ${ imports }
252-
253- ${ registration }
254-
255- // Export all components for manual use
256- export { ${ exports } };
257-
258- // Export component registry
259- export default { ${ exports } };
260- ` ;
261- }
0 commit comments