Skip to content

Commit 8b37207

Browse files
authored
Merge pull request #29 from pie-framework/fix/PIE-624
fix(element-demo): unblock ESM player for React/tiptap elements
2 parents f631303 + 82d5560 commit 8b37207

50 files changed

Lines changed: 837 additions & 609 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@pie-element/multiple-choice": patch
3+
---
4+
5+
Include changes and fix dependency issues

apps/element-demo/src/app.html

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,23 @@
44
<meta charset="utf-8" />
55
<link rel="icon" href="%sveltekit.assets%/pie-logo-orange.svg" type="image/svg+xml" />
66
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
<!--
8+
React Refresh preamble. Must be injected here (and not via a Vite
9+
`transformIndexHtml` plugin) because SvelteKit serves its own HTML
10+
template and never goes through Vite's index.html transform pipeline.
11+
Without this, any React TSX module loaded from a workspace element
12+
package throws "@vitejs/plugin-react can't detect preamble" during
13+
dev, which is what breaks the ESM view of every React element.
14+
The script is a no-op in production: `/@react-refresh` only exists
15+
under the Vite dev server.
16+
-->
17+
<script type="module">
18+
import RefreshRuntime from '/@react-refresh';
19+
RefreshRuntime.injectIntoGlobalHook(window);
20+
window.$RefreshReg$ = () => {};
21+
window.$RefreshSig$ = () => (type) => type;
22+
window.__vite_plugin_react_preamble_installed__ = true;
23+
</script>
724
%sveltekit.head%
825
</head>
926
<body data-sveltekit-preload-data="off">

apps/element-demo/vite.config.ts

Lines changed: 88 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import react from '@vitejs/plugin-react';
33
import { sveltekit } from '@sveltejs/kit/vite';
44
import { defineConfig } from 'vite';
55
import { findWorkspaceRoot, workspaceResolver } from './src/vite-plugin-workspace-resolver';
6-
import { createReadStream, existsSync } from 'node:fs';
6+
import { createReadStream, existsSync, readdirSync } from 'node:fs';
77
import { stat } from 'node:fs/promises';
88
import { join } from 'node:path';
99

@@ -12,6 +12,78 @@ const bundlerInstanceDir =
1212
process.env.DEMO_BUNDLER_INSTANCE_DIR || join(process.cwd(), '.cache', 'demo-bundler');
1313
const bundlerOutputDir = join(bundlerInstanceDir, 'bundles');
1414

