@elytracms/cli 0.0.82 → 0.0.83

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,7 +1,7 @@
1
1
  {
2
2
  "name": "@elytracms/cli",
3
3
  "author": "finaldream <hi@finaldream.de>",
4
- "version": "0.0.82",
4
+ "version": "0.0.83",
5
5
  "description": "The Elytra CLI — run a consuming repo's studio, frontend, and Convex backend under one command.",
6
6
  "license": "MIT",
7
7
  "type": "module",
@@ -14,10 +14,10 @@
14
14
  "README.md"
15
15
  ],
16
16
  "engines": {
17
- "node": ">=20"
17
+ "node": ">=22.18.0"
18
18
  },
19
19
  "dependencies": {
20
- "@elytracms/core": "^0.0.82"
20
+ "@elytracms/core": "^0.0.83"
21
21
  },
22
22
  "devDependencies": {
23
23
  "vitest": "^4.1.10"
@@ -75,6 +75,66 @@ export function virtualModulesPlugin(name, modules) {
75
75
  * because the studio `vite.config.ts` is never loaded), and automatic JSX so
76
76
  * `.tsx` component barrels transform without the app's React plugin.
77
77
  */
78
+ /**
79
+ * React specifiers the automatic-JSX transform injects, aliased to the STUDIO's
80
+ * own copies (EC-537).
81
+ *
82
+ * `jsx: 'automatic'` below rewrites every transformed `.tsx` to import
83
+ * `react/jsx-dev-runtime`, and Vite resolves a bare specifier from the
84
+ * IMPORTER's directory. A consumer's page-builder components live at their repo
85
+ * root, outside `studioDir` — so on a repo whose root `package.json` does not
86
+ * declare react, every one of them fails to resolve and `config-valid` reports
87
+ * a config that "could not be executed", taking `asset-collection` and
88
+ * `presentation-targets` down with it.
89
+ *
90
+ * Measured 2026-08-02 on a real `create-elytra@0.0.82` scaffold, both
91
+ * directions: with react at the root `✓ config-valid` (11 ok); with react
92
+ * removed, `✗ Cannot find module 'react/jsx-dev-runtime' imported from
93
+ * components/marketing/hero.tsx` and three checks lost. Both production
94
+ * consumers hit exactly this on upgrading and both added react to their root.
95
+ *
96
+ * That was a workaround for a requirement the HARNESS invented: automatic JSX is
97
+ * our transform setting, chosen for our convenience, in a boot whose only job is
98
+ * to *evaluate* a config — nothing here mounts or renders. So the package that
99
+ * mandated React supplies it. The studio always has react (it is a React app),
100
+ * and resolving from `studioDir` is what this harness already does for `vite`
101
+ * itself, one line above.
102
+ *
103
+ * Opportunistic by contract, like everything else in this file: a specifier that
104
+ * cannot be resolved is simply left unaliased rather than throwing, so a partial
105
+ * install degrades to the previous behaviour instead of crashing the CLI.
106
+ */
107
+ export function reactRuntimeFromStudioPlugin(studioDir) {
108
+ // Resolve THROUGH VITE, from a studio-side importer — never to a raw path.
109
+ //
110
+ // The obvious implementation is `resolve.alias` to
111
+ // `createRequire(studioDir).resolve('react/jsx-dev-runtime')`. That was tried
112
+ // first and is wrong twice over (both measured 2026-08-02): `createRequire`
113
+ // returns the **CJS** entry, and an absolute-path alias bypasses Vite's
114
+ // exports-map/condition/interop handling entirely, so its SSR runner
115
+ // evaluates CommonJS as ESM — `elytra previews` died with
116
+ // `ReferenceError: module is not defined` at `react/index.js:9`, then at
117
+ // `react/jsx-dev-runtime.js:9` when the alias was narrowed.
118
+ //
119
+ // Handing Vite a studio-side *importer* instead lets its own resolver do the
120
+ // work — right conditions, right ESM build — while only changing WHERE the
121
+ // lookup starts. That is the whole intent: not a different React, just one
122
+ // that can be found.
123
+ const importer = join(studioDir, '__elytra-harness__.js')
124
+ return {
125
+ name: 'elytra:react-runtime-from-studio',
126
+ async resolveId(id, from) {
127
+ if (id !== 'react/jsx-runtime' && id !== 'react/jsx-dev-runtime') return null
128
+ // Only rescue importers OUTSIDE the studio (the consumer's root-level
129
+ // components). Anything already inside studio/ resolves on its own, and
130
+ // intercepting it would be a silent no-op at best.
131
+ if (from === undefined || from.startsWith(studioDir)) return null
132
+ const resolved = await this.resolve(id, importer, { skipSelf: true })
133
+ return resolved ?? null
134
+ },
135
+ }
136
+ }
137
+
78
138
  export async function startConsumerVite({ root, studioDir, port, plugins }) {
79
139
  const vite = await import(pathToFileURL(resolveFromDir(studioDir, 'vite')).href)
80
140
  const server = await vite.createServer({
@@ -83,7 +143,11 @@ export async function startConsumerVite({ root, studioDir, port, plugins }) {
83
143
  mode: 'development',
84
144
  appType: 'custom',
85
145
  logLevel: 'warn',
86
- plugins,
146
+ // EC-537 — the rescue runs BEFORE the caller's plugins so nothing downstream
147
+ // has to know about it. `dedupe` rides along: once the consumer's components
148
+ // and the studio can both resolve React, they must agree on ONE copy.
149
+ plugins: [reactRuntimeFromStudioPlugin(studioDir), ...(plugins ?? [])],
150
+ resolve: { dedupe: ['react', 'react-dom'] },
87
151
  server: {
88
152
  port,
89
153
  strictPort: true,