From 956bfc784f5ec22a6347bea92d239e40e4dbac32 Mon Sep 17 00:00:00 2001 From: Filip Sobol Date: Wed, 13 May 2026 13:47:16 +0200 Subject: [PATCH 01/12] Migrate `ckeditor5-build-tools` from Rollup to Rolldown. --- .../ckeditor5-dev-build-tools/package.json | 18 +- .../ckeditor5-dev-build-tools/src/build.ts | 34 +- .../ckeditor5-dev-build-tools/src/config.ts | 254 +++++-------- .../ckeditor5-dev-build-tools/src/index.ts | 3 - .../src/plugins/banner.ts | 17 +- .../src/plugins/bundleCss.ts | 83 ++++- .../src/plugins/declarations.ts | 77 ++++ .../src/plugins/emitCss.ts | 37 -- .../src/plugins/loadSourcemaps.ts | 2 +- .../src/plugins/loadSources.ts | 42 --- .../src/plugins/rawImport.ts | 38 +- .../src/plugins/replace.ts | 116 ------ .../src/plugins/splitCss.ts | 39 +- .../src/plugins/translations.ts | 2 +- .../ckeditor5-dev-build-tools/src/utils.ts | 3 +- .../tests/_utils/utils.ts | 30 +- .../tests/build/arguments.test.ts | 24 +- .../tests/build/build.test.ts | 111 ++++-- .../build/fixtures/src/external-reexport.js | 6 + .../build/fixtures/src/js-extension-import.ts | 8 + .../build/fixtures/src/js-extension-source.js | 6 + .../build/fixtures/src/js-extension-source.ts | 6 + .../tests/config/config.test.ts | 96 +++-- .../tests/index.test.ts | 17 + .../tests/plugins/banner/banner.test.ts | 13 +- .../bundleCss/bundleCss.mocked.test.ts | 62 +++- .../tests/plugins/bundleCss/bundleCss.test.ts | 28 +- .../plugins/declarations/declarations.test.ts | 81 ++++ .../plugins/declarations/fixtures/common.cts | 1 + .../declarations/fixtures/ignored.d.ts | 6 + .../plugins/declarations/fixtures/module.mts | 1 + .../plugins/declarations/fixtures/valid.ts | 6 + .../tests/plugins/emitCss/emitCss.test.ts | 48 --- .../loadSourcemaps/loadSourcemaps.test.ts | 10 +- .../plugins/loadSources/loadSources.test.ts | 46 --- .../tests/plugins/rawImport/rawImport.test.ts | 15 +- .../tests/plugins/replace/replace.test.ts | 110 ------ .../tests/plugins/splitCss/splitCss.test.ts | 12 +- .../plugins/translations/translations.test.ts | 6 +- .../tests/utils.test.ts | 6 +- pnpm-lock.yaml | 346 +----------------- 41 files changed, 714 insertions(+), 1152 deletions(-) create mode 100644 packages/ckeditor5-dev-build-tools/src/plugins/declarations.ts delete mode 100644 packages/ckeditor5-dev-build-tools/src/plugins/emitCss.ts delete mode 100644 packages/ckeditor5-dev-build-tools/src/plugins/loadSources.ts delete mode 100644 packages/ckeditor5-dev-build-tools/src/plugins/replace.ts create mode 100644 packages/ckeditor5-dev-build-tools/tests/build/fixtures/src/external-reexport.js create mode 100644 packages/ckeditor5-dev-build-tools/tests/build/fixtures/src/js-extension-import.ts create mode 100644 packages/ckeditor5-dev-build-tools/tests/build/fixtures/src/js-extension-source.js create mode 100644 packages/ckeditor5-dev-build-tools/tests/build/fixtures/src/js-extension-source.ts create mode 100644 packages/ckeditor5-dev-build-tools/tests/index.test.ts create mode 100644 packages/ckeditor5-dev-build-tools/tests/plugins/declarations/declarations.test.ts create mode 100644 packages/ckeditor5-dev-build-tools/tests/plugins/declarations/fixtures/common.cts create mode 100644 packages/ckeditor5-dev-build-tools/tests/plugins/declarations/fixtures/ignored.d.ts create mode 100644 packages/ckeditor5-dev-build-tools/tests/plugins/declarations/fixtures/module.mts create mode 100644 packages/ckeditor5-dev-build-tools/tests/plugins/declarations/fixtures/valid.ts delete mode 100644 packages/ckeditor5-dev-build-tools/tests/plugins/emitCss/emitCss.test.ts delete mode 100644 packages/ckeditor5-dev-build-tools/tests/plugins/loadSources/loadSources.test.ts delete mode 100644 packages/ckeditor5-dev-build-tools/tests/plugins/replace/replace.test.ts diff --git a/packages/ckeditor5-dev-build-tools/package.json b/packages/ckeditor5-dev-build-tools/package.json index 909c8397a..8122eb34c 100644 --- a/packages/ckeditor5-dev-build-tools/package.json +++ b/packages/ckeditor5-dev-build-tools/package.json @@ -1,7 +1,7 @@ { "name": "@ckeditor/ckeditor5-dev-build-tools", "version": "55.5.0", - "description": "Rollup-based tools used to build CKEditor 5 packages.", + "description": "Rolldown-based tools used to build CKEditor 5 packages.", "keywords": [], "author": "CKSource (http://cksource.com/)", "license": "GPL-2.0-or-later", @@ -27,25 +27,16 @@ "ckeditor5-dev-build-tools": "bin/build-project.js" }, "dependencies": { - "@rollup/plugin-commonjs": "^28.0.9", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^16.0.3", - "@rollup/plugin-swc": "^0.4.0", - "@rollup/plugin-terser": "^1.0.0", - "@rollup/plugin-typescript": "12.3.0", "@rollup/pluginutils": "^5.3.0", - "@swc/core": "^1.15.24", "cssnano": "^7.1.4", "cssnano-preset-lite": "^4.0.4", "es-toolkit": "^1.45.1", - "estree-walker": "^3.0.3", "glob": "^13.0.6", "lightningcss": "^1.32.0", "magic-string": "^0.30.21", "pofile": "^1.1.4", "purgecss": "^8.0.0", - "rollup": "^4.60.1", - "rollup-plugin-svg-import": "^3.0.0", + "rolldown": "^1.0.0", "source-map": "^0.7.6", "upath": "^2.0.1" }, @@ -53,7 +44,6 @@ "@types/css": "^0.0.38", "@types/node": "^22.19.17", "@vitest/coverage-v8": "^4.1.2", - "rolldown": "^1.0.0", "type-fest": "^4.41.0", "vitest": "^4.1.2" }, @@ -66,8 +56,6 @@ }, "depcheckIgnore": [ "@types/css", - "@vitest/coverage-v8", - "estree", - "typescript" + "@vitest/coverage-v8" ] } diff --git a/packages/ckeditor5-dev-build-tools/src/build.ts b/packages/ckeditor5-dev-build-tools/src/build.ts index 293552261..ae443d3c4 100644 --- a/packages/ckeditor5-dev-build-tools/src/build.ts +++ b/packages/ckeditor5-dev-build-tools/src/build.ts @@ -7,11 +7,13 @@ import fs from 'node:fs'; import url from 'node:url'; import { styleText, parseArgs } from 'node:util'; import path from 'upath'; -import { rollup, type RollupOutput, type GlobalsOption, type LogLevelOption } from 'rollup'; +import { rolldown, type LogLevelOption, type OutputOptions, type RolldownOutput } from 'rolldown'; import { loadSourcemaps } from './plugins/loadSourcemaps.js'; -import { getRollupConfig } from './config.js'; +import { getRolldownConfig } from './config.js'; import { camelizeObjectKeys, removeWhitespace, getOptionalPlugin } from './utils.js'; +type GlobalsOption = Record | ( ( name: string ) => string ); + export interface BuildOptions { input: string; output: string; @@ -107,20 +109,20 @@ function normalizeGlobalsParameter( globals: GlobalsOption | Array ): Gl /** * Generates `UMD` build based on previous `ESM` build. */ -async function generateUmdBuild( args: BuildOptions, bundle: RollupOutput ): Promise { +async function generateUmdBuild( args: BuildOptions, bundle: RolldownOutput ): Promise { args.input = args.output; const { dir, name } = path.parse( args.output ); // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { plugins, ...config } = await getRollupConfig( args ); + const { plugins, resolve, tsconfig, transform, moduleTypes, output, ...config } = await getRolldownConfig( args ); /** * Ignore the plugins we used for the ESM build. Instead, add a new plugin to not only - * load the source code of the dependencies (which is the default in Rollup for better + * load the source code of the dependencies (which is the default in Rolldown for better * performance), but also their source maps to generate a proper final source map for * the UMD bundle. */ - const build = await rollup( { + const build = await rolldown( { ...config, plugins: [ getOptionalPlugin( @@ -131,9 +133,10 @@ async function generateUmdBuild( args: BuildOptions, bundle: RollupOutput ): Pro } ); const umdBundle = await build.write( { + ...( output as OutputOptions ), format: 'umd', file: path.join( dir, `${ name }.umd.js` ), - inlineDynamicImports: true, + codeSplitting: false, assetFileNames: '[name][extname]', sourcemap: args.sourceMap, name: args.name, @@ -148,7 +151,7 @@ async function generateUmdBuild( args: BuildOptions, bundle: RollupOutput ): Pro ...bundle.output, ...umdBundle.output ] - }; + } as unknown as RolldownOutput; } /** @@ -196,7 +199,7 @@ async function normalizeOptions( options: Partial ): Promise = getCliArguments() -): Promise { +): Promise { try { const args: BuildOptions = await normalizeOptions( options ); @@ -210,22 +213,23 @@ export async function build( } /** - * Create Rollup configuration based on provided arguments. + * Create Rolldown configuration based on provided arguments. */ - const config = await getRollupConfig( args ); + const { output, ...config } = await getRolldownConfig( args ); /** - * Run Rollup to generate bundles. + * Run Rolldown to generate bundles. */ - const build = await rollup( config ); + const build = await rolldown( config ); /** * Write bundles to the filesystem. */ const bundle = await build.write( { + ...( output as OutputOptions ), format: 'esm', file: args.output, - inlineDynamicImports: true, + codeSplitting: false, assetFileNames: '[name][extname]', sourcemap: args.sourceMap, name: args.name @@ -242,7 +246,7 @@ export async function build( } catch ( error: any ) { let message: string; - if ( error.name === 'RollupError' ) { + if ( error.id ) { message = ` ${ styleText( 'red', 'ERROR: Error occurred when processing the file ' + error.id ) }. ${ error.message } diff --git a/packages/ckeditor5-dev-build-tools/src/config.ts b/packages/ckeditor5-dev-build-tools/src/config.ts index 80216b482..604298e5d 100644 --- a/packages/ckeditor5-dev-build-tools/src/config.ts +++ b/packages/ckeditor5-dev-build-tools/src/config.ts @@ -7,32 +7,19 @@ import path from 'upath'; import { existsSync } from 'node:fs'; import { getOptionalPlugin, getUserDependency } from './utils.js'; import type { PackageJson } from 'type-fest'; -import { defineConfig, type Plugin, type RollupOptions } from 'rollup'; +import { defineConfig, type RolldownOptions } from 'rolldown'; import type { BuildOptions } from './build.js'; - -/** - * Rollup plugins - */ -import swc from '@rollup/plugin-swc'; -import json from '@rollup/plugin-json'; -import terser from '@rollup/plugin-terser'; -import svg from 'rollup-plugin-svg-import'; -import commonjs from '@rollup/plugin-commonjs'; -import typescriptPlugin from '@rollup/plugin-typescript'; -import { nodeResolve } from '@rollup/plugin-node-resolve'; import { addBanner } from './plugins/banner.js'; import { bundleCss } from './plugins/bundleCss.js'; -import { emitCss } from './plugins/emitCss.js'; +import { declarationFiles } from './plugins/declarations.js'; import { rawImport } from './plugins/rawImport.js'; -import { replaceImports } from './plugins/replace.js'; import { splitCss } from './plugins/splitCss.js'; -import { loadTypeScriptSources } from './plugins/loadSources.js'; import { translations as translationsPlugin } from './plugins/translations.js'; /** - * Generates Rollup configurations. + * Generates Rolldown configurations. */ -export async function getRollupConfig( options: BuildOptions ): Promise { +export async function getRolldownConfig( options: BuildOptions ): Promise { const { input, output, @@ -53,7 +40,7 @@ export async function getRollupConfig( options: BuildOptions ): Promise getOutputPath( id, { + browser, + configuredRewrites, + coreRewriteSet, + commercialRewriteSet + } ) + }, + experimental: { + nativeMagicString: true, + lazyBarrel: true, + attachDebugInfo: 'none' + }, /** * List of packages that will not be bundled, but their imports will be left as they are. @@ -117,40 +140,6 @@ export async function getRollupConfig( options: BuildOptions ): Promise [ - pkg, - browser ? 'ckeditor5' : `${ pkg }/dist/index.js` - ] as [ string, string ] ), - - ...commercialRewrites.map( pkg => [ - pkg, - browser ? 'ckeditor5-premium-features' : `${ pkg }/dist/index.js` - ] as [ string, string ] ) - ] - } ), - - /** - * Minifies and mangles the output. It also removes all code comments except for license comments. - */ - getOptionalPlugin( minify, terser( { - sourceMap, - format: { - comments: false - } - } ) ), + getOptionalPlugin( + declarations && hasTsconfig, + declarationFiles( { + sourceDirectory: path.dirname( input ) + } ) + ), /** * Adds provided banner to the top of output JavaScript and CSS files. @@ -270,6 +186,52 @@ export async function getRollupConfig( options: BuildOptions ): Promise; + coreRewriteSet: Set; + commercialRewriteSet: Set; + } +): string { + const configuredRewrite = configuredRewrites.get( id ); + + if ( configuredRewrite !== undefined ) { + return configuredRewrite; + } + + const coreSourceImport = id.match( /ckeditor5\/src\/([a-z-]+)(?:[a-z-/.]+)?/ ); + + if ( coreSourceImport ) { + return browser ? 'ckeditor5' : `@ckeditor/ckeditor5-${ coreSourceImport[ 1 ] }/dist/index.js`; + } + + const commercialSourceImport = id.match( /ckeditor5-collaboration\/src\/([a-z-]+)(?:[a-z-/.]+)?/ ); + + if ( commercialSourceImport ) { + return browser ? 'ckeditor5-premium-features' : 'ckeditor5-collaboration/dist/index.js'; + } + + if ( coreRewriteSet.has( id ) ) { + return browser ? 'ckeditor5' : `${ id }/dist/index.js`; + } + + if ( commercialRewriteSet.has( id ) ) { + return browser ? 'ckeditor5-premium-features' : `${ id }/dist/index.js`; + } + + return id; +} + /** * Returns a list of keys in `package.json` file of a given dependency. */ @@ -282,31 +244,3 @@ function getPackageDependencies( packageName: string ): Array { return []; } } - -/** - * Returns the TypeScript plugin if tsconfig file exists, otherwise doesn't return anything. - */ -function getTypeScriptPlugin( { - tsconfig, - output, - sourceMap, - declarations -}: Pick ): Plugin | undefined { - if ( !existsSync( tsconfig ) ) { - return; - } - - return typescriptPlugin( { - noForceEmit: true, - tsconfig, - sourceMap, - inlineSources: sourceMap, // https://github.com/rollup/plugins/issues/260 - typescript: getUserDependency( 'typescript' ), - declaration: declarations, - declarationDir: declarations ? path.parse( output ).dir : undefined, - compilerOptions: { - noEmitOnError: true, - ...( declarations ? { emitDeclarationOnly: true } : { noEmit: true } ) - } - } ); -} diff --git a/packages/ckeditor5-dev-build-tools/src/index.ts b/packages/ckeditor5-dev-build-tools/src/index.ts index 13c7875f8..123183287 100644 --- a/packages/ckeditor5-dev-build-tools/src/index.ts +++ b/packages/ckeditor5-dev-build-tools/src/index.ts @@ -6,10 +6,7 @@ export { build } from './build.js'; export { addBanner, type RollupBannerOptions } from './plugins/banner.js'; export { bundleCss, type RollupBundleCssOptions } from './plugins/bundleCss.js'; -export { emitCss, type RollupEmitCssOptions } from './plugins/emitCss.js'; export { loadSourcemaps } from './plugins/loadSourcemaps.js'; -export { loadTypeScriptSources } from './plugins/loadSources.js'; export { rawImport } from './plugins/rawImport.js'; -export { replaceImports, type RollupReplaceOptions } from './plugins/replace.js'; export { splitCss, type RollupSplitCssOptions } from './plugins/splitCss.js'; export { translations, type RollupTranslationsOptions } from './plugins/translations.js'; diff --git a/packages/ckeditor5-dev-build-tools/src/plugins/banner.ts b/packages/ckeditor5-dev-build-tools/src/plugins/banner.ts index d74466f73..3670a76a6 100644 --- a/packages/ckeditor5-dev-build-tools/src/plugins/banner.ts +++ b/packages/ckeditor5-dev-build-tools/src/plugins/banner.ts @@ -5,8 +5,8 @@ import MagicString from 'magic-string'; import { SourceMapConsumer, SourceMapGenerator } from 'source-map'; +import type { Plugin, OutputChunk, OutputAsset } from 'rolldown'; import { createFilter, type FilterPattern } from '@rollup/pluginutils'; -import type { Plugin, OutputChunk, OutputAsset } from 'rollup'; export interface RollupBannerOptions { @@ -44,13 +44,6 @@ export function addBanner( pluginOptions: RollupBannerOptions ): Plugin { name: 'cke5-add-banner', async generateBundle( outputOptions, bundle ) { - /** - * Source maps cannot be overwritten using `chunk.map`. Instead, - * the old source map must be removed from the bundle, - * and the new source map with the same name must be added. - * - * See: https://github.com/rollup/rollup/issues/4665. - */ const updateSourceMap = async ( fileName: string, magic: MagicString ): Promise => { if ( !outputOptions.sourcemap ) { return; @@ -87,13 +80,7 @@ export function addBanner( pluginOptions: RollupBannerOptions ): Plugin { fileName ); - delete bundle[ sourceMapName ]; - - this.emitFile( { - type: 'asset', - fileName: sourceMapName, - source: generator.toString() - } ); + originalSourceMap.source = generator.toString(); }; /** diff --git a/packages/ckeditor5-dev-build-tools/src/plugins/bundleCss.ts b/packages/ckeditor5-dev-build-tools/src/plugins/bundleCss.ts index 247e39069..84de647c2 100644 --- a/packages/ckeditor5-dev-build-tools/src/plugins/bundleCss.ts +++ b/packages/ckeditor5-dev-build-tools/src/plugins/bundleCss.ts @@ -6,9 +6,8 @@ import { Buffer } from 'node:buffer'; import fs from 'node:fs'; import { basename, dirname, isAbsolute, resolve } from 'node:path'; -import { createFilter } from '@rollup/pluginutils'; import { bundleAsync, Features, type Warning as LightningCssWarning } from 'lightningcss'; -import type { OutputBundle, OutputChunk, Plugin, PluginContext, NormalizedOutputOptions } from 'rollup'; +import type { OutputBundle, OutputChunk, Plugin, PluginContext, NormalizedOutputOptions } from 'rolldown'; export interface RollupBundleCssOptions { @@ -32,7 +31,7 @@ export interface RollupBundleCssOptions { sourceMap?: boolean; } -const filter = createFilter( [ '**/*.css' ] ); +const CSS_ID_REGEXP = /\.css(?:[?#].*)?$/; const VIRTUAL_ENTRY_ID = '/__cke5_bundle_css__.css'; @@ -57,7 +56,7 @@ function createVirtualEntry( filePaths: Array ): { content: string; impo const content = filePaths .map( ( filePath, index ) => { - const importId = `__rollup_bundle_css_${ index }__`; + const importId = `__rolldown_bundle_css_${ index }__`; imports.set( importId, filePath ); @@ -75,7 +74,7 @@ function createVirtualEntry( filePaths: Array ): { content: string; impo * Returns whether the module id points to a CSS file. */ function isCssModule( id: string ): boolean { - return filter( normalizeId( id ) ); + return CSS_ID_REGEXP.test( id ); } /** @@ -105,10 +104,22 @@ function emitLightningCssWarnings( context: PluginContext, warnings: Array { +function getChunkCssImports( + chunk: OutputChunk, + getModuleInfo: PluginContext[ 'getModuleInfo' ], + moduleCssImports: Map> +): Array { const ids: Array = []; for ( const moduleId of Object.keys( chunk.modules ) ) { + const cachedCssImports = moduleCssImports.get( moduleId ); + + if ( cachedCssImports ) { + ids.push( ...cachedCssImports ); + + continue; + } + const traversed = new Set(); let current = [ moduleId ]; @@ -139,6 +150,7 @@ function getChunkCssImports( chunk: OutputChunk, getModuleInfo: PluginContext[ ' current = imports; } + moduleCssImports.set( moduleId, current ); ids.push( ...current ); } @@ -153,6 +165,7 @@ function getOrderedCssModules( outputOptions: NormalizedOutputOptions, getModuleInfo: PluginContext[ 'getModuleInfo' ] ): Array { + const moduleCssImports = new Map>(); const chunks = Object.values( bundle ).filter( ( output ): output is OutputChunk => output.type === 'chunk' ); const manualChunks = chunks.filter( chunk => !chunk.facadeModuleId ); const emittedChunks = outputOptions.preserveModules ? @@ -162,10 +175,10 @@ function getOrderedCssModules( const ids: Array = []; const moved = new Set(); - // Rollup may move modules from entry chunks to manual chunks. + // Rolldown may move modules from entry chunks to manual chunks. // Process manual chunks first to preserve their priority. for ( const chunk of manualChunks ) { - const chunkIds = getChunkCssImports( chunk, getModuleInfo ); + const chunkIds = getChunkCssImports( chunk, getModuleInfo, moduleCssImports ); chunkIds.forEach( id => moved.add( id ) ); @@ -175,7 +188,7 @@ function getOrderedCssModules( // Entry/dynamic chunks can still reference modules already moved above. // Skipping them here keeps ordering stable and prevents duplicates. for ( const chunk of emittedChunks ) { - const chunkIds = getChunkCssImports( chunk, getModuleInfo ); + const chunkIds = getChunkCssImports( chunk, getModuleInfo, moduleCssImports ); ids.push( ...chunkIds.filter( id => !moved.has( id ) ) ); } @@ -200,18 +213,48 @@ export function bundleCss( pluginOptions: RollupBundleCssOptions ): Plugin { styles.clear(); }, - transform( code: string, id: string ): string | undefined { - if ( !isCssModule( id ) ) { - return; + load: { + filter: { + id: CSS_ID_REGEXP + }, + + handler( id: string ) { + const normalizedId = normalizeId( id ); + + this.addWatchFile( normalizedId ); + + return { + code: fs.readFileSync( normalizedId, 'utf-8' ), + moduleType: 'js' + }; } + }, - styles.set( normalizeId( id ), code ); + transform: { + filter: { + id: CSS_ID_REGEXP + }, - return ''; + handler( code: string, id: string ): string | undefined { + styles.set( normalizeId( id ), code ); + + return ''; + } }, async generateBundle( outputOptions, bundle ) { const orderedCssModules = getOrderedCssModules( bundle, outputOptions, this.getModuleInfo ); + + if ( orderedCssModules.length === 0 && !options.sourceMap ) { + this.emitFile( { + type: 'asset', + fileName: options.fileName, + source: '\n' + } ); + + return; + } + // Lightning CSS bundles from a single entry file, so create a virtual one // that imports CSS modules in the desired order. const virtualEntry = createVirtualEntry( orderedCssModules ); @@ -232,7 +275,7 @@ export function bundleCss( pluginOptions: RollupBundleCssOptions ): Plugin { const normalizedPath = normalizeId( filePath ); const transformedStyles = styles.get( normalizedPath ); - // Prefer styles transformed by earlier Rollup plugins. + // Prefer styles transformed by earlier Rolldown plugins. if ( transformedStyles !== undefined ) { return transformedStyles; } @@ -261,15 +304,15 @@ export function bundleCss( pluginOptions: RollupBundleCssOptions ): Plugin { this.error( `External CSS imports are not supported. Found ${ specifier } in ${ normalizedOrigin }.` ); } - // Ask Rollup first so aliases/custom resolvers stay in effect. - const resolvedByRollup = await this.resolve( specifier, normalizedOrigin, { skipSelf: true } ); + // Ask Rolldown first so aliases/custom resolvers stay in effect. + const resolvedByRolldown = await this.resolve( specifier, normalizedOrigin, { skipSelf: true } ); - if ( resolvedByRollup ) { - if ( resolvedByRollup.external ) { + if ( resolvedByRolldown ) { + if ( resolvedByRolldown.external ) { this.error( `External CSS imports are not supported. Found ${ specifier } in ${ normalizedOrigin }.` ); } - return normalizeId( resolvedByRollup.id ); + return normalizeId( resolvedByRolldown.id ); } if ( isAbsolute( specifier ) ) { diff --git a/packages/ckeditor5-dev-build-tools/src/plugins/declarations.ts b/packages/ckeditor5-dev-build-tools/src/plugins/declarations.ts new file mode 100644 index 000000000..79f66bdac --- /dev/null +++ b/packages/ckeditor5-dev-build-tools/src/plugins/declarations.ts @@ -0,0 +1,77 @@ +/** + * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { globSync, readFileSync } from 'node:fs'; +import path from 'upath'; +import { isolatedDeclaration } from 'rolldown/experimental'; +import type { Plugin } from 'rolldown'; + +export interface RolldownDeclarationOptions { + + /** + * Directory containing TypeScript source files. + */ + sourceDirectory: string; +} + +const declarationExtensions = new Set( [ '.mts', '.cts' ] ); + +/** + * Returns TypeScript source files that should have matching declaration files emitted. + */ +function getTypeScriptSourceFiles( directoryPath: string ): Array { + const sourceFileNames = globSync( '**/*.{ts,tsx,mts,cts}', { + cwd: directoryPath, + exclude: [ '**/*.d.ts', '**/*.d.mts', '**/*.d.cts' ] + } ); + + return sourceFileNames.map( file => path.join( directoryPath, file ) ); +} + +/** + * Returns declaration file name for a TypeScript source file. + */ +function getDeclarationFileName( sourceFileName: string ): string { + const { dir, name, ext } = path.parse( sourceFileName ); + const declarationExtension = declarationExtensions.has( ext ) ? ext : '.ts'; + + return path.join( dir, `${ name }.d${ declarationExtension }` ); +} + +/** + * Generates declaration files using isolated declarations. + */ +export function declarationFiles( pluginOptions: RolldownDeclarationOptions ): Plugin { + return { + name: 'emit-declaration-files', + + async generateBundle() { + const sourceFilePaths = getTypeScriptSourceFiles( pluginOptions.sourceDirectory ); + + await Promise.all( sourceFilePaths.map( async sourceFilePath => { + const filename = path.relative( pluginOptions.sourceDirectory, sourceFilePath ); + const source = readFileSync( sourceFilePath, 'utf8' ); + const { errors, code } = await isolatedDeclaration( filename, source, { + sourcemap: false, + stripInternal: true + } ); + + if ( errors.length ) { + const errorMessage = errors + .map( error => [ error.message, error.codeframe ].filter( Boolean ).join( '\n' ) ) + .join( '\n\n' ); + + this.error( `Could not generate a declaration file for "${ filename }".\n${ errorMessage }` ); + } + + this.emitFile( { + type: 'asset', + fileName: getDeclarationFileName( filename ), + source: code + } ); + } ) ); + } + }; +} diff --git a/packages/ckeditor5-dev-build-tools/src/plugins/emitCss.ts b/packages/ckeditor5-dev-build-tools/src/plugins/emitCss.ts deleted file mode 100644 index 902c257c5..000000000 --- a/packages/ckeditor5-dev-build-tools/src/plugins/emitCss.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -import type { Plugin } from 'rollup'; - -export interface RollupEmitCssOptions { - - /** - * Name of the empty CSS files that will be emitted if - * no other CSS files were emitted during the build process. - */ - fileNames: Array; -} - -export function emitCss( pluginOptions: RollupEmitCssOptions ): Plugin { - return { - name: 'cke5-emit-css', - - async generateBundle( outputOptions, bundle ) { - const emittedCss = Object.keys( bundle ); - - for ( const fileName of pluginOptions.fileNames ) { - if ( emittedCss.includes( fileName ) ) { - continue; - } - - this.emitFile( { - type: 'asset', - fileName, - source: '' - } ); - } - } - }; -} diff --git a/packages/ckeditor5-dev-build-tools/src/plugins/loadSourcemaps.ts b/packages/ckeditor5-dev-build-tools/src/plugins/loadSourcemaps.ts index abd3e6cef..4cdb68f7e 100644 --- a/packages/ckeditor5-dev-build-tools/src/plugins/loadSourcemaps.ts +++ b/packages/ckeditor5-dev-build-tools/src/plugins/loadSourcemaps.ts @@ -4,7 +4,7 @@ */ import fs from 'node:fs'; -import type { Plugin } from 'rollup'; +import type { Plugin } from 'rolldown'; export function loadSourcemaps(): Plugin { return { diff --git a/packages/ckeditor5-dev-build-tools/src/plugins/loadSources.ts b/packages/ckeditor5-dev-build-tools/src/plugins/loadSources.ts deleted file mode 100644 index 520939d00..000000000 --- a/packages/ckeditor5-dev-build-tools/src/plugins/loadSources.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -import { accessSync } from 'node:fs'; -import { resolve, dirname } from 'node:path'; -import type { Plugin } from 'rollup'; - -export function loadTypeScriptSources(): Plugin { - const cache: Record = {}; - - return { - name: 'load-typescript-sources', - - resolveId( source: string, importer: string | undefined ) { - if ( !importer || !source.startsWith( '.' ) || !source.endsWith( '.js' ) ) { - return null; - } - - const path = resolve( - dirname( importer ), - source.replace( /\.js$/, '.ts' ) - ); - - if ( cache[ path ] ) { - return cache[ path ]; - } - - try { - accessSync( path ); - cache[ path ] = path; - - return path; - } catch { - cache[ path ] = null; - - return null; - } - } - }; -} diff --git a/packages/ckeditor5-dev-build-tools/src/plugins/rawImport.ts b/packages/ckeditor5-dev-build-tools/src/plugins/rawImport.ts index 037a7a8af..ed48ffcef 100644 --- a/packages/ckeditor5-dev-build-tools/src/plugins/rawImport.ts +++ b/packages/ckeditor5-dev-build-tools/src/plugins/rawImport.ts @@ -5,7 +5,7 @@ import fs from 'node:fs'; import { resolve, dirname } from 'node:path'; -import type { Plugin } from 'rollup'; +import type { Plugin } from 'rolldown'; /** * Allows importing raw file content using the `?raw` query parameter. @@ -16,25 +16,35 @@ export function rawImport(): Plugin { return { name: 'cke5-raw-import', - resolveId( source, importer ) { - if ( !importer || !rawRE.test( source ) ) { - return null; - } + resolveId: { + filter: { + id: rawRE + }, - const cleaned = source.replace( rawRE, '' ); + handler( source, importer ) { + if ( !importer ) { + return null; + } - return resolve( dirname( importer ), cleaned ) + '?raw'; - }, + const cleaned = source.replace( rawRE, '' ); - load( id ) { - if ( !rawRE.test( id ) ) { - return null; + return resolve( dirname( importer ), cleaned ) + '?raw'; } + }, - const [ path ] = id.split( '?' ); - const content = fs.readFileSync( path!, 'utf-8' ); + load: { + filter: { + id: rawRE + }, - return `export default ${ JSON.stringify( content ) };`; + handler( id ) { + const [ path ] = id.split( '?' ); + + return { + code: fs.readFileSync( path!, 'utf-8' ), + moduleType: 'text' + }; + } } }; } diff --git a/packages/ckeditor5-dev-build-tools/src/plugins/replace.ts b/packages/ckeditor5-dev-build-tools/src/plugins/replace.ts deleted file mode 100644 index 8dc99a7f8..000000000 --- a/packages/ckeditor5-dev-build-tools/src/plugins/replace.ts +++ /dev/null @@ -1,116 +0,0 @@ -/** - * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -import { walk, type Node } from 'estree-walker'; -import type { ImportDeclaration, ExportAllDeclaration, ExportNamedDeclaration } from 'estree'; -import MagicString from 'magic-string'; -import type { Plugin } from 'rollup'; - -export interface RollupReplaceOptions { - - /** - * Array containing tuples of pattern and replace value. RegExp must have the `/g` flag. - * - * @example - * [ - * [ 'find', 'replace' ], - * [ /find/g, 'replace' ] - * ] - * - * If the third element is set to `true`, the replacement will be done BEFORE bundling, - * meaning that the import will be replaced in the source code, not only in the resulting bundle. - * - * @default [] - */ - replace: Array<[ RegExp | string, string, true? ]>; -} - -export function replaceImports( pluginOptions: RollupReplaceOptions ): Plugin { - const options: Required = Object.assign( - { replace: [] }, - pluginOptions - ); - - function isModule( node: Node ): node is ImportDeclaration | ExportAllDeclaration | ExportNamedDeclaration { - return [ - 'ImportDeclaration', - 'ExportAllDeclaration', - 'ExportNamedDeclaration' - ].includes( node.type ); - } - - const transformReplace: Array<[ RegExp | string, string ]> = []; - const renderReplace: Array<[ RegExp | string, string ]> = []; - - options.replace.forEach( ( [ pattern, replacement, transformOnly ] ) => { - if ( transformOnly === true ) { - transformReplace.push( [ pattern, replacement ] ); - } else { - renderReplace.push( [ pattern, replacement ] ); - } - } ); - - return { - name: 'cke5-replace-import', - - transform( source ) { - const magic = new MagicString( source ); - - transformReplace.forEach( replace => magic.replaceAll( ...replace ) ); - - return { - code: magic.toString(), - map: magic.generateMap( { - includeContent: true, - hires: 'boundary' - } ) - }; - }, - - renderChunk( source, chunk ) { - const magic = new MagicString( source ); - const ast = this.parse( source ); - - walk( ast as Node, { - enter( node ) { - if ( !isModule( node ) || !node.source ) { - return; - } - - const path = node.source.value as string; - - const replacer = renderReplace.find( ( [ pattern ] ) => { - if ( typeof pattern === 'string' ) { - return pattern === path; - } - - return pattern.test( path ); - } ); - - if ( replacer ) { - magic.overwrite( - ( node.source as any ).start + 1, // Skip opening quote - ( node.source as any ).end - 1, // Skip closing quote - path.replace( ...replacer ) - ); - } - } - } ); - - if ( !magic.hasChanged() ) { - return null; - } - - return { - code: magic.toString(), - map: magic.generateMap( { - source: chunk.fileName, - includeContent: true, - hires: 'boundary' - } ) - }; - } - }; -} diff --git a/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts b/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts index 8d70b759c..9c6f86df6 100644 --- a/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts +++ b/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts @@ -5,7 +5,7 @@ import { Buffer } from 'node:buffer'; import { createFilter } from '@rollup/pluginutils'; -import type { Plugin, OutputBundle, NormalizedOutputOptions, EmittedAsset } from 'rollup'; +import type { Plugin, OutputBundle, NormalizedOutputOptions, EmittedAsset } from 'rolldown'; import cssnano from 'cssnano'; import litePreset from 'cssnano-preset-lite'; import { transform, type Selector, type SelectorComponent } from 'lightningcss'; @@ -32,6 +32,8 @@ type PurgeCSSContent = NonNullable[ number ]; const filter = createFilter( [ '**/*.css' ] ); +const CSS_ID_REGEXP = /\.css(?:[?#].*)?$/; + const REGEX_FOR_REMOVING_VAR_WHITESPACE = /(?<=var\()\s+|\s+(?=\))/g; const CONTENT_PURGE_OPTIONS: PurgeCSSOptions = { @@ -91,12 +93,14 @@ export function splitCss( pluginOptions: RollupSplitCssOptions ): Plugin { return { name: 'cke5-split-css', - transform( code: string, id: string ): string | undefined { - if ( !filter( id ) ) { - return; - } + transform: { + filter: { + id: CSS_ID_REGEXP + }, - return ''; + handler(): string { + return ''; + } }, async generateBundle( output: NormalizedOutputOptions, bundle: OutputBundle ): Promise { @@ -112,25 +116,30 @@ export function splitCss( pluginOptions: RollupSplitCssOptions ): Plugin { const normalizedCss = css.replace( REGEX_FOR_REMOVING_VAR_WHITESPACE, '' ); // Generate stylesheets for editor and content. - const editorStyles = await getStyles( normalizedCss, EDITOR_PURGE_OPTIONS ); - const contentStyles = await getStyles( normalizedCss, { - ...CONTENT_PURGE_OPTIONS, - content: [ - ...createSyntheticContentSelectors( normalizedCss, 'ck-content' ) - ] - } ); + const [ editorStyles, contentStyles ] = await Promise.all( [ + getStyles( normalizedCss, EDITOR_PURGE_OPTIONS ), + getStyles( normalizedCss, { + ...CONTENT_PURGE_OPTIONS, + content: [ + ...createSyntheticContentSelectors( normalizedCss, 'ck-content' ) + ] + } ) + ] ); + const [ editorSource, contentSource ] = options.minimize ? + await Promise.all( [ minifyContent( editorStyles ), minifyContent( contentStyles ) ] ) : + [ editorStyles, contentStyles ]; // Emit those styles into files. this.emitFile( { type: 'asset', fileName: `${ options.baseFileName }-editor.css`, - source: options.minimize ? await minifyContent( editorStyles ) : editorStyles + source: editorSource } ); this.emitFile( { type: 'asset', fileName: `${ options.baseFileName }-content.css`, - source: options.minimize ? await minifyContent( contentStyles ) : contentStyles + source: contentSource } ); } }; diff --git a/packages/ckeditor5-dev-build-tools/src/plugins/translations.ts b/packages/ckeditor5-dev-build-tools/src/plugins/translations.ts index eeed972a8..215752e8d 100644 --- a/packages/ckeditor5-dev-build-tools/src/plugins/translations.ts +++ b/packages/ckeditor5-dev-build-tools/src/plugins/translations.ts @@ -10,7 +10,7 @@ import path from 'upath'; import PO from 'pofile'; import { groupBy, merge } from 'es-toolkit/compat'; import { glob } from 'glob'; -import type { Plugin } from 'rollup'; +import type { Plugin } from 'rolldown'; import { removeWhitespace } from '../utils.js'; const TYPINGS = removeWhitespace( ` diff --git a/packages/ckeditor5-dev-build-tools/src/utils.ts b/packages/ckeditor5-dev-build-tools/src/utils.ts index 97a600355..a842aad7d 100644 --- a/packages/ckeditor5-dev-build-tools/src/utils.ts +++ b/packages/ckeditor5-dev-build-tools/src/utils.ts @@ -5,7 +5,6 @@ import { createRequire } from 'node:module'; import type { CamelCase, CamelCasedProperties } from 'type-fest'; -import type { InputPluginOption } from 'rollup'; const require = createRequire( import.meta.url ); @@ -57,6 +56,6 @@ export function getUserDependency( name: string ): any { /** * Returns plugin if condition is truthy. This is used only to get the types right. */ -export function getOptionalPlugin( condition: unknown, plugin: T ): T | undefined { +export function getOptionalPlugin( condition: unknown, plugin: T ): T | undefined { return condition ? plugin : undefined; } diff --git a/packages/ckeditor5-dev-build-tools/tests/_utils/utils.ts b/packages/ckeditor5-dev-build-tools/tests/_utils/utils.ts index 9bb59e475..9ecf64acf 100644 --- a/packages/ckeditor5-dev-build-tools/tests/_utils/utils.ts +++ b/packages/ckeditor5-dev-build-tools/tests/_utils/utils.ts @@ -4,15 +4,14 @@ */ import { expect, vi } from 'vitest'; -import swc from '@rollup/plugin-swc'; -import type { RollupOutput, OutputChunk, OutputAsset, Plugin } from 'rollup'; +import type { RolldownOutput, OutputChunk, OutputAsset } from 'rolldown'; import * as utils from '../../src/utils.js'; /** - * Helper function for validating Rollup asset. + * Helper function for validating Rolldown asset. */ export function verifyAsset( - output: RollupOutput['output'], + output: RolldownOutput['output'], filename: string, source: string ): void { @@ -24,10 +23,10 @@ export function verifyAsset( } /** - * Helper function for validating Rollup chunk. + * Helper function for validating Rolldown chunk. */ export function verifyChunk( - output: RollupOutput['output'], + output: RolldownOutput['output'], filename: string, code: string ): void { @@ -42,7 +41,7 @@ export function verifyChunk( * Helper function for verifying `CSS` output. */ export function verifyDividedStyleSheet( - output: RollupOutput['output'], + output: RolldownOutput['output'], outputFileName: string, expectedResult: string ): void { @@ -79,20 +78,3 @@ export async function mockGetUserDependency( path: string, cb: () => any ): Prom return actualImport( url ); } ); } - -/** - * Helper function for getting a preconfigured `swc` plugin. - */ -export function swcPlugin(): Plugin { - return swc( { - include: [ '**/*.[jt]s' ], - swc: { - jsc: { - target: 'es2022' - }, - module: { - type: 'es6' - } - } - } ); -} diff --git a/packages/ckeditor5-dev-build-tools/tests/build/arguments.test.ts b/packages/ckeditor5-dev-build-tools/tests/build/arguments.test.ts index 21faf2a5e..8f281ffb9 100644 --- a/packages/ckeditor5-dev-build-tools/tests/build/arguments.test.ts +++ b/packages/ckeditor5-dev-build-tools/tests/build/arguments.test.ts @@ -10,10 +10,14 @@ import * as config from '../../src/config.js'; import { build } from '../../src/build.js'; /** - * Mock `rollup`, so it doesn't try to build anything. + * Mock `rolldown`, so it doesn't try to build anything. */ -vi.mock( 'rollup', () => ( { - rollup() { +vi.mock( 'rolldown', () => ( { + defineConfig( config: any ) { + return config; + }, + + rolldown() { return { write() {} }; @@ -30,19 +34,21 @@ vi.mock( 'fs', () => ( { } ) ); /** - * Mock function for generating rollup configuration. + * Mock function for generating Rolldown configuration. */ -vi.mock( '../../src/config.ts', () => ( { - getRollupConfig() {} +vi.mock( '../../src/config.js', () => ( { + getRolldownConfig() { + return {}; + } } ) ); /** - * Returns spy for the `getRollupConfig` function. + * Returns spy for the `getRolldownConfig` function. */ function getConfigMock() { return vi - .spyOn( config, 'getRollupConfig' ) - .mockImplementationOnce( (): any => { } ); + .spyOn( config, 'getRolldownConfig' ) + .mockImplementationOnce( (): any => ( {} ) ); } /** diff --git a/packages/ckeditor5-dev-build-tools/tests/build/build.test.ts b/packages/ckeditor5-dev-build-tools/tests/build/build.test.ts index 06294fb13..f41c09d33 100644 --- a/packages/ckeditor5-dev-build-tools/tests/build/build.test.ts +++ b/packages/ckeditor5-dev-build-tools/tests/build/build.test.ts @@ -5,23 +5,23 @@ import { test, expect, vi, beforeEach } from 'vitest'; import upath from 'upath'; -import * as Rollup from 'rollup'; +import * as Rolldown from 'rolldown'; import { readFileSync } from 'node:fs'; import { build } from '../../src/build.js'; import { mockGetUserDependency } from '../_utils/utils.js'; /** - * Mock `rollup` to replace `rollup.write` with `rollup.generate`. + * Mock `rolldown` to replace `rolldown.write` with `rolldown.generate`. */ -vi.mock( 'rollup', async () => { - const { rollup, defineConfig } = await vi.importActual( 'rollup' ); +vi.mock( 'rolldown', async () => { + const { rolldown, defineConfig } = await vi.importActual( 'rolldown' ); return { - async rollup( rollupOptions: Rollup.RollupOptions ) { - const build = await rollup( rollupOptions ); + async rolldown( rolldownOptions: Rolldown.RolldownOptions ) { + const build = await rolldown( rolldownOptions ); return { - write: build.generate + write: ( options: Rolldown.OutputOptions ) => build.generate( options ) }; }, @@ -30,10 +30,10 @@ vi.mock( 'rollup', async () => { } ); /** - * Mocks Rollup. + * Mocks Rolldown. */ -function setRollupMock( mock: any ) { - vi.spyOn( Rollup, 'rollup' ).mockImplementationOnce( mock ); +function setRolldownMock( mock: any ) { + vi.spyOn( Rolldown, 'rolldown' ).mockImplementationOnce( mock ); } /** @@ -80,6 +80,13 @@ beforeEach( () => { setProcessCwdMock(); } ); +function expectFileNames( output: Rolldown.RolldownOutput[ 'output' ], fileNames: Array ): void { + const actualFileNames = output.map( o => o.fileName ); + + expect( actualFileNames ).toHaveLength( fileNames.length ); + expect( actualFileNames ).toEqual( expect.arrayContaining( fileNames ) ); +} + /** * Input */ @@ -99,7 +106,7 @@ test( 'TypeScript input', async () => { expect( output[ 0 ].code ).not.contain( 'TestType' ); - expect( output.map( o => o.fileName ) ).toMatchObject( [ + expectFileNames( output, [ 'index.js', 'index.css', 'index-editor.css', @@ -116,15 +123,26 @@ test( 'TypeScript declarations', async () => { expect( output[ 0 ].code ).not.contain( 'TestType' ); - expect( output.map( o => o.fileName ) ).toMatchObject( [ + expectFileNames( output, [ 'index.js', 'index.css', 'index-editor.css', 'index-content.css', - 'input.d.ts' + 'input.d.ts', + 'js-extension-import.d.ts', + 'js-extension-source.d.ts' ] ); } ); +test( 'TypeScript source is preferred when a `.js` import has matching `.ts` and `.js` files', async () => { + const { output } = await build( { + input: 'src/js-extension-import.ts' + } ); + + expect( output[ 0 ].code ).toContain( 'typescript-source' ); + expect( output[ 0 ].code ).not.toContain( 'javascript-source' ); +} ); + /** * Browser */ @@ -135,7 +153,7 @@ test( 'Browser parameter set to `false`', async () => { browser: false } ); - expect( output.map( o => o.fileName ) ).toMatchObject( [ + expectFileNames( output, [ 'index.js', 'index.css', 'index-editor.css', @@ -150,7 +168,7 @@ test( 'Browser parameter set to `true` and name parameter not set', async () => browser: true } ); - expect( output.map( o => o.fileName ) ).toMatchObject( [ + expectFileNames( output, [ 'index.js', 'index.css', 'index-editor.css', @@ -166,7 +184,7 @@ test( 'Browser parameter set to `true` and name parameter set', async () => { name: 'EXAMPLE_OUTPUT_NAME' } ); - expect( output.map( o => o.fileName ) ).toMatchObject( [ + expectFileNames( output, [ 'index.js', 'index.css', 'index-editor.css', @@ -198,7 +216,7 @@ test( 'Output name', async () => { expect( code ).toContain( 'EXAMPLE_OUTPUT_NAME' ); } - expect( output.map( o => o.fileName ) ).toMatchObject( [ + expectFileNames( output, [ 'index.js', 'index.css', 'index-editor.css', @@ -212,7 +230,7 @@ test( 'Custom output name', async () => { input: 'src/input.ts', /** - * Because we mocked rollup to use `rollup.generate` instead of `rollup.write`, the output of the + * Because we mocked rolldown to use `rolldown.generate` instead of `rolldown.write`, the output of the * first ESM build is not saved to the disk, so the UMD build cannot be based on it. That's why the * `output` filename matches the `input` filename. However, because the default output name is `index`, * this is still a valid test. @@ -225,7 +243,7 @@ test( 'Custom output name', async () => { globals: { 'es-toolkit': '_' } } ); - expect( output.map( o => o.fileName ) ).toMatchObject( [ + expectFileNames( output, [ 'input.js', 'input.css', 'input-editor.css', @@ -264,7 +282,7 @@ test( 'Externals', async () => { external: [ 'es-toolkit' ] } ); - expect( output[ 0 ].code ).toContain( 'from \'es-toolkit\'' ); + expect( output[ 0 ].code ).toContain( 'from "es-toolkit"' ); } ); /** @@ -276,9 +294,9 @@ test( 'Translations', async () => { translations: '**/*.po' } ); - expect( ( output[ 1 ] as Rollup.OutputChunk ).code ).toContain( 'Hello world' ); + expect( ( output[ 1 ] as Rolldown.OutputChunk ).code ).toContain( 'Hello world' ); - expect( output.map( o => o.fileName ) ).toMatchObject( [ + expectFileNames( output, [ 'index.js', 'translations/en.js', 'translations/en.umd.js', @@ -298,7 +316,25 @@ test( 'Source map', async () => { sourceMap: true } ); - expect( output.map( o => o.fileName ) ).toMatchObject( [ + expectFileNames( output, [ + 'index.js', + 'index.js.map', + 'index.css.map', + 'index.css', + 'index-editor.css', + 'index-content.css' + ] ); +} ); + +test( 'Source map for chunk re-exporting external modules', async () => { + const { output } = await build( { + input: 'src/external-reexport.js', + external: [ 'external-dependency' ], + sourceMap: true + } ); + const chunk = output.find( item => item.type === 'chunk' && item.fileName === 'index.js' ); + + expectFileNames( output, [ 'index.js', 'index.js.map', 'index.css.map', @@ -306,6 +342,7 @@ test( 'Source map', async () => { 'index-editor.css', 'index-content.css' ] ); + expect( chunk?.type === 'chunk' ? chunk.code : '' ).toContain( 'sourceMappingURL=index.js.map' ); } ); /** @@ -370,7 +407,7 @@ test( 'Overriding', async () => { * Error handling */ test( 'Throws error with nicely formatter message when build fails', async () => { - setRollupMock( () => ( { + setRolldownMock( () => ( { write() { throw new Error( 'REASON' ); } @@ -381,12 +418,11 @@ test( 'Throws error with nicely formatter message when build fails', async () => await expect( fn ).rejects.toThrow( /The build process failed with the following error(.*)REASON/s ); } ); -test( 'Throws Rollup error with nicely formatter message when build fails', async () => { - setRollupMock( () => ( { +test( 'Throws error with file context using nicely formatter message when build fails', async () => { + setRolldownMock( () => ( { write() { const err = new Error() as any; - err.name = 'RollupError'; err.id = 'FILENAME'; err.message = 'REASON'; @@ -399,12 +435,11 @@ test( 'Throws Rollup error with nicely formatter message when build fails', asyn await expect( fn ).rejects.toThrow( /Error occurred when processing the file(.*)FILENAME(.*)REASON/s ); } ); -test( 'Rollup error includes frame if provided', async () => { - setRollupMock( () => ( { +test( 'Error with file context includes frame if provided', async () => { + setRolldownMock( () => ( { write() { const err = new Error() as any; - err.name = 'RollupError'; err.id = 'FILENAME'; err.message = 'REASON'; err.frame = 'FRAME'; @@ -419,7 +454,7 @@ test( 'Rollup error includes frame if provided', async () => { } ); /** - * Mocking real CKE5 packages and test the `Replace' plugin. + * Mocking real CKE5 packages and testing output path rewrites. */ test( 'Replace - export from core (browser = false)', async () => { @@ -435,7 +470,7 @@ test( 'Replace - export from core (browser = false)', async () => { } ); expect( inputFileContent ).toContain( 'export * from \'@ckeditor/ckeditor5-core\'' ); - expect( output[ 0 ].code ).toContain( 'export * from \'@ckeditor/ckeditor5-core/dist/index.js\'' ); + expect( output[ 0 ].code ).toContain( 'export * from "@ckeditor/ckeditor5-core/dist/index.js"' ); } ); test( 'Replace - export from commercial (browser = false)', async () => { @@ -453,7 +488,7 @@ test( 'Replace - export from commercial (browser = false)', async () => { } ); expect( inputFileContent ).toContain( 'export * from \'@ckeditor/ckeditor5-ai\'' ); - expect( output[ 0 ].code ).toContain( 'export * from \'@ckeditor/ckeditor5-ai/dist/index.js\'' ); + expect( output[ 0 ].code ).toContain( 'export * from "@ckeditor/ckeditor5-ai/dist/index.js"' ); } ); test( 'Replace - import from core (browser = true)', async () => { @@ -471,7 +506,7 @@ test( 'Replace - import from core (browser = true)', async () => { } ); expect( inputFileContent ).toContain( 'import { Plugin } from \'ckeditor5/src/core.js\'' ); - expect( output[ 0 ].code ).toContain( 'import { Plugin } from \'ckeditor5\'' ); + expect( output[ 0 ].code ).toContain( 'import { Plugin } from "ckeditor5"' ); } ); test( 'Replace - import from core and from commercial (browser = true)', async () => { @@ -491,16 +526,16 @@ test( 'Replace - import from core and from commercial (browser = true)', async ( } ); expect( inputFileContent ).toContain( 'import { Plugin } from \'ckeditor5/src/core.js\'' ); - expect( output[ 0 ].code ).toContain( 'import { Plugin } from \'ckeditor5\'' ); + expect( output[ 0 ].code ).toContain( 'import { Plugin } from "ckeditor5"' ); expect( inputFileContent ).toContain( 'import { Command } from \'@ckeditor/ckeditor5-core\'' ); - expect( output[ 0 ].code ).toContain( 'import { Plugin } from \'ckeditor5\'' ); + expect( output[ 0 ].code ).toContain( 'import { Command } from "ckeditor5"' ); expect( inputFileContent ).toContain( 'import { AIAssistant } from \'@ckeditor/ckeditor5-ai\'' ); - expect( output[ 0 ].code ).toContain( 'import { AIAssistant } from \'ckeditor5-premium-features\'' ); + expect( output[ 0 ].code ).toContain( 'import { AIAssistant } from "ckeditor5-premium-features"' ); expect( inputFileContent ).toContain( 'import { Users } from \'ckeditor5-collaboration/src/collaboration-core.js\'' ); - expect( output[ 0 ].code ).toContain( 'import { Users } from \'ckeditor5-premium-features\'' ); + expect( output[ 0 ].code ).toContain( 'import { Users } from "ckeditor5-premium-features"' ); } ); test( 'should not throw when processing a file including a dynamic import expression', async () => { diff --git a/packages/ckeditor5-dev-build-tools/tests/build/fixtures/src/external-reexport.js b/packages/ckeditor5-dev-build-tools/tests/build/fixtures/src/external-reexport.js new file mode 100644 index 000000000..cb760e725 --- /dev/null +++ b/packages/ckeditor5-dev-build-tools/tests/build/fixtures/src/external-reexport.js @@ -0,0 +1,6 @@ +/** + * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +export * from 'external-dependency'; diff --git a/packages/ckeditor5-dev-build-tools/tests/build/fixtures/src/js-extension-import.ts b/packages/ckeditor5-dev-build-tools/tests/build/fixtures/src/js-extension-import.ts new file mode 100644 index 000000000..0f5043a1b --- /dev/null +++ b/packages/ckeditor5-dev-build-tools/tests/build/fixtures/src/js-extension-import.ts @@ -0,0 +1,8 @@ +/** + * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { value } from './js-extension-source.js'; + +export const result: string = value; diff --git a/packages/ckeditor5-dev-build-tools/tests/build/fixtures/src/js-extension-source.js b/packages/ckeditor5-dev-build-tools/tests/build/fixtures/src/js-extension-source.js new file mode 100644 index 000000000..e2cef6eec --- /dev/null +++ b/packages/ckeditor5-dev-build-tools/tests/build/fixtures/src/js-extension-source.js @@ -0,0 +1,6 @@ +/** + * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +export const value = 'javascript-source'; diff --git a/packages/ckeditor5-dev-build-tools/tests/build/fixtures/src/js-extension-source.ts b/packages/ckeditor5-dev-build-tools/tests/build/fixtures/src/js-extension-source.ts new file mode 100644 index 000000000..e1f2c4d44 --- /dev/null +++ b/packages/ckeditor5-dev-build-tools/tests/build/fixtures/src/js-extension-source.ts @@ -0,0 +1,6 @@ +/** + * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +export const value = 'typescript-source'; diff --git a/packages/ckeditor5-dev-build-tools/tests/config/config.test.ts b/packages/ckeditor5-dev-build-tools/tests/config/config.test.ts index 35b056ba3..501b17921 100644 --- a/packages/ckeditor5-dev-build-tools/tests/config/config.test.ts +++ b/packages/ckeditor5-dev-build-tools/tests/config/config.test.ts @@ -4,11 +4,11 @@ */ import { test, expect } from 'vitest'; -import { getRollupConfig } from '../../src/config.js'; +import { getRolldownConfig } from '../../src/config.js'; import { mockGetUserDependency } from '../_utils/utils.js'; -import type { Plugin } from 'rollup'; +import type { OutputOptions, Plugin, RolldownOptions } from 'rolldown'; -type Options = Parameters[0]; +type Options = Parameters[0]; const defaults: Options = { input: 'src/index.js', @@ -29,8 +29,18 @@ const defaults: Options = { cwd: '' }; -function getConfig( config: Partial = {} ): ReturnType { - return getRollupConfig( Object.assign( {}, defaults, config ) ); +function getConfig( config: Partial = {} ): ReturnType { + return getRolldownConfig( Object.assign( {}, defaults, config ) ); +} + +function getOutputPath( config: RolldownOptions, id: string ): string { + const paths = ( config.output as OutputOptions ).paths; + + if ( typeof paths === 'function' ) { + return paths( id ); + } + + return paths?.[ id ] ?? id; } test( '--input', async () => { @@ -54,23 +64,13 @@ test( '--tsconfig', async () => { declarations: false } ); - expect( ( fileExists.plugins as Array ).some( plugin => plugin?.name === 'typescript' ) ).toBe( true ); - expect( ( fileDoesntExist.plugins as Array ).some( plugin => plugin?.name === 'typescript' ) ).toBe( false ); - expect( ( declarationsFalse.plugins as Array ).some( plugin => plugin?.name === 'typescript' ) ).toBe( true ); -} ); + expect( ( fileExists.plugins as Array ).some( plugin => plugin?.name === 'emit-declaration-files' ) ).toBe( true ); + expect( ( fileDoesntExist.plugins as Array ).some( plugin => plugin?.name === 'emit-declaration-files' ) ).toBe( false ); + expect( ( declarationsFalse.plugins as Array ).some( plugin => plugin?.name === 'emit-declaration-files' ) ).toBe( false ); -test( '--tsconfig should do typechecking regardless of --declarations', async () => { - const withDeclarations = await getConfig( { - tsconfig: process.cwd() + '/tests/config/fixtures/tsconfig.fixture.json', - declarations: true - } ); - const withoutDeclarations = await getConfig( { - tsconfig: process.cwd() + '/tests/config/fixtures/tsconfig.fixture.json', - declarations: false - } ); - - expect( ( withDeclarations.plugins as Array ).some( plugin => plugin?.name === 'typescript' ) ).toBe( true ); - expect( ( withoutDeclarations.plugins as Array ).some( plugin => plugin?.name === 'typescript' ) ).toBe( true ); + expect( fileExists.tsconfig ).toBe( process.cwd() + '/tests/config/fixtures/tsconfig.fixture.json' ); + expect( fileDoesntExist.tsconfig ).toBeUndefined(); + expect( declarationsFalse.tsconfig ).toBe( process.cwd() + '/tests/config/fixtures/tsconfig.fixture.json' ); } ); test( '--external', async () => { @@ -120,6 +120,9 @@ test( '--external automatically adds packages that make up the "ckeditor5"', asy expect( ( config.external as Function )( 'ckeditor5/src/ui.js' ) ).toBe( true ); expect( ( config.external as Function )( '@ckeditor/ckeditor5-core' ) ).toBe( true ); expect( ( config.external as Function )( '@ckeditor/ckeditor5-code-block/theme/codeblock.css' ) ).toBe( false ); + + expect( getOutputPath( config, 'ckeditor5/src/ui.js' ) ).toBe( '@ckeditor/ckeditor5-ui/dist/index.js' ); + expect( getOutputPath( config, '@ckeditor/ckeditor5-core' ) ).toBe( '@ckeditor/ckeditor5-core/dist/index.js' ); } ); test( '--external automatically adds packages that make up the "ckeditor5-premium-features"', async () => { @@ -143,6 +146,53 @@ test( '--external automatically adds packages that make up the "ckeditor5-premiu expect( ( config.external as Function )( 'ckeditor5-collaboration/src/collaboration-core.js' ) ).toBe( true ); expect( ( config.external as Function )( '@ckeditor/ckeditor5-case-change' ) ).toBe( true ); expect( ( config.external as Function )( '@ckeditor/ckeditor5-real-time-collaboration/theme/usermarkers.css' ) ).toBe( false ); + + expect( getOutputPath( config, 'ckeditor5-collaboration/src/collaboration-core.js' ) ).toBe( 'ckeditor5-collaboration/dist/index.js' ); + expect( getOutputPath( config, '@ckeditor/ckeditor5-case-change' ) ).toBe( '@ckeditor/ckeditor5-case-change/dist/index.js' ); +} ); + +test( '--external rewrites CKEditor paths to aggregate packages in browser builds', async () => { + await mockGetUserDependency( + 'ckeditor5/package.json', + () => ( { + name: 'ckeditor5', + dependencies: { + '@ckeditor/ckeditor5-core': '*' + } + } ) + ); + await mockGetUserDependency( + 'ckeditor5-premium-features/package.json', + () => ( { + name: 'ckeditor5-premium-features', + dependencies: { + '@ckeditor/ckeditor5-ai': '*', + 'ckeditor5-collaboration': '*' + } + } ) + ); + + const config = await getConfig( { + external: [ + 'ckeditor5', + 'ckeditor5-premium-features' + ], + browser: true + } ); + + expect( getOutputPath( config, 'ckeditor5/src/core.js' ) ).toBe( 'ckeditor5' ); + expect( getOutputPath( config, '@ckeditor/ckeditor5-core' ) ).toBe( 'ckeditor5' ); + expect( getOutputPath( config, 'ckeditor5-collaboration/src/collaboration-core.js' ) ).toBe( 'ckeditor5-premium-features' ); + expect( getOutputPath( config, '@ckeditor/ckeditor5-ai' ) ).toBe( 'ckeditor5-premium-features' ); +} ); + +test( '--rewrite maps output paths', async () => { + const config = await getConfig( { + rewrite: [ [ 'dependency', 'dependency/dist/index.js' ] ] + } ); + + expect( getOutputPath( config, 'dependency' ) ).toBe( 'dependency/dist/index.js' ); + expect( getOutputPath( config, 'unmatched-dependency' ) ).toBe( 'unmatched-dependency' ); } ); test( '--external doesn\'t fail when "ckeditor5-premium-features" is not installed', async () => { @@ -176,6 +226,6 @@ test( '--minify', async () => { minify: true } ); - expect( ( withoutMinification.plugins as Array ).some( plugin => plugin?.name === 'terser' ) ).toBe( false ); - expect( ( withMinification.plugins as Array ).some( plugin => plugin?.name === 'terser' ) ).toBe( true ); + expect( withoutMinification.output ).toMatchObject( { minify: false } ); + expect( withMinification.output ).toMatchObject( { minify: true } ); } ); diff --git a/packages/ckeditor5-dev-build-tools/tests/index.test.ts b/packages/ckeditor5-dev-build-tools/tests/index.test.ts new file mode 100644 index 000000000..acba9c060 --- /dev/null +++ b/packages/ckeditor5-dev-build-tools/tests/index.test.ts @@ -0,0 +1,17 @@ +/** + * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { expect, test } from 'vitest'; +import * as buildTools from '../src/index.js'; + +test( 'exports the public build tools API', () => { + expect( buildTools.build ).toBeTypeOf( 'function' ); + expect( buildTools.addBanner ).toBeTypeOf( 'function' ); + expect( buildTools.bundleCss ).toBeTypeOf( 'function' ); + expect( buildTools.loadSourcemaps ).toBeTypeOf( 'function' ); + expect( buildTools.rawImport ).toBeTypeOf( 'function' ); + expect( buildTools.splitCss ).toBeTypeOf( 'function' ); + expect( buildTools.translations ).toBeTypeOf( 'function' ); +} ); diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/banner/banner.test.ts b/packages/ckeditor5-dev-build-tools/tests/plugins/banner/banner.test.ts index e49e79cee..e095ee4be 100644 --- a/packages/ckeditor5-dev-build-tools/tests/plugins/banner/banner.test.ts +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/banner/banner.test.ts @@ -5,8 +5,8 @@ import { join } from 'node:path'; import { test, expect, vi } from 'vitest'; -import { rollup, type RollupOutput, type OutputAsset, type Plugin } from 'rollup'; -import { swcPlugin, verifyAsset, verifyChunk } from '../../_utils/utils.js'; +import { rolldown, type RolldownOutput, type OutputAsset, type Plugin } from 'rolldown'; +import { verifyAsset, verifyChunk } from '../../_utils/utils.js'; import { addBanner, bundleCss, type RollupBannerOptions } from '../../../src/index.js'; const createFilterSpy = vi.hoisted( vi.fn ); @@ -26,12 +26,10 @@ vi.mock( '@rollup/pluginutils', async importOriginal => { /** * Helper function for creating a bundle that won't be written to the file system. */ -async function generateBundle( options: RollupBannerOptions, sourcemap: boolean = false ): Promise { - const bundle = await rollup( { +async function generateBundle( options: RollupBannerOptions, sourcemap: boolean = false ): Promise { + const bundle = await rolldown( { input: join( import.meta.dirname, './fixtures/input.ts' ), plugins: [ - swcPlugin, - bundleCss( { fileName: 'styles.css' } ), @@ -134,10 +132,9 @@ test( 'Should have proper default values', async () => { test( 'Handles source maps without the "file" property', async () => { const banner = '/* CUSTOM BANNER */\n'; - const bundle = await rollup( { + const bundle = await rolldown( { input: join( import.meta.dirname, './fixtures/input.ts' ), plugins: [ - swcPlugin, emitMapWithoutFileProperty(), bundleCss( { fileName: 'styles.css' diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/bundleCss/bundleCss.mocked.test.ts b/packages/ckeditor5-dev-build-tools/tests/plugins/bundleCss/bundleCss.mocked.test.ts index 966d4a37c..0eca36683 100644 --- a/packages/ckeditor5-dev-build-tools/tests/plugins/bundleCss/bundleCss.mocked.test.ts +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/bundleCss/bundleCss.mocked.test.ts @@ -5,7 +5,7 @@ import { Buffer } from 'node:buffer'; import { expect, test, vi } from 'vitest'; -import type { ModuleInfo, NormalizedOutputOptions, OutputBundle, OutputChunk, PartialResolvedId } from 'rollup'; +import type { ModuleInfo, NormalizedOutputOptions, OutputBundle, OutputChunk, PartialResolvedId } from 'rolldown'; const bundleAsyncMock = vi.hoisted( () => vi.fn() ); @@ -97,7 +97,10 @@ test( 'Processes CSS collected from manual chunks before entry chunks', async () }; } ); - const plugin = bundleCss( { fileName: 'styles.css' } ); + const plugin = bundleCss( { + fileName: 'styles.css', + sourceMap: true + } ); const context = createContext( { moduleInfo: { '/src/shared.ts': { importedIds: [ '/styles/shared.css' ] }, @@ -109,7 +112,7 @@ test( 'Processes CSS collected from manual chunks before entry chunks', async () facadeModuleId: null, isEntry: false } ), - 'main.js': createChunk( 'main', [ '/src/main.ts' ] ) + 'main.js': createChunk( 'main', [ '/src/shared.ts', '/src/main.ts' ] ) }; await runGenerateBundle( plugin, context, { @@ -134,7 +137,10 @@ test( 'Throws when a generated virtual stylesheet import cannot be resolved', as }; } ); - const plugin = bundleCss( { fileName: 'styles.css' } ); + const plugin = bundleCss( { + fileName: 'styles.css', + sourceMap: true + } ); await runGenerateBundle( plugin, createContext(), { file: '/dist/main.js', @@ -142,7 +148,7 @@ test( 'Throws when a generated virtual stylesheet import cannot be resolved', as } as NormalizedOutputOptions, {} ); } ); -test( 'Throws when Rollup resolves a CSS import as external', async () => { +test( 'Throws when Rolldown resolves a CSS import as external', async () => { bundleAsyncMock.mockImplementationOnce( async options => { await expect( options.resolver.resolve( './external.css', '/src/source.css' ) ) .rejects.toThrow( 'External CSS imports are not supported. Found ./external.css in /src/source.css.' ); @@ -153,7 +159,10 @@ test( 'Throws when Rollup resolves a CSS import as external', async () => { }; } ); - const plugin = bundleCss( { fileName: 'styles.css' } ); + const plugin = bundleCss( { + fileName: 'styles.css', + sourceMap: true + } ); const context = createContext( { resolve: () => ( { id: '/styles/external.css', @@ -167,7 +176,7 @@ test( 'Throws when Rollup resolves a CSS import as external', async () => { } as NormalizedOutputOptions, {} ); } ); -test( 'Resolves absolute and relative CSS imports without Rollup results', async () => { +test( 'Resolves absolute and relative CSS imports without Rolldown results', async () => { bundleAsyncMock.mockImplementationOnce( async options => { expect( await options.resolver.resolve( '/styles/absolute.css', '/src/components/source.css' ) ) .toBe( '/styles/absolute.css' ); @@ -180,7 +189,10 @@ test( 'Resolves absolute and relative CSS imports without Rollup results', async }; } ); - const plugin = bundleCss( { fileName: 'styles.css' } ); + const plugin = bundleCss( { + fileName: 'styles.css', + sourceMap: true + } ); await runGenerateBundle( plugin, createContext(), { file: '/dist/main.js', @@ -199,10 +211,42 @@ test( 'Throws when a non-relative CSS import cannot be resolved', async () => { }; } ); - const plugin = bundleCss( { fileName: 'styles.css' } ); + const plugin = bundleCss( { + fileName: 'styles.css', + sourceMap: true + } ); await runGenerateBundle( plugin, createContext(), { file: '/dist/main.js', preserveModules: false } as NormalizedOutputOptions, {} ); } ); + +test( 'Emits warnings for virtual entry diagnostics without a warning type', async () => { + bundleAsyncMock.mockImplementationOnce( async () => ( { + code: Buffer.from( '' ), + warnings: [ { + loc: { + filename: '/__cke5_bundle_css__.css', + line: 3, + column: 4 + }, + message: 'Virtual entry warning' + } ] + } ) ); + + const plugin = bundleCss( { + fileName: 'styles.css', + sourceMap: true + } ); + const context = createContext(); + + await runGenerateBundle( plugin, context, { + file: '/dist/main.js', + preserveModules: false + } as NormalizedOutputOptions, {} ); + + expect( context.warn ).toHaveBeenCalledWith( + 'Lightning CSS warning in styles.css:3:5: Virtual entry warning' + ); +} ); diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/bundleCss/bundleCss.test.ts b/packages/ckeditor5-dev-build-tools/tests/plugins/bundleCss/bundleCss.test.ts index c0ccb98c7..20d03ff5c 100644 --- a/packages/ckeditor5-dev-build-tools/tests/plugins/bundleCss/bundleCss.test.ts +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/bundleCss/bundleCss.test.ts @@ -5,9 +5,7 @@ import { join } from 'node:path'; import { test, expect } from 'vitest'; -import { rollup, type RollupOutput, type OutputAsset, type OutputOptions, type Plugin } from 'rollup'; -import { nodeResolve } from '@rollup/plugin-node-resolve'; -import { swcPlugin } from '../../_utils/utils.js'; +import { rolldown, type RolldownOutput, type OutputAsset, type OutputOptions, type Plugin } from 'rolldown'; import { bundleCss, type RollupBundleCssOptions } from '../../../src/index.js'; async function generateBundle( @@ -15,11 +13,13 @@ async function generateBundle( input: string = './fixtures/input.ts', plugins: Array = [], outputOptions: OutputOptions = {} -): Promise { - const bundle = await rollup( { +): Promise { + const bundle = await rolldown( { input: join( import.meta.dirname, input ), + resolve: { + modules: [ join( import.meta.dirname, './fixtures/packages' ), 'node_modules' ] + }, plugins: [ - swcPlugin, ...plugins, bundleCss( options ) ] @@ -40,7 +40,7 @@ async function generateBundle( return output; } -function getAsset( output: RollupOutput[ 'output' ], fileName: string ): OutputAsset { +function getAsset( output: RolldownOutput[ 'output' ], fileName: string ): OutputAsset { const asset = output.find( output => output.fileName === fileName ); expect( asset ).toBeDefined(); @@ -127,15 +127,10 @@ test( 'Uses transformed CSS code from previous plugins', async () => { expect( stylesheet ).toContain( '.transformed-by-plugin' ); } ); -test( 'Resolves package CSS imports via Rollup resolution', async () => { +test( 'Resolves package CSS imports via Rolldown resolution', async () => { const output = await generateBundle( { fileName: 'styles.css' - }, './fixtures/input-package-import.ts', [ - nodeResolve( { - extensions: [ '.ts', '.js', '.css' ], - modulePaths: [ join( import.meta.dirname, './fixtures/packages' ) ] - } ) - ] ); + }, './fixtures/input-package-import.ts' ); const stylesheet = getAsset( output, 'styles.css' ).source.toString(); @@ -143,16 +138,15 @@ test( 'Resolves package CSS imports via Rollup resolution', async () => { expect( stylesheet ).toContain( '.package-import-local' ); } ); -test( 'Emits Lightning CSS warnings through Rollup warnings', async () => { +test( 'Emits Lightning CSS warnings through Rolldown warnings', async () => { const warnings: Array = []; - const bundle = await rollup( { + const bundle = await rolldown( { input: join( import.meta.dirname, './fixtures/input-warning.ts' ), onwarn( warning ) { warnings.push( warning.message ); }, plugins: [ - swcPlugin, bundleCss( { fileName: 'styles.css' } ) diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/declarations/declarations.test.ts b/packages/ckeditor5-dev-build-tools/tests/plugins/declarations/declarations.test.ts new file mode 100644 index 000000000..3db382df3 --- /dev/null +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/declarations/declarations.test.ts @@ -0,0 +1,81 @@ +/** + * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { join } from 'node:path'; +import { expect, test, vi } from 'vitest'; + +const isolatedDeclarationMock = vi.hoisted( () => vi.fn() ); + +vi.mock( 'rolldown/experimental', () => ( { + isolatedDeclaration: isolatedDeclarationMock +} ) ); + +import { declarationFiles } from '../../../src/plugins/declarations.js'; + +async function runGenerateBundle( plugin: ReturnType, context: any ) { + const generateBundle = typeof plugin.generateBundle == 'function' ? plugin.generateBundle : plugin.generateBundle!.handler; + + await generateBundle.call( context, {} as any, {}, false ); +} + +function createContext() { + const emittedFiles: Array = []; + + return { + emitFile: vi.fn( file => emittedFiles.push( file ) ), + error: ( message: string ) => { + throw new Error( message ); + }, + emittedFiles + }; +} + +test( 'Emits declaration files for TypeScript source files', async () => { + isolatedDeclarationMock.mockResolvedValue( { + errors: [], + code: 'export declare const value: string;' + } ); + + const plugin = declarationFiles( { + sourceDirectory: join( import.meta.dirname, './fixtures' ) + } ); + const context = createContext(); + + await runGenerateBundle( plugin, context ); + + expect( context.emittedFiles ).toContainEqual( { + type: 'asset', + fileName: 'valid.d.ts', + source: 'export declare const value: string;' + } ); + expect( context.emittedFiles ).toContainEqual( { + type: 'asset', + fileName: 'module.d.mts', + source: 'export declare const value: string;' + } ); + expect( context.emittedFiles ).toContainEqual( { + type: 'asset', + fileName: 'common.d.cts', + source: 'export declare const value: string;' + } ); +} ); + +test( 'Throws formatted declaration generation errors', async () => { + isolatedDeclarationMock.mockResolvedValue( { + errors: [ { + message: 'Declaration error', + codeframe: 'const value = unknown;' + } ], + code: '' + } ); + + const plugin = declarationFiles( { + sourceDirectory: join( import.meta.dirname, './fixtures' ) + } ); + + await expect( runGenerateBundle( plugin, createContext() ) ).rejects.toThrow( + /Could not generate a declaration file for ".+"\.\nDeclaration error\nconst value = unknown;/ + ); +} ); diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/declarations/fixtures/common.cts b/packages/ckeditor5-dev-build-tools/tests/plugins/declarations/fixtures/common.cts new file mode 100644 index 000000000..26a49ade0 --- /dev/null +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/declarations/fixtures/common.cts @@ -0,0 +1 @@ +export const value = 'common'; diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/declarations/fixtures/ignored.d.ts b/packages/ckeditor5-dev-build-tools/tests/plugins/declarations/fixtures/ignored.d.ts new file mode 100644 index 000000000..8e0f9b733 --- /dev/null +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/declarations/fixtures/ignored.d.ts @@ -0,0 +1,6 @@ +/** + * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +export declare const ignored: string; diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/declarations/fixtures/module.mts b/packages/ckeditor5-dev-build-tools/tests/plugins/declarations/fixtures/module.mts new file mode 100644 index 000000000..617ddc40f --- /dev/null +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/declarations/fixtures/module.mts @@ -0,0 +1 @@ +export const value = 'module'; diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/declarations/fixtures/valid.ts b/packages/ckeditor5-dev-build-tools/tests/plugins/declarations/fixtures/valid.ts new file mode 100644 index 000000000..612488034 --- /dev/null +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/declarations/fixtures/valid.ts @@ -0,0 +1,6 @@ +/** + * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +export const value = 'valid'; diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/emitCss/emitCss.test.ts b/packages/ckeditor5-dev-build-tools/tests/plugins/emitCss/emitCss.test.ts deleted file mode 100644 index ee74d4b4f..000000000 --- a/packages/ckeditor5-dev-build-tools/tests/plugins/emitCss/emitCss.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -import { join } from 'node:path'; -import { test } from 'vitest'; -import { rollup, type RollupOutput } from 'rollup'; -import { swcPlugin, verifyAsset } from '../../_utils/utils.js'; - -import { emitCss, bundleCss } from '../../../src/index.js'; - -async function generateBundle( input: string ): Promise { - const bundle = await rollup( { - input: join( import.meta.dirname, input ), - plugins: [ - swcPlugin, - - bundleCss( { - fileName: 'styles.css' - } ), - - emitCss( { - fileNames: [ 'styles.css' ] - } ) - ] - } ); - - const { output } = await bundle.generate( { - format: 'esm', - file: 'input.js', - assetFileNames: '[name][extname]' - } ); - - return output; -} - -test( 'Emits file if it wasn\'t already', async () => { - const output = await generateBundle( './fixtures/input.ts' ); - - verifyAsset( output, 'styles.css', '' ); -} ); - -test( 'Doesn\'t override file if it was emitted', async () => { - const output = await generateBundle( './fixtures/input-css.ts' ); - - verifyAsset( output, 'styles.css', 'div' ); -} ); diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/loadSourcemaps/loadSourcemaps.test.ts b/packages/ckeditor5-dev-build-tools/tests/plugins/loadSourcemaps/loadSourcemaps.test.ts index 78da285a8..9e607e74a 100644 --- a/packages/ckeditor5-dev-build-tools/tests/plugins/loadSourcemaps/loadSourcemaps.test.ts +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/loadSourcemaps/loadSourcemaps.test.ts @@ -5,19 +5,15 @@ import { join } from 'node:path'; import { test, expect } from 'vitest'; -import { rollup, type RollupOutput, type OutputAsset } from 'rollup'; -import { nodeResolve } from '@rollup/plugin-node-resolve'; -import { swcPlugin } from '../../_utils/utils.js'; +import { rolldown, type RolldownOutput, type OutputAsset } from 'rolldown'; import { loadSourcemaps } from '../../../src/index.js'; import { getOptionalPlugin } from '../../../src/utils.js'; -async function generateBundle( input: string, sourcemap: boolean = false ): Promise { - const bundle = await rollup( { +async function generateBundle( input: string, sourcemap: boolean = false ): Promise { + const bundle = await rolldown( { input: join( import.meta.dirname, input ), plugins: [ - nodeResolve(), - swcPlugin, getOptionalPlugin( sourcemap, loadSourcemaps() ) ] } ); diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/loadSources/loadSources.test.ts b/packages/ckeditor5-dev-build-tools/tests/plugins/loadSources/loadSources.test.ts deleted file mode 100644 index 2637818b8..000000000 --- a/packages/ckeditor5-dev-build-tools/tests/plugins/loadSources/loadSources.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -import { join } from 'node:path'; -import { test } from 'vitest'; -import { rollup, type RollupOutput } from 'rollup'; -import { swcPlugin, verifyChunk } from '../../_utils/utils.js'; - -import { loadTypeScriptSources } from '../../../src/index.js'; - -/** - * Helper function for creating a bundle that won't be written to the file system. - */ -async function generateBundle( enabled = true ): Promise { - const bundle = await rollup( { - input: join( import.meta.dirname, './fixtures/input.ts' ), - plugins: [ - swcPlugin, - - enabled && loadTypeScriptSources() - ] - } ); - - const { output } = await bundle.generate( { - format: 'esm', - file: 'input.js', - assetFileNames: '[name][extname]' - } ); - - return output; -} - -test( 'Prioritizes `.ts` files over `.js` files', async () => { - const output = await generateBundle(); - - verifyChunk( output, 'input.js', '123' ); -} ); - -test( 'When not enabled, prioritizes `.js` files over `.ts` files', async () => { - const output = await generateBundle( false ); - - verifyChunk( output, 'input.js', '456' ); -} ); - diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/rawImport/rawImport.test.ts b/packages/ckeditor5-dev-build-tools/tests/plugins/rawImport/rawImport.test.ts index 89ae4a229..3c5791d38 100644 --- a/packages/ckeditor5-dev-build-tools/tests/plugins/rawImport/rawImport.test.ts +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/rawImport/rawImport.test.ts @@ -4,8 +4,8 @@ */ import upath from 'upath'; -import { test } from 'vitest'; -import { rollup, type RollupOutput } from 'rollup'; +import { test, expect } from 'vitest'; +import { rolldown, type RolldownOutput } from 'rolldown'; import { verifyChunk } from '../../_utils/utils.js'; import { rawImport } from '../../../src/index.js'; @@ -13,8 +13,8 @@ import { rawImport } from '../../../src/index.js'; /** * Helper function for creating a bundle that won't be written to the file system. */ -async function generateBundle(): Promise { - const bundle = await rollup( { +async function generateBundle(): Promise { + const bundle = await rolldown( { input: upath.join( import.meta.dirname, '/fixtures/input.js' ), plugins: [ rawImport() @@ -41,3 +41,10 @@ test( 'can import raw file content using the `?raw` query parameter', async () = // Content from `dependency.txt` file. verifyChunk( output, 'input.js', 'Hello World!' ); } ); + +test( 'does not resolve raw imports without importer context', () => { + const plugin = rawImport(); + const resolveId = typeof plugin.resolveId == 'function' ? plugin.resolveId : plugin.resolveId!.handler; + + expect( resolveId.call( {}, './dependency.css?raw', undefined, {} as any ) ).toBeNull(); +} ); diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/replace/replace.test.ts b/packages/ckeditor5-dev-build-tools/tests/plugins/replace/replace.test.ts deleted file mode 100644 index e79259feb..000000000 --- a/packages/ckeditor5-dev-build-tools/tests/plugins/replace/replace.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -import { join } from 'node:path'; -import { expect, test } from 'vitest'; -import { rollup, type RollupOutput, type OutputAsset } from 'rollup'; -import { verifyChunk } from '../../_utils/utils.js'; - -import { replaceImports, type RollupReplaceOptions } from '../../../src/index.js'; - -async function generateBundle( - options: RollupReplaceOptions, - sourcemap?: boolean -): Promise { - const bundle = await rollup( { - input: join( import.meta.dirname, './fixtures/input.js' ), - external: [ - 'node:fs' - ], - plugins: [ - replaceImports( options ) - ] - } ); - - const { output } = await bundle.generate( { - format: 'esm', - file: 'input.js', - sourcemap - } ); - - return output; -} - -test( 'Doesnt replace anything by default', async () => { - const output = await generateBundle( { replace: [] } ); - - verifyChunk( output, 'input.js', 'const test = 123;' ); -} ); - -test( 'Accepts string', async () => { - const output = await generateBundle( { - replace: [ - [ 'node:fs', 'another-dependency' ] - ] - } ); - - verifyChunk( output, 'input.js', 'export * from \'another-dependency\';' ); -} ); - -test( 'Accepts RegExp', async () => { - const output = await generateBundle( { - replace: [ - [ /node:fs/, 'another-dependency' ] - ] - } ); - - verifyChunk( output, 'input.js', 'export * from \'another-dependency\';' ); -} ); - -test( 'Replaces import BEFORE bundling when 3rd parameter is set to true', async () => { - const output = await generateBundle( { - replace: [ - [ './dependency.js', './replaced-dependency.js', true ] - ] - } ); - - verifyChunk( output, 'input.js', 'const test = 456;' ); -} ); - -test( 'Updates the source map', async () => { - const unmodifiedOutput = await generateBundle( { - replace: [] - }, true ); - - const output = await generateBundle( { - replace: [ - [ 'node:fs', 'another-dependency' ] - ] - }, true ); - - expect( ( unmodifiedOutput[ 1 ] as OutputAsset ).source ).not.toBe( ( output[ 1 ] as OutputAsset ).source ); - verifyChunk( output, 'input.js', 'another-dependency' ); -} ); - -test( 'Updates the source map when 3rd parameter is set to true', async () => { - const unmodifiedOutput = await generateBundle( { - replace: [] - }, true ); - - const output = await generateBundle( { - replace: [ - [ './dependency.js', './replaced-dependency.js', true ] - ] - }, true ); - - expect( ( unmodifiedOutput[ 1 ] as OutputAsset ).source ).not.toBe( ( output[ 1 ] as OutputAsset ).source ); - verifyChunk( output, 'input.js', 'const test = 456;' ); -} ); - -test( 'Replacing happens after the code is parsed and tree-shaken', async () => { - const output = await generateBundle( { - replace: [ - [ './dependency.js', 'node:fs' ] - ] - } ); - - verifyChunk( output, 'input.js', 'const test = 123;' ); -} ); diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/splitCss.test.ts b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/splitCss.test.ts index 052a801f7..8eaadb7b5 100644 --- a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/splitCss.test.ts +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/splitCss.test.ts @@ -5,7 +5,7 @@ import { join } from 'node:path'; import { test, expect } from 'vitest'; -import { rollup, type RollupOutput, type OutputAsset } from 'rollup'; +import { rolldown, type RolldownOutput, type OutputAsset } from 'rolldown'; import { verifyDividedStyleSheet } from '../../_utils/utils.js'; import { splitCss, bundleCss, type RollupSplitCssOptions } from '../../../src/index.js'; import { removeWhitespace } from '../../../src/utils.js'; @@ -17,8 +17,8 @@ async function generateBundle( input: string, options: RollupSplitCssOptions, banner?: string -): Promise { - const bundle = await rollup( { +): Promise { + const bundle = await rolldown( { input: join( import.meta.dirname, input ), plugins: [ bundleCss( { @@ -38,7 +38,7 @@ async function generateBundle( return output; } -function getStylesheetSource( output: RollupOutput['output'], outputFileName: string ): string { +function getStylesheetSource( output: RolldownOutput['output'], outputFileName: string ): string { const styles = output.find( output => output.fileName === outputFileName ); expect( styles ).toBeDefined(); @@ -50,8 +50,8 @@ function getStylesheetSource( output: RollupOutput['output'], outputFileName: st async function generateBundleWithoutBundledCss( input: string, options: RollupSplitCssOptions -): Promise { - const bundle = await rollup( { +): Promise { + const bundle = await rolldown( { input: join( import.meta.dirname, input ), plugins: [ splitCss( options ) ] } ); diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/translations/translations.test.ts b/packages/ckeditor5-dev-build-tools/tests/plugins/translations/translations.test.ts index 3c418c516..8ce923a7c 100644 --- a/packages/ckeditor5-dev-build-tools/tests/plugins/translations/translations.test.ts +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/translations/translations.test.ts @@ -5,7 +5,7 @@ import upath from 'upath'; import { test } from 'vitest'; -import { rollup, type RollupOutput } from 'rollup'; +import { rolldown, type RolldownOutput } from 'rolldown'; import { verifyChunk } from '../../_utils/utils.js'; import { translations, type RollupTranslationsOptions } from '../../../src/index.js'; @@ -24,8 +24,8 @@ const ENGLISH_TRANSLATIONS_FROM_ROOT = 'export default {"en":{"dictionary":{"Hel /** * Helper function for creating a bundle that won't be written to the file system. */ -async function generateBundle( options?: RollupTranslationsOptions ): Promise { - const bundle = await rollup( { +async function generateBundle( options?: RollupTranslationsOptions ): Promise { + const bundle = await rolldown( { input: upath.join( import.meta.dirname, '/fixtures/input.js' ), plugins: [ translations( options ) diff --git a/packages/ckeditor5-dev-build-tools/tests/utils.test.ts b/packages/ckeditor5-dev-build-tools/tests/utils.test.ts index bfbb753b2..36fbbf269 100644 --- a/packages/ckeditor5-dev-build-tools/tests/utils.test.ts +++ b/packages/ckeditor5-dev-build-tools/tests/utils.test.ts @@ -4,7 +4,7 @@ */ import { test, expect } from 'vitest'; -import { camelize, camelizeObjectKeys, removeNewline } from '../src/utils.js'; +import { camelize, camelizeObjectKeys, getUserDependency, removeNewline } from '../src/utils.js'; test( 'camelize()', () => { expect( camelize( 'this-is-a-test' ) ).toBe( 'thisIsATest' ); @@ -31,3 +31,7 @@ test( 'camelizeObjectKeys()', () => { } } ); } ); + +test( 'getUserDependency()', () => { + expect( getUserDependency( 'upath' ) ).toBeDefined(); +} ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4cb84ac4d..8dfea6cf3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,30 +79,9 @@ importers: packages/ckeditor5-dev-build-tools: dependencies: - '@rollup/plugin-commonjs': - specifier: ^28.0.9 - version: 28.0.9(rollup@4.60.1) - '@rollup/plugin-json': - specifier: ^6.1.0 - version: 6.1.0(rollup@4.60.1) - '@rollup/plugin-node-resolve': - specifier: ^16.0.3 - version: 16.0.3(rollup@4.60.1) - '@rollup/plugin-swc': - specifier: ^0.4.0 - version: 0.4.0(@swc/core@1.15.24)(rollup@4.60.1) - '@rollup/plugin-terser': - specifier: ^1.0.0 - version: 1.0.0(rollup@4.60.1) - '@rollup/plugin-typescript': - specifier: 12.3.0 - version: 12.3.0(rollup@4.60.1)(tslib@2.8.1)(typescript@5.5.4) '@rollup/pluginutils': specifier: ^5.3.0 version: 5.3.0(rollup@4.60.1) - '@swc/core': - specifier: ^1.15.24 - version: 1.15.24 cssnano: specifier: ^7.1.4 version: 7.1.4(postcss@8.5.10) @@ -112,9 +91,6 @@ importers: es-toolkit: specifier: ^1.45.1 version: 1.45.1 - estree-walker: - specifier: ^3.0.3 - version: 3.0.3 glob: specifier: ^13.0.6 version: 13.0.6 @@ -130,12 +106,9 @@ importers: purgecss: specifier: ^8.0.0 version: 8.0.0 - rollup: - specifier: ^4.60.1 - version: 4.60.1 - rollup-plugin-svg-import: - specifier: ^3.0.0 - version: 3.0.0(rollup@4.60.1) + rolldown: + specifier: ^1.0.0 + version: 1.0.0 source-map: specifier: ^0.7.6 version: 0.7.6 @@ -152,9 +125,6 @@ importers: '@vitest/coverage-v8': specifier: ^4.1.2 version: 4.1.2(vitest@4.1.2(@types/node@22.19.17)(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3))) - rolldown: - specifier: ^1.0.0 - version: 1.0.0 type-fest: specifier: ^4.41.0 version: 4.41.0 @@ -1629,65 +1599,6 @@ packages: '@rolldown/pluginutils@1.0.0': resolution: {integrity: sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==} - '@rollup/plugin-commonjs@28.0.9': - resolution: {integrity: sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA==} - engines: {node: '>=16.0.0 || 14 >= 14.17'} - peerDependencies: - rollup: ^2.68.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-json@6.1.0': - resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-node-resolve@16.0.3': - resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.78.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-swc@0.4.0': - resolution: {integrity: sha512-oAtqXa8rOl7BOK1Rz3rRxI+LIL53S9SqO2KSq2UUUzWgOgXg6492Jh5mL2mv/f9cpit8zFWdwILuVeozZ0C8mg==} - engines: {node: '>=14.0.0'} - peerDependencies: - '@swc/core': ^1.3.0 - rollup: ^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-terser@1.0.0': - resolution: {integrity: sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==} - engines: {node: '>=20.0.0'} - peerDependencies: - rollup: ^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-typescript@12.3.0': - resolution: {integrity: sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.14.0||^3.0.0||^4.0.0 - tslib: '*' - typescript: '>=3.7.0' - peerDependenciesMeta: - rollup: - optional: true - tslib: - optional: true - '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} @@ -1932,99 +1843,6 @@ packages: peerDependencies: eslint: '>=9.0.0' - '@swc/core-darwin-arm64@1.15.24': - resolution: {integrity: sha512-uM5ZGfFXjtvtJ+fe448PVBEbn/CSxS3UAyLj3O9xOqKIWy3S6hPTXSPbszxkSsGDYKi+YFhzAsR4r/eXLxEQ0g==} - engines: {node: '>=10'} - cpu: [arm64] - os: [darwin] - - '@swc/core-darwin-x64@1.15.24': - resolution: {integrity: sha512-fMIb/Zfn929pw25VMBhV7Ji2Dl+lCWtUPNdYJQYOke+00E5fcQ9ynxtP8+qhUo/HZc+mYQb1gJxwHM9vty+lXg==} - engines: {node: '>=10'} - cpu: [x64] - os: [darwin] - - '@swc/core-linux-arm-gnueabihf@1.15.24': - resolution: {integrity: sha512-vOkjsyjjxnoYx3hMEWcGxQrMgnNrRm6WAegBXrN8foHtDAR+zpdhpGF5a4lj1bNPgXAvmysjui8cM1ov/Clkaw==} - engines: {node: '>=10'} - cpu: [arm] - os: [linux] - - '@swc/core-linux-arm64-gnu@1.15.24': - resolution: {integrity: sha512-h/oNu+upkXJ6Cicnq7YGVj9PkdfarLCdQa8l/FlHYvfv8CEiMaeeTnpLU7gSBH/rGxosM6Qkfa/J9mThGF9CLA==} - engines: {node: '>=10'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@swc/core-linux-arm64-musl@1.15.24': - resolution: {integrity: sha512-ZpF/pRe1guk6sKzQI9D1jAORtjTdNlyeXn9GDz8ophof/w2WhojRblvSDJaGe7rJjcPN8AaOkhwdRUh7q8oYIg==} - engines: {node: '>=10'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@swc/core-linux-ppc64-gnu@1.15.24': - resolution: {integrity: sha512-QZEsZfisHTSJlmyChgDFNmKPb3W6Lhbfo/O76HhIngfEdnQNmukS38/VSe1feho+xkV5A5hETyCbx3sALBZKAQ==} - engines: {node: '>=10'} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@swc/core-linux-s390x-gnu@1.15.24': - resolution: {integrity: sha512-DLdJKVsJgglqQrJBuoUYNmzm3leI7kUZhLbZGHv42onfKsGf6JDS3+bzCUQfte/XOqDjh/tmmn1DR/CF/tCJFw==} - engines: {node: '>=10'} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@swc/core-linux-x64-gnu@1.15.24': - resolution: {integrity: sha512-IpLYfposPA/XLxYOKpRfeccl1p5dDa3+okZDHHTchBkXEaVCnq5MADPmIWwIYj1tudt7hORsEHccG5no6IUQRw==} - engines: {node: '>=10'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@swc/core-linux-x64-musl@1.15.24': - resolution: {integrity: sha512-JHy3fMSc0t/EPWgo74+OK5TGr51aElnzqfUPaiRf2qJ/BfX5CUCfMiWVBuhI7qmVMBnk1jTRnL/xZnOSHDPLYg==} - engines: {node: '>=10'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@swc/core-win32-arm64-msvc@1.15.24': - resolution: {integrity: sha512-Txj+qUH1z2bUd1P3JvwByfjKFti3cptlAxhWgmunBUUxy/IW3CXLZ6l6Gk4liANadKkU71nIU1X30Z5vpMT3BA==} - engines: {node: '>=10'} - cpu: [arm64] - os: [win32] - - '@swc/core-win32-ia32-msvc@1.15.24': - resolution: {integrity: sha512-15D/nl3XwrhFpMv+MADFOiVwv3FvH9j8c6Rf8EXBT3Q5LoMh8YnDnSgPYqw1JzPnksvsBX6QPXLiPqmcR/Z4qQ==} - engines: {node: '>=10'} - cpu: [ia32] - os: [win32] - - '@swc/core-win32-x64-msvc@1.15.24': - resolution: {integrity: sha512-PR0PlTlPra2JbaDphrOAzm6s0v9rA0F17YzB+XbWD95B4g2cWcZY9LAeTa4xll70VLw9Jr7xBrlohqlQmelMFQ==} - engines: {node: '>=10'} - cpu: [x64] - os: [win32] - - '@swc/core@1.15.24': - resolution: {integrity: sha512-5Hj8aNasue7yusUt8LGCUe/AjM7RMAce8ZoyDyiFwx7Al+GbYKL+yE7g4sJk8vEr1dKIkTRARkNIJENc4CjkBQ==} - engines: {node: '>=10'} - peerDependencies: - '@swc/helpers': '>=0.5.17' - peerDependenciesMeta: - '@swc/helpers': - optional: true - - '@swc/counter@0.1.3': - resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - - '@swc/types@0.1.26': - resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==} - '@tootallnate/quickjs-emscripten@0.23.0': resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} @@ -2114,9 +1932,6 @@ packages: '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} - '@types/resolve@1.20.2': - resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} @@ -2751,9 +2566,6 @@ packages: resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} engines: {node: '>= 12.0.0'} - commondir@1.0.1: - resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} - commonmark@0.31.2: resolution: {integrity: sha512-2fRLTyb9r/2835k5cwcAwOj0DEc44FARnMp5veGsJ+mEAZdi52sNopLu07ZyElQUz058H43whzlERDIaaSw4rg==} hasBin: true @@ -2914,10 +2726,6 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -3708,9 +3516,6 @@ packages: resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} engines: {node: '>=12'} - is-module@1.0.0: - resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} - is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -3731,9 +3536,6 @@ packages: resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} engines: {node: '>=8'} - is-reference@1.2.1: - resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} - is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -5090,12 +4892,6 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rollup-plugin-svg-import@3.0.0: - resolution: {integrity: sha512-5fUESTM5hdqJojrwO53JQUO7NespLNx4iLeMsToQfuaGGqGT5sz85Ns5gCDNxLO6yBPbn7p0A/6YA+Rq3clg4Q==} - engines: {node: '>=18.0.0'} - peerDependencies: - rollup: ^3.0.0||^4.0.0 - rollup@4.60.1: resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -5245,10 +5041,6 @@ packages: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - smob@1.6.1: - resolution: {integrity: sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g==} - engines: {node: '>=20.0.0'} - snyk@1.1303.2: resolution: {integrity: sha512-hZBvU1MiqTBfoFDcgoIFKZEfR98Y9lYtabhDS1vH77S1JWwLaRyeiiMiPGjrI56SD2ZRfHcfyicDI1eQ6A60VQ==} engines: {node: '>=12'} @@ -6716,59 +6508,6 @@ snapshots: '@rolldown/pluginutils@1.0.0': {} - '@rollup/plugin-commonjs@28.0.9(rollup@4.60.1)': - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.60.1) - commondir: 1.0.1 - estree-walker: 2.0.2 - fdir: 6.5.0(picomatch@4.0.4) - is-reference: 1.2.1 - magic-string: 0.30.21 - picomatch: 4.0.4 - optionalDependencies: - rollup: 4.60.1 - - '@rollup/plugin-json@6.1.0(rollup@4.60.1)': - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.60.1) - optionalDependencies: - rollup: 4.60.1 - - '@rollup/plugin-node-resolve@16.0.3(rollup@4.60.1)': - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.60.1) - '@types/resolve': 1.20.2 - deepmerge: 4.3.1 - is-module: 1.0.0 - resolve: 1.22.11 - optionalDependencies: - rollup: 4.60.1 - - '@rollup/plugin-swc@0.4.0(@swc/core@1.15.24)(rollup@4.60.1)': - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.60.1) - '@swc/core': 1.15.24 - smob: 1.6.1 - optionalDependencies: - rollup: 4.60.1 - - '@rollup/plugin-terser@1.0.0(rollup@4.60.1)': - dependencies: - serialize-javascript: 7.0.5 - smob: 1.6.1 - terser: 5.46.1 - optionalDependencies: - rollup: 4.60.1 - - '@rollup/plugin-typescript@12.3.0(rollup@4.60.1)(tslib@2.8.1)(typescript@5.5.4)': - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.60.1) - resolve: 1.22.11 - typescript: 5.5.4 - optionalDependencies: - rollup: 4.60.1 - tslib: 2.8.1 - '@rollup/pluginutils@5.3.0(rollup@4.60.1)': dependencies: '@types/estree': 1.0.8 @@ -6972,66 +6711,6 @@ snapshots: - supports-color - typescript - '@swc/core-darwin-arm64@1.15.24': - optional: true - - '@swc/core-darwin-x64@1.15.24': - optional: true - - '@swc/core-linux-arm-gnueabihf@1.15.24': - optional: true - - '@swc/core-linux-arm64-gnu@1.15.24': - optional: true - - '@swc/core-linux-arm64-musl@1.15.24': - optional: true - - '@swc/core-linux-ppc64-gnu@1.15.24': - optional: true - - '@swc/core-linux-s390x-gnu@1.15.24': - optional: true - - '@swc/core-linux-x64-gnu@1.15.24': - optional: true - - '@swc/core-linux-x64-musl@1.15.24': - optional: true - - '@swc/core-win32-arm64-msvc@1.15.24': - optional: true - - '@swc/core-win32-ia32-msvc@1.15.24': - optional: true - - '@swc/core-win32-x64-msvc@1.15.24': - optional: true - - '@swc/core@1.15.24': - dependencies: - '@swc/counter': 0.1.3 - '@swc/types': 0.1.26 - optionalDependencies: - '@swc/core-darwin-arm64': 1.15.24 - '@swc/core-darwin-x64': 1.15.24 - '@swc/core-linux-arm-gnueabihf': 1.15.24 - '@swc/core-linux-arm64-gnu': 1.15.24 - '@swc/core-linux-arm64-musl': 1.15.24 - '@swc/core-linux-ppc64-gnu': 1.15.24 - '@swc/core-linux-s390x-gnu': 1.15.24 - '@swc/core-linux-x64-gnu': 1.15.24 - '@swc/core-linux-x64-musl': 1.15.24 - '@swc/core-win32-arm64-msvc': 1.15.24 - '@swc/core-win32-ia32-msvc': 1.15.24 - '@swc/core-win32-x64-msvc': 1.15.24 - - '@swc/counter@0.1.3': {} - - '@swc/types@0.1.26': - dependencies: - '@swc/counter': 0.1.3 - '@tootallnate/quickjs-emscripten@0.23.0': {} '@tufjs/canonical-json@2.0.0': {} @@ -7146,8 +6825,6 @@ snapshots: '@types/parse-json@4.0.2': {} - '@types/resolve@1.20.2': {} - '@types/semver@7.7.1': {} '@types/sinon-chai@2.7.42': @@ -7863,8 +7540,6 @@ snapshots: comment-parser@1.4.1: {} - commondir@1.0.1: {} - commonmark@0.31.2: dependencies: entities: 3.0.1 @@ -8048,8 +7723,6 @@ snapshots: deep-is@0.1.4: {} - deepmerge@4.3.1: {} - define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -8922,8 +8595,6 @@ snapshots: is-interactive@2.0.0: {} - is-module@1.0.0: {} - is-number@7.0.0: {} is-path-cwd@3.0.0: {} @@ -8934,10 +8605,6 @@ snapshots: is-plain-obj@2.1.0: {} - is-reference@1.2.1: - dependencies: - '@types/estree': 1.0.8 - is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -10609,11 +10276,6 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0 '@rolldown/binding-win32-x64-msvc': 1.0.0 - rollup-plugin-svg-import@3.0.0(rollup@4.60.1): - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.60.1) - rollup: 4.60.1 - rollup@4.60.1: dependencies: '@types/estree': 1.0.8 @@ -10802,8 +10464,6 @@ snapshots: smart-buffer@4.2.0: {} - smob@1.6.1: {} - snyk@1.1303.2: dependencies: '@sentry/node': 7.120.4 From bd3df555fca5f88a9ffa9420ca6378db8f66c9c2 Mon Sep 17 00:00:00 2001 From: Filip Sobol Date: Thu, 14 May 2026 10:54:35 +0200 Subject: [PATCH 02/12] Fix TypeDoc event inheritance for typed mixin aliases. --- .../src/event-inheritance-fixer/index.ts | 103 +++++++++++++----- .../class-default-observable-mixin.ts | 14 +++ .../fixtures/class-observable-mixin-foo.ts | 15 +++ .../fixtures/observable-mixin.ts | 45 ++++++++ .../tests/event-inheritance-fixer/index.ts | 20 +++- 5 files changed, 167 insertions(+), 30 deletions(-) create mode 100644 packages/typedoc-plugins/tests/event-inheritance-fixer/fixtures/class-default-observable-mixin.ts create mode 100644 packages/typedoc-plugins/tests/event-inheritance-fixer/fixtures/class-observable-mixin-foo.ts create mode 100644 packages/typedoc-plugins/tests/event-inheritance-fixer/fixtures/observable-mixin.ts diff --git a/packages/typedoc-plugins/src/event-inheritance-fixer/index.ts b/packages/typedoc-plugins/src/event-inheritance-fixer/index.ts index a7fc60217..6f51dbcea 100644 --- a/packages/typedoc-plugins/src/event-inheritance-fixer/index.ts +++ b/packages/typedoc-plugins/src/event-inheritance-fixer/index.ts @@ -5,13 +5,14 @@ import { Converter, + DeclarationReflection, ReferenceType, ReflectionKind, + TypeScript, type Context, type Application, type SomeType, - type IntersectionType, - type DeclarationReflection + type IntersectionType } from 'typedoc'; import { getPluginPriority } from '../utils/getpluginpriority.js'; @@ -35,7 +36,7 @@ function onEventEnd( context: Context ) { for ( const reflection of reflections ) { // Find all parents of a given reflection. // Filter function is required in case if the purge plugin removed a reflection. - const parentReflections = getParentClasses( reflection ).filter( Boolean ); + const parentReflections = getParentClasses( context, reflection ).filter( Boolean ); // Collect all events. The goal is to insert them into a reflection. // When using the mixins concept, Typedoc loses the inheritance tree. @@ -128,39 +129,87 @@ function getDerivedReflections( reflection: DeclarationReflection ): Array { +function getParentClasses( context: Context, reflection: DeclarationReflection ): Array { const extendedTypes = reflection.extendedTypes || []; return extendedTypes - .filter( entry => { - // Cover: `class extends Mixin( BaseClass )`. - if ( isIntersectionType( entry ) ) { - return entry.types.filter( isReferenceType ).length; - } - - return ( entry as ReferenceType ).reflection; - } ) - .flatMap( entry => { - if ( isIntersectionType( entry ) ) { - const parents = entry.types - .filter( isReferenceType ) - .map( e => e.reflection as DeclarationReflection ); - - return [ - ...parents.flatMap( parent => getParentClasses( parent ) ), - ...parents - ]; - } - - const parent = ( entry as ReferenceType ).reflection as DeclarationReflection; - + .flatMap( entry => getDirectParentClasses( context, reflection, entry ) ) + .flatMap( parent => { return [ - ...getParentClasses( parent ), + ...getParentClasses( context, parent ), parent ]; } ); } +function getDirectParentClasses( + context: Context, + reflection: DeclarationReflection, + type: SomeType +): Array { + // Cover: `class extends Mixin( BaseClass )`. + if ( isIntersectionType( type ) ) { + return type.types + .filter( isReferenceType ) + .map( type => type.reflection as DeclarationReflection ); + } + + const parentReflection = ( type as ReferenceType ).reflection as DeclarationReflection | undefined; + + if ( parentReflection ) { + return [ parentReflection ]; + } + + // TypeDoc does not link classes extending a typed mixin alias, e.g. `class Foo extends FooBase`. + // Resolve that alias through TypeScript and read the constructor return type to recover mixed-in parents. + const aliasDeclaration = getUnresolvedBaseDeclaration( context, reflection ); + + return aliasDeclaration ? getConstructorReturnTypeReflections( context, aliasDeclaration ) : []; +} + +function getUnresolvedBaseDeclaration( + context: Context, + reflection: DeclarationReflection +): TypeScript.VariableDeclaration | null { + const classDeclaration = context.getSymbolFromReflection( reflection ) + ?.declarations + ?.find( TypeScript.isClassDeclaration ); + + const baseExpression = classDeclaration?.heritageClauses + ?.find( clause => clause.token === TypeScript.SyntaxKind.ExtendsKeyword ) + ?.types[ 0 ] + ?.expression; + + const aliasSymbol = baseExpression && context.getSymbolAtLocation( baseExpression ); + const aliasDeclaration = aliasSymbol?.declarations?.find( TypeScript.isVariableDeclaration ); + + return aliasDeclaration || null; +} + +function getConstructorReturnTypeReflections( + context: Context, + aliasDeclaration: TypeScript.VariableDeclaration +): Array { + return context.checker.getTypeAtLocation( aliasDeclaration.name ) + .getConstructSignatures() + .flatMap( signature => context.checker.getReturnTypeOfSignature( signature ) ) + .flatMap( type => getReflectionsFromType( context, type ) ); +} + +function getReflectionsFromType( context: Context, type: TypeScript.Type ): Array { + // Mixin constructor aliases usually return intersections like `Base & Observable`. + // Each part may define events that need to be inherited by the generated class. + if ( type.isIntersection() ) { + return type.types.flatMap( type => getReflectionsFromType( context, type ) ); + } + + const symbol = type.getSymbol() || type.aliasSymbol; + const resolvedSymbol = symbol && context.resolveAliasedSymbol( symbol ); + const reflection = resolvedSymbol && context.getReflectionFromSymbol( resolvedSymbol ); + + return reflection instanceof DeclarationReflection ? [ reflection ] : []; +} + function isIntersectionType( type: SomeType ): type is IntersectionType { return type.type === 'intersection'; } diff --git a/packages/typedoc-plugins/tests/event-inheritance-fixer/fixtures/class-default-observable-mixin.ts b/packages/typedoc-plugins/tests/event-inheritance-fixer/fixtures/class-default-observable-mixin.ts new file mode 100644 index 000000000..3a6727e96 --- /dev/null +++ b/packages/typedoc-plugins/tests/event-inheritance-fixer/fixtures/class-default-observable-mixin.ts @@ -0,0 +1,14 @@ +/** + * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module fixtures/class-default-observable-mixin + */ + +import { ObservableMixin, type ObservableMixinConstructor } from './observable-mixin.js'; + +const ClassDefaultObservableMixinBase: ObservableMixinConstructor = ObservableMixin(); + +export class ClassDefaultObservableMixin extends ClassDefaultObservableMixinBase {} diff --git a/packages/typedoc-plugins/tests/event-inheritance-fixer/fixtures/class-observable-mixin-foo.ts b/packages/typedoc-plugins/tests/event-inheritance-fixer/fixtures/class-observable-mixin-foo.ts new file mode 100644 index 000000000..360449ae4 --- /dev/null +++ b/packages/typedoc-plugins/tests/event-inheritance-fixer/fixtures/class-observable-mixin-foo.ts @@ -0,0 +1,15 @@ +/** + * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module fixtures/class-observable-mixin-foo + */ + +import { ClassFoo } from './class-foo.js'; +import { ObservableMixin, type ObservableMixinConstructor } from './observable-mixin.js'; + +const ClassObservableMixinFooBase: ObservableMixinConstructor = ObservableMixin( ClassFoo ); + +export class ClassObservableMixinFoo extends ClassObservableMixinFooBase {} diff --git a/packages/typedoc-plugins/tests/event-inheritance-fixer/fixtures/observable-mixin.ts b/packages/typedoc-plugins/tests/event-inheritance-fixer/fixtures/observable-mixin.ts new file mode 100644 index 000000000..ff41463b3 --- /dev/null +++ b/packages/typedoc-plugins/tests/event-inheritance-fixer/fixtures/observable-mixin.ts @@ -0,0 +1,45 @@ +/** + * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module fixtures/observable-mixin + */ + +import type { Constructor, Mixed } from './mixin.js'; + +export interface Observable { + set( name: string, value: unknown ): void; +} + +export type ObservableMixinConstructor = Base extends Constructor ? + Mixed : + { + new (): Observable; + prototype: Observable; + }; + +export function ObservableMixin( base: Base ): ObservableMixinConstructor; + +export function ObservableMixin(): ObservableMixinConstructor; + +export function ObservableMixin( base?: Constructor ): unknown { + if ( !base ) { + abstract class ObservableMixin {} + + return ObservableMixin; + } + + abstract class ObservableMixin extends base {} + + return ObservableMixin; +} + +/** + * @eventName ~Observable#observable-property + */ +export type ObservablePropertyEvent = { + name: string; + args: []; +}; diff --git a/packages/typedoc-plugins/tests/event-inheritance-fixer/index.ts b/packages/typedoc-plugins/tests/event-inheritance-fixer/index.ts index dec8f5010..970aa51b7 100644 --- a/packages/typedoc-plugins/tests/event-inheritance-fixer/index.ts +++ b/packages/typedoc-plugins/tests/event-inheritance-fixer/index.ts @@ -98,6 +98,11 @@ describe( 'typedoc-plugins/event-inheritance-fixer', () => { // ClassFoo ⟶ "property" // ⤷ ClassMixinFoo ⟶ "property" (inherited from ClassFoo) // ⤷ ClassMixinFooBar ⟶ "property" (inherited from ClassFoo) + // ⤷ ClassObservableMixinFoo ⟶ "property" (inherited from ClassFoo) + + // Observable ⟶ "observable-property" + // ⤷ ClassObservableMixinFoo ⟶ "observable-property" (inherited from Observable) + // ⤷ ClassDefaultObservableMixin ⟶ "observable-property" (inherited from Observable) // InterfaceA ⟶ "event-1-interface-a" // InterfaceA ⟶ "event-2-interface-a" @@ -117,8 +122,9 @@ describe( 'typedoc-plugins/event-inheritance-fixer', () => { it( 'should find all events within the project', () => { // There are 8 events from classes and 9 events from interfaces. // There are also 3 events from `MixedClass`, which implements the `InterfaceC`. - // Also, 3 events come from the `ClassFoo` and its descendant classes. - expect( events ).to.lengthOf( 23 ); + // Also, 4 events come from the `ClassFoo` and its descendant classes. + // The last 3 events come from the `Observable` interface and classes that extend its typed mixin aliases. + expect( events ).to.lengthOf( 27 ); } ); it( 'should find all events within the project (verifying classes A-C)', () => { @@ -156,6 +162,14 @@ describe( 'typedoc-plugins/event-inheritance-fixer', () => { expect( findEvent( 'ClassMixinFooBar', 'property' ) ).to.not.be.undefined; } ); + it( 'should inherit events through typed mixin aliases', () => { + expect( findEvent( 'ClassFoo', 'property' ) ).to.not.be.undefined; + expect( findEvent( 'Observable', 'observable-property' ) ).to.not.be.undefined; + expect( findEvent( 'ClassObservableMixinFoo', 'property' ) ).to.not.be.undefined; + expect( findEvent( 'ClassObservableMixinFoo', 'observable-property' ) ).to.not.be.undefined; + expect( findEvent( 'ClassDefaultObservableMixin', 'observable-property' ) ).to.not.be.undefined; + } ); + it( 'should create new events with own ids in the inherited classes and interfaces', () => { const numberOfUniqueEvents = new Set( events.map( event => event.id ) ).size; const numberOfExpectedEvents = events.length; @@ -379,7 +393,7 @@ describe( 'typedoc-plugins/event-inheritance-fixer', () => { } ); it( 'should find all events within the project', () => { - expect( events ).to.lengthOf( 20 ); + expect( events ).to.lengthOf( 24 ); } ); it( 'should find all events within the project (verifying classes A-C)', () => { From 7d157caa3a57ea2c20f7e3f82339c8e69340436a Mon Sep 17 00:00:00 2001 From: Filip Sobol Date: Thu, 14 May 2026 11:04:36 +0200 Subject: [PATCH 03/12] Fix type error. --- .../tests/plugins/rawImport/rawImport.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/rawImport/rawImport.test.ts b/packages/ckeditor5-dev-build-tools/tests/plugins/rawImport/rawImport.test.ts index 3c5791d38..c542b3b17 100644 --- a/packages/ckeditor5-dev-build-tools/tests/plugins/rawImport/rawImport.test.ts +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/rawImport/rawImport.test.ts @@ -5,7 +5,7 @@ import upath from 'upath'; import { test, expect } from 'vitest'; -import { rolldown, type RolldownOutput } from 'rolldown'; +import { rolldown, type PluginContext, type RolldownOutput } from 'rolldown'; import { verifyChunk } from '../../_utils/utils.js'; import { rawImport } from '../../../src/index.js'; @@ -46,5 +46,5 @@ test( 'does not resolve raw imports without importer context', () => { const plugin = rawImport(); const resolveId = typeof plugin.resolveId == 'function' ? plugin.resolveId : plugin.resolveId!.handler; - expect( resolveId.call( {}, './dependency.css?raw', undefined, {} as any ) ).toBeNull(); + expect( resolveId.call( {} as PluginContext, './dependency.css?raw', undefined, {} as any ) ).toBeNull(); } ); From d29b3ed6c3eebdb04491b845eda1cfc68b7e41d3 Mon Sep 17 00:00:00 2001 From: Filip Sobol Date: Thu, 14 May 2026 11:50:53 +0200 Subject: [PATCH 04/12] Reuse `declarationFiles` plugin from `build-tools` when building the packages themselves. --- .../ckeditor5-dev-build-tools/package.json | 4 +- ...{rolldown.config.js => rolldown.config.ts} | 19 ++++-- .../ckeditor5-dev-build-tools/src/index.ts | 1 + packages/ckeditor5-dev-changelog/package.json | 4 +- ...{rolldown.config.js => rolldown.config.ts} | 19 ++++-- .../package.json | 4 +- ...{rolldown.config.js => rolldown.config.ts} | 19 ++++-- packages/ckeditor5-dev-utils/package.json | 4 +- ...{rolldown.config.js => rolldown.config.ts} | 19 ++++-- .../ckeditor5-dev-web-crawler/package.json | 4 +- ...{rolldown.config.js => rolldown.config.ts} | 19 ++++-- packages/typedoc-plugins/package.json | 4 +- ...{rolldown.config.js => rolldown.config.ts} | 19 ++++-- scripts/plugin-declarations.js | 61 ------------------- 14 files changed, 91 insertions(+), 109 deletions(-) rename packages/ckeditor5-dev-build-tools/{rolldown.config.js => rolldown.config.ts} (52%) rename packages/ckeditor5-dev-changelog/{rolldown.config.js => rolldown.config.ts} (52%) rename packages/ckeditor5-dev-license-checker/{rolldown.config.js => rolldown.config.ts} (52%) rename packages/ckeditor5-dev-utils/{rolldown.config.js => rolldown.config.ts} (56%) rename packages/ckeditor5-dev-web-crawler/{rolldown.config.js => rolldown.config.ts} (50%) rename packages/typedoc-plugins/{rolldown.config.js => rolldown.config.ts} (50%) delete mode 100644 scripts/plugin-declarations.js diff --git a/packages/ckeditor5-dev-build-tools/package.json b/packages/ckeditor5-dev-build-tools/package.json index 8122eb34c..80a6d0b1c 100644 --- a/packages/ckeditor5-dev-build-tools/package.json +++ b/packages/ckeditor5-dev-build-tools/package.json @@ -48,8 +48,8 @@ "vitest": "^4.1.2" }, "scripts": { - "build": "rolldown -c rolldown.config.js", - "dev": "rolldown -c rolldown.config.js --watch", + "build": "rolldown -c rolldown.config.ts", + "dev": "rolldown -c rolldown.config.ts --watch", "test": "vitest run --config vitest.config.ts", "coverage": "vitest run --config vitest.config.ts --coverage", "test:dev": "vitest dev" diff --git a/packages/ckeditor5-dev-build-tools/rolldown.config.js b/packages/ckeditor5-dev-build-tools/rolldown.config.ts similarity index 52% rename from packages/ckeditor5-dev-build-tools/rolldown.config.js rename to packages/ckeditor5-dev-build-tools/rolldown.config.ts index 5c70b6606..436e0cb66 100644 --- a/packages/ckeditor5-dev-build-tools/rolldown.config.js +++ b/packages/ckeditor5-dev-build-tools/rolldown.config.ts @@ -3,13 +3,18 @@ * For licensing, see LICENSE.md. */ -import { defineConfig } from 'rolldown'; -import { declarationFilesPlugin } from '../../scripts/plugin-declarations.js'; +import { defineConfig, type RolldownOptions } from 'rolldown'; +import { declarationFiles } from './src/plugins/declarations.js'; import pkg from './package.json' with { type: 'json' }; +const packageJson = pkg as { + dependencies?: Record; + peerDependencies?: Record; +}; + const externals = [ - ...Object.keys( pkg.dependencies || {} ), - ...Object.keys( pkg.peerDependencies || {} ) + ...Object.keys( packageJson.dependencies || {} ), + ...Object.keys( packageJson.peerDependencies || {} ) ]; export default defineConfig( { @@ -22,7 +27,9 @@ export default defineConfig( { assetFileNames: '[name][extname]' }, plugins: [ - declarationFilesPlugin() + declarationFiles( { + sourceDirectory: 'src' + } ) ], external: id => externals.some( name => id.startsWith( name ) ) -} ); +} ) as RolldownOptions; diff --git a/packages/ckeditor5-dev-build-tools/src/index.ts b/packages/ckeditor5-dev-build-tools/src/index.ts index 123183287..a47479648 100644 --- a/packages/ckeditor5-dev-build-tools/src/index.ts +++ b/packages/ckeditor5-dev-build-tools/src/index.ts @@ -6,6 +6,7 @@ export { build } from './build.js'; export { addBanner, type RollupBannerOptions } from './plugins/banner.js'; export { bundleCss, type RollupBundleCssOptions } from './plugins/bundleCss.js'; +export { declarationFiles, type RolldownDeclarationOptions } from './plugins/declarations.js'; export { loadSourcemaps } from './plugins/loadSourcemaps.js'; export { rawImport } from './plugins/rawImport.js'; export { splitCss, type RollupSplitCssOptions } from './plugins/splitCss.js'; diff --git a/packages/ckeditor5-dev-changelog/package.json b/packages/ckeditor5-dev-changelog/package.json index 9eeadcbf7..48b9cb2ad 100644 --- a/packages/ckeditor5-dev-changelog/package.json +++ b/packages/ckeditor5-dev-changelog/package.json @@ -44,8 +44,8 @@ "vitest": "^4.1.2" }, "scripts": { - "build": "rolldown -c rolldown.config.js", - "dev": "rolldown -c rolldown.config.js --watch", + "build": "rolldown -c rolldown.config.ts", + "dev": "rolldown -c rolldown.config.ts --watch", "test": "vitest run --config vitest.config.ts", "coverage": "vitest run --config vitest.config.ts --coverage", "test:dev": "vitest dev" diff --git a/packages/ckeditor5-dev-changelog/rolldown.config.js b/packages/ckeditor5-dev-changelog/rolldown.config.ts similarity index 52% rename from packages/ckeditor5-dev-changelog/rolldown.config.js rename to packages/ckeditor5-dev-changelog/rolldown.config.ts index d1c21fd87..25e7f680a 100644 --- a/packages/ckeditor5-dev-changelog/rolldown.config.js +++ b/packages/ckeditor5-dev-changelog/rolldown.config.ts @@ -3,13 +3,18 @@ * For licensing, see LICENSE.md. */ -import { defineConfig } from 'rolldown'; -import { declarationFilesPlugin } from '../../scripts/plugin-declarations.js'; +import { defineConfig, type RolldownOptions } from 'rolldown'; +import { declarationFiles } from '../ckeditor5-dev-build-tools/src/plugins/declarations.js'; import pkg from './package.json' with { type: 'json' }; +const packageJson = pkg as { + dependencies?: Record; + peerDependencies?: Record; +}; + const externals = [ - ...Object.keys( pkg.dependencies || {} ), - ...Object.keys( pkg.peerDependencies || {} ) + ...Object.keys( packageJson.dependencies || {} ), + ...Object.keys( packageJson.peerDependencies || {} ) ]; export default defineConfig( { @@ -26,6 +31,8 @@ export default defineConfig( { }, external: id => externals.some( name => id.startsWith( name ) ), plugins: [ - declarationFilesPlugin() + declarationFiles( { + sourceDirectory: 'src' + } ) ] -} ); +} ) as RolldownOptions; diff --git a/packages/ckeditor5-dev-license-checker/package.json b/packages/ckeditor5-dev-license-checker/package.json index ab5b672f0..645cd55f6 100644 --- a/packages/ckeditor5-dev-license-checker/package.json +++ b/packages/ckeditor5-dev-license-checker/package.json @@ -32,8 +32,8 @@ "vitest": "^4.1.2" }, "scripts": { - "build": "rolldown -c rolldown.config.js", - "dev": "rolldown -c rolldown.config.js --watch", + "build": "rolldown -c rolldown.config.ts", + "dev": "rolldown -c rolldown.config.ts --watch", "test": "vitest run --config vitest.config.ts", "coverage": "vitest run --config vitest.config.ts --coverage" }, diff --git a/packages/ckeditor5-dev-license-checker/rolldown.config.js b/packages/ckeditor5-dev-license-checker/rolldown.config.ts similarity index 52% rename from packages/ckeditor5-dev-license-checker/rolldown.config.js rename to packages/ckeditor5-dev-license-checker/rolldown.config.ts index c89a670c3..c5874dc1a 100644 --- a/packages/ckeditor5-dev-license-checker/rolldown.config.js +++ b/packages/ckeditor5-dev-license-checker/rolldown.config.ts @@ -3,14 +3,19 @@ * For licensing, see LICENSE.md. */ -import { defineConfig } from 'rolldown'; -import { declarationFilesPlugin } from '../../scripts/plugin-declarations.js'; +import { defineConfig, type RolldownOptions } from 'rolldown'; +import { declarationFiles } from '../ckeditor5-dev-build-tools/src/plugins/declarations.js'; import pkg from './package.json' with { type: 'json' }; +const packageJson = pkg as { + dependencies?: Record; + peerDependencies?: Record; +}; + // List of external dependencies const externals = [ - ...Object.keys( pkg.dependencies || {} ), - ...Object.keys( pkg.peerDependencies || {} ) + ...Object.keys( packageJson.dependencies || {} ), + ...Object.keys( packageJson.peerDependencies || {} ) ]; export default defineConfig( { @@ -23,7 +28,9 @@ export default defineConfig( { assetFileNames: '[name][extname]' }, plugins: [ - declarationFilesPlugin() + declarationFiles( { + sourceDirectory: 'src' + } ) ], external: id => externals.some( name => id.startsWith( name ) ) -} ); +} ) as RolldownOptions; diff --git a/packages/ckeditor5-dev-utils/package.json b/packages/ckeditor5-dev-utils/package.json index c736e378b..798f58a4a 100644 --- a/packages/ckeditor5-dev-utils/package.json +++ b/packages/ckeditor5-dev-utils/package.json @@ -50,8 +50,8 @@ "vitest": "^4.1.2" }, "scripts": { - "build": "rolldown -c rolldown.config.js", - "dev": "rolldown -c rolldown.config.js --watch", + "build": "rolldown -c rolldown.config.ts", + "dev": "rolldown -c rolldown.config.ts --watch", "test": "vitest run --config vitest.config.ts", "coverage": "vitest run --config vitest.config.ts --coverage", "test:dev": "vitest dev" diff --git a/packages/ckeditor5-dev-utils/rolldown.config.js b/packages/ckeditor5-dev-utils/rolldown.config.ts similarity index 56% rename from packages/ckeditor5-dev-utils/rolldown.config.js rename to packages/ckeditor5-dev-utils/rolldown.config.ts index 3d7d67a67..84f37cdee 100644 --- a/packages/ckeditor5-dev-utils/rolldown.config.js +++ b/packages/ckeditor5-dev-utils/rolldown.config.ts @@ -3,13 +3,18 @@ * For licensing, see LICENSE.md. */ -import { defineConfig } from 'rolldown'; -import { declarationFilesPlugin } from '../../scripts/plugin-declarations.js'; +import { defineConfig, type RolldownOptions } from 'rolldown'; +import { declarationFiles } from '../ckeditor5-dev-build-tools/src/plugins/declarations.js'; import pkg from './package.json' with { type: 'json' }; +const packageJson = pkg as { + dependencies?: Record; + peerDependencies?: Record; +}; + const externals = [ - ...Object.keys( pkg.dependencies || {} ), - ...Object.keys( pkg.peerDependencies || {} ) + ...Object.keys( packageJson.dependencies || {} ), + ...Object.keys( packageJson.peerDependencies || {} ) ]; export default defineConfig( { @@ -26,7 +31,9 @@ export default defineConfig( { assetFileNames: '[name][extname]' }, plugins: [ - declarationFilesPlugin() + declarationFiles( { + sourceDirectory: 'src' + } ) ], external: id => externals.some( name => id.startsWith( name ) ) -} ); +} ) as RolldownOptions; diff --git a/packages/ckeditor5-dev-web-crawler/package.json b/packages/ckeditor5-dev-web-crawler/package.json index b39da274b..b137f81a1 100644 --- a/packages/ckeditor5-dev-web-crawler/package.json +++ b/packages/ckeditor5-dev-web-crawler/package.json @@ -23,8 +23,8 @@ "dist" ], "scripts": { - "build": "rolldown -c rolldown.config.js", - "dev": "rolldown -c rolldown.config.js --watch", + "build": "rolldown -c rolldown.config.ts", + "dev": "rolldown -c rolldown.config.ts --watch", "test": "vitest run --config vitest.config.ts", "coverage": "vitest run --config vitest.config.ts --coverage", "test:dev": "vitest dev" diff --git a/packages/ckeditor5-dev-web-crawler/rolldown.config.js b/packages/ckeditor5-dev-web-crawler/rolldown.config.ts similarity index 50% rename from packages/ckeditor5-dev-web-crawler/rolldown.config.js rename to packages/ckeditor5-dev-web-crawler/rolldown.config.ts index 5c70b6606..0efbd5dbe 100644 --- a/packages/ckeditor5-dev-web-crawler/rolldown.config.js +++ b/packages/ckeditor5-dev-web-crawler/rolldown.config.ts @@ -3,13 +3,18 @@ * For licensing, see LICENSE.md. */ -import { defineConfig } from 'rolldown'; -import { declarationFilesPlugin } from '../../scripts/plugin-declarations.js'; +import { defineConfig, type RolldownOptions } from 'rolldown'; +import { declarationFiles } from '../ckeditor5-dev-build-tools/src/plugins/declarations.js'; import pkg from './package.json' with { type: 'json' }; +const packageJson = pkg as { + dependencies?: Record; + peerDependencies?: Record; +}; + const externals = [ - ...Object.keys( pkg.dependencies || {} ), - ...Object.keys( pkg.peerDependencies || {} ) + ...Object.keys( packageJson.dependencies || {} ), + ...Object.keys( packageJson.peerDependencies || {} ) ]; export default defineConfig( { @@ -22,7 +27,9 @@ export default defineConfig( { assetFileNames: '[name][extname]' }, plugins: [ - declarationFilesPlugin() + declarationFiles( { + sourceDirectory: 'src' + } ) ], external: id => externals.some( name => id.startsWith( name ) ) -} ); +} ) as RolldownOptions; diff --git a/packages/typedoc-plugins/package.json b/packages/typedoc-plugins/package.json index efc38e055..64610911f 100644 --- a/packages/typedoc-plugins/package.json +++ b/packages/typedoc-plugins/package.json @@ -36,8 +36,8 @@ "typedoc": "^0.28.14" }, "scripts": { - "build": "rolldown -c rolldown.config.js", - "dev": "rolldown -c rolldown.config.js --watch", + "build": "rolldown -c rolldown.config.ts", + "dev": "rolldown -c rolldown.config.ts --watch", "test": "vitest run --config vitest.config.ts", "coverage": "vitest run --config vitest.config.ts --coverage", "test:dev": "vitest dev" diff --git a/packages/typedoc-plugins/rolldown.config.js b/packages/typedoc-plugins/rolldown.config.ts similarity index 50% rename from packages/typedoc-plugins/rolldown.config.js rename to packages/typedoc-plugins/rolldown.config.ts index 5c70b6606..0efbd5dbe 100644 --- a/packages/typedoc-plugins/rolldown.config.js +++ b/packages/typedoc-plugins/rolldown.config.ts @@ -3,13 +3,18 @@ * For licensing, see LICENSE.md. */ -import { defineConfig } from 'rolldown'; -import { declarationFilesPlugin } from '../../scripts/plugin-declarations.js'; +import { defineConfig, type RolldownOptions } from 'rolldown'; +import { declarationFiles } from '../ckeditor5-dev-build-tools/src/plugins/declarations.js'; import pkg from './package.json' with { type: 'json' }; +const packageJson = pkg as { + dependencies?: Record; + peerDependencies?: Record; +}; + const externals = [ - ...Object.keys( pkg.dependencies || {} ), - ...Object.keys( pkg.peerDependencies || {} ) + ...Object.keys( packageJson.dependencies || {} ), + ...Object.keys( packageJson.peerDependencies || {} ) ]; export default defineConfig( { @@ -22,7 +27,9 @@ export default defineConfig( { assetFileNames: '[name][extname]' }, plugins: [ - declarationFilesPlugin() + declarationFiles( { + sourceDirectory: 'src' + } ) ], external: id => externals.some( name => id.startsWith( name ) ) -} ); +} ) as RolldownOptions; diff --git a/scripts/plugin-declarations.js b/scripts/plugin-declarations.js deleted file mode 100644 index 585d29a7a..000000000 --- a/scripts/plugin-declarations.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -import { readFileSync, globSync } from 'node:fs'; -import { isolatedDeclaration } from 'oxc-transform'; -import path from 'upath'; - -const declarationExtensions = new Set( [ '.mts', '.cts' ] ); - -function getTypeScriptSourceFiles( directoryPath ) { - const sourceFileNames = globSync( '**/*.{ts,tsx,mts,cts}', { - cwd: directoryPath, - exclude: [ '**/*.d.ts', '**/*.d.mts', '**/*.d.cts' ] - } ); - - return sourceFileNames.map( file => path.join( directoryPath, file ) ); -} - -function getDeclarationFileName( sourceFileName ) { - const { dir, name, ext } = path.parse( sourceFileName ); - const declarationExtension = declarationExtensions.has( ext ) ? ext : '.ts'; - - return path.join( dir, `${ name }.d${ declarationExtension }` ); -} - -export function declarationFilesPlugin() { - const sourceDirectoryPath = path.join( process.cwd(), 'src' ); - - return { - name: 'emit-declaration-files', - - async generateBundle() { - const sourceFilePaths = getTypeScriptSourceFiles( sourceDirectoryPath ); - - await Promise.all( sourceFilePaths.map( async sourceFilePath => { - const filename = path.relative( sourceDirectoryPath, sourceFilePath ); - const source = readFileSync( sourceFilePath, 'utf8' ); - const { errors, code } = await isolatedDeclaration( filename, source, { - sourcemap: false, - stripInternal: true - } ); - - if ( errors.length ) { - const errorMessage = errors - .map( error => [ error.message, error.codeframe ].filter( Boolean ).join( '\n' ) ) - .join( '\n\n' ); - - this.error( `Could not generate a declaration file for "${ filename }".\n${ errorMessage }` ); - } - - this.emitFile( { - type: 'asset', - fileName: getDeclarationFileName( filename ), - source: code - } ); - } ) ); - } - }; -} From a820808271fa68caa40c6066a5e1b76f9f998ea0 Mon Sep 17 00:00:00 2001 From: Filip Sobol Date: Fri, 15 May 2026 11:06:02 +0200 Subject: [PATCH 05/12] Remove `rewrite` API from the `build-tools` package. --- ...07_ci_4316_use_rolldown_for_build_tools.md | 9 +++ packages/ckeditor5-dev-build-tools/README.md | 12 --- .../ckeditor5-dev-build-tools/src/build.ts | 2 - .../ckeditor5-dev-build-tools/src/config.ts | 81 +++++-------------- .../tests/build/arguments.test.ts | 8 -- .../tests/build/build.test.ts | 72 +---------------- .../import-from-packages.js} | 4 +- .../export-from-commercial.js | 6 -- .../export-from-core.js | 6 -- .../import-from-core.js | 8 -- .../tests/config/config.test.ts | 37 ++------- 11 files changed, 38 insertions(+), 207 deletions(-) create mode 100644 .changelog/20260515104307_ci_4316_use_rolldown_for_build_tools.md rename packages/ckeditor5-dev-build-tools/tests/build/fixtures/{data-for-rewrites-tests/import-from-both.js => data-for-aggregate-rewrites-tests/import-from-packages.js} (57%) delete mode 100644 packages/ckeditor5-dev-build-tools/tests/build/fixtures/data-for-rewrites-tests/export-from-commercial.js delete mode 100644 packages/ckeditor5-dev-build-tools/tests/build/fixtures/data-for-rewrites-tests/export-from-core.js delete mode 100644 packages/ckeditor5-dev-build-tools/tests/build/fixtures/data-for-rewrites-tests/import-from-core.js diff --git a/.changelog/20260515104307_ci_4316_use_rolldown_for_build_tools.md b/.changelog/20260515104307_ci_4316_use_rolldown_for_build_tools.md new file mode 100644 index 000000000..47f753ae0 --- /dev/null +++ b/.changelog/20260515104307_ci_4316_use_rolldown_for_build_tools.md @@ -0,0 +1,9 @@ +--- +type: Major breaking change +scope: + - ckeditor5-dev-build-tools +--- + +Removed output path rewriting from `ckeditor5-dev-build-tools` except for package imports rewritten to `ckeditor5` and `ckeditor5-premium-features` in browser builds. + +The `rewrite` JavaScript API option was removed. Imports using `ckeditor5/src/*` and `ckeditor5-collaboration/src/*` are no longer rewritten automatically. diff --git a/packages/ckeditor5-dev-build-tools/README.md b/packages/ckeditor5-dev-build-tools/README.md index 7740b74d9..2d9b2ab9a 100644 --- a/packages/ckeditor5-dev-build-tools/README.md +++ b/packages/ckeditor5-dev-build-tools/README.md @@ -184,18 +184,6 @@ When using the JavaScript API, the option must be an array. **Example value:** `external: [ 'lodash', 'moment' ]` -#### `rewrite` - -**Type:** `string[]` - -**Default value:** `[]` - -A list of imports to rewrite in the output file. This option can be used if one of the dependencies provided in `external` has a separate build for the new install methods that should be used instead of the one used in the source code. - -This option is only available for the JavaScript API. - -**Example value:** `rewrite: [ 'dependency', 'dependency/dist/index.js' ]` - ## Changelog See the [`CHANGELOG.md`](https://github.com/ckeditor/ckeditor5-dev/blob/master/packages/ckeditor5-dev-bump-year/CHANGELOG.md) file. diff --git a/packages/ckeditor5-dev-build-tools/src/build.ts b/packages/ckeditor5-dev-build-tools/src/build.ts index ae443d3c4..d1dfc9ae8 100644 --- a/packages/ckeditor5-dev-build-tools/src/build.ts +++ b/packages/ckeditor5-dev-build-tools/src/build.ts @@ -22,7 +22,6 @@ export interface BuildOptions { globals: GlobalsOption | Array; banner: string; external: Array; - rewrite: Array<[string, string]>; declarations: boolean; translations: string; sourceMap: boolean; @@ -41,7 +40,6 @@ export const defaultOptions: BuildOptions = { globals: {}, banner: '', external: [], - rewrite: [], declarations: false, translations: '', sourceMap: false, diff --git a/packages/ckeditor5-dev-build-tools/src/config.ts b/packages/ckeditor5-dev-build-tools/src/config.ts index 604298e5d..2df0d892b 100644 --- a/packages/ckeditor5-dev-build-tools/src/config.ts +++ b/packages/ckeditor5-dev-build-tools/src/config.ts @@ -26,7 +26,6 @@ export async function getRolldownConfig( options: BuildOptions ): Promise getOutputPath( id, { - browser, - configuredRewrites, - coreRewriteSet, - commercialRewriteSet - } ) + paths: id => { + if ( !browser ) { + return id; + } + + if ( coreRewriteSet.has( id ) ) { + return 'ckeditor5'; + } + + if ( commercialRewriteSet.has( id ) ) { + return 'ckeditor5-premium-features'; + } + + return id; + } }, experimental: { nativeMagicString: true, @@ -186,52 +193,6 @@ export async function getRolldownConfig( options: BuildOptions ): Promise; - coreRewriteSet: Set; - commercialRewriteSet: Set; - } -): string { - const configuredRewrite = configuredRewrites.get( id ); - - if ( configuredRewrite !== undefined ) { - return configuredRewrite; - } - - const coreSourceImport = id.match( /ckeditor5\/src\/([a-z-]+)(?:[a-z-/.]+)?/ ); - - if ( coreSourceImport ) { - return browser ? 'ckeditor5' : `@ckeditor/ckeditor5-${ coreSourceImport[ 1 ] }/dist/index.js`; - } - - const commercialSourceImport = id.match( /ckeditor5-collaboration\/src\/([a-z-]+)(?:[a-z-/.]+)?/ ); - - if ( commercialSourceImport ) { - return browser ? 'ckeditor5-premium-features' : 'ckeditor5-collaboration/dist/index.js'; - } - - if ( coreRewriteSet.has( id ) ) { - return browser ? 'ckeditor5' : `${ id }/dist/index.js`; - } - - if ( commercialRewriteSet.has( id ) ) { - return browser ? 'ckeditor5-premium-features' : `${ id }/dist/index.js`; - } - - return id; -} - /** * Returns a list of keys in `package.json` file of a given dependency. */ diff --git a/packages/ckeditor5-dev-build-tools/tests/build/arguments.test.ts b/packages/ckeditor5-dev-build-tools/tests/build/arguments.test.ts index 8f281ffb9..5efbde311 100644 --- a/packages/ckeditor5-dev-build-tools/tests/build/arguments.test.ts +++ b/packages/ckeditor5-dev-build-tools/tests/build/arguments.test.ts @@ -252,14 +252,6 @@ test( '.globals (empty object)', async () => { expect( spy ).toHaveBeenCalledWith( expect.objectContaining( { globals: {} } ) ); } ); -test( '.rewrite', async () => { - const spy = getConfigMock(); - - await build( { rewrite: [ [ 'foo', 'bar' ] ] } ); - - expect( spy ).toHaveBeenCalledWith( expect.objectContaining( { rewrite: [ [ 'foo', 'bar' ] ] } ) ); -} ); - test( '.declarations', async () => { const spy = getConfigMock(); diff --git a/packages/ckeditor5-dev-build-tools/tests/build/build.test.ts b/packages/ckeditor5-dev-build-tools/tests/build/build.test.ts index f41c09d33..585419806 100644 --- a/packages/ckeditor5-dev-build-tools/tests/build/build.test.ts +++ b/packages/ckeditor5-dev-build-tools/tests/build/build.test.ts @@ -6,7 +6,6 @@ import { test, expect, vi, beforeEach } from 'vitest'; import upath from 'upath'; import * as Rolldown from 'rolldown'; -import { readFileSync } from 'node:fs'; import { build } from '../../src/build.js'; import { mockGetUserDependency } from '../_utils/utils.js'; @@ -453,70 +452,12 @@ test( 'Error with file context includes frame if provided', async () => { await expect( fn ).rejects.toThrow( /Error occurred when processing the file(.*)FRAME/s ); } ); -/** - * Mocking real CKE5 packages and testing output path rewrites. - */ - -test( 'Replace - export from core (browser = false)', async () => { - const inputFileContent = readFileSync( upath.join( process.cwd(), 'data-for-rewrites-tests', 'export-from-core.js' ), 'utf-8' ); - - await mockCoreDependencies(); - - const { output } = await build( { - input: 'data-for-rewrites-tests/export-from-core.js', - external: [ - 'ckeditor5' - ] - } ); - - expect( inputFileContent ).toContain( 'export * from \'@ckeditor/ckeditor5-core\'' ); - expect( output[ 0 ].code ).toContain( 'export * from "@ckeditor/ckeditor5-core/dist/index.js"' ); -} ); - -test( 'Replace - export from commercial (browser = false)', async () => { - const inputFileContent = readFileSync( upath.join( process.cwd(), 'data-for-rewrites-tests', 'export-from-commercial.js' ), 'utf-8' ); - - await mockCoreDependencies(); - await mockCommercialDependencies(); - - const { output } = await build( { - input: 'data-for-rewrites-tests/export-from-commercial.js', - external: [ - 'ckeditor5', - 'ckeditor5-premium-features' - ] - } ); - - expect( inputFileContent ).toContain( 'export * from \'@ckeditor/ckeditor5-ai\'' ); - expect( output[ 0 ].code ).toContain( 'export * from "@ckeditor/ckeditor5-ai/dist/index.js"' ); -} ); - -test( 'Replace - import from core (browser = true)', async () => { - const inputFileContent = readFileSync( upath.join( process.cwd(), 'data-for-rewrites-tests', 'import-from-core.js' ), 'utf-8' ); - - await mockCoreDependencies(); - - const { output } = await build( { - input: 'data-for-rewrites-tests/import-from-core.js', - external: [ - 'ckeditor5' - ], - browser: true, - name: 'ckeditor5-premium-features' - } ); - - expect( inputFileContent ).toContain( 'import { Plugin } from \'ckeditor5/src/core.js\'' ); - expect( output[ 0 ].code ).toContain( 'import { Plugin } from "ckeditor5"' ); -} ); - -test( 'Replace - import from core and from commercial (browser = true)', async () => { - const inputFileContent = readFileSync( upath.join( process.cwd(), 'data-for-rewrites-tests', 'import-from-both.js' ), 'utf-8' ); - +test( 'Rewrites package imports to aggregate packages in browser builds', async () => { await mockCoreDependencies(); await mockCommercialDependencies(); const { output } = await build( { - input: 'data-for-rewrites-tests/import-from-both.js', + input: 'data-for-aggregate-rewrites-tests/import-from-packages.js', external: [ 'ckeditor5', 'ckeditor5-premium-features' @@ -525,17 +466,8 @@ test( 'Replace - import from core and from commercial (browser = true)', async ( name: 'ckeditor5-premium-features' } ); - expect( inputFileContent ).toContain( 'import { Plugin } from \'ckeditor5/src/core.js\'' ); - expect( output[ 0 ].code ).toContain( 'import { Plugin } from "ckeditor5"' ); - - expect( inputFileContent ).toContain( 'import { Command } from \'@ckeditor/ckeditor5-core\'' ); expect( output[ 0 ].code ).toContain( 'import { Command } from "ckeditor5"' ); - - expect( inputFileContent ).toContain( 'import { AIAssistant } from \'@ckeditor/ckeditor5-ai\'' ); expect( output[ 0 ].code ).toContain( 'import { AIAssistant } from "ckeditor5-premium-features"' ); - - expect( inputFileContent ).toContain( 'import { Users } from \'ckeditor5-collaboration/src/collaboration-core.js\'' ); - expect( output[ 0 ].code ).toContain( 'import { Users } from "ckeditor5-premium-features"' ); } ); test( 'should not throw when processing a file including a dynamic import expression', async () => { diff --git a/packages/ckeditor5-dev-build-tools/tests/build/fixtures/data-for-rewrites-tests/import-from-both.js b/packages/ckeditor5-dev-build-tools/tests/build/fixtures/data-for-aggregate-rewrites-tests/import-from-packages.js similarity index 57% rename from packages/ckeditor5-dev-build-tools/tests/build/fixtures/data-for-rewrites-tests/import-from-both.js rename to packages/ckeditor5-dev-build-tools/tests/build/fixtures/data-for-aggregate-rewrites-tests/import-from-packages.js index e983b093c..4635c7f05 100644 --- a/packages/ckeditor5-dev-build-tools/tests/build/fixtures/data-for-rewrites-tests/import-from-both.js +++ b/packages/ckeditor5-dev-build-tools/tests/build/fixtures/data-for-aggregate-rewrites-tests/import-from-packages.js @@ -3,9 +3,7 @@ * For licensing, see LICENSE.md. */ -import { Plugin } from 'ckeditor5/src/core.js'; import { Command } from '@ckeditor/ckeditor5-core'; import { AIAssistant } from '@ckeditor/ckeditor5-ai'; -import { Users } from 'ckeditor5-collaboration/src/collaboration-core.js'; -console.log( Plugin, Command, AIAssistant, Users ); +console.log( Command, AIAssistant ); diff --git a/packages/ckeditor5-dev-build-tools/tests/build/fixtures/data-for-rewrites-tests/export-from-commercial.js b/packages/ckeditor5-dev-build-tools/tests/build/fixtures/data-for-rewrites-tests/export-from-commercial.js deleted file mode 100644 index c71e7fff0..000000000 --- a/packages/ckeditor5-dev-build-tools/tests/build/fixtures/data-for-rewrites-tests/export-from-commercial.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -export * from '@ckeditor/ckeditor5-ai'; diff --git a/packages/ckeditor5-dev-build-tools/tests/build/fixtures/data-for-rewrites-tests/export-from-core.js b/packages/ckeditor5-dev-build-tools/tests/build/fixtures/data-for-rewrites-tests/export-from-core.js deleted file mode 100644 index 9ece79d38..000000000 --- a/packages/ckeditor5-dev-build-tools/tests/build/fixtures/data-for-rewrites-tests/export-from-core.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -export * from '@ckeditor/ckeditor5-core'; diff --git a/packages/ckeditor5-dev-build-tools/tests/build/fixtures/data-for-rewrites-tests/import-from-core.js b/packages/ckeditor5-dev-build-tools/tests/build/fixtures/data-for-rewrites-tests/import-from-core.js deleted file mode 100644 index decf15f99..000000000 --- a/packages/ckeditor5-dev-build-tools/tests/build/fixtures/data-for-rewrites-tests/import-from-core.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -import { Plugin } from 'ckeditor5/src/core.js'; - -console.log( Plugin ); diff --git a/packages/ckeditor5-dev-build-tools/tests/config/config.test.ts b/packages/ckeditor5-dev-build-tools/tests/config/config.test.ts index 501b17921..a5eac9e11 100644 --- a/packages/ckeditor5-dev-build-tools/tests/config/config.test.ts +++ b/packages/ckeditor5-dev-build-tools/tests/config/config.test.ts @@ -6,7 +6,7 @@ import { test, expect } from 'vitest'; import { getRolldownConfig } from '../../src/config.js'; import { mockGetUserDependency } from '../_utils/utils.js'; -import type { OutputOptions, Plugin, RolldownOptions } from 'rolldown'; +import type { OutputOptions, Plugin } from 'rolldown'; type Options = Parameters[0]; @@ -16,7 +16,6 @@ const defaults: Options = { tsconfig: '', banner: '', external: [], - rewrite: [], globals: [], declarations: false, translations: '', @@ -33,16 +32,6 @@ function getConfig( config: Partial = {} ): ReturnType { const input = process.cwd() + '/src/main.ts'; const config = await getConfig( { input } ); @@ -117,12 +106,8 @@ test( '--external automatically adds packages that make up the "ckeditor5"', asy } ); expect( ( config.external as Function )( 'ckeditor5' ) ).toBe( true ); - expect( ( config.external as Function )( 'ckeditor5/src/ui.js' ) ).toBe( true ); expect( ( config.external as Function )( '@ckeditor/ckeditor5-core' ) ).toBe( true ); expect( ( config.external as Function )( '@ckeditor/ckeditor5-code-block/theme/codeblock.css' ) ).toBe( false ); - - expect( getOutputPath( config, 'ckeditor5/src/ui.js' ) ).toBe( '@ckeditor/ckeditor5-ui/dist/index.js' ); - expect( getOutputPath( config, '@ckeditor/ckeditor5-core' ) ).toBe( '@ckeditor/ckeditor5-core/dist/index.js' ); } ); test( '--external automatically adds packages that make up the "ckeditor5-premium-features"', async () => { @@ -143,12 +128,9 @@ test( '--external automatically adds packages that make up the "ckeditor5-premiu } ); expect( ( config.external as Function )( 'ckeditor5-premium-features' ) ).toBe( true ); - expect( ( config.external as Function )( 'ckeditor5-collaboration/src/collaboration-core.js' ) ).toBe( true ); + expect( ( config.external as Function )( 'ckeditor5-collaboration' ) ).toBe( true ); expect( ( config.external as Function )( '@ckeditor/ckeditor5-case-change' ) ).toBe( true ); expect( ( config.external as Function )( '@ckeditor/ckeditor5-real-time-collaboration/theme/usermarkers.css' ) ).toBe( false ); - - expect( getOutputPath( config, 'ckeditor5-collaboration/src/collaboration-core.js' ) ).toBe( 'ckeditor5-collaboration/dist/index.js' ); - expect( getOutputPath( config, '@ckeditor/ckeditor5-case-change' ) ).toBe( '@ckeditor/ckeditor5-case-change/dist/index.js' ); } ); test( '--external rewrites CKEditor paths to aggregate packages in browser builds', async () => { @@ -180,19 +162,10 @@ test( '--external rewrites CKEditor paths to aggregate packages in browser build browser: true } ); - expect( getOutputPath( config, 'ckeditor5/src/core.js' ) ).toBe( 'ckeditor5' ); - expect( getOutputPath( config, '@ckeditor/ckeditor5-core' ) ).toBe( 'ckeditor5' ); - expect( getOutputPath( config, 'ckeditor5-collaboration/src/collaboration-core.js' ) ).toBe( 'ckeditor5-premium-features' ); - expect( getOutputPath( config, '@ckeditor/ckeditor5-ai' ) ).toBe( 'ckeditor5-premium-features' ); -} ); - -test( '--rewrite maps output paths', async () => { - const config = await getConfig( { - rewrite: [ [ 'dependency', 'dependency/dist/index.js' ] ] - } ); + const paths = ( config.output as OutputOptions ).paths as Function; - expect( getOutputPath( config, 'dependency' ) ).toBe( 'dependency/dist/index.js' ); - expect( getOutputPath( config, 'unmatched-dependency' ) ).toBe( 'unmatched-dependency' ); + expect( paths( '@ckeditor/ckeditor5-core' ) ).toBe( 'ckeditor5' ); + expect( paths( '@ckeditor/ckeditor5-ai' ) ).toBe( 'ckeditor5-premium-features' ); } ); test( '--external doesn\'t fail when "ckeditor5-premium-features" is not installed', async () => { From a287a706d90ba43c3d0e5bb56a9f2d8069d81684 Mon Sep 17 00:00:00 2001 From: Filip Sobol Date: Mon, 18 May 2026 13:16:36 +0200 Subject: [PATCH 06/12] Add temporary plugin to restore `@preserve` annotations in `@__PURE__` comments. --- .../ckeditor5-dev-build-tools/src/config.ts | 4 + .../src/plugins/preservePureAnnotations.ts | 83 +++++++++++++++++++ .../tests/build/build.test.ts | 27 ++++++ .../build/fixtures/src/pure-annotation.js | 10 +++ 4 files changed, 124 insertions(+) create mode 100644 packages/ckeditor5-dev-build-tools/src/plugins/preservePureAnnotations.ts create mode 100644 packages/ckeditor5-dev-build-tools/tests/build/fixtures/src/pure-annotation.js diff --git a/packages/ckeditor5-dev-build-tools/src/config.ts b/packages/ckeditor5-dev-build-tools/src/config.ts index 2df0d892b..a8e22d07a 100644 --- a/packages/ckeditor5-dev-build-tools/src/config.ts +++ b/packages/ckeditor5-dev-build-tools/src/config.ts @@ -12,6 +12,7 @@ import type { BuildOptions } from './build.js'; import { addBanner } from './plugins/banner.js'; import { bundleCss } from './plugins/bundleCss.js'; import { declarationFiles } from './plugins/declarations.js'; +import { preservePureAnnotations } from './plugins/preservePureAnnotations.js'; import { rawImport } from './plugins/rawImport.js'; import { splitCss } from './plugins/splitCss.js'; import { translations as translationsPlugin } from './plugins/translations.js'; @@ -94,6 +95,9 @@ export async function getRolldownConfig( options: BuildOptions ): Promise { if ( !browser ) { return id; diff --git a/packages/ckeditor5-dev-build-tools/src/plugins/preservePureAnnotations.ts b/packages/ckeditor5-dev-build-tools/src/plugins/preservePureAnnotations.ts new file mode 100644 index 000000000..65f41da97 --- /dev/null +++ b/packages/ckeditor5-dev-build-tools/src/plugins/preservePureAnnotations.ts @@ -0,0 +1,83 @@ +/** + * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import MagicString from 'magic-string'; +import { SourceMapConsumer, SourceMapGenerator } from 'source-map'; +import type { OutputAsset, OutputChunk, Plugin } from 'rolldown'; + +const PURE_ANNOTATION = '/* @__PURE__ */'; +const PRESERVED_PURE_ANNOTATION = '/* @__PURE__ -- @preserve */'; + +/** + * Restores the `@preserve` marker removed by Rolldown when it normalizes pure annotations. + * + * This plugin can be removed once https://github.com/rolldown/rolldown/issues/9408 is fixed. + */ +export function preservePureAnnotations(): Plugin { + return { + name: 'cke5-preserve-pure-annotations', + + async generateBundle( outputOptions, bundle ) { + const updateSourceMap = async ( fileName: string, magic: MagicString ): Promise => { + if ( !outputOptions.sourcemap ) { + return; + } + + const sourceMapName = fileName + '.map'; + const originalSourceMap = bundle[ sourceMapName ] as OutputAsset | undefined; + + if ( !originalSourceMap ) { + return; + } + + const newSourceMap = magic.generateMap( { + hires: 'boundary', + file: sourceMapName, + source: fileName, + includeContent: true + } ); + + const generator = SourceMapGenerator.fromSourceMap( + await new SourceMapConsumer( newSourceMap ) + ); + + const originalMapConsumer = await new SourceMapConsumer( + JSON.parse( originalSourceMap.source.toString() ) + ); + + generator.applySourceMap( + originalMapConsumer, + fileName + ); + + originalSourceMap.source = generator.toString(); + }; + + const updateChunk = ( chunk: OutputChunk ) => { + if ( !chunk.code.includes( PURE_ANNOTATION ) ) { + return; + } + + const magic = new MagicString( chunk.code ); + let index = chunk.code.indexOf( PURE_ANNOTATION ); + + while ( index !== -1 ) { + magic.overwrite( index, index + PURE_ANNOTATION.length, PRESERVED_PURE_ANNOTATION ); + index = chunk.code.indexOf( PURE_ANNOTATION, index + PURE_ANNOTATION.length ); + } + + chunk.code = magic.toString(); + + return updateSourceMap( chunk.fileName, magic ); + }; + + for ( const item of Object.values( bundle ) ) { + if ( item.type === 'chunk' ) { + await updateChunk( item ); + } + } + } + }; +} diff --git a/packages/ckeditor5-dev-build-tools/tests/build/build.test.ts b/packages/ckeditor5-dev-build-tools/tests/build/build.test.ts index 585419806..412c09873 100644 --- a/packages/ckeditor5-dev-build-tools/tests/build/build.test.ts +++ b/packages/ckeditor5-dev-build-tools/tests/build/build.test.ts @@ -6,6 +6,7 @@ import { test, expect, vi, beforeEach } from 'vitest'; import upath from 'upath'; import * as Rolldown from 'rolldown'; +import { SourceMapConsumer } from 'source-map'; import { build } from '../../src/build.js'; import { mockGetUserDependency } from '../_utils/utils.js'; @@ -378,6 +379,32 @@ test( 'Minification doesn\'t remove banner', async () => { expect( output[ 0 ].code ).toContain( 'TEST BANNER' ); } ); +test( 'Pure annotations preserve the marker used by post-processing tools', async () => { + const { output } = await build( { + input: 'src/pure-annotation.js', + sourceMap: true + } ); + const chunk = output.find( item => item.type === 'chunk' && item.fileName === 'index.js' ) as Rolldown.OutputChunk; + const sourceMap = output.find( item => item.type === 'asset' && item.fileName === 'index.js.map' ) as Rolldown.OutputAsset; + const sourceMapContent = JSON.parse( sourceMap.source.toString() ); + const originalSourceIndex = sourceMapContent.sources.findIndex( ( source: string ) => source.endsWith( 'pure-annotation.js' ) ); + const source = sourceMapContent.sourcesContent[ originalSourceIndex ]; + const originalLine = source.split( '\n' ).findIndex( ( line: string ) => line.includes( 'factory()' ) ) + 1; + const originalColumn = source.split( '\n' )[ originalLine - 1 ].indexOf( 'factory()' ); + const generatedLine = chunk.code.split( '\n' ).findIndex( line => line.includes( 'factory()' ) ) + 1; + const generatedColumn = chunk.code.split( '\n' )[ generatedLine - 1 ]!.indexOf( 'factory()' ); + const consumer = await new SourceMapConsumer( sourceMapContent ); + const originalPosition = consumer.originalPositionFor( { + line: generatedLine, + column: generatedColumn + } ); + + expect( chunk.code ).toContain( '/* @__PURE__ -- @preserve */ factory()' ); + expect( originalPosition.source ).toBe( sourceMapContent.sources[ originalSourceIndex ] ); + expect( originalPosition.line ).toBe( originalLine ); + expect( originalPosition.column ).toBe( originalColumn ); +} ); + /** * Overriding */ diff --git a/packages/ckeditor5-dev-build-tools/tests/build/fixtures/src/pure-annotation.js b/packages/ckeditor5-dev-build-tools/tests/build/fixtures/src/pure-annotation.js new file mode 100644 index 000000000..0405b8cf8 --- /dev/null +++ b/packages/ckeditor5-dev-build-tools/tests/build/fixtures/src/pure-annotation.js @@ -0,0 +1,10 @@ +/** + * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +export const value = /* #__PURE__ -- @preserve */ factory(); + +function factory() { + return {}; +} From c3e7516589e004ff371c30886364ab66ed1b67aa Mon Sep 17 00:00:00 2001 From: Filip Sobol Date: Mon, 18 May 2026 13:39:46 +0200 Subject: [PATCH 07/12] Use more precise path in `tsconfig.json`. --- packages/ckeditor5-dev-build-tools/rolldown.config.ts | 4 ++-- packages/ckeditor5-dev-changelog/rolldown.config.ts | 4 ++-- packages/ckeditor5-dev-license-checker/rolldown.config.ts | 4 ++-- packages/ckeditor5-dev-utils/rolldown.config.ts | 4 ++-- packages/ckeditor5-dev-web-crawler/rolldown.config.ts | 4 ++-- packages/typedoc-plugins/rolldown.config.ts | 4 ++-- tsconfig.json | 3 ++- 7 files changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/ckeditor5-dev-build-tools/rolldown.config.ts b/packages/ckeditor5-dev-build-tools/rolldown.config.ts index 436e0cb66..6247ee3bd 100644 --- a/packages/ckeditor5-dev-build-tools/rolldown.config.ts +++ b/packages/ckeditor5-dev-build-tools/rolldown.config.ts @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md. */ -import { defineConfig, type RolldownOptions } from 'rolldown'; +import { defineConfig } from 'rolldown'; import { declarationFiles } from './src/plugins/declarations.js'; import pkg from './package.json' with { type: 'json' }; @@ -32,4 +32,4 @@ export default defineConfig( { } ) ], external: id => externals.some( name => id.startsWith( name ) ) -} ) as RolldownOptions; +} ); diff --git a/packages/ckeditor5-dev-changelog/rolldown.config.ts b/packages/ckeditor5-dev-changelog/rolldown.config.ts index 25e7f680a..6471eea40 100644 --- a/packages/ckeditor5-dev-changelog/rolldown.config.ts +++ b/packages/ckeditor5-dev-changelog/rolldown.config.ts @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md. */ -import { defineConfig, type RolldownOptions } from 'rolldown'; +import { defineConfig } from 'rolldown'; import { declarationFiles } from '../ckeditor5-dev-build-tools/src/plugins/declarations.js'; import pkg from './package.json' with { type: 'json' }; @@ -35,4 +35,4 @@ export default defineConfig( { sourceDirectory: 'src' } ) ] -} ) as RolldownOptions; +} ); diff --git a/packages/ckeditor5-dev-license-checker/rolldown.config.ts b/packages/ckeditor5-dev-license-checker/rolldown.config.ts index c5874dc1a..00647c911 100644 --- a/packages/ckeditor5-dev-license-checker/rolldown.config.ts +++ b/packages/ckeditor5-dev-license-checker/rolldown.config.ts @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md. */ -import { defineConfig, type RolldownOptions } from 'rolldown'; +import { defineConfig } from 'rolldown'; import { declarationFiles } from '../ckeditor5-dev-build-tools/src/plugins/declarations.js'; import pkg from './package.json' with { type: 'json' }; @@ -33,4 +33,4 @@ export default defineConfig( { } ) ], external: id => externals.some( name => id.startsWith( name ) ) -} ) as RolldownOptions; +} ); diff --git a/packages/ckeditor5-dev-utils/rolldown.config.ts b/packages/ckeditor5-dev-utils/rolldown.config.ts index 84f37cdee..95ef4e361 100644 --- a/packages/ckeditor5-dev-utils/rolldown.config.ts +++ b/packages/ckeditor5-dev-utils/rolldown.config.ts @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md. */ -import { defineConfig, type RolldownOptions } from 'rolldown'; +import { defineConfig } from 'rolldown'; import { declarationFiles } from '../ckeditor5-dev-build-tools/src/plugins/declarations.js'; import pkg from './package.json' with { type: 'json' }; @@ -36,4 +36,4 @@ export default defineConfig( { } ) ], external: id => externals.some( name => id.startsWith( name ) ) -} ) as RolldownOptions; +} ); diff --git a/packages/ckeditor5-dev-web-crawler/rolldown.config.ts b/packages/ckeditor5-dev-web-crawler/rolldown.config.ts index 0efbd5dbe..30437262e 100644 --- a/packages/ckeditor5-dev-web-crawler/rolldown.config.ts +++ b/packages/ckeditor5-dev-web-crawler/rolldown.config.ts @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md. */ -import { defineConfig, type RolldownOptions } from 'rolldown'; +import { defineConfig } from 'rolldown'; import { declarationFiles } from '../ckeditor5-dev-build-tools/src/plugins/declarations.js'; import pkg from './package.json' with { type: 'json' }; @@ -32,4 +32,4 @@ export default defineConfig( { } ) ], external: id => externals.some( name => id.startsWith( name ) ) -} ) as RolldownOptions; +} ); diff --git a/packages/typedoc-plugins/rolldown.config.ts b/packages/typedoc-plugins/rolldown.config.ts index 0efbd5dbe..30437262e 100644 --- a/packages/typedoc-plugins/rolldown.config.ts +++ b/packages/typedoc-plugins/rolldown.config.ts @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md. */ -import { defineConfig, type RolldownOptions } from 'rolldown'; +import { defineConfig } from 'rolldown'; import { declarationFiles } from '../ckeditor5-dev-build-tools/src/plugins/declarations.js'; import pkg from './package.json' with { type: 'json' }; @@ -32,4 +32,4 @@ export default defineConfig( { } ) ], external: id => externals.some( name => id.startsWith( name ) ) -} ) as RolldownOptions; +} ); diff --git a/tsconfig.json b/tsconfig.json index a77a4714d..0741d08c9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,8 @@ "noImplicitOverride": true }, "include": [ - "packages/**/*.ts" + "packages/*/src/*.ts", + "packages/*/tests/*.ts" ], "exclude": [ "**/fixtures" From 86b0fbb269c4e9b705fa35df1b2b2efffeb6f14b Mon Sep 17 00:00:00 2001 From: Filip Sobol Date: Mon, 18 May 2026 13:39:59 +0200 Subject: [PATCH 08/12] Remove unused test and dependency. --- package.json | 1 - pnpm-lock.yaml | 225 -------------------------- scripts-tests/plugin-declarations.mjs | 125 -------------- 3 files changed, 351 deletions(-) delete mode 100644 scripts-tests/plugin-declarations.mjs diff --git a/package.json b/package.json index d5e58ca40..563e785d7 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "lint-staged": "^16.4.0", "listr2": "^8.3.3", "minimist": "^1.2.8", - "oxc-transform": "^0.112.0", "semver": "^7.7.4", "typescript": "5.5.4", "upath": "^2.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8dfea6cf3..cd0dc9775 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,9 +58,6 @@ importers: minimist: specifier: ^1.2.8 version: 1.2.8 - oxc-transform: - specifier: ^0.112.0 - version: 0.112.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) semver: specifier: ^7.7.4 version: 7.7.4 @@ -1365,133 +1362,6 @@ packages: '@oxc-project/types@0.72.3': resolution: {integrity: sha512-CfAC4wrmMkUoISpQkFAIfMVvlPfQV3xg7ZlcqPXPOIMQhdKIId44G8W0mCPgtpWdFFAyJ+SFtiM+9vbyCkoVng==} - '@oxc-transform/binding-android-arm-eabi@0.112.0': - resolution: {integrity: sha512-r4LuBaPnOAi0eUOBNi880Fm2tO2omH7N1FRrL6+nyz/AjQ+QPPLtoyZJva0O+sKi1buyN/7IzM5p9m+5ANSDbg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [android] - - '@oxc-transform/binding-android-arm64@0.112.0': - resolution: {integrity: sha512-ve46vQcQrY8eGe8990VSlS9gkD+AogJqbtfOkeua+5sQGQTDgeIRRxOm7ktCo19uZc2bEBwXRJITgosd+NRVmQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@oxc-transform/binding-darwin-arm64@0.112.0': - resolution: {integrity: sha512-ddbmLU3Tr+i7MOynfwAXxUXud3SjJKlv7XNjaq08qiI8Av/QvhXVGc2bMhXkWQSMSBUeTDoiughKjK+Zsb6y/A==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@oxc-transform/binding-darwin-x64@0.112.0': - resolution: {integrity: sha512-TKvmNw96jQZPqYb4pRrzLFDailNB3YS14KNn+x2hwRbqc6CqY96S9PYwyOpVpYdxfoRjYO9WgX9SoS+62a1DPA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@oxc-transform/binding-freebsd-x64@0.112.0': - resolution: {integrity: sha512-YPMkSCDaelO8HHYRMYjm+Q+IfkfIbdtQzwPuasItYkq8UUkNeHNPheNh2JkvQa3c+io3E9ePOgHQ2yihpk7o/Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@oxc-transform/binding-linux-arm-gnueabihf@0.112.0': - resolution: {integrity: sha512-nA7kzQGNEpuTRknst/IJ3l8hqmDmEda3aun6jkXgp7gKxESjuHeaNH04mKISxvJ7fIacvP2g/wtTSnm4u5jL8Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@oxc-transform/binding-linux-arm-musleabihf@0.112.0': - resolution: {integrity: sha512-w8GuLmckKlGc3YujaZKhtbFxziCcosvM2l9GnQjCb/yENWLGDiyQOy0BTAgPGdJwpYTiOeJblEXSuXYvlE1Ong==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@oxc-transform/binding-linux-arm64-gnu@0.112.0': - resolution: {integrity: sha512-9LwwGnJ8+WT0rXcrI8M0RJtDNt91eMqcDPPEvJxhRFHIMcHTy5D5xT+fOl3Us0yMqKo3HUWkbfUYqAp4GoZ3Jw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@oxc-transform/binding-linux-arm64-musl@0.112.0': - resolution: {integrity: sha512-Lg6VOuSd3oXv7J0eGywgqh/086h+qQzIBOD+47pYKMTTJcbDe+f3h/RgGoMKJE5HhiwT5sH1aGEJfIfaYUiVSw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@oxc-transform/binding-linux-ppc64-gnu@0.112.0': - resolution: {integrity: sha512-PXzmj82o1moA4IGphYImTRgc2youTi4VRfyFX3CHwLjxPcQ5JtcsgbDt4QUdOzXZ+zC07s5jf2ZzhRapEOlj2w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@oxc-transform/binding-linux-riscv64-gnu@0.112.0': - resolution: {integrity: sha512-vhJsMsVH/6xwa3bt1LGts33FXUkGjaEGDwsRyp4lIfOjSfQVWMtCmWMFNaA0dW9FVWdD2Gt2fSFBSZ+azDxlpg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@oxc-transform/binding-linux-riscv64-musl@0.112.0': - resolution: {integrity: sha512-cXWFb7z+2IjFUEcXtRwluq9oEG5qnyFCjiu3SWrgYNcWwPdHusv3I/7K5/CTbbi4StoZ5txbi7/iSfDHNyWuRw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@oxc-transform/binding-linux-s390x-gnu@0.112.0': - resolution: {integrity: sha512-eEFu4SRqJTJ20/88KRWmp+jpHKAw0Y1DsnSgpEeXyBIIcsOaLIUMU/TfYWUmqRbvbMV9rmOmI3kp5xWYUq6kSQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@oxc-transform/binding-linux-x64-gnu@0.112.0': - resolution: {integrity: sha512-ST1MDT+TlOyZ1c5btrGinRSUW2Jf4Pa+0gdKwsyjDSOC3dxy2ZNkN3mosTf4ywc3J+mxfYKqtjs7zSwHz03ILA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@oxc-transform/binding-linux-x64-musl@0.112.0': - resolution: {integrity: sha512-ISQoA3pD4cyTGpf9sXXeerH6pL2L6EIpdy6oAy2ttkswyVFDyQNVOVIGIdLZDgbpmqGljxZnWqt/J/N68pQaig==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@oxc-transform/binding-openharmony-arm64@0.112.0': - resolution: {integrity: sha512-UOGVrGIv7yLJovyEXEyUTADuLq98vd/cbMHFLJweRXD+11I8Tn4jASi4WzdsN8C3BVYGRHrXH2NlSBmhz33a4g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@oxc-transform/binding-wasm32-wasi@0.112.0': - resolution: {integrity: sha512-XIX7Gpq9koAvzBVHDlVFHM79r5uOVK6kTEsdsN4qaajpjkgtv4tdsAOKIYK6l7fUbsbE6xS+6w1+yRFrDeC1kg==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - - '@oxc-transform/binding-win32-arm64-msvc@0.112.0': - resolution: {integrity: sha512-EgXef9kOne9BNsbYBbuRqxk2hteT0xsAGcx/VbtCBMJYNj8fANFhT271DUSOgfa4DAgrQQmsyt/Kr1aV9mpU9w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@oxc-transform/binding-win32-ia32-msvc@0.112.0': - resolution: {integrity: sha512-6QaB0qjNaou2YR+blncHdw7j0e26IOwOIjLbhVGDeuf9+4rjJeiqRXJ2hOtCcS4zblnao/MjdgQuZ3fM0nl+Kw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ia32] - os: [win32] - - '@oxc-transform/binding-win32-x64-msvc@0.112.0': - resolution: {integrity: sha512-FRKYlY959QeqRPx9kXs0HjU2xuXPT1cdF+vvA200D9uAX/KLcC34MwRqUKTYml4kCc2Vf/P2pBR9cQuBm3zECQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -4383,10 +4253,6 @@ packages: resolution: {integrity: sha512-JYQeJKDcUTTZ/uTdJ+fZBGFjAjkLD1h0p3Tf44ZYXRcoMk+57d81paNPFAAwzrzzqhZmkGvKKXDxwyhJXYZlpg==} engines: {node: '>=14.0.0'} - oxc-transform@0.112.0: - resolution: {integrity: sha512-cIRRvZgrHfsAHrkt8LWdAX4+Do8R0MzQSfeo9yzErzHeYiuyNiP4PCTPbOy/wBXL4MYzt3ebrBa5jt3akQkKAg==} - engines: {node: ^20.19.0 || >=22.12.0} - oxc-walker@0.3.0: resolution: {integrity: sha512-mGGgl9dmYHUX7Z3bxXhibwarI0fJVj2E64FNIOZQWUDvEeIPyJTe5ElyJmp4nmDdfdnrlG0bhdR+bR9D6DM/dA==} peerDependencies: @@ -6374,71 +6240,6 @@ snapshots: '@oxc-project/types@0.72.3': {} - '@oxc-transform/binding-android-arm-eabi@0.112.0': - optional: true - - '@oxc-transform/binding-android-arm64@0.112.0': - optional: true - - '@oxc-transform/binding-darwin-arm64@0.112.0': - optional: true - - '@oxc-transform/binding-darwin-x64@0.112.0': - optional: true - - '@oxc-transform/binding-freebsd-x64@0.112.0': - optional: true - - '@oxc-transform/binding-linux-arm-gnueabihf@0.112.0': - optional: true - - '@oxc-transform/binding-linux-arm-musleabihf@0.112.0': - optional: true - - '@oxc-transform/binding-linux-arm64-gnu@0.112.0': - optional: true - - '@oxc-transform/binding-linux-arm64-musl@0.112.0': - optional: true - - '@oxc-transform/binding-linux-ppc64-gnu@0.112.0': - optional: true - - '@oxc-transform/binding-linux-riscv64-gnu@0.112.0': - optional: true - - '@oxc-transform/binding-linux-riscv64-musl@0.112.0': - optional: true - - '@oxc-transform/binding-linux-s390x-gnu@0.112.0': - optional: true - - '@oxc-transform/binding-linux-x64-gnu@0.112.0': - optional: true - - '@oxc-transform/binding-linux-x64-musl@0.112.0': - optional: true - - '@oxc-transform/binding-openharmony-arm64@0.112.0': - optional: true - - '@oxc-transform/binding-wasm32-wasi@0.112.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': - dependencies: - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - optional: true - - '@oxc-transform/binding-win32-arm64-msvc@0.112.0': - optional: true - - '@oxc-transform/binding-win32-ia32-msvc@0.112.0': - optional: true - - '@oxc-transform/binding-win32-x64-msvc@0.112.0': - optional: true - '@pkgjs/parseargs@0.11.0': optional: true @@ -9724,32 +9525,6 @@ snapshots: '@oxc-parser/binding-win32-arm64-msvc': 0.72.3 '@oxc-parser/binding-win32-x64-msvc': 0.72.3 - oxc-transform@0.112.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): - optionalDependencies: - '@oxc-transform/binding-android-arm-eabi': 0.112.0 - '@oxc-transform/binding-android-arm64': 0.112.0 - '@oxc-transform/binding-darwin-arm64': 0.112.0 - '@oxc-transform/binding-darwin-x64': 0.112.0 - '@oxc-transform/binding-freebsd-x64': 0.112.0 - '@oxc-transform/binding-linux-arm-gnueabihf': 0.112.0 - '@oxc-transform/binding-linux-arm-musleabihf': 0.112.0 - '@oxc-transform/binding-linux-arm64-gnu': 0.112.0 - '@oxc-transform/binding-linux-arm64-musl': 0.112.0 - '@oxc-transform/binding-linux-ppc64-gnu': 0.112.0 - '@oxc-transform/binding-linux-riscv64-gnu': 0.112.0 - '@oxc-transform/binding-linux-riscv64-musl': 0.112.0 - '@oxc-transform/binding-linux-s390x-gnu': 0.112.0 - '@oxc-transform/binding-linux-x64-gnu': 0.112.0 - '@oxc-transform/binding-linux-x64-musl': 0.112.0 - '@oxc-transform/binding-openharmony-arm64': 0.112.0 - '@oxc-transform/binding-wasm32-wasi': 0.112.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - '@oxc-transform/binding-win32-arm64-msvc': 0.112.0 - '@oxc-transform/binding-win32-ia32-msvc': 0.112.0 - '@oxc-transform/binding-win32-x64-msvc': 0.112.0 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - oxc-walker@0.3.0(oxc-parser@0.72.3): dependencies: estree-walker: 3.0.3 diff --git a/scripts-tests/plugin-declarations.mjs b/scripts-tests/plugin-declarations.mjs deleted file mode 100644 index 54ef1697c..000000000 --- a/scripts-tests/plugin-declarations.mjs +++ /dev/null @@ -1,125 +0,0 @@ -/** - * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { globSync, readFileSync } from 'node:fs'; -import { isolatedDeclaration } from 'oxc-transform'; -import { declarationFilesPlugin } from '../scripts/plugin-declarations.js'; - -vi.mock( 'node:fs', () => ( { - globSync: vi.fn(), - readFileSync: vi.fn() -} ) ); - -vi.mock( 'oxc-transform', () => ( { - isolatedDeclaration: vi.fn() -} ) ); - -describe( 'scripts/plugin-declarations', () => { - beforeEach( () => { - vi.spyOn( process, 'cwd' ).mockReturnValue( '/workspace/package' ); - } ); - - it( 'creates declaration assets for TypeScript source files', async () => { - vi.mocked( globSync ).mockReturnValue( [ - 'foo.ts', - 'nested/bar.tsx', - 'module.mts', - 'legacy.cts' - ] ); - - vi.mocked( readFileSync ).mockImplementation( sourceFilePath => `source:${ sourceFilePath }` ); - vi.mocked( isolatedDeclaration ).mockImplementation( async filename => ( { - errors: [], - code: `declaration:${ filename }` - } ) ); - - const plugin = declarationFilesPlugin(); - const emitFile = vi.fn(); - const error = vi.fn(); - - await plugin.generateBundle.call( { emitFile, error } ); - - expect( globSync ).toHaveBeenCalledWith( '**/*.{ts,tsx,mts,cts}', { - cwd: '/workspace/package/src', - exclude: [ '**/*.d.ts', '**/*.d.mts', '**/*.d.cts' ] - } ); - - expect( readFileSync ).toHaveBeenNthCalledWith( 1, '/workspace/package/src/foo.ts', 'utf8' ); - expect( readFileSync ).toHaveBeenNthCalledWith( 2, '/workspace/package/src/nested/bar.tsx', 'utf8' ); - expect( readFileSync ).toHaveBeenNthCalledWith( 3, '/workspace/package/src/module.mts', 'utf8' ); - expect( readFileSync ).toHaveBeenNthCalledWith( 4, '/workspace/package/src/legacy.cts', 'utf8' ); - - expect( isolatedDeclaration ).toHaveBeenNthCalledWith( 1, 'foo.ts', 'source:/workspace/package/src/foo.ts', { - sourcemap: false, - stripInternal: true - } ); - - expect( isolatedDeclaration ).toHaveBeenNthCalledWith( 2, 'nested/bar.tsx', 'source:/workspace/package/src/nested/bar.tsx', { - sourcemap: false, - stripInternal: true - } ); - - expect( isolatedDeclaration ).toHaveBeenNthCalledWith( 3, 'module.mts', 'source:/workspace/package/src/module.mts', { - sourcemap: false, - stripInternal: true - } ); - - expect( isolatedDeclaration ).toHaveBeenNthCalledWith( 4, 'legacy.cts', 'source:/workspace/package/src/legacy.cts', { - sourcemap: false, - stripInternal: true - } ); - - expect( emitFile ).toHaveBeenNthCalledWith( 1, { - type: 'asset', - fileName: 'foo.d.ts', - source: 'declaration:foo.ts' - } ); - - expect( emitFile ).toHaveBeenNthCalledWith( 2, { - type: 'asset', - fileName: 'nested/bar.d.ts', - source: 'declaration:nested/bar.tsx' - } ); - - expect( emitFile ).toHaveBeenNthCalledWith( 3, { - type: 'asset', - fileName: 'module.d.mts', - source: 'declaration:module.mts' - } ); - - expect( emitFile ).toHaveBeenNthCalledWith( 4, { - type: 'asset', - fileName: 'legacy.d.cts', - source: 'declaration:legacy.cts' - } ); - - expect( error ).not.toHaveBeenCalled(); - } ); - - it( 'throws an error when declaration generation reports problems', async () => { - vi.mocked( globSync ).mockReturnValue( [ 'broken.ts' ] ); - vi.mocked( readFileSync ).mockReturnValue( 'export const broken = true;' ); - vi.mocked( isolatedDeclaration ).mockResolvedValue( { - errors: [ - { message: 'Broken declaration', codeframe: 'line 1' }, - { message: 'Another issue' } - ], - code: '' - } ); - - const plugin = declarationFilesPlugin(); - const emitFile = vi.fn(); - const error = vi.fn( message => { - throw new Error( message ); - } ); - - await expect( plugin.generateBundle.call( { emitFile, error } ) ).rejects.toThrow( - 'Could not generate a declaration file for "broken.ts".\nBroken declaration\nline 1\n\nAnother issue' - ); - - expect( emitFile ).not.toHaveBeenCalled(); - } ); -} ); From 26d1169a0436efbce31435b11674f1c9e8c78fd9 Mon Sep 17 00:00:00 2001 From: Filip Sobol Date: Mon, 18 May 2026 15:01:52 +0200 Subject: [PATCH 09/12] Add missing `@types/shelljs` to `dev-utils` package. --- packages/ckeditor5-dev-utils/package.json | 1 + pnpm-lock.yaml | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/packages/ckeditor5-dev-utils/package.json b/packages/ckeditor5-dev-utils/package.json index 798f58a4a..1d37d77a5 100644 --- a/packages/ckeditor5-dev-utils/package.json +++ b/packages/ckeditor5-dev-utils/package.json @@ -23,6 +23,7 @@ "dist" ], "dependencies": { + "@types/shelljs": "^0.10.0", "@types/through2": "^2.0.41", "babel-loader": "^10.1.1", "cli-cursor": "^5.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd0dc9775..907e726f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -512,6 +512,9 @@ importers: packages/ckeditor5-dev-utils: dependencies: + '@types/shelljs': + specifier: ^0.10.0 + version: 0.10.0 '@types/through2': specifier: ^2.0.41 version: 2.0.41 @@ -1805,6 +1808,9 @@ packages: '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + '@types/shelljs@0.10.0': + resolution: {integrity: sha512-OEfyhE5Ox+FeoHbhrEDwm0kXxntO6nsyMRCFvNsIBHPZu5rV1w2OjPcLclaC/IZ1TlzZPgbeMfwAZEi5N238yQ==} + '@types/sinon-chai@2.7.42': resolution: {integrity: sha512-6M+l7agAuaS4iFwC9KtmcGznH4WYTcF3/Jq/m4jZ5/Gm1Rlufc7o7rl1Sb9++U81NliT+mV2Ri6UQziaJi6opw==} @@ -6628,6 +6634,11 @@ snapshots: '@types/semver@7.7.1': {} + '@types/shelljs@0.10.0': + dependencies: + '@types/node': 22.19.17 + fast-glob: 3.3.3 + '@types/sinon-chai@2.7.42': dependencies: '@types/chai': 4.3.20 From ee6c76aa1b79043b302a7d31f7b0768946959237 Mon Sep 17 00:00:00 2001 From: Filip Sobol Date: Mon, 18 May 2026 15:20:41 +0200 Subject: [PATCH 10/12] Fix paths in `tsconfig.json`. --- tsconfig.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 0741d08c9..d71139582 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,8 +20,8 @@ "noImplicitOverride": true }, "include": [ - "packages/*/src/*.ts", - "packages/*/tests/*.ts" + "packages/*/src/**/*.ts", + "packages/*/tests/**/*.ts" ], "exclude": [ "**/fixtures" From 17787b1d3d3bd159a3ce79d7d9560607966cb2ed Mon Sep 17 00:00:00 2001 From: Filip Sobol Date: Mon, 18 May 2026 16:02:36 +0200 Subject: [PATCH 11/12] Add changelog entries. --- ...0260518155955_ci_4316_use_rolldown_for_build_tools.md | 9 +++++++++ ...0260518155957_ci_4316_use_rolldown_for_build_tools.md | 9 +++++++++ ...0260518155959_ci_4316_use_rolldown_for_build_tools.md | 7 +++++++ ...0260518160000_ci_4316_use_rolldown_for_build_tools.md | 9 +++++++++ ...0260518160002_ci_4316_use_rolldown_for_build_tools.md | 7 +++++++ 5 files changed, 41 insertions(+) create mode 100644 .changelog/20260518155955_ci_4316_use_rolldown_for_build_tools.md create mode 100644 .changelog/20260518155957_ci_4316_use_rolldown_for_build_tools.md create mode 100644 .changelog/20260518155959_ci_4316_use_rolldown_for_build_tools.md create mode 100644 .changelog/20260518160000_ci_4316_use_rolldown_for_build_tools.md create mode 100644 .changelog/20260518160002_ci_4316_use_rolldown_for_build_tools.md diff --git a/.changelog/20260518155955_ci_4316_use_rolldown_for_build_tools.md b/.changelog/20260518155955_ci_4316_use_rolldown_for_build_tools.md new file mode 100644 index 000000000..25761c48e --- /dev/null +++ b/.changelog/20260518155955_ci_4316_use_rolldown_for_build_tools.md @@ -0,0 +1,9 @@ +--- +type: Major breaking change +scope: + - ckeditor5-dev-build-tools +--- + +Replaced Rollup with Rolldown in `ckeditor5-dev-build-tools`. + +Rollup-specific dependencies that are no longer needed because Rolldown provides equivalent features out of the box were removed. diff --git a/.changelog/20260518155957_ci_4316_use_rolldown_for_build_tools.md b/.changelog/20260518155957_ci_4316_use_rolldown_for_build_tools.md new file mode 100644 index 000000000..73f07b822 --- /dev/null +++ b/.changelog/20260518155957_ci_4316_use_rolldown_for_build_tools.md @@ -0,0 +1,9 @@ +--- +type: Major breaking change +scope: + - ckeditor5-dev-build-tools +--- + +Changed `ckeditor5-dev-build-tools` to assume that package sources use TypeScript isolated declarations. + +Declaration files are now generated without invoking TypeScript, so builds no longer perform type checking. diff --git a/.changelog/20260518155959_ci_4316_use_rolldown_for_build_tools.md b/.changelog/20260518155959_ci_4316_use_rolldown_for_build_tools.md new file mode 100644 index 000000000..5fd94263a --- /dev/null +++ b/.changelog/20260518155959_ci_4316_use_rolldown_for_build_tools.md @@ -0,0 +1,7 @@ +--- +type: Feature +scope: + - ckeditor5-dev-build-tools +--- + +Added the `declarationFiles` plugin to `ckeditor5-dev-build-tools` for generating `.d.ts` files from TypeScript sources that use isolated declarations. diff --git a/.changelog/20260518160000_ci_4316_use_rolldown_for_build_tools.md b/.changelog/20260518160000_ci_4316_use_rolldown_for_build_tools.md new file mode 100644 index 000000000..4360b7f89 --- /dev/null +++ b/.changelog/20260518160000_ci_4316_use_rolldown_for_build_tools.md @@ -0,0 +1,9 @@ +--- +type: Major breaking change +scope: + - ckeditor5-dev-build-tools +--- + +Removed the `loadTypeScriptSources`, `replaceImports`, and `emitCss` exports from `ckeditor5-dev-build-tools`. + +The `loadTypeScriptSources` and `replaceImports` behavior is now handled by Rolldown. The `bundleCss` plugin now ensures that a CSS file is always emitted, including when the generated file is empty. diff --git a/.changelog/20260518160002_ci_4316_use_rolldown_for_build_tools.md b/.changelog/20260518160002_ci_4316_use_rolldown_for_build_tools.md new file mode 100644 index 000000000..06d010093 --- /dev/null +++ b/.changelog/20260518160002_ci_4316_use_rolldown_for_build_tools.md @@ -0,0 +1,7 @@ +--- +type: Other +scope: + - ckeditor5-dev-build-tools +--- + +Improved the performance of selected `ckeditor5-dev-build-tools` plugins. From 46eb32b0c8dac702820275cdaa8fc8442e0649bc Mon Sep 17 00:00:00 2001 From: Filip Sobol Date: Tue, 19 May 2026 15:53:52 +0200 Subject: [PATCH 12/12] Address declaration generation review comments. --- .../src/plugins/declarations.ts | 31 +++++++++++----- .../plugins/declarations/declarations.test.ts | 36 ++++++++++++++----- 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/packages/ckeditor5-dev-build-tools/src/plugins/declarations.ts b/packages/ckeditor5-dev-build-tools/src/plugins/declarations.ts index 79f66bdac..309982207 100644 --- a/packages/ckeditor5-dev-build-tools/src/plugins/declarations.ts +++ b/packages/ckeditor5-dev-build-tools/src/plugins/declarations.ts @@ -50,7 +50,7 @@ export function declarationFiles( pluginOptions: RolldownDeclarationOptions ): P async generateBundle() { const sourceFilePaths = getTypeScriptSourceFiles( pluginOptions.sourceDirectory ); - await Promise.all( sourceFilePaths.map( async sourceFilePath => { + const declarationFiles = await Promise.all( sourceFilePaths.map( async sourceFilePath => { const filename = path.relative( pluginOptions.sourceDirectory, sourceFilePath ); const source = readFileSync( sourceFilePath, 'utf8' ); const { errors, code } = await isolatedDeclaration( filename, source, { @@ -58,20 +58,35 @@ export function declarationFiles( pluginOptions: RolldownDeclarationOptions ): P stripInternal: true } ); - if ( errors.length ) { - const errorMessage = errors + return { + fileName: getDeclarationFileName( filename ), + filename, + errors, + source: code + }; + } ) ); + + const errorMessages = declarationFiles + .filter( declarationFile => declarationFile.errors.length ) + .map( declarationFile => { + const errors = declarationFile.errors .map( error => [ error.message, error.codeframe ].filter( Boolean ).join( '\n' ) ) .join( '\n\n' ); - this.error( `Could not generate a declaration file for "${ filename }".\n${ errorMessage }` ); - } + return `Could not generate a declaration file for "${ declarationFile.filename }".\n${ errors }`; + } ); + if ( errorMessages.length ) { + this.error( errorMessages.join( '\n\n' ) ); + } + + for ( const declarationFile of declarationFiles ) { this.emitFile( { type: 'asset', - fileName: getDeclarationFileName( filename ), - source: code + fileName: declarationFile.fileName, + source: declarationFile.source } ); - } ) ); + } } }; } diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/declarations/declarations.test.ts b/packages/ckeditor5-dev-build-tools/tests/plugins/declarations/declarations.test.ts index 3db382df3..d6abf3ad0 100644 --- a/packages/ckeditor5-dev-build-tools/tests/plugins/declarations/declarations.test.ts +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/declarations/declarations.test.ts @@ -63,19 +63,37 @@ test( 'Emits declaration files for TypeScript source files', async () => { } ); test( 'Throws formatted declaration generation errors', async () => { - isolatedDeclarationMock.mockResolvedValue( { - errors: [ { - message: 'Declaration error', - codeframe: 'const value = unknown;' - } ], - code: '' - } ); + isolatedDeclarationMock + .mockResolvedValueOnce( { + errors: [ { + message: 'Declaration error', + codeframe: 'const value = unknown;' + } ], + code: '' + } ) + .mockResolvedValueOnce( { + errors: [ { + message: 'Second declaration error', + codeframe: 'const secondValue = unknown;' + } ], + code: '' + } ) + .mockResolvedValue( { + errors: [], + code: 'export declare const value: string;' + } ); const plugin = declarationFiles( { sourceDirectory: join( import.meta.dirname, './fixtures' ) } ); + const context = createContext(); - await expect( runGenerateBundle( plugin, createContext() ) ).rejects.toThrow( - /Could not generate a declaration file for ".+"\.\nDeclaration error\nconst value = unknown;/ + await expect( runGenerateBundle( plugin, context ) ).rejects.toThrow( + new RegExp( [ + 'Could not generate a declaration file for ".+"\\.\\nDeclaration error\\nconst value = unknown;', + 'Could not generate a declaration file for ".+"\\.\\nSecond declaration error\\nconst secondValue = unknown;' + ].join( '\\n\\n' ) ) ); + + expect( context.emitFile ).not.toHaveBeenCalled(); } );