-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy pathvite.config.js
More file actions
539 lines (530 loc) · 19.6 KB
/
vite.config.js
File metadata and controls
539 lines (530 loc) · 19.6 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
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
/**
* Vite configuration for the WP Desktop Mode plugin.
*
* Builds two TypeScript entries into IIFE bundles:
*
* `src/desktop.ts` →
* - `assets/js/desktop.js` (development, unminified — loaded when SCRIPT_DEBUG is true)
* - `assets/js/desktop.min.js` (production, esbuild-minified — loaded otherwise)
*
* `src/iframe-bridge-standalone.ts` →
* - `assets/js/iframe-bridge.js` (development)
* - `assets/js/iframe-bridge.min.js` (production)
*
* Which entry the current invocation builds is controlled by the
* `DESKTOP_MODE_TARGET` env var (`desktop` — default — or `iframe-bridge`).
* `npm run build` runs Vite four times (two targets × two modes).
* `npm run dev` watches and rebuilds the unminified `desktop` bundle
* only — iframe-bridge changes are rare so a one-shot
* `npm run build:iframe-bridge` covers them.
*
* **Source policy:** `assets/js/*.js` is build output. NEVER hand-edit
* those files — only edit the TS sources under `src/` and run a build.
*
* @since 0.5.0
*/
import { defineConfig } from 'vite';
import { resolve } from 'node:path';
import { visualizer } from 'rollup-plugin-visualizer';
/**
* Strip `static help = { … };` blocks from production builds.
*
* Every `<wpd-*>` component class declares a `static help = { … }`
* descriptor — title, summary, props/slots/parts/cssProps tables,
* examples, status, since. ~82 kB of plain documentation across the
* 47 components in the kit.
*
* That descriptor has exactly one runtime consumer: the OS Settings
* → Help tab (`src/settings/sections/help.ts`), which iterates
* `WPD_COMPONENT_TAGS` and renders the metadata. The same module
* already handles components without a descriptor — it falls back
* to a minimal stub built from `static props`. So in production we
* can drop the descriptor from the bundle entirely and the help
* screen still works, just without the rich copy.
*
* Dev builds keep `static help` intact so live exploration and the
* component help screen stay fully informative during development.
* Production builds get a one-liner: `static help = void 0;`.
*
* Conservative parser:
* - Only `.ts` files under `src/ui/components/` are inspected.
* - Block must begin with the exact source `\tstatic help = {`
* to avoid false-positives elsewhere.
* - Strings and nested object literals are balanced before the
* replacement; the trailing `;` is consumed if present.
*/
function stripStaticHelpInProd( enabled ) {
if ( ! enabled ) {
return null;
}
return {
name: 'wp-desktop-mode-strip-static-help',
enforce: 'pre',
apply: 'build',
transform( code, id ) {
if ( ! id.endsWith( '.ts' ) ) {
return null;
}
if ( ! id.includes( '/src/ui/components/' ) ) {
return null;
}
const marker = 'static help';
let start = code.indexOf( marker );
if ( start < 0 ) {
return null;
}
const eq = code.indexOf( '=', start );
const braceOpen = code.indexOf( '{', eq );
if ( eq < 0 || braceOpen < 0 || braceOpen - start > 32 ) {
return null;
}
let depth = 1;
let i = braceOpen + 1;
while ( i < code.length && depth > 0 ) {
const ch = code[ i ];
if ( ch === '{' ) {
depth++;
i++;
} else if ( ch === '}' ) {
depth--;
i++;
} else if ( ch === '"' || ch === "'" || ch === '`' ) {
const q = ch;
i++;
while ( i < code.length && code[ i ] !== q ) {
if ( code[ i ] === '\\' ) {
i += 2;
} else {
i++;
}
}
i++;
} else if ( ch === '/' && code[ i + 1 ] === '/' ) {
const nl = code.indexOf( '\n', i );
i = nl < 0 ? code.length : nl + 1;
} else if ( ch === '/' && code[ i + 1 ] === '*' ) {
const end = code.indexOf( '*/', i + 2 );
i = end < 0 ? code.length : end + 2;
} else {
i++;
}
}
// Trailing tokens: optional ` as const` (TypeScript widening
// guard some component classes apply to the descriptor) and
// the closing `;`. Walk forward until the next `;` or EOL,
// whichever comes first — we control the source shape so a
// stray `;` inside the descriptor would already have been
// consumed by the string/object scanner above.
let blockEnd = i;
while ( blockEnd < code.length && code[ blockEnd ] !== ';' && code[ blockEnd ] !== '\n' ) {
blockEnd++;
}
if ( code[ blockEnd ] === ';' ) {
blockEnd++;
}
const replacement = 'static help = void 0;';
const out = code.slice( 0, start ) + replacement + code.slice( blockEnd );
return { code: out, map: null };
},
};
}
/**
* Minify the contents of `css\`...\`` tagged-template literals.
*
* Esbuild's JS minifier treats template literals as opaque string
* data — it won't touch their content even when that content is CSS.
* Every `*.styles.ts` file in `src/ui/components/` defines its
* stylesheet inside one of these templates, so its CSS comments and
* indentation ship into the bundle byte-for-byte. This transform
* runs at the Vite `transform` stage (before esbuild) and rewrites
* each `css\`…\`` body with a minimal CSS minifier: strip `/* … *\/`
* block comments, collapse runs of whitespace, drop whitespace
* adjacent to `{ } : ; , >`.
*
* Conservative on purpose:
* - Only `.ts` files are inspected.
* - Only tagged templates whose tag is the bare identifier `css`
* are touched (no `someObj.css\`\``, no `customCss\`\``).
* - Interpolation slots (`${…}`) are preserved verbatim — we
* minify the literal segments between them and leave the
* expression text alone.
* - Disabled in dev so source still maps cleanly during debug.
*/
function minifyCssTemplates() {
// Minify one CSS chunk *between* template interpolations.
//
// Crucially, we do NOT `.trim()` here — chunks that end right
// before a `${…}` slot need to keep their trailing whitespace,
// and chunks that start right after a `${…}` slot need to keep
// their leading whitespace. Otherwise a literal like
//
// calc( 100% - ${ CHEVRON_W } )
//
// minifies to `calc(100% -${CHEVRON_W} )` and resolves at
// runtime to `calc(100% -10px)`, which CSS rejects because `-`
// in `calc()` requires whitespace on both sides (without it,
// `-10px` parses as a single negative-length token). The
// `wpd-crumb-chain` chevron polygon broke exactly this way.
//
// We still collapse adjacent whitespace and strip it around
// punctuation that doesn't care (`{ } : ; , >`), so the
// per-chunk minification is unchanged everywhere else. The
// leading/trailing whitespace of the WHOLE template gets
// trimmed once at the call site.
const minifyCssChunk = ( text ) =>
text
.replace( /\/\*[\s\S]*?\*\//g, '' )
.replace( /\s+/g, ' ' )
.replace( /\s*([{}:;,>])\s*/g, '$1' )
.replace( /;}/g, '}' );
return {
name: 'wp-desktop-mode-minify-css-templates',
enforce: 'pre',
apply: 'build',
transform( code, id ) {
if ( ! id.endsWith( '.ts' ) ) {
return null;
}
if ( ! code.includes( 'css`' ) ) {
return null;
}
let out = '';
let i = 0;
let changed = false;
while ( i < code.length ) {
// Look for the literal `css\`` not preceded by an
// identifier character — avoids matching `.css\``,
// `myCss\``, etc.
const m = code.indexOf( 'css`', i );
if ( m < 0 ) {
out += code.slice( i );
break;
}
const prev = m === 0 ? '' : code[ m - 1 ];
if ( /[A-Za-z0-9_$.]/.test( prev ) ) {
// Not the bare `css` tag — keep walking.
out += code.slice( i, m + 4 );
i = m + 4;
continue;
}
out += code.slice( i, m + 4 ); // up to and including ``css``
let j = m + 4;
let segStart = j;
let interpStart = -1;
let interpDepth = 0;
let closed = false;
while ( j < code.length ) {
const ch = code[ j ];
if ( interpDepth === 0 ) {
if ( ch === '\\' ) {
j += 2;
continue;
}
if ( ch === '`' ) {
out += minifyCssChunk( code.slice( segStart, j ) );
out += '`';
i = j + 1;
changed = true;
closed = true;
break;
}
if ( ch === '$' && code[ j + 1 ] === '{' ) {
out += minifyCssChunk( code.slice( segStart, j ) );
interpStart = j;
interpDepth = 1;
j += 2;
continue;
}
j++;
} else {
if ( ch === '{' ) {
interpDepth++;
} else if ( ch === '}' ) {
interpDepth--;
if ( interpDepth === 0 ) {
out += code.slice( interpStart, j + 1 );
segStart = j + 1;
interpStart = -1;
}
}
j++;
}
}
if ( ! closed ) {
// Unterminated template (shouldn't happen on valid TS,
// but be defensive) — keep the original rest.
out += code.slice( m + 4 );
i = code.length;
}
}
return changed ? { code: out, map: null } : null;
},
};
}
const TARGETS = {
desktop: {
entry: 'src/desktop.ts',
fileBase: 'desktop',
// Exports from the entry land on `window.desktopMode` — a no-op
// today (no external consumers) but leaves the door open for
// tests or devtools probing.
iifeName: 'desktopMode',
},
'iframe-bridge': {
entry: 'src/iframe-bridge-standalone.ts',
fileBase: 'iframe-bridge',
iifeName: 'desktopModeIframeBridge',
},
// Gutenberg drop-receiver — tiny iframe-side bundle enqueued only
// on post.php / post-new.php. Listens for `desktop-mode-drop`
// messages from the shell and inserts the corresponding block via
// `wp.data.dispatch('core/block-editor').insertBlocks(...)`. See
// `src/drag/iframe-drop-targets.ts` for the shell side.
'gutenberg-drop-receiver': {
entry: 'src/gutenberg-drop-receiver.ts',
fileBase: 'gutenberg-drop-receiver',
iifeName: 'desktopModeGutenbergDropReceiver',
},
// Recycle Bin app — a thin bundle that registers a render
// callback on `window.desktopModeNativeWindows['desktop-mode-recycle-bin']`
// and renders a `<wpd-table>` populated from the REST list. The
// `<wpd-*>` elements themselves are defined by the main desktop
// bundle, so this module just consumes them.
'recycle-bin': {
entry: 'src/recycle-bin/index.ts',
fileBase: 'recycle-bin',
iifeName: 'desktopModeRecycleBin',
},
// Native Posts window — `<wpd-table>`-driven replacement for the
// chromeless `edit.php` iframe, opt-in per user via OS Settings →
// Features. Same shape as recycle-bin: registers a render
// callback on `window.desktopModeNativeWindows['desktop-mode-posts']`
// and consumes the `<wpd-*>` tags defined by the main bundle.
'posts-window': {
entry: 'src/posts-window/index.ts',
fileBase: 'posts-window',
iifeName: 'desktopModePostsWindow',
},
// "My WordPress" file-explorer window — registers a render
// callback on `window.desktopModeNativeWindows['desktop-mode-my-wordpress']`
// and reuses the `<wpd-*>` tags defined by the main desktop bundle.
'my-wordpress': {
entry: 'src/my-wordpress/index.ts',
fileBase: 'my-wordpress',
iifeName: 'desktopModeMyWordpress',
},
// Content Graph — PixiJS-driven force-directed map of every post
// and page (and any opt-in public CPT) wired together by their
// internal hyperlinks. Lazy-loads PixiJS via the same module
// registry the wallpapers + posts-window mindmap use. Registers a
// render callback on `window.desktopModeNativeWindows['desktop-mode-content-graph']`.
'content-graph': {
entry: 'src/content-graph/index.ts',
fileBase: 'content-graph',
iifeName: 'desktopModeContentGraph',
},
// Service worker — own bundle so it can be served from a stable
// path with the `Service-Worker-Allowed: /` header. The IIFE
// wrapper is harmless inside a SW context: top-level
// `self.addEventListener` calls happen synchronously when the
// IIFE runs, which is exactly what the SW spec wants.
'pwa-sw': {
entry: 'src/pwa/sw.ts',
fileBase: 'sw',
iifeName: 'desktopModeServiceWorker',
},
// Native Comments window — replaces the chromeless
// `edit-comments.php` iframe with a `<wpd-table>`-driven moderation
// queue: Pending/All/Spam/Trash/Mine tabs, bulk + undo,
// inline reply, keyboard nav, spam confidence score, author
// insights drawer. Same shape as posts-window: registers a
// render callback on
// `window.desktopModeNativeWindows['desktop-mode-comments']`.
'comments-window': {
entry: 'src/comments-window/index.ts',
fileBase: 'comments-window',
iifeName: 'desktopModeCommentsWindow',
},
// Native Plugins window — replaces the chromeless `plugins.php`
// and `plugin-install.php` iframes with a `<wpd-tabs>`-driven
// installed list + browse-the-repo gallery + detail flyout. Same
// shape as posts-window: registers a render callback on
// `window.desktopModeNativeWindows['desktop-mode-plugins']` and
// consumes the `<wpd-*>` tags defined by the main desktop bundle.
'plugins-window': {
entry: 'src/plugins-window/index.ts',
fileBase: 'plugins-window',
iifeName: 'desktopModePluginsWindow',
},
// AI Assistant — moved out of the main bundle in 0.8.4. The
// main `desktop[.min].js` bundle ships a tiny `AiAssistantStub`
// matching the public `wp.desktop.ai` contract; this bundle
// holds the 38 kB implementation and is `<script>`-injected by
// the stub on the user's first invocation. Publishes
// `window.desktopModeCreateAiAssistant`.
'ai-assistant': {
entry: 'src/ai-assistant/entry.ts',
fileBase: 'ai-assistant',
iifeName: 'desktopModeAiAssistant',
},
// Animated WP Logo wallpaper — built-in canvas wallpaper moved
// out of the main bundle in 0.8.4. PHP registers the wallpaper
// via `desktop_mode_register_wallpaper()` with a `script` handle;
// the shell's wallpaper sync loads this bundle only when the
// user selects (or hovers in OS Settings) the wallpaper. The
// bundle's only side effect is publishing the `WallpaperDef` on
// `window.desktopModeWallpapers['wp-animated-logo']`.
'animated-logo-wallpaper': {
entry: 'src/plugins/animated-logo-wallpaper/index.ts',
fileBase: 'animated-logo-wallpaper',
iifeName: 'desktopModeAnimatedLogoWallpaper',
},
// About-scene — the PixiJS particle scene rendered inside OS
// Settings → About. ~25 kB of code that only ever runs after the
// user explicitly opens that tab. Loaded by the main-bundle
// `about-scene-loader.ts` on first mount; publishes
// `window.desktopModeMountAboutScene`.
'about-scene': {
entry: 'src/settings/sections/about-scene-entry.ts',
fileBase: 'about-scene',
iifeName: 'desktopModeAboutScene',
},
// OS Settings panel — the big lazy bundle (Stage 8). Hosts every
// section renderer + the `<wpd-*>` components only the panel
// uses (color/range field, swatch, swatch-grid, section,
// segmented, tabs, panel, empty-state, checkbox-label, button,
// select, text-field). Loaded on the user's first Settings open
// by the `OsSettings.renderPanel()` stub. Publishes
// `window.desktopModeRenderOsSettingsPanel`.
'os-settings-panel': {
entry: 'src/settings/panel-entry.ts',
fileBase: 'os-settings-panel',
iifeName: 'desktopModeOsSettingsPanel',
},
// Shell overlays — toast, confirm dialog, context menus (Stage 9).
// Components for action-triggered overlays that aren't constructed
// at first paint. Preloaded by main after first paint via
// `preloadShellOverlays( … )` so the first toast / wpdConfirm /
// right-click feels instant. Side-effect-only bundle: each leaf
// import runs its `defineComponent( … )` call at top level.
'shell-overlays': {
entry: 'src/shell-overlays/entry.ts',
fileBase: 'shell-overlays',
iifeName: 'desktopModeShellOverlays',
},
// Window system (Stage 11) — the `Window` class + DOM / pointer
// / tab / chrome helpers. Largest single module in the pre-0.8.4
// main bundle (~68 kB pre-min just for `window/index.ts`).
// Loaded on demand by the first call to
// `WindowManager.open()` / `openNew()` — both async since
// 0.8.4. Publishes `window.desktopModeWindowSystem`. Pre-loaded
// by `desktop.ts` after first paint via
// `preloadWindowSystem( … )` so any "user clicks the first icon"
// click typically lands on the sync fast path.
'window-system': {
entry: 'src/window-system/entry.ts',
fileBase: 'window-system',
iifeName: 'desktopModeWindowSystemBundle',
},
// Heartbeat widget — built-in PixiJS widget moved out of the
// main bundle in 0.18.0. Same registration shape third-party
// widgets use: PHP declares it via `desktop_mode_register_widget()`
// with the `desktop-mode-heartbeat-widget` script handle; the
// shell's widgets server-sync loads the bundle on demand. The
// bundle ships JS + a co-located `styles.css` chunk so widget
// chrome stays out of the main `desktop.css`.
'widget-heartbeat': {
entry: 'src/plugins/heartbeat-widget/index.ts',
fileBase: 'widget-heartbeat',
iifeName: 'desktopModeHeartbeatWidget',
},
};
export default defineConfig( ( { mode } ) => {
const isProd = mode === 'production';
const targetKey = process.env.DESKTOP_MODE_TARGET || 'desktop';
const target = TARGETS[ targetKey ];
if ( ! target ) {
throw new Error(
`vite.config.js: unknown DESKTOP_MODE_TARGET="${ targetKey }". ` +
`Expected one of: ${ Object.keys( TARGETS ).join( ', ' ) }.`,
);
}
// Bundle treemap: `BUNDLE_REPORT=1 npm run build:desktop` writes an
// HTML treemap next to the bundle so we can see which modules are
// pulling weight. Off by default — has zero impact on shipped code.
const wantReport = process.env.BUNDLE_REPORT === '1' && isProd;
const reportPlugins = wantReport
? [
visualizer( {
filename: `assets/js/${ target.fileBase }.report.html`,
template: 'treemap',
gzipSize: true,
brotliSize: false,
sourcemap: false,
emitFile: false,
open: false,
} ),
]
: [];
return {
plugins: [
minifyCssTemplates(),
stripStaticHelpInProd( isProd ),
...reportPlugins,
].filter( Boolean ),
resolve: {
alias: {
'@/': resolve( __dirname, 'src/' ) + '/',
'@api/': resolve( __dirname, 'src/api/' ) + '/',
'@boot/': resolve( __dirname, 'src/boot/' ) + '/',
'@core/': resolve( __dirname, 'src/core/' ) + '/',
'@features/': resolve( __dirname, 'src/features/' ) + '/',
'@layout/': resolve( __dirname, 'src/layout/' ) + '/',
'@protocol/': resolve( __dirname, 'src/protocol/' ) + '/',
'@ui/': resolve( __dirname, 'src/ui/' ) + '/',
'@window-system/': resolve( __dirname, 'src/window-system/' ) + '/',
},
},
build: {
outDir: 'assets/js',
// Every run writes into the same dir — don't let later runs
// delete what earlier ones produced.
emptyOutDir: false,
target: 'es2020',
// esbuild minification is ~10x faster than terser with comparable
// output for plain TS; no separate dep needed.
minify: isProd ? 'esbuild' : false,
sourcemap: false,
lib: {
entry: resolve( __dirname, target.entry ),
// IIFE wraps the module so it runs on script load without any
// module-system glue. WordPress admin can't reliably import
// <script type="module">, so we ship a self-contained bundle.
formats: [ 'iife' ],
name: target.iifeName,
fileName: () =>
isProd
? `${ target.fileBase }.min.js`
: `${ target.fileBase }.js`,
},
rollupOptions: {
output: {
// Vite's lib mode defaults `style.css` for bundled CSS.
// Rename to match the target's fileBase so a widget
// bundle and its co-located CSS share a name —
// `widget-heartbeat[.min].css` next to the JS,
// matching what `wp_register_style()` looks for in
// `includes/widgets/heartbeat.php`.
assetFileNames: ( asset ) => {
if ( asset.name && asset.name.endsWith( '.css' ) ) {
return isProd
? `${ target.fileBase }.min.css`
: `${ target.fileBase }.css`;
}
return '[name].[hash][extname]';
},
},
},
},
};
} );