@@ -3,7 +3,7 @@ import react from '@vitejs/plugin-react';
33import { sveltekit } from '@sveltejs/kit/vite' ;
44import { defineConfig } from 'vite' ;
55import { findWorkspaceRoot , workspaceResolver } from './src/vite-plugin-workspace-resolver' ;
6- import { createReadStream , existsSync } from 'node:fs' ;
6+ import { createReadStream , existsSync , readdirSync } from 'node:fs' ;
77import { stat } from 'node:fs/promises' ;
88import { join } from 'node:path' ;
99
@@ -12,6 +12,78 @@ const bundlerInstanceDir =
1212 process . env . DEMO_BUNDLER_INSTANCE_DIR || join ( process . cwd ( ) , '.cache' , 'demo-bundler' ) ;
1313const 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 / \/ l i b - r e a c t \/ .* \. ( j s x | t s x ) (?: \? .* ) ? $ / ,
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