@useavalon/avalon 0.1.13 → 0.1.15

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.
Files changed (230) hide show
  1. package/dist/mod.js +1 -0
  2. package/dist/src/build/integration-bundler-plugin.js +1 -0
  3. package/dist/src/build/integration-config.js +1 -0
  4. package/dist/src/build/integration-detection-plugin.js +1 -0
  5. package/dist/src/build/integration-resolver-plugin.js +1 -0
  6. package/dist/src/build/island-manifest.js +1 -0
  7. package/dist/src/build/island-types-generator.js +5 -0
  8. package/dist/src/build/mdx-island-transform.js +2 -0
  9. package/dist/src/build/mdx-plugin.js +1 -0
  10. package/dist/src/build/page-island-transform.js +3 -0
  11. package/dist/src/build/prop-extractors/index.js +1 -0
  12. package/dist/src/build/prop-extractors/lit.js +1 -0
  13. package/dist/src/build/prop-extractors/qwik.js +1 -0
  14. package/dist/src/build/prop-extractors/solid.js +1 -0
  15. package/dist/src/build/prop-extractors/svelte.js +1 -0
  16. package/dist/src/build/prop-extractors/vue.js +1 -0
  17. package/dist/src/build/sidecar-file-manager.js +1 -0
  18. package/dist/src/build/sidecar-renderer.js +6 -0
  19. package/dist/src/client/adapters/index.js +1 -0
  20. package/dist/src/client/components.js +1 -0
  21. package/dist/src/client/css-hmr-handler.js +1 -0
  22. package/dist/src/client/framework-adapter.js +13 -0
  23. package/dist/src/client/hmr-coordinator.js +1 -0
  24. package/dist/src/client/hmr-error-overlay.js +214 -0
  25. package/dist/src/client/main.js +39 -0
  26. package/dist/src/components/Image.js +1 -0
  27. package/dist/src/components/IslandErrorBoundary.js +1 -0
  28. package/dist/src/components/LayoutDataErrorBoundary.js +1 -0
  29. package/dist/src/components/LayoutErrorBoundary.js +1 -0
  30. package/dist/src/components/PersistentIsland.js +1 -0
  31. package/dist/src/components/StreamingErrorBoundary.js +1 -0
  32. package/dist/src/components/StreamingLayout.js +29 -0
  33. package/dist/src/core/components/component-analyzer.js +1 -0
  34. package/dist/src/core/components/component-detection.js +5 -0
  35. package/dist/src/core/components/enhanced-framework-detector.js +1 -0
  36. package/dist/src/core/components/framework-registry.js +1 -0
  37. package/dist/src/core/content/mdx-processor.js +1 -0
  38. package/dist/src/core/integrations/index.js +1 -0
  39. package/dist/src/core/integrations/loader.js +1 -0
  40. package/dist/src/core/integrations/registry.js +1 -0
  41. package/dist/src/core/islands/island-persistence.js +1 -0
  42. package/dist/src/core/islands/island-state-serializer.js +1 -0
  43. package/dist/src/core/islands/persistent-island-context.js +1 -0
  44. package/dist/src/core/islands/use-persistent-state.js +1 -0
  45. package/dist/src/core/layout/enhanced-layout-resolver.js +1 -0
  46. package/dist/src/core/layout/layout-cache-manager.js +1 -0
  47. package/dist/src/core/layout/layout-composer.js +1 -0
  48. package/dist/src/core/layout/layout-data-loader.js +1 -0
  49. package/dist/src/core/layout/layout-discovery.js +1 -0
  50. package/dist/src/core/layout/layout-matcher.js +1 -0
  51. package/dist/src/core/layout/layout-types.js +1 -0
  52. package/dist/src/core/modules/framework-module-resolver.js +1 -0
  53. package/dist/src/islands/component-analysis.js +1 -0
  54. package/dist/src/islands/css-utils.js +17 -0
  55. package/dist/src/islands/discovery/index.js +1 -0
  56. package/dist/src/islands/discovery/registry.js +1 -0
  57. package/dist/src/islands/discovery/resolver.js +2 -0
  58. package/dist/src/islands/discovery/scanner.js +1 -0
  59. package/dist/src/islands/discovery/types.js +1 -0
  60. package/dist/src/islands/discovery/validator.js +18 -0
  61. package/dist/src/islands/discovery/watcher.js +1 -0
  62. package/dist/src/islands/framework-detection.js +1 -0
  63. package/dist/src/islands/integration-loader.js +1 -0
  64. package/dist/src/islands/island.js +1 -0
  65. package/dist/src/islands/render-cache.js +1 -0
  66. package/dist/src/islands/types.js +1 -0
  67. package/dist/src/islands/universal-css-collector.js +5 -0
  68. package/dist/src/islands/universal-head-collector.js +2 -0
  69. package/dist/src/layout-system.js +1 -0
  70. package/dist/src/middleware/discovery.js +1 -0
  71. package/dist/src/middleware/executor.js +1 -0
  72. package/dist/src/middleware/index.js +1 -0
  73. package/dist/src/middleware/types.js +1 -0
  74. package/dist/src/nitro/build-config.js +1 -0
  75. package/dist/src/nitro/config.js +1 -0
  76. package/dist/src/nitro/error-handler.js +198 -0
  77. package/dist/src/nitro/index.js +1 -0
  78. package/dist/src/nitro/island-manifest.js +2 -0
  79. package/dist/src/nitro/middleware-adapter.js +1 -0
  80. package/dist/src/nitro/renderer.js +183 -0
  81. package/dist/src/nitro/route-discovery.js +1 -0
  82. package/dist/src/nitro/types.js +1 -0
  83. package/dist/src/render/collect-css.js +3 -0
  84. package/dist/src/render/error-pages.js +48 -0
  85. package/dist/src/render/isolated-ssr-renderer.js +1 -0
  86. package/dist/src/render/ssr.js +90 -0
  87. package/dist/src/schemas/api.js +1 -0
  88. package/dist/src/schemas/core.js +1 -0
  89. package/dist/src/schemas/index.js +1 -0
  90. package/dist/src/schemas/layout.js +1 -0
  91. package/dist/src/schemas/routing/index.js +1 -0
  92. package/dist/src/schemas/routing.js +1 -0
  93. package/dist/src/types/as-island.js +1 -0
  94. package/dist/src/types/layout.js +1 -0
  95. package/dist/src/types/routing.js +1 -0
  96. package/dist/src/types/types.js +1 -0
  97. package/dist/src/utils/dev-logger.js +12 -0
  98. package/dist/src/utils/fs.js +1 -0
  99. package/dist/src/vite-plugin/auto-discover.js +1 -0
  100. package/dist/src/vite-plugin/config.js +1 -0
  101. package/dist/src/vite-plugin/errors.js +1 -0
  102. package/dist/src/vite-plugin/image-optimization.js +45 -0
  103. package/dist/src/vite-plugin/integration-activator.js +1 -0
  104. package/dist/src/vite-plugin/island-sidecar-plugin.js +1 -0
  105. package/dist/src/vite-plugin/module-discovery.js +1 -0
  106. package/dist/src/vite-plugin/nitro-integration.js +42 -0
  107. package/dist/src/vite-plugin/plugin.js +1 -0
  108. package/dist/src/vite-plugin/types.js +1 -0
  109. package/dist/src/vite-plugin/validation.js +2 -0
  110. package/package.json +14 -20
  111. package/mod.ts +0 -302
  112. package/src/build/integration-bundler-plugin.ts +0 -116
  113. package/src/build/integration-config.ts +0 -168
  114. package/src/build/integration-detection-plugin.ts +0 -117
  115. package/src/build/integration-resolver-plugin.ts +0 -90
  116. package/src/build/island-manifest.ts +0 -269
  117. package/src/build/island-types-generator.ts +0 -476
  118. package/src/build/mdx-island-transform.ts +0 -464
  119. package/src/build/mdx-plugin.ts +0 -98
  120. package/src/build/page-island-transform.ts +0 -598
  121. package/src/build/prop-extractors/index.ts +0 -21
  122. package/src/build/prop-extractors/lit.ts +0 -140
  123. package/src/build/prop-extractors/qwik.ts +0 -16
  124. package/src/build/prop-extractors/solid.ts +0 -125
  125. package/src/build/prop-extractors/svelte.ts +0 -194
  126. package/src/build/prop-extractors/vue.ts +0 -111
  127. package/src/build/sidecar-file-manager.ts +0 -104
  128. package/src/build/sidecar-renderer.ts +0 -30
  129. package/src/client/adapters/index.ts +0 -21
  130. package/src/client/components.ts +0 -35
  131. package/src/client/css-hmr-handler.ts +0 -344
  132. package/src/client/framework-adapter.ts +0 -462
  133. package/src/client/hmr-coordinator.ts +0 -396
  134. package/src/client/hmr-error-overlay.js +0 -533
  135. package/src/client/main.js +0 -824
  136. package/src/components/Image.tsx +0 -123
  137. package/src/components/IslandErrorBoundary.tsx +0 -145
  138. package/src/components/LayoutDataErrorBoundary.tsx +0 -141
  139. package/src/components/LayoutErrorBoundary.tsx +0 -127
  140. package/src/components/PersistentIsland.tsx +0 -52
  141. package/src/components/StreamingErrorBoundary.tsx +0 -233
  142. package/src/components/StreamingLayout.tsx +0 -538
  143. package/src/core/components/component-analyzer.ts +0 -192
  144. package/src/core/components/component-detection.ts +0 -508
  145. package/src/core/components/enhanced-framework-detector.ts +0 -500
  146. package/src/core/components/framework-registry.ts +0 -563
  147. package/src/core/content/mdx-processor.ts +0 -46
  148. package/src/core/integrations/index.ts +0 -19
  149. package/src/core/integrations/loader.ts +0 -125
  150. package/src/core/integrations/registry.ts +0 -175
  151. package/src/core/islands/island-persistence.ts +0 -325
  152. package/src/core/islands/island-state-serializer.ts +0 -258
  153. package/src/core/islands/persistent-island-context.tsx +0 -80
  154. package/src/core/islands/use-persistent-state.ts +0 -68
  155. package/src/core/layout/enhanced-layout-resolver.ts +0 -322
  156. package/src/core/layout/layout-cache-manager.ts +0 -485
  157. package/src/core/layout/layout-composer.ts +0 -357
  158. package/src/core/layout/layout-data-loader.ts +0 -516
  159. package/src/core/layout/layout-discovery.ts +0 -243
  160. package/src/core/layout/layout-matcher.ts +0 -299
  161. package/src/core/layout/layout-types.ts +0 -110
  162. package/src/core/modules/framework-module-resolver.ts +0 -273
  163. package/src/islands/component-analysis.ts +0 -213
  164. package/src/islands/css-utils.ts +0 -565
  165. package/src/islands/discovery/index.ts +0 -80
  166. package/src/islands/discovery/registry.ts +0 -340
  167. package/src/islands/discovery/resolver.ts +0 -477
  168. package/src/islands/discovery/scanner.ts +0 -386
  169. package/src/islands/discovery/types.ts +0 -117
  170. package/src/islands/discovery/validator.ts +0 -544
  171. package/src/islands/discovery/watcher.ts +0 -368
  172. package/src/islands/framework-detection.ts +0 -428
  173. package/src/islands/integration-loader.ts +0 -490
  174. package/src/islands/island.tsx +0 -565
  175. package/src/islands/render-cache.ts +0 -550
  176. package/src/islands/types.ts +0 -80
  177. package/src/islands/universal-css-collector.ts +0 -157
  178. package/src/islands/universal-head-collector.ts +0 -137
  179. package/src/layout-system.ts +0 -218
  180. package/src/middleware/discovery.ts +0 -268
  181. package/src/middleware/executor.ts +0 -315
  182. package/src/middleware/index.ts +0 -76
  183. package/src/middleware/types.ts +0 -99
  184. package/src/nitro/build-config.ts +0 -576
  185. package/src/nitro/config.ts +0 -483
  186. package/src/nitro/error-handler.ts +0 -636
  187. package/src/nitro/index.ts +0 -173
  188. package/src/nitro/island-manifest.ts +0 -584
  189. package/src/nitro/middleware-adapter.ts +0 -260
  190. package/src/nitro/renderer.ts +0 -1471
  191. package/src/nitro/route-discovery.ts +0 -439
  192. package/src/nitro/types.ts +0 -321
  193. package/src/render/collect-css.ts +0 -198
  194. package/src/render/error-pages.ts +0 -79
  195. package/src/render/isolated-ssr-renderer.ts +0 -654
  196. package/src/render/ssr.ts +0 -1030
  197. package/src/schemas/api.ts +0 -30
  198. package/src/schemas/core.ts +0 -64
  199. package/src/schemas/index.ts +0 -212
  200. package/src/schemas/layout.ts +0 -279
  201. package/src/schemas/routing/index.ts +0 -38
  202. package/src/schemas/routing.ts +0 -376
  203. package/src/types/as-island.ts +0 -20
  204. package/src/types/layout.ts +0 -285
  205. package/src/types/routing.ts +0 -555
  206. package/src/types/types.ts +0 -5
  207. package/src/utils/dev-logger.ts +0 -299
  208. package/src/utils/fs.ts +0 -151
  209. package/src/vite-plugin/auto-discover.ts +0 -551
  210. package/src/vite-plugin/config.ts +0 -266
  211. package/src/vite-plugin/errors.ts +0 -127
  212. package/src/vite-plugin/image-optimization.ts +0 -156
  213. package/src/vite-plugin/integration-activator.ts +0 -126
  214. package/src/vite-plugin/island-sidecar-plugin.ts +0 -176
  215. package/src/vite-plugin/module-discovery.ts +0 -189
  216. package/src/vite-plugin/nitro-integration.ts +0 -1354
  217. package/src/vite-plugin/plugin.ts +0 -403
  218. package/src/vite-plugin/types.ts +0 -327
  219. package/src/vite-plugin/validation.ts +0 -228
  220. /package/{src → dist/src}/client/types/framework-runtime.d.ts +0 -0
  221. /package/{src → dist/src}/client/types/vite-hmr.d.ts +0 -0
  222. /package/{src → dist/src}/client/types/vite-virtual-modules.d.ts +0 -0
  223. /package/{src → dist/src}/layout-system.d.ts +0 -0
  224. /package/{src → dist/src}/types/image.d.ts +0 -0
  225. /package/{src → dist/src}/types/index.d.ts +0 -0
  226. /package/{src → dist/src}/types/island-jsx.d.ts +0 -0
  227. /package/{src → dist/src}/types/island-prop.d.ts +0 -0
  228. /package/{src → dist/src}/types/mdx.d.ts +0 -0
  229. /package/{src → dist/src}/types/urlpattern.d.ts +0 -0
  230. /package/{src → dist/src}/types/vite-env.d.ts +0 -0
