@useavalon/avalon 0.1.1 → 0.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@useavalon/avalon",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Multi-framework islands architecture for the modern web",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -94,56 +94,36 @@ export class IntegrationRegistry {
94
94
  }
95
95
  }
96
96
 
97
- /**
98
- * Internal method to load integration module
99
- * Note: In development mode, integrations should be pre-loaded via preloader.ts
100
- * before Vite's SSR context starts. This method is a fallback for production
101
- * or when integrations weren't pre-loaded.
102
- */
103
97
  private async loadIntegration(name: string): Promise<Integration> {
98
+ const integrationKey = `${name}Integration`;
99
+
100
+ // 1. Try loading from the installed npm package first (@useavalon/<name>)
101
+ try {
102
+ const packageName = `@useavalon/${name}`;
103
+ const module = await import(/* @vite-ignore */ packageName);
104
+ const integration = module[integrationKey] || module.default;
105
+ if (integration) return integration as Integration;
106
+ } catch {
107
+ // Package not installed or import failed — try monorepo path
108
+ }
109
+
110
+ // 2. Monorepo fallback: resolve via packages/integrations/<name>/mod.ts
104
111
  try {
105
- // Use absolute file:// URL to bypass Vite's module resolution
106
- // This ensures we use Deno's native import which handles npm: specifiers correctly
107
112
  const monorepoRoot = findMonorepoRoot();
108
113
  const integrationPath = join(monorepoRoot, "packages", "integrations", name, "mod.ts");
109
114
  const fileUrl = `file://${integrationPath}`;
110
-
111
- // Dynamic import with file:// URL bypasses Vite's SSR module loader
112
115
  const module = await import(/* @vite-ignore */ fileUrl);
113
-
114
- // Look for the integration export (e.g., preactIntegration)
115
- const integrationKey = `${name}Integration`;
116
116
  const integration = module[integrationKey] || module.default;
117
-
118
- if (!integration) {
119
- throw new Error(
120
- `Integration module '${name}' does not export '${integrationKey}' or a default export`
121
- );
122
- }
123
-
124
- return integration as Integration;
125
- } catch (error) {
126
- // Check if this is a Vite SSR context issue
127
- const errorMessage = error instanceof Error ? error.message : String(error);
128
- const isViteIssue = errorMessage.includes('ERR_UNSUPPORTED_ESM_URL_SCHEME') ||
129
- errorMessage.includes('Only file and data URLs are supported');
130
-
131
- if (isViteIssue) {
132
- throw new Error(
133
- `Integration '${name}' could not be loaded within Vite's SSR context. ` +
134
- `This usually means the integration wasn't pre-loaded at server startup. ` +
135
- `Make sure preloadIntegrationsNative() is called before Vite server starts.`,
136
- { cause: error }
137
- );
138
- }
139
-
140
- throw new Error(
141
- `Failed to load integration for framework '${name}'. ` +
142
- `Make sure @useavalon/${name} is installed.\n` +
143
- `Install it with: bun add @useavalon/${name}`,
144
- { cause: error }
145
- );
117
+ if (integration) return integration as Integration;
118
+ } catch {
119
+ // Monorepo path also failed
146
120
  }
121
+
122
+ throw new Error(
123
+ `Failed to load integration for framework '${name}'. ` +
124
+ `Make sure @useavalon/${name} is installed.\n` +
125
+ `Install it with: bun add @useavalon/${name}`,
126
+ );
147
127
  }
148
128
 
