@@ -10,16 +10,12 @@ import { existsSync } from 'node:fs';
1010 *
1111 * WHY this is needed
1212 * ──────────────────
13- * Rollup's code-splitting can place shared IgniteUI library symbols inside the
14- * *first* sample chunk that imports them (e.g. annotations-all). Any other
15- * sample that uses those same symbols imports from that chunk at runtime.
16- * Without this plugin the module-level `new Sample()` inside the host chunk
17- * would fire immediately — but the DOM for that sample isn't present on the
18- * page — causing "Cannot set properties of null" errors.
19- *
20- * The `[...slug].astro` loader instead calls `new module.Sample()` explicitly
21- * after the dynamic import resolves, so instantiation is always deferred until
22- * the correct page is loaded.
13+ * The `[...slug].astro` loader calls `new module.Sample()` explicitly after
14+ * the dynamic import resolves, so the page controls when a sample
15+ * instantiates. Keeping the module-level `new Sample()` as well would run
16+ * every sample twice. Stripping it here (instead of editing 900+ samples)
17+ * keeps each sample runnable standalone while the browser instantiates it
18+ * exactly once.
2319 */
2420/** @returns {import('vite').Plugin } */
2521function stripSampleInstantiation ( ) {
@@ -40,26 +36,44 @@ function stripSampleInstantiation() {
4036}
4137
4238/**
43- * Vite plugin: convert local CSS/SCSS imports in sample index.ts files to
44- * `?inline` imports with dynamic `<style>` injection.
39+ * Vite plugin: take the stylesheet imports out of every sample entry module.
4540 *
4641 * WHY this is needed
4742 * ──────────────────
48- * Rollup picks certain sample chunks as "hosts" for shared IgniteUI code.
49- * Any CSS imported by those host chunks ends up in a SHA-named CSS file that
50- * Vite's `__vite__mapDeps` then preloads on EVERY page that transits through
51- * that shared chunk — causing unrelated sample styles to appear on the wrong
52- * pages and override each other.
43+ * A sample's entry module is loaded lazily, by slug, from a dynamic import that
44+ * only runs after DOMContentLoaded — and it drags in megabytes of library code.
45+ * While that is in flight the page has already painted, so any CSS the module
46+ * owns arrives far too late: the sample flashes unstyled first.
47+ *
48+ * It was also a correctness problem. Rollup hoists shared code into some
49+ * chunk, and evaluating a chunk runs the whole module body — so a *different*
50+ * sample's theme import could land in the document first and win the library's
51+ * one-shot `getTheme()` check, rendering material samples with the bootstrap
52+ * theme.
5353 *
54- * By converting import './index.css' → import __css from './index.css?inline'
55- * we keep the CSS as a plain string inside the owning JS module. Vite no
56- * longer emits a separate CSS chunk for it, so nothing leaks into other pages.
57- * The `<style>` is only injected when that specific sample's JS actually runs.
54+ * So `[...slug].astro` now emits these stylesheets into <head> at build time
55+ * (see resolveSampleStyles): themes as <link>s to public/ig-themes/, everything
56+ * sample-local inlined. This plugin drops the imports it has taken over, so the
57+ * two can't both own the same sheet — otherwise the module's copy would
58+ * re-append itself last on every load and clobber a theme swap.
59+ *
60+ * Anything whose shape the page does NOT resolve is deliberately left alone and
61+ * still injected at runtime, so an unrecognised import degrades to the old
62+ * behaviour instead of silently losing its styles.
5863 */
5964/** @returns {import('vite').Plugin } */
6065function inlineSampleCss ( ) {
6166 // Matches any CSS / SCSS side-effect import inside a sample file (relative or package).
62- const localCssRe = / ^ i m p o r t \s + [ ' " ] ( [ ^ ' " ] + \. (?: c s s | s c s s ) ) [ ' " ] ; ? \s * $ / gm;
67+ const cssImportRe = / ^ i m p o r t \s + [ ' " ] ( [ ^ ' " ] + \. (?: c s s | s c s s ) ) [ ' " ] ; ? \s * $ / gm;
68+
69+ // The two shapes [...slug].astro knows how to put in <head>. Keep in sync
70+ // with resolveSampleStyles() in src/utils/samples.ts.
71+ const themeSpecRe =
72+ / ^ i g n i t e u i - w e b c o m p o n e n t s ( - g r i d s \/ g r i d s ) ? \/ t h e m e s \/ ( l i g h t | d a r k ) \/ ( m a t e r i a l | b o o t s t r a p | f l u e n t | i n d i g o ) \. c s s $ / ;
73+ const sampleLocalRe = / ^ \. \/ [ ^ / ] + \. (?: c s s | s c s s ) $ / ;
74+
75+ const handledInHead = spec => themeSpecRe . test ( spec ) || sampleLocalRe . test ( spec ) ;
76+
6377 let isBuild = false ;
6478
6579 return {
@@ -69,22 +83,25 @@ function inlineSampleCss() {
6983 isBuild = config . command === 'build' ;
7084 } ,
7185 transform ( code , id ) {
72- // Production build only — in dev, Vite handles CSS imports natively
73- // (injects as <style> tags automatically) which works correctly per-module.
74- if ( ! isBuild ) return ;
7586 if ( ! id . replace ( / \\ / g, '/' ) . match ( / \/ s a m p l e s \/ .+ \/ s r c \/ i n d e x \. t s $ / ) ) return ;
76- localCssRe . lastIndex = 0 ;
77- if ( ! localCssRe . test ( code ) ) return ;
78- localCssRe . lastIndex = 0 ;
87+ cssImportRe . lastIndex = 0 ;
88+ if ( ! cssImportRe . test ( code ) ) return ;
89+ cssImportRe . lastIndex = 0 ;
7990
8091 let i = 0 ;
81- const newCode = code . replace ( localCssRe , ( _ , cssPath ) => {
92+ const newCode = code . replace ( cssImportRe , ( line , spec ) => {
93+ // Already in <head> — drop it so nothing is styled twice.
94+ if ( handledInHead ( spec ) ) return '' ;
95+
96+ // In dev Vite injects CSS imports natively, which is correct per-module.
97+ if ( ! isBuild ) return line ;
98+
99+ // Production fallback for shapes the page could not resolve. ?inline
100+ // keeps the CSS as a string inside this module, so Vite emits no shared
101+ // CSS chunk that could leak onto unrelated pages.
82102 const v = `__sampleCss${ i ++ } ` ;
83- // ?inline → Vite compiles SCSS→CSS and returns the result as a string.
84- // We inject it via a <style> element so it only appears in the DOM
85- // when this exact sample's JS module is imported — never on other pages.
86103 return [
87- `import ${ v } from '${ cssPath } ?inline';` ,
104+ `import ${ v } from '${ spec } ?inline';` ,
88105 `{const __s=document.createElement('style');__s.textContent=${ v } ;document.head.appendChild(__s);}` ,
89106 ] . join ( '\n' ) ;
90107 } ) ;
@@ -186,6 +203,14 @@ export default defineConfig({
186203 // Match IIS behaviour: routes are served without trailing slashes
187204 trailingSlash : 'never' ,
188205
206+ // Keep every stylesheet as an emitted file. With the default 'auto',
207+ // Astro inlines small CSS assets into page HTML and deletes the files,
208+ // but the sample chunks' __vite__mapDeps still preload them at runtime
209+ // → "Unable to preload CSS" on every sample page.
210+ build : {
211+ inlineStylesheets : 'never' ,
212+ } ,
213+
189214 vite : {
190215 plugins : [ resolveIgniteUiScoped ( ) , stripSampleInstantiation ( ) , inlineSampleCss ( ) ] ,
191216 // samples/ and node_modules/ are already at the repo root (__dirname),
@@ -234,10 +259,42 @@ export default defineConfig({
234259 // Give every sample its own chunk so Rollup doesn't try to inline
235260 // all 700+ samples into a single bundle (causes OOM).
236261 manualChunks ( id ) {
262+ // Vite's dynamic-import preload helper is shared by every lazy
263+ // chunk. Left unassigned, Rollup hosts it inside one vendor
264+ // chunk and the others import it back — a chunk cycle
265+ // (grid-lite → webcomponents → grid-lite build warning).
266+ if ( id . includes ( 'vite/preload-helper' ) ) {
267+ return 'preload-helper' ;
268+ }
269+
237270 const match = id . match ( / [ \\ / ] s a m p l e s [ \\ / ] ( .+ ) [ \\ / ] s r c [ \\ / ] i n d e x \. t s $ / ) ;
238271 if ( match ) {
239272 return `samples/${ match [ 1 ] . replace ( / [ \\ / ] / g, '--' ) } ` ;
240273 }
274+
275+ // Shared IgniteUI runtime → one vendor chunk per package.
276+ // Without this, Rollup hosts shared library code inside the first
277+ // sample chunk that imports it, so every other sample transits
278+ // through that chunk (e.g. an 11MB annotations-all hosting the
279+ // charts runtime) and pulls in its side effects.
280+ const vendor = id
281+ . replace ( / \\ / g, '/' )
282+ . match ( / \/ n o d e _ m o d u l e s \/ (?: @ i n f r a g i s t i c s \/ ) ? ( i g n i t e u i - [ ^ / ] + ) \/ / ) ;
283+ if ( vendor ) {
284+ return `vendor/${ vendor [ 1 ] } ` ;
285+ }
286+
287+ // Every other node_modules package too. A shared non-IgniteUI
288+ // dep (lit, file-saver, …) left unassigned gets hosted inside the
289+ // first *sample* chunk that imports it, so unrelated pages
290+ // evaluate that sample's module body — its defineAllComponents()
291+ // and theme CSS included (the bootstrap-instead-of-material bug).
292+ const dep = id
293+ . replace ( / \\ / g, '/' )
294+ . match ( / \/ n o d e _ m o d u l e s \/ ( (?: @ [ ^ / ] + \/ ) ? [ ^ / ] + ) \/ / ) ;
295+ if ( dep ) {
296+ return `vendor/${ dep [ 1 ] . replace ( '/' , '--' ) } ` ;
297+ }
241298 } ,
242299 // Keep sample CSS files scoped to their own chunk names
243300 assetFileNames ( assetInfo ) {
0 commit comments