build: improve webpack config (#3561)

This commit is contained in:
Alex Yang 2023-08-03 16:05:46 -07:00 committed by GitHub
parent f2ac4c7eda
commit 0c16eb1189
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 365 additions and 234 deletions

View File

@ -79,6 +79,10 @@ export const createConfiguration: (
name: 'affine',
// to set a correct base path for the source map
context: projectRoot,
experiments: {
topLevelAwait: true,
outputModule: false,
},
output: {
environment: {
module: true,
@ -130,6 +134,10 @@ export const createConfiguration: (
module: {
parser: {
javascript: {
// Do not mock Node.js globals
node: false,
requireJs: false,
import: true,
// Treat as missing export as error
strictExportPresence: true,
},
@ -137,6 +145,20 @@ export const createConfiguration: (
rules: [
{
test: /\.m?js?$/,
enforce: 'pre',
use: [
{
loader: require.resolve('source-map-loader'),
options: {
filterSourceMappingUrl: (
_url: string,
resourcePath: string
) => {
return resourcePath.includes('@blocksuite');
},
},
},
],
resolve: {
fullySpecified: false,
},

View File

@ -15,23 +15,21 @@ export default async function (cli_env: any, _: any) {
const config = createConfiguration(flags, runtimeConfig);
return merge(config, {
entry: {
'polyfill-ses': {
asyncChunks: false,
'polyfill/ses': {
import: resolve(rootPath, 'src/polyfill/ses.ts'),
},
plugin: {
asyncChunks: true,
dependOn: ['polyfill-ses'],
dependOn: ['polyfill/ses'],
import: resolve(rootPath, 'src/bootstrap/register-plugins.ts'),
},
index: {
asyncChunks: false,
dependOn: ['polyfill-ses', 'plugin'],
app: {
chunkLoading: 'import',
dependOn: ['polyfill/ses', 'plugin'],
import: resolve(rootPath, 'src/index.tsx'),
},
'_plugin/index.test': {
asyncChunks: false,
dependOn: ['polyfill-ses', 'plugin'],
chunkLoading: 'import',
dependOn: ['polyfill/ses', 'plugin'],
import: resolve(rootPath, 'src/_plugin/index.test.tsx'),
},
},
@ -41,7 +39,7 @@ export default async function (cli_env: any, _: any) {
inject: 'body',
scriptLoading: 'module',
minify: false,
chunks: ['index', 'plugin', 'polyfill-ses'],
chunks: ['app', 'plugin', 'polyfill/ses'],
filename: 'index.html',
}),
new HTMLPlugin({
@ -49,7 +47,7 @@ export default async function (cli_env: any, _: any) {
inject: 'body',
scriptLoading: 'module',
minify: false,
chunks: ['_plugin/index.test', 'plugin', 'polyfill-ses'],
chunks: ['_plugin/index.test', 'plugin', 'polyfill/ses'],
filename: '_plugin/index.html',
}),
],

View File

@ -69,6 +69,7 @@
"express": "^4.18.2",
"html-webpack-plugin": "^5.5.3",
"raw-loader": "^4.0.2",
"source-map-loader": "^4.0.1",
"style-loader": "^3.3.3",
"swc-loader": "^0.2.3",
"swc-plugin-coverage-instrument": "^0.0.19",

View File

@ -1,16 +1,21 @@
{
"name": "@affine/core",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"root": "apps/core",
"sourceRoot": "apps/core/src",
"targets": {
"build": {
"executor": "nx:run-script",
"dependsOn": ["^build"],
"dependsOn": [
{
"projects": ["tag:plugin"],
"target": "build",
"params": "ignore"
},
"^build"
],
"inputs": [
"{projectRoot}/.webpack/**/*",
"{projectRoot}/**/*",
"{projectRoot}/public/plugins/**/*",
"{projectRoot}/public/**/*",
"{workspaceRoot}/packages/component/src/**/*",
"{workspaceRoot}/packages/debug/src/**/*",
"{workspaceRoot}/packages/graphql/src/**/*",
@ -46,7 +51,7 @@
"options": {
"script": "build"
},
"outputs": ["{projectRoot}/dist", "{projectRoot}/public/plugins"]
"outputs": ["{projectRoot}/dist"]
}
}
}

View File

@ -6,7 +6,10 @@ const PORT = process.env.PORT || 8080;
app.use('/', express.static('dist'));
app.get('/*', (_, res) => {
app.get('/*', (req, res) => {
if (req.url.startsWith('/plugins')) {
res.sendFile(req.url, { root: 'dist' });
}
res.sendFile('index.html', { root: 'dist' });
});

View File

@ -9,31 +9,39 @@ import { createRoot } from 'react-dom/client';
import { _pluginNestedImportsMap } from '../bootstrap/plugins/setup';
import { pluginRegisterPromise } from '../bootstrap/register-plugins';
const root = document.getElementById('app');
assertExists(root);
async function main() {
const { setup } = await import('../bootstrap/setup');
await setup();
const root = document.getElementById('app');
assertExists(root);
const App = () => {
use(pluginRegisterPromise);
const plugins = useAtomValue(registeredPluginAtom);
_pluginNestedImportsMap.forEach(value => {
const exports = value.get('index.js');
assertExists(exports);
assertExists(exports?.get('entry'));
});
return (
<div>
<div data-plugins-load-status="success">Successfully loaded plugins:</div>
{plugins.map(plugin => {
return <div key={plugin}>{plugin}</div>;
})}
</div>
const App = () => {
use(pluginRegisterPromise);
const plugins = useAtomValue(registeredPluginAtom);
_pluginNestedImportsMap.forEach(value => {
const exports = value.get('index.js');
assertExists(exports);
assertExists(exports?.get('entry'));
});
return (
<div>
<div data-plugins-load-status="success">
Successfully loaded plugins:
</div>
{plugins.map(plugin => {
return <div key={plugin}>{plugin}</div>;
})}
</div>
);
};
createRoot(root).render(
<StrictMode>
<Provider store={rootStore}>
<App />
</Provider>
</StrictMode>
);
};
}
createRoot(root).render(
<StrictMode>
<Provider store={rootStore}>
<App />
</Provider>
</StrictMode>
);
await main();

View File

@ -3,7 +3,6 @@ import '@affine/component/theme/theme.css';
import { AffineContext } from '@affine/component/context';
import { WorkspaceFallback } from '@affine/component/workspace';
import { createI18n, setUpLanguage } from '@affine/i18n';
import { CacheProvider } from '@emotion/react';
import { use } from 'foxact/use';
import type { PropsWithChildren, ReactElement } from 'react';
@ -13,7 +12,6 @@ import { RouterProvider } from 'react-router-dom';
import { router } from './router';
import createEmotionCache from './utils/create-emotion-cache';
const i18n = createI18n();
const cache = createEmotionCache();
const DevTools = lazy(() =>
@ -33,18 +31,16 @@ const future = {
v7_startTransition: true,
} as const;
const languageLoadingPromise = new Promise<void>(resolve => {
async function loadLanguage() {
if (environment.isBrowser) {
const { createI18n, setUpLanguage } = await import('@affine/i18n');
const i18n = createI18n();
document.documentElement.lang = i18n.language;
setUpLanguage(i18n)
.then(() => resolve())
.catch(error => {
console.error(error);
});
} else {
resolve();
await setUpLanguage(i18n);
}
});
}
const languageLoadingPromise = loadLanguage().catch(console.error);
export const App = memo(function App() {
use(languageLoadingPromise);

View File

@ -1,164 +0,0 @@
import {
migrateDatabaseBlockTo3,
migrateToSubdoc,
} from '@affine/env/blocksuite';
import { setupGlobal } from '@affine/env/global';
import type {
LocalIndexedDBDownloadProvider,
WorkspaceAdapter,
} from '@affine/env/workspace';
import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import {
type RootWorkspaceMetadataV2,
rootWorkspacesMetadataAtom,
workspaceAdaptersAtom,
} from '@affine/workspace/atom';
import {
migrateLocalBlobStorage,
upgradeV1ToV2,
} from '@affine/workspace/migration';
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
import { assertExists } from '@blocksuite/global/utils';
import { rootStore } from '@toeverything/infra/atom';
import { WorkspaceAdapters } from '../adapters/workspace';
console.log('setup global');
setupGlobal();
rootStore.set(
workspaceAdaptersAtom,
WorkspaceAdapters as Record<
WorkspaceFlavour,
WorkspaceAdapter<WorkspaceFlavour>
>
);
const value = localStorage.getItem('jotai-workspaces');
if (value) {
try {
const metadata = JSON.parse(value) as RootWorkspaceMetadata[];
const promises: Promise<void>[] = [];
const newMetadata = [...metadata];
metadata.forEach(oldMeta => {
if (!('version' in oldMeta)) {
const adapter = WorkspaceAdapters[oldMeta.flavour];
assertExists(adapter);
const upgrade = async () => {
const workspace = await adapter.CRUD.get(oldMeta.id);
if (!workspace) {
console.warn('cannot find workspace', oldMeta.id);
return;
}
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
console.warn('not supported');
return;
}
const doc = workspace.blockSuiteWorkspace.doc;
const provider = createIndexedDBDownloadProvider(workspace.id, doc, {
awareness: workspace.blockSuiteWorkspace.awarenessStore.awareness,
}) as LocalIndexedDBDownloadProvider;
provider.sync();
await provider.whenReady;
const newDoc = migrateToSubdoc(doc);
if (doc === newDoc) {
console.log('doc not changed');
return;
}
const newWorkspace = upgradeV1ToV2(workspace);
await migrateDatabaseBlockTo3(newWorkspace.blockSuiteWorkspace.doc);
const newId = await adapter.CRUD.create(
newWorkspace.blockSuiteWorkspace
);
await adapter.CRUD.delete(workspace as any);
console.log('migrated', oldMeta.id, newId);
const index = newMetadata.findIndex(meta => meta.id === oldMeta.id);
newMetadata[index] = {
...oldMeta,
id: newId,
version: WorkspaceVersion.DatabaseV3,
};
await migrateLocalBlobStorage(workspace.id, newId);
console.log('migrate to v2');
};
// create a new workspace and push it to metadata
promises.push(upgrade());
} else if (oldMeta.version < WorkspaceVersion.DatabaseV3) {
const adapter = WorkspaceAdapters[oldMeta.flavour];
assertExists(adapter);
promises.push(
(async () => {
const workspace = await adapter.CRUD.get(oldMeta.id);
if (workspace) {
const provider = createIndexedDBDownloadProvider(
workspace.id,
workspace.blockSuiteWorkspace.doc,
{
awareness:
workspace.blockSuiteWorkspace.awarenessStore.awareness,
}
) as LocalIndexedDBDownloadProvider;
provider.sync();
await provider.whenReady;
await migrateDatabaseBlockTo3(workspace.blockSuiteWorkspace.doc);
}
const index = newMetadata.findIndex(meta => meta.id === oldMeta.id);
console.log('migrate to v3');
newMetadata[index] = {
...oldMeta,
version: WorkspaceVersion.DatabaseV3,
};
})()
);
}
});
await Promise.all(promises)
.then(() => {
console.log('migration done');
})
.catch(() => {
console.error('migration failed');
})
.finally(() => {
localStorage.setItem('jotai-workspaces', JSON.stringify(newMetadata));
window.dispatchEvent(new CustomEvent('migration-done'));
window.$migrationDone = true;
});
} catch (e) {
console.error('error when migrating data', e);
}
}
const createFirst = (): RootWorkspaceMetadataV2[] => {
const Plugins = Object.values(WorkspaceAdapters).sort(
(a, b) => a.loadPriority - b.loadPriority
);
return Plugins.flatMap(Plugin => {
return Plugin.Events['app:init']?.().map(
id =>
<RootWorkspaceMetadataV2>{
id,
flavour: Plugin.flavour,
version: WorkspaceVersion.DatabaseV3,
}
);
}).filter((ids): ids is RootWorkspaceMetadataV2 => !!ids);
};
await rootStore
.get(rootWorkspacesMetadataAtom)
.then(meta => {
if (meta.length === 0 && localStorage.getItem('is-first-open') === null) {
const result = createFirst();
console.info('create first workspace', result);
localStorage.setItem('is-first-open', 'false');
rootStore.set(rootWorkspacesMetadataAtom, result);
}
})
.catch(console.error);

View File

@ -0,0 +1,180 @@
import {
migrateDatabaseBlockTo3,
migrateToSubdoc,
} from '@affine/env/blocksuite';
import { setupGlobal } from '@affine/env/global';
import type {
LocalIndexedDBDownloadProvider,
WorkspaceAdapter,
} from '@affine/env/workspace';
import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import {
type RootWorkspaceMetadataV2,
rootWorkspacesMetadataAtom,
workspaceAdaptersAtom,
} from '@affine/workspace/atom';
import {
migrateLocalBlobStorage,
upgradeV1ToV2,
} from '@affine/workspace/migration';
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
import { assertExists } from '@blocksuite/global/utils';
import { rootStore } from '@toeverything/infra/atom';
import { WorkspaceAdapters } from '../adapters/workspace';
async function tryMigration() {
const value = localStorage.getItem('jotai-workspaces');
if (value) {
try {
const metadata = JSON.parse(value) as RootWorkspaceMetadata[];
const promises: Promise<void>[] = [];
const newMetadata = [...metadata];
metadata.forEach(oldMeta => {
if (!('version' in oldMeta)) {
const adapter = WorkspaceAdapters[oldMeta.flavour];
assertExists(adapter);
const upgrade = async () => {
const workspace = await adapter.CRUD.get(oldMeta.id);
if (!workspace) {
console.warn('cannot find workspace', oldMeta.id);
return;
}
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
console.warn('not supported');
return;
}
const doc = workspace.blockSuiteWorkspace.doc;
const provider = createIndexedDBDownloadProvider(
workspace.id,
doc,
{
awareness:
workspace.blockSuiteWorkspace.awarenessStore.awareness,
}
) as LocalIndexedDBDownloadProvider;
provider.sync();
await provider.whenReady;
const newDoc = migrateToSubdoc(doc);
if (doc === newDoc) {
console.log('doc not changed');
return;
}
const newWorkspace = upgradeV1ToV2(workspace);
await migrateDatabaseBlockTo3(newWorkspace.blockSuiteWorkspace.doc);
const newId = await adapter.CRUD.create(
newWorkspace.blockSuiteWorkspace
);
await adapter.CRUD.delete(workspace as any);
console.log('migrated', oldMeta.id, newId);
const index = newMetadata.findIndex(meta => meta.id === oldMeta.id);
newMetadata[index] = {
...oldMeta,
id: newId,
version: WorkspaceVersion.DatabaseV3,
};
await migrateLocalBlobStorage(workspace.id, newId);
console.log('migrate to v2');
};
// create a new workspace and push it to metadata
promises.push(upgrade());
} else if (oldMeta.version < WorkspaceVersion.DatabaseV3) {
console.log('migrate to v3');
const adapter = WorkspaceAdapters[oldMeta.flavour];
assertExists(adapter);
promises.push(
(async () => {
const workspace = await adapter.CRUD.get(oldMeta.id);
if (workspace) {
const provider = createIndexedDBDownloadProvider(
workspace.id,
workspace.blockSuiteWorkspace.doc,
{
awareness:
workspace.blockSuiteWorkspace.awarenessStore.awareness,
}
) as LocalIndexedDBDownloadProvider;
provider.sync();
await provider.whenReady;
await migrateDatabaseBlockTo3(
workspace.blockSuiteWorkspace.doc
);
}
const index = newMetadata.findIndex(
meta => meta.id === oldMeta.id
);
console.log('migrate to v3');
newMetadata[index] = {
...oldMeta,
version: WorkspaceVersion.DatabaseV3,
};
})()
);
}
});
await Promise.all(promises)
.then(() => {
console.log('migration done');
})
.catch(() => {
console.error('migration failed');
})
.finally(() => {
localStorage.setItem('jotai-workspaces', JSON.stringify(newMetadata));
window.dispatchEvent(new CustomEvent('migration-done'));
window.$migrationDone = true;
});
} catch (e) {
console.error('error when migrating data', e);
}
}
}
function createFirstAppData() {
const createFirst = (): RootWorkspaceMetadataV2[] => {
const Plugins = Object.values(WorkspaceAdapters).sort(
(a, b) => a.loadPriority - b.loadPriority
);
return Plugins.flatMap(Plugin => {
return Plugin.Events['app:init']?.().map(
id =>
<RootWorkspaceMetadataV2>{
id,
flavour: Plugin.flavour,
version: WorkspaceVersion.DatabaseV3,
}
);
}).filter((ids): ids is RootWorkspaceMetadataV2 => !!ids);
};
if (localStorage.getItem('is-first-open') !== null) {
return;
}
const result = createFirst();
console.info('create first workspace', result);
localStorage.setItem('is-first-open', 'false');
rootStore.set(rootWorkspacesMetadataAtom, result);
}
export async function setup() {
rootStore.set(
workspaceAdaptersAtom,
WorkspaceAdapters as Record<
WorkspaceFlavour,
WorkspaceAdapter<WorkspaceFlavour>
>
);
console.log('setup global');
setupGlobal();
createFirstAppData();
await tryMigration();
await rootStore.get(rootWorkspacesMetadataAtom);
console.log('setup done');
}

View File

@ -1,16 +1,20 @@
import { WorkspaceFallback } from '@affine/component/workspace';
import { assertExists } from '@blocksuite/global/utils';
import { StrictMode } from 'react';
import { StrictMode, Suspense } from 'react';
import { createRoot } from 'react-dom/client';
async function main() {
await import('./bootstrap/before-app');
const { setup } = await import('./bootstrap/setup');
await setup();
const { App } = await import('./app');
const root = document.getElementById('app');
assertExists(root);
createRoot(root).render(
<StrictMode>
<App />
<Suspense fallback={<WorkspaceFallback key="AppLoading" />}>
<App />
</Suspense>
</StrictMode>
);
}

View File

@ -10,6 +10,7 @@
"description": "AFFiNE App",
"homepage": "https://github.com/toeverything/AFFiNE",
"scripts": {
"start": "electron .",
"dev": "yarn cross-env DEV_SERVER_URL=http://localhost:8080 node scripts/dev.mjs",
"dev:prod": "yarn node scripts/dev.mjs",
"build": "NODE_ENV=production zx scripts/build-layers.mjs",

View File

@ -7,7 +7,14 @@
"targets": {
"build": {
"executor": "nx:run-script",
"dependsOn": ["^build"],
"dependsOn": [
{
"projects": ["tag:plugin"],
"target": "build",
"params": "ignore"
},
"^build"
],
"options": {
"script": "build"
},

View File

@ -33,9 +33,7 @@
"{projectRoot}/build",
"{projectRoot}/out",
"{projectRoot}/storybook-static",
"{workspaceRoot}/packages/i18n/src/i18n-generated.ts",
"{workspaceRoot}/apps/electron/dist/plugins",
"{workspaceRoot}/apps/core/public/plugins"
"{workspaceRoot}/packages/i18n/src/i18n-generated.ts"
],
"inputs": [
"{workspaceRoot}/infra/**/*",

View File

@ -206,10 +206,6 @@ export function setupGlobal() {
}
globalThis.environment = environment;
if (environment.isBrowser) {
globalThis.editorVersion = global.editorVersion;
}
let prefixUrl: string;
if (!isBrowser || isDesktop) {
// SSR or Desktop

View File

@ -1,5 +1,19 @@
{
"name": "@affine/bookmark-plugin",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"targets": {
"build": {
"executor": "nx:run-script",
"options": {
"script": "build"
},
"dependsOn": ["^build"],
"inputs": ["{projectRoot}/**/*"],
"outputs": [
"{workspaceRoot}/apps/core/public/plugins/bookmark",
"{workspaceRoot}/apps/electron/dist/plugins/bookmark"
]
}
},
"tags": ["plugin"]
}

View File

@ -1,5 +1,19 @@
{
"name": "@affine/copilot-plugin",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"tags": ["plugin"]
"tags": ["plugin"],
"targets": {
"build": {
"executor": "nx:run-script",
"options": {
"script": "build"
},
"dependsOn": ["^build"],
"inputs": ["{projectRoot}/**/*"],
"outputs": [
"{workspaceRoot}/apps/core/public/plugins/copilot",
"{workspaceRoot}/apps/electron/dist/plugins/copilot"
]
}
}
}

View File

@ -1,5 +1,19 @@
{
"name": "@affine/hello-world-plugin",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"tags": ["plugin"]
"tags": ["plugin"],
"targets": {
"build": {
"executor": "nx:run-script",
"options": {
"script": "build"
},
"dependsOn": ["^build"],
"inputs": ["{projectRoot}/**/*"],
"outputs": [
"{workspaceRoot}/apps/core/public/plugins/hello-world",
"{workspaceRoot}/apps/electron/dist/plugins/hello-world"
]
}
}
}

View File

@ -1,5 +1,4 @@
import type { PluginContext } from '@affine/sdk/entry';
import { currentWorkspaceAtom, rootStore } from '@affine/sdk/entry';
import { createElement } from 'react';
import { lazy } from 'react';
import { createRoot } from 'react-dom/client';
@ -11,7 +10,6 @@ const HeaderItem = lazy(() =>
export const entry = (context: PluginContext) => {
console.log('register');
console.log('hello, world!');
console.log(rootStore.get(currentWorkspaceAtom));
context.register('headerItem', div => {
const root = createRoot(div);
root.render(createElement(HeaderItem));

View File

@ -1,5 +1,19 @@
{
"name": "@affine/image-preview-plugin",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"tags": ["plugin"]
"tags": ["plugin"],
"targets": {
"build": {
"executor": "nx:run-script",
"options": {
"script": "build"
},
"dependsOn": ["^build"],
"inputs": ["{projectRoot}/**/*"],
"outputs": [
"{workspaceRoot}/apps/core/public/plugins/image-preview",
"{workspaceRoot}/apps/electron/dist/plugins/image-preview"
]
}
}
}

View File

@ -75,6 +75,7 @@ test('database migration', async ({ page, context }) => {
await switchToNext();
await page.waitForTimeout(1000);
await page.goto('http://localhost:8081/');
await page.click('text=hello');
await page.waitForSelector('v-line', {
timeout: 10000,
});

View File

@ -260,6 +260,7 @@ __metadata:
react-router-dom: ^6.14.2
rxjs: ^7.8.1
ses: ^0.18.5
source-map-loader: ^4.0.1
style-loader: ^3.3.3
swc-loader: ^0.2.3
swc-plugin-coverage-instrument: ^0.0.19
@ -13254,6 +13255,13 @@ __metadata:
languageName: node
linkType: hard
"abab@npm:^2.0.6":
version: 2.0.6
resolution: "abab@npm:2.0.6"
checksum: 6ffc1af4ff315066c62600123990d87551ceb0aafa01e6539da77b0f5987ac7019466780bf480f1787576d4385e3690c81ccc37cfda12819bf510b8ab47e5a3e
languageName: node
linkType: hard
"abbrev@npm:1, abbrev@npm:^1.0.0":
version: 1.1.1
resolution: "abbrev@npm:1.1.1"
@ -29183,6 +29191,19 @@ __metadata:
languageName: node
linkType: hard
"source-map-loader@npm:^4.0.1":
version: 4.0.1
resolution: "source-map-loader@npm:4.0.1"
dependencies:
abab: ^2.0.6
iconv-lite: ^0.6.3
source-map-js: ^1.0.2
peerDependencies:
webpack: ^5.72.1
checksum: 4ddca8b03dc61f406effd4bffe70de4b87fef48bae6f737017b2dabcbc7d609133325be1e73838e9265331de28039111d729fcbb8bce88a6018a816bef510eb1
languageName: node
linkType: hard
"source-map-support@npm:0.5.13":
version: 0.5.13
resolution: "source-map-support@npm:0.5.13"