what-compiler 0.8.4 → 0.10.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.
@@ -206,6 +206,20 @@ export function extractPageConfig(source) {
206
206
  }
207
207
  }
208
208
 
209
+ /**
210
+ * Detect which named exports a page module declares. Functions (loader,
211
+ * getStaticPaths) cannot live in `export const page` (JSON-only), so the codegen
212
+ * imports them as live bindings instead. \b anchors avoid false positives like
213
+ * `loaderState` or `getStaticPathsHelper`.
214
+ */
215
+ export function detectPageExports(source) {
216
+ return {
217
+ hasLoader: /export\s+(?:async\s+)?(?:const|let|var|function)\s+loader\b/.test(source),
218
+ hasGetStaticPaths: /export\s+(?:async\s+)?(?:const|let|var|function)\s+getStaticPaths\b/.test(source),
219
+ hasPageConfig: /export\s+const\s+page\b/.test(source),
220
+ };
221
+ }
222
+
209
223
  /**
210
224
  * Generate the virtual routes module source code.
211
225
  * This is what gets imported as 'virtual:what-routes'.
@@ -231,11 +245,13 @@ export function generateRoutesModule(pagesDir, rootDir) {
231
245
  const relPath = toImportPath(page.filePath, rootDir);
232
246
  imports.push(`import ${varName} from '${relPath}';`);
233
247
 
234
- // Read file to extract page config
248
+ // Read file to extract page config + detect loader/getStaticPaths exports
235
249
  let pageConfig = { mode: 'client' };
250
+ let detected = { hasLoader: false, hasGetStaticPaths: false, hasPageConfig: false };
236
251
  try {
237
252
  const source = fs.readFileSync(page.filePath, 'utf-8');
238
253
  pageConfig = extractPageConfig(source);
254
+ detected = detectPageExports(source);
239
255
  } catch {}
240
256
 
241
257
  // Find matching layout (closest parent)
@@ -246,6 +262,7 @@ export function generateRoutesModule(pagesDir, rootDir) {
246
262
  component: varName,
247
263
  mode: pageConfig.mode || 'client',
248
264
  layout: layoutVar || null,
265
+ hasLoader: detected.hasLoader,
249
266
  };
250
267
 
251
268
  routeEntries.push(entry);
@@ -272,7 +289,7 @@ export function generateRoutesModule(pagesDir, rootDir) {
272
289
  '',
273
290
  'export const routes = [',
274
291
  ...routeEntries.map(r =>
275
- ` { path: '${r.path}', component: ${r.component}, mode: '${r.mode}'${r.layout ? `, layout: ${r.layout}` : ''} },`
292
+ ` { path: '${r.path}', component: ${r.component}, mode: '${r.mode}'${r.layout ? `, layout: ${r.layout}` : ''}${r.hasLoader ? ', hasLoader: true' : ''} },`
276
293
  ),
277
294
  '];',
278
295
  '',
@@ -293,6 +310,91 @@ export function generateRoutesModule(pagesDir, rootDir) {
293
310
  return lines.join('\n');
294
311
  }
295
312
 
313
+ /**
314
+ * Generate the SERVER routes module ('virtual:what-routes/server'). Same routes
315
+ * as the client module PLUS live bindings for loader / getStaticPaths / page so
316
+ * the deploy adapter can run them. The client module (above) deliberately omits
317
+ * these so server loaders are never bundled into the browser.
318
+ */
319
+ export function generateServerRoutesModule(pagesDir, rootDir) {
320
+ const { pages, layouts, apiRoutes } = scanPages(pagesDir);
321
+
322
+ const imports = [];
323
+ const routeEntries = [];
324
+
325
+ const layoutMap = new Map();
326
+ layouts.forEach((layout, i) => {
327
+ const varName = `_layout${i}`;
328
+ imports.push(`import ${varName} from '${toImportPath(layout.filePath, rootDir)}';`);
329
+ layoutMap.set(layout.urlPrefix, varName);
330
+ });
331
+
332
+ pages.forEach((page, i) => {
333
+ const def = `_page${i}`;
334
+ const ns = `_page${i}_ns`;
335
+ const relPath = toImportPath(page.filePath, rootDir);
336
+
337
+ let pageConfig = { mode: 'client' };
338
+ let detected = { hasLoader: false, hasGetStaticPaths: false, hasPageConfig: false };
339
+ try {
340
+ const source = fs.readFileSync(page.filePath, 'utf-8');
341
+ pageConfig = extractPageConfig(source);
342
+ detected = detectPageExports(source);
343
+ } catch {}
344
+
345
+ const needsNs = detected.hasLoader || detected.hasGetStaticPaths || detected.hasPageConfig;
346
+ if (needsNs) {
347
+ imports.push(`import ${def}, * as ${ns} from '${relPath}';`);
348
+ } else {
349
+ imports.push(`import ${def} from '${relPath}';`);
350
+ }
351
+
352
+ routeEntries.push({
353
+ path: page.routePath,
354
+ component: def,
355
+ ns,
356
+ mode: pageConfig.mode || 'client',
357
+ layout: findLayout(page.routePath, layoutMap) || null,
358
+ ...detected,
359
+ });
360
+ });
361
+
362
+ const apiEntries = [];
363
+ apiRoutes.forEach((route, i) => {
364
+ const varName = `_api${i}`;
365
+ imports.push(`import * as ${varName} from '${toImportPath(route.filePath, rootDir)}';`);
366
+ apiEntries.push({ path: route.routePath, handlers: varName });
367
+ });
368
+
369
+ const routeLine = (r) =>
370
+ ` { path: '${r.path}', component: ${r.component}, mode: '${r.mode}'` +
371
+ `${r.layout ? `, layout: ${r.layout}` : ''}` +
372
+ `${r.hasLoader ? `, loader: ${r.ns}.loader` : ''}` +
373
+ `${r.hasGetStaticPaths ? `, getStaticPaths: ${r.ns}.getStaticPaths` : ''}` +
374
+ `${r.hasPageConfig ? `, page: ${r.ns}.page` : ''} },`;
375
+
376
+ const lines = [
377
+ '// Auto-generated by What Framework file router (server)',
378
+ '// Do not edit — changes will be overwritten',
379
+ '',
380
+ ...imports,
381
+ '',
382
+ 'export const routes = [',
383
+ ...routeEntries.map(routeLine),
384
+ '];',
385
+ '',
386
+ 'export const apiRoutes = [',
387
+ ...apiEntries.map(r => ` { path: '${r.path}', handlers: ${r.handlers} },`),
388
+ '];',
389
+ '',
390
+ 'export const pageModes = {',
391
+ ...routeEntries.map(r => ` '${r.path}': '${r.mode}',`),
392
+ '};',
393
+ ];
394
+
395
+ return lines.join('\n');
396
+ }
397
+
296
398
  /**
297
399
  * Convert absolute file path to a root-relative import path.
298
400
  */
