diff --git a/.env.template b/.env.template
deleted file mode 100644
index 4c8ede67baf33..0000000000000
--- a/.env.template
+++ /dev/null
@@ -1,7 +0,0 @@
-CHANGELOG_URL=
-ENABLE_NEW_SETTING_UNSTABLE_API=
-ENABLE_CAPTCHA=
-CAPTCHA_SITE_KEY=
-ENABLE_ENHANCE_SHARE_MODE=
-ALLOW_LOCAL_WORKSPACE=
-DEBUG_JOTAI=
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index a2a3f8492a044..cf88160ead31d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -81,3 +81,9 @@ apps/web/next-routes.conf
packages/frontend/templates/edgeless
packages/frontend/core/public/static/templates
+
+# new dist folder
+tsbuild/
+# script
+af
+affine
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 04b636602900c..17f5e06a1a2a1 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -216,7 +216,7 @@ export default tseslint.config(
},
},
{
- files: ['packages/**/*.{ts,tsx}', 'tools/cli/**/*.{ts,tsx}'],
+ files: ['packages/**/*.{ts,tsx}', 'tools/**/*.ts'],
rules: {
'@typescript-eslint/no-floating-promises': [
'error',
diff --git a/oxlint.json b/oxlint.json
index 45aa4926f8627..12cf04135cf14 100644
--- a/oxlint.json
+++ b/oxlint.json
@@ -5,7 +5,7 @@
"correctness": "error",
"perf": "error"
},
- "ignorePatterns": ["tools/cli/src/webpack/error-handler.js"],
+ "ignorePatterns": ["tools/scripts/src/webpack/error-handler.js"],
"rules": {
"import/named": "allow",
"no-await-in-loop": "allow",
@@ -167,7 +167,8 @@
"files": [
"*.{spec,test,e2e,stories}.{ts,tsx}",
"tests/**/*.ts",
- "packages/backend/server/tests/**/*.ts"
+ "packages/backend/server/tests/**/*.ts",
+ "tools/**.*"
],
"rules": {
"typescript/no-non-null-assertion": "off",
diff --git a/package.json b/package.json
index ba95f87de66c0..18fbb496fb8d6 100644
--- a/package.json
+++ b/package.json
@@ -17,12 +17,10 @@
"node": "<21.0.0"
},
"scripts": {
- "dev": "yarn workspace @affine/cli dev",
- "build": "yarn workspace @affine/cli bundle",
- "dev:electron": "yarn workspace @affine/electron dev",
- "build:electron": "yarn nx build @affine/electron",
- "build:server-native": "yarn nx run-many -t build -p @affine/server-native",
- "start:web-static": "yarn workspace @affine/web static-server",
+ "affine": "yarn workspace @affine-tools/scripts affine",
+ "af": "yarn workspace @affine-tools/scripts af",
+ "dev": "yarn affine dev",
+ "build": "yarn affine build",
"lint:eslint": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" eslint --report-unused-disable-directives-severity=off . --cache",
"lint:eslint:fix": "yarn lint:eslint --fix",
"lint:prettier": "prettier --ignore-unknown --cache --check .",
@@ -33,9 +31,8 @@
"test": "vitest --run",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
- "typecheck": "tsc -b tsconfig.json",
- "postinstall": "node ./scripts/check-version.mjs && yarn workspace @affine/i18n i18n-codegen gen && yarn husky install",
- "prepare": "husky"
+ "typecheck": "tsc -b tsconfig.project.json --verbose",
+ "postinstall": "yarn affine codegen && yarn husky"
},
"lint-staged": {
"*": "prettier --write --ignore-unknown --cache",
@@ -51,7 +48,7 @@
]
},
"devDependencies": {
- "@affine/cli": "workspace:*",
+ "@affine-tools/scripts": "workspace:*",
"@capacitor/cli": "^6.2.0",
"@eslint/js": "^9.16.0",
"@faker-js/faker": "^9.3.0",
diff --git a/packages/frontend/admin/package.json b/packages/frontend/admin/package.json
index 9f969a735ef71..fa8bd3a5b810a 100644
--- a/packages/frontend/admin/package.json
+++ b/packages/frontend/admin/package.json
@@ -62,7 +62,8 @@
"tailwindcss-animate": "^1.0.7"
},
"scripts": {
- "build": "cross-env DISTRIBUTION=admin yarn workspace @affine/cli bundle",
+ "build": "affine bundle",
+ "dev": "affine bundle --dev",
"update-shadcn": "shadcn-ui add -p src/components/ui"
},
"exports": {
diff --git a/packages/frontend/apps/android/package.json b/packages/frontend/apps/android/package.json
index 26c4e8bf98a24..55a04de002b04 100644
--- a/packages/frontend/apps/android/package.json
+++ b/packages/frontend/apps/android/package.json
@@ -5,9 +5,8 @@
"private": true,
"browser": "src/index.tsx",
"scripts": {
- "build": "cross-env DISTRIBUTION=android yarn workspace @affine/cli bundle",
- "dev": "yarn workspace @affine/cli dev",
- "static-server": "cross-env DISTRIBUTION=android yarn workspace @affine/cli dev --static"
+ "build": "affine bundle",
+ "dev": "affine bundle --dev"
},
"dependencies": {
"@affine/component": "workspace:*",
diff --git a/packages/frontend/apps/electron/package.json b/packages/frontend/apps/electron/package.json
index 3d691804524fb..a131468dd8f80 100644
--- a/packages/frontend/apps/electron/package.json
+++ b/packages/frontend/apps/electron/package.json
@@ -11,19 +11,20 @@
"homepage": "https://github.com/toeverything/AFFiNE",
"scripts": {
"start": "electron .",
- "dev": "DEV_SERVER_URL=http://localhost:8080 node --loader ts-node/esm/transpile-only ./scripts/dev.ts",
- "dev:prod": "yarn node --loader ts-node/esm/transpile-only scripts/dev.ts",
- "build": "NODE_ENV=production node --loader ts-node/esm/transpile-only scripts/build-layers.ts",
- "build:dev": "NODE_ENV=development node --loader ts-node/esm/transpile-only scripts/build-layers.ts",
- "generate-assets": "node --loader ts-node/esm/transpile-only scripts/generate-assets.ts",
- "package": "cross-env NODE_OPTIONS=\"--loader ts-node/esm/transpile-only\" electron-forge package",
- "make": "cross-env NODE_OPTIONS=\"--loader ts-node/esm/transpile-only\" electron-forge make",
- "make-squirrel": "node --loader ts-node/esm/transpile-only scripts/make-squirrel.ts",
- "make-nsis": "node --loader ts-node/esm/transpile-only scripts/make-nsis.ts"
+ "dev": "cross-env DEV_SERVER_URL=http://localhost:8080 node ./scripts/dev.ts",
+ "dev:prod": "node ./scripts/dev.ts",
+ "build": "cross-env NODE_ENV=production node ./scripts/build-layers.ts",
+ "build:dev": "node ./scripts/build-layers.ts",
+ "generate-assets": "node ./scripts/generate-assets.ts",
+ "package": "electron-forge package",
+ "make": "electron-forge make",
+ "make-squirrel": "node ./scripts/make-squirrel.ts",
+ "make-nsis": "node ./scripts/make-nsis.ts"
},
"main": "./dist/main.js",
"devDependencies": {
"@affine-test/kit": "workspace:*",
+ "@affine-tools/utils": "workspace:*",
"@affine/component": "workspace:*",
"@affine/core": "workspace:*",
"@affine/electron-api": "workspace:*",
diff --git a/packages/frontend/apps/electron/scripts/common.ts b/packages/frontend/apps/electron/scripts/common.ts
index ddc10995088f0..5775953a16c13 100644
--- a/packages/frontend/apps/electron/scripts/common.ts
+++ b/packages/frontend/apps/electron/scripts/common.ts
@@ -1,8 +1,8 @@
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
-// eslint-disable-next-line @typescript-eslint/no-restricted-imports
-import { getBuildConfig } from '@affine/cli/src/webpack/runtime-config';
+import { getBuildConfig } from '@affine-tools/utils/build-config';
+import { Workspace } from '@affine-tools/utils/workspace';
import { sentryEsbuildPlugin } from '@sentry/esbuild-plugin';
import type { BuildOptions, Plugin } from 'esbuild';
@@ -24,12 +24,10 @@ export const config = (): BuildOptions => {
'process.env.NODE_ENV': process.env.NODE_ENV,
REPLACE_ME_BUILD_ENV: process.env.BUILD_TYPE ?? 'stable',
...Object.entries(
- getBuildConfig({
- channel: (process.env.BUILD_TYPE as any) ?? 'canary',
- distribution: 'desktop',
+ getBuildConfig(new Workspace().getPackage('@affine/electron'), {
mode:
process.env.NODE_ENV === 'production' ? 'production' : 'development',
- static: false,
+ channel: (process.env.BUILD_TYPE as any) ?? 'canary',
})
).reduce(
(def, [key, val]) => {
diff --git a/packages/frontend/apps/electron/tsconfig.json b/packages/frontend/apps/electron/tsconfig.json
index df1043bd00f99..2b455fb9eaf05 100644
--- a/packages/frontend/apps/electron/tsconfig.json
+++ b/packages/frontend/apps/electron/tsconfig.json
@@ -33,6 +33,9 @@
},
{
"path": "../../../../tests/kit"
+ },
+ {
+ "path": "../../../../tools/utils"
}
],
"ts-node": {
diff --git a/packages/frontend/apps/electron/tsconfig.node.json b/packages/frontend/apps/electron/tsconfig.node.json
index 2beac5166efc1..2fcb612fd0800 100644
--- a/packages/frontend/apps/electron/tsconfig.node.json
+++ b/packages/frontend/apps/electron/tsconfig.node.json
@@ -5,7 +5,7 @@
"target": "ESNext",
"module": "ESNext",
"resolveJsonModule": true,
- "moduleResolution": "Node",
+ "moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"noEmit": false,
"outDir": "./lib/scripts",
@@ -17,5 +17,10 @@
"ts-node": {
"esm": true,
"experimentalSpecifierResolution": "node"
- }
+ },
+ "references": [
+ {
+ "path": "../../../../tools/utils"
+ }
+ ]
}
diff --git a/packages/frontend/apps/ios/package.json b/packages/frontend/apps/ios/package.json
index 65e1ed3b42ab3..afc1b9f2645af 100644
--- a/packages/frontend/apps/ios/package.json
+++ b/packages/frontend/apps/ios/package.json
@@ -5,11 +5,10 @@
"private": true,
"browser": "src/index.tsx",
"scripts": {
- "build": "cross-env DISTRIBUTION=ios yarn workspace @affine/cli bundle",
- "dev": "yarn workspace @affine/cli dev",
+ "build": "affine bundle",
+ "dev": "affine bundle --dev",
"sync": "yarn cap sync",
- "sync:dev": "CAP_SERVER_URL=http://localhost:8080 yarn cap sync",
- "static-server": "cross-env DISTRIBUTION=ios yarn workspace @affine/cli dev --static"
+ "sync:dev": "CAP_SERVER_URL=http://localhost:8080 yarn cap sync"
},
"dependencies": {
"@affine/component": "workspace:*",
diff --git a/packages/frontend/apps/mobile/package.json b/packages/frontend/apps/mobile/package.json
index bc8cce6e0727b..41120ce081770 100644
--- a/packages/frontend/apps/mobile/package.json
+++ b/packages/frontend/apps/mobile/package.json
@@ -5,9 +5,8 @@
"private": true,
"browser": "src/index.tsx",
"scripts": {
- "build": "cross-env DISTRIBUTION=mobile yarn workspace @affine/cli bundle",
- "dev": "yarn workspace @affine/cli dev",
- "static-server": "cross-env DISTRIBUTION=mobile yarn workspace @affine/cli dev --static"
+ "build": "affine bundle",
+ "dev": "affine bundle --dev"
},
"dependencies": {
"@affine/component": "workspace:*",
diff --git a/packages/frontend/apps/web/package.json b/packages/frontend/apps/web/package.json
index b3a995920f089..d60092b257f6e 100644
--- a/packages/frontend/apps/web/package.json
+++ b/packages/frontend/apps/web/package.json
@@ -5,9 +5,8 @@
"private": true,
"browser": "src/index.tsx",
"scripts": {
- "build": "cross-env DISTRIBUTION=web yarn workspace @affine/cli bundle",
- "dev": "yarn workspace @affine/cli dev",
- "static-server": "yarn workspace @affine/cli dev --static"
+ "build": "affine bundle",
+ "dev": "affine bundle --dev"
},
"dependencies": {
"@affine/component": "workspace:*",
diff --git a/packages/frontend/component/.storybook/main.ts b/packages/frontend/component/.storybook/main.ts
index ac46638d1d16a..9da8b74fc958e 100644
--- a/packages/frontend/component/.storybook/main.ts
+++ b/packages/frontend/component/.storybook/main.ts
@@ -3,7 +3,7 @@ import { StorybookConfig } from '@storybook/react-vite';
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
import swc from 'unplugin-swc';
import { mergeConfig } from 'vite';
-import { getBuildConfig } from '@affine/cli/src/webpack/runtime-config';
+import { getBuildConfig } from '@affine-tools/scripts/src/webpack/runtime-config';
export default {
stories: ['../src/ui/**/*.@(mdx|stories.@(js|jsx|ts|tsx))'],
diff --git a/packages/frontend/component/package.json b/packages/frontend/component/package.json
index b35b9a93fef42..93cdf0e94ac39 100644
--- a/packages/frontend/component/package.json
+++ b/packages/frontend/component/package.json
@@ -20,7 +20,7 @@
"react-dom": "^19.0.0"
},
"dependencies": {
- "@affine/cli": "workspace:*",
+ "@affine-tools/scripts": "workspace:*",
"@affine/debug": "workspace:*",
"@affine/electron-api": "workspace:*",
"@affine/graphql": "workspace:*",
diff --git a/packages/frontend/core/tsconfig.node.json b/packages/frontend/core/tsconfig.node.json
index acbcfbefb94c4..22e466d88025e 100644
--- a/packages/frontend/core/tsconfig.node.json
+++ b/packages/frontend/core/tsconfig.node.json
@@ -13,9 +13,6 @@
},
"include": [".webpack/*.ts"],
"references": [
- {
- "path": "../../../tools/cli"
- },
{
"path": "../../common/env"
}
diff --git a/packages/frontend/graphql/package.json b/packages/frontend/graphql/package.json
index 1aad8f3c8f8ee..706ceb2418375 100644
--- a/packages/frontend/graphql/package.json
+++ b/packages/frontend/graphql/package.json
@@ -21,7 +21,7 @@
"vitest": "2.1.8"
},
"scripts": {
- "postinstall": "gql-gen --errors-only"
+ "build": "gql-gen --errors-only"
},
"dependencies": {
"@affine/env": "workspace:*",
diff --git a/packages/frontend/templates/package.json b/packages/frontend/templates/package.json
index 6646cb336f90a..698643e1259e2 100644
--- a/packages/frontend/templates/package.json
+++ b/packages/frontend/templates/package.json
@@ -4,7 +4,8 @@
"sideEffect": false,
"version": "0.18.0",
"scripts": {
- "postinstall": "node ./build-edgeless.mjs && node ./build-stickers.mjs"
+ "build": "node ./build-edgeless.mjs && node ./build-stickers.mjs",
+ "postinstall": "yarn build"
},
"type": "module",
"exports": {
diff --git a/scripts/setup/global.ts b/scripts/setup/global.ts
index cedc216dd425e..d674303711394 100644
--- a/scripts/setup/global.ts
+++ b/scripts/setup/global.ts
@@ -1,5 +1,5 @@
-import { getBuildConfig } from '@affine/cli/src/webpack/runtime-config';
import { setupGlobal } from '@affine/env/global';
+import { getBuildConfig } from '@affine-tools/scripts/src/webpack/runtime-config';
globalThis.BUILD_CONFIG = getBuildConfig({
distribution: 'web',
diff --git a/tools/cli/src/bin/build.ts b/tools/cli/src/bin/build.ts
deleted file mode 100644
index 64113974a61dd..0000000000000
--- a/tools/cli/src/bin/build.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import { spawn } from 'node:child_process';
-
-import webpack from 'webpack';
-
-import { getCwdFromDistribution } from '../config/cwd.cjs';
-import type { BuildFlags } from '../config/index.js';
-import { createWebpackConfig } from '../webpack/webpack.config.js';
-
-// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-const buildType = process.env.BUILD_TYPE_OVERRIDE || process.env.BUILD_TYPE;
-
-if (process.env.BUILD_TYPE_OVERRIDE) {
- process.env.BUILD_TYPE = process.env.BUILD_TYPE_OVERRIDE;
-}
-
-const getChannel = () => {
- switch (buildType) {
- case 'canary':
- case 'beta':
- case 'stable':
- case 'internal':
- return buildType;
- case '':
- throw new Error('BUILD_TYPE is not set');
- default: {
- throw new Error(
- `BUILD_TYPE must be one of canary, beta, stable, internal, received [${buildType}]`
- );
- }
- }
-};
-
-let entry: BuildFlags['entry'];
-
-const { DISTRIBUTION = 'web' } = process.env;
-
-const cwd = getCwdFromDistribution(DISTRIBUTION);
-
-if (DISTRIBUTION === 'desktop') {
- entry = { app: './index.tsx', shell: './shell/index.tsx' };
-}
-
-const flags = {
- distribution: DISTRIBUTION as BuildFlags['distribution'],
- mode: 'production',
- channel: getChannel(),
- coverage: process.env.COVERAGE === 'true',
- entry,
- static: false,
-} satisfies BuildFlags;
-
-spawn('yarn', ['workspace', '@affine/i18n', 'build'], {
- stdio: 'inherit',
-});
-
-// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-webpack(createWebpackConfig(cwd!, flags), (err, stats) => {
- if (err) {
- console.error(err);
- process.exit(1);
- }
- if (stats?.hasErrors()) {
- console.error(stats.toString('errors-only'));
- process.exit(1);
- }
-});
diff --git a/tools/cli/src/bin/dev.ts b/tools/cli/src/bin/dev.ts
deleted file mode 100644
index 8962d797e263b..0000000000000
--- a/tools/cli/src/bin/dev.ts
+++ /dev/null
@@ -1,146 +0,0 @@
-import { spawn } from 'node:child_process';
-import { existsSync } from 'node:fs';
-import { join } from 'node:path';
-
-import * as p from '@clack/prompts';
-import { config } from 'dotenv';
-import webpack from 'webpack';
-import WebpackDevServer from 'webpack-dev-server';
-
-import { getCwdFromDistribution, projectRoot } from '../config/cwd.cjs';
-import type { BuildFlags } from '../config/index.js';
-import { createWebpackConfig } from '../webpack/webpack.config.js';
-
-const flags: BuildFlags = {
- distribution:
- (process.env.DISTRIBUTION as BuildFlags['distribution']) ?? 'web',
- mode: 'development',
- static: false,
- channel: 'canary',
- coverage: process.env.COVERAGE === 'true',
-};
-
-const files = ['.env', '.env.local'];
-
-for (const file of files) {
- if (existsSync(join(projectRoot, file))) {
- config({
- path: join(projectRoot, file),
- });
- console.log(`${file} loaded`);
- break;
- }
-}
-
-const buildFlags = process.argv.includes('--static')
- ? { ...flags, static: true }
- : ((await p.group(
- {
- distribution: () =>
- p.select({
- message: 'Distribution',
- options: [
- {
- value: 'web',
- },
- {
- value: 'desktop',
- },
- {
- value: 'admin',
- },
- {
- value: 'mobile',
- },
- {
- value: 'ios',
- },
- ],
- initialValue: 'web',
- }),
- mode: () =>
- p.select({
- message: 'Mode',
- options: [
- {
- value: 'development',
- },
- {
- value: 'production',
- },
- ],
- initialValue: 'development',
- }),
- channel: () =>
- p.select({
- message: 'Channel',
- options: [
- {
- value: 'canary',
- },
- {
- value: 'beta',
- },
- {
- value: 'stable',
- },
- ],
- initialValue: 'canary',
- }),
- coverage: () =>
- p.confirm({
- message: 'Enable coverage',
- initialValue: process.env.COVERAGE === 'true',
- }),
- },
- {
- onCancel: () => {
- p.cancel('Operation cancelled.');
- process.exit(0);
- },
- }
- )) as BuildFlags);
-
-flags.distribution = buildFlags.distribution;
-flags.mode = buildFlags.mode;
-flags.channel = buildFlags.channel;
-flags.coverage = buildFlags.coverage;
-flags.static = buildFlags.static;
-flags.entry = undefined;
-
-const cwd = getCwdFromDistribution(flags.distribution);
-
-process.env.DISTRIBUTION = flags.distribution;
-
-if (flags.distribution === 'desktop') {
- flags.entry = {
- app: join(cwd, 'index.tsx'),
- shell: join(cwd, 'shell/index.tsx'),
- };
-}
-
-console.info(flags);
-
-if (!flags.static) {
- spawn('yarn', ['workspace', '@affine/i18n', 'dev'], {
- stdio: 'inherit',
- shell: true,
- });
-}
-
-try {
- // @ts-expect-error no types
- await import('@affine/templates/build-edgeless');
- const config = createWebpackConfig(cwd, flags);
- if (flags.static) {
- config.watch = false;
- }
- const compiler = webpack(config);
- // Start webpack
- const devServer = new WebpackDevServer(config.devServer, compiler);
-
- await devServer.start();
-} catch (error) {
- console.error('Error during build:', error);
- process.exit(1);
-}
diff --git a/tools/cli/src/config/cwd.cjs b/tools/cli/src/config/cwd.cjs
deleted file mode 100644
index b01b774455bcb..0000000000000
--- a/tools/cli/src/config/cwd.cjs
+++ /dev/null
@@ -1,38 +0,0 @@
-// @ts-check
-
-const { join } = require('node:path');
-
-const projectRoot = join(__dirname, '../../../..');
-
-module.exports.projectRoot = projectRoot;
-
-/**
- *
- * @param {string | undefined} distribution
- * @returns string
- */
-module.exports.getCwdFromDistribution = function getCwdFromDistribution(
- distribution
-) {
- switch (distribution) {
- case 'web':
- case undefined:
- case null:
- return join(projectRoot, 'packages/frontend/apps/web');
- case 'desktop':
- return join(projectRoot, 'packages/frontend/apps/electron/renderer');
- case 'admin':
- return join(projectRoot, 'packages/frontend/admin');
- case 'mobile':
- return join(projectRoot, 'packages/frontend/apps/mobile');
- case 'ios':
- return join(projectRoot, 'packages/frontend/apps/ios');
- case 'android':
- return join(projectRoot, 'packages/frontend/apps/android');
- default: {
- throw new Error(
- 'DISTRIBUTION must be one of web, desktop, admin, mobile'
- );
- }
- }
-};
diff --git a/tools/cli/src/config/index.ts b/tools/cli/src/config/index.ts
deleted file mode 100644
index f4452c5da070a..0000000000000
--- a/tools/cli/src/config/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export type BuildFlags = {
- distribution: 'web' | 'desktop' | 'admin' | 'mobile' | 'ios' | 'android';
- mode: 'development' | 'production';
- channel: 'stable' | 'beta' | 'canary' | 'internal';
- static: boolean;
- coverage?: boolean;
- localBlockSuite?: string;
- entry?: string | { [key: string]: string };
-};
diff --git a/tools/cli/src/util/infra.ts b/tools/cli/src/util/infra.ts
deleted file mode 100644
index 4e88c9ae8d816..0000000000000
--- a/tools/cli/src/util/infra.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { spawn } from 'node:child_process';
-import { resolve } from 'node:path';
-
-import { build } from 'vite';
-
-import { projectRoot } from '../config/cwd.cjs';
-
-const infraFilePath = resolve(
- projectRoot,
- 'packages',
- 'infra',
- 'vite.config.ts'
-);
-export const buildInfra = async () => {
- await build({
- configFile: infraFilePath,
- });
-};
-
-export const watchInfra = async () => {
- spawn('vite', ['build', '--watch'], {
- cwd: resolve(projectRoot, 'packages/common/infra'),
- shell: true,
- stdio: 'inherit',
- });
-};
diff --git a/tools/cli/src/webpack/postcss.config.cjs b/tools/cli/src/webpack/postcss.config.cjs
deleted file mode 100644
index 9bc817f28a73f..0000000000000
--- a/tools/cli/src/webpack/postcss.config.cjs
+++ /dev/null
@@ -1,47 +0,0 @@
-const { join } = require('node:path');
-
-const cssnano = require('cssnano');
-const tailwindcss = require('tailwindcss');
-const autoprefixer = require('autoprefixer');
-
-const { getCwdFromDistribution } = require('../config/cwd.cjs');
-
-const projectCwd = getCwdFromDistribution(process.env.DISTRIBUTION);
-
-const twConfig = (function () {
- try {
- const config = require(`${projectCwd}/tailwind.config.js`);
- const { content } = config;
- if (Array.isArray(content)) {
- config.content = content.map(c =>
- c.startsWith(projectCwd) ? c : join(projectCwd, c)
- );
- }
- return config;
- } catch {
- return null;
- }
-})();
-
-module.exports = function (context) {
- const plugins = [
- cssnano({
- preset: [
- 'default',
- {
- convertValues: false,
- },
- ],
- }),
- ];
-
- if (twConfig) {
- plugins.push(tailwindcss(twConfig), autoprefixer());
- }
-
- return {
- from: context.from,
- plugins,
- to: context.to,
- };
-};
diff --git a/tools/cli/src/webpack/webpack.config.ts b/tools/cli/src/webpack/webpack.config.ts
deleted file mode 100644
index 290ce1fc386cc..0000000000000
--- a/tools/cli/src/webpack/webpack.config.ts
+++ /dev/null
@@ -1,183 +0,0 @@
-import { execSync } from 'node:child_process';
-import { readFileSync } from 'node:fs';
-import { join, resolve } from 'node:path';
-
-import type { BuildFlags } from '@affine/cli/config';
-import { Repository } from '@napi-rs/simple-git';
-import HTMLPlugin from 'html-webpack-plugin';
-import { once } from 'lodash-es';
-import type { Compiler } from 'webpack';
-import webpack from 'webpack';
-import { merge } from 'webpack-merge';
-
-import {
- createConfiguration,
- getPublicPath,
- rootPath,
- workspaceRoot,
-} from './config.js';
-import { getBuildConfig } from './runtime-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 repo = new Repository(workspaceRoot);
- const shortSha = repo.head().target()?.substring(0, 9);
- if (shortSha) {
- return shortSha;
- }
- const sha = execSync(`git rev-parse --short HEAD`, {
- encoding: 'utf-8',
- }).trim();
- return sha;
-});
-
-export function createWebpackConfig(cwd: string, flags: BuildFlags) {
- console.log('build flags', flags);
- const runtimeConfig = getBuildConfig(flags);
- console.log('BUILD_CONFIG', runtimeConfig);
- const config = createConfiguration(cwd, flags, runtimeConfig);
- const entry =
- typeof flags.entry === 'string' || !flags.entry
- ? {
- app: flags.entry ?? resolve(cwd, 'src/index.tsx'),
- }
- : flags.entry;
-
- const publicPath = getPublicPath(flags);
- const cdnOrigin = publicPath.startsWith('/')
- ? undefined
- : new URL(publicPath).origin;
-
- const globalErrorHandler = [
- 'js/global-error-handler.js',
- readFileSync(
- join(workspaceRoot, 'tools/cli/src/webpack/error-handler.js'),
- 'utf-8'
- ),
- ];
-
- const templateParams = {
- GIT_SHORT_SHA: gitShortHash(),
- DESCRIPTION,
- PRECONNECT: cdnOrigin
- ? ``
- : '',
- VIEWPORT_FIT:
- flags.distribution === 'mobile' ||
- flags.distribution === 'ios' ||
- flags.distribution === 'android'
- ? 'cover'
- : 'auto',
- };
-
- const createHTMLPlugins = (entryName: string) => {
- const htmlPluginOptions = {
- template: join(rootPath, 'webpack', 'template.html'),
- inject: 'body',
- filename: 'index.html',
- minify: false,
- templateParameters: templateParams,
- chunks: [entryName],
- } satisfies HTMLPlugin.Options;
-
- if (entryName === 'app') {
- return [
- {
- apply(compiler: Compiler) {
- compiler.hooks.compilation.tap(
- 'assets-manifest-plugin',
- compilation => {
- HTMLPlugin.getHooks(compilation).beforeAssetTagGeneration.tap(
- 'assets-manifest-plugin',
- arg => {
- if (
- flags.distribution !== 'desktop' &&
- !compilation.getAsset(globalErrorHandler[0])
- ) {
- compilation.emitAsset(
- globalErrorHandler[0],
- new webpack.sources.RawSource(globalErrorHandler[1])
- );
- arg.assets.js.unshift(
- arg.assets.publicPath + globalErrorHandler[0]
- );
- }
-
- if (!compilation.getAsset('assets-manifest.json')) {
- compilation.emitAsset(
- globalErrorHandler[0],
- new webpack.sources.RawSource(globalErrorHandler[1])
- );
- compilation.emitAsset(
- `assets-manifest.json`,
- new webpack.sources.RawSource(
- JSON.stringify(
- {
- ...arg.assets,
- js: arg.assets.js.map(file =>
- file.substring(arg.assets.publicPath.length)
- ),
- css: arg.assets.css.map(file =>
- file.substring(arg.assets.publicPath.length)
- ),
- gitHash: templateParams.GIT_SHORT_SHA,
- description: templateParams.DESCRIPTION,
- },
- null,
- 2
- )
- ),
- {
- immutable: false,
- }
- );
- }
-
- return arg;
- }
- );
- }
- );
- },
- },
- new HTMLPlugin({
- ...htmlPluginOptions,
- publicPath,
- meta: {
- 'env:publicPath': publicPath,
- },
- }),
- // selfhost html
- new HTMLPlugin({
- ...htmlPluginOptions,
- meta: {
- 'env:isSelfHosted': 'true',
- 'env:publicPath': '/',
- },
- filename: 'selfhost.html',
- templateParameters: {
- ...htmlPluginOptions.templateParameters,
- PRECONNECT: '',
- },
- }),
- ];
- } else {
- return [
- new HTMLPlugin({
- ...htmlPluginOptions,
- filename: `${entryName}.html`,
- }),
- ];
- }
- };
-
- return merge(config, {
- entry,
- plugins: Object.keys(entry).map(createHTMLPlugins).flat(),
- });
-}
diff --git a/tools/cli/tsconfig.json b/tools/cli/tsconfig.json
deleted file mode 100644
index 796d7ab24f54a..0000000000000
--- a/tools/cli/tsconfig.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "extends": "../../tsconfig.json",
- "compilerOptions": {
- "composite": true,
- "allowJs": true,
- "module": "ESNext",
- "moduleResolution": "Node",
- "outDir": "lib"
- },
- "include": ["src", "package.json"],
- "references": [{ "path": "../../packages/common/env" }]
-}
diff --git a/tools/commitlint/.commitlintrc.json b/tools/commitlint/.commitlintrc.json
index e3ef1467c514a..8be259393347f 100644
--- a/tools/commitlint/.commitlintrc.json
+++ b/tools/commitlint/.commitlintrc.json
@@ -14,19 +14,17 @@
"ios",
"android",
"docs",
- "storybook",
"component",
- "workspace",
"env",
"graphql",
- "cli",
"hooks",
"i18n",
"native",
"templates",
"debug",
"nbstore",
- "infra"
+ "infra",
+ "tools"
]
]
}
diff --git a/tools/scripts/README.md b/tools/scripts/README.md
new file mode 100644
index 0000000000000..f5fd4153ec166
--- /dev/null
+++ b/tools/scripts/README.md
@@ -0,0 +1,74 @@
+# AFFiNE Monorepo scripts
+
+## Start
+
+```bash
+yarn affine -h
+```
+
+### Run build command defined in package.json
+
+```bash
+yarn affine i18n build
+# or
+yarn build -p i18n
+```
+
+### Run dev command defined in package.json
+
+```bash
+yarn affine web dev
+# or
+yarn dev -p i18n
+```
+
+### Clean
+
+```bash
+yarn affine clean --ts --dist --rust
+# clean node_modules
+yarn affine clean --node-modules
+```
+
+## Tricks
+
+### Short your key presses
+
+```bash
+# af is also available for running the scripts
+yarn af web build
+```
+
+#### by custom shell script
+
+> personally, I use 'af', and only demoed in macos.
+
+create file `af` in the root of AFFiNE project with the following content
+
+```bash
+#!/usr/bin/env sh
+./tools/scripts/bin/runner.js affine.ts $@
+```
+
+and give it executable permission
+
+```bash
+chmod a+x ./af
+
+# now you can run scripts with simply
+./af web build
+```
+
+if you want to go further, but for vscode(or other forks) only, add the following to your `.vscode/settings.json`
+
+```json
+"terminal.integrated.env.osx": {
+ "PATH": ".:$PATH"
+}
+```
+
+restart all the integrated terminals and now you get:
+
+```bash
+af web build
+```
diff --git a/tools/scripts/bin/runner.js b/tools/scripts/bin/runner.js
new file mode 100755
index 0000000000000..8f10896225fea
--- /dev/null
+++ b/tools/scripts/bin/runner.js
@@ -0,0 +1,68 @@
+#!/usr/bin/env node
+import { spawn } from 'node:child_process';
+import { existsSync } from 'node:fs';
+import { join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const scriptsFolder = join(fileURLToPath(import.meta.url), '..', '..');
+const scriptsSrcFolder = join(scriptsFolder, 'src');
+const projectRoot = join(scriptsFolder, '..', '..');
+const loader = join(scriptsFolder, 'loader.js');
+
+const [node, _self, file, ...options] = process.argv;
+
+if (!file) {
+ console.error(`Please provide a file to run, e.g. 'run src/index.{js/ts}'`);
+ process.exit(1);
+}
+
+const fileLocationCandidates = new Set([
+ process.cwd(),
+ scriptsSrcFolder,
+ projectRoot,
+]);
+const lookups = [];
+
+/**
+ * @type {string | undefined}
+ */
+let scriptLocation;
+for (const location of fileLocationCandidates) {
+ const fileCandidates = [file, `${file}.js`, `${file}.ts`];
+ for (const candidate of fileCandidates) {
+ const candidateLocation = join(location, candidate);
+ if (existsSync(candidateLocation)) {
+ scriptLocation = candidateLocation;
+ break;
+ }
+ lookups.push(candidateLocation);
+ }
+}
+
+if (!scriptLocation) {
+ console.error(
+ `File ${file} not found, please make sure the first parameter passed to 'run' script is a valid js or ts file.`
+ );
+ console.error(`Searched locations: `);
+ lookups.forEach(location => {
+ console.error(` - ${location}`);
+ });
+ process.exit(1);
+}
+
+const nodeOptions = [];
+
+if (
+ scriptLocation.endsWith('.ts') ||
+ scriptLocation.startsWith(scriptsFolder)
+) {
+ nodeOptions.unshift(`--import=${loader}`);
+}
+
+nodeOptions.unshift('--experimental-specifier-resolution=node');
+
+spawn(node, [...nodeOptions, scriptLocation, ...options], {
+ stdio: 'inherit',
+}).on('exit', code => {
+ process.exit(code);
+});
diff --git a/tools/scripts/loader.js b/tools/scripts/loader.js
new file mode 100644
index 0000000000000..57272fa69dbd0
--- /dev/null
+++ b/tools/scripts/loader.js
@@ -0,0 +1,4 @@
+import { register } from 'node:module';
+import { pathToFileURL } from 'node:url';
+
+register('ts-node/esm/transpile-only.mjs', pathToFileURL('./'));
diff --git a/tools/cli/package.json b/tools/scripts/package.json
similarity index 69%
rename from tools/cli/package.json
rename to tools/scripts/package.json
index 444e9795c0d31..9f3196db5e47d 100644
--- a/tools/cli/package.json
+++ b/tools/scripts/package.json
@@ -1,27 +1,34 @@
{
- "name": "@affine/cli",
+ "name": "@affine-tools/scripts",
+ "version": "0.0.1",
"type": "module",
"private": true,
+ "bin": {
+ "r": "./bin/runner.js"
+ },
+ "exports": {
+ "./loader": "./loader.js"
+ },
+ "scripts": {
+ "affine": "r ./src/affine.ts"
+ },
"devDependencies": {
- "@affine/env": "workspace:*",
- "@affine/templates": "workspace:*",
+ "@affine-tools/utils": "workspace:*",
"@aws-sdk/client-s3": "^3.709.0",
- "@blocksuite/affine": "0.19.2",
- "@clack/core": "^0.3.5",
- "@clack/prompts": "^0.8.2",
- "@magic-works/i18n-codegen": "^0.6.1",
"@napi-rs/simple-git": "^0.1.19",
"@perfsee/webpack": "^1.13.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
"@sentry/webpack-plugin": "^2.22.7",
+ "@types/lodash-es": "^4.17.12",
"@types/mime-types": "^2.1.4",
+ "@types/node": "^20.17.10",
"@types/webpack-env": "^1.18.5",
"@vanilla-extract/webpack-plugin": "^2.3.15",
"autoprefixer": "^10.4.20",
+ "clipanion": "^3.2.1",
"copy-webpack-plugin": "^12.0.2",
"css-loader": "^7.1.2",
"cssnano": "^7.0.6",
- "dotenv": "^16.4.7",
"html-webpack-plugin": "^5.6.3",
"lodash-es": "^4.17.21",
"mime-types": "^2.1.35",
@@ -35,14 +42,10 @@
"tailwindcss": "^3.4.16",
"terser-webpack-plugin": "^5.3.10",
"ts-node": "^10.9.2",
- "vite": "^6.0.3",
+ "typanion": "^3.14.0",
+ "typescript": "^5.5.4",
"webpack": "^5.97.1",
"webpack-dev-server": "^5.2.0",
"webpack-merge": "^6.0.1"
- },
- "scripts": {
- "bundle": "node --loader ts-node/esm/transpile-only.mjs ./src/bin/build.ts",
- "dev": "node --loader ts-node/esm/transpile-only.mjs ./src/bin/dev.ts"
- },
- "version": "0.18.0"
+ }
}
diff --git a/tools/scripts/src/affine.ts b/tools/scripts/src/affine.ts
new file mode 100644
index 0000000000000..d69acfaabe6eb
--- /dev/null
+++ b/tools/scripts/src/affine.ts
@@ -0,0 +1,32 @@
+import { Workspace } from '@affine-tools/utils/workspace';
+import { Cli } from 'clipanion';
+
+import { BuildCommand } from './build';
+import { BundleCommand } from './bundle';
+import { CleanCommand } from './clean';
+import { CodegenCommand } from './codegen';
+import type { CliContext } from './context';
+import { DevCommand } from './dev';
+import { RunCommand } from './run';
+
+const cli = new Cli({
+ binaryName: 'affine',
+ binaryVersion: '0.0.0',
+ binaryLabel: 'AFFiNE Monorepo Tools',
+ enableColors: true,
+ enableCapture: true,
+});
+
+cli.register(RunCommand);
+cli.register(CodegenCommand);
+cli.register(CleanCommand);
+cli.register(BuildCommand);
+cli.register(DevCommand);
+cli.register(BundleCommand);
+
+await cli.runExit(process.argv.slice(2), {
+ workspace: new Workspace(),
+ stdin: process.stdin,
+ stdout: process.stdout,
+ stderr: process.stderr,
+});
diff --git a/tools/scripts/src/build.ts b/tools/scripts/src/build.ts
new file mode 100644
index 0000000000000..4e63be82e4579
--- /dev/null
+++ b/tools/scripts/src/build.ts
@@ -0,0 +1,15 @@
+import { PackageCommand } from './command';
+
+export class BuildCommand extends PackageCommand {
+ static override paths = [['build'], ['b']];
+
+ async execute() {
+ this.logger.info(`Building package ${this.package}...`);
+
+ await this.workspace.run(this.package, 'build', {
+ includeDependencies: this.deps,
+ });
+
+ this.logger.info(`${this.package} built.`);
+ }
+}
diff --git a/tools/scripts/src/bundle.ts b/tools/scripts/src/bundle.ts
new file mode 100644
index 0000000000000..fc32e844988fd
--- /dev/null
+++ b/tools/scripts/src/bundle.ts
@@ -0,0 +1,94 @@
+import { existsSync } from 'node:fs';
+
+import webpack, { type Compiler, type Configuration } from 'webpack';
+import WebpackDevServer from 'webpack-dev-server';
+import { merge } from 'webpack-merge';
+
+import { Option, PackageCommand } from './command';
+import { createWebpackConfig } from './webpack';
+
+function getChannel() {
+ const channel = process.env.BUILD_TYPE ?? 'canary';
+ switch (channel) {
+ case 'canary':
+ case 'beta':
+ case 'stable':
+ case 'internal':
+ return channel;
+ default: {
+ throw new Error(
+ `BUILD_TYPE must be one of canary, beta, stable, internal, received [${channel}]`
+ );
+ }
+ }
+}
+
+export class BundleCommand extends PackageCommand {
+ static override paths = [['webpack'], ['pack'], ['bundle'], ['bun']];
+
+ // bundle is not able to run with deps
+ override deps = false;
+
+ dev = Option.Boolean('--dev,-d', false, {
+ description: 'Run in Development mode',
+ });
+
+ async execute() {
+ this.logger.info(`Packing package ${this.package}...`);
+
+ const config = await this.getConfig();
+
+ const compiler = webpack(config);
+
+ if (this.dev) {
+ await this.start(compiler, config.devServer);
+ } else {
+ await this.build(compiler);
+ }
+ }
+
+ async getConfig() {
+ let config = createWebpackConfig(this.workspace.getPackage(this.package), {
+ mode: this.dev ? 'development' : 'production',
+ channel: getChannel(),
+ });
+
+ let configOverride: Configuration | undefined;
+ const overridePath = this.workspace
+ .getPackage(this.package)
+ .absolute('webpack.config.ts');
+
+ if (existsSync(overridePath)) {
+ configOverride = await import(overridePath);
+ }
+
+ if (configOverride) {
+ config = merge(config, configOverride);
+ }
+
+ return config;
+ }
+
+ async start(compiler: Compiler, config: Configuration['devServer']) {
+ const devServer = new WebpackDevServer(config, compiler);
+
+ await devServer.start();
+ }
+
+ async build(compiler: Compiler) {
+ compiler.run((error, stats) => {
+ if (error) {
+ console.error(error);
+ process.exit(1);
+ }
+ if (stats) {
+ if (stats.hasErrors()) {
+ console.error(stats.toString('errors-only'));
+ process.exit(1);
+ } else {
+ console.log(stats.toString('minimal'));
+ }
+ }
+ });
+ }
+}
diff --git a/tools/scripts/src/clean.ts b/tools/scripts/src/clean.ts
new file mode 100644
index 0000000000000..d51a37b970fb2
--- /dev/null
+++ b/tools/scripts/src/clean.ts
@@ -0,0 +1,84 @@
+import { existsSync, rmSync } from 'node:fs';
+
+import { exec } from '@affine-tools/utils/process';
+
+import { Command, Option } from './command';
+
+export class CleanCommand extends Command {
+ static override paths = [['clean']];
+
+ cleanTsBuild = Option.Boolean('--ts', false);
+ cleanDist = Option.Boolean('--dist', false);
+ cleanRustTarget = Option.Boolean('--rust', false);
+ cleanNodeModules = Option.Boolean('--node-modules', false);
+ all = Option.Boolean('--all,-a', false);
+
+ async execute() {
+ this.logger.info('Cleaning Workspace...');
+ if (this.all || this.cleanNodeModules) {
+ this.doCleanNodeModules();
+ }
+
+ if (this.all || this.cleanDist) {
+ this.doCleanDist();
+ }
+
+ if (this.all || this.cleanRustTarget) {
+ this.doCleanRust();
+ }
+
+ if (this.all || this.cleanTsBuild) {
+ this.doCleanTs();
+ }
+ }
+
+ doCleanNodeModules() {
+ this.logger.info('Cleaning node_modules...');
+
+ const rootNodeModules = this.workspace.absolute('node_modules');
+ if (existsSync(rootNodeModules)) {
+ this.logger.info(`Cleaning ${rootNodeModules}`);
+ rmSync(rootNodeModules, { recursive: true });
+ }
+
+ this.workspace.forEach(pkg => {
+ const nodeModules = pkg.nodeModulesPath;
+ if (existsSync(nodeModules)) {
+ this.logger.info(`Cleaning ${nodeModules}`);
+ rmSync(nodeModules, { recursive: true });
+ }
+ });
+
+ this.logger.info('node_modules cleaned');
+ }
+
+ doCleanTs() {
+ this.logger.info('Cleaning ts build outputs...');
+
+ this.workspace.forEach(pkg => {
+ if (existsSync(pkg.tsbuildPath)) {
+ this.logger.info(`Cleaning ${pkg.tsbuildPath}`);
+ rmSync(pkg.tsbuildPath, { recursive: true });
+ }
+ });
+
+ this.logger.info('ts build outputs cleaned');
+ }
+
+ doCleanDist() {
+ this.logger.info('Cleaning dist...');
+
+ this.workspace.forEach(pkg => {
+ if (existsSync(pkg.distPath)) {
+ this.logger.info(`Cleaning ${pkg.distPath}`);
+ rmSync(pkg.distPath, { recursive: true });
+ }
+ });
+
+ this.logger.info('dist cleaned');
+ }
+
+ doCleanRust() {
+ exec('', 'cargo clean');
+ }
+}
diff --git a/tools/scripts/src/codegen.ts b/tools/scripts/src/codegen.ts
new file mode 100755
index 0000000000000..6bf2252465ab9
--- /dev/null
+++ b/tools/scripts/src/codegen.ts
@@ -0,0 +1,35 @@
+import { writeFileSync } from 'node:fs';
+
+import { Command } from './command';
+
+export class CodegenCommand extends Command {
+ static override paths = [['codegen'], ['gg'], ['g']];
+
+ async execute() {
+ this.logger.info('Generating Workspace configs');
+ this.generateWorkspaceFiles();
+ this.logger.info('Workspace configs generated');
+ }
+
+ generateWorkspaceFiles() {
+ const filesToGenerate: [string, () => string][] = [
+ [
+ this.workspace.relative('tsconfig.project.json'),
+ this.workspace.genProjectTsConfig,
+ ],
+ [
+ this.workspace
+ .getPackage('@affine-tools/utils')
+ .relative('src/workspace.gen.ts'),
+ this.workspace.genWorkspaceInfo,
+ ],
+ ];
+ for (const [path, content] of filesToGenerate) {
+ this.logger.info(`Output: ${path}`);
+ writeFileSync(
+ this.workspace.absolute(path),
+ content.apply(this.workspace)
+ );
+ }
+ }
+}
diff --git a/tools/scripts/src/command.ts b/tools/scripts/src/command.ts
new file mode 100644
index 0000000000000..ab5b54da14c06
--- /dev/null
+++ b/tools/scripts/src/command.ts
@@ -0,0 +1,71 @@
+import { AliasToPackage } from '@affine-tools/utils/distribution';
+import { Logger } from '@affine-tools/utils/logger';
+import { type PackageName, Workspace } from '@affine-tools/utils/workspace';
+import { Command as BaseCommand, Option } from 'clipanion';
+import * as t from 'typanion';
+
+import type { CliContext } from './context';
+
+export abstract class Command extends BaseCommand {
+ get logger() {
+ // @ts-expect-error hack: Get the command name
+ return new Logger(this.constructor.paths[0][0]);
+ }
+
+ get workspace() {
+ return this.context.workspace;
+ }
+}
+
+export abstract class PackageCommand extends Command {
+ protected availablePackageNameArgs = (
+ Workspace.PackageNames as string[]
+ ).concat(Array.from(AliasToPackage.keys()));
+ protected packageNameValidator = t.isOneOf(
+ this.availablePackageNameArgs.map(k => t.isLiteral(k))
+ );
+
+ protected packageNameOrAlias = Option.String('--package,-p', {
+ required: true,
+ validator: this.packageNameValidator,
+ description: 'The package name or alias to be run with',
+ });
+
+ get package(): PackageName {
+ return (
+ AliasToPackage.get(this.packageNameOrAlias as any) ??
+ (this.packageNameOrAlias as PackageName)
+ );
+ }
+
+ deps = Option.Boolean('--deps', false, {
+ description:
+ 'Execute the same command in workspace dependencies, if defined.',
+ });
+}
+
+export abstract class PackagesCommand extends Command {
+ protected availablePackageNameArgs = (
+ Workspace.PackageNames as string[]
+ ).concat(Array.from(AliasToPackage.keys()));
+ protected packageNameValidator = t.isOneOf(
+ this.availablePackageNameArgs.map(k => t.isLiteral(k))
+ );
+
+ protected packageNamesOrAliases = Option.Array('--package,-p', {
+ required: true,
+ validator: t.isArray(this.packageNameValidator),
+ });
+ get packages() {
+ return this.packageNamesOrAliases.map(
+ name => AliasToPackage.get(name as any) ?? name
+ );
+ }
+
+ deps = Option.Boolean('--deps', false, {
+ description:
+ 'Execute the same command in workspace dependencies, if defined.',
+ });
+}
+
+export { Option };
diff --git a/tools/scripts/src/context.ts b/tools/scripts/src/context.ts
new file mode 100644
index 0000000000000..bc97d15a67328
--- /dev/null
+++ b/tools/scripts/src/context.ts
@@ -0,0 +1,6 @@
+import type { Workspace } from '@affine-tools/utils/workspace';
+import type { BaseContext } from 'clipanion';
+
+export interface CliContext extends BaseContext {
+ workspace: Workspace;
+}
diff --git a/tools/scripts/src/dev.ts b/tools/scripts/src/dev.ts
new file mode 100644
index 0000000000000..e449ca40e6882
--- /dev/null
+++ b/tools/scripts/src/dev.ts
@@ -0,0 +1,13 @@
+import { PackageCommand } from './command';
+
+export class DevCommand extends PackageCommand {
+ static override paths = [['dev'], ['d']];
+ override deps = false;
+
+ async execute() {
+ await this.workspace.run(this.package, 'dev', {
+ // dev command not support run with dependencies
+ includeDependencies: false,
+ });
+ }
+}
diff --git a/tools/scripts/src/run.ts b/tools/scripts/src/run.ts
new file mode 100644
index 0000000000000..56b17e5dd6093
--- /dev/null
+++ b/tools/scripts/src/run.ts
@@ -0,0 +1,104 @@
+import { pathRelativeToMe } from '@affine-tools/utils/path';
+import { execAsync } from '@affine-tools/utils/process';
+import type { PackageName } from '@affine-tools/utils/workspace';
+
+import { Option, PackageCommand } from './command';
+
+interface RunScriptOptions {
+ ignoreMissingScript?: boolean;
+ includeDependencies?: boolean;
+ waitDependencies?: boolean;
+}
+
+const pathTo = pathRelativeToMe(import.meta.url);
+
+export class RunCommand extends PackageCommand {
+ static override paths = [[], ['run'], ['r']];
+
+ static override usage = PackageCommand.Usage({
+ description: 'AFFiNE Monorepo scripts',
+ details: `
+ \`affine my-script\` Run any script defined in package's package.json
+
+ \`affine codegen\` Generate the required files if there are any package added or removed
+
+ \`affine clean\` Clean the output files of ts, cargo, webpack, etc.
+
+ \`affine bundle\` Bundle the packages
+
+ \`affine build\` A proxy for <-p package>'s \`build\` script
+
+ \`affine dev\` A proxy for <-p package>'s \`dev\` script
+ `,
+ examples: [
+ [`See detail of each command`, '$0 -h'],
+ [
+ `Run custom 'xxx' script defined in @affine/web's package.json`,
+ '$0 web xxx',
+ ],
+ [`Run 'codegen' for workspace`, '$0 codegen'],
+ [`Clean tsbuild and dist under each package`, '$0 clean --ts --dist'],
+ [`Clean node_modules under each package`, '$0 clean --node-modules'],
+ [`Clean everything`, '$0 clean --all'],
+ [`Run 'build' script for @affine/web`, '$0 build -p web'],
+ [
+ `Run 'build' script for @affine/web with all deps prebuild before`,
+ '$0 build -p web --deps',
+ ],
+ ],
+ });
+
+ // we use positional arguments instead of options
+ protected override packageNameOrAlias: string = Option.String({
+ required: true,
+ validator: this.packageNameValidator,
+ });
+ script = Option.String({ required: true });
+
+ async execute() {
+ await this.run(this.package, this.script, {
+ includeDependencies: this.deps,
+ waitDependencies: true,
+ });
+ }
+
+ async run(name: PackageName, script: string, opts: RunScriptOptions = {}) {
+ opts = { includeDependencies: false, ignoreMissingScript: false, ...opts };
+
+ const pkg = this.workspace.getPackage(name);
+ const scriptToRun = pkg.scripts[script];
+
+ if (opts.includeDependencies) {
+ await Promise.all(
+ pkg.deps.map(dep =>
+ this.run(dep.name, script, {
+ ...opts,
+ // the dependency may not have the script
+ ignoreMissingScript: true,
+ })
+ )
+ );
+ }
+
+ if (!scriptToRun) {
+ if (!opts.ignoreMissingScript) {
+ throw new Error(
+ `Script '${script}' is not found in ${pkg.relative('package.json')} of package '${name}'. `
+ );
+ }
+
+ return;
+ } else {
+ if (scriptToRun.startsWith('affine ')) {
+ await this.cli.run([scriptToRun.slice(7), '-p', pkg.name]);
+ } else {
+ await execAsync(name, `yarn run ${script}`, {
+ cwd: pkg.path,
+ env: {
+ NODE_OPTIONS: `--experimental-specifier-resolution=node --import=${pathTo('../loader.js')}`,
+ },
+ });
+ }
+ }
+ }
+}
diff --git a/tools/cli/src/webpack/cache-group.ts b/tools/scripts/src/webpack/cache-group.ts
similarity index 100%
rename from tools/cli/src/webpack/cache-group.ts
rename to tools/scripts/src/webpack/cache-group.ts
diff --git a/tools/cli/src/webpack/error-handler.js b/tools/scripts/src/webpack/error-handler.js
similarity index 100%
rename from tools/cli/src/webpack/error-handler.js
rename to tools/scripts/src/webpack/error-handler.js
diff --git a/tools/scripts/src/webpack/html-plugin.ts b/tools/scripts/src/webpack/html-plugin.ts
new file mode 100644
index 0000000000000..53eccdcda98fe
--- /dev/null
+++ b/tools/scripts/src/webpack/html-plugin.ts
@@ -0,0 +1,180 @@
+import { execSync } from 'node:child_process';
+import { readFileSync } from 'node:fs';
+
+import type { BUILD_CONFIG_TYPE } from '@affine/env/global';
+import { pathRelativeToMe, root } from '@affine-tools/utils/path';
+import { Repository } from '@napi-rs/simple-git';
+import HTMLPlugin from 'html-webpack-plugin';
+import { once } from 'lodash-es';
+import type { Compiler, WebpackPluginInstance } from 'webpack';
+import webpack from 'webpack';
+
+import type { BuildFlags } from './types.js';
+
+export const getPublicPath = (flags: BuildFlags) => {
+ const { BUILD_TYPE } = process.env;
+ if (typeof process.env.PUBLIC_PATH === 'string') {
+ return process.env.PUBLIC_PATH;
+ }
+
+ if (flags.mode === 'development') {
+ return '/';
+ }
+
+ switch (BUILD_TYPE) {
+ case 'stable':
+ return 'https://prod.affineassets.com/';
+ case 'beta':
+ return 'https://beta.affineassets.com/';
+ default:
+ return 'https://dev.affineassets.com/';
+ }
+};
+
+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 repo = new Repository(root);
+ const shortSha = repo.head().target()?.substring(0, 9);
+ if (shortSha) {
+ return shortSha;
+ }
+ const sha = execSync(`git rev-parse --short HEAD`, {
+ encoding: 'utf-8',
+ }).trim();
+ return sha;
+});
+
+const pathTo = pathRelativeToMe(import.meta.url);
+
+function getHTMLPluginOptions(flags: BuildFlags, BUILD_CONFIG: BUILD_CONFIG_TYPE) {
+ const publicPath = getPublicPath(flags);
+ const cdnOrigin = publicPath.startsWith('/')
+ ? undefined
+ : new URL(publicPath).origin;
+
+ const templateParams = {
+ GIT_SHORT_SHA: gitShortHash(),
+ DESCRIPTION,
+ PRECONNECT: cdnOrigin
+ ? ``
+ : '',
+ VIEWPORT_FIT: BUILD_CONFIG.isMobileEdition
+ ? 'cover'
+ : 'auto',
+ };
+
+ return {
+ template: pathTo('template.html'),
+ inject: 'body',
+ filename: 'index.html',
+ minify: false,
+ templateParameters: templateParams,
+ chunks: ['app'],
+ } satisfies HTMLPlugin.Options;
+}
+
+export function createShellHTMLPlugin(flags: BuildFlags, BUILD_CONFIG: BUILD_CONFIG_TYPE) {
+ const htmlPluginOptions = getHTMLPluginOptions(flags, BUILD_CONFIG);
+
+ return new HTMLPlugin({
+ ...htmlPluginOptions,
+ chunks: ['shell'],
+ filename: `shell.html`,
+ })
+}
+
+export function createHTMLPlugins(flags: BuildFlags, BUILD_CONFIG: BUILD_CONFIG_TYPE): WebpackPluginInstance[] {
+ const publicPath = getPublicPath(flags);
+ const globalErrorHandler = [
+ 'js/global-error-handler.js',
+ readFileSync(pathTo('./error-handler.js'), 'utf-8'),
+ ];
+
+ const htmlPluginOptions = getHTMLPluginOptions(flags, BUILD_CONFIG);
+
+ return [
+ {
+ apply(compiler: Compiler) {
+ compiler.hooks.compilation.tap(
+ 'assets-manifest-plugin',
+ compilation => {
+ HTMLPlugin.getHooks(compilation).beforeAssetTagGeneration.tap(
+ 'assets-manifest-plugin',
+ arg => {
+ if (
+ !BUILD_CONFIG.isElectron &&
+ !compilation.getAsset(globalErrorHandler[0])
+ ) {
+ compilation.emitAsset(
+ globalErrorHandler[0],
+ new webpack.sources.RawSource(globalErrorHandler[1])
+ );
+ arg.assets.js.unshift(
+ arg.assets.publicPath + globalErrorHandler[0]
+ );
+ }
+
+ if (!compilation.getAsset('assets-manifest.json')) {
+ compilation.emitAsset(
+ globalErrorHandler[0],
+ new webpack.sources.RawSource(globalErrorHandler[1])
+ );
+ compilation.emitAsset(
+ `assets-manifest.json`,
+ new webpack.sources.RawSource(
+ JSON.stringify(
+ {
+ ...arg.assets,
+ js: arg.assets.js.map(file =>
+ file.substring(arg.assets.publicPath.length)
+ ),
+ css: arg.assets.css.map(file =>
+ file.substring(arg.assets.publicPath.length)
+ ),
+ gitHash: htmlPluginOptions.templateParameters.GIT_SHORT_SHA,
+ description: htmlPluginOptions.templateParameters.DESCRIPTION,
+ },
+ null,
+ 2
+ )
+ ),
+ {
+ immutable: false,
+ }
+ );
+ }
+
+ return arg;
+ }
+ );
+ }
+ );
+ },
+ },
+ new HTMLPlugin({
+ ...htmlPluginOptions,
+ publicPath,
+ meta: {
+ 'env:publicPath': publicPath,
+ },
+ }),
+ // selfhost html
+ new HTMLPlugin({
+ ...htmlPluginOptions,
+ meta: {
+ 'env:isSelfHosted': 'true',
+ 'env:publicPath': '/',
+ },
+ filename: 'selfhost.html',
+ templateParameters: {
+ ...htmlPluginOptions.templateParameters,
+ PRECONNECT: '',
+ },
+ }),
+ ];
+}
diff --git a/tools/cli/src/webpack/config.ts b/tools/scripts/src/webpack/index.ts
similarity index 76%
rename from tools/cli/src/webpack/config.ts
rename to tools/scripts/src/webpack/index.ts
index e39038277d9df..8f5150f57cf26 100644
--- a/tools/cli/src/webpack/config.ts
+++ b/tools/scripts/src/webpack/index.ts
@@ -1,7 +1,5 @@
-import { join } from 'node:path';
-import { fileURLToPath } from 'node:url';
-
-import type { BUILD_CONFIG_TYPE } from '@affine/env/global';
+import { getBuildConfig } from '@affine-tools/utils/build-config';
+import type { Package } from '@affine-tools/utils/workspace';
import { PerfseePlugin } from '@perfsee/webpack';
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
import { sentryWebpackPlugin } from '@sentry/webpack-plugin';
@@ -13,20 +11,17 @@ import TerserPlugin from 'terser-webpack-plugin';
import webpack from 'webpack';
import type { Configuration as DevServerConfiguration } from 'webpack-dev-server';
-import { projectRoot } from '../config/cwd.cjs';
-import type { BuildFlags } from '../config/index.js';
import { productionCacheGroups } from './cache-group.js';
+import { createHTMLPlugins, createShellHTMLPlugin } from './html-plugin.js';
import { WebpackS3Plugin } from './s3-plugin.js';
+import type { BuildFlags } from './types';
const IN_CI = !!process.env.CI;
-export const rootPath = join(fileURLToPath(import.meta.url), '..', '..');
-export const workspaceRoot = join(rootPath, '..', '..', '..');
-
const OptimizeOptionOptions: (
- buildFlags: BuildFlags
-) => webpack.Configuration['optimization'] = buildFlags => ({
- minimize: buildFlags.mode === 'production',
+ flags: BuildFlags
+) => webpack.Configuration['optimization'] = flags => ({
+ minimize: flags.mode === 'production',
minimizer: [
new TerserPlugin({
minify: TerserPlugin.swcMinify,
@@ -60,71 +55,45 @@ const OptimizeOptionOptions: (
},
});
-export const getPublicPath = (buildFlags: BuildFlags) => {
- const { BUILD_TYPE } = process.env;
- if (typeof process.env.PUBLIC_PATH === 'string') {
- return process.env.PUBLIC_PATH;
- }
-
- if (
- buildFlags.mode === 'development' ||
- process.env.COVERAGE ||
- buildFlags.distribution === 'desktop' ||
- buildFlags.distribution === 'ios' ||
- buildFlags.distribution === 'android'
- ) {
- return '/';
- }
-
- switch (BUILD_TYPE) {
- case 'stable':
- return 'https://prod.affineassets.com/';
- case 'beta':
- return 'https://beta.affineassets.com/';
- default:
- return 'https://dev.affineassets.com/';
- }
-};
+export function createWebpackConfig(pkg: Package, flags: BuildFlags): webpack.Configuration {
+ const buildConfig = getBuildConfig(pkg, flags);
-export const createConfiguration: (
- cwd: string,
- buildFlags: BuildFlags,
- buildConfig: BUILD_CONFIG_TYPE
-) => webpack.Configuration = (cwd, buildFlags, buildConfig) => {
const config = {
name: 'affine',
- // to set a correct base path for the source map
- context: cwd,
+ context: pkg.path,
experiments: {
topLevelAwait: true,
outputModule: false,
syncWebAssembly: true,
},
+ entry: {
+ app: pkg.entry ?? './src/index.tsx',
+ },
output: {
environment: {
module: true,
dynamicImport: true,
},
filename:
- buildFlags.mode === 'production'
+ flags.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: pathData =>
pathData.chunk?.name?.endsWith?.('worker')
? 'js/[name].[contenthash:8].js'
- : buildFlags.mode === 'production'
+ : flags.mode === 'production'
? 'js/chunk.[name].[contenthash:8].js'
: 'js/chunk.[name].js',
assetModuleFilename:
- buildFlags.mode === 'production'
+ flags.mode === 'production'
? 'assets/[name].[contenthash:8][ext][query]'
: '[name].[contenthash:8][ext]',
devtoolModuleFilenameTemplate: 'webpack://[namespace]/[resource-path]',
hotUpdateChunkFilename: 'hot/[id].[fullhash].js',
hotUpdateMainFilename: 'hot/[runtime].[fullhash].json',
- path: join(cwd, 'dist'),
- clean: buildFlags.mode === 'production',
+ path: pkg.distPath,
+ clean: flags.mode === 'production',
globalObject: 'globalThis',
// NOTE(@forehalo): always keep it '/'
publicPath: '/',
@@ -132,10 +101,10 @@ export const createConfiguration: (
},
target: ['web', 'es2022'],
- mode: buildFlags.mode,
+ mode: flags.mode,
devtool:
- buildFlags.mode === 'production'
+ flags.mode === 'production'
? 'source-map'
: 'eval-cheap-module-source-map',
@@ -147,10 +116,9 @@ export const createConfiguration: (
},
extensions: ['.js', '.ts', '.tsx'],
alias: {
- yjs: join(workspaceRoot, 'node_modules', 'yjs'),
- lit: join(workspaceRoot, 'node_modules', 'lit'),
- '@preact/signals-core': join(
- workspaceRoot,
+ yjs: pkg.workspace.absolute('node_modules', 'yjs'),
+ lit: pkg.workspace.absolute('node_modules', 'lit'),
+ '@preact/signals-core': pkg.workspace.absolute(
'node_modules',
'@preact',
'signals-core'
@@ -230,7 +198,7 @@ export const createConfiguration: (
transform: {
react: {
runtime: 'automatic',
- refresh: buildFlags.mode === 'development' && {
+ refresh: flags.mode === 'development' && {
refreshReg: '$RefreshReg$',
refreshSig: '$RefreshSig$',
emitFullSignatures: true,
@@ -263,7 +231,7 @@ export const createConfiguration: (
{
test: /\.css$/,
use: [
- buildFlags.mode === 'development'
+ flags.mode === 'development'
? 'style-loader'
: MiniCssExtractPlugin.loader,
{
@@ -280,7 +248,10 @@ export const createConfiguration: (
loader: 'postcss-loader',
options: {
postcssOptions: {
- config: join(rootPath, 'webpack', 'postcss.config.cjs'),
+ plugins: [
+ 'tailwindcss',
+ 'autoprefixer'
+ ]
},
},
},
@@ -292,7 +263,7 @@ export const createConfiguration: (
},
plugins: compact([
IN_CI ? null : new webpack.ProgressPlugin({ percentBy: 'entries' }),
- buildFlags.mode === 'development'
+ flags.mode === 'development'
? new ReactRefreshWebpackPlugin({
overlay: false,
esModule: true,
@@ -305,7 +276,7 @@ export const createConfiguration: (
}),
new VanillaExtractPlugin(),
new webpack.DefinePlugin({
- 'process.env.NODE_ENV': JSON.stringify(buildFlags.mode),
+ 'process.env.NODE_ENV': JSON.stringify(flags.mode),
'process.env.CAPTCHA_SITE_KEY': JSON.stringify(
process.env.CAPTCHA_SITE_KEY
),
@@ -322,25 +293,19 @@ export const createConfiguration: (
},
{} as Record
),
- }),
- buildFlags.distribution === 'admin'
+ }),
+ buildConfig.isAdmin
? null
: new CopyPlugin({
patterns: [
{
// copy the shared public assets into dist
- from: join(
- workspaceRoot,
- 'packages',
- 'frontend',
- 'core',
- 'public'
- ),
- to: join(cwd, 'dist'),
+ from: pkg.workspace.getPackage('@affine/core').absolute('public'),
+ to: pkg.distPath,
},
],
}),
- buildFlags.mode === 'production' &&
+ flags.mode === 'production' &&
(buildConfig.isWeb || buildConfig.isMobileWeb || buildConfig.isAdmin) &&
process.env.R2_SECRET_ACCESS_KEY
? new WebpackS3Plugin()
@@ -349,34 +314,21 @@ export const createConfiguration: (
stats: {
errorDetails: true,
},
-
- optimization: OptimizeOptionOptions(buildFlags),
-
+ optimization: OptimizeOptionOptions(flags),
devServer: {
host: '0.0.0.0',
allowedHosts: 'all',
- hot: buildFlags.static ? false : 'only',
- liveReload: !buildFlags.static,
+ hot: true,
+ liveReload: true,
client: {
overlay: process.env.DISABLE_DEV_OVERLAY === 'true' ? false : undefined,
},
historyApiFallback: true,
static: [
{
- directory: join(
- projectRoot,
- 'packages',
- 'frontend',
- 'core',
- 'public'
- ),
+ directory: pkg.workspace.getPackage('@affine/core').absolute('public'),
publicPath: '/',
- watch: !buildFlags.static,
- },
- {
- directory: join(cwd, 'public'),
- publicPath: '/',
- watch: !buildFlags.static,
+ watch: true,
},
],
proxy: [
@@ -393,7 +345,7 @@ export const createConfiguration: (
} as DevServerConfiguration,
} satisfies webpack.Configuration;
- if (buildFlags.mode === 'production' && process.env.PERFSEE_TOKEN) {
+ if (flags.mode === 'production' && process.env.PERFSEE_TOKEN) {
config.plugins.push(
new PerfseePlugin({
project: 'affine-toeverything',
@@ -401,7 +353,7 @@ export const createConfiguration: (
);
}
- if (buildFlags.mode === 'development') {
+ if (flags.mode === 'development') {
config.optimization = {
...config.optimization,
minimize: false,
@@ -445,5 +397,13 @@ export const createConfiguration: (
);
}
+ config.plugins = config.plugins.concat(
+ createHTMLPlugins(flags, buildConfig)
+)
+
+ if (buildConfig.isElectron) {
+ config.plugins.push(createShellHTMLPlugin(flags, buildConfig));
+ }
+
return config;
};
diff --git a/tools/cli/src/webpack/s3-plugin.ts b/tools/scripts/src/webpack/s3-plugin.ts
similarity index 90%
rename from tools/cli/src/webpack/s3-plugin.ts
rename to tools/scripts/src/webpack/s3-plugin.ts
index c6cc3aaf108b9..fdb2a894642ba 100644
--- a/tools/cli/src/webpack/s3-plugin.ts
+++ b/tools/scripts/src/webpack/s3-plugin.ts
@@ -15,9 +15,9 @@ export class WebpackS3Plugin implements WebpackPluginInstance {
region: 'auto',
endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
},
});
diff --git a/tools/cli/src/webpack/template.html b/tools/scripts/src/webpack/template.html
similarity index 100%
rename from tools/cli/src/webpack/template.html
rename to tools/scripts/src/webpack/template.html
diff --git a/tools/scripts/src/webpack/types.ts b/tools/scripts/src/webpack/types.ts
new file mode 100644
index 0000000000000..9e210731cd413
--- /dev/null
+++ b/tools/scripts/src/webpack/types.ts
@@ -0,0 +1,6 @@
+export interface BuildFlags {
+ mode: 'development' | 'production';
+ channel: 'stable' | 'beta' | 'canary' | 'internal';
+}
+
+
diff --git a/tools/scripts/tsconfig.json b/tools/scripts/tsconfig.json
new file mode 100644
index 0000000000000..2009c2543cbaf
--- /dev/null
+++ b/tools/scripts/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "composite": true,
+ "lib": ["ES2023"],
+ "rootDir": "./src",
+ "outDir": "./dist"
+ },
+ "include": ["./src"],
+ "references": [
+ { "path": "../../packages/common/env" },
+ { "path": "../utils" }
+ ]
+}
diff --git a/tools/utils/package.json b/tools/utils/package.json
new file mode 100644
index 0000000000000..107d2c29f7f21
--- /dev/null
+++ b/tools/utils/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@affine-tools/utils",
+ "version": "0.0.1",
+ "type": "module",
+ "private": true,
+ "exports": {
+ "./path": "./src/path.ts",
+ "./workspace": "./src/workspace.ts",
+ "./process": "./src/process.ts",
+ "./logger": "./src/logger.ts",
+ "./build-config": "./src/build-config.ts",
+ "./distribution": "./src/distribution.ts"
+ },
+ "devDependencies": {
+ "@types/lodash-es": "^4.17.12",
+ "@types/node": "^20.17.10",
+ "chalk": "^5.3.0",
+ "lodash-es": "^4.17.21",
+ "typescript": "^5.5.4"
+ }
+}
diff --git a/tools/cli/src/webpack/runtime-config.ts b/tools/utils/src/build-config.ts
similarity index 67%
rename from tools/cli/src/webpack/runtime-config.ts
rename to tools/utils/src/build-config.ts
index c5d61254c46c5..2d8bfe1305ae7 100644
--- a/tools/cli/src/webpack/runtime-config.ts
+++ b/tools/utils/src/build-config.ts
@@ -1,32 +1,46 @@
import type { BUILD_CONFIG_TYPE } from '@affine/env/global';
+import type { Package } from '@affine-tools/utils/workspace';
-import packageJson from '../../package.json' with { type: 'json' };
-import type { BuildFlags } from '../config';
+import { PackageToDistribution } from './distribution';
+
+export interface BuildFlags {
+ channel: 'stable' | 'beta' | 'internal' | 'canary';
+ mode: 'development' | 'production';
+}
+
+export function getBuildConfig(
+ pkg: Package,
+ buildFlags: BuildFlags
+): BUILD_CONFIG_TYPE {
+ const distribution = PackageToDistribution.get(pkg.name);
+
+ if (!distribution) {
+ throw new Error(`Distribution for ${pkg.name} is not found`);
+ }
-export function getBuildConfig(buildFlags: BuildFlags): BUILD_CONFIG_TYPE {
const buildPreset: Record = {
get stable() {
return {
debug: buildFlags.mode === 'development',
- distribution: buildFlags.distribution,
+ distribution,
isDesktopEdition: (
- ['web', 'desktop', 'admin'] as BuildFlags['distribution'][]
- ).includes(buildFlags.distribution),
+ ['web', 'desktop', 'admin'] as BUILD_CONFIG_TYPE['distribution'][]
+ ).includes(distribution),
isMobileEdition: (
- ['mobile', 'ios', 'android'] as BuildFlags['distribution'][]
- ).includes(buildFlags.distribution),
- isElectron: buildFlags.distribution === 'desktop',
- isWeb: buildFlags.distribution === 'web',
- isMobileWeb: buildFlags.distribution === 'mobile',
- isIOS: buildFlags.distribution === 'ios',
- isAndroid: buildFlags.distribution === 'android',
- isAdmin: buildFlags.distribution === 'admin',
+ ['mobile', 'ios', 'android'] as BUILD_CONFIG_TYPE['distribution'][]
+ ).includes(distribution),
+ isElectron: distribution === 'desktop',
+ isWeb: distribution === 'web',
+ isMobileWeb: distribution === 'mobile',
+ isIOS: distribution === 'ios',
+ isAndroid: distribution === 'android',
+ isAdmin: distribution === 'admin',
isSelfHosted: process.env.SELF_HOSTED === 'true',
appBuildType: 'stable' as const,
serverUrlPrefix: 'https://app.affine.pro',
- appVersion: packageJson.version,
- editorVersion: packageJson.devDependencies['@blocksuite/affine'],
+ appVersion: pkg.workspace.version,
+ editorVersion: pkg.workspace.devDependencies['@blocksuite/affine'],
githubUrl: 'https://github.com/toeverything/AFFiNE',
changelogUrl: 'https://affine.pro/what-is-new',
downloadUrl: 'https://affine.pro/download',
diff --git a/tools/utils/src/distribution.ts b/tools/utils/src/distribution.ts
new file mode 100644
index 0000000000000..e17b76d7258e5
--- /dev/null
+++ b/tools/utils/src/distribution.ts
@@ -0,0 +1,30 @@
+import type { BUILD_CONFIG_TYPE } from '@affine/env/global';
+
+import { PackageList, type PackageName } from './workspace.gen';
+
+export const PackageToDistribution = new Map<
+ PackageName,
+ BUILD_CONFIG_TYPE['distribution']
+>([
+ ['@affine/admin', 'admin'],
+ ['@affine/web', 'web'],
+ ['@affine/electron', 'desktop'],
+ ['@affine/mobile', 'mobile'],
+ ['@affine/ios', 'ios'],
+ ['@affine/android', 'android'],
+]);
+
+export const AliasToPackage = new Map([
+ ['admin', '@affine/admin'],
+ ['web', '@affine/web'],
+ ['desktop', '@affine/electron'],
+ ['electron', '@affine/electron'],
+ ['mobile', '@affine/mobile'],
+ ['ios', '@affine/ios'],
+ ['android', '@affine/android'],
+ ['server', '@affine/server'],
+ ['gql', '@affine/graphql'],
+ ...PackageList.map(
+ pkg => [pkg.name.split('/').pop()!, pkg.name] as [string, PackageName]
+ ),
+]);
diff --git a/tools/utils/src/logger.ts b/tools/utils/src/logger.ts
new file mode 100644
index 0000000000000..a429cbea9d18d
--- /dev/null
+++ b/tools/utils/src/logger.ts
@@ -0,0 +1,46 @@
+import chalk from 'chalk';
+import { identity } from 'lodash-es';
+
+export const newLineSeparator = /\r\n|[\n\r\x85\u2028\u2029]/g;
+
+interface StringLike {
+ toString: () => string;
+}
+
+export class Logger {
+ log = this.getLineLogger(console.log.bind(console));
+ info = this.getLineLogger(console.info.bind(console), chalk.blue);
+ warn = this.getLineLogger(
+ console.warn.bind(console),
+ chalk.bgHex('#322b08').hex('#fadea6')
+ );
+ error = this.getLineLogger(
+ console.error.bind(console),
+ chalk.bgHex('#250201').hex('#ef8784')
+ );
+ success = this.getLineLogger(console.log.bind(console), chalk.green);
+
+ constructor(private readonly tag: string = '') {}
+
+ getLineLogger(
+ logLine: (...line: string[]) => void,
+ color: (...text: string[]) => string = identity
+ ) {
+ return (...args: StringLike[]) => {
+ args.forEach(arg => {
+ arg
+ .toString()
+ .split(newLineSeparator)
+ .forEach(line => {
+ if (line.length !== 0) {
+ if (this.tag) {
+ logLine(color(`[${this.tag}] ${line}`));
+ } else {
+ logLine(color(line));
+ }
+ }
+ });
+ });
+ };
+ }
+}
diff --git a/tools/utils/src/package.ts b/tools/utils/src/package.ts
new file mode 100644
index 0000000000000..a2b764c8935e8
--- /dev/null
+++ b/tools/utils/src/package.ts
@@ -0,0 +1,71 @@
+import { existsSync, readFileSync } from 'node:fs';
+import { join, parse } from 'node:path';
+
+import { pathToRoot } from './path';
+import type { CommonPackageJsonContent, YarnWorkspaceItem } from './types';
+import type { Workspace } from './workspace';
+import type { PackageName } from './workspace.gen';
+
+
+export function readPackageJson(path: string): CommonPackageJsonContent {
+ const content = readFileSync(path + '/package.json', 'utf-8');
+
+ return JSON.parse(content);
+}
+
+export class Package {
+ name: PackageName;
+ packageJson: CommonPackageJsonContent;
+ dirname: string;
+ path: string;
+ relativePath: string;
+ srcPath: string;
+ nodeModulesPath: string;
+ tsbuildPath: string;
+ distPath: string;
+ version: string;
+ isTsProject: boolean;
+
+ _workspaceDependencies: string[];
+ deps: Package[] = [];
+
+ get entry() {
+ return this.packageJson.main || this.packageJson.exports?.['.'];
+ }
+
+ constructor(public readonly workspace: Workspace, meta: YarnWorkspaceItem) {
+ const { location: relativePath, name, workspaceDependencies } = meta;
+ // TODO: check [mismatchedWorkspaceDependencies]
+
+ this.name = name as PackageName;
+
+ // parse paths
+ this.relativePath = relativePath;
+ this.path = pathToRoot(relativePath);
+ this.dirname = parse(relativePath).name;
+ this.srcPath = this.absolute('src');
+ this.tsbuildPath = this.absolute('tsbuild');
+ this.distPath = this.absolute('dist');
+ this.nodeModulesPath = this.absolute('node_modules');
+
+ // parse workspace
+ const packageJson = readPackageJson(this.path);
+ this.packageJson = packageJson;
+ this.version = packageJson.version;
+ this._workspaceDependencies = workspaceDependencies;
+ this.isTsProject = existsSync(this.absolute('tsconfig.json'));
+ }
+
+ get scripts() {
+ return this.packageJson.scripts || {};
+ }
+
+ relative(...paths: string[]) {
+ return join(this.relativePath, ...paths);
+ }
+
+ absolute(...paths: string[]) {
+ return join(this.path, ...paths);
+ }
+
+}
diff --git a/tools/utils/src/path.ts b/tools/utils/src/path.ts
new file mode 100644
index 0000000000000..552c46aa7c83d
--- /dev/null
+++ b/tools/utils/src/path.ts
@@ -0,0 +1,17 @@
+import { join, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+export const root = resolve(fileURLToPath(import.meta.url), '../../../../');
+export function pathToRoot(...paths: string[]) {
+ return join(root, ...paths);
+}
+export function relativeToRoot(...paths: string[]) {
+ return join(root, ...paths).slice(root.length + 1);
+}
+
+export function pathRelativeToMe(url: string) {
+ const path = fileURLToPath(url);
+ return (...paths: string[]) => {
+ return join(path, '../', ...paths);
+ }
+}
\ No newline at end of file
diff --git a/tools/utils/src/process.ts b/tools/utils/src/process.ts
new file mode 100644
index 0000000000000..97e594877f33a
--- /dev/null
+++ b/tools/utils/src/process.ts
@@ -0,0 +1,99 @@
+import {
+ type ChildProcess,
+ execSync,
+ spawn as RawSpawn,
+ type SpawnOptions,
+} from 'node:child_process';
+
+import { Logger } from './logger';
+
+const children = new Set();
+
+export function spawn(
+ tag: string,
+ cmd: string | string[],
+ options: SpawnOptions = {}
+) {
+ cmd = typeof cmd === 'string' ? cmd.split(' ') : cmd;
+ const isYarnSpawn = cmd[0] === 'yarn';
+
+ const spawnOptions: SpawnOptions = {
+ stdio: isYarnSpawn
+ ? ['inherit', 'inherit', 'inherit']
+ : ['inherit', 'pipe', 'pipe'],
+ shell: true,
+ ...options,
+ env: { ...process.env, ...options.env },
+ };
+
+ const logger = new Logger(tag);
+ logger.info(cmd.join(' '));
+ const childProcess = RawSpawn(cmd[0], cmd.slice(1), spawnOptions);
+ children.add(childProcess);
+
+ const drain = (_code: number | null, signal: any) => {
+ children.delete(childProcess);
+
+ // don't run repeatedly if this is the error event
+ if (signal === undefined) {
+ childProcess.removeListener('exit', drain);
+ }
+ };
+
+ childProcess.stdout?.on('data', chunk => {
+ logger.log(chunk);
+ });
+
+ childProcess.stderr?.on('data', chunk => {
+ logger.error(chunk);
+ });
+
+ childProcess.once('error', e => {
+ logger.error(e.toString());
+ children.delete(childProcess);
+ });
+
+ childProcess.once('exit', (code, signal) => {
+ if (code !== 0) {
+ logger.error('Finished with non-zero exit code.');
+ }
+
+ drain(code, signal);
+ });
+
+ return childProcess;
+}
+
+export function execAsync(
+ tag: string,
+ cmd: string | string[],
+ options?: SpawnOptions
+): Promise {
+ return new Promise((resolve, reject) => {
+ const childProcess = spawn(tag, cmd, options);
+
+ childProcess.once('error', e => {
+ reject(e);
+ });
+
+ childProcess.once('exit', code => {
+ if (code === 0) {
+ resolve();
+ } else {
+ reject(new Error(`Child process exits with non-zero code ${code}`));
+ }
+ });
+ });
+}
+
+export function exec(
+ tag: string,
+ cmd: string,
+ { silent }: { silent: boolean } = { silent: false }
+): string {
+ const logger = new Logger(tag);
+ !silent && logger.info(cmd);
+ const result = execSync(cmd, { encoding: 'utf8' }).trim();
+ !silent && logger.log(result);
+ return result;
+}
diff --git a/tools/utils/src/types.ts b/tools/utils/src/types.ts
new file mode 100644
index 0000000000000..9a7fd1cc2e86d
--- /dev/null
+++ b/tools/utils/src/types.ts
@@ -0,0 +1,20 @@
+export interface YarnWorkspaceItem {
+ name: string;
+ location: string;
+ workspaceDependencies: string[];
+ // we don't need it
+ mismatchedWorkspaceDependencies?: string[];
+}
+
+export interface CommonPackageJsonContent {
+ name: string;
+ version: string;
+ private?: boolean;
+ dependencies?: { [key: string]: string };
+ devDependencies?: { [key: string]: string };
+ scripts?: { [key: string]: string };
+ main?: string;
+ exports?: {
+ [key: string]: string;
+ };
+}
diff --git a/tools/utils/src/workspace.gen.ts b/tools/utils/src/workspace.gen.ts
new file mode 100644
index 0000000000000..32db784a56b5c
--- /dev/null
+++ b/tools/utils/src/workspace.gen.ts
@@ -0,0 +1,271 @@
+// Auto generated content
+// DO NOT MODIFY THIS FILE MANUALLY
+
+export const PackageList = [
+ {
+ "location": "docs/reference",
+ "name": "@affine/docs",
+ "workspaceDependencies": []
+ },
+ {
+ "location": "packages/backend/native",
+ "name": "@affine/server-native",
+ "workspaceDependencies": []
+ },
+ {
+ "location": "packages/backend/server",
+ "name": "@affine/server",
+ "workspaceDependencies": [
+ "tests/kit",
+ "packages/backend/native"
+ ]
+ },
+ {
+ "location": "packages/common/debug",
+ "name": "@affine/debug",
+ "workspaceDependencies": []
+ },
+ {
+ "location": "packages/common/env",
+ "name": "@affine/env",
+ "workspaceDependencies": []
+ },
+ {
+ "location": "packages/common/infra",
+ "name": "@toeverything/infra",
+ "workspaceDependencies": [
+ "packages/common/debug",
+ "packages/common/env",
+ "packages/frontend/templates"
+ ]
+ },
+ {
+ "location": "packages/common/nbstore",
+ "name": "@affine/nbstore",
+ "workspaceDependencies": [
+ "packages/common/infra",
+ "packages/frontend/electron-api",
+ "packages/frontend/graphql"
+ ]
+ },
+ {
+ "location": "packages/frontend/admin",
+ "name": "@affine/admin",
+ "workspaceDependencies": [
+ "packages/frontend/component",
+ "packages/frontend/core",
+ "packages/frontend/graphql",
+ "packages/common/infra"
+ ]
+ },
+ {
+ "location": "packages/frontend/apps/android",
+ "name": "@affine/android",
+ "workspaceDependencies": [
+ "packages/frontend/component",
+ "packages/frontend/core",
+ "packages/frontend/i18n"
+ ]
+ },
+ {
+ "location": "packages/frontend/apps/electron",
+ "name": "@affine/electron",
+ "workspaceDependencies": [
+ "tests/kit",
+ "tools/utils",
+ "packages/frontend/component",
+ "packages/frontend/core",
+ "packages/frontend/electron-api",
+ "packages/frontend/i18n",
+ "packages/frontend/native",
+ "packages/common/nbstore",
+ "packages/common/infra"
+ ]
+ },
+ {
+ "location": "packages/frontend/apps/ios",
+ "name": "@affine/ios",
+ "workspaceDependencies": [
+ "packages/frontend/component",
+ "packages/frontend/core",
+ "packages/frontend/i18n"
+ ]
+ },
+ {
+ "location": "packages/frontend/apps/mobile",
+ "name": "@affine/mobile",
+ "workspaceDependencies": [
+ "packages/frontend/component",
+ "packages/frontend/core",
+ "packages/frontend/i18n"
+ ]
+ },
+ {
+ "location": "packages/frontend/apps/web",
+ "name": "@affine/web",
+ "workspaceDependencies": [
+ "packages/frontend/component",
+ "packages/frontend/core",
+ "packages/frontend/i18n"
+ ]
+ },
+ {
+ "location": "packages/frontend/component",
+ "name": "@affine/component",
+ "workspaceDependencies": [
+ "tools/scripts",
+ "packages/common/debug",
+ "packages/frontend/electron-api",
+ "packages/frontend/graphql",
+ "packages/frontend/i18n"
+ ]
+ },
+ {
+ "location": "packages/frontend/core",
+ "name": "@affine/core",
+ "workspaceDependencies": [
+ "packages/frontend/component",
+ "packages/common/debug",
+ "packages/frontend/electron-api",
+ "packages/common/env",
+ "packages/frontend/graphql",
+ "packages/frontend/i18n",
+ "packages/frontend/templates",
+ "packages/frontend/track"
+ ]
+ },
+ {
+ "location": "packages/frontend/electron-api",
+ "name": "@affine/electron-api",
+ "workspaceDependencies": []
+ },
+ {
+ "location": "packages/frontend/graphql",
+ "name": "@affine/graphql",
+ "workspaceDependencies": [
+ "packages/common/env"
+ ]
+ },
+ {
+ "location": "packages/frontend/i18n",
+ "name": "@affine/i18n",
+ "workspaceDependencies": [
+ "packages/common/debug"
+ ]
+ },
+ {
+ "location": "packages/frontend/native",
+ "name": "@affine/native",
+ "workspaceDependencies": []
+ },
+ {
+ "location": "packages/frontend/templates",
+ "name": "@affine/templates",
+ "workspaceDependencies": []
+ },
+ {
+ "location": "packages/frontend/track",
+ "name": "@affine/track",
+ "workspaceDependencies": [
+ "packages/common/debug"
+ ]
+ },
+ {
+ "location": "tests/affine-cloud",
+ "name": "@affine-test/affine-cloud",
+ "workspaceDependencies": [
+ "tests/kit"
+ ]
+ },
+ {
+ "location": "tests/affine-cloud-copilot",
+ "name": "@affine-test/affine-cloud-copilot",
+ "workspaceDependencies": [
+ "tests/kit"
+ ]
+ },
+ {
+ "location": "tests/affine-desktop",
+ "name": "@affine-test/affine-desktop",
+ "workspaceDependencies": [
+ "tests/kit",
+ "packages/frontend/electron-api"
+ ]
+ },
+ {
+ "location": "tests/affine-desktop-cloud",
+ "name": "@affine-test/affine-desktop-cloud",
+ "workspaceDependencies": [
+ "tests/kit"
+ ]
+ },
+ {
+ "location": "tests/affine-local",
+ "name": "@affine-test/affine-local",
+ "workspaceDependencies": [
+ "tests/kit"
+ ]
+ },
+ {
+ "location": "tests/affine-mobile",
+ "name": "@affine-test/affine-mobile",
+ "workspaceDependencies": [
+ "tests/kit"
+ ]
+ },
+ {
+ "location": "tests/kit",
+ "name": "@affine-test/kit",
+ "workspaceDependencies": []
+ },
+ {
+ "location": "tools/@types/env",
+ "name": "@types/affine__env",
+ "workspaceDependencies": [
+ "packages/common/env"
+ ]
+ },
+ {
+ "location": "tools/bump-blocksuite",
+ "name": "@affine/bump-blocksuite",
+ "workspaceDependencies": []
+ },
+ {
+ "location": "tools/changelog",
+ "name": "@affine/changelog",
+ "workspaceDependencies": []
+ },
+ {
+ "location": "tools/commitlint",
+ "name": "@affine/commitlint-config",
+ "workspaceDependencies": []
+ },
+ {
+ "location": "tools/copilot-result",
+ "name": "@affine/copilot-result",
+ "workspaceDependencies": []
+ },
+ {
+ "location": "tools/playstore-auto-bump",
+ "name": "@affine/playstore-auto-bump",
+ "workspaceDependencies": []
+ },
+ {
+ "location": "tools/scripts",
+ "name": "@affine-tools/scripts",
+ "workspaceDependencies": [
+ "tools/utils"
+ ]
+ },
+ {
+ "location": "tools/utils",
+ "name": "@affine-tools/utils",
+ "workspaceDependencies": []
+ },
+ {
+ "location": "tools/workers",
+ "name": "@affine/workers",
+ "workspaceDependencies": []
+ }
+]
+export type PackageName = '@affine/docs' | '@affine/server-native' | '@affine/server' | '@affine/debug' | '@affine/env' | '@toeverything/infra' | '@affine/nbstore' | '@affine/admin' | '@affine/android' | '@affine/electron' | '@affine/ios' | '@affine/mobile' | '@affine/web' | '@affine/component' | '@affine/core' | '@affine/electron-api' | '@affine/graphql' | '@affine/i18n' | '@affine/native' | '@affine/templates' | '@affine/track' | '@affine-test/affine-cloud' | '@affine-test/affine-cloud-copilot' | '@affine-test/affine-desktop' | '@affine-test/affine-desktop-cloud' | '@affine-test/affine-local' | '@affine-test/affine-mobile' | '@affine-test/kit' | '@types/affine__env' | '@affine/bump-blocksuite' | '@affine/changelog' | '@affine/commitlint-config' | '@affine/copilot-result' | '@affine/playstore-auto-bump' | '@affine-tools/scripts' | '@affine-tools/utils' | '@affine/workers'
\ No newline at end of file
diff --git a/tools/utils/src/workspace.ts b/tools/utils/src/workspace.ts
new file mode 100644
index 0000000000000..1eaacd735f9e7
--- /dev/null
+++ b/tools/utils/src/workspace.ts
@@ -0,0 +1,244 @@
+import { once } from 'lodash-es';
+
+import { Logger } from './logger';
+import { Package, readPackageJson } from './package';
+import { pathToRoot, relativeToRoot, root } from './path';
+import { exec, execAsync } from './process';
+import type { CommonPackageJsonContent, YarnWorkspaceItem } from './types';
+import { PackageList, type PackageName } from './workspace.gen';
+
+class CircularDependenciesError extends Error {
+ constructor(public currentName: string) {
+ super('Circular dependencies error');
+ }
+}
+
+class ForbiddenPackageRefError extends Error {
+ constructor(
+ public currentName: string,
+ public refName: string
+ ) {
+ super(
+ `Public package cannot reference private package. Found '${refName}' in dependencies of '${currentName}'`
+ );
+ }
+}
+
+interface RunScriptOptions {
+ includeDependencies?: boolean;
+ ignoreMissingScript?: boolean;
+}
+
+export class Workspace {
+ static PackageNames: PackageName[] = PackageList.map(
+ p => p.name
+ ) as PackageName[];
+ packages: Package[];
+
+ packageJson: CommonPackageJsonContent;
+
+ logger = new Logger('AFFiNE');
+
+ readonly path = root;
+
+ get version() {
+ return this.packageJson.version;
+ }
+
+ get devDependencies() {
+ return this.packageJson.devDependencies ?? {};
+ }
+
+ get dependencies() {
+ return this.packageJson.dependencies ?? {};
+ }
+
+ constructor() {
+ this.packageJson = readPackageJson(root);
+ const packages = new Map();
+
+ for (const meta of PackageList) {
+ try {
+ packages.set(meta.location, new Package(this, meta));
+ } catch (e) {
+ this.logger.error(e as Error);
+ }
+ }
+
+ const building = new Set();
+ try {
+ packages.forEach(pkg => this.buildDeps(pkg, packages, building));
+ } catch (e) {
+ if (e instanceof CircularDependenciesError) {
+ const inProcessPackages = Array.from(building);
+ const circle = inProcessPackages
+ .slice(inProcessPackages.indexOf(e.currentName))
+ .concat(e.currentName);
+ this.logger.error(
+ `Circular dependencies found: \n ${circle.join(' -> ')}`
+ );
+ process.exit(1);
+ }
+
+ throw e;
+ }
+
+ this.packages = Array.from(packages.values());
+ }
+
+ tryGetPackage(name: PackageName) {
+ return this.packages.find(p => p.name === name);
+ }
+
+ getPackage(name: PackageName) {
+ const pkg = this.tryGetPackage(name);
+
+ if (!pkg) {
+ throw new Error(`Cannot find package with name '${name}'`);
+ }
+
+ return pkg;
+ }
+
+ absolute(...paths: string[]) {
+ return pathToRoot(...paths);
+ }
+
+ relative(...paths: string[]) {
+ return relativeToRoot(...paths);
+ }
+
+ buildDeps(
+ pkg: Package,
+ packages: Map,
+ building: Set
+ ) {
+ if (pkg.deps.length) {
+ return;
+ }
+
+ building.add(pkg.name);
+ pkg.deps = pkg._workspaceDependencies
+ .map(relativeDepPath => {
+ const dep = packages.get(relativeDepPath);
+
+ if (!dep) {
+ this.logger.error(
+ `Cannot find package at ${relativeDepPath}. While build dependencies of ${pkg.name}`
+ );
+ return null;
+ }
+
+ if (building.has(dep.name)) {
+ throw new CircularDependenciesError(pkg.name);
+ }
+
+ if (!pkg.packageJson.private && dep.packageJson.private) {
+ throw new ForbiddenPackageRefError(pkg.name, dep.name);
+ }
+
+ this.buildDeps(dep, packages, building);
+ return dep;
+ })
+ .filter(Boolean) as Package[];
+
+ building.delete(pkg.name);
+ }
+
+ yarnList = once(() => {
+ const output = exec('', 'yarn workspaces list -v --json', { silent: true });
+
+ let packageList = JSON.parse(
+ `[${output.trim().replace(/\r\n|\n/g, ',')}]`
+ ) as YarnWorkspaceItem[];
+
+ packageList.forEach(p => {
+ p.location = p.location.replaceAll(/\\/g, '/');
+ delete p['mismatchedWorkspaceDependencies'];
+ });
+
+ // ignore root package
+ return packageList.filter(p => p.location !== '.');
+ });
+
+ genWorkspaceInfo() {
+ const list = this.yarnList();
+
+ const names = list.map(p => p.name);
+
+ const content = [
+ '// Auto generated content',
+ '// DO NOT MODIFY THIS FILE MANUALLY',
+ '',
+ `export const PackageList = ${JSON.stringify(list, null, 2)}`,
+ `export type PackageName = ${names.map(n => `'${n}'`).join(' | ')}`,
+ ];
+
+ return content.join('\n');
+ }
+
+ genProjectTsConfig() {
+ const content = [
+ '// Auto generated content',
+ '// DO NOT MODIFY THIS FILE MANUALLY',
+ '{',
+ ' "compilerOptions": {',
+ ' "noEmit": true',
+ ' },',
+ ' "include": [],',
+ ' "references": [',
+ ...this.packages
+ .filter(p => p.isTsProject)
+ .map(p => ` { "path": "./${p.relativePath}" },`),
+ ' ]',
+ '}',
+ ];
+
+ return content.join('\n');
+ }
+
+ forEach(callback: (pkg: Package) => void) {
+ this.packages.forEach(callback);
+ }
+
+ async run(name: PackageName, script: string, opts: RunScriptOptions = {}) {
+ opts = { includeDependencies: false, ignoreMissingScript: false, ...opts };
+
+ const pkg = this.getPackage(name);
+ const scriptToRun = pkg.scripts[script];
+
+ if (opts.includeDependencies) {
+ await Promise.all(
+ pkg.deps.map(dep =>
+ this.run(dep.name, script, {
+ ...opts,
+ // the dependency may not have the script
+ ignoreMissingScript: true,
+ })
+ )
+ );
+ }
+
+ if (!scriptToRun) {
+ if (!opts.ignoreMissingScript) {
+ throw new Error(
+ `Script '${script}' is not found in ${pkg.relative('package.json')} of package '${name}'. `
+ );
+ }
+
+ return;
+ } else {
+ if (scriptToRun.startsWith('affine ')) {
+ await execAsync(name, `yarn ${scriptToRun} -p ${pkg.name}`, {
+ cwd: root,
+ });
+ } else {
+ await execAsync(name, `yarn run ${script}`, {
+ cwd: pkg.path,
+ });
+ }
+ }
+ }
+}
+
+export { Package, type PackageName };
diff --git a/tools/utils/tsconfig.json b/tools/utils/tsconfig.json
new file mode 100644
index 0000000000000..dc8dfde479722
--- /dev/null
+++ b/tools/utils/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "types": ["node"],
+ "moduleResolution": "bundler",
+ "composite": true,
+ "rootDir": "./src",
+ "outDir": "./dist"
+ },
+ "include": ["./src"],
+ "references": [
+ {
+ "path": "../../packages/common/env"
+ }
+ ]
+}
diff --git a/tsconfig.json b/tsconfig.json
index 5b53d870620e6..b60e30da35f55 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -52,7 +52,6 @@
"@affine/admin/components/ui/*": [
"./packages/frontend/admin/src/components/ui/*"
],
- "@affine/cli/*": ["./tools/cli/src/*"],
"@affine/server/*": ["./packages/backend/server/src/*"],
"@affine/component": ["./packages/frontend/component/src/index"],
"@affine/component/*": [
@@ -76,99 +75,6 @@
}
},
"include": [],
- "references": [
- // Backend
- {
- "path": "./packages/backend/server"
- },
- {
- "path": "./packages/backend/server/tests"
- },
- // Frontend
- {
- "path": "./packages/frontend/admin"
- },
- {
- "path": "./packages/frontend/component"
- },
- {
- "path": "./packages/frontend/core"
- },
- {
- "path": "./packages/frontend/track"
- },
- {
- "path": "./packages/frontend/apps/web"
- },
- {
- "path": "./packages/frontend/apps/mobile"
- },
- {
- "path": "./packages/frontend/apps/ios"
- },
- {
- "path": "./packages/frontend/apps/android"
- },
- {
- "path": "./packages/frontend/apps/electron/tsconfig.test.json"
- },
- {
- "path": "./packages/frontend/apps/electron/renderer/tsconfig.json"
- },
- {
- "path": "./packages/frontend/graphql"
- },
- {
- "path": "./packages/frontend/i18n"
- },
- // Common
- {
- "path": "./packages/common/debug"
- },
- {
- "path": "./packages/common/env"
- },
- {
- "path": "./packages/common/infra"
- },
- {
- "path": "./packages/common/nbstore"
- },
- // Tools
- {
- "path": "./tools/cli"
- },
- {
- "path": "./tools/playstore-auto-bump"
- },
- // Tests
- {
- "path": "./tests/kit"
- },
- {
- "path": "./tests/affine-local"
- },
- {
- "path": "./tests/affine-mobile"
- },
- {
- "path": "./tests/affine-cloud"
- },
- {
- "path": "./tests/affine-desktop"
- },
- // Others
- {
- "path": "./tsconfig.node.json"
- }
- ],
"files": [],
"exclude": ["node_modules", "target", "lib", "test-results", "dist"],
- "ts-node": {
- "esm": true,
- "compilerOptions": {
- "module": "ESNext",
- "moduleResolution": "Node"
- }
- }
}
diff --git a/tsconfig.node.json b/tsconfig.node.json
index c1b97280ac740..3a26d44943c9e 100644
--- a/tsconfig.node.json
+++ b/tsconfig.node.json
@@ -12,8 +12,5 @@
{
"path": "./packages/common/env"
},
- {
- "path": "./tools/cli"
- }
]
}
diff --git a/tsconfig.project.json b/tsconfig.project.json
new file mode 100644
index 0000000000000..de4bb208bff0e
--- /dev/null
+++ b/tsconfig.project.json
@@ -0,0 +1,39 @@
+// Auto generated content
+// DO NOT MODIFY THIS FILE MANUALLY
+{
+ "compilerOptions": {
+ "noEmit": true
+ },
+ "include": [],
+ "references": [
+ { "path": "./packages/backend/native" },
+ { "path": "./packages/backend/server" },
+ { "path": "./packages/common/debug" },
+ { "path": "./packages/common/env" },
+ { "path": "./packages/common/infra" },
+ { "path": "./packages/common/nbstore" },
+ { "path": "./packages/frontend/admin" },
+ { "path": "./packages/frontend/apps/android" },
+ { "path": "./packages/frontend/apps/electron" },
+ { "path": "./packages/frontend/apps/ios" },
+ { "path": "./packages/frontend/apps/mobile" },
+ { "path": "./packages/frontend/apps/web" },
+ { "path": "./packages/frontend/component" },
+ { "path": "./packages/frontend/core" },
+ { "path": "./packages/frontend/electron-api" },
+ { "path": "./packages/frontend/graphql" },
+ { "path": "./packages/frontend/i18n" },
+ { "path": "./packages/frontend/native" },
+ { "path": "./packages/frontend/track" },
+ { "path": "./tests/affine-cloud" },
+ { "path": "./tests/affine-cloud-copilot" },
+ { "path": "./tests/affine-desktop" },
+ { "path": "./tests/affine-desktop-cloud" },
+ { "path": "./tests/affine-local" },
+ { "path": "./tests/affine-mobile" },
+ { "path": "./tests/kit" },
+ { "path": "./tools/playstore-auto-bump" },
+ { "path": "./tools/scripts" },
+ { "path": "./tools/utils" },
+ ]
+}
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index d92a93fea9d19..4d6555e073360 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -92,6 +92,61 @@ __metadata:
languageName: unknown
linkType: soft
+"@affine-tools/scripts@workspace:*, @affine-tools/scripts@workspace:tools/scripts":
+ version: 0.0.0-use.local
+ resolution: "@affine-tools/scripts@workspace:tools/scripts"
+ dependencies:
+ "@affine-tools/utils": "workspace:*"
+ "@aws-sdk/client-s3": "npm:^3.709.0"
+ "@napi-rs/simple-git": "npm:^0.1.19"
+ "@perfsee/webpack": "npm:^1.13.0"
+ "@pmmmwh/react-refresh-webpack-plugin": "npm:^0.5.15"
+ "@sentry/webpack-plugin": "npm:^2.22.7"
+ "@types/lodash-es": "npm:^4.17.12"
+ "@types/mime-types": "npm:^2.1.4"
+ "@types/node": "npm:^20.17.10"
+ "@types/webpack-env": "npm:^1.18.5"
+ "@vanilla-extract/webpack-plugin": "npm:^2.3.15"
+ autoprefixer: "npm:^10.4.20"
+ clipanion: "npm:^3.2.1"
+ copy-webpack-plugin: "npm:^12.0.2"
+ css-loader: "npm:^7.1.2"
+ cssnano: "npm:^7.0.6"
+ html-webpack-plugin: "npm:^5.6.3"
+ lodash-es: "npm:^4.17.21"
+ mime-types: "npm:^2.1.35"
+ mini-css-extract-plugin: "npm:^2.9.2"
+ postcss: "npm:^8.4.49"
+ postcss-loader: "npm:^8.1.1"
+ react-refresh: "npm:^0.16.0"
+ source-map-loader: "npm:^5.0.0"
+ style-loader: "npm:^4.0.0"
+ swc-loader: "npm:^0.2.6"
+ tailwindcss: "npm:^3.4.16"
+ terser-webpack-plugin: "npm:^5.3.10"
+ ts-node: "npm:^10.9.2"
+ typanion: "npm:^3.14.0"
+ typescript: "npm:^5.5.4"
+ webpack: "npm:^5.97.1"
+ webpack-dev-server: "npm:^5.2.0"
+ webpack-merge: "npm:^6.0.1"
+ bin:
+ r: ./bin/runner.js
+ languageName: unknown
+ linkType: soft
+
+"@affine-tools/utils@workspace:*, @affine-tools/utils@workspace:tools/utils":
+ version: 0.0.0-use.local
+ resolution: "@affine-tools/utils@workspace:tools/utils"
+ dependencies:
+ "@types/lodash-es": "npm:^4.17.12"
+ "@types/node": "npm:^20.17.10"
+ chalk: "npm:^5.3.0"
+ lodash-es: "npm:^4.17.21"
+ typescript: "npm:^5.5.4"
+ languageName: unknown
+ linkType: soft
+
"@affine/admin@workspace:packages/frontend/admin":
version: 0.0.0-use.local
resolution: "@affine/admin@workspace:packages/frontend/admin"
@@ -201,49 +256,6 @@ __metadata:
languageName: unknown
linkType: soft
-"@affine/cli@workspace:*, @affine/cli@workspace:tools/cli":
- version: 0.0.0-use.local
- resolution: "@affine/cli@workspace:tools/cli"
- dependencies:
- "@affine/env": "workspace:*"
- "@affine/templates": "workspace:*"
- "@aws-sdk/client-s3": "npm:^3.709.0"
- "@blocksuite/affine": "npm:0.19.2"
- "@clack/core": "npm:^0.3.5"
- "@clack/prompts": "npm:^0.8.2"
- "@magic-works/i18n-codegen": "npm:^0.6.1"
- "@napi-rs/simple-git": "npm:^0.1.19"
- "@perfsee/webpack": "npm:^1.13.0"
- "@pmmmwh/react-refresh-webpack-plugin": "npm:^0.5.15"
- "@sentry/webpack-plugin": "npm:^2.22.7"
- "@types/mime-types": "npm:^2.1.4"
- "@types/webpack-env": "npm:^1.18.5"
- "@vanilla-extract/webpack-plugin": "npm:^2.3.15"
- autoprefixer: "npm:^10.4.20"
- copy-webpack-plugin: "npm:^12.0.2"
- css-loader: "npm:^7.1.2"
- cssnano: "npm:^7.0.6"
- dotenv: "npm:^16.4.7"
- html-webpack-plugin: "npm:^5.6.3"
- lodash-es: "npm:^4.17.21"
- mime-types: "npm:^2.1.35"
- mini-css-extract-plugin: "npm:^2.9.2"
- postcss: "npm:^8.4.49"
- postcss-loader: "npm:^8.1.1"
- react-refresh: "npm:^0.16.0"
- source-map-loader: "npm:^5.0.0"
- style-loader: "npm:^4.0.0"
- swc-loader: "npm:^0.2.6"
- tailwindcss: "npm:^3.4.16"
- terser-webpack-plugin: "npm:^5.3.10"
- ts-node: "npm:^10.9.2"
- vite: "npm:^6.0.3"
- webpack: "npm:^5.97.1"
- webpack-dev-server: "npm:^5.2.0"
- webpack-merge: "npm:^6.0.1"
- languageName: unknown
- linkType: soft
-
"@affine/commitlint-config@workspace:tools/commitlint":
version: 0.0.0-use.local
resolution: "@affine/commitlint-config@workspace:tools/commitlint"
@@ -258,7 +270,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@affine/component@workspace:packages/frontend/component"
dependencies:
- "@affine/cli": "workspace:*"
+ "@affine-tools/scripts": "workspace:*"
"@affine/debug": "workspace:*"
"@affine/electron-api": "workspace:*"
"@affine/graphql": "workspace:*"
@@ -459,6 +471,7 @@ __metadata:
resolution: "@affine/electron@workspace:packages/frontend/apps/electron"
dependencies:
"@affine-test/kit": "workspace:*"
+ "@affine-tools/utils": "workspace:*"
"@affine/component": "workspace:*"
"@affine/core": "workspace:*"
"@affine/electron-api": "workspace:*"
@@ -619,7 +632,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@affine/monorepo@workspace:."
dependencies:
- "@affine/cli": "workspace:*"
+ "@affine-tools/scripts": "workspace:*"
"@capacitor/cli": "npm:^6.2.0"
"@eslint/js": "npm:^9.16.0"
"@faker-js/faker": "npm:^9.3.0"
@@ -3740,27 +3753,6 @@ __metadata:
languageName: node
linkType: hard
-"@clack/core@npm:0.3.5, @clack/core@npm:^0.3.5":
- version: 0.3.5
- resolution: "@clack/core@npm:0.3.5"
- dependencies:
- picocolors: "npm:^1.0.0"
- sisteransi: "npm:^1.0.5"
- checksum: 10/329840301b91df2957d6d3a5832946d6a3c8683aeccf98b77f559c518a9e7b75f5e59392228a51fc97ae950cf21438f1b77fb5529affd93df0106f52d9cc0881
- languageName: node
- linkType: hard
-
-"@clack/prompts@npm:^0.8.2":
- version: 0.8.2
- resolution: "@clack/prompts@npm:0.8.2"
- dependencies:
- "@clack/core": "npm:0.3.5"
- picocolors: "npm:^1.0.0"
- sisteransi: "npm:^1.0.5"
- checksum: 10/06859acc2cc8919255592150f898d08c93e6d6041d22b92fafa55f48265a681ab3506bde76fad5a03be3ea6f46e8408e1f1b1d88d259a0169e30b6f8b28acbfe
- languageName: node
- linkType: hard
-
"@cloudflare/kv-asset-handler@npm:0.3.4":
version: 0.3.4
resolution: "@cloudflare/kv-asset-handler@npm:0.3.4"
@@ -32903,7 +32895,7 @@ __metadata:
languageName: node
linkType: hard
-"typescript@npm:^5, typescript@npm:^5.3.3, typescript@npm:^5.4.3, typescript@npm:^5.7.2":
+"typescript@npm:^5, typescript@npm:^5.3.3, typescript@npm:^5.4.3, typescript@npm:^5.5.4, typescript@npm:^5.7.2":
version: 5.7.2
resolution: "typescript@npm:5.7.2"
bin:
@@ -32923,7 +32915,7 @@ __metadata:
languageName: node
linkType: hard
-"typescript@patch:typescript@npm%3A^5#optional!builtin, typescript@patch:typescript@npm%3A^5.3.3#optional!builtin, typescript@patch:typescript@npm%3A^5.4.3#optional!builtin, typescript@patch:typescript@npm%3A^5.7.2#optional!builtin":
+"typescript@patch:typescript@npm%3A^5#optional!builtin, typescript@patch:typescript@npm%3A^5.3.3#optional!builtin, typescript@patch:typescript@npm%3A^5.4.3#optional!builtin, typescript@patch:typescript@npm%3A^5.5.4#optional!builtin, typescript@patch:typescript@npm%3A^5.7.2#optional!builtin":
version: 5.7.2
resolution: "typescript@patch:typescript@npm%3A5.7.2#optional!builtin::version=5.7.2&hash=5786d5"
bin: