-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathastro.config.mjs
More file actions
311 lines (282 loc) · 12 KB
/
Copy pathastro.config.mjs
File metadata and controls
311 lines (282 loc) · 12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
// @ts-check
import { defineConfig } from 'astro/config';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { existsSync } from 'node:fs';
/**
* Vite plugin: strip the module-level `new Sample();` (or `new ClassName();`)
* from every sample src/index.ts.
*
* WHY this is needed
* ──────────────────
* The `[...slug].astro` loader calls `new module.Sample()` explicitly after
* the dynamic import resolves, so the page controls when a sample
* instantiates. Keeping the module-level `new Sample()` as well would run
* every sample twice. Stripping it here (instead of editing 900+ samples)
* keeps each sample runnable standalone while the browser instantiates it
* exactly once.
*/
/** @returns {import('vite').Plugin} */
function stripSampleInstantiation() {
// Match the trailing `new ClassName();` that sample generators emit.
// It is always the last non-empty statement in the file.
const trailingNewRe = /\bnew\s+\w+\(\)\s*;?\s*$/;
return {
name: 'strip-sample-instantiation',
enforce: /** @type {'pre'} */ ('pre'),
transform(code, id) {
// Only touch samples/**/src/index.ts
if (!id.replace(/\\/g, '/').match(/\/samples\/.+\/src\/index\.ts$/)) return;
if (!trailingNewRe.test(code)) return;
return { code: code.replace(trailingNewRe, ''), map: null };
},
};
}
/**
* Vite plugin: take the stylesheet imports out of every sample entry module.
*
* WHY this is needed
* ──────────────────
* A sample's entry module is loaded lazily, by slug, from a dynamic import that
* only runs after DOMContentLoaded — and it drags in megabytes of library code.
* While that is in flight the page has already painted, so any CSS the module
* owns arrives far too late: the sample flashes unstyled first.
*
* It was also a correctness problem. Rollup hoists shared code into some
* chunk, and evaluating a chunk runs the whole module body — so a *different*
* sample's theme import could land in the document first and win the library's
* one-shot `getTheme()` check, rendering material samples with the bootstrap
* theme.
*
* So `[...slug].astro` now emits these stylesheets into <head> at build time
* (see resolveSampleStyles): themes as <link>s to public/ig-themes/, everything
* sample-local inlined. This plugin drops the imports it has taken over, so the
* two can't both own the same sheet — otherwise the module's copy would
* re-append itself last on every load and clobber a theme swap.
*
* Anything whose shape the page does NOT resolve is deliberately left alone and
* still injected at runtime, so an unrecognised import degrades to the old
* behaviour instead of silently losing its styles.
*/
/** @returns {import('vite').Plugin} */
function inlineSampleCss() {
// Matches any CSS / SCSS side-effect import inside a sample file (relative or package).
const cssImportRe = /^import\s+['"]([^'"]+\.(?:css|scss))['"];?\s*$/gm;
// The two shapes [...slug].astro knows how to put in <head>. Keep in sync
// with resolveSampleStyles() in src/utils/samples.ts.
const themeSpecRe =
/^igniteui-webcomponents(-grids\/grids)?\/themes\/(light|dark)\/(material|bootstrap|fluent|indigo)\.css$/;
const sampleLocalRe = /^\.\/[^/]+\.(?:css|scss)$/;
const handledInHead = spec => themeSpecRe.test(spec) || sampleLocalRe.test(spec);
let isBuild = false;
return {
name: 'inline-sample-css',
enforce: /** @type {'pre'} */ ('pre'),
configResolved(config) {
isBuild = config.command === 'build';
},
transform(code, id) {
if (!id.replace(/\\/g, '/').match(/\/samples\/.+\/src\/index\.ts$/)) return;
cssImportRe.lastIndex = 0;
if (!cssImportRe.test(code)) return;
cssImportRe.lastIndex = 0;
let i = 0;
const newCode = code.replace(cssImportRe, (line, spec) => {
// Already in <head> — drop it so nothing is styled twice.
if (handledInHead(spec)) return '';
// In dev Vite injects CSS imports natively, which is correct per-module.
if (!isBuild) return line;
// Production fallback for shapes the page could not resolve. ?inline
// keeps the CSS as a string inside this module, so Vite emits no shared
// CSS chunk that could leak onto unrelated pages.
const v = `__sampleCss${i++}`;
return [
`import ${v} from '${spec}?inline';`,
`{const __s=document.createElement('style');__s.textContent=${v};document.head.appendChild(__s);}`,
].join('\n');
});
return { code: newCode, map: null };
},
};
}
const __dirname = path.dirname(fileURLToPath(import.meta.url));
/**
* Ignite UI packages that may be installed either as unscoped (e.g.
* `igniteui-dockmanager`) or as `@infragistics/`-scoped equivalents.
* Used by both the resolveId plugin and the optimizeDeps.include list.
*/
const IGNITEUI_PACKAGES = [
'igniteui-dockmanager',
'igniteui-webcomponents-core',
'igniteui-webcomponents-charts',
'igniteui-webcomponents-gauges',
'igniteui-webcomponents-datasources',
'igniteui-webcomponents-excel',
'igniteui-webcomponents-inputs',
'igniteui-webcomponents-data-grids',
'igniteui-webcomponents-maps',
'igniteui-webcomponents-spreadsheet',
'igniteui-webcomponents-spreadsheet-chart-adapter',
'igniteui-webcomponents-layouts',
'igniteui-webcomponents-dashboards',
'igniteui-webcomponents-grids',
];
/**
* Vite plugin: resolve unscoped igniteui-* package names to their @infragistics/
* scoped equivalents when the unscoped package is not installed.
*
* WHY a resolveId plugin instead of resolve.alias
* ────────────────────────────────────────────────
* Astro merges its own Vite config last and can replace resolve.alias arrays.
* A resolveId hook is part of the Rollup plugin pipeline and is always called
* for every import, regardless of how Astro configures the resolver.
*/
/** @returns {import('vite').Plugin} */
function resolveIgniteUiScoped() {
// Build a map at startup: unscoped name → scoped name, only for packages
// that are absent from node_modules unscoped.
/** @type {Map<string, string>} */
const redirects = new Map();
for (const pkg of IGNITEUI_PACKAGES) {
if (!existsSync(path.resolve(__dirname, 'node_modules', pkg))) {
redirects.set(pkg, `@infragistics/${pkg}`);
}
}
return {
name: 'resolve-igniteui-scoped',
async resolveId(id, importer, options) {
// Exact match (e.g. 'igniteui-dockmanager')
if (redirects.has(id)) {
return this.resolve(redirects.get(id), importer, { ...options, skipSelf: true });
}
// Subpath match (e.g. 'igniteui-webcomponents-grids/grids/combined')
for (const [unscoped, scoped] of redirects) {
if (id.startsWith(`${unscoped}/`)) {
const newId = `${scoped}${id.slice(unscoped.length)}`;
return this.resolve(newId, importer, { ...options, skipSelf: true });
}
}
},
};
}
// Set BASE_PATH env variable to deploy under a sub-path, e.g. "/webcomponents-demos"
const base = process.env.BASE_PATH ?? '';
/**
* Returns the installed package name for a given unscoped igniteui-* id.
* If the unscoped package exists in node_modules it is returned as-is;
* otherwise the @infragistics/ scoped name is returned.
* @param {string} pkg
*/
function ig(pkg) {
return existsSync(path.resolve(__dirname, 'node_modules', pkg))
? pkg
: `@infragistics/${pkg}`;
}
// https://astro.build/config
export default defineConfig({
// Static output — builds to dist/ as plain HTML + JS assets (ideal for IIS / Nginx / CDN)
output: 'static',
// When deploying to https://staging.infragistics.com/webcomponents-demos set:
// BASE_PATH=/webcomponents-demos npm run build
base,
// Match IIS behaviour: routes are served without trailing slashes
trailingSlash: 'never',
// Keep every stylesheet as an emitted file. With the default 'auto',
// Astro inlines small CSS assets into page HTML and deletes the files,
// but the sample chunks' __vite__mapDeps still preload them at runtime
// → "Unable to preload CSS" on every sample page.
build: {
inlineStylesheets: 'never',
},
vite: {
plugins: [resolveIgniteUiScoped(), stripSampleInstantiation(), inlineSampleCss()],
// samples/ and node_modules/ are already at the repo root (__dirname),
// so no extra fs.allow entries are needed.
server: {
fs: {
allow: [path.resolve(__dirname)],
},
},
// Dep optimisation:
// noDiscovery stops esbuild from scanning any source files (including
// [..slug].astro whose client script globs sample TS files that have CSS
// side-effect imports — causing "Expected ';'" crashes).
// We explicitly pre-bundle only the igniteui runtime packages so the first
// sample click is fast without triggering the scanner.
optimizeDeps: {
noDiscovery: true,
include: [
'igniteui-webcomponents',
...IGNITEUI_PACKAGES.map(ig),
'igniteui-grid-lite',
// CJS-only packages that need pre-bundling for named-export interop
'file-saver',
],
},
// CSS / SCSS:
// `loadPaths` / `includePaths` make node_modules visible to Sass so
// samples that @use 'igniteui-theming/sass/...' resolve correctly.
css: {
devSourcemap: true,
preprocessorOptions: {
scss: {
loadPaths: [path.resolve(__dirname, 'node_modules')],
},
},
},
build: {
chunkSizeWarningLimit: 16000,
sourcemap: process.env.NODE_ENV !== 'production',
cssCodeSplit: true,
rollupOptions: {
output: {
// Give every sample its own chunk so Rollup doesn't try to inline
// all 700+ samples into a single bundle (causes OOM).
manualChunks(id) {
// Vite's dynamic-import preload helper is shared by every lazy
// chunk. Left unassigned, Rollup hosts it inside one vendor
// chunk and the others import it back — a chunk cycle
// (grid-lite → webcomponents → grid-lite build warning).
if (id.includes('vite/preload-helper')) {
return 'preload-helper';
}
const match = id.match(/[\\/]samples[\\/](.+)[\\/]src[\\/]index\.ts$/);
if (match) {
return `samples/${match[1].replace(/[\\/]/g, '--')}`;
}
// Shared IgniteUI runtime → one vendor chunk per package.
// Without this, Rollup hosts shared library code inside the first
// sample chunk that imports it, so every other sample transits
// through that chunk (e.g. an 11MB annotations-all hosting the
// charts runtime) and pulls in its side effects.
const vendor = id
.replace(/\\/g, '/')
.match(/\/node_modules\/(?:@infragistics\/)?(igniteui-[^/]+)\//);
if (vendor) {
return `vendor/${vendor[1]}`;
}
// Every other node_modules package too. A shared non-IgniteUI
// dep (lit, file-saver, …) left unassigned gets hosted inside the
// first *sample* chunk that imports it, so unrelated pages
// evaluate that sample's module body — its defineAllComponents()
// and theme CSS included (the bootstrap-instead-of-material bug).
const dep = id
.replace(/\\/g, '/')
.match(/\/node_modules\/((?:@[^/]+\/)?[^/]+)\//);
if (dep) {
return `vendor/${dep[1].replace('/', '--')}`;
}
},
// Keep sample CSS files scoped to their own chunk names
assetFileNames(assetInfo) {
const name = assetInfo.name ?? '';
if (name.endsWith('.css') && assetInfo.source) {
return '_astro/[name].[hash][extname]';
}
return '_astro/[name].[hash][extname]';
},
},
},
},
},
});