refactor(core): split web entry from core (#6082)

This pr is trying to split `web` and `electron` entries from `core`. It allows more platform-related optimization to be addressed in each entry.
We should remove all browser/electron only codes from `core` eventually, this is the very first step for that.
This commit is contained in:
LongYinan
2024-03-19 07:48:56 +00:00
parent 26925c96e4
commit 332cd3b380
54 changed files with 963 additions and 800 deletions

View File

@@ -1,8 +1,7 @@
import { StorybookConfig } from '@storybook/react-vite';
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
import { fileURLToPath } from 'url';
import { mergeConfig } from 'vite';
import { getRuntimeConfig } from '../../core/.webpack/runtime-config';
import { getRuntimeConfig } from '@affine/cli/src/webpack/runtime-config';
export default {
stories: ['../src/ui/**/*.stories.@(js|jsx|ts|tsx|mdx)'],

View File

@@ -20,6 +20,7 @@
"@blocksuite/store": "*"
},
"dependencies": {
"@affine/cli": "workspace:*",
"@affine/debug": "workspace:*",
"@affine/electron-api": "workspace:*",
"@affine/graphql": "workspace:*",

View File

@@ -1,80 +0,0 @@
function testPackageName(regexp: RegExp): (module: any) => boolean {
return (module: any) =>
module.nameForCondition && regexp.test(module.nameForCondition());
}
// https://hackernoon.com/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758
export const productionCacheGroups = {
asyncVendor: {
test: /[\\/]node_modules[\\/]/,
name(module: any) {
// monorepo linked in node_modules, so it's not a npm package
if (!module.context.includes('node_modules')) {
return `app-async`;
}
const name = module.context.match(
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
)?.[1];
return `npm-async-${name}`;
},
priority: Number.MAX_SAFE_INTEGER,
chunks: 'async' as const,
},
blocksuite: {
name: `npm-blocksuite`,
test: testPackageName(/[\\/]node_modules[\\/](@blocksuite)[\\/]/),
priority: 200,
enforce: true,
},
react: {
name: `npm-react`,
test: testPackageName(
/[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/
),
priority: 200,
enforce: true,
},
jotai: {
name: `npm-jotai`,
test: testPackageName(/[\\/]node_modules[\\/](jotai)[\\/]/),
priority: 200,
enforce: true,
},
rxjs: {
name: `npm-rxjs`,
test: testPackageName(/[\\/]node_modules[\\/]rxjs[\\/]/),
priority: 200,
enforce: true,
},
lodash: {
name: `npm-lodash`,
test: testPackageName(/[\\/]node_modules[\\/]lodash[\\/]/),
priority: 200,
enforce: true,
},
emotion: {
name: `npm-emotion`,
test: testPackageName(/[\\/]node_modules[\\/](@emotion)[\\/]/),
priority: 200,
enforce: true,
},
vendor: {
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
priority: 190,
enforce: true,
},
styles: {
name: 'styles',
test: (module: any) =>
module.nameForCondition &&
/\.css$/.test(module.nameForCondition()) &&
!/^javascript/.test(module.type),
chunks: 'all' as const,
minSize: 1,
minChunks: 1,
reuseExistingChunk: true,
priority: 1000,
enforce: true,
},
};

View File

@@ -1,452 +0,0 @@
import { join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';
import type { Configuration as DevServerConfiguration } from 'webpack-dev-server';
import { PerfseePlugin } from '@perfsee/webpack';
import { sentryWebpackPlugin } from '@sentry/webpack-plugin';
import CopyPlugin from 'copy-webpack-plugin';
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
import TerserPlugin from 'terser-webpack-plugin';
import webpack from 'webpack';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import { compact } from 'lodash-es';
import { productionCacheGroups } from './cache-group.js';
import type { BuildFlags } from '@affine/cli/config';
import { projectRoot } from '@affine/cli/config';
import { VanillaExtractPlugin } from '@vanilla-extract/webpack-plugin';
import type { RuntimeConfig } from '@affine/env/global';
import { WebpackS3Plugin } from './s3-plugin.js';
const IN_CI = !!process.env.CI;
export const rootPath = join(fileURLToPath(import.meta.url), '..', '..');
const workspaceRoot = join(rootPath, '..', '..', '..');
const require = createRequire(rootPath);
const OptimizeOptionOptions: (
buildFlags: BuildFlags
) => webpack.Configuration['optimization'] = buildFlags => ({
minimize: buildFlags.mode === 'production',
minimizer: [
new TerserPlugin({
minify: TerserPlugin.swcMinify,
exclude: [/plugins\/.+\/.+\.js$/, /plugins\/.+\/.+\.mjs$/],
parallel: true,
extractComments: true,
terserOptions: {
ecma: 2020,
compress: {
unused: true,
},
mangle: true,
},
}),
],
removeEmptyChunks: true,
providedExports: true,
usedExports: true,
sideEffects: true,
removeAvailableModules: true,
runtimeChunk: {
name: 'runtime',
},
splitChunks: {
chunks: 'all',
minSize: 1,
minChunks: 1,
maxInitialRequests: Number.MAX_SAFE_INTEGER,
maxAsyncRequests: Number.MAX_SAFE_INTEGER,
cacheGroups:
buildFlags.mode === 'production'
? productionCacheGroups
: {
default: false,
vendors: false,
},
},
});
export const getPublicPath = (buildFlags: BuildFlags) => {
const { BUILD_TYPE } = process.env;
if (typeof process.env.PUBLIC_PATH === 'string') {
return process.env.PUBLIC_PATH;
}
const publicPath = '/';
if (process.env.COVERAGE || buildFlags.distribution === 'desktop') {
return publicPath;
}
if (BUILD_TYPE === 'canary') {
return `https://dev.affineassets.com/`;
} else if (BUILD_TYPE === 'beta' || BUILD_TYPE === 'stable') {
return `https://prod.affineassets.com/`;
}
return publicPath;
};
export const createConfiguration: (
buildFlags: BuildFlags,
runtimeConfig: RuntimeConfig
) => webpack.Configuration = (buildFlags, runtimeConfig) => {
const blocksuiteBaseDir = buildFlags.localBlockSuite;
const config = {
name: 'affine',
// to set a correct base path for the source map
context: projectRoot,
experiments: {
topLevelAwait: true,
outputModule: false,
syncWebAssembly: true,
},
output: {
environment: {
module: true,
dynamicImport: true,
},
filename:
buildFlags.mode === 'production'
? 'js/[name]-[contenthash:8].js'
: 'js/[name].js',
// In some cases webpack will emit files starts with "_" which is reserved in web extension.
chunkFilename:
buildFlags.mode === 'production'
? 'js/chunk.[name]-[contenthash:8].js'
: 'js/chunk.[name].js',
assetModuleFilename:
buildFlags.mode === 'production'
? 'assets/[name]-[contenthash:8][ext][query]'
: '[name][ext]',
devtoolModuleFilenameTemplate: 'webpack://[namespace]/[resource-path]',
hotUpdateChunkFilename: 'hot/[id].[fullhash].js',
hotUpdateMainFilename: 'hot/[runtime].[fullhash].json',
path: join(rootPath, 'dist'),
clean: buildFlags.mode === 'production',
globalObject: 'globalThis',
publicPath: getPublicPath(buildFlags),
},
target: ['web', 'es2022'],
mode: buildFlags.mode,
devtool:
buildFlags.mode === 'production'
? 'source-map'
: 'eval-cheap-module-source-map',
resolve: {
symlinks: true,
extensionAlias: {
'.js': ['.js', '.tsx', '.ts'],
'.mjs': ['.mjs', '.mts'],
},
extensions: ['.js', '.ts', '.tsx'],
fallback:
blocksuiteBaseDir === undefined
? undefined
: {
events: false,
},
alias: {
yjs: require.resolve('yjs'),
'@blocksuite/block-std': blocksuiteBaseDir
? join(blocksuiteBaseDir, 'packages', 'framework', 'block-std', 'src')
: join(
workspaceRoot,
'node_modules',
'@blocksuite',
'block-std',
'dist'
),
'@blocksuite/blocks': blocksuiteBaseDir
? join(blocksuiteBaseDir, 'packages', 'blocks', 'src')
: join(
workspaceRoot,
'node_modules',
'@blocksuite',
'blocks',
'dist'
),
'@blocksuite/presets': blocksuiteBaseDir
? join(blocksuiteBaseDir, 'packages', 'presets', 'src')
: join(
workspaceRoot,
'node_modules',
'@blocksuite',
'presets',
'dist'
),
'@blocksuite/global': blocksuiteBaseDir
? join(blocksuiteBaseDir, 'packages', 'framework', 'global', 'src')
: join(
workspaceRoot,
'node_modules',
'@blocksuite',
'global',
'dist'
),
'@blocksuite/store/providers/broadcast-channel': blocksuiteBaseDir
? join(
blocksuiteBaseDir,
'packages',
'framework',
'store',
'src/providers/broadcast-channel'
)
: join(
workspaceRoot,
'node_modules',
'@blocksuite',
'store',
'dist',
'providers',
'broadcast-channel.js'
),
'@blocksuite/store': blocksuiteBaseDir
? join(blocksuiteBaseDir, 'packages', 'framework', 'store', 'src')
: join(workspaceRoot, 'node_modules', '@blocksuite', 'store', 'dist'),
'@blocksuite/inline': blocksuiteBaseDir
? join(blocksuiteBaseDir, 'packages', 'framework', 'inline', 'src')
: join(
workspaceRoot,
'node_modules',
'@blocksuite',
'inline',
'dist'
),
'@blocksuite/lit': blocksuiteBaseDir
? join(blocksuiteBaseDir, 'packages', 'framework', 'lit', 'src')
: join(workspaceRoot, 'node_modules', '@blocksuite', 'lit', 'dist'),
},
},
module: {
parser: {
javascript: {
// Do not mock Node.js globals
node: false,
requireJs: false,
import: true,
// Treat as missing export as error
strictExportPresence: true,
},
},
rules: [
{
test: /\.m?js?$/,
resolve: {
fullySpecified: false,
},
},
{
test: /\.js$/,
enforce: 'pre',
include: /@blocksuite/,
use: ['source-map-loader'],
},
{
oneOf: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
loader: require.resolve('swc-loader'),
options: {
// https://swc.rs/docs/configuring-swc/
jsc: {
preserveAllComments: true,
parser: {
syntax: 'typescript',
dynamicImport: true,
topLevelAwait: false,
tsx: true,
decorators: true,
},
target: 'es2022',
externalHelpers: false,
transform: {
react: {
runtime: 'automatic',
refresh: buildFlags.mode === 'development' && {
refreshReg: '$RefreshReg$',
refreshSig: '$RefreshSig$',
emitFullSignatures: true,
},
},
useDefineForClassFields: false,
},
},
},
},
{
test: /\.(png|jpg|gif|svg|webp|mp4)$/,
type: 'asset/resource',
},
{
test: /\.(ttf|eot|woff|woff2)$/,
type: 'asset/resource',
},
{
test: /\.txt$/,
loader: 'raw-loader',
},
{
test: /\.css$/,
use: [
buildFlags.mode === 'development'
? 'style-loader'
: MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
url: true,
sourceMap: false,
modules: false,
import: true,
importLoaders: 1,
},
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
config: resolve(
rootPath,
'.webpack',
'postcss.config.cjs'
),
},
},
},
],
},
],
},
],
},
plugins: compact([
IN_CI ? null : new webpack.ProgressPlugin({ percentBy: 'entries' }),
buildFlags.mode === 'development'
? new ReactRefreshWebpackPlugin({ overlay: false, esModule: true })
: new MiniCssExtractPlugin({
filename: `[name].[contenthash:8].css`,
ignoreOrder: true,
}),
new VanillaExtractPlugin(),
new webpack.DefinePlugin({
'process.env': JSON.stringify({}),
'process.env.COVERAGE': JSON.stringify(!!buildFlags.coverage),
'process.env.NODE_ENV': JSON.stringify(buildFlags.mode),
'process.env.SHOULD_REPORT_TRACE': JSON.stringify(
Boolean(process.env.SHOULD_REPORT_TRACE === 'true')
),
'process.env.TRACE_REPORT_ENDPOINT': JSON.stringify(
process.env.TRACE_REPORT_ENDPOINT
),
'process.env.CAPTCHA_SITE_KEY': JSON.stringify(
process.env.CAPTCHA_SITE_KEY
),
'process.env.SENTRY_DSN': JSON.stringify(process.env.SENTRY_DSN),
'process.env.BUILD_TYPE': JSON.stringify(process.env.BUILD_TYPE),
runtimeConfig: JSON.stringify(runtimeConfig),
}),
new CopyPlugin({
patterns: [
{
from: resolve(rootPath, 'public'),
to: resolve(rootPath, 'dist'),
},
],
}),
buildFlags.mode === 'production' && process.env.R2_SECRET_ACCESS_KEY
? new WebpackS3Plugin()
: null,
]),
optimization: OptimizeOptionOptions(buildFlags),
devServer: {
hot: 'only',
liveReload: true,
client: {
overlay: process.env.DISABLE_DEV_OVERLAY === 'true' ? false : undefined,
},
historyApiFallback: true,
static: {
directory: resolve(rootPath, 'public'),
publicPath: '/',
watch: true,
},
proxy: [
{
context: '/api/worker/',
target: 'https://affine.fail',
changeOrigin: true,
secure: false,
},
{ context: '/api', target: 'http://localhost:3010' },
{ context: '/socket.io', target: 'http://localhost:3010', ws: true },
{ context: '/graphql', target: 'http://localhost:3010' },
{ context: '/oauth', target: 'http://localhost:3010' },
],
} as DevServerConfiguration,
} satisfies webpack.Configuration;
if (buildFlags.mode === 'production' && process.env.PERFSEE_TOKEN) {
config.devtool = 'hidden-nosources-source-map';
config.plugins.push(
new PerfseePlugin({
project: 'affine-toeverything',
})
);
}
if (buildFlags.mode === 'development') {
config.optimization = {
...config.optimization,
minimize: false,
runtimeChunk: false,
splitChunks: {
maxInitialRequests: Infinity,
chunks: 'all',
cacheGroups: {
defaultVendors: {
test: `[\\/]node_modules[\\/](?!.*vanilla-extract)`,
priority: -10,
reuseExistingChunk: true,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
styles: {
name: 'styles',
type: 'css/mini-extract',
chunks: 'all',
enforce: true,
},
},
},
};
}
if (
process.env.SENTRY_AUTH_TOKEN &&
process.env.SENTRY_ORG &&
process.env.SENTRY_PROJECT
) {
config.plugins.push(
sentryWebpackPlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
})
);
}
return config;
};

