From ad3f4929a9e88236da6f173267db96eac9c4f99c Mon Sep 17 00:00:00 2001 From: forehalo Date: Tue, 17 Dec 2024 12:57:21 +0800 Subject: [PATCH] chore: add monorepo tools --- .env.template | 7 - .gitignore | 6 + eslint.config.mjs | 2 +- oxlint.json | 5 +- package.json | 17 +- packages/frontend/admin/package.json | 3 +- packages/frontend/apps/android/package.json | 5 +- packages/frontend/apps/electron/package.json | 19 +- .../frontend/apps/electron/scripts/common.ts | 10 +- packages/frontend/apps/electron/tsconfig.json | 3 + .../frontend/apps/electron/tsconfig.node.json | 9 +- packages/frontend/apps/ios/package.json | 7 +- packages/frontend/apps/mobile/package.json | 5 +- packages/frontend/apps/web/package.json | 5 +- .../frontend/component/.storybook/main.ts | 2 +- packages/frontend/component/package.json | 2 +- packages/frontend/core/tsconfig.node.json | 3 - packages/frontend/graphql/package.json | 2 +- packages/frontend/templates/package.json | 3 +- scripts/setup/global.ts | 2 +- tools/cli/src/bin/build.ts | 66 ----- tools/cli/src/bin/dev.ts | 146 ---------- tools/cli/src/config/cwd.cjs | 38 --- tools/cli/src/config/index.ts | 9 - tools/cli/src/util/infra.ts | 26 -- tools/cli/src/webpack/postcss.config.cjs | 47 --- tools/cli/src/webpack/webpack.config.ts | 183 ------------ tools/cli/tsconfig.json | 12 - tools/commitlint/.commitlintrc.json | 6 +- tools/scripts/README.md | 74 +++++ tools/scripts/bin/runner.js | 68 +++++ tools/scripts/loader.js | 4 + tools/{cli => scripts}/package.json | 33 ++- tools/scripts/src/affine.ts | 32 +++ tools/scripts/src/build.ts | 15 + tools/scripts/src/bundle.ts | 94 ++++++ tools/scripts/src/clean.ts | 84 ++++++ tools/scripts/src/codegen.ts | 35 +++ tools/scripts/src/command.ts | 71 +++++ tools/scripts/src/context.ts | 6 + tools/scripts/src/dev.ts | 13 + tools/scripts/src/run.ts | 104 +++++++ .../src/webpack/cache-group.ts | 0 .../src/webpack/error-handler.js | 0 tools/scripts/src/webpack/html-plugin.ts | 180 ++++++++++++ .../src/webpack/index.ts} | 142 ++++----- .../{cli => scripts}/src/webpack/s3-plugin.ts | 4 +- .../src/webpack/template.html | 0 tools/scripts/src/webpack/types.ts | 6 + tools/scripts/tsconfig.json | 14 + tools/utils/package.json | 21 ++ .../src/build-config.ts} | 46 +-- tools/utils/src/distribution.ts | 30 ++ tools/utils/src/logger.ts | 46 +++ tools/utils/src/package.ts | 71 +++++ tools/utils/src/path.ts | 17 ++ tools/utils/src/process.ts | 99 +++++++ tools/utils/src/types.ts | 20 ++ tools/utils/src/workspace.gen.ts | 271 ++++++++++++++++++ tools/utils/src/workspace.ts | 244 ++++++++++++++++ tools/utils/tsconfig.json | 16 ++ tsconfig.json | 94 ------ tsconfig.node.json | 3 - tsconfig.project.json | 39 +++ yarn.lock | 128 ++++----- 65 files changed, 1895 insertions(+), 879 deletions(-) delete mode 100644 .env.template delete mode 100644 tools/cli/src/bin/build.ts delete mode 100644 tools/cli/src/bin/dev.ts delete mode 100644 tools/cli/src/config/cwd.cjs delete mode 100644 tools/cli/src/config/index.ts delete mode 100644 tools/cli/src/util/infra.ts delete mode 100644 tools/cli/src/webpack/postcss.config.cjs delete mode 100644 tools/cli/src/webpack/webpack.config.ts delete mode 100644 tools/cli/tsconfig.json create mode 100644 tools/scripts/README.md create mode 100755 tools/scripts/bin/runner.js create mode 100644 tools/scripts/loader.js rename tools/{cli => scripts}/package.json (69%) create mode 100644 tools/scripts/src/affine.ts create mode 100644 tools/scripts/src/build.ts create mode 100644 tools/scripts/src/bundle.ts create mode 100644 tools/scripts/src/clean.ts create mode 100755 tools/scripts/src/codegen.ts create mode 100644 tools/scripts/src/command.ts create mode 100644 tools/scripts/src/context.ts create mode 100644 tools/scripts/src/dev.ts create mode 100644 tools/scripts/src/run.ts rename tools/{cli => scripts}/src/webpack/cache-group.ts (100%) rename tools/{cli => scripts}/src/webpack/error-handler.js (100%) create mode 100644 tools/scripts/src/webpack/html-plugin.ts rename tools/{cli/src/webpack/config.ts => scripts/src/webpack/index.ts} (76%) rename tools/{cli => scripts}/src/webpack/s3-plugin.ts (90%) rename tools/{cli => scripts}/src/webpack/template.html (100%) create mode 100644 tools/scripts/src/webpack/types.ts create mode 100644 tools/scripts/tsconfig.json create mode 100644 tools/utils/package.json rename tools/{cli/src/webpack/runtime-config.ts => utils/src/build-config.ts} (67%) create mode 100644 tools/utils/src/distribution.ts create mode 100644 tools/utils/src/logger.ts create mode 100644 tools/utils/src/package.ts create mode 100644 tools/utils/src/path.ts create mode 100644 tools/utils/src/process.ts create mode 100644 tools/utils/src/types.ts create mode 100644 tools/utils/src/workspace.gen.ts create mode 100644 tools/utils/src/workspace.ts create mode 100644 tools/utils/tsconfig.json create mode 100644 tsconfig.project.json 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: