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
+111
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;