View File

@@ -1,20 +0,0 @@
const cssnano = require('cssnano');
module.exports = function (context) {
const plugins = [
cssnano({
preset: [
'default',
{
convertValues: false,
},
],
}),
];
return {
from: context.from,
plugins,
to: context.to,
};
};

View File

@@ -1,162 +0,0 @@
import type { RuntimeConfig } from '@affine/env/global';
import type { BuildFlags } from '@affine/cli/config';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const packageJson = require('../package.json');
export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
const buildPreset: Record<BuildFlags['channel'], RuntimeConfig> = {
stable: {
enableTestProperties: false,
enableBroadcastChannelProvider: true,
enableDebugPage: true,
githubUrl: 'https://github.com/toeverything/AFFiNE',
changelogUrl: 'https://affine.pro/what-is-new',
downloadUrl: 'https://affine.pro/download',
imageProxyUrl: '/api/worker/image-proxy',
linkPreviewUrl: '/api/worker/link-preview',
enablePreloading: true,
enableNewSettingModal: true,
enableNewSettingUnstableApi: false,
enableSQLiteProvider: true,
enableMoveDatabase: false,
enableNotificationCenter: true,
enableCloud: true,
enableCaptcha: true,
enableEnhanceShareMode: false,
enablePayment: true,
enablePageHistory: true,
allowLocalWorkspace: false,
serverUrlPrefix: 'https://app.affine.pro',
appVersion: packageJson.version,
editorVersion: packageJson.dependencies['@blocksuite/presets'],
appBuildType: 'stable',
},
get beta() {
return {
...this.stable,
enablePageHistory: true,
serverUrlPrefix: 'https://insider.affine.pro',
appBuildType: 'beta' as const,
};
},
get internal() {
return {
...this.stable,
serverUrlPrefix: 'https://insider.affine.pro',
appBuildType: 'internal' as const,
};
},
// canary will be aggressive and enable all features
canary: {
enableTestProperties: true,
enableBroadcastChannelProvider: true,
enableDebugPage: true,
githubUrl: 'https://github.com/toeverything/AFFiNE',
changelogUrl: 'https://github.com/toeverything/AFFiNE/releases',
downloadUrl: 'https://affine.pro/download',
imageProxyUrl: '/api/worker/image-proxy',
linkPreviewUrl: '/api/worker/link-preview',
enablePreloading: true,
enableNewSettingModal: true,
enableNewSettingUnstableApi: false,
enableSQLiteProvider: true,
enableMoveDatabase: false,
enableNotificationCenter: true,
enableCloud: true,
enableCaptcha: true,
enableEnhanceShareMode: false,
enablePayment: true,
enablePageHistory: true,
allowLocalWorkspace: false,
serverUrlPrefix: 'https://affine.fail',
appVersion: packageJson.version,
editorVersion: packageJson.dependencies['@blocksuite/presets'],
appBuildType: 'canary',
},
};
const currentBuild = buildFlags.channel;
if (!(currentBuild in buildPreset)) {
throw new Error(`BUILD_TYPE ${currentBuild} is not supported`);
}
const currentBuildPreset = buildPreset[currentBuild];
const environmentPreset = {
enableTestProperties: process.env.ENABLE_TEST_PROPERTIES
? process.env.ENABLE_TEST_PROPERTIES === 'true'
: currentBuildPreset.enableTestProperties,
enableBroadcastChannelProvider: process.env.ENABLE_BC_PROVIDER
? process.env.ENABLE_BC_PROVIDER !== 'false'
: currentBuildPreset.enableBroadcastChannelProvider,
changelogUrl: process.env.CHANGELOG_URL ?? currentBuildPreset.changelogUrl,
enablePreloading: process.env.ENABLE_PRELOADING
? process.env.ENABLE_PRELOADING === 'true'
: currentBuildPreset.enablePreloading,
enableNewSettingModal: process.env.ENABLE_NEW_SETTING_MODAL
? process.env.ENABLE_NEW_SETTING_MODAL === 'true'
: currentBuildPreset.enableNewSettingModal,
enableSQLiteProvider: process.env.ENABLE_SQLITE_PROVIDER
? process.env.ENABLE_SQLITE_PROVIDER === 'true'
: currentBuildPreset.enableSQLiteProvider,
enableNewSettingUnstableApi: process.env.ENABLE_NEW_SETTING_UNSTABLE_API
? process.env.ENABLE_NEW_SETTING_UNSTABLE_API === 'true'
: currentBuildPreset.enableNewSettingUnstableApi,
enableNotificationCenter: process.env.ENABLE_NOTIFICATION_CENTER
? process.env.ENABLE_NOTIFICATION_CENTER === 'true'
: currentBuildPreset.enableNotificationCenter,
enableCloud: process.env.ENABLE_CLOUD
? process.env.ENABLE_CLOUD === 'true'
: currentBuildPreset.enableCloud,
enableCaptcha: process.env.ENABLE_CAPTCHA
? process.env.ENABLE_CAPTCHA === 'true'
: buildFlags.mode === 'development'
? false
: currentBuildPreset.enableCaptcha,
enableEnhanceShareMode: process.env.ENABLE_ENHANCE_SHARE_MODE
? process.env.ENABLE_ENHANCE_SHARE_MODE === 'true'
: currentBuildPreset.enableEnhanceShareMode,
enableMoveDatabase: process.env.ENABLE_MOVE_DATABASE
? process.env.ENABLE_MOVE_DATABASE === 'true'
: currentBuildPreset.enableMoveDatabase,
enablePayment: process.env.ENABLE_PAYMENT
? process.env.ENABLE_PAYMENT !== 'false'
: buildFlags.mode === 'development'
? true
: currentBuildPreset.enablePayment,
enablePageHistory: process.env.ENABLE_PAGE_HISTORY
? process.env.ENABLE_PAGE_HISTORY === 'true'
: buildFlags.mode === 'development'
? true
: currentBuildPreset.enablePageHistory,
allowLocalWorkspace: process.env.ALLOW_LOCAL_WORKSPACE
? process.env.ALLOW_LOCAL_WORKSPACE === 'true'
: buildFlags.mode === 'development'
? true
: currentBuildPreset.allowLocalWorkspace,
isSelfHosted: process.env.SELF_HOSTED === 'true',
};
const testEnvironmentPreset = {
allowLocalWorkspace: true,
};
if (buildFlags.mode === 'development') {
currentBuildPreset.serverUrlPrefix = 'http://localhost:8080';
}
return {
...currentBuildPreset,
// environment preset will overwrite current build preset
// this environment variable is for debug proposes only
// do not put them into CI
...(process.env.CI ? {} : environmentPreset),
// test environment preset will overwrite current build preset
// this environment variable is for github workflow e2e-test only
...(process.env.IN_CI_TEST ? testEnvironmentPreset : {}),
};
}

