what-compiler 0.8.4 → 0.11.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
  */
@@ -36,6 +36,11 @@ export default function whatVitePlugin(options = {}) {
36
36
  pages = 'src/pages',
37
37
  // HMR: enabled by default in dev, disabled in production
38
38
  hot = !production,
39
+ // Resolve the `production` exports condition (dist/*.min.js — pre-minified,
40
+ // dev warnings compiled out) during `vite build`. Set to false to build
41
+ // against package sources instead — needed e.g. in a monorepo where
42
+ // workspace-linked dist/ output may be stale or absent. See config() below.
43
+ prodBundles = true,
39
44
  } = options;
40
45
 
41
46
  let rootDir = '';
@@ -105,6 +110,13 @@ export default function whatVitePlugin(options = {}) {
105
110
  const result = transformSync(code, {
106
111
  filename: id,
107
112
  sourceMaps,
113
+ // Hermetic transform (SPRINT v0.11 C7): never load the project's
114
+ // babel.config.js/.babelrc. A user's React preset or unrelated
115
+ // plugins corrupting What's JSX output is a debugging nightmare —
116
+ // and scanning the disk for config files on every transform is
117
+ // wasted I/O in dev.
118
+ configFile: false,
119
+ babelrc: false,
108
120
  plugins: [
109
121
  [whatBabelPlugin, { production }]
110
122
  ],
@@ -165,15 +177,60 @@ export default function whatVitePlugin(options = {}) {
165
177
  },
166
178
 
167
179
  // Configure for development
168
- config(config, { mode }) {
180
+ config(config, { mode, command }) {
181
+ // SPRINT v0.11 C7: make the `production` exports condition reachable.
182
+ // what-framework/what-core ship pre-minified production bundles behind
183
+ // the `production` condition in their exports maps, but Vite's default
184
+ // resolve conditions never include `production` — so production builds
185
+ // silently shipped the dev source (larger, with dev-only warnings).
186
+ //
187
+ // Guard rationale (documented choice):
188
+ // - Only during `vite build` in production mode — dev always uses src
189
+ // so the dev server, HMR, and devtools see un-minified modules.
190
+ // - Opt-out via `what({ prodBundles: false })` — in a monorepo with
191
+ // workspace-linked packages, dist/ can be stale (or missing before
192
+ // the first `npm run build`), and resolving `production` there would
193
+ // bundle outdated framework code. Apps installing from npm always
194
+ // have dist/ in sync with the published package, so the default is on.
195
+ // - `resolve.conditions` is ADDITIVE in Vite (extra conditions on top
196
+ // of the defaults), so import/browser/default resolution for other
197
+ // packages is unaffected.
198
+ const useProdCondition = command === 'build' && mode === 'production' && prodBundles;
169
199
  return {
200
+ ...(useProdCondition ? { resolve: { conditions: ['production'] } } : {}),
170
201
  esbuild: {
171
202
  // Preserve JSX so our babel plugin handles it -- don't let esbuild transform it
172
203
  jsx: 'preserve',
173
204
  },
174
205
  optimizeDeps: {
175
- // Pre-bundle the framework
176
- include: ['what-framework']
206
+ // Exclude framework packages from Vite's dependency pre-bundling.
207
+ //
208
+ // Bug class this prevents — "dual module instance":
209
+ // The compiler emits `import { ... } from 'what-framework/render'`
210
+ // (a subpath resolved to the source file). Meanwhile user code
211
+ // imports `'what-framework'` (the package entry). If Vite
212
+ // pre-bundles `'what-framework'` into an esbuild chunk under
213
+ // node_modules/.vite, those two import paths resolve to two
214
+ // *different* module instances. Module-scoped state — the
215
+ // `componentStack` used by createComponent, effect ownership,
216
+ // the signal subscriber registry — is duplicated, so a signal
217
+ // created in user code never notifies effects created via the
218
+ // compiler-emitted path, and `getCurrentComponent()` returns
219
+ // undefined inside components mounted through compiler output.
220
+ //
221
+ // Why `exclude` is the right knob:
222
+ // `include` would force pre-bundling of the package entry, which
223
+ // does not resolve the subpath import the compiler emits — so the
224
+ // split persists. Using `exclude` tells Vite to skip the optimizer
225
+ // for these packages and serve them via the normal module graph,
226
+ // where both the package entry and the `/render` subpath share
227
+ // a single ESM module record.
228
+ //
229
+ // Regression symptom if this is removed:
230
+ // Components mount but lifecycle hooks (onMount, onCleanup) and
231
+ // shared store state silently no-op; effects don't re-run on
232
+ // signal writes from user code; SSR/CSR hydration mismatches.
233
+ exclude: ['what-framework', 'what-core', 'what-compiler', 'what-router'],
177
234
  }
178
235
  };
179
236
  }