@useavalon/avalon 0.1.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.
Files changed (159) hide show
  1. package/README.md +54 -0
  2. package/mod.ts +301 -0
  3. package/package.json +85 -0
  4. package/src/build/README.md +310 -0
  5. package/src/build/integration-bundler-plugin.ts +116 -0
  6. package/src/build/integration-config.ts +168 -0
  7. package/src/build/integration-detection-plugin.ts +117 -0
  8. package/src/build/integration-resolver-plugin.ts +90 -0
  9. package/src/build/island-manifest.ts +269 -0
  10. package/src/build/island-types-generator.ts +476 -0
  11. package/src/build/mdx-island-transform.ts +464 -0
  12. package/src/build/mdx-plugin.ts +98 -0
  13. package/src/build/page-island-transform.ts +598 -0
  14. package/src/build/prop-extractors/index.ts +21 -0
  15. package/src/build/prop-extractors/lit.ts +140 -0
  16. package/src/build/prop-extractors/qwik.ts +16 -0
  17. package/src/build/prop-extractors/solid.ts +125 -0
  18. package/src/build/prop-extractors/svelte.ts +194 -0
  19. package/src/build/prop-extractors/vue.ts +111 -0
  20. package/src/build/sidecar-file-manager.ts +104 -0
  21. package/src/build/sidecar-renderer.ts +30 -0
  22. package/src/client/adapters/index.ts +13 -0
  23. package/src/client/adapters/lit-adapter.ts +654 -0
  24. package/src/client/adapters/preact-adapter.ts +331 -0
  25. package/src/client/adapters/qwik-adapter.ts +345 -0
  26. package/src/client/adapters/react-adapter.ts +353 -0
  27. package/src/client/adapters/solid-adapter.ts +451 -0
  28. package/src/client/adapters/svelte-adapter.ts +524 -0
  29. package/src/client/adapters/vue-adapter.ts +467 -0
  30. package/src/client/components.ts +35 -0
  31. package/src/client/css-hmr-handler.ts +344 -0
  32. package/src/client/framework-adapter.ts +462 -0
  33. package/src/client/hmr-coordinator.ts +396 -0
  34. package/src/client/hmr-error-overlay.js +533 -0
  35. package/src/client/main.js +816 -0
  36. package/src/client/tests/css-hmr-handler.test.ts +360 -0
  37. package/src/client/tests/framework-adapter.test.ts +519 -0
  38. package/src/client/tests/hmr-coordinator.test.ts +176 -0
  39. package/src/client/tests/hydration-option-parsing.test.ts +107 -0
  40. package/src/client/tests/lit-adapter.test.ts +427 -0
  41. package/src/client/tests/preact-adapter.test.ts +353 -0
  42. package/src/client/tests/qwik-adapter.test.ts +343 -0
  43. package/src/client/tests/react-adapter.test.ts +317 -0
  44. package/src/client/tests/solid-adapter.test.ts +396 -0
  45. package/src/client/tests/svelte-adapter.test.ts +387 -0
  46. package/src/client/tests/vue-adapter.test.ts +407 -0
  47. package/src/client/types/framework-runtime.d.ts +68 -0
  48. package/src/client/types/vite-hmr.d.ts +46 -0
  49. package/src/client/types/vite-virtual-modules.d.ts +60 -0
  50. package/src/components/Image.tsx +123 -0
  51. package/src/components/IslandErrorBoundary.tsx +145 -0
  52. package/src/components/LayoutDataErrorBoundary.tsx +141 -0
  53. package/src/components/LayoutErrorBoundary.tsx +127 -0
  54. package/src/components/PersistentIsland.tsx +52 -0
  55. package/src/components/StreamingErrorBoundary.tsx +233 -0
  56. package/src/components/StreamingLayout.tsx +538 -0
  57. package/src/components/tests/component-analyzer.test.ts +96 -0
  58. package/src/components/tests/component-detection.test.ts +347 -0
  59. package/src/components/tests/persistent-islands.test.ts +398 -0
  60. package/src/core/components/component-analyzer.ts +192 -0
  61. package/src/core/components/component-detection.ts +508 -0
  62. package/src/core/components/enhanced-framework-detector.ts +500 -0
  63. package/src/core/components/framework-registry.ts +563 -0
  64. package/src/core/components/tests/enhanced-framework-detector.test.ts +577 -0
  65. package/src/core/components/tests/framework-registry.test.ts +465 -0
  66. package/src/core/content/mdx-processor.ts +46 -0
  67. package/src/core/integrations/README.md +282 -0
  68. package/src/core/integrations/index.ts +19 -0
  69. package/src/core/integrations/loader.ts +125 -0
  70. package/src/core/integrations/registry.ts +195 -0
  71. package/src/core/islands/island-persistence.ts +325 -0
  72. package/src/core/islands/island-state-serializer.ts +258 -0
  73. package/src/core/islands/persistent-island-context.tsx +80 -0
  74. package/src/core/islands/use-persistent-state.ts +68 -0
  75. package/src/core/layout/enhanced-layout-resolver.ts +322 -0
  76. package/src/core/layout/layout-cache-manager.ts +485 -0
  77. package/src/core/layout/layout-composer.ts +357 -0
  78. package/src/core/layout/layout-data-loader.ts +516 -0
  79. package/src/core/layout/layout-discovery.ts +243 -0
  80. package/src/core/layout/layout-matcher.ts +299 -0
  81. package/src/core/layout/layout-types.ts +110 -0
  82. package/src/core/layout/tests/enhanced-layout-resolver.test.ts +477 -0
  83. package/src/core/layout/tests/layout-cache-optimization.test.ts +149 -0
  84. package/src/core/layout/tests/layout-composer.test.ts +486 -0
  85. package/src/core/layout/tests/layout-data-loader.test.ts +443 -0
  86. package/src/core/layout/tests/layout-discovery.test.ts +253 -0
  87. package/src/core/layout/tests/layout-matcher.test.ts +480 -0
  88. package/src/core/modules/framework-module-resolver.ts +273 -0
  89. package/src/core/modules/tests/framework-module-resolver.test.ts +263 -0
  90. package/src/core/modules/tests/module-resolution-integration.test.ts +117 -0
  91. package/src/islands/component-analysis.ts +213 -0
  92. package/src/islands/css-utils.ts +565 -0
  93. package/src/islands/discovery/index.ts +80 -0
  94. package/src/islands/discovery/registry.ts +340 -0
  95. package/src/islands/discovery/resolver.ts +477 -0
  96. package/src/islands/discovery/scanner.ts +386 -0
  97. package/src/islands/discovery/tests/island-discovery.test.ts +881 -0
  98. package/src/islands/discovery/types.ts +117 -0
  99. package/src/islands/discovery/validator.ts +544 -0
  100. package/src/islands/discovery/watcher.ts +368 -0
  101. package/src/islands/framework-detection.ts +428 -0
  102. package/src/islands/integration-loader.ts +490 -0
  103. package/src/islands/island.tsx +565 -0
  104. package/src/islands/render-cache.ts +550 -0
  105. package/src/islands/types.ts +80 -0
  106. package/src/islands/universal-css-collector.ts +157 -0
  107. package/src/islands/universal-head-collector.ts +137 -0
  108. package/src/layout-system.d.ts +592 -0
  109. package/src/layout-system.ts +218 -0
  110. package/src/middleware/__tests__/discovery.test.ts +107 -0
  111. package/src/middleware/discovery.ts +268 -0
  112. package/src/middleware/executor.ts +315 -0
  113. package/src/middleware/index.ts +76 -0
  114. package/src/middleware/types.ts +99 -0
  115. package/src/nitro/build-config.ts +576 -0
  116. package/src/nitro/config.ts +483 -0
  117. package/src/nitro/error-handler.ts +636 -0
  118. package/src/nitro/index.ts +173 -0
  119. package/src/nitro/island-manifest.ts +584 -0
  120. package/src/nitro/middleware-adapter.ts +260 -0
  121. package/src/nitro/renderer.ts +1458 -0
  122. package/src/nitro/route-discovery.ts +439 -0
  123. package/src/nitro/types.ts +321 -0
  124. package/src/render/collect-css.ts +198 -0
  125. package/src/render/error-pages.ts +79 -0
  126. package/src/render/isolated-ssr-renderer.ts +654 -0
  127. package/src/render/ssr.ts +1030 -0
  128. package/src/schemas/api.ts +30 -0
  129. package/src/schemas/core.ts +64 -0
  130. package/src/schemas/index.ts +212 -0
  131. package/src/schemas/layout.ts +279 -0
  132. package/src/schemas/routing/index.ts +38 -0
  133. package/src/schemas/routing.ts +376 -0
  134. package/src/types/as-island.ts +20 -0
  135. package/src/types/image.d.ts +106 -0
  136. package/src/types/index.d.ts +22 -0
  137. package/src/types/island-jsx.d.ts +33 -0
  138. package/src/types/island-prop.d.ts +20 -0
  139. package/src/types/layout.ts +285 -0
  140. package/src/types/mdx.d.ts +6 -0
  141. package/src/types/routing.ts +555 -0
  142. package/src/types/tests/layout-types.test.ts +197 -0
  143. package/src/types/types.ts +5 -0
  144. package/src/types/urlpattern.d.ts +49 -0
  145. package/src/types/vite-env.d.ts +11 -0
  146. package/src/utils/dev-logger.ts +299 -0
  147. package/src/utils/fs.ts +151 -0
  148. package/src/vite-plugin/auto-discover.ts +551 -0
  149. package/src/vite-plugin/config.ts +266 -0
  150. package/src/vite-plugin/errors.ts +127 -0
  151. package/src/vite-plugin/image-optimization.ts +151 -0
  152. package/src/vite-plugin/integration-activator.ts +126 -0
  153. package/src/vite-plugin/island-sidecar-plugin.ts +176 -0
  154. package/src/vite-plugin/module-discovery.ts +189 -0
  155. package/src/vite-plugin/nitro-integration.ts +1334 -0
  156. package/src/vite-plugin/plugin.ts +329 -0
  157. package/src/vite-plugin/tests/image-optimization.test.ts +54 -0
  158. package/src/vite-plugin/types.ts +327 -0
  159. package/src/vite-plugin/validation.ts +228 -0
@@ -0,0 +1,176 @@
1
+ import type { Plugin } from "vite";
2
+ import { readFile, access } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { detectFrameworkFromPath } from "../islands/integration-loader.ts";
5
+ import { EXTRACTOR_MAP } from "../build/prop-extractors/index.ts";
6
+ import { renderSidecarContent } from "../build/sidecar-renderer.ts";
7
+ import {
8
+ getSidecarPath,
9
+ writeSidecarIfChanged,
10
+ deleteSidecar,
11
+ isSidecarFresh,
12
+ } from "../build/sidecar-file-manager.ts";
13
+
14
+ export interface SidecarPluginOptions {
15
+ /** Whether to log verbose output */
16
+ verbose?: boolean;
17
+ }
18
+
19
+ /** Frameworks that should be skipped — they already work natively with Preact/React JSX */
20
+ const SKIP_FRAMEWORKS = new Set(["react", "preact"]);
21
+
22
+ /** File extensions that qualify as island source files for HMR */
23
+ const ISLAND_EXTENSIONS = [".vue", ".svelte", ".lit.ts", ".solid.tsx", ".qwik.tsx"];
24
+
25
+ /**
26
+ * Check tsconfig.json for `allowArbitraryExtensions` and warn if missing.
27
+ */
28
+ export async function checkTsConfigForArbitraryExtensions(
29
+ projectRoot: string,
30
+ ): Promise<void> {
31
+ const tsconfigPath = path.join(projectRoot, "tsconfig.json");
32
+ try {
33
+ const raw = await readFile(tsconfigPath, "utf-8");
34
+ const tsconfig = JSON.parse(raw);
35
+ if (tsconfig?.compilerOptions?.allowArbitraryExtensions !== true) {
36
+ console.warn(
37
+ '[avalon] tsconfig.json is missing "allowArbitraryExtensions: true" — sidecar .d.[ext].ts files require this setting',
38
+ );
39
+ }
40
+ } catch {
41
+ // tsconfig doesn't exist or can't be parsed - skip warning
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Check if a file path is already a sidecar declaration file.
47
+ * Sidecar files contain `.d.` before the framework extension.
48
+ */
49
+ function isSidecarFile(filePath: string): boolean {
50
+ const basename = path.basename(filePath);
51
+ return /\.d\.(vue|svelte|lit|solid\.tsx)/.test(basename);
52
+ }
53
+
54
+ /**
55
+ * Check if a file path looks like a supported component file that needs a sidecar.
56
+ * Excludes files that are already sidecar declaration files.
57
+ */
58
+ function needsSidecar(filePath: string): boolean {
59
+ if (isSidecarFile(filePath)) {
60
+ return false;
61
+ }
62
+ return ISLAND_EXTENSIONS.some((ext) => filePath.endsWith(ext));
63
+ }
64
+
65
+ /**
66
+ * Generate a sidecar for a single component file.
67
+ * Returns true if a sidecar was written/updated, false otherwise.
68
+ */
69
+ async function generateSidecarForFile(filePath: string, verbose?: boolean): Promise<boolean> {
70
+ try {
71
+ const framework = detectFrameworkFromPath(filePath);
72
+ if (SKIP_FRAMEWORKS.has(framework)) {
73
+ return false;
74
+ }
75
+
76
+ const extractor = EXTRACTOR_MAP[framework];
77
+ if (!extractor) {
78
+ return false;
79
+ }
80
+
81
+ const source = await readFile(filePath, "utf-8");
82
+ const result = extractor(source);
83
+ const content = renderSidecarContent(result.propsType);
84
+ const sidecarPath = getSidecarPath(filePath);
85
+ return await writeSidecarIfChanged(sidecarPath, content);
86
+ } catch (err) {
87
+ if (verbose) {
88
+ console.warn(
89
+ `[avalon] Failed to generate sidecar for ${filePath}:`,
90
+ err instanceof Error ? err.message : err,
91
+ );
92
+ }
93
+ return false;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Vite plugin that auto-generates `.d.[ext].ts` sidecar declaration files
99
+ * for non-React/Preact components when they are used as islands.
100
+ *
101
+ * Sidecars are generated on-demand when component files are loaded,
102
+ * rather than scanning a fixed directory at startup.
103
+ */
104
+ export function islandSidecarPlugin(options: SidecarPluginOptions = {}): Plugin {
105
+ let projectRoot: string;
106
+ const processedFiles = new Set<string>();
107
+
108
+ return {
109
+ name: "avalon:island-sidecar",
110
+
111
+ configResolved(config) {
112
+ projectRoot = config.root;
113
+ },
114
+
115
+ async buildStart() {
116
+ await checkTsConfigForArbitraryExtensions(projectRoot);
117
+ processedFiles.clear();
118
+ },
119
+
120
+ // Generate sidecar when a component file is loaded
121
+ async load(id) {
122
+ if (!needsSidecar(id) || processedFiles.has(id)) {
123
+ return null;
124
+ }
125
+
126
+ processedFiles.add(id);
127
+
128
+ // Check if sidecar needs regeneration
129
+ const sidecarPath = getSidecarPath(id);
130
+ if (await isSidecarFresh(id, sidecarPath)) {
131
+ return null;
132
+ }
133
+
134
+ const wrote = await generateSidecarForFile(id, options.verbose);
135
+ if (wrote && options.verbose) {
136
+ console.log(`[avalon] Generated sidecar for: ${id}`);
137
+ }
138
+
139
+ return null; // Let Vite handle the actual file loading
140
+ },
141
+
142
+ async handleHotUpdate(ctx) {
143
+ const filePath = ctx.file;
144
+
145
+ if (!needsSidecar(filePath)) {
146
+ return;
147
+ }
148
+
149
+ // Check if the file was deleted
150
+ let fileExists = true;
151
+ try {
152
+ await access(filePath);
153
+ } catch {
154
+ fileExists = false;
155
+ }
156
+
157
+ if (!fileExists) {
158
+ // File was deleted — remove the sidecar
159
+ const sidecarPath = getSidecarPath(filePath);
160
+ const deleted = await deleteSidecar(sidecarPath);
161
+ if (deleted && options.verbose) {
162
+ console.log(`[avalon] Deleted sidecar for removed file: ${filePath}`);
163
+ }
164
+ processedFiles.delete(filePath);
165
+ return;
166
+ }
167
+
168
+ // File was changed — regenerate sidecar
169
+ processedFiles.delete(filePath); // Allow re-processing
170
+ const wrote = await generateSidecarForFile(filePath, options.verbose);
171
+ if (wrote && options.verbose) {
172
+ console.log(`[avalon] Updated sidecar for: ${filePath}`);
173
+ }
174
+ },
175
+ };
176
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Module Discovery for Modular Architecture
3
+ *
4
+ * Discovers pages and layouts within feature modules for file-based routing.
5
+ * Supports co-located architecture where each module contains its own pages/layouts.
6
+ */
7
+
8
+ import { readdir, stat } from "node:fs/promises";
9
+ import { resolve, join } from "node:path";
10
+ import type { ResolvedModulesConfig } from "./types.ts";
11
+
12
+ /**
13
+ * Discovered module with its pages and layouts directories
14
+ */
15
+ export interface DiscoveredModule {
16
+ /** Module name (folder name) */
17
+ name: string;
18
+ /** Absolute path to the module directory */
19
+ path: string;
20
+ /** Absolute path to pages directory (if exists) */
21
+ pagesDir: string | null;
22
+ /** Absolute path to layouts directory (if exists) */
23
+ layoutsDir: string | null;
24
+ /** Route prefix derived from module name */
25
+ routePrefix: string;
26
+ }
27
+
28
+ /**
29
+ * Result of module discovery
30
+ */
31
+ export interface ModuleDiscoveryResult {
32
+ /** All discovered modules */
33
+ modules: DiscoveredModule[];
34
+ /** All page directories (for route discovery) */
35
+ pageDirs: Array<{ dir: string; prefix: string }>;
36
+ /** All layout directories */
37
+ layoutDirs: Array<{ dir: string; prefix: string }>;
38
+ }
39
+
40
+ /**
41
+ * Discover all modules within the modules directory
42
+ *
43
+ * @param modulesConfig - Resolved modules configuration
44
+ * @param projectRoot - Project root directory
45
+ * @returns Discovery result with modules, page dirs, and layout dirs
46
+ */
47
+ export async function discoverModules(
48
+ modulesConfig: ResolvedModulesConfig,
49
+ projectRoot: string
50
+ ): Promise<ModuleDiscoveryResult> {
51
+ const modulesDir = resolve(projectRoot, modulesConfig.dir);
52
+ const modules: DiscoveredModule[] = [];
53
+ const pageDirs: Array<{ dir: string; prefix: string }> = [];
54
+ const layoutDirs: Array<{ dir: string; prefix: string }> = [];
55
+
56
+ try {
57
+ const entries = await readdir(modulesDir, { withFileTypes: true });
58
+
59
+ for (const entry of entries) {
60
+ if (!entry.isDirectory()) continue;
61
+
62
+ // Skip hidden directories and common non-module folders
63
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
64
+
65
+ const modulePath = join(modulesDir, entry.name);
66
+ const pagesPath = join(modulePath, modulesConfig.pagesDirName);
67
+ const layoutsPath = join(modulePath, modulesConfig.layoutsDirName);
68
+
69
+ // Check if pages/layouts directories exist
70
+ const [pagesExists, layoutsExists] = await Promise.all([
71
+ directoryExists(pagesPath),
72
+ directoryExists(layoutsPath),
73
+ ]);
74
+
75
+ // Determine route prefix
76
+ // 'home' or 'root' module maps to '/', others map to '/moduleName'
77
+ const routePrefix = getRoutePrefix(entry.name);
78
+
79
+ const module: DiscoveredModule = {
80
+ name: entry.name,
81
+ path: modulePath,
82
+ pagesDir: pagesExists ? pagesPath : null,
83
+ layoutsDir: layoutsExists ? layoutsPath : null,
84
+ routePrefix,
85
+ };
86
+
87
+ modules.push(module);
88
+
89
+ if (pagesExists) {
90
+ pageDirs.push({ dir: pagesPath, prefix: routePrefix });
91
+ }
92
+
93
+ if (layoutsExists) {
94
+ layoutDirs.push({ dir: layoutsPath, prefix: routePrefix });
95
+ }
96
+ }
97
+ } catch (error) {
98
+ // Modules directory doesn't exist or can't be read
99
+ // This is fine - modules are optional
100
+ }
101
+
102
+ // Sort modules so 'home'/'root' comes first (for route priority)
103
+ modules.sort((a, b) => {
104
+ if (a.routePrefix === '/') return -1;
105
+ if (b.routePrefix === '/') return 1;
106
+ return a.name.localeCompare(b.name);
107
+ });
108
+
109
+ return { modules, pageDirs, layoutDirs };
110
+ }
111
+
112
+ /**
113
+ * Get the route prefix for a module
114
+ * Special modules like 'home', 'root', 'main' map to '/'
115
+ */
116
+ function getRoutePrefix(moduleName: string): string {
117
+ const rootModules = ['home', 'root', 'main', 'index'];
118
+ if (rootModules.includes(moduleName.toLowerCase())) {
119
+ return '/';
120
+ }
121
+ return '/' + moduleName;
122
+ }
123
+
124
+ /**
125
+ * Check if a directory exists
126
+ */
127
+ async function directoryExists(path: string): Promise<boolean> {
128
+ try {
129
+ const stats = await stat(path);
130
+ return stats.isDirectory();
131
+ } catch {
132
+ return false;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Get all page directories including both traditional and modular
138
+ *
139
+ * @param pagesDir - Traditional pages directory
140
+ * @param modulesConfig - Modules configuration (if any)
141
+ * @param projectRoot - Project root
142
+ * @returns Array of page directories with their route prefixes
143
+ */
144
+ export async function getAllPageDirs(
145
+ pagesDir: string,
146
+ modulesConfig: ResolvedModulesConfig | null,
147
+ projectRoot: string
148
+ ): Promise<Array<{ dir: string; prefix: string }>> {
149
+ const dirs: Array<{ dir: string; prefix: string }> = [];
150
+
151
+ // Add traditional pages directory
152
+ const traditionalPagesPath = resolve(projectRoot, pagesDir);
153
+ if (await directoryExists(traditionalPagesPath)) {
154
+ dirs.push({ dir: traditionalPagesPath, prefix: '/' });
155
+ }
156
+
157
+ // Add modular page directories
158
+ if (modulesConfig) {
159
+ const { pageDirs } = await discoverModules(modulesConfig, projectRoot);
160
+ dirs.push(...pageDirs);
161
+ }
162
+
163
+ return dirs;
164
+ }
165
+
166
+ /**
167
+ * Get all layout directories including both traditional and modular
168
+ */
169
+ export async function getAllLayoutDirs(
170
+ layoutsDir: string,
171
+ modulesConfig: ResolvedModulesConfig | null,
172
+ projectRoot: string
173
+ ): Promise<Array<{ dir: string; prefix: string }>> {
174
+ const dirs: Array<{ dir: string; prefix: string }> = [];
175
+
176
+ // Add traditional layouts directory
177
+ const traditionalLayoutsPath = resolve(projectRoot, layoutsDir);
178
+ if (await directoryExists(traditionalLayoutsPath)) {
179
+ dirs.push({ dir: traditionalLayoutsPath, prefix: '/' });
180
+ }
181
+
182
+ // Add modular layout directories
183
+ if (modulesConfig) {
184
+ const { layoutDirs } = await discoverModules(modulesConfig, projectRoot);
185
+ dirs.push(...layoutDirs);
186
+ }
187
+
188
+ return dirs;
189
+ }