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/.github/workflows/build-test.yml b/.github/workflows/build-test.yml
index d1599299fa3cc..8a29d11c6403b 100644
--- a/.github/workflows/build-test.yml
+++ b/.github/workflows/build-test.yml
@@ -283,8 +283,7 @@ jobs:
electron-install: false
full-cache: true
- name: Build Electron renderer
- # always skip cache because its fast, and cache configuration is always changing
- run: yarn build
+ run: yarn affine @affine/electron bundle
env:
DISTRIBUTION: desktop
- name: zip web
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/.prettierignore b/.prettierignore
index 6aa0948e1fdde..59b17bb746f0c 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -25,4 +25,5 @@ packages/frontend/templates/onboarding
packages/backend/native/index.d.ts
packages/frontend/native/index.d.ts
packages/frontend/native/index.js
-compose.yaml
\ No newline at end of file
+compose.yaml
+*.gen.ts
\ No newline at end of file
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 04b636602900c..764923081b672 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -31,6 +31,7 @@ export default tseslint.config(
'.yarn/**',
'**/*.d.ts',
'.github/**/*',
+ '**/*.gen.ts',
'packages/frontend/component/.storybook/**/*',
'packages/frontend/i18n/src/i18n-generated.ts',
'packages/frontend/i18n/src/i18n-completenesses.json',
@@ -216,7 +217,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/admin/tailwind.config.js b/packages/frontend/admin/tailwind.config.js
index ee3b0653cb636..f433890cff0f8 100644
--- a/packages/frontend/admin/tailwind.config.js
+++ b/packages/frontend/admin/tailwind.config.js
@@ -1,7 +1,8 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ['class'],
- content: ['./src/**/*.{ts,tsx}'],
+ // TODO(@forehalo): we are not running webpack in admin dir
+ content: ['./packages/frontend/admin/src/**/*.{ts,tsx}'],
prefix: '',
theme: {
container: {
diff --git a/packages/frontend/apps/android/package.json b/packages/frontend/apps/android/package.json
index d8e8832fd6635..97220a334ab7b 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 a20be5b11a5d8..acef6bbf29a0c 100644
--- a/packages/frontend/apps/electron/package.json
+++ b/packages/frontend/apps/electron/package.json
@@ -2,28 +2,23 @@
"name": "@affine/electron",
"private": true,
"version": "0.18.0",
- "author": "toeverything",
- "repository": {
- "url": "https://github.com/toeverything/AFFiNE",
- "type": "git"
- },
- "description": "AFFiNE Desktop App",
- "homepage": "https://github.com/toeverything/AFFiNE",
+ "main": "./dist/main.js",
"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",
+ "bundle": "affine bundle",
+ "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/electron/webpack.config.ts b/packages/frontend/apps/electron/webpack.config.ts
new file mode 100644
index 0000000000000..6c2c6e2fa1b06
--- /dev/null
+++ b/packages/frontend/apps/electron/webpack.config.ts
@@ -0,0 +1,7 @@
+// TODO(@forehalo): all packages would become 'module'
+module.exports.config = {
+ entry: {
+ app: './renderer/index.tsx',
+ shell: './renderer/shell/index.tsx',
+ },
+};
diff --git a/packages/frontend/apps/ios/package.json b/packages/frontend/apps/ios/package.json
index c3aa1b6c1f38a..78a99fec7c5e5 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 ae1d786897b6a..f2d859da27ff3 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 bb20658c7bcee..9577f34adee44 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/public/robots.txt b/packages/frontend/core/public/robots.txt
index 7b537c62f904c..66864824ccdb4 100644
--- a/packages/frontend/core/public/robots.txt
+++ b/packages/frontend/core/public/robots.txt
@@ -2,6 +2,7 @@
User-agent: *
Allow: /
+Allow: /api/workspaces/*/blobs/*
Disallow: /oauth/*
Disallow: /workspace/*
Disallow: /public-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..cc268a8ccb7e8 100644
--- a/scripts/setup/global.ts
+++ b/scripts/setup/global.ts
@@ -1,12 +1,14 @@
-import { getBuildConfig } from '@affine/cli/src/webpack/runtime-config';
import { setupGlobal } from '@affine/env/global';
+import { getBuildConfig } from '@affine-tools/utils/build-config';
+import { Workspace } from '@affine-tools/utils/workspace';
-globalThis.BUILD_CONFIG = getBuildConfig({
- distribution: 'web',
- mode: 'development',
- channel: 'canary',
- static: false,
-});
+globalThis.BUILD_CONFIG = getBuildConfig(
+ new Workspace().getPackage('@affine/web'),
+ {
+ mode: 'development',
+ channel: 'canary',
+ }
+);
if (typeof window !== 'undefined') {
window.location.search = '?prefixUrl=http://127.0.0.1:3010/';
diff --git a/tests/affine-cloud-copilot/playwright.config.ts b/tests/affine-cloud-copilot/playwright.config.ts
index 4b6736354bba5..faff4e14eb90f 100644
--- a/tests/affine-cloud-copilot/playwright.config.ts
+++ b/tests/affine-cloud-copilot/playwright.config.ts
@@ -26,9 +26,8 @@ const config: PlaywrightTestConfig = {
retries: 3,
reporter: process.env.CI ? 'github' : 'list',
webServer: [
- // Intentionally not building the web, reminds you to run it by yourself.
{
- command: 'yarn -T run start:web-static',
+ command: 'yarn run -T affine dev -p @affine/web',
port: 8080,
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
@@ -37,7 +36,7 @@ const config: PlaywrightTestConfig = {
},
},
{
- command: 'yarn workspace @affine/server start',
+ command: 'yarn run -T affine dev -p @affine/server',
port: 3010,
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
diff --git a/tests/affine-cloud/package.json b/tests/affine-cloud/package.json
index 67995a5496ada..c7d0a614373fe 100644
--- a/tests/affine-cloud/package.json
+++ b/tests/affine-cloud/package.json
@@ -6,6 +6,7 @@
},
"devDependencies": {
"@affine-test/kit": "workspace:*",
+ "@affine-tools/scripts": "workspace:*",
"@playwright/test": "=1.49.1"
},
"version": "0.18.0"
diff --git a/tests/affine-cloud/playwright.config.ts b/tests/affine-cloud/playwright.config.ts
index 68e8d852e5e3f..59d59729b62c6 100644
--- a/tests/affine-cloud/playwright.config.ts
+++ b/tests/affine-cloud/playwright.config.ts
@@ -26,9 +26,12 @@ const config: PlaywrightTestConfig = {
retries: process.env.COPILOT ? 1 : 3,
reporter: process.env.CI ? 'github' : 'list',
webServer: [
- // Intentionally not building the web, reminds you to run it by yourself.
{
- command: 'yarn -T run start:web-static',
+ // TODO(@forehalo):
+ // in ci, all the target will be built,
+ // we could download the builds from archives
+ // and then run the web with simple http serve, it's will be faster
+ command: 'yarn run -T affine dev -p @affine/web',
port: 8080,
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
@@ -37,12 +40,10 @@ const config: PlaywrightTestConfig = {
},
},
{
- command: 'yarn workspace @affine/server start',
+ command: 'yarn run -T affine dev -p @affine/server',
port: 3010,
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
- stdout: 'pipe',
- stderr: 'pipe',
env: {
DATABASE_URL:
process.env.DATABASE_URL ??
diff --git a/tests/affine-desktop-cloud/playwright.config.ts b/tests/affine-desktop-cloud/playwright.config.ts
index 3432035187049..7765e1bc628b6 100644
--- a/tests/affine-desktop-cloud/playwright.config.ts
+++ b/tests/affine-desktop-cloud/playwright.config.ts
@@ -23,7 +23,7 @@ const config: PlaywrightTestConfig = {
webServer: [
// Intentionally not building the web, reminds you to run it by yourself.
{
- command: 'yarn -T run start:web-static',
+ command: 'yarn run -T affine dev -p @affine/web',
port: 8080,
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
@@ -34,7 +34,7 @@ const config: PlaywrightTestConfig = {
},
},
{
- command: 'yarn workspace @affine/server start',
+ command: 'yarn run -T affine dev -p @affine/server',
port: 3010,
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
diff --git a/tests/affine-desktop/playwright.config.ts b/tests/affine-desktop/playwright.config.ts
index f6cd39740d55d..ffa4003023c48 100644
--- a/tests/affine-desktop/playwright.config.ts
+++ b/tests/affine-desktop/playwright.config.ts
@@ -41,7 +41,7 @@ if (process.env.DEV_SERVER_URL) {
);
config.webServer = [
{
- command: 'yarn run start:web-static',
+ command: 'yarn run -T affine dev -p @affine/web',
port: 8080,
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
diff --git a/tests/affine-local/playwright.config.ts b/tests/affine-local/playwright.config.ts
index 8337cb53b111a..7f68d6a403089 100644
--- a/tests/affine-local/playwright.config.ts
+++ b/tests/affine-local/playwright.config.ts
@@ -46,7 +46,7 @@ const config: PlaywrightTestConfig = {
webServer: [
// Intentionally not building the web, reminds you to run it by yourself.
{
- command: 'yarn run start:web-static',
+ command: 'yarn run -T affine dev -p @affine/web',
port: 8080,
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
diff --git a/tests/affine-mobile/playwright.config.ts b/tests/affine-mobile/playwright.config.ts
index 6804256482284..76b381bc588f9 100644
--- a/tests/affine-mobile/playwright.config.ts
+++ b/tests/affine-mobile/playwright.config.ts
@@ -49,7 +49,7 @@ const config: PlaywrightTestConfig = {
webServer: [
// Intentionally not building the web, reminds you to run it by yourself.
{
- command: 'yarn workspace @affine/mobile static-server',
+ command: 'yarn run -T affine dev -p @affine/mobile',
port: 8080,
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
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 ee11989048b11..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.3",
- "@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..94337b55d3ad3
--- /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() {
+ const args = ['affine build', this.package];
+
+ if (this.deps) {
+ args.push('--deps');
+ }
+
+ await this.cli.run(args);
+ }
+}
diff --git a/tools/scripts/src/bundle.ts b/tools/scripts/src/bundle.ts
new file mode 100644
index 0000000000000..933000c03f957
--- /dev/null
+++ b/tools/scripts/src/bundle.ts
@@ -0,0 +1,93 @@
+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 = [['bundle'], ['webpack'], ['pack'], ['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 overrideConfigPath = this.workspace
+ .getPackage(this.package)
+ .join('webpack.config.ts');
+
+ if (overrideConfigPath.isFile()) {
+ const override = await import(overrideConfigPath.value);
+ configOverride = override.config ?? override.default;
+ }
+
+ 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..b963907032cfb
--- /dev/null
+++ b/tools/scripts/src/clean.ts
@@ -0,0 +1,84 @@
+import { 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.join('node_modules');
+ if (rootNodeModules.isDirectory()) {
+ this.logger.info(`Cleaning ${rootNodeModules}`);
+ rmSync(rootNodeModules.value, { recursive: true });
+ }
+
+ this.workspace.forEach(pkg => {
+ const nodeModules = pkg.nodeModulesPath;
+ if (nodeModules.isDirectory()) {
+ this.logger.info(`Cleaning ${nodeModules}`);
+ rmSync(nodeModules.value, { recursive: true });
+ }
+ });
+
+ this.logger.info('node_modules cleaned');
+ }
+
+ doCleanTs() {
+ this.logger.info('Cleaning ts build outputs...');
+
+ this.workspace.forEach(pkg => {
+ if (pkg.tsbuildPath.isDirectory()) {
+ this.logger.info(`Cleaning ${pkg.tsbuildPath}`);
+ rmSync(pkg.tsbuildPath.value, { recursive: true });
+ }
+ });
+
+ this.logger.info('ts build outputs cleaned');
+ }
+
+ doCleanDist() {
+ this.logger.info('Cleaning dist...');
+
+ this.workspace.forEach(pkg => {
+ if (pkg.distPath.isDirectory()) {
+ this.logger.info(`Cleaning ${pkg.distPath}`);
+ rmSync(pkg.distPath.value, { 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..35a7946d3fe80
--- /dev/null
+++ b/tools/scripts/src/codegen.ts
@@ -0,0 +1,35 @@
+import { writeFileSync } from 'node:fs';
+
+import type { Path } from '@affine-tools/utils/path';
+
+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: [Path, () => string][] = [
+ [
+ this.workspace.join('tsconfig.project.json'),
+ this.workspace.genProjectTsConfig,
+ ],
+ [
+ this.workspace
+ .getPackage('@affine-tools/utils')
+ .join('src/workspace.gen.ts'),
+ this.workspace.genWorkspaceInfo,
+ ],
+ ];
+
+ for (const [path, content] of filesToGenerate) {
+ this.logger.info(`Output: ${path}`);
+ writeFileSync(path.value, 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..efa3c3028c937
--- /dev/null
+++ b/tools/scripts/src/dev.ts
@@ -0,0 +1,15 @@
+import { PackageCommand } from './command';
+
+export class DevCommand extends PackageCommand {
+ static override paths = [['dev'], ['d']];
+
+ async execute() {
+ const args = [this.package, 'dev'];
+
+ if (this.deps) {
+ args.push('--deps');
+ }
+
+ await this.cli.run(args);
+ }
+}
diff --git a/tools/scripts/src/run.ts b/tools/scripts/src/run.ts
new file mode 100644
index 0000000000000..dc747d8794c7e
--- /dev/null
+++ b/tools/scripts/src/run.ts
@@ -0,0 +1,111 @@
+import { Path } 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 currentDir = Path.dir(import.meta.url);
+
+export class RunCommand extends PackageCommand {
+ static override paths = [[], ['run'], ['r']];
+
+ static override usage = PackageCommand.Usage({
+ description: 'AFFiNE Monorepo scripts',
+ details: `
+ \`affine web