Skip to content

Commit fa4ed0d

Browse files
authored
Merge pull request #1255 from IgniteUI/vnext
Merge pull request #1253 from IgniteUI/rkaraivanov/app-architecture-i…
2 parents 7d7d085 + a50cf23 commit fa4ed0d

17 files changed

Lines changed: 969 additions & 460 deletions

File tree

.github/workflows/node.js.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,11 @@ jobs:
3636

3737
- name: npm install --legacy-peer-deps
3838
run: npm ci --legacy-peer-deps
39+
- name: Type check
40+
run: npm run check
3941
- name: Build
4042
run: npm run build
43+
- name: Install Playwright browser
44+
run: npx playwright install --with-deps chromium
45+
- name: Smoke tests
46+
run: npm run test:smoke

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@
1010
/samples/**/package-lock.json
1111
/samples/**/yarn.lock
1212
/tmp/
13-
/tests/
1413

1514
# Astro / Vite build output:
1615
/.astro
1716
/dist
1817
/public/assets/code-viewer
18+
/public/ig-themes
1919

2020
# browser auto-generated files:
2121
/browser/node_modules
@@ -57,3 +57,5 @@ npm-debug.log*
5757
yarn-debug.log*
5858
yarn-error.log*
5959
*.db
60+
61+
test-results

astro.config.mjs

Lines changed: 90 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -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} */
2521
function 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} */
6065
function inlineSampleCss() {
6166
// Matches any CSS / SCSS side-effect import inside a sample file (relative or package).
62-
const localCssRe = /^import\s+['"]([^'"]+\.(?:css|scss))['"];?\s*$/gm;
67+
const cssImportRe = /^import\s+['"]([^'"]+\.(?:css|scss))['"];?\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+
/^igniteui-webcomponents(-grids\/grids)?\/themes\/(light|dark)\/(material|bootstrap|fluent|indigo)\.css$/;
73+
const sampleLocalRe = /^\.\/[^/]+\.(?:css|scss)$/;
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(/\/samples\/.+\/src\/index\.ts$/)) 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(/[\\/]samples[\\/](.+)[\\/]src[\\/]index\.ts$/);
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(/\/node_modules\/(?:@infragistics\/)?(igniteui-[^/]+)\//);
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(/\/node_modules\/((?:@[^/]+\/)?[^/]+)\//);
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) {

azure-pipelines/build-pipeline.yml

Lines changed: 0 additions & 112 deletions
This file was deleted.

0 commit comments

Comments
 (0)