@@ -1,598 +0,0 @@
1
- /**
2
- * Page Island Transform Plugin
3
- *
4
- * Transforms components with an `island` prop in TSX/JSX page files so that developers
5
- * can use any component as an island by simply adding the `island` prop.
6
- *
7
- * Before (manual):
8
- * import { renderIsland } from '@useavalon/avalon';
9
- * {await renderIsland({ src: '/src/components/Counter.tsx', condition: 'on:interaction', framework: 'preact' })}
10
- *
11
- * After (auto-wrapped):
12
- * import Counter from '../components/Counter.tsx';
13
- * <Counter island={{ condition: 'on:interaction' }} someProp={42} />
14
- *
15
- * How it works:
16
- * The plugin rewrites each `<Component island={opts} ...props />` JSX usage
17
- * into an `{await renderIsland({...})}` expression inline in the JSX.
18
- * Any component can be an island - no special directory required.
19
- *
20
- * Preact's renderToString does NOT support async child components in the JSX
21
- * tree, so we cannot use async wrapper functions. Instead we directly replace
22
- * the JSX element with an await expression.
23
- *
24
- * Only applies to files inside the configured pages or layouts directories.
25
- */
26
-
27
- import type { Plugin } from 'vite';
28
- import { dirname } from 'node:path';
29
-
30
- export interface PageIslandTransformOptions {
31
- /** Directory containing page files (default: src/pages/) */
32
- pagesDir?: string;
33
- /** Directory containing layout files (default: src/layouts/) */
34
- layoutsDir?: string;
35
- /** Modules configuration for modular architecture */
36
- modules?: {
37
- dir: string;
38
- pagesDirName: string;
39
- layoutsDirName: string;
40
- } | null;
41
- /** Whether to enable verbose logging */
42
- verbose?: boolean;
43
- }
44
-
45
- interface ComponentImport {
46
- localName: string;
47
- importPath: string;
48
- fullMatch: string;
49
- }
50
-
51
- interface ParsedJSXElement {
52
- endIdx: number;
53
- islandProp: string | null;
54
- otherProps: string[];
55
- }
56
-
57
- interface ParsedAttribute {
58
- name: string;
59
- value: string | null;
60
- endIdx: number;
61
- }
62
-
63
- // ─── Import Discovery ────────────────────────────────────────────────
64
-
65
- /**
66
- * Find all default imports in the code (any component import, not filtered by path)
67
- */
68
- function findAllDefaultImports(code: string): ComponentImport[] {
69
- const imports: ComponentImport[] = [];
70
- const re = /^[ \t]*import\s+([A-Z]\w*)\s+from\s+(['"][^'"]+['"])/gm;
71
- let m;
72
- while ((m = re.exec(code)) !== null) {
73
- imports.push({
74
- localName: m[1],
75
- importPath: m[2].slice(1, -1),
76
- fullMatch: m[0].trimStart(),
77
- });
78
- }
79
- return imports;
80
- }
81
-
82
- /**
83
- * Resolve an import path to an absolute src path for renderIsland
84
- */
85
- function resolveIslandSrc(importPath: string, fileId: string): string {
86
- // Already absolute
87
- if (importPath.startsWith('/src/')) return importPath;
88
- if (importPath.startsWith('/app/')) return importPath;
89
- if (importPath.startsWith('/')) return importPath;
90
-
91
- // Handle aliases - convert to absolute paths
92
- if (importPath.startsWith('@/')) {
93
- return '/app/' + importPath.slice(2);
94
- }
95
- if (importPath.startsWith('@shared/')) {
96
- return '/app/shared/' + importPath.slice(8);
97
- }
98
- if (importPath.startsWith('@modules/')) {
99
- return '/app/modules/' + importPath.slice(9);
100
- }
101
- if (importPath.startsWith('$components/')) {
102
- return '/src/components/' + importPath.slice(12);
103
- }
104
- if (importPath.startsWith('$islands/')) {
105
- return '/src/islands/' + importPath.slice(9);
106
- }
107
- if (importPath.startsWith('~/')) {
108
- return '/src/' + importPath.slice(2);
109
- }
110
-
111
- // Relative import - resolve relative to the file
112
- if (importPath.startsWith('.')) {
113
- const normalized = fileId.replaceAll('\\', '/');
114
-
115
- // Try to find /app/ or /src/ in the path
116
- let baseIndex = normalized.indexOf('/app/');
117
- if (baseIndex === -1) baseIndex = normalized.indexOf('/src/');
118
-
119
- if (baseIndex !== -1) {
120
- const fileDir = dirname(normalized.slice(baseIndex));
121
- // Simple path resolution
122
- const parts = fileDir.split('/');
123
- const importParts = importPath.split('/');
124
-
125
- for (const part of importParts) {
126
- if (part === '..') {
127
- parts.pop();
128
- } else if (part !== '.') {
129
- parts.push(part);
130
- }
131
- }
132
-
133
- return parts.join('/');
134
- }
135
- }
136
-
137
- // Fallback: return as-is with /src/ prefix
138
- return '/src/' + importPath.split('/').pop();
139
- }
140
-
141
- function detectFramework(src: string): string | undefined {
142
- if (src.endsWith('.vue')) return 'vue';
143
- if (src.endsWith('.svelte')) return 'svelte';
144
- if (src.includes('.solid.')) return 'solid';
145
- if (src.includes('.lit.')) return 'lit';
146
- if (src.includes('.qwik.')) return 'qwik';
147
- return undefined;
148
- }
149
-
150
- function isPageFile(id: string, pagesDir: string, modules?: PageIslandTransformOptions['modules']): boolean {
151
- const normalized = id.replaceAll('\\', '/');
152
-
153
- // Check traditional pages directory
154
- const dir = pagesDir.replace(/^\//, '');
155
- if (normalized.includes('/' + dir + '/') && /\.(tsx|jsx)$/.test(normalized)) {
156
- return true;
157
- }
158
-
159
- // Check modular pages directories
160
- if (modules) {
161
- const modulesDir = modules.dir.replace(/^\//, '');
162
- // Pattern: /modules/*/pages/
163
- const modulePagePattern = new RegExp('/' + modulesDir + '/[^/]+/' + modules.pagesDirName + '/');
164
- if (modulePagePattern.test(normalized) && /\.(tsx|jsx)$/.test(normalized)) {
165
- return true;
166
- }
167
- }
168
-
169
- return false;
170
- }
171
-
172
- /** Check whether a file is inside the layouts directory */
173
- function isLayoutFile(id: string, layoutsDir: string, modules?: PageIslandTransformOptions['modules']): boolean {
174
- const normalized = id.replaceAll('\\', '/');
175
-
176
- // Check traditional layouts directory
177
- const dir = layoutsDir.replace(/^\//, '');
178
- if (normalized.includes('/' + dir + '/') && /\.(tsx|jsx)$/.test(normalized)) {
179
- return true;
180
- }
181
-
182
- // Check modular layouts directories
183
- if (modules) {
184
- const modulesDir = modules.dir.replace(/^\//, '');
185
- // Pattern: /modules/*/layouts/
186
- const moduleLayoutPattern = new RegExp('/' + modulesDir + '/[^/]+/' + modules.layoutsDirName + '/');
187
- if (moduleLayoutPattern.test(normalized) && /\.(tsx|jsx)$/.test(normalized)) {
188
- return true;
189
- }
190
- }
191
-
192
- return false;
193
- }
194
-
195
- /** Frameworks that are auto-wrapped as islands without requiring the `island` prop.
196
- * Qwik is resumable — it gets SSR'd with ssrOnly:true and the Qwikloader handles the rest. */
197
- const AUTO_ISLAND_FRAMEWORKS = new Set(['qwik']);
198
-
199
- /** Check if a component import is for an auto-island framework */
200
- function isAutoIslandImport(importPath: string): boolean {
201
- const src = importPath; // raw import path, not resolved
202
- const framework = detectFramework(src);
203
- return framework !== undefined && AUTO_ISLAND_FRAMEWORKS.has(framework);
204
- }
205
-
206
- function hasIslandPropUsage(code: string, componentNames: string[]): boolean {
207
- return componentNames.some((name) => {
208
- const pattern = new RegExp('<' + name + String.raw`[\s][^>]*island[\s]*[={]`);
209
- return pattern.test(code);
210
- });
211
- }
212
-
213
- /** Check if any auto-island components are used as JSX elements */
214
- function hasAutoIslandUsage(code: string, imports: ComponentImport[]): boolean {
215
- return imports.some((imp) => {
216
- if (!isAutoIslandImport(imp.importPath)) return false;
217
- const pattern = new RegExp('<' + imp.localName + String.raw`[\s/>]`);
218
- return pattern.test(code);
219
- });
220
- }
221
-
222
- /**
223
- * Build metadata for components that are used with island prop
224
- */
225
- function buildIslandMeta(
226
- code: string,
227
- imports: ComponentImport[],
228
- fileId: string,
229
- ): Map<string, { srcPath: string; framework: string | undefined; importPath: string; autoIsland: boolean }> {
230
- const meta = new Map<string, { srcPath: string; framework: string | undefined; importPath: string; autoIsland: boolean }>();
231
- for (const imp of imports) {
232
- const srcPath = resolveIslandSrc(imp.importPath, fileId);
233
- const framework = detectFramework(srcPath);
234
-
235
- // Check for explicit island prop usage
236
- const islandPattern = new RegExp('<' + imp.localName + String.raw`[\s][^>]*island[\s]*[={]`);
237
- if (islandPattern.test(code)) {
238
- meta.set(imp.localName, { srcPath, framework, importPath: imp.importPath, autoIsland: false });
239
- continue;
240
- }
241
-
242
- // Check for auto-island frameworks (e.g. Qwik) used as JSX without island prop
243
- if (framework && AUTO_ISLAND_FRAMEWORKS.has(framework)) {
244
- const usagePattern = new RegExp('<' + imp.localName + String.raw`[\s/>]`);
245
- if (usagePattern.test(code)) {
246
- meta.set(imp.localName, { srcPath, framework, importPath: imp.importPath, autoIsland: true });
247
- }
248
- }
249
- }
250
- return meta;
251
- }
252
-
253
- // ─── Low-level string scanning helpers ───────────────────────────────
254
-
255
- function skipWhitespace(code: string, pos: number): number {
256
- while (pos < code.length && /\s/.test(code[pos])) pos++;
257
- return pos;
258
- }
259
-
260
- /** Skip a string literal (single, double, or backtick). Returns index after closing quote. */
261
- function skipStringLiteral(code: string, pos: number): number {
262
- const quote = code[pos];
263
- pos++;
264
- while (pos < code.length && code[pos] !== quote) {
265
- if (code[pos] === '\\') pos++; // skip escaped char
266
- pos++;
267
- }
268
- return pos < code.length ? pos + 1 : pos;
269
- }
270
-
271
- /** Skip a template literal including ${...} expressions. Returns index after closing backtick. */
272
- function skipTemplateLiteral(code: string, pos: number): number {
273
- pos++; // skip opening backtick
274
- while (pos < code.length && code[pos] !== '`') {
275
- if (code[pos] === '\\') {
276
- pos += 2;
277
- continue;
278
- }
279
- if (code[pos] === '$' && code[pos + 1] === '{') {
280
- pos = skipBracedExpression(pos + 1, code);
281
- continue;
282
- }
283
- pos++;
284
- }
285
- return pos < code.length ? pos + 1 : pos;
286
- }
287
-
288
- /** Skip a brace-delimited expression `{...}`, handling nested braces and strings. */
289
- function skipBracedExpression(openBraceIdx: number, code: string): number {
290
- let pos = openBraceIdx + 1;
291
- let depth = 1;
292
- while (pos < code.length && depth > 0) {
293
- const ch = code[pos];
294
- if (ch === '{') { depth++; pos++; }
295
- else if (ch === '}') { depth--; if (depth > 0) pos++; }
296
- else if (ch === "'" || ch === '"' || ch === '`') { pos = skipStringLiteral(code, pos); }
297
- else { pos++; }
298
- }
299
- return pos < code.length ? pos + 1 : pos;
300
- }
301
-
302
- // ─── JSX Attribute Parsing ───────────────────────────────────────────
303
-
304
- /** Parse a JSX expression value `{...}`. Returns the inner expression and end index (after `}`). */
305
- function parseJSXExpressionValue(code: string, pos: number): { value: string; endIdx: number } {
306
- const exprStart = pos + 1;
307
- const endIdx = skipBracedExpression(pos, code);
308
- // endIdx is after the closing }, inner content is between { and }
309
- return { value: code.slice(exprStart, endIdx - 1), endIdx };
310
- }
311
-
312
- /** Parse a quoted string value `"..."` or `'...'`. Returns the value (with double quotes) and end index. */
313
- function parseQuotedValue(code: string, pos: number): { value: string; endIdx: number } {
314
- const quote = code[pos];
315
- let i = pos + 1;
316
- while (i < code.length && code[i] !== quote) {
317
- if (code[i] === '\\') i++;
318
- i++;
319
- }
320
- const value = '"' + code.slice(pos + 1, i) + '"';
321
- return { value, endIdx: i + 1 };
322
- }
323
-
324
- /** Parse a single JSX attribute (name + optional value). Returns null on failure. */
325
- function parseAttribute(code: string, pos: number): ParsedAttribute | null {
326
- const nameStart = pos;
327
- let i = pos;
328
- while (i < code.length && /[a-zA-Z0-9_$]/.test(code[i])) i++;
329
- const name = code.slice(nameStart, i);
330
- if (!name) return null;
331
-
332
- i = skipWhitespace(code, i);
333
-
334
- // Boolean attribute (no `=`)
335
- if (code[i] !== '=') {
336
- return { name, value: null, endIdx: i };
337
- }
338
- i = skipWhitespace(code, i + 1); // skip `=` and whitespace
339
-
340
- // Expression value: {expr}
341
- if (code[i] === '{') {
342
- const parsed = parseJSXExpressionValue(code, i);
343
- return { name, value: parsed.value, endIdx: parsed.endIdx };
344
- }
345
-
346
- // Quoted string value
347
- if (code[i] === '"' || code[i] === "'") {
348
- const parsed = parseQuotedValue(code, i);
349
- return { name, value: parsed.value, endIdx: parsed.endIdx };
350
- }
351
-
352
- return null; // unexpected token
353
- }
354
-
355
- // ─── JSX Element Parsing ─────────────────────────────────────────────
356
-
357
- /** Find the end of a JSX tag — either self-closing `/>` or `>...</Component>`. */
358
- function findTagEnd(
359
- code: string,
360
- pos: number,
361
- componentName: string,
362
- ): { endIdx: number; selfClosing: boolean } | null {
363
- if (code[pos] === '/' && code[pos + 1] === '>') {
364
- return { endIdx: pos + 2, selfClosing: true };
365
- }
366
- if (code[pos] === '>') {
367
- const closeTag = '</' + componentName + '>';
368
- const closeIdx = code.indexOf(closeTag, pos + 1);
369
- if (closeIdx === -1) return null;
370
- return { endIdx: closeIdx + closeTag.length, selfClosing: false };
371
- }
372
- return null;
373
- }
374
-
375
- /**
376
- * Parse a JSX element starting at `<ComponentName`.
377
- * Returns the end index and extracted props, or null if parsing fails.
378
- */
379
- function parseJSXElement(
380
- code: string,
381
- startIdx: number,
382
- componentName: string,
383
- ): ParsedJSXElement | null {
384
- let i = skipWhitespace(code, startIdx + 1 + componentName.length);
385
-
386
- let islandProp: string | null = null;
387
- const otherProps: string[] = [];
388
-
389
- while (i < code.length) {
390
- i = skipWhitespace(code, i);
391
-
392
- // Check for end of opening tag
393
- const tagEnd = findTagEnd(code, i, componentName);
394
- if (tagEnd) {
395
- return { endIdx: tagEnd.endIdx, islandProp, otherProps };
396
- }
397
-
398
- // Parse next attribute
399
- const attr = parseAttribute(code, i);
400
- if (!attr) return null;
401
- i = attr.endIdx;
402
-
403
- if (attr.name === 'island') {
404
- islandProp = attr.value ?? '{}';
405
- } else {
406
- const propValue = attr.value === null
407
- ? attr.name + ': true'
408
- : attr.name + ': ' + attr.value;
409
- otherProps.push(propValue);
410
- }
411
- }
412
-
413
- return null;
414
- }
415
-
416
- // ─── JSX Replacement ─────────────────────────────────────────────────
417
-
418
- /** Build the `{await __pageRenderIsland({...})}` call from parsed element data. */
419
- function buildRenderCall(
420
- parsed: ParsedJSXElement,
421
- srcPath: string,
422
- framework: string | undefined,
423
- autoIsland: boolean,
424
- ): string {
425
- const fwArg = framework ? ', framework: "' + framework + '"' : '';
426
- const propsArg = parsed.otherProps.length > 0
427
- ? ', props: { ' + parsed.otherProps.join(', ') + ' }'
428
- : '';
429
-
430
- if (autoIsland) {
431
- // Auto-island (e.g. Qwik): SSR-only, no client hydration needed
432
- return '{await __pageRenderIsland({ src: "' + srcPath + '"' + fwArg
433
- + propsArg
434
- + ', ssr: true, ssrOnly: true'
435
- + ' })}';
436
- }
437
-
438
- const islandValue = parsed.islandProp!;
439
- // Qwik is resumable — SSR the HTML but skip client hydration.
440
- // The Qwikloader handles resumption automatically.
441
- const ssrOnlyArg = framework === 'qwik' ? ', ssrOnly: true' : '';
442
-
443
- return '{await __pageRenderIsland({ src: "' + srcPath + '"' + fwArg
444
- + ', ...(' + islandValue + ')'
445
- + propsArg
446
- + ssrOnlyArg
447
- + ', ssr: (' + islandValue + ').ssr !== undefined ? (' + islandValue + ').ssr : true'
448
- + ' })}';
449
- }
450
-
451
- /** Check if position `i` is the start of a `<ComponentName` tag (not a longer identifier). */
452
- function isComponentTagStart(code: string, pos: number, tag: string): boolean {
453
- if (!code.startsWith(tag, pos)) return false;
454
- const afterTag = pos + tag.length;
455
- return afterTag >= code.length || !/[a-zA-Z0-9_$]/.test(code[afterTag]);
456
- }
457
-
458
- /**
459
- * Replace all `<Component island={...} />` JSX usages with `{await __pageRenderIsland({...})}`.
460
- */
461
- function replaceIslandJSX(
462
- code: string,
463
- componentName: string,
464
- srcPath: string,
465
- framework: string | undefined,
466
- autoIsland: boolean,
467
- ): string {
468
- const tag = '<' + componentName;
469
- let result = '';
470
- let i = 0;
471
-
472
- while (i < code.length) {
473
- // Skip template literals to avoid transforming code examples
474
- if (code[i] === '`') {
475
- const start = i;
476
- i = skipTemplateLiteral(code, i);
477
- result += code.slice(start, i);
478
- continue;
479
- }
480
-
481
- // Skip JSX comments: {/* ... */}
482
- // When we see '{' followed by '/*', skip until '*/' then '}'
483
- if (code[i] === '{' && code[i + 1] === '/' && code[i + 2] === '*') {
484
- const commentEnd = code.indexOf('*/', i + 3);
485
- if (commentEnd !== -1) {
486
- // Find the closing '}' after '*/'
487
- let afterComment = commentEnd + 2;
488
- while (afterComment < code.length && /\s/.test(code[afterComment])) afterComment++;
489
- if (afterComment < code.length && code[afterComment] === '}') {
490
- result += code.slice(i, afterComment + 1);
491
- i = afterComment + 1;
492
- continue;
493
- }
494
- }
495
- }
496
-
497
- // Skip single-line comments
498
- if (code[i] === '/' && code[i + 1] === '/') {
499
- const lineEnd = code.indexOf('\n', i);
500
- const end = lineEnd === -1 ? code.length : lineEnd + 1;
501
- result += code.slice(i, end);
502
- i = end;
503
- continue;
504
- }
505
-
506
- // Skip block comments
507
- if (code[i] === '/' && code[i + 1] === '*') {
508
- const commentEnd = code.indexOf('*/', i + 2);
509
- const end = commentEnd === -1 ? code.length : commentEnd + 2;
510
- result += code.slice(i, end);
511
- i = end;
512
- continue;
513
- }
514
-
515
- // Check for component tag
516
- if (!isComponentTagStart(code, i, tag)) {
517
- result += code[i];
518
- i++;
519
- continue;
520
- }
521
-
522
- const parsed = parseJSXElement(code, i, componentName);
523
- if (!parsed || (!parsed.islandProp && !autoIsland)) {
524
- // Not parseable, or no island prop and not an auto-island — emit as-is
525
- const end = parsed ? parsed.endIdx : i + 1;
526
- result += code.slice(i, end);
527
- i = end;
528
- continue;
529
- }
530
-
531
- result += buildRenderCall(parsed, srcPath, framework, autoIsland && !parsed.islandProp);
532
- i = parsed.endIdx;
533
- }
534
-
535
- return result;
536
- }
537
-
538
- // ─── Vite Plugin ─────────────────────────────────────────────────────
539
-
540
- export function pageIslandTransform(
541
- options: PageIslandTransformOptions = {},
542
- ): Plugin {
543
- const {
544
- pagesDir = 'src/pages',
545
- layoutsDir = 'src/layouts',
546
- modules = null,
547
- } = options;
548
-
549
- return {
550
- name: 'avalon:page-island-transform',
551
- enforce: 'pre',
552
-
553
- transform(code: string, id: string) {
554
- const isLayout = isLayoutFile(id, layoutsDir, modules);
555
- if (!isPageFile(id, pagesDir, modules) && !isLayout) return null;
556
-
557
- // Find all component imports (PascalCase default imports)
558
- const componentImports = findAllDefaultImports(code);
559
- if (componentImports.length === 0) return null;
560
-
561
- const componentNames = componentImports.map((i) => i.localName);
562
- if (!hasIslandPropUsage(code, componentNames) && !hasAutoIslandUsage(code, componentImports)) return null;
563
-
564
- // Build metadata only for components actually used with island prop
565
- const islandMeta = buildIslandMeta(code, componentImports, id);
566
- if (islandMeta.size === 0) return null;
567
-
568
- let transformed =
569
- "import { renderIsland as __pageRenderIsland } from '@useavalon/avalon';\n" + code;
570
-
571
- for (const [name, meta] of islandMeta) {
572
- transformed = replaceIslandJSX(transformed, name, meta.srcPath, meta.framework, meta.autoIsland);
573
- }
574
-
575
- // Update imports for components used as islands
576
- for (const imp of componentImports) {
577
- if (islandMeta.has(imp.localName)) {
578
- if (isLayout) {
579
- // In layouts, keep the import as a side-effect-only import so the
580
- // island module (and its CSS) stays in Vite's module graph for CSS
581
- // collection. Only the default binding is removed.
582
- transformed = transformed.replace(
583
- imp.fullMatch,
584
- "import '" + imp.importPath + "'; // [page-island-transform] kept for CSS graph: " + imp.localName,
585
- );
586
- } else {
587
- transformed = transformed.replace(
588
- imp.fullMatch,
589
- '// [page-island-transform] removed: ' + imp.localName,
590
- );
591
- }
592
- }
593
- }
594
-
595
- return { code: transformed, map: null };
596
- },
597
- };
598
- }
@@ -1,21 +0,0 @@
1
- export { type PropExtractionResult, FALLBACK_PROPS, extractVueProps } from "./vue.ts";
2
- export { extractSvelteProps } from "./svelte.ts";
3
- export { extractLitProps } from "./lit.ts";
4
- export { extractSolidProps } from "./solid.ts";
5
- export { extractQwikProps } from "./qwik.ts";
6
-
7
- import type { PropExtractionResult } from "./vue.ts";
8
- import { extractVueProps } from "./vue.ts";
9
- import { extractSvelteProps } from "./svelte.ts";
10
- import { extractLitProps } from "./lit.ts";
11
- import { extractSolidProps } from "./solid.ts";
12
- import { extractQwikProps } from "./qwik.ts";
13
-
14
- /** Maps framework name to its prop extractor function */
15
- export const EXTRACTOR_MAP: Record<string, (source: string) => PropExtractionResult> = {
16
- vue: extractVueProps,
17
- svelte: extractSvelteProps,
18
- lit: extractLitProps,
19
- solid: extractSolidProps,
20
- qwik: extractQwikProps,
21
- };