View File

@@ -1,45 +0,0 @@
import { join } from 'node:path';
import { readFile } from 'node:fs/promises';
import type { PutObjectCommandInput } from '@aws-sdk/client-s3';
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { lookup } from 'mime-types';
import type { Compiler, WebpackPluginInstance } from 'webpack';
export const R2_BUCKET =
process.env.R2_BUCKET! ??
(process.env.BUILD_TYPE === 'canary' ? 'assets-dev' : 'assets-prod');
export class WebpackS3Plugin implements WebpackPluginInstance {
private readonly s3 = new S3Client({
region: 'auto',
endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
},
});
apply(compiler: Compiler) {
compiler.hooks.assetEmitted.tapPromise(
'WebpackS3Plugin',
async (asset, { outputPath }) => {
if (asset === 'index.html') {
return;
}
const assetPath = join(outputPath, asset);
const assetSource = await readFile(assetPath);
const putObjectCommandOptions: PutObjectCommandInput = {
Body: assetSource,
Bucket: R2_BUCKET,
Key: asset,
};
const contentType = lookup(asset);
if (contentType) {
putObjectCommandOptions.ContentType = contentType;
}
await this.s3.send(new PutObjectCommand(putObjectCommandOptions));
}
);
}
}

View File

@@ -1,43 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1"
/>
<title>AFFiNE</title>
<meta name="theme-color" content="#fafafa" />
<link rel="manifest" href="/manifest.json" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" sizes="192x192" href="/chrome-192x192.png" />
<meta name="emotion-insertion-point" content="" />
<meta property="description" content="<%= DESCRIPTION %>" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content="https://app.affine.pro/" />
<meta
name="twitter:title"
content="AFFiNE: There can be more than Notion and Miro."
/>
<meta name="twitter:description" content="<%= DESCRIPTION %>" />
<meta name="twitter:site" content="@AffineOfficial" />
<meta name="twitter:image" content="https://affine.pro/og.jpeg" />
<meta
property="og:title"
content="AFFiNE: There can be more than Notion and Miro."
/>
<meta property="og:type" content="website" />
<meta property="og:description" content="<%= DESCRIPTION %>" />
<meta property="og:url" content="https://app.affine.pro/" />
<meta property="og:image" content="https://affine.pro/og.jpeg" />
<link
data-react-helmet="true"
rel="shortcut icon"
href="https://affine.pro/favicon.ico"
/>
</head>
<body>
<div id="app" data-version="<%= GIT_SHORT_SHA %>"></div>
</body>
</html>

