@ckeditor/ckeditor5-dev-manual-server 61.0.0 → 61.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,8 +10,8 @@ More information about development tools packages can be found at the following
10
10
 
11
11
  ## Loading translations
12
12
 
13
- Pass a language code to `manualTestsPlugin()` to load and combine that language's TypeScript translation files from the configured
14
- package paths. The selected language becomes the default UI and content language for editors and contexts created by manual tests.
13
+ Pass a language code to `manualTestsPlugin()` to load and combine that language's TypeScript translation files from the configured package paths. The selected language becomes the default UI and content language for editors and contexts created by manual tests.
14
+
15
15
  An explicit language in an editor or context configuration still takes precedence.
16
16
 
17
17
  ```ts
@@ -23,6 +23,29 @@ manualTestsPlugin( {
23
23
 
24
24
  For example, start the manual test server in Polish using `CK_LANGUAGE=pl pnpm manual`.
25
25
 
26
+ ## Preserving production CSS order
27
+
28
+ Vite may order extracted stylesheets differently from their source imports in multi-page production builds. Add `preserveCssImportOrderPlugin()` to keep the CSS cascade consistent with the bundled development server:
29
+
30
+ ```ts
31
+ import { defineConfig } from 'vite';
32
+ import {
33
+ manualTestsPlugin,
34
+ preserveCssImportOrderPlugin
35
+ } from '@ckeditor/ckeditor5-dev-manual-server';
36
+
37
+ export default defineConfig( {
38
+ plugins: [
39
+ preserveCssImportOrderPlugin(),
40
+ manualTestsPlugin( {
41
+ paths: [ 'packages/*' ]
42
+ } )
43
+ ]
44
+ } );
45
+ ```
46
+
47
+ The plugin enables Rolldown's strict module execution order in development and production, which can increase the generated bundle size. During production builds, it additionally rewrites plain CSS imports into ordered JavaScript side effects.
48
+
26
49
  ## Changelog
27
50
 
28
51
  See the [`CHANGELOG.md`](https://github.com/ckeditor/ckeditor5-dev/blob/master/packages/ckeditor5-dev-manual-server/CHANGELOG.md) file.
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  * For licensing, see LICENSE.md.
4
4
  */
5
5
  export { manualTestsPlugin, type ManualTestsPluginOptions } from "./manual-test-plugin/plugin.js";
6
+ export { preserveCssImportOrderPlugin } from "./preserve-css-import-order-plugin/plugin.js";
6
7
  export { ckDebugPlugin } from "./debug-plugin/plugin.js";
7
8
  export { refreshPlugin } from "./refresh-plugin/plugin.js";
8
9
  export { rawSvgPlugin } from "./svg-plugin/plugin.js";
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync, globSync, readFileSync, realpathSync } from "node:fs";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { basename, isAbsolute, posix, relative, resolve } from "node:path";
4
+ import { createHash } from "node:crypto";
4
5
  //#region src/utils.ts
5
6
  /**
6
7
  * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
@@ -183,10 +184,7 @@ function manualTestsPlugin(options) {
183
184
  name: "ckeditor5-manual-tests",
184
185
  config(config = {}) {
185
186
  workspaceRoot = resolve(config.root || process.cwd());
186
- return {
187
- input: getManualBuildInputs(),
188
- build: { rolldownOptions: { output: { strictExecutionOrder: true } } }
189
- };
187
+ return { input: getManualBuildInputs() };
190
188
  },
191
189
  configResolved(config) {
192
190
  workspaceRoot = config.root;
@@ -199,7 +197,6 @@ function manualTestsPlugin(options) {
199
197
  rewriteCatalogRequest(request, getManualCatalogPublicPath());
200
198
  next();
201
199
  });
202
- keepManualHtmlSourceFresh(server.environments.client.bundledDev?.memoryFiles, getManualPages, workspaceRoot);
203
200
  },
204
201
  resolveId(source) {
205
202
  if (source == MANUAL_ENTRIES_VIRTUAL_ID) return resolvedVirtualModuleId;
@@ -248,77 +245,6 @@ function manualTestsPlugin(options) {
248
245
  };
249
246
  }
250
247
  /**
251
- * Keeps the served manual test HTML in sync with the source file while the dev server runs.
252
- *
253
- * Under `experimental.bundledDev`, Vite serves each manual test's HTML from an in-memory bundle
254
- * produced by the rolldown dev engine. That engine emits the HTML output only during the initial
255
- * build and never regenerates it when the source `.html` changes: its bundle state reports no
256
- * stale output for HTML entries, `devEngine.invalidate()` throws on a non-JS module, and even a
257
- * forced full build leaves the HTML memory file untouched (verified against Vite 8.2.1). A `.html`
258
- * edit still triggers a full page reload, so without this the browser reloads into the same stale
259
- * HTML until the server is restarted.
260
- *
261
- * The built-in-memory HTML rewrites the entry script, injects the asset tags and manual chrome,
262
- * and copies the source `<head>` — all inside `<head>`. The markup after `</head>` is a verbatim
263
- * copy of the source. So on every request we keep the freshly built `<head>` and splice in the
264
- * current post-`</head>` markup read from disk. Splicing unconditionally (rather than only after a
265
- * detected change) also covers sources that changed between the initial build and the first request
266
- * for the page, and it is idempotent for unchanged sources. This reflects edits to the test body
267
- * without re-running Vite's HTML transform or parsing the document. Any edit that would change
268
- * what belongs in `<head>` is not picked up this way and still needs a server restart, because
269
- * reproducing it would require re-running the build pipeline the dev engine refuses to run for HTML
270
- * entries. That covers edits confined to the `<head>` (styles, meta, extra scripts) as well as
271
- * adding or removing `<ck-manual-header>` in the body: toggling it changes the injected header
272
- * chrome (`<meta>` and the component script) in `<head>`, so the chrome only updates after a
273
- * restart even though the body splice already reflects the element itself.
274
- *
275
- * When `bundledDev` is not enabled the store is absent and this is a no-op: Vite's normal dev
276
- * pipeline already serves fresh HTML on every request.
277
- */
278
- function keepManualHtmlSourceFresh(memoryFiles, getManualPages, workspaceRoot) {
279
- if (!memoryFiles) return;
280
- const getBundledFile = memoryFiles.get.bind(memoryFiles);
281
- memoryFiles.get = (filePath) => {
282
- const file = getBundledFile(filePath);
283
- if (!file || !getManualPages().has(toPublicSpecifier(filePath))) return file;
284
- try {
285
- const sourceFilePath = resolve(workspaceRoot, stripLeadingSlash(filePath));
286
- const freshHtml = composeFreshManualHtml(file.source, readFileSync(sourceFilePath, "utf8"));
287
- return freshHtml == null ? file : { source: freshHtml };
288
- } catch {
289
- return file;
290
- }
291
- };
292
- }
293
- /**
294
- * Combines the freshly built `<head>` (asset tags and injected manual chrome) with the current
295
- * post-`</head>` markup from the source file. Returns `null` when either document is missing a
296
- * `</head>`, so the caller can fall back to the built output.
297
- */
298
- function composeFreshManualHtml(builtHtml, sourceHtml) {
299
- const builtHeadEnd = findHeadCloseTagIndex(builtHtml);
300
- const sourceHeadEnd = findHeadCloseTagIndex(sourceHtml);
301
- if (builtHeadEnd == -1 || sourceHeadEnd == -1) return null;
302
- return builtHtml.slice(0, builtHeadEnd + 7) + sourceHtml.slice(sourceHeadEnd + 7);
303
- }
304
- /**
305
- * Finds the index of the `</head>` closing tag, or `-1` when the document has none. A `</head>`
306
- * literal may also appear earlier inside a `<head>` comment or an inline script string, so of all
307
- * the occurrences preceding the `<body>` tag the last one is taken. Without a `<body>` tag (or
308
- * when every occurrence follows it) the first occurrence wins, matching the pre-heuristic
309
- * behavior for such malformed documents.
310
- */
311
- function findHeadCloseTagIndex(html) {
312
- const firstHeadCloseIndex = /<\/head>/i.exec(html)?.index ?? -1;
313
- const bodyMatch = /<body[\s>]/i.exec(html);
314
- if (!bodyMatch) return firstHeadCloseIndex;
315
- const headClosePattern = /<\/head>/gi;
316
- let lastHeadCloseIndex = -1;
317
- let match;
318
- while ((match = headClosePattern.exec(html)) && match.index < bodyMatch.index) lastHeadCloseIndex = match.index;
319
- return lastHeadCloseIndex == -1 ? firstHeadCloseIndex : lastHeadCloseIndex;
320
- }
321
- /**
322
248
  * Injection contract for the `<ck-manual-header>` component, added to `<head>` only when the
323
249
  * page source contains the element:
324
250
  * - a `<meta>` carrying the package name and the base-aware catalog href the component reads;
@@ -374,6 +300,78 @@ function parseRequestUrl(requestUrl) {
374
300
  return URL.parse(requestUrl || "", "http://localhost");
375
301
  }
376
302
  //#endregion
303
+ //#region src/preserve-css-import-order-plugin/plugin.ts
304
+ /**
305
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
306
+ * For licensing, see LICENSE.md.
307
+ */
308
+ const INJECTED_CSS_VIRTUAL_ID_PREFIX = "\0virtual:ckeditor5-preserve-css-import-order:";
309
+ const INJECTED_CSS_VIRTUAL_ID_SUFFIX = ".js";
310
+ const CSS_FILE_REGEXP = /^[^?#]+\.css$/;
311
+ /**
312
+ * Preserves the source execution order of CSS imports in manual development and production builds.
313
+ *
314
+ * Vite extracts imported CSS into assets and orders those assets by the generated chunk graph.
315
+ * In a multi-page build that order may differ from the source module execution order, changing
316
+ * the cascade between CKEditor stylesheets. This is a known Vite limitation:
317
+ * https://github.com/vitejs/vite/issues/4890.
318
+ *
319
+ * Until Vite handles this natively (https://github.com/vitejs/vite/pull/22541), plain CSS imports
320
+ * in production builds are replaced with virtual JavaScript modules. Each module loads the
321
+ * processed CSS through Vite's `?inline` query and appends it as a `<style>` side effect.
322
+ * Rolldown's `strictExecutionOrder` keeps both these production effects and Vite's native bundled
323
+ * development CSS effects in source order.
324
+ *
325
+ * Query-bearing CSS imports retain their explicit Vite semantics and pass through unchanged.
326
+ */
327
+ function preserveCssImportOrderPlugin() {
328
+ let rewriteCssImports = false;
329
+ const cssFilePaths = /* @__PURE__ */ new Map();
330
+ return {
331
+ name: "ckeditor5-preserve-css-import-order",
332
+ config(_config, { command }) {
333
+ rewriteCssImports = command == "build";
334
+ return { build: { rolldownOptions: { output: { strictExecutionOrder: true } } } };
335
+ },
336
+ resolveId: {
337
+ order: "pre",
338
+ filter: { id: CSS_FILE_REGEXP },
339
+ async handler(source, importer, options) {
340
+ if (!rewriteCssImports) return null;
341
+ const resolved = await this.resolve(source, importer, {
342
+ ...options,
343
+ skipSelf: true
344
+ });
345
+ if (!resolved || resolved.external) return null;
346
+ const cssIdHash = createHash("sha256").update(resolved.id).digest("hex");
347
+ const virtualId = `${INJECTED_CSS_VIRTUAL_ID_PREFIX}${cssIdHash}${INJECTED_CSS_VIRTUAL_ID_SUFFIX}`;
348
+ cssFilePaths.set(virtualId, resolved.id);
349
+ return virtualId;
350
+ }
351
+ },
352
+ load: {
353
+ filter: { id: new RegExp(`^${INJECTED_CSS_VIRTUAL_ID_PREFIX}`) },
354
+ handler(id) {
355
+ const cssFilePath = cssFilePaths.get(id);
356
+ if (!cssFilePath) return null;
357
+ return {
358
+ code: createCssInjectionModule(cssFilePath),
359
+ moduleSideEffects: true
360
+ };
361
+ }
362
+ }
363
+ };
364
+ }
365
+ function createCssInjectionModule(cssFilePath) {
366
+ return `
367
+ import css from ${JSON.stringify(`${cssFilePath}?inline`)};
368
+
369
+ const style = document.createElement( 'style' );
370
+ style.textContent = css;
371
+ document.head.appendChild( style );
372
+ `;
373
+ }
374
+ //#endregion
377
375
  //#region src/debug-plugin/plugin.ts
378
376
  /**
379
377
  * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
@@ -423,7 +421,6 @@ function refreshPlugin() {
423
421
  configureServer(server) {
424
422
  const { bundledDev } = server.environments.client;
425
423
  wrapBundledDevClientSend(bundledDev);
426
- wrapBundledDevFullReloads(bundledDev, server.config.root);
427
424
  },
428
425
  hotUpdate({ file, modules }) {
429
426
  if (file.endsWith(".manual.html")) return modules;
@@ -444,31 +441,6 @@ function wrapBundledDevClientSend(bundledDev) {
444
441
  return setupIfNeeded(client, clientId);
445
442
  };
446
443
  }
447
- function wrapBundledDevFullReloads(bundledDev, workspaceRoot) {
448
- const handleHmrOutput = bundledDev.handleHmrOutput.bind(bundledDev);
449
- bundledDev.handleHmrOutput = (client, files, hmrOutput) => {
450
- if (hmrOutput.type != "FullReload") return handleHmrOutput(client, files, hmrOutput);
451
- if (!shouldShowManualRefreshPrompt(files)) {
452
- reloadClientAfterLatestBuildOutput(bundledDev, client, files, workspaceRoot);
453
- return;
454
- }
455
- ensureLatestBuildOutput(bundledDev);
456
- client.send({
457
- type: "custom",
458
- event: MANUAL_REFRESH_EVENT_NAME
459
- });
460
- };
461
- }
462
- async function reloadClientAfterLatestBuildOutput(bundledDev, client, files, workspaceRoot) {
463
- try {
464
- await bundledDev.devEngine.ensureLatestBuildOutput();
465
- } catch {}
466
- const htmlFile = files.find((file) => isHtmlFile(file));
467
- client.send({
468
- type: "full-reload",
469
- path: htmlFile ? toPublicFilePath(htmlFile, workspaceRoot) : void 0
470
- });
471
- }
472
444
  function ensureLatestBuildOutput(bundledDev) {
473
445
  try {
474
446
  bundledDev.devEngine.ensureLatestBuildOutput().catch(() => {});
@@ -527,4 +499,4 @@ function rawSvgPlugin() {
527
499
  * For licensing, see LICENSE.md.
528
500
  */
529
501
  //#endregion
530
- export { ckDebugPlugin, manualTestsPlugin, rawSvgPlugin, refreshPlugin, stringifyValues };
502
+ export { ckDebugPlugin, manualTestsPlugin, preserveCssImportOrderPlugin, rawSvgPlugin, refreshPlugin, stringifyValues };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md.
4
+ */
5
+ import type { Plugin } from "vite";
6
+ /**
7
+ * Preserves the source execution order of CSS imports in manual development and production builds.
8
+ *
9
+ * Vite extracts imported CSS into assets and orders those assets by the generated chunk graph.
10
+ * In a multi-page build that order may differ from the source module execution order, changing
11
+ * the cascade between CKEditor stylesheets. This is a known Vite limitation:
12
+ * https://github.com/vitejs/vite/issues/4890.
13
+ *
14
+ * Until Vite handles this natively (https://github.com/vitejs/vite/pull/22541), plain CSS imports
15
+ * in production builds are replaced with virtual JavaScript modules. Each module loads the
16
+ * processed CSS through Vite's `?inline` query and appends it as a `<style>` side effect.
17
+ * Rolldown's `strictExecutionOrder` keeps both these production effects and Vite's native bundled
18
+ * development CSS effects in source order.
19
+ *
20
+ * Query-bearing CSS imports retain their explicit Vite semantics and pass through unchanged.
21
+ */
22
+ export declare function preserveCssImportOrderPlugin(): Plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-dev-manual-server",
3
- "version": "61.0.0",
3
+ "version": "61.2.0",
4
4
  "description": "Used to extend Vite to create a manual test server for CKEditor 5.",
5
5
  "keywords": [],
6
6
  "author": "CKSource (http://cksource.com/)",
@@ -25,6 +25,6 @@
25
25
  ],
26
26
  "dependencies": {
27
27
  "@ckeditor/ckeditor5-inspector": "^5.0.1",
28
- "vite": "^8.2.1"
28
+ "vite": "^8.2.2"
29
29
  }
30
30
  }