@@ -172,8 +172,34 @@ export default function whatVitePlugin(options = {}) {
172
172
  jsx: 'preserve',
173
173
  },
174
174
  optimizeDeps: {
175
- // Pre-bundle the framework
176
- include: ['what-framework']
175
+ // Exclude framework packages from Vite's dependency pre-bundling.
176
+ //
177
+ // Bug class this prevents — "dual module instance":
178
+ // The compiler emits `import { ... } from 'what-framework/render'`
179
+ // (a subpath resolved to the source file). Meanwhile user code
180
+ // imports `'what-framework'` (the package entry). If Vite
181
+ // pre-bundles `'what-framework'` into an esbuild chunk under
182
+ // node_modules/.vite, those two import paths resolve to two
183
+ // *different* module instances. Module-scoped state — the
184
+ // `componentStack` used by createComponent, effect ownership,
185
+ // the signal subscriber registry — is duplicated, so a signal
186
+ // created in user code never notifies effects created via the
187
+ // compiler-emitted path, and `getCurrentComponent()` returns
188
+ // undefined inside components mounted through compiler output.
189
+ //
190
+ // Why `exclude` is the right knob:
191
+ // `include` would force pre-bundling of the package entry, which
192
+ // does not resolve the subpath import the compiler emits — so the
193
+ // split persists. Using `exclude` tells Vite to skip the optimizer
194
+ // for these packages and serve them via the normal module graph,
195
+ // where both the package entry and the `/render` subpath share
196
+ // a single ESM module record.
197
+ //
198
+ // Regression symptom if this is removed:
199
+ // Components mount but lifecycle hooks (onMount, onCleanup) and
200
+ // shared store state silently no-op; effects don't re-run on
201
+ // signal writes from user code; SSR/CSR hydration mismatches.
202
+ exclude: ['what-framework', 'what-core', 'what-compiler', 'what-router'],
177
203
  }
178
204
  };
179
205
  }