149
129
  /**
@@ -44,6 +44,8 @@ import {
44
44
  discoverErrorPages,
45
45
  type ErrorHandlerOptions,
46
46
  } from './error-handler.ts';
47
+ import { h } from 'preact';
48
+ import preactRenderToString from 'preact-render-to-string';
47
49
 
48
50
  /**
49
51
  * Resolved page route information
@@ -626,17 +628,27 @@ async function renderPageComponent(
626
628
  context: NitroRenderContext,
627
629
  _options: SSRRenderOptions,
628
630
  ): Promise<string> {
629
- // This would integrate with the existing renderToHtml function
630
- // For now, return a basic structure showing the integration point
631
+ const Component = pageModule.default as (props?: Record<string, unknown>) => unknown;
632
+ const metadata = pageModule.metadata || {};
631
633
 
632
- // In the real implementation, this would:
633
- // 1. Import and use renderToHtml from '../render/ssr.ts'
634
- // 2. Create a RouteConfig from the pageModule
635
- // 3. Apply layouts using the layout resolver
636
- // 4. Return the fully rendered HTML
634
+ // Call the page component (supports async components)
635
+ let vnode: unknown;
636
+ try {
637
+ const result = Component(pageProps);
638
+ vnode = result instanceof Promise ? await result : result;
639
+ } catch (err) {
640
+ console.error('[renderer] Error calling page component:', err);
641
+ vnode = h('div', null, 'Error rendering page');
642
+ }
637
643
 
638
- const componentName = (pageModule.default as { name?: string })?.name || 'Page';
639
- const metadata = pageModule.metadata || {};
644
+ // Render the vnode to HTML string using Preact SSR
645
+ let pageHtml: string;
646
+ try {
647
+ pageHtml = preactRenderToString(vnode as any);
648
+ } catch (err) {
649
+ console.error('[renderer] Error in preactRenderToString:', err);
650
+ pageHtml = '<div>Error rendering page</div>';
651
+ }
640
652
 
641
653
  return `<!DOCTYPE html>
642
654
  <html lang="en">
@@ -647,9 +659,10 @@ async function renderPageComponent(
647
659
  ${metadata.description ? `<meta name="description" content="${escapeHtml(String(metadata.description))}">` : ''}
648
660
  </head>
649
661
  <body>
650
- <div id="app" data-page="${escapeHtml(String(componentName))}" data-props='${escapeHtml(JSON.stringify(pageProps))}'>
651
- <!-- Page content rendered by Avalon SSR pipeline -->
662
+ <div id="app">
663
+ ${pageHtml}
652
664
  </div>
665
+ <script type="module" src="/src/client/main.js"></script>
653
666
  </body>
654
667
  </html>`;
655
668
  }
@@ -10,6 +10,8 @@
10
10
  import type { Plugin, ViteDevServer } from 'vite';
11
11
  import { nitro as nitroVitePlugin } from 'nitro/vite';
12
12
  import { stat as fsStat } from 'node:fs/promises';
13
+ import { createRequire } from 'node:module';
14
+ import { dirname, join } from 'node:path';
13
15
  import type { ResolvedAvalonConfig } from './types.ts';
14
16
  import { createNitroConfig, type AvalonNitroConfig, type NitroConfigOutput } from '../nitro/config.ts';
15
17
  import type { PageModule } from '../nitro/types.ts';
@@ -27,6 +29,26 @@ import { collectCssFromModuleGraph, injectSsrCss } from '../render/collect-css.t
27
29
  import { getUniversalCSSForHead } from '../islands/universal-css-collector.ts';
28
30
  import { getUniversalHeadForInjection } from '../islands/universal-head-collector.ts';
29
31
 
32
+ /**
33
+ * Resolves the absolute path to a file inside @useavalon/avalon's source tree.
34
+ */
35
+ function resolveAvalonPackagePath(relativePath: string): string {
36
+ const require = createRequire(import.meta.url);
37
+ const modEntry = require.resolve('@useavalon/avalon');
38
+ const pkgRoot = dirname(modEntry);
39
+ return join(pkgRoot, relativePath);
40
+ }
41
+
42
+ /**
43
+ * Resolves the absolute path to a file inside an @useavalon/<name> integration package.
44
+ */
45
+ function resolveIntegrationPackagePath(name: string, relativePath: string): string {
46
+ const require = createRequire(import.meta.url);
47
+ const modEntry = require.resolve(`@useavalon/${name}`);
48
+ const pkgRoot = dirname(modEntry);
49
+ return join(pkgRoot, relativePath);
50
+ }
51
+
30
52
  export const VIRTUAL_MODULE_IDS = {
31
53
  PAGE_ROUTES: 'virtual:avalon/page-routes',
32
54
  ISLAND_MANIFEST: 'virtual:avalon/island-manifest',
@@ -342,18 +364,16 @@ async function handle404(
342
364
  async function prewarmCoreModules(server: ViteDevServer, verbose?: boolean): Promise<void> {
343
365
  const prewarmStart = performance.now();
344
366
 
367
+ const frameworkNames = ['react', 'vue', 'solid', 'svelte', 'lit', 'preact'] as const;
368
+
345
369
  const coreModules = [
346
- // SSR infrastructure
347
- { path: '../packages/avalon/src/render/ssr.ts', assignTo: 'ssr' },
348
- { path: '../packages/avalon/src/core/layout/enhanced-layout-resolver.ts', assignTo: 'layout' },
349
- { path: '../packages/avalon/src/middleware/index.ts', assignTo: null },
350
- // Framework renderers — prewarm so first island render is fast
351
- { path: '../packages/integrations/react/server/renderer.ts', assignTo: null },
352
- { path: '../packages/integrations/vue/server/renderer.ts', assignTo: null },
353
- { path: '../packages/integrations/solid/server/renderer.ts', assignTo: null },
354
- { path: '../packages/integrations/svelte/server/renderer.ts', assignTo: null },
355
- { path: '../packages/integrations/lit/server/renderer.ts', assignTo: null },
356
- { path: '../packages/integrations/preact/server/renderer.ts', assignTo: null },
370
+ { path: resolveAvalonPackagePath('src/render/ssr.ts'), assignTo: 'ssr' as string | null },
371
+ { path: resolveAvalonPackagePath('src/core/layout/enhanced-layout-resolver.ts'), assignTo: 'layout' as string | null },
372
+ { path: resolveAvalonPackagePath('src/middleware/index.ts'), assignTo: null as string | null },
373
+ ...frameworkNames.map(name => ({
374
+ path: resolveIntegrationPackagePath(name, 'server/renderer.ts'),
375
+ assignTo: null as string | null,
376
+ })),
357
377
  ];
358
378
 
359
379
  const results = await Promise.allSettled(
@@ -1198,14 +1218,14 @@ async function renderPageToHtml(
1198
1218
 
1199
1219
  try {
1200
1220
  if (!cachedSSRModule) {
1201
- cachedSSRModule = await server.ssrLoadModule('../packages/avalon/src/render/ssr.ts');
1221
+ cachedSSRModule = await server.ssrLoadModule(resolveAvalonPackagePath('src/render/ssr.ts'));
1202
1222
  }
1203
1223
  // cachedSSRModule is the dynamically-loaded render/ssr.ts module;
1204
1224
  // expected exports: renderToHtml, renderToHtmlWithLayouts
1205
1225
  const ssrModule = cachedSSRModule as Record<string, unknown>;
1206
1226
 
1207
1227
  if (!cachedLayoutModule) {
1208
- cachedLayoutModule = await server.ssrLoadModule('../packages/avalon/src/core/layout/enhanced-layout-resolver.ts');
1228
+ cachedLayoutModule = await server.ssrLoadModule(resolveAvalonPackagePath('src/core/layout/enhanced-layout-resolver.ts'));
1209
1229
  }
1210
1230
  // cachedLayoutModule is the dynamically-loaded enhanced-layout-resolver.ts module;
1211
1231
  // expected exports: EnhancedLayoutResolver, EnhancedLayoutResolverUtils
@@ -16,6 +16,8 @@ import type {
16
16
  IntegrationName,
17
17
  ResolvedAvalonConfig,
18
18
  } from "./types.ts";
19
+ import { createRequire } from "node:module";
20
+ import { dirname, join } from "node:path";
19
21
  import { resolveConfig, checkDirectoriesExist } from "./config.ts";
20
22
  import { activateIntegrations, activateSingleIntegration } from "./integration-activator.ts";
21
23
  import { discoverIntegrationsFromIslandUsage } from "./auto-discover.ts";
@@ -254,6 +256,18 @@ export async function avalon(config?: AvalonPluginConfig): Promise<PluginOption[
254
256
  verbose: preResolvedConfig.verbose,
255
257
  });
256
258
 
259
+ // Resolve the path to @useavalon/avalon's client entry for the /src/client/main.js alias.
260
+ // In the monorepo www/ project this is handled by a manual resolve.alias; for standalone
261
+ // projects the plugin must do it automatically.
262
+ let clientMainResolved: string | null = null;
263
+ try {
264
+ const require = createRequire(import.meta.url);
265
+ const clientEntry = require.resolve("@useavalon/avalon/client");
266
+ clientMainResolved = join(dirname(clientEntry), "main.js");
267
+ } catch {
268
+ // Inside the monorepo www/ project — it sets its own alias in vite.config.ts
269
+ }
270
+
257
271
  // The main Avalon plugin
258
272
  const avalonPlugin: Plugin = {
259
273
  name: "avalon",
@@ -269,6 +283,16 @@ export async function avalon(config?: AvalonPluginConfig): Promise<PluginOption[
269
283
  checkDirectoriesExist(resolvedConfig, resolvedViteConfig.root);
270
284
  },
271
285
 
286
+ resolveId(id: string) {
287
+ // Resolve /src/client/main.js to the actual file inside @useavalon/avalon.
288
+ // The SSR renderer injects <script src="/src/client/main.js"> into HTML.
289
+ // Without this alias, standalone projects get a 404.
290
+ if (id === "/src/client/main.js" && clientMainResolved) {
291
+ return clientMainResolved;
292
+ }
293
+ return null;
294
+ },
295
+
272
296
  async buildStart() {
273
297
  await runAutoDiscovery(resolvedConfig, viteConfig?.root, activeIntegrations);
274
298
  runValidation(resolvedConfig, activeIntegrations);