View File

@@ -1,52 +0,0 @@
import { execSync } from 'node:child_process';
import { join, resolve } from 'node:path';
import { once } from 'lodash-es';
import { merge } from 'webpack-merge';
import type { BuildFlags } from '@affine/cli/config';
import HTMLPlugin from 'html-webpack-plugin';
import { getRuntimeConfig } from './runtime-config.js';
import { createConfiguration, rootPath } from './config.js';
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 sha = execSync(`git rev-parse --short HEAD`, {
encoding: 'utf-8',
}).trim();
return sha;
});
export default async function (cli_env: any, _: any) {
const flags: BuildFlags = JSON.parse(
Buffer.from(cli_env.flags, 'hex').toString('utf-8')
);
console.log('build flags', flags);
const runtimeConfig = getRuntimeConfig(flags);
console.log('runtime config', runtimeConfig);
const config = createConfiguration(flags, runtimeConfig);
return merge(config, {
entry: {
app: resolve(rootPath, 'src/index.tsx'),
},
plugins: [
new HTMLPlugin({
template: join(rootPath, '.webpack', 'template.html'),
inject: 'body',
scriptLoading: 'module',
minify: false,
chunks: ['app'],
filename: 'index.html',
templateParameters: {
GIT_SHORT_SHA: gitShortHash(),
DESCRIPTION,
},
}),
],
});
}

