Compare commits

...

5 Commits

Author SHA1 Message Date
DarkSky
b438e04b2a fix: ignore list 2026-02-22 06:41:27 +08:00
DarkSky
b0494ec3c1 fix: locale 2026-02-22 03:02:28 +08:00
DarkSky
096bbcf7e6 chore: trim useless files for client 2026-02-22 02:28:02 +08:00
DarkSky
3d01766f55 fix: history may duplicate on concurrency (#14487)
#### PR Dependency Tree


* **PR #14487** 👈

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

* **Bug Fixes**
* Enhanced history record creation to prevent duplicate entries in
concurrent scenarios.

* **Tests**
  * Added validation for idempotent history record creation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-22 02:13:51 +08:00
DarkSky
2414aa5848 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 -->
2026-02-21 23:25:05 +08:00
23 changed files with 608 additions and 87 deletions

View File

@@ -71,7 +71,7 @@
"@opentelemetry/semantic-conventions": "^1.38.0",
"@prisma/client": "^6.6.0",
"@prisma/instrumentation": "^6.7.0",
"@queuedash/api": "^3.14.0",
"@queuedash/api": "^3.16.0",
"@react-email/components": "0.0.38",
"@socket.io/redis-adapter": "^8.3.0",
"ai": "^5.0.118",

View File

@@ -97,7 +97,7 @@ test('should always return static asset files', async t => {
t.is(res.text, "const name = 'affine'");
res = await request(t.context.app.getHttpServer())
.get('/main.b.js')
.get('/admin/main.b.js')
.expect(200);
t.is(res.text, "const name = 'affine-admin'");
@@ -119,7 +119,7 @@ test('should always return static asset files', async t => {
t.is(res.text, "const name = 'affine'");
res = await request(t.context.app.getHttpServer())
.get('/main.b.js')
.get('/admin/main.b.js')
.expect(200);
t.is(res.text, "const name = 'affine-admin'");

View File

@@ -276,22 +276,16 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
return false;
}
try {
await this.models.history.create(
{
spaceId: snapshot.spaceId,
docId: snapshot.docId,
timestamp: snapshot.timestamp,
blob: Buffer.from(snapshot.bin),
editorId: snapshot.editor,
},
historyMaxAge
);
} catch (e) {
// safe to ignore
// only happens when duplicated history record created in multi processes
this.logger.error('Failed to create history record', e);
}
await this.models.history.create(
{
spaceId: snapshot.spaceId,
docId: snapshot.docId,
timestamp: snapshot.timestamp,
blob: Buffer.from(snapshot.bin),
editorId: snapshot.editor,
},
historyMaxAge
);
metrics.doc
.counter('history_created_counter', {

View File

@@ -52,7 +52,7 @@ export class StaticFilesResolver implements OnModuleInit {
// serve all static files
app.use(
basePath,
basePath + '/admin',
serveStatic(join(staticPath, 'admin'), {
redirect: false,
index: false,

View File

@@ -74,6 +74,27 @@ test('should create a history record', async t => {
});
});
test('should not fail on duplicated history record', async t => {
const snapshot = {
spaceId: workspace.id,
docId: randomUUID(),
blob: Uint8Array.from([1, 2, 3]),
timestamp: Date.now(),
editorId: user.id,
};
const created1 = await t.context.history.create(snapshot, 1000);
const created2 = await t.context.history.create(snapshot, 1000);
t.deepEqual(created1.timestamp, snapshot.timestamp);
t.deepEqual(created2.timestamp, snapshot.timestamp);
const histories = await t.context.history.findMany(
snapshot.spaceId,
snapshot.docId
);
t.is(histories.length, 1);
});
test('should return null when history timestamp not match', async t => {
const snapshot = {
spaceId: workspace.id,

View File

@@ -33,22 +33,33 @@ export class HistoryModel extends BaseModel {
* Create a doc history with a max age.
*/
async create(snapshot: Doc, maxAge: number): Promise<DocHistorySimple> {
const row = await this.db.snapshotHistory.create({
select: {
timestamp: true,
createdByUser: { select: publicUserSelect },
const timestamp = new Date(snapshot.timestamp);
const expiredAt = new Date(Date.now() + maxAge);
// This method may be called concurrently by multiple processes for the same
// (workspaceId, docId, timestamp). Using upsert avoids duplicate key errors
// that would otherwise abort the surrounding transaction.
const row = await this.db.snapshotHistory.upsert({
where: {
workspaceId_id_timestamp: {
workspaceId: snapshot.spaceId,
id: snapshot.docId,
timestamp,
},
},
data: {
select: { timestamp: true, createdByUser: { select: publicUserSelect } },
create: {
workspaceId: snapshot.spaceId,
id: snapshot.docId,
timestamp: new Date(snapshot.timestamp),
timestamp,
blob: snapshot.blob,
createdBy: snapshot.editorId,
expiredAt: new Date(Date.now() + maxAge),
expiredAt,
},
update: { expiredAt },
});
this.logger.debug(
`Created history ${row.timestamp} for ${snapshot.docId} in ${snapshot.spaceId}`
`Upserted history ${row.timestamp} for ${snapshot.docId} in ${snapshot.spaceId}`
);
return {
timestamp: row.timestamp.getTime(),

View File

@@ -9,7 +9,7 @@
"@affine/graphql": "workspace:*",
"@affine/routes": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@queuedash/ui": "^3.14.0",
"@queuedash/ui": "^3.16.0",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-alert-dialog": "^1.1.3",
"@radix-ui/react-aspect-ratio": "^1.1.1",
@@ -74,7 +74,7 @@
"scripts": {
"build": "affine bundle",
"dev": "affine bundle --dev",
"update-shadcn": "shadcn-ui add -p src/components/ui"
"update-shadcn": "yarn dlx shadcn@latest add -p src/components/ui"
},
"exports": {
"./*": "./src/*.ts",

View File

@@ -1,5 +1,7 @@
@config '../tailwind.config.js';
@layer properties, theme, base, components, utilities, queuedash;
@import 'tailwindcss';
@import 'tailwindcss/utilities';
@import '@toeverything/theme/style.css';

View File

@@ -1,19 +1,98 @@
import '@queuedash/ui/dist/styles.css';
import './queue.css';
import './queuedash.css';
import { QueueDashApp } from '@queuedash/ui';
import { useEffect } from 'react';
import { Header } from '../header';
const QUEUEDASH_SCOPE_CLASS = 'affine-queuedash';
const PORTAL_CONTENT_SELECTOR =
'.react-aria-ModalOverlay, .react-aria-Menu, [data-rac][data-placement][data-trigger]';
export function QueuePage() {
useEffect(() => {
const marked = new Set<HTMLElement>();
const markScopeRoot = (el: Element) => {
if (!(el instanceof HTMLElement)) {
return;
}
if (el.classList.contains(QUEUEDASH_SCOPE_CLASS)) {
return;
}
el.classList.add(QUEUEDASH_SCOPE_CLASS);
marked.add(el);
};
const isPortalContent = (el: Element) => {
return (
el.matches(PORTAL_CONTENT_SELECTOR) ||
!!el.querySelector(PORTAL_CONTENT_SELECTOR)
);
};
const markIfPortalRoot = (el: Element) => {
if (!isPortalContent(el)) {
return;
}
markScopeRoot(el);
};
const getBodyChildRoot = (el: Element) => {
let current: Element | null = el;
while (
current?.parentElement &&
current.parentElement !== document.body
) {
current = current.parentElement;
}
return current?.parentElement === document.body ? current : null;
};
Array.from(document.body.children).forEach(child => {
if (child.id === 'app') {
return;
}
markIfPortalRoot(child);
});
const observer = new MutationObserver(mutations => {
const appRoot = document.getElementById('app');
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (!(node instanceof Element)) {
continue;
}
const root = getBodyChildRoot(node) ?? node;
if (appRoot && root === appRoot) {
continue;
}
markIfPortalRoot(root);
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
return () => {
observer.disconnect();
marked.forEach(el => el.classList.remove(QUEUEDASH_SCOPE_CLASS));
};
}, []);
return (
<div className="h-dvh flex-1 flex-col flex overflow-hidden">
<Header title="Queue" />
<div className="flex-1 overflow-hidden">
<QueueDashApp
apiUrl={`${environment.subPath}/api/queue/trpc`}
basename="/admin/queue"
/>
<div className={`${QUEUEDASH_SCOPE_CLASS} h-full`}>
<QueueDashApp
apiUrl={`${environment.subPath}/api/queue/trpc`}
basename="/admin/queue"
/>
</div>
</div>
</div>
);

View File

@@ -1,5 +0,0 @@
/* Scoped queuedash modal alignment */
.react-aria-ModalOverlay section[role='dialog'] {
transform: unset;
}

View File

@@ -0,0 +1,14 @@
@import '@queuedash/ui/dist/styles.css' layer(queuedash);
/*
* QueueDash UI is built with Tailwind v3 (translate via `transform`), while AFFiNE Admin
* uses Tailwind v4 (translate via the individual `translate` property). When QueueDash
* overlays are portaled to `document.body`, both utility sets can apply at once and
* result in double transforms (mis-centered dialogs, etc). Reset individual transform
* properties within the queuedash scope so Tailwind v3 styles win.
*/
:where(.affine-queuedash) * {
translate: none;
rotate: none;
scale: none;
}

View File

@@ -1,5 +1,5 @@
import cp from 'node:child_process';
import { rm, symlink } from 'node:fs/promises';
import { readdir, rm, symlink } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -25,6 +25,144 @@ const fromBuildIdentifier = utils.fromBuildIdentifier;
const linuxMimeTypes = [`x-scheme-handler/${productName.toLowerCase()}`];
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const DEFAULT_ELECTRON_LOCALES_KEEP = new Set([
'en',
'en_US',
'en_GB',
'zh_CN',
'zh_TW',
'fr',
'es',
'es_419',
'pl',
'de',
'ru',
'ja',
'it',
'ca',
'da',
'hi',
'sv',
'ur',
'ar',
'uk',
'ko',
'pt_BR',
'fa',
'nb',
]);
const getElectronLocalesKeep = () => {
const raw = process.env.ELECTRON_LOCALES_KEEP?.trim();
if (!raw) return DEFAULT_ELECTRON_LOCALES_KEEP;
const normalized = raw.toLowerCase();
if (normalized === 'all' || normalized === '*') return null;
const keep = new Set(
raw
.split(',')
.map(s => s.trim())
.filter(Boolean)
);
// Always keep English as a safe fallback.
keep.add('en');
keep.add('en_US');
keep.add('en_GB');
return keep;
};
const getElectronPakLocalesKeep = keep => {
const pakKeep = new Set();
for (const locale of keep) {
if (locale === 'en') {
pakKeep.add('en-US');
continue;
}
pakKeep.add(locale.replaceAll('_', '-'));
}
// Always keep English (US) as a safe fallback for Chromium/Electron locales.
pakKeep.add('en');
pakKeep.add('en-US');
pakKeep.add('en-GB');
return pakKeep;
};
const trimElectronFrameworkLocales = async (
resourcesAppDir,
targetPlatform
) => {
if (process.env.TRIM_ELECTRON_LOCALES === '0') return;
if (targetPlatform !== 'darwin' && targetPlatform !== 'mas') return;
const keep = getElectronLocalesKeep();
if (!keep) return;
const contentsDir = path.resolve(resourcesAppDir, '..', '..');
const frameworkResourcesDir = path.join(
contentsDir,
'Frameworks',
'Electron Framework.framework',
'Versions',
'A',
'Resources'
);
let entries;
try {
entries = await readdir(frameworkResourcesDir, { withFileTypes: true });
} catch {
return;
}
const localeDirs = entries
.filter(entry => entry.isDirectory() && entry.name.endsWith('.lproj'))
.map(entry => entry.name);
await Promise.all(
localeDirs.map(async dirName => {
const locale = dirName.slice(0, -'.lproj'.length);
if (keep.has(locale)) return;
await rm(path.join(frameworkResourcesDir, dirName), {
recursive: true,
force: true,
});
})
);
};
const trimElectronPakLocales = async (resourcesAppDir, targetPlatform) => {
if (process.env.TRIM_ELECTRON_LOCALES === '0') return;
if (targetPlatform !== 'win32' && targetPlatform !== 'linux') return;
const keep = getElectronLocalesKeep();
if (!keep) return;
const rootDir = path.resolve(resourcesAppDir, '..', '..');
const localesDir = path.join(rootDir, 'locales');
let entries;
try {
entries = await readdir(localesDir, { withFileTypes: true });
} catch {
return;
}
const pakKeep = getElectronPakLocalesKeep(keep);
await Promise.all(
entries
.filter(entry => entry.isFile() && entry.name.endsWith('.pak'))
.map(async entry => {
const locale = entry.name.slice(0, -'.pak'.length);
if (pakKeep.has(locale)) return;
await rm(path.join(localesDir, entry.name), { force: true });
})
);
};
const makers = [
!process.env.SKIP_BUNDLE &&
platform === 'darwin' && {
@@ -204,7 +342,27 @@ export default {
},
],
executableName: productName,
ignore: [/\.map$/],
ignore: [
/\.map$/,
/\/test($|\/)/,
/\/scripts($|\/)/,
/\/examples($|\/)/,
/\/docs($|\/)/,
/\/README\.md$/,
/\/forge\.config\.mjs$/,
/\/dev-app-update\.yml$/,
/\/resources\/app-update\.yml$/,
],
afterCopy: [
(buildPath, _electronVersion, targetPlatform, _arch, done) => {
Promise.all([
trimElectronFrameworkLocales(buildPath, targetPlatform),
trimElectronPakLocales(buildPath, targetPlatform),
])
.then(() => done())
.catch(done);
},
],
asar: true,
extendInfo: {
NSAudioCaptureUsageDescription:

View File

@@ -25,6 +25,9 @@ function setupRendererConnection(rendererPort: Electron.MessagePortMain) {
return result;
} catch (error) {
logger.error('[async-api]', `${namespace}.${name}`, error);
// Propagate errors to the renderer so callers don't receive `undefined`
// and fail with confusing TypeErrors.
throw error instanceof Error ? error : new Error(String(error));
}
};
return [`${namespace}:${name}`, handlerWithLog];

View File

@@ -40,14 +40,7 @@ interface CodeArtifactToolResult {
toolCallId: string;
toolName: string; // 'code_artifact'
args: { title: string };
result:
| {
title: string;
html: string;
size: number;
}
| ToolError
| null;
result: { title: string; html: string; size: number } | ToolError | null;
}
export class CodeHighlighter extends SignalWatcher(WithDisposable(LitElement)) {

View File

@@ -4,7 +4,7 @@ use affine_schema::get_migrator;
use memory_indexer::InMemoryIndex;
use sqlx::{
Pool, Row,
migrate::MigrateDatabase,
migrate::{MigrateDatabase, Migration, Migrator},
sqlite::{Sqlite, SqliteConnectOptions, SqlitePoolOptions},
};
use tokio::sync::RwLock;
@@ -75,11 +75,74 @@ impl SqliteDocStorage {
async fn migrate(&self) -> Result<()> {
let migrator = get_migrator();
migrator.run(&self.pool).await?;
if let Err(err) = migrator.run(&self.pool).await {
// Compatibility: migration 3 (`add_idx_snapshots`) had a whitespace-only SQL
// change (trailing space) between releases, which causes sqlx to reject
// existing DBs with: `VersionMismatch(3)`. It's safe to fix by updating
// the stored checksum.
if matches!(err, sqlx::migrate::MigrateError::VersionMismatch(3))
&& self.try_repair_migration_3_checksum(&migrator).await?
{
migrator.run(&self.pool).await?;
} else {
return Err(err.into());
}
}
Ok(())
}
async fn try_repair_migration_3_checksum(&self, migrator: &Migrator) -> Result<bool> {
let Some(migration) = migrator.iter().find(|m| m.version == 3) else {
return Ok(false);
};
// We're only prepared to repair the known `add_idx_snapshots` whitespace-only
// mismatch.
if migration.description.as_ref() != "add_idx_snapshots" {
return Ok(false);
}
let row = sqlx::query("SELECT description, checksum FROM _sqlx_migrations WHERE version = 3")
.fetch_optional(&self.pool)
.await?;
let Some(row) = row else {
return Ok(false);
};
let applied_description: String = row.try_get("description")?;
if applied_description != migration.description.as_ref() {
return Ok(false);
}
let applied_checksum: Vec<u8> = row.try_get("checksum")?;
let expected_checksum = migration.checksum.as_ref();
// sqlx computes the checksum as SHA-384 of the raw SQL bytes. The legacy
// variant had an extra trailing space at the end of the SQL string (after
// the final newline).
let legacy_sql = format!("{} ", migration.sql);
let legacy_migration = Migration::new(
migration.version,
migration.description.clone(),
migration.migration_type,
std::borrow::Cow::Owned(legacy_sql),
migration.no_tx,
);
if applied_checksum.as_slice() != legacy_migration.checksum.as_ref() {
return Ok(false);
}
sqlx::query("UPDATE _sqlx_migrations SET checksum = ? WHERE version = 3")
.bind(expected_checksum)
.execute(&self.pool)
.await?;
Ok(true)
}
pub async fn close(&self) {
self.pool.close().await
}
@@ -100,6 +163,11 @@ impl SqliteDocStorage {
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use affine_schema::get_migrator;
use sqlx::migrate::{Migration, Migrator};
use super::*;
async fn get_storage() -> SqliteDocStorage {
@@ -135,4 +203,57 @@ mod tests {
let storage = SqliteDocStorage::new(":memory:".to_string());
assert!(!storage.validate().await.unwrap());
}
#[tokio::test]
async fn connect_repairs_whitespace_only_migration_checksum_mismatch() {
// Simulate a DB migrated with an older `add_idx_snapshots` SQL that had a
// trailing space.
let storage = SqliteDocStorage::new(":memory:".to_string());
let new_migrator = get_migrator();
let mut migrations = new_migrator.migrations.to_vec();
assert!(migrations.len() >= 3);
let mig3 = migrations[2].clone();
assert_eq!(mig3.version, 3);
assert_eq!(mig3.description.as_ref(), "add_idx_snapshots");
let legacy_sql = format!("{} ", mig3.sql);
migrations[2] = Migration::new(
mig3.version,
mig3.description.clone(),
mig3.migration_type,
Cow::Owned(legacy_sql),
mig3.no_tx,
);
// The legacy DB didn't have newer migrations.
migrations.truncate(3);
let legacy_migrator = Migrator {
migrations: Cow::Owned(migrations),
..Migrator::DEFAULT
};
legacy_migrator.run(&storage.pool).await.unwrap();
// Now connecting with the current code should auto-repair the checksum and
// succeed.
storage.connect().await.unwrap();
let expected_checksum = get_migrator()
.iter()
.find(|m| m.version == 3)
.unwrap()
.checksum
.as_ref()
.to_vec();
let row = sqlx::query("SELECT checksum FROM _sqlx_migrations WHERE version = 3")
.fetch_one(&storage.pool)
.await
.unwrap();
let checksum: Vec<u8> = row.get("checksum");
assert_eq!(checksum, expected_checksum);
}
}

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({

View File

@@ -143,6 +143,7 @@ __metadata:
node-loader: "npm:^2.1.0"
postcss: "npm:^8.4.49"
postcss-loader: "npm:^8.1.1"
postcss-selector-parser: "npm:^7.1.0"
prettier: "npm:^3.7.4"
react-refresh: "npm:^0.17.0"
source-map-loader: "npm:^5.0.0"
@@ -185,7 +186,7 @@ __metadata:
"@affine/graphql": "workspace:*"
"@affine/routes": "workspace:*"
"@blocksuite/icons": "npm:^2.2.17"
"@queuedash/ui": "npm:^3.14.0"
"@queuedash/ui": "npm:^3.16.0"
"@radix-ui/react-accordion": "npm:^1.2.2"
"@radix-ui/react-alert-dialog": "npm:^1.1.3"
"@radix-ui/react-aspect-ratio": "npm:^1.1.1"
@@ -1022,7 +1023,7 @@ __metadata:
"@opentelemetry/semantic-conventions": "npm:^1.38.0"
"@prisma/client": "npm:^6.6.0"
"@prisma/instrumentation": "npm:^6.7.0"
"@queuedash/api": "npm:^3.14.0"
"@queuedash/api": "npm:^3.16.0"
"@react-email/components": "npm:0.0.38"
"@socket.io/redis-adapter": "npm:^8.3.0"
"@types/cookie-parser": "npm:^1.4.8"
@@ -11676,11 +11677,11 @@ __metadata:
languageName: node
linkType: hard
"@queuedash/api@npm:3.14.0, @queuedash/api@npm:^3.14.0":
version: 3.14.0
resolution: "@queuedash/api@npm:3.14.0"
"@queuedash/api@npm:3.16.0, @queuedash/api@npm:^3.16.0":
version: 3.16.0
resolution: "@queuedash/api@npm:3.16.0"
dependencies:
"@trpc/server": "npm:^11.6.0"
"@trpc/server": "npm:^11.8.1"
redis: "npm:^4.7.0"
redis-info: "npm:^3.1.0"
zod: "npm:^3.24.2"
@@ -11713,23 +11714,23 @@ __metadata:
optional: true
hono:
optional: true
checksum: 10/d0117ab10c4a59ea0b3d29bc783b273b7b2b08290e122ed1b05bbf185f44ba87b5229ae8e98ce41692de334548c02bf8f7980f7b42a623352d0a048b74966a36
checksum: 10/84c3b552450a72a76245930caab2371aabe85168a2accaa9bc3aefa7e4ea22e99a0e77005f528995e66f39adb63a109721ff89cb556565292d7429c22c5254ec
languageName: node
linkType: hard
"@queuedash/ui@npm:^3.14.0":
version: 3.14.0
resolution: "@queuedash/ui@npm:3.14.0"
"@queuedash/ui@npm:^3.16.0":
version: 3.16.0
resolution: "@queuedash/ui@npm:3.16.0"
dependencies:
"@monaco-editor/react": "npm:^4.7.0"
"@queuedash/api": "npm:3.14.0"
"@queuedash/api": "npm:3.16.0"
"@radix-ui/react-checkbox": "npm:^1.3.3"
"@radix-ui/react-icons": "npm:^1.3.2"
"@tanstack/react-query": "npm:^5.90.5"
"@tanstack/react-table": "npm:^8.21.3"
"@trpc/client": "npm:^11.6.0"
"@trpc/react-query": "npm:^11.6.0"
"@trpc/server": "npm:^11.6.0"
"@trpc/client": "npm:^11.8.1"
"@trpc/react-query": "npm:^11.8.1"
"@trpc/server": "npm:^11.8.1"
clsx: "npm:^2.1.1"
cronstrue: "npm:^2.61.0"
date-fns: "npm:^4.1.0"
@@ -11744,7 +11745,7 @@ __metadata:
peerDependencies:
react: ">=18"
react-dom: ">=18"
checksum: 10/d35dd553bc5a8896d93b1ef6bbd1c7801e5a6ac8e91f392b107d4c4681aa0d46e933ae9c258f2ef0611bd7a1bbc8279506168461ec390afbe3c5a90931c71579
checksum: 10/a3be3e738e281cde533ccadefadaeae1a324fec7658ed38d5e942bd965a4866f5a335f6b4b9a6701386a9cf74a8b931faac45662f8e895df467a2cff75584dd0
languageName: node
linkType: hard
@@ -16711,36 +16712,35 @@ __metadata:
languageName: node
linkType: hard
"@trpc/client@npm:^11.6.0":
version: 11.8.1
resolution: "@trpc/client@npm:11.8.1"
"@trpc/client@npm:^11.8.1":
version: 11.10.0
resolution: "@trpc/client@npm:11.10.0"
peerDependencies:
"@trpc/server": 11.8.1
"@trpc/server": 11.10.0
typescript: ">=5.7.2"
checksum: 10/1411acb66ad8c94cb5cf90386fcca2cf0f67a7d6a7ca90e42b23e737f39d1e8079e8c9088d9b46d3643a57e6eefbf721f83b372624cb0062b371e4c362d2bd98
checksum: 10/91c6c7bf8ac471e4e15042097eace3cfbed10e9af986bef57dfa320c55802aeabe45fbc0bed785bd42b18e8158dd5d8b89709d6bc41c595d4cab70f72f0c3dc9
languageName: node
linkType: hard
"@trpc/react-query@npm:^11.6.0":
version: 11.8.1
resolution: "@trpc/react-query@npm:11.8.1"
"@trpc/react-query@npm:^11.8.1":
version: 11.10.0
resolution: "@trpc/react-query@npm:11.10.0"
peerDependencies:
"@tanstack/react-query": ^5.80.3
"@trpc/client": 11.8.1
"@trpc/server": 11.8.1
"@trpc/client": 11.10.0
"@trpc/server": 11.10.0
react: ">=18.2.0"
react-dom: ">=18.2.0"
typescript: ">=5.7.2"
checksum: 10/6244083404ff0f9e218a6239af43f84941cf20fe30ea6a8b02655649ea627161294360228c54070b0bde6332252c26545b53c7e2148c67afe691863a976422b2
checksum: 10/80b76ad84915a693504545cde5d8d8d9c0f26beefea7809c852d44779e27d01e6af95d0154546887ecdc22f2f747dec7d26a609527a627f7eea198990867f0af
languageName: node
linkType: hard
"@trpc/server@npm:^11.6.0":
version: 11.8.1
resolution: "@trpc/server@npm:11.8.1"
"@trpc/server@npm:^11.8.1":
version: 11.10.0
resolution: "@trpc/server@npm:11.10.0"
peerDependencies:
typescript: ">=5.7.2"
checksum: 10/074b7bd564d0821cbd0711486fb51ee5733d13afe894e5f3e4b0b5ff5c1b71b156d820322e068763b47e076e5cd22a81f8c1df085834cc4ff93c4a1e0005e1de
checksum: 10/e50d2aaefa7b40ef1a270d209ea7b37f37d3990dd15008fc5069c73cf2f108fe3a8777c7c498a1950a984f34f7a26e3922f03a2e87a8ba05d8bc5f2c82f04ff3
languageName: node
linkType: hard