what-compiler 0.13.4 → 0.13.6

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.
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import path from 'path';
12
+ import { createRequire } from 'node:module';
12
13
  import { transformSync } from '@babel/core';
13
14
  import whatBabelPlugin from './babel-plugin.js';
14
15
  import { generateRoutesModule } from './file-router.js';
@@ -57,15 +58,16 @@ export function jsxPreserveConfig({ rolldownVersion, viteVersion } = {}) {
57
58
  : { esbuild: { jsx: 'preserve' } };
58
59
  }
59
60
 
60
- // Resolved once per process — the Vite version can't change mid-run.
61
- let viteVersionPromise = null;
62
- function detectViteVersion() {
63
- if (!viteVersionPromise) {
64
- viteVersionPromise = import('vite')
65
- .then((vite) => vite.version || '')
66
- .catch(() => ''); // vite not resolvable (tests) — esbuild fallback
61
+ async function detectViteVersion(root) {
62
+ // A workspace-linked compiler can resolve a different Vite than its consumer.
63
+ // Multiple apps may also build with different versions in the same process.
64
+ try {
65
+ const require = createRequire(path.resolve(root || process.cwd(), 'package.json'));
66
+ return require('vite/package.json').version || '';
67
+ } catch {
68
+ // Keep programmatic/global Vite use working when the app has no local copy.
69
+ return import('vite').then((vite) => vite.version || '').catch(() => '');
67
70
  }
68
- return viteVersionPromise;
69
71
  }
70
72
 
71
73
  // Pattern: exported function starting with uppercase = component
@@ -73,6 +75,84 @@ const COMPONENT_EXPORT_RE = /export\s+(?:default\s+)?function\s+([A-Z]\w*)/;
73
75
  // Pattern: files that are likely signal/store/utility files
74
76
  const UTILITY_FILE_RE = /(?:store|signal|state|context|util|helper|lib|config)\b/i;
75
77
 
78
+ // --- SSR guard --------------------------------------------------------------
79
+ //
80
+ // A module that what-compiler lowers cannot run on a server, and the failures it
81
+ // produces without this guard both name the wrong thing.
82
+ //
83
+ // The lowering emits a module-scope `const _tmpl$0 = _$template("<div>...")`,
84
+ // and `_$template` calls `document.createElement('template')` EAGERLY. So the
85
+ // module throws `ReferenceError: document is not defined` at IMPORT time, before
86
+ // any render function runs, with a stack that points into what-core rather than
87
+ // at the file the developer wrote. If a DOM shim happens to exist, the component
88
+ // instead returns a cloned Element and what-server's assertSafeTag reports
89
+ // ERR_COMPILED_JSX_IN_SSR.
90
+ //
91
+ // Both are runtime failures for something decidable at build time: if Vite is
92
+ // transforming this module for the SSR environment and the transform emitted
93
+ // DOM-building code, the resulting bundle cannot work. Fail here, name the file,
94
+ // and name the two configurations that DO server-render, rather than emitting a
95
+ // bundle whose only possible behaviour is to crash.
96
+ //
97
+ // This is a guard, not a feature. It does not make compiled JSX server-render;
98
+ // what-compiler has no hydratable/SSR codegen target. See
99
+ // docs/SSR-COMPILED-JSX-SCOPING.md for the seams and the staged plan.
100
+
101
+ // The two compiler-generated locals that make a module client-only.
102
+ //
103
+ // _$template — hoisted to module scope and calls
104
+ // document.createElement('template') EAGERLY, so the
105
+ // module throws at import time on a server.
106
+ // _$createComponent — runs the component and builds its DOM at call time.
107
+ //
108
+ // Deliberately NOT in this list: _$componentVNode, which is _$createComponent
109
+ // stopping one step short of createDOM (what-core render.js). A module whose
110
+ // only JSX is the argument of a hydrate() call emits that and nothing else, and
111
+ // it is import-safe, so flagging it would be wrong. The other helpers
112
+ // (_$insert, _$spread, _$setProp, ...) are all reached only THROUGH a template,
113
+ // so they add no cases and would only widen the blast radius.
114
+ //
115
+ // The `_$` prefix is compiler-generated and never written by hand, which is what
116
+ // makes matching on the name safe.
117
+ const DOM_BUILDING_LOCALS = /\b_\$(?:template|createComponent)\b/;
118
+
119
+ function buildsDom(outputCode) {
120
+ return DOM_BUILDING_LOCALS.test(outputCode);
121
+ }
122
+
123
+ function ssrGuardError(id) {
124
+ // ERROR_CODES.COMPILED_JSX_IN_SSR
125
+ return Object.assign(
126
+ new Error(
127
+ `[what-compiler] ${id} is being compiled for the server, but what-compiler's ` +
128
+ 'JSX output is client-only. It lowers JSX to module-scope _$template() calls ' +
129
+ 'that run document.createElement() at import time, so this module throws ' +
130
+ '"document is not defined" the moment a server imports it.\n\n' +
131
+ 'Server-rendered views have two supported spellings:\n' +
132
+ ' 1. Author them with h() from what-framework.\n' +
133
+ ' 2. Compile them with the automatic JSX runtime instead of what-compiler ' +
134
+ '(jsxImportSource: "what-framework"), which emits h() calls that ' +
135
+ 'renderToString and renderToHydratableString understand.\n\n' +
136
+ 'If a DOM already exists in this process on purpose, set ssrGuard: false ' +
137
+ 'on the plugin. The likeliest reason is a test runner: Vitest applies an ' +
138
+ "SSR transform under `environment: 'node'`, so a component test that " +
139
+ 'shims a DOM itself lands here. Note that with the guard off the result ' +
140
+ 'is a full client render, not SSR.'
141
+ ),
142
+ { code: 'ERR_COMPILED_JSX_IN_SSR', id, plugin: 'vite-plugin-what' },
143
+ );
144
+ }
145
+
146
+ // Vite reports "this transform is for the server" in two ways depending on its
147
+ // major: the third `transform` argument (`{ ssr: true }`) on every version, and
148
+ // the Environment API (`this.environment.name === 'ssr'`) from Vite 6. Read both
149
+ // — a plugin that checked only one would silently stop guarding on the other.
150
+ function isSsrTransform(transformOptions, ctx) {
151
+ if (transformOptions && transformOptions.ssr) return true;
152
+ const env = ctx && ctx.environment;
153
+ return !!(env && env.name === 'ssr');
154
+ }
155
+
76
156
  export default function whatVitePlugin(options = {}) {
77
157
  const {
78
158
  // File extensions to process
@@ -92,6 +172,10 @@ export default function whatVitePlugin(options = {}) {
92
172
  // against package sources instead — needed e.g. in a monorepo where
93
173
  // workspace-linked dist/ output may be stale or absent. See config() below.
94
174
  prodBundles = true,
175
+ // Refuse to lower JSX for a module in the SSR graph. See ssrGuardError().
176
+ // Set to false only if you have installed a DOM in the server process and
177
+ // accept that the render is a full client render, not SSR.
178
+ ssrGuard = true,
95
179
  } = options;
96
180
 
97
181
  let rootDir = '';
@@ -182,7 +266,7 @@ export default function whatVitePlugin(options = {}) {
182
266
  },
183
267
 
184
268
  // Transform JSX files
185
- transform(code, id) {
269
+ transform(code, id, transformOptions) {
186
270
  const cleanId = id.replace(/[?#].*$/, '');
187
271
  const hasJsx = patternMatches(include, cleanId);
188
272
  const isScriptModule = SCRIPT_MODULE_RE.test(cleanId);
@@ -198,6 +282,8 @@ export default function whatVitePlugin(options = {}) {
198
282
  // they contain no JSX.
199
283
  if (!hasJsx && !hasServerActions) return null;
200
284
 
285
+ const guardSsr = ssrGuard && isSsrTransform(transformOptions, this);
286
+
201
287
  try {
202
288
  const result = transformSync(code, {
203
289
  filename: id,
@@ -227,6 +313,16 @@ export default function whatVitePlugin(options = {}) {
227
313
 
228
314
  let outputCode = result.code;
229
315
 
316
+ // Decided from the OUTPUT, not the filename. `.jsx` in the include
317
+ // pattern says the file MAY contain JSX, not that any was lowered, and a
318
+ // .js/.ts module compiled here purely for its server-action metadata
319
+ // emits nothing DOM-building at all. Both would be false positives.
320
+ // buildsDom() asks the only question that matters: did this transform
321
+ // emit code that constructs DOM?
322
+ if (guardSsr && buildsDom(outputCode)) {
323
+ throw ssrGuardError(cleanId);
324
+ }
325
+
230
326
  // HMR: append hot boundary code for component files in dev mode
231
327
  if (hot && isDevMode && !production) {
232
328
  const isComponentFile = isComponentModule(code, id);
@@ -241,6 +337,11 @@ export default function whatVitePlugin(options = {}) {
241
337
  map: result.map
242
338
  };
243
339
  } catch (error) {
340
+ // The SSR guard is a verdict on a transform that SUCCEEDED, not a Babel
341
+ // failure. Passing it through the enrichment below would log "[what]
342
+ // Error transforming <file>" over it, which says the compile broke when
343
+ // it did not, and buries the part the developer has to read.
344
+ if (error && error.code === 'ERR_COMPILED_JSX_IN_SSR') throw error;
244
345
  // Enrich Babel errors with file context for the error overlay
245
346
  error.plugin = 'vite-plugin-what';
246
347
  if (!error.id) error.id = id;
@@ -297,7 +398,7 @@ export default function whatVitePlugin(options = {}) {
297
398
  // jsxPreserveConfig picks the right option key for the running version.
298
399
  const jsxPreserve = jsxPreserveConfig({
299
400
  rolldownVersion: this?.meta?.rolldownVersion,
300
- viteVersion: await detectViteVersion(),
401
+ viteVersion: await detectViteVersion(config.root),
301
402
  });
302
403
  return {
303
404
  ...(useProdCondition ? { resolve: { conditions: ['production'] } } : {}),