Files
AFFiNE-Mirror/tools/cli/src/rspack-shared/html-plugin.ts
T
DarkSky aca47445aa feat(client): migration old package to rspack (#15068)
#### PR Dependency Tree


* **PR #15068** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Chores**
* Upgraded Vitest across packages to 4.1.8 and bumped Tailwind PostCSS
to 4.3.0
* CLI/tooling updated to support the media-capture-playground package
and adjust build/dev server behavior

* **Bug Fixes**
  * Improved workspace deletion reliability in the Electron app

* **Refactor**
* Simplified media capture playground build setup (build/config
adjustments)

* **Tests**
* Made tests more robust by preserving/restoring environment state
during runs
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-02 12:00:50 +08:00

298 lines
7.7 KiB
TypeScript

import { execSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { Path, ProjectRoot } from '@affine-tools/utils/path';
import { Repository } from '@napi-rs/simple-git';
import { HtmlRspackPlugin, type HtmlRspackPluginOptions } from '@rspack/core';
import { once } from 'lodash-es';
type HtmlRspackPluginInstance = InstanceType<typeof HtmlRspackPlugin>;
type PluginLike = {
apply: (compiler: CompilerLike) => void;
};
type CompilerLike = {
webpack?: {
sources?: {
RawSource?: new (source: string) => unknown;
};
};
hooks: {
compilation: {
tap: (name: string, callback: (compilation: any) => void) => void;
};
};
};
function createRawSource(compiler: CompilerLike, source: string) {
const RawSource = compiler.webpack?.sources?.RawSource;
if (!RawSource) {
throw new Error(
'compiler.webpack.sources.RawSource is required for html plugin assets emission'
);
}
return new RawSource(source);
}
export const getPublicPath = (BUILD_CONFIG: BUILD_CONFIG_TYPE) => {
const { BUILD_TYPE } = process.env;
if (typeof process.env.PUBLIC_PATH === 'string') {
return process.env.PUBLIC_PATH;
}
if (
BUILD_CONFIG.debug ||
BUILD_CONFIG.distribution === 'desktop' ||
BUILD_CONFIG.distribution === 'ios' ||
BUILD_CONFIG.distribution === 'android'
) {
return '/';
}
switch (BUILD_TYPE) {
case 'stable':
return 'https://prod.affineassets.com/';
case 'beta':
return 'https://beta.affineassets.com/';
default:
return 'https://dev.affineassets.com/';
}
};
const DESCRIPTION = `There can be more than Notion and Miro. AFFiNE is a next-gen knowledge base that brings planning, sorting and creating all together.`;
const gitShortHash = once(() => {
const { GITHUB_SHA } = process.env;
if (GITHUB_SHA) {
return GITHUB_SHA.substring(0, 9);
}
const repo = new Repository(ProjectRoot.value);
const shortSha = repo.head().target()?.substring(0, 9);
if (shortSha) {
return shortSha;
}
const sha = execSync(`git rev-parse --short HEAD`, {
encoding: 'utf-8',
}).trim();
return sha;
});
const currentDir = Path.dir(import.meta.url);
export interface CreateHTMLPluginConfig {
filename?: string;
template?: string;
copySharedPublicAssets?: boolean;
additionalEntryForSelfhost?: boolean;
selfhostPublicPath?: string;
injectGlobalErrorHandler?: boolean;
emitAssetsManifest?: boolean;
}
function getHTMLPluginOptions(BUILD_CONFIG: BUILD_CONFIG_TYPE) {
const publicPath = getPublicPath(BUILD_CONFIG);
const cdnOrigin = publicPath.startsWith('/')
? undefined
: new URL(publicPath).origin;
const templateParams = {
GIT_SHORT_SHA: gitShortHash(),
DESCRIPTION,
PRECONNECT: cdnOrigin
? `<link rel="preconnect" href="${cdnOrigin}" />`
: '',
VIEWPORT_FIT: BUILD_CONFIG.isMobileEdition ? 'cover' : 'auto',
};
return {
template: currentDir.join('template.html').toString(),
inject: 'body',
minify: false,
templateParameters: templateParams,
chunks: ['app'],
scriptLoading: 'blocking',
} satisfies HtmlRspackPluginOptions;
}
const AssetsManifestPlugin = {
apply(compiler: CompilerLike) {
compiler.hooks.compilation.tap('assets-manifest-plugin', compilation => {
HtmlRspackPlugin.getCompilationHooks(
compilation
).beforeAssetTagGeneration.tap('assets-manifest-plugin', arg => {
if (!compilation.getAsset('assets-manifest.json')) {
compilation.emitAsset(
`assets-manifest.json`,
createRawSource(
compiler,
JSON.stringify(
{
...arg.assets,
js: arg.assets.js.map(file =>
file.substring(arg.assets.publicPath.length)
),
css: arg.assets.css.map(file =>
file.substring(arg.assets.publicPath.length)
),
gitHash: gitShortHash(),
description: DESCRIPTION,
},
null,
2
)
),
{
immutable: false,
}
);
}
return arg;
});
});
},
};
const GlobalErrorHandlerPlugin = {
apply(compiler: CompilerLike) {
const globalErrorHandler = [
'js/global-error-handler.js',
readFileSync(currentDir.join('./error-handler.js').toString(), 'utf-8'),
];
compiler.hooks.compilation.tap(
'global-error-handler-plugin',
compilation => {
HtmlRspackPlugin.getCompilationHooks(
compilation
).beforeAssetTagGeneration.tap('global-error-handler-plugin', arg => {
if (!compilation.getAsset(globalErrorHandler[0])) {
compilation.emitAsset(
globalErrorHandler[0],
createRawSource(compiler, globalErrorHandler[1])
);
arg.assets.js.unshift(
arg.assets.publicPath + globalErrorHandler[0]
);
}
return arg;
});
}
);
},
};
const CorsPlugin = {
apply(compiler: CompilerLike) {
compiler.hooks.compilation.tap('html-js-cors-plugin', compilation => {
HtmlRspackPlugin.getCompilationHooks(compilation).alterAssetTags.tap(
'html-js-cors-plugin',
options => {
if (options.publicPath !== '/') {
options.assetTags.scripts.forEach(script => {
script.attributes.crossorigin = true;
});
options.assetTags.styles.forEach(style => {
style.attributes.crossorigin = true;
});
}
return options;
}
);
});
},
};
export function createHTMLPlugins(
BUILD_CONFIG: BUILD_CONFIG_TYPE,
config: CreateHTMLPluginConfig
): (HtmlRspackPluginInstance | PluginLike)[] {
const publicPath = getPublicPath(BUILD_CONFIG);
const htmlPluginOptions = {
...getHTMLPluginOptions(BUILD_CONFIG),
...(config.template ? { template: config.template } : {}),
};
const selfhostPublicPath = config.selfhostPublicPath ?? '/';
const plugins: (HtmlRspackPluginInstance | PluginLike)[] = [];
plugins.push(
new HtmlRspackPlugin({
...htmlPluginOptions,
chunks: ['index'],
filename: config.filename,
publicPath,
meta: {
'env:publicPath': publicPath,
},
})
);
if (BUILD_CONFIG.isElectron) {
plugins.push(
new HtmlRspackPlugin({
...htmlPluginOptions,
chunks: ['shell'],
filename: 'shell.html',
publicPath,
meta: {
'env:publicPath': publicPath,
},
}),
new HtmlRspackPlugin({
...htmlPluginOptions,
filename: 'popup.html',
chunks: ['popup'],
publicPath,
meta: {
'env:publicPath': publicPath,
},
}),
new HtmlRspackPlugin({
...htmlPluginOptions,
filename: 'background-worker.html',
chunks: ['backgroundWorker'],
publicPath,
meta: {
'env:publicPath': publicPath,
},
})
);
}
if (!BUILD_CONFIG.isElectron) {
plugins.push(CorsPlugin);
}
if (config.emitAssetsManifest) {
plugins.push(AssetsManifestPlugin);
}
if (config.injectGlobalErrorHandler) {
plugins.push(GlobalErrorHandlerPlugin);
}
if (config.additionalEntryForSelfhost) {
plugins.push(
new HtmlRspackPlugin({
...htmlPluginOptions,
chunks: ['index'],
publicPath: selfhostPublicPath,
meta: {
'env:isSelfHosted': 'true',
'env:publicPath': selfhostPublicPath,
},
filename: 'selfhost.html',
templateParameters: {
...htmlPluginOptions.templateParameters,
PRECONNECT: '',
},
})
);
}
return plugins;
}