feat: improve admin build (#14485)

#### PR Dependency Tree


* **PR #14485** 👈

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**
  * Admin static assets now served under /admin for self-hosted installs
  * CLI is directly executable from the command line
  * Build tooling supports a configurable self-hosted public path
  * Updated admin package script for adding UI components
* Added a PostCSS dependency and plugin to the build toolchain for admin
builds

* **Style**
* Switched queue module to a local queuedash stylesheet, added queuedash
Tailwind layer, and scoped queuedash styles for the admin UI

* **Bug Fixes**
  * Improved error propagation in the Electron renderer
* Migration compatibility to repair a legacy checksum during native
storage upgrades

* **Tests**
  * Added tests covering the migration repair flow
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-02-21 23:25:05 +08:00
committed by GitHub
parent 0de1bd0da8
commit 2414aa5848
18 changed files with 397 additions and 53 deletions

View File

@@ -1,3 +1,4 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
spawnSync('yarn', ['r', 'affine.ts', ...process.argv.slice(2)], {

View File

@@ -39,6 +39,7 @@
"node-loader": "^2.1.0",
"postcss": "^8.4.49",
"postcss-loader": "^8.1.1",
"postcss-selector-parser": "^7.1.0",
"prettier": "^3.7.4",
"react-refresh": "^0.17.0",
"source-map-loader": "^5.0.0",

View File

@@ -88,7 +88,11 @@ function getWebpackBundleConfigs(pkg: Package): webpack.MultiConfiguration {
switch (pkg.name) {
case '@affine/admin': {
return [
createWebpackHTMLTargetConfig(pkg, pkg.srcPath.join('index.tsx').value),
createWebpackHTMLTargetConfig(
pkg,
pkg.srcPath.join('index.tsx').value,
{ selfhostPublicPath: '/admin/' }
),
] as webpack.MultiConfiguration;
}
case '@affine/web':
@@ -158,7 +162,9 @@ function getRspackBundleConfigs(pkg: Package): MultiRspackOptions {
switch (pkg.name) {
case '@affine/admin': {
return [
createRspackHTMLTargetConfig(pkg, pkg.srcPath.join('index.tsx').value),
createRspackHTMLTargetConfig(pkg, pkg.srcPath.join('index.tsx').value, {
selfhostPublicPath: '/admin/',
}),
] as MultiRspackOptions;
}
case '@affine/web':

View File

@@ -0,0 +1,111 @@
import type { AtRule, Container, Node, PluginCreator, Rule } from 'postcss';
import selectorParser from 'postcss-selector-parser';
export interface QueuedashScopeOptions {
scopeClass?: string;
}
function normalizeFilePath(filePath: string) {
return filePath.replaceAll('\\', '/').split('?')[0];
}
function isInKeyframes(rule: Rule) {
let parent: Node | undefined = rule.parent;
while (parent) {
if (parent.type === 'atrule') {
const name = (parent as AtRule).name?.toLowerCase();
if (name && name.endsWith('keyframes')) {
return true;
}
}
parent = parent.parent;
}
return false;
}
const DEFAULT_SCOPE_CLASS = 'affine-queuedash';
export const queuedashScopePostcssPlugin: PluginCreator<
QueuedashScopeOptions
> = (options = {}) => {
const scopeClass = options.scopeClass ?? DEFAULT_SCOPE_CLASS;
const scopeSelector = `:where(.${scopeClass})`;
const scopeAst = selectorParser().astSync(scopeSelector);
const scopeNodes = scopeAst.nodes[0]?.nodes;
if (!scopeNodes) {
throw new Error(
`[queuedashScopePostcssPlugin] Failed to parse scope selector: ${scopeSelector}`
);
}
const scopeProcessor = selectorParser(selectors => {
selectors.each(selector => {
const raw = selector.toString().trim();
if (
raw.startsWith(scopeSelector) ||
raw.startsWith(`.${scopeClass}`) ||
raw.startsWith(`:where(.${scopeClass})`)
) {
return;
}
if (
raw === 'html' ||
raw === 'body' ||
raw === ':host' ||
raw === ':root'
) {
selector.nodes = scopeNodes.map(node => node.clone());
return;
}
const prefixNodes = scopeNodes.map(node => node.clone());
const space = selectorParser.combinator({ value: ' ' });
selector.nodes = [...prefixNodes, space, ...selector.nodes];
});
});
return {
postcssPlugin: 'affine-queuedash-scope',
Once(root, { result }) {
const from =
root.source?.input.file ||
root.source?.input.from ||
result.opts.from ||
'';
const normalized = from ? normalizeFilePath(from) : '';
const isQueuedashVendorCss = normalized.endsWith(
'/@queuedash/ui/dist/styles.css'
);
const queuedashLayers: AtRule[] = [];
root.walkAtRules('layer', atRule => {
if (atRule.params?.trim() === 'queuedash' && atRule.nodes?.length) {
queuedashLayers.push(atRule);
}
});
if (!isQueuedashVendorCss && queuedashLayers.length === 0) {
return;
}
const targets: Container[] =
queuedashLayers.length > 0 ? queuedashLayers : [root];
targets.forEach(container => {
container.walkRules(rule => {
if (!rule.selector || isInKeyframes(rule)) {
return;
}
rule.selector = scopeProcessor.processSync(rule.selector);
});
});
},
};
};
queuedashScopePostcssPlugin.postcss = true;

View File

@@ -12,6 +12,7 @@ import { VanillaExtractPlugin } from '@vanilla-extract/webpack-plugin';
import cssnano from 'cssnano';
import { compact, merge } from 'lodash-es';
import { queuedashScopePostcssPlugin } from '../postcss/queuedash-scope.js';
import { productionCacheGroups } from '../webpack/cache-group.js';
import {
type CreateHTMLPluginConfig,
@@ -228,6 +229,9 @@ export function createHTMLTargetConfig(
require(pkg.join('tailwind.config.js').value),
],
['autoprefixer'],
...(buildConfig.isAdmin
? [queuedashScopePostcssPlugin()]
: []),
]
: [
cssnano({

View File

@@ -79,6 +79,7 @@ const currentDir = Path.dir(import.meta.url);
export interface CreateHTMLPluginConfig {
filename?: string;
additionalEntryForSelfhost?: boolean;
selfhostPublicPath?: string;
injectGlobalErrorHandler?: boolean;
emitAssetsManifest?: boolean;
}
@@ -206,6 +207,7 @@ export function createHTMLPlugins(
): WebpackPluginInstance[] {
const publicPath = getPublicPath(BUILD_CONFIG);
const htmlPluginOptions = getHTMLPluginOptions(BUILD_CONFIG);
const selfhostPublicPath = config.selfhostPublicPath ?? '/';
const plugins: WebpackPluginInstance[] = [];
plugins.push(
@@ -269,9 +271,10 @@ export function createHTMLPlugins(
new HTMLPlugin({
...htmlPluginOptions,
chunks: ['index'],
publicPath: selfhostPublicPath,
meta: {
'env:isSelfHosted': 'true',
'env:publicPath': '/',
'env:publicPath': selfhostPublicPath,
},
filename: 'selfhost.html',
templateParameters: {

View File

@@ -13,6 +13,7 @@ import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import TerserPlugin from 'terser-webpack-plugin';
import webpack from 'webpack';
import { queuedashScopePostcssPlugin } from '../postcss/queuedash-scope.js';
import { productionCacheGroups } from './cache-group.js';
import {
type CreateHTMLPluginConfig,
@@ -230,6 +231,9 @@ export function createHTMLTargetConfig(
require(pkg.join('tailwind.config.js').value),
],
['autoprefixer'],
...(buildConfig.isAdmin
? [queuedashScopePostcssPlugin()]
: []),
]
: [
cssnano({