15+
/**
16+
* Pin a package to a single physical directory by scanning Bun's content
17+
* store (`node_modules/.bun/<pkg>@<version>/node_modules/<pkg>`).
18+
*
19+
* Why this is needed:
20+
* Bun's nested layout installs multiple copies of some prosemirror plugins
21+
* (e.g. prosemirror-gapcursor 1.4.0 AND 1.4.1, pulled in by different tiptap
22+
* versions) and does NOT hoist them to the root `node_modules`. So plain
23+
* Node resolution from the workspace root fails, and `resolve.dedupe` cannot
24+
* collapse them because they live in unrelated subtrees. The dep optimizer
25+
* then inlines two different gapcursor copies into separate optimized
26+
* chunks; both run `Selection.jsonID('gapcursor', GapCursor)` against the
27+
* shared `prosemirror-state` Selection class, throwing "Duplicate use of
28+
* selection JSON ID gapcursor". That crash breaks the ESM author/configure
29+
* view of every tiptap-based element.
30+
*
31+
* Aliasing each bare specifier to ONE absolute directory makes the optimizer
32+
* treat every reference as the same module, so it executes once. We pick the
33+
* highest installed version. Returns undefined (alias skipped) if nothing is
34+
* found, so config evaluation never crashes.
35+
*/
36+
function compareSemver(a: string, b: string): number {
37+
const pa = a.split('.').map((n) => Number.parseInt(n, 10) || 0);
38+
const pb = b.split('.').map((n) => Number.parseInt(n, 10) || 0);
39+
for (let i = 0; i < 3; i++) {
40+
if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0);
41+
}
42+
return 0;
43+
}
44+
45+
const bunStoreDir = join(workspaceRoot, 'node_modules', '.bun');
46+
function resolveSingletonDir(pkg: string): string | undefined {
47+
let storeEntries: string[];
48+
try {
49+
storeEntries = readdirSync(bunStoreDir);
50+
} catch {
51+
return undefined;
52+
}
53+
// Store entries look like "prosemirror-gapcursor@1.4.1". Match exactly this
54+
// package name (not e.g. "prosemirror-gapcursor-x@...") by requiring "@<digit>".
55+
const prefix = `${pkg}@`;
56+
const versions = storeEntries
57+
.filter((e) => e.startsWith(prefix) && /\d/.test(e.charAt(prefix.length)))
58+
.map((e) => ({ entry: e, version: e.slice(prefix.length) }))
59+
.sort((x, y) => compareSemver(y.version, x.version));
60+
61+
for (const { entry } of versions) {
62+
const dir = join(bunStoreDir, entry, 'node_modules', pkg);
63+
if (existsSync(join(dir, 'package.json'))) return dir;
64+
}
65+
return undefined;
66+
}
67+
68+
const proseMirrorSingletonAliases = Object.fromEntries(
69+
// Packages that register a global identity (selection JSON ID, plugin key
70+
// class, etc.) and therefore MUST be a single instance across the bundle.
71+
[
72+
'prosemirror-state',
73+
'prosemirror-gapcursor',
74+
'prosemirror-view',
75+
'prosemirror-transform',
76+
'prosemirror-model',
77+
'prosemirror-keymap',
78+
'prosemirror-commands',
79+
'prosemirror-history',
80+
'prosemirror-inputrules',
81+
'prosemirror-schema-list',
82+
]
83+
.map((pkg) => [pkg, resolveSingletonDir(pkg)] as const)
84+
.filter(([, dir]) => dir !== undefined) as [string, string][]
85+
);
86+
1587
/**
1688
* Vite config for element demo app.
1789
*
@@ -51,26 +123,9 @@ export default defineConfig({
51123
/\/lib-react\/.*\.(jsx|tsx)(?:\?.*)?$/,
52124
],
53125
}),
54-
{
55-
name: 'react-refresh-preamble-dev-only',
56-
apply: 'serve',
57-
transformIndexHtml() {
58-
return [
59-
{
60-
tag: 'script',
61-
attrs: { type: 'module' },
62-
children: [
63-
"import RefreshRuntime from '/@react-refresh';",
64-
'RefreshRuntime.injectIntoGlobalHook(window);',
65-
'window.$RefreshReg$ = () => {};',
66-
'window.$RefreshSig$ = () => (type) => type;',
67-
'window.__vite_plugin_react_preamble_installed__ = true;',
68-
].join('\n'),
69-
injectTo: 'head',
70-
},
71-
];
72-
},
73-
},
126+
// NOTE: The React Refresh preamble is injected directly in `src/app.html`,
127+
// not by a Vite plugin. SvelteKit serves its own HTML and skips Vite's
128+
// `transformIndexHtml` pipeline, so plugin-based injection never fires.
74129
{
75130
name: 'serve-demo-iife-bundles',
76131
apply: 'serve',
@@ -137,10 +192,21 @@ export default defineConfig({
137192
// The package has module: "src/index.ts" which doesn't exist in installed package
138193
// Force Vite to use the main field (lib/index.js) instead
139194
'@pie-framework/math-validation': '@pie-framework/math-validation/lib/index.js',
195+
// See proseMirrorSingletonAliases comment above. Aliasing each pkg's
196+
// bare specifier to its single resolved directory forces every chunk
197+
// (including dep-optimizer pre-bundles) to share one physical copy.
198+
...proseMirrorSingletonAliases,
140199
},
141200
// Keep a single ProseMirror instance across workspace packages.
142201
// Without this, mixed tiptap/prosemirror imports can register duplicate
143202
// selection IDs (e.g. gapcursor) and crash module evaluation.
203+
//
204+
// `react-transition-group` is deduped because likert pins ^2.3.1 (a
205+
// stale, unused dep) while every other element/lib uses v4's named
206+
// exports. Bun's nested layout otherwise lets Vite's dep optimizer
207+
// pick the v2 CJS index, which only re-exports `default`, breaking
208+
// `import { CSSTransition } from 'react-transition-group'` at runtime
209+
// (the symptom is the ESM view of every React element failing to load).
144210
dedupe: [
145211
'@tiptap/pm',
146212
'prosemirror-state',
@@ -153,6 +219,7 @@ export default defineConfig({
153219
'prosemirror-inputrules',
154220
'prosemirror-gapcursor',
155221
'prosemirror-schema-list',
222+
'react-transition-group',
156223
],
157224
},
158225

0 commit comments

Comments
 (0)