View File

@@ -3,14 +3,9 @@
"type": "module",
"private": true,
"version": "0.14.0",
"scripts": {
"build": "yarn -T run build-core",
"dev": "yarn -T run dev-core",
"static-server": "yarn -T run dev-core --static"
},
"exports": {
"./app": "./src/app.tsx",
"./router": "./src/router.ts",
"./router": "./src/router.tsx",
"./bootstrap/setup": "./src/bootstrap/setup.ts",
"./bootstrap/register-plugins": "./src/bootstrap/register-plugins.ts",
"./*": "./src/*"
@@ -40,7 +35,6 @@
"@emotion/react": "^11.11.3",
"@emotion/server": "^11.11.0",
"@emotion/styled": "^11.11.0",
"@juggle/resize-observer": "^3.4.0",
"@marsidev/react-turnstile": "^0.5.3",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
@@ -59,7 +53,6 @@
"clsx": "^2.1.0",
"cmdk": "patch:cmdk@npm%3A0.2.0#~/.yarn/patches/cmdk-npm-0.2.0-302237a911.patch",
"css-spring": "^4.1.0",
"cssnano": "^6.0.4",
"dayjs": "^1.11.10",
"foxact": "^0.2.31",
"fractional-indexing": "^3.2.0",
@@ -67,7 +60,6 @@
"history": "^5.3.0",
"idb": "^8.0.0",
"image-blob-reduce": "^4.1.0",
"intl-segmenter-polyfill-rs": "^0.1.7",
"jotai": "^2.6.5",
"jotai-devtools": "^0.8.0",
"jotai-effect": "^0.6.0",
@@ -76,10 +68,8 @@
"lodash-es": "^4.17.21",
"lottie-react": "^2.4.0",
"lottie-web": "^5.12.2",
"mini-css-extract-plugin": "^2.8.0",
"nanoid": "^5.0.6",
"next-themes": "^0.3.0",
"postcss-loader": "^8.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-error-boundary": "^4.0.12",
@@ -108,24 +98,11 @@
"@types/image-blob-reduce": "^4.1.4",
"@types/lodash-es": "^4.17.12",
"@types/uuid": "^9.0.8",
"@types/webpack-env": "^1.18.4",
"@vanilla-extract/css": "^1.14.1",
"copy-webpack-plugin": "^12.0.2",
"css-loader": "^6.10.0",
"express": "^4.18.2",
"fake-indexeddb": "^5.0.2",
"html-webpack-plugin": "^5.6.0",
"lodash-es": "^4.17.21",
"mime-types": "^2.1.35",
"raw-loader": "^4.0.2",
"source-map-loader": "^5.0.0",
"style-loader": "^3.3.4",
"swc-loader": "^0.2.6",
"thread-loader": "^4.0.2",
"vitest": "1.4.0",
"webpack": "^5.90.3",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.2",
"webpack-merge": "^5.10.0"
"vitest": "1.4.0"
}
}

View File

@@ -1,4 +1,5 @@
import { apis } from '@affine/electron-api';
import { setupGlobal } from '@affine/env/global';
import { assertExists } from '@blocksuite/global/utils';
import {
type AppConfigSchema,
@@ -35,6 +36,8 @@ class AppConfigProxy {
}
export const appConfigProxy = new AppConfigProxy();
setupGlobal();
const storage = environment.isDesktop
? new AppConfigStorage({
config: defaultAppConfig,

View File

@@ -1,43 +1 @@
import './polyfill/intl-segmenter';
import './polyfill/request-idle-callback';
import './polyfill/resize-observer';
import { assertExists } from '@blocksuite/global/utils';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './app';
import { setup } from './bootstrap/setup';
import { performanceLogger } from './shared';
const performanceMainLogger = performanceLogger.namespace('main');
function main() {
performanceMainLogger.info('start');
// skip bootstrap setup for desktop onboarding
if (window.appInfo?.windowName !== 'onboarding') {
performanceMainLogger.info('setup start');
setup();
performanceMainLogger.info('setup done');
}
mountApp();
}
function mountApp() {
performanceMainLogger.info('import app');
const root = document.getElementById('app');
assertExists(root);
performanceMainLogger.info('render app');
createRoot(root).render(
<StrictMode>
<App />
</StrictMode>
);
}
try {
main();
} catch (err) {
console.error('Failed to bootstrap app', err);
}
export * from './web';

View File

@@ -1,6 +1,8 @@
import { configureWorkspaceImplServices } from '@affine/workspace-impl';
import type { ServiceCollection } from '@toeverything/infra';
import { configureInfraServices } from '@toeverything/infra';
import {
configureInfraServices,
type ServiceCollection,
} from '@toeverything/infra';
import {
configureBusinessServices,

View File

@@ -23,7 +23,10 @@
"main": "./dist/main.js",
"devDependencies": {
"@affine-test/kit": "workspace:*",
"@affine/component": "workspace:*",
"@affine/core": "workspace:*",
"@affine/env": "workspace:*",
"@affine/i18n": "workspace:*",
"@affine/native": "workspace:*",
"@blocksuite/blocks": "0.13.0-canary-202403140735-2367cd5",
"@blocksuite/lit": "0.13.0-canary-202403140735-2367cd5",
@@ -38,6 +41,7 @@
"@electron-forge/maker-zip": "^7.3.0",
"@electron-forge/plugin-auto-unpack-natives": "^7.3.0",
"@electron-forge/shared-types": "^7.3.0",
"@emotion/react": "^11.11.4",
"@pengx17/electron-forge-maker-appimage": "^1.0.2",
"@toeverything/infra": "workspace:*",
"@types/uuid": "^9.0.8",
@@ -51,7 +55,11 @@
"fs-extra": "^11.2.0",
"glob": "^10.3.10",
"jotai": "^2.6.5",
"jotai-devtools": "^0.8.0",
"lodash-es": "^4.17.21",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.3",
"rxjs": "^7.8.1",
"semver": "^7.6.0",
"tinykeys": "patch:tinykeys@npm%3A2.1.0#~/.yarn/patches/tinykeys-npm-2.1.0-819feeaed0.patch",

View File

@@ -4,24 +4,24 @@ import '@affine/component/theme/theme.css';
import { AffineContext } from '@affine/component/context';
import { GlobalLoading } from '@affine/component/global-loading';
import { NotificationCenter } from '@affine/component/notification-center';
import { WorkspaceFallback } from '@affine/core/components/workspace';
import { GlobalScopeProvider } from '@affine/core/modules/infra-web/global-scope';
import { CloudSessionProvider } from '@affine/core/providers/session-provider';
import { router } from '@affine/core/router';
import {
performanceLogger,
performanceRenderLogger,
} from '@affine/core/shared';
import createEmotionCache from '@affine/core/utils/create-emotion-cache';
import { configureWebServices } from '@affine/core/web';
import { createI18n, setUpLanguage } from '@affine/i18n';
import { CacheProvider } from '@emotion/react';
import { getCurrentStore } from '@toeverything/infra/atom';
import {
ServiceCollection,
ServiceProviderContext,
} from '@toeverything/infra/di';
import { ServiceCollection } from '@toeverything/infra/di';
import type { PropsWithChildren, ReactElement } from 'react';
import { lazy, memo, Suspense } from 'react';
import { lazy, Suspense } from 'react';
import { RouterProvider } from 'react-router-dom';
import { WorkspaceFallback } from './components/workspace';
import { CloudSessionProvider } from './providers/session-provider';
import { router } from './router';
import { performanceLogger, performanceRenderLogger } from './shared';
import createEmotionCache from './utils/create-emotion-cache';
import { configureWebServices } from './web';
const performanceI18nLogger = performanceLogger.namespace('i18n');
const cache = createEmotionCache();
@@ -43,16 +43,14 @@ const future = {
} as const;
async function loadLanguage() {
if (environment.isBrowser) {
performanceI18nLogger.info('start');
performanceI18nLogger.info('start');
const i18n = createI18n();
document.documentElement.lang = i18n.language;
const i18n = createI18n();
document.documentElement.lang = i18n.language;
performanceI18nLogger.info('set up');
await setUpLanguage(i18n);
performanceI18nLogger.info('done');
}
performanceI18nLogger.info('set up');
await setUpLanguage(i18n);
performanceI18nLogger.info('done');
}
let languageLoadingPromise: Promise<void> | null = null;
@@ -61,7 +59,7 @@ const services = new ServiceCollection();
configureWebServices(services);
const serviceProvider = services.provider();
export const App = memo(function App() {
export function App() {
performanceRenderLogger.info('App');
if (!languageLoadingPromise) {
@@ -70,15 +68,13 @@ export const App = memo(function App() {
return (
<Suspense>
<ServiceProviderContext.Provider value={serviceProvider}>
<GlobalScopeProvider provider={serviceProvider}>
<CacheProvider value={cache}>
<AffineContext store={getCurrentStore()}>
<CloudSessionProvider>
<DebugProvider>
<GlobalLoading />
{runtimeConfig.enableNotificationCenter && (
<NotificationCenter />
)}
<NotificationCenter />
<RouterProvider
fallbackElement={<WorkspaceFallback key="RouterFallback" />}
router={router}
@@ -88,7 +84,7 @@ export const App = memo(function App() {
</CloudSessionProvider>
</AffineContext>
</CacheProvider>
</ServiceProviderContext.Provider>
</GlobalScopeProvider>
</Suspense>
);
});
}

View File

@@ -0,0 +1,43 @@
// Side effect import, "declare global"
import '@affine/env/constant';
import { setup } from '@affine/core/bootstrap/setup';
import { performanceLogger } from '@affine/core/shared';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './app';
const performanceMainLogger = performanceLogger.namespace('main');
function main() {
performanceMainLogger.info('start');
// skip bootstrap setup for desktop onboarding
if (window.appInfo?.windowName === 'onboarding') {
performanceMainLogger.info('skip setup');
} else {
performanceMainLogger.info('setup start');
setup();
performanceMainLogger.info('setup done');
}
mountApp();
}
function mountApp() {
performanceMainLogger.info('import app');
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const root = document.getElementById('app')!;
performanceMainLogger.info('render app');
createRoot(root).render(
<StrictMode>
<App />
</StrictMode>
);
}
try {
main();
} catch (err) {
console.error('Failed to bootstrap app', err);
}

View File

@@ -0,0 +1,29 @@
{
"extends": "../../../../tsconfig.json",
"compilerOptions": {
"composite": true,
"target": "ESNext",
"module": "ESNext",
"resolveJsonModule": true,
"moduleResolution": "Bundler",
"allowSyntheticDefaultImports": true,
"noEmit": false,
"outDir": "../lib",
"allowJs": true
},
"references": [
{
"path": "../../../common/env"
},
{
"path": "../../core"
},
{
"path": "../../component"
},
{
"path": "../../../common/infra"
}
],
"include": ["."]
}

View File

@@ -12,8 +12,8 @@ const __dirname = fileURLToPath(new URL('.', import.meta.url));
const repoRootDir = path.join(__dirname, '..', '..', '..', '..');
const electronRootDir = path.join(__dirname, '..');
const publicDistDir = path.join(electronRootDir, 'resources');
const affineCoreDir = path.join(repoRootDir, 'packages', 'frontend', 'core');
const affineCoreOutDir = path.join(affineCoreDir, 'dist');
const webDir = path.join(repoRootDir, 'packages', 'frontend', 'electron');
const affineWebOutDir = path.join(webDir, 'dist');
const publicAffineOutDir = path.join(publicDistDir, `web-static`);
const releaseVersionEnv = process.env.RELEASE_VERSION || '';
@@ -21,8 +21,8 @@ console.log('build with following variables', {
repoRootDir,
electronRootDir,
publicDistDir,
affineSrcDir: affineCoreDir,
affineSrcOutDir: affineCoreOutDir,
affineSrcDir: webDir,
affineSrcOutDir: affineWebOutDir,
publicAffineOutDir,
releaseVersionEnv,
});
@@ -45,7 +45,7 @@ const nxFlag = SKIP_NX_CACHE ? '--skip-nx-cache' : '';
// step 1: build web dist
if (!process.env.SKIP_WEB_BUILD) {
spawnSync('yarn', ['nx', 'build', '@affine/core', nxFlag], {
spawnSync('yarn', ['nx', 'build', '@affine/web', nxFlag], {
stdio: 'inherit',
env: process.env,
cwd,
@@ -58,10 +58,10 @@ if (!process.env.SKIP_WEB_BUILD) {
});
// step 1.5: amend sourceMappingURL to allow debugging in devtools
await glob('**/*.{js,css}', { cwd: affineCoreOutDir }).then(files => {
await glob('**/*.{js,css}', { cwd: affineWebOutDir }).then(files => {
return files.map(async file => {
const dir = path.dirname(file);
const fullpath = path.join(affineCoreOutDir, file);
const fullpath = path.join(affineWebOutDir, file);
let content = await fs.readFile(fullpath, 'utf-8');
// replace # sourceMappingURL=76-6370cd185962bc89.js.map
// to # sourceMappingURL=assets://./{dir}/76-6370cd185962bc89.js.map
@@ -78,7 +78,7 @@ if (!process.env.SKIP_WEB_BUILD) {
});
});
await fs.move(affineCoreOutDir, publicAffineOutDir, { overwrite: true });
await fs.move(affineWebOutDir, publicAffineOutDir, { overwrite: true });
}
// step 2: update app-updater.yml content with build type in resources folder

View File

@@ -14,7 +14,7 @@
"noImplicitOverride": true
},
"include": ["./src"],
"exclude": ["node_modules", "lib", "dist", "**/__tests__/**/*"],
"exclude": ["renderer", "node_modules", "lib", "dist", "**/__tests__/**/*"],
"references": [
{
"path": "../../frontend/native"

View File

@@ -306,9 +306,5 @@ export const builtInTemplates = {
});
};
async function main() {
const templatesInGroup = await parseSnapshot();
await buildScript(templatesInGroup);
}
main();
const templatesInGroup = await parseSnapshot();
await buildScript(templatesInGroup);

View File

@@ -9,7 +9,8 @@
"type": "module",
"exports": {
".": "./templates.gen.ts",
"./edgeless": "./edgeless-templates.gen.ts"
"./edgeless": "./edgeless-templates.gen.ts",
"./build-edgeless": "./build-edgeless.mjs"
},
"devDependencies": {
"jszip": "^3.10.1"

View File

@@ -0,0 +1,27 @@
{
"name": "@affine/web",
"version": "0.0.0",
"description": "AFFiNE Desktop Web application",
"private": true,
"browser": "src/index.tsx",
"scripts": {
"build": "yarn workspace @affine/cli build",
"dev": "yarn workspace @affine/cli dev",
"static-server": "yarn workspace @affine/cli dev --static"
},
"dependencies": {
"@affine/component": "workspace:*",
"@affine/core": "workspace:*",
"@affine/env": "workspace:*",
"@juggle/resize-observer": "^3.4.0",
"intl-segmenter-polyfill-rs": "^0.1.7",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@affine/cli": "workspace:*",
"@types/react": "^18.2.60",
"@types/react-dom": "^18.2.19",
"typescript": "^5.3.3"
}
}

View File

@@ -1,5 +1,5 @@
{
"name": "@affine/core",
"name": "@affine/web",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"targets": {
"build": {
@@ -13,9 +13,9 @@
"^build"
],
"inputs": [
"{projectRoot}/.webpack/**/*",
"{projectRoot}/**/*",
"{projectRoot}/public/**/*",
"{workspaceRoot}/tools/**/*",
"{workspaceRoot}/packages/frontend/core/**/*",
"{workspaceRoot}/packages/**/*",
{
"env": "BUILD_TYPE"

View File

@@ -0,0 +1,90 @@
import '@affine/component/theme/global.css';
import '@affine/component/theme/theme.css';
import { AffineContext } from '@affine/component/context';
import { GlobalLoading } from '@affine/component/global-loading';
import { NotificationCenter } from '@affine/component/notification-center';
import { WorkspaceFallback } from '@affine/core/components/workspace';
import { GlobalScopeProvider } from '@affine/core/modules/infra-web/global-scope';
import { CloudSessionProvider } from '@affine/core/providers/session-provider';
import { router } from '@affine/core/router';
import {
performanceLogger,
performanceRenderLogger,
} from '@affine/core/shared';
import createEmotionCache from '@affine/core/utils/create-emotion-cache';
import { configureWebServices } from '@affine/core/web';
import { createI18n, setUpLanguage } from '@affine/i18n';
import { CacheProvider } from '@emotion/react';
import { getCurrentStore } from '@toeverything/infra/atom';
import { ServiceCollection } from '@toeverything/infra/di';
import type { PropsWithChildren, ReactElement } from 'react';
import { lazy, Suspense } from 'react';
import { RouterProvider } from 'react-router-dom';
const performanceI18nLogger = performanceLogger.namespace('i18n');
const cache = createEmotionCache();
const DevTools = lazy(() =>
import('jotai-devtools').then(m => ({ default: m.DevTools }))
);
const DebugProvider = ({ children }: PropsWithChildren): ReactElement => {
return (
<>
<Suspense>{process.env.DEBUG_JOTAI === 'true' && <DevTools />}</Suspense>
{children}
</>
);
};
const future = {
v7_startTransition: true,
} as const;
async function loadLanguage() {
performanceI18nLogger.info('start');
const i18n = createI18n();
document.documentElement.lang = i18n.language;
performanceI18nLogger.info('set up');
await setUpLanguage(i18n);
performanceI18nLogger.info('done');
}
let languageLoadingPromise: Promise<void> | null = null;
const services = new ServiceCollection();
configureWebServices(services);
const serviceProvider = services.provider();
export function App() {
performanceRenderLogger.info('App');
if (!languageLoadingPromise) {
languageLoadingPromise = loadLanguage().catch(console.error);
}
return (
<Suspense>
<GlobalScopeProvider provider={serviceProvider}>
<CacheProvider value={cache}>
<AffineContext store={getCurrentStore()}>
<CloudSessionProvider>
<DebugProvider>
<GlobalLoading />
<NotificationCenter />
<RouterProvider
fallbackElement={<WorkspaceFallback key="RouterFallback" />}
router={router}
future={future}
/>
</DebugProvider>
</CloudSessionProvider>
</AffineContext>
</CacheProvider>
</GlobalScopeProvider>
</Suspense>
);
}

View File

@@ -0,0 +1,45 @@
import './polyfill/intl-segmenter';
import './polyfill/request-idle-callback';
import './polyfill/resize-observer';
import { setup } from '@affine/core/bootstrap/setup';
import { performanceLogger } from '@affine/core/shared';
import { isDesktop } from '@affine/env/constant';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './app';
const performanceMainLogger = performanceLogger.namespace('main');
function main() {
performanceMainLogger.info('start');
// skip bootstrap setup for desktop onboarding
if (isDesktop && window.appInfo?.windowName === 'onboarding') {
performanceMainLogger.info('skip setup');
} else {
performanceMainLogger.info('setup start');
setup();
performanceMainLogger.info('setup done');
}
mountApp();
}
function mountApp() {
performanceMainLogger.info('import app');
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const root = document.getElementById('app')!;
performanceMainLogger.info('render app');
createRoot(root).render(
<StrictMode>
<App />
</StrictMode>
);
}
try {
main();
} catch (err) {
console.error('Failed to bootstrap app', err);
}

View File

@@ -0,0 +1,12 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"composite": true,
"outDir": "lib",
"moduleResolution": "Bundler",
"types": ["affine__env"],
"rootDir": "./src"
},
"include": ["./src"],
"references": [{ "path": "../core" }]
}