diff --git a/.github/deployment/node/Dockerfile b/.github/deployment/node/Dockerfile index 873d62daf..353642f9a 100644 --- a/.github/deployment/node/Dockerfile +++ b/.github/deployment/node/Dockerfile @@ -10,4 +10,4 @@ RUN apt-get update && \ apt-get install -y --no-install-recommends openssl && \ rm -rf /var/lib/apt/lists/* -CMD ["node", "--import", "./scripts/register.js", "./dist/index.js"] +CMD ["node", "./dist/main.js"] diff --git a/.github/deployment/self-host/compose.yaml b/.github/deployment/self-host/compose.yaml deleted file mode 100644 index dde7dc8ba..000000000 --- a/.github/deployment/self-host/compose.yaml +++ /dev/null @@ -1,60 +0,0 @@ -services: - affine: - image: ghcr.io/toeverything/affine-graphql:stable - container_name: affine_selfhosted - command: - ['sh', '-c', 'node ./scripts/self-host-predeploy && node ./dist/index.js'] - ports: - - '3010:3010' - - '5555:5555' - depends_on: - redis: - condition: service_healthy - postgres: - condition: service_healthy - volumes: - # custom configurations - - ~/.affine/self-host/config:/root/.affine/config - # blob storage - - ~/.affine/self-host/storage:/root/.affine/storage - logging: - driver: 'json-file' - options: - max-size: '1000m' - restart: unless-stopped - environment: - - NODE_OPTIONS="--import=./scripts/register.js" - - AFFINE_CONFIG_PATH=/root/.affine/config - - REDIS_SERVER_HOST=redis - - DATABASE_URL=postgres://affine:affine@postgres:5432/affine - - NODE_ENV=production - # Telemetry allows us to collect data on how you use the affine. This data will helps us improve the app and provide better features. - # Uncomment next line if you wish to quit telemetry. - # - TELEMETRY_ENABLE=false - redis: - image: redis - container_name: affine_redis - restart: unless-stopped - volumes: - - ~/.affine/self-host/redis:/data - healthcheck: - test: ['CMD', 'redis-cli', '--raw', 'incr', 'ping'] - interval: 10s - timeout: 5s - retries: 5 - postgres: - image: postgres:16 - container_name: affine_postgres - restart: unless-stopped - volumes: - - ~/.affine/self-host/postgres:/var/lib/postgresql/data - healthcheck: - test: ['CMD-SHELL', 'pg_isready -U affine'] - interval: 10s - timeout: 5s - retries: 5 - environment: - POSTGRES_USER: affine - POSTGRES_PASSWORD: affine - POSTGRES_DB: affine - PGDATA: /var/lib/postgresql/data/pgdata diff --git a/.github/workflows/build-images.yml b/.github/workflows/build-images.yml index 644bbe5c0..32b6b88f4 100644 --- a/.github/workflows/build-images.yml +++ b/.github/workflows/build-images.yml @@ -13,31 +13,6 @@ permissions: packages: 'write' jobs: - build-server: - name: Build Server - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Setup Version - id: version - uses: ./.github/actions/setup-version - - name: Setup Node.js - uses: ./.github/actions/setup-node - with: - electron-install: false - extra-flags: workspaces focus @affine/server @types/affine__env - - name: Build Server - run: | - find packages/backend/server/src -type d -name "__tests__" -exec rm -rf {} + - rm -rf packages/backend/server/src/seed - yarn workspace @affine/server build - - name: Upload server dist - uses: actions/upload-artifact@v4 - with: - name: server-dist - path: ./packages/backend/server/dist - if-no-files-found: error - build-web: name: Build @affine/web runs-on: ubuntu-latest @@ -143,7 +118,7 @@ jobs: matrix: targets: - name: x86_64-unknown-linux-gnu - file: server-native.node + file: server-native.x64.node - name: aarch64-unknown-linux-gnu file: server-native.arm64.node - name: armv7-unknown-linux-gnueabihf @@ -164,11 +139,46 @@ jobs: with: target: ${{ matrix.targets.name }} package: '@affine/server-native' + - name: Rename ${{ matrix.targets.file }} + run: | + mv ./packages/backend/native/server-native.node ./packages/backend/native/${{ matrix.targets.file }} - name: Upload ${{ matrix.targets.file }} uses: actions/upload-artifact@v4 with: - name: ${{ matrix.targets.file }} - path: ./packages/backend/native/server-native.node + name: server-native-${{ matrix.targets.file }} + path: ./packages/backend/native/${{ matrix.targets.file }} + if-no-files-found: error + + build-server: + name: Build Server + runs-on: ubuntu-latest + needs: + - build-server-native + steps: + - uses: actions/checkout@v4 + - name: Setup Version + id: version + uses: ./.github/actions/setup-version + - name: Setup Node.js + uses: ./.github/actions/setup-node + with: + electron-install: false + extra-flags: workspaces focus @affine/server @types/affine__env + - name: Download server-native + uses: actions/download-artifact@v4 + with: + pattern: server-native-* + merge-multiple: true + path: ./packages/backend/native + - name: List server-native files + run: ls -alh ./packages/backend/native + - name: Build Server + run: yarn workspace @affine/server build + - name: Upload server dist + uses: actions/upload-artifact@v4 + with: + name: server-dist + path: ./packages/backend/server/dist if-no-files-found: error build-images: @@ -179,7 +189,6 @@ jobs: - build-web - build-mobile - build-admin - - build-server-native steps: - uses: actions/checkout@v4 - name: Download server dist @@ -187,25 +196,6 @@ jobs: with: name: server-dist path: ./packages/backend/server/dist - - name: Download server-native.node - uses: actions/download-artifact@v4 - with: - name: server-native.node - path: ./packages/backend/server - - name: Download server-native.node arm64 - uses: actions/download-artifact@v4 - with: - name: server-native.arm64.node - path: ./packages/backend/native - - name: Download server-native.node arm64 - uses: actions/download-artifact@v4 - with: - name: server-native.armv7.node - path: . - - name: move server-native files - run: | - mv ./packages/backend/native/server-native.node ./packages/backend/server/server-native.arm64.node - mv server-native.node ./packages/backend/server/server-native.armv7.node - name: Setup env run: | echo "GIT_SHORT_HASH=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV" diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 008ed9051..34e1a8b40 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -582,7 +582,7 @@ jobs: uses: actions/download-artifact@v4 with: name: server-native.node - path: ./packages/backend/server + path: ./packages/backend/native - name: Prepare Server Test Environment uses: ./.github/actions/server-test-env @@ -644,7 +644,7 @@ jobs: uses: actions/download-artifact@v4 with: name: server-native.node - path: ./packages/backend/server + path: ./packages/backend/native - name: Prepare Server Test Environment uses: ./.github/actions/server-test-env @@ -885,7 +885,7 @@ jobs: uses: actions/download-artifact@v4 with: name: server-native.node - path: ./packages/backend/server + path: ./packages/backend/native - name: Prepare Server Test Environment if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }} @@ -982,7 +982,7 @@ jobs: uses: actions/download-artifact@v4 with: name: server-native.node - path: ./packages/backend/server + path: ./packages/backend/native - name: Prepare Server Test Environment if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }} @@ -1077,7 +1077,7 @@ jobs: uses: actions/download-artifact@v4 with: name: server-native.node - path: ./packages/backend/server + path: ./packages/backend/native - name: Download affine.linux-x64-gnu.node uses: actions/download-artifact@v4 diff --git a/.github/workflows/copilot-test.yml b/.github/workflows/copilot-test.yml index 8e78b9585..c50420b68 100644 --- a/.github/workflows/copilot-test.yml +++ b/.github/workflows/copilot-test.yml @@ -73,7 +73,7 @@ jobs: uses: actions/download-artifact@v4 with: name: server-native.node - path: ./packages/backend/server + path: ./packages/backend/native - name: Prepare Server Test Environment env: @@ -142,7 +142,7 @@ jobs: uses: actions/download-artifact@v4 with: name: server-native.node - path: ./packages/backend/server + path: ./packages/backend/native - name: Prepare Server Test Environment env: diff --git a/package.json b/package.json index c70596db6..b4a647148 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,6 @@ "@magic-works/i18n-codegen": "^0.6.1", "@playwright/test": "=1.51.1", "@smarttools/eslint-plugin-rxjs": "^1.0.8", - "@swc/core": "^1.10.1", "@taplo/cli": "^0.7.0", "@toeverything/infra": "workspace:*", "@types/eslint": "^9.6.1", diff --git a/packages/backend/native/index.js b/packages/backend/native/index.js index 7a9334462..5392699df 100644 --- a/packages/backend/native/index.js +++ b/packages/backend/native/index.js @@ -1,15 +1,14 @@ -import { createRequire } from 'node:module'; - -const require = createRequire(import.meta.url); - /** @type {import('.')} */ -const binding = require('./server-native.node'); +let binding; +try { + binding = require('./server-native.node'); +} catch { + binding = + process.arch === 'arm64' + ? require('./server-native.arm64.node') + : process.arch === 'arm' + ? require('./server-native.armv7.node') + : require('./server-native.x64.node'); +} -export const mergeUpdatesInApplyWay = binding.mergeUpdatesInApplyWay; -export const verifyChallengeResponse = binding.verifyChallengeResponse; -export const mintChallengeResponse = binding.mintChallengeResponse; -export const getMime = binding.getMime; -export const Tokenizer = binding.Tokenizer; -export const fromModelName = binding.fromModelName; -export const htmlSanitize = binding.htmlSanitize; -export const parseDoc = binding.parseDoc; +module.exports = binding; diff --git a/packages/backend/native/package.json b/packages/backend/native/package.json index d1182a222..dfa425256 100644 --- a/packages/backend/native/package.json +++ b/packages/backend/native/package.json @@ -4,7 +4,6 @@ "engines": { "node": ">= 10.16.0 < 11 || >= 11.8.0" }, - "type": "module", "main": "./index.js", "module": "./index.js", "types": "index.d.ts", diff --git a/packages/backend/server/package.json b/packages/backend/server/package.json index 190f11121..819b5848a 100644 --- a/packages/backend/server/package.json +++ b/packages/backend/server/package.json @@ -8,7 +8,7 @@ "run-test": "./scripts/run-test.ts" }, "scripts": { - "build": "tsc -b", + "build": "affine bundle -p @affine/server", "dev": "nodemon ./src/index.ts", "dev:mail": "email dev -d src/mails", "test": "ava --concurrency 1 --serial", @@ -17,14 +17,16 @@ "test:copilot:coverage": "c8 ava --timeout=5m \"src/__tests__/copilot-*.spec.ts\"", "e2e": "cross-env TEST_MODE=e2e ava --serial", "e2e:coverage": "cross-env TEST_MODE=e2e c8 ava --serial", - "data-migration": "cross-env NODE_ENV=development r ./src/data/index.ts", + "data-migration": "cross-env NODE_ENV=development SERVER_FLAVOR=script r ./src/index.ts", "init": "yarn prisma migrate dev && yarn data-migration run", "seed": "r ./src/seed/index.ts", "genconfig": "r ./scripts/genconfig.ts", - "predeploy": "yarn prisma migrate deploy && node --import ./scripts/register.js ./dist/data/index.js run", + "cli": "cross-env SERVER_FLAVOR=script node ./dist/main.js", + "predeploy": "yarn prisma migrate deploy && yarn cli run", "postinstall": "prisma generate" }, "dependencies": { + "@affine/server-native": "workspace:*", "@ai-sdk/google": "^1.2.10", "@ai-sdk/openai": "^1.3.18", "@ai-sdk/perplexity": "^1.1.6", @@ -72,6 +74,7 @@ "ai": "^4.3.4", "bullmq": "^5.40.2", "cookie-parser": "^1.4.7", + "cross-env": "^7.0.3", "date-fns": "^4.0.0", "dotenv": "^16.4.7", "eventemitter2": "^6.4.9", @@ -117,7 +120,6 @@ "@affine-tools/cli": "workspace:*", "@affine-tools/utils": "workspace:*", "@affine/graphql": "workspace:*", - "@affine/server-native": "workspace:*", "@faker-js/faker": "^9.6.0", "@nestjs/testing": "patch:@nestjs/testing@npm%3A10.4.15#~/.yarn/patches/@nestjs-testing-npm-10.4.15-d591a1705a.patch", "@types/cookie-parser": "^1.4.8", @@ -138,7 +140,6 @@ "@types/supertest": "^6.0.2", "ava": "^6.2.0", "c8": "^10.1.3", - "cross-env": "^7.0.3", "nodemon": "^3.1.7", "react-email": "4.0.7", "sinon": "^20.0.0", diff --git a/packages/backend/server/scripts/loader.js b/packages/backend/server/scripts/loader.js deleted file mode 100644 index ce31fb509..000000000 --- a/packages/backend/server/scripts/loader.js +++ /dev/null @@ -1,11 +0,0 @@ -import { create, createEsmHooks } from 'ts-node'; - -const service = create({ - experimentalSpecifierResolution: 'node', - transpileOnly: true, - logError: true, - skipProject: true, -}); -const hooks = createEsmHooks(service); - -export const resolve = hooks.resolve; diff --git a/packages/backend/server/scripts/register.js b/packages/backend/server/scripts/register.js deleted file mode 100644 index ce20ff536..000000000 --- a/packages/backend/server/scripts/register.js +++ /dev/null @@ -1,4 +0,0 @@ -import { register } from 'node:module'; -import { pathToFileURL } from 'node:url'; - -register('./scripts/loader.js', pathToFileURL('./')); diff --git a/packages/backend/server/src/__tests__/env.spec.ts b/packages/backend/server/src/__tests__/env.spec.ts index cd7f317dc..970b464fd 100644 --- a/packages/backend/server/src/__tests__/env.spec.ts +++ b/packages/backend/server/src/__tests__/env.spec.ts @@ -111,7 +111,7 @@ test('should tell flavors correctly', t => { sync: true, renderer: true, doc: true, - script: true, + script: false, }); process.env.SERVER_FLAVOR = 'graphql'; @@ -122,6 +122,15 @@ test('should tell flavors correctly', t => { doc: false, script: false, }); + + process.env.SERVER_FLAVOR = 'script'; + t.deepEqual(new Env().flavors, { + graphql: false, + sync: false, + renderer: false, + doc: false, + script: true, + }); }); test('should tell selfhosted correctly', t => { diff --git a/packages/backend/server/src/data/index.ts b/packages/backend/server/src/cli.ts similarity index 57% rename from packages/backend/server/src/data/index.ts rename to packages/backend/server/src/cli.ts index 3797a9bad..6e347a9c1 100644 --- a/packages/backend/server/src/data/index.ts +++ b/packages/backend/server/src/cli.ts @@ -1,16 +1,12 @@ import { Logger } from '@nestjs/common'; import { CommandFactory } from 'nest-commander'; -async function bootstrap() { - process.env.SERVER_FLAVOR = 'script'; +import { CliAppModule } from './data/app'; - await import('../prelude'); - const { CliAppModule } = await import('./app'); +export async function run() { await CommandFactory.run(CliAppModule, new Logger()).catch(e => { console.error(e); process.exit(1); }); process.exit(0); } - -await bootstrap(); diff --git a/packages/backend/server/src/data/commands/create.ts b/packages/backend/server/src/data/commands/create.ts index 504dc7e36..32ec68910 100644 --- a/packages/backend/server/src/data/commands/create.ts +++ b/packages/backend/server/src/data/commands/create.ts @@ -1,5 +1,5 @@ -import { writeFileSync } from 'node:fs'; -import { join } from 'node:path'; +import { appendFileSync, writeFileSync } from 'node:fs'; +import { join, parse } from 'node:path'; import { fileURLToPath } from 'node:url'; import { Logger } from '@nestjs/common'; @@ -45,15 +45,21 @@ export class CreateCommand extends CommandRunner { const timestamp = Date.now(); const content = this.createScript(upperFirst(camelCase(name)) + timestamp); - const fileName = `${timestamp}-${kebabCase(name)}.ts`; - const filePath = join( + const migrationDir = join( fileURLToPath(import.meta.url), - '../../migrations', - fileName + '../../migrations' ); + const fileName = `${timestamp}-${kebabCase(name)}.ts`; + const filePath = join(migrationDir, fileName); this.logger.log(`Creating ${fileName}...`); writeFileSync(filePath, content); + const indexFile = join(migrationDir, 'index.ts'); + appendFileSync( + indexFile, + `export * from './${parse(fileName).name}';`, + 'utf-8' + ); this.logger.log(`Migration file created at ${filePath}`); this.logger.log('Done'); } diff --git a/packages/backend/server/src/data/commands/run.ts b/packages/backend/server/src/data/commands/run.ts index 50f501602..5f318d40f 100644 --- a/packages/backend/server/src/data/commands/run.ts +++ b/packages/backend/server/src/data/commands/run.ts @@ -1,49 +1,28 @@ -import { readdirSync } from 'node:fs'; -import { join } from 'node:path'; -import { fileURLToPath, pathToFileURL } from 'node:url'; - import { Logger } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; import { PrismaClient } from '@prisma/client'; import { once } from 'lodash-es'; import { Command, CommandRunner } from 'nest-commander'; +import * as migrations from '../migrations'; + interface Migration { - file: string; name: string; always?: boolean; up: (db: PrismaClient, injector: ModuleRef) => Promise; down: (db: PrismaClient, injector: ModuleRef) => Promise; } -export const collectMigrations = once(async () => { - const folder = join(fileURLToPath(import.meta.url), '../../migrations'); - - const migrationFiles = readdirSync(folder) - .filter(desc => - desc.endsWith(import.meta.url.endsWith('.ts') ? '.ts' : '.js') - ) - .map(desc => join(folder, desc)); - - migrationFiles.sort((a, b) => a.localeCompare(b)); - - const migrations: Migration[] = await Promise.all( - migrationFiles.map(async file => { - return import(pathToFileURL(file).href).then(mod => { - const migration = mod[Object.keys(mod)[0]]; - - return { - file, - name: migration.name, - always: migration.always, - up: migration.up, - down: migration.down, - }; - }); - }) - ); - - return migrations; +export const collectMigrations = once(() => { + return Object.values(migrations).map(migration => { + return { + name: migration.name, + // @ts-expect-error optional + always: migration.always, + up: migration.up, + down: migration.down, + }; + }) as Migration[]; }); @Command({ @@ -60,7 +39,7 @@ export class RunCommand extends CommandRunner { } override async run(): Promise { - const migrations = await collectMigrations(); + const migrations = collectMigrations(); const done: Migration[] = []; for (const migration of migrations) { const exists = await this.db.dataMigration.count({ @@ -85,7 +64,7 @@ export class RunCommand extends CommandRunner { } async runOne(name: string) { - const migrations = await collectMigrations(); + const migrations = collectMigrations(); const migration = migrations.find(m => m.name === name); if (!migration) { @@ -162,7 +141,7 @@ export class RevertCommand extends CommandRunner { throw new Error('A migration name is required'); } - const migrations = await collectMigrations(); + const migrations = collectMigrations(); const migration = migrations.find(m => m.name === name); diff --git a/packages/backend/server/src/data/migrations/index.ts b/packages/backend/server/src/data/migrations/index.ts new file mode 100644 index 000000000..8b0906b45 --- /dev/null +++ b/packages/backend/server/src/data/migrations/index.ts @@ -0,0 +1,7 @@ +export * from './0001-refresh-features'; +export * from './1698398506533-guid'; +export * from './1703756315970-unamed-account'; +export * from './1721299086340-refresh-unnamed-user'; +export * from './1732861452428-migrate-invite-status'; +export * from './1733125339942-universal-subscription'; +export * from './1738590347632-feature-redundant'; diff --git a/packages/backend/server/src/env.ts b/packages/backend/server/src/env.ts index c308e88d4..ea2a76b5e 100644 --- a/packages/backend/server/src/env.ts +++ b/packages/backend/server/src/env.ts @@ -102,7 +102,8 @@ export class Env implements AppEnv { sync: this.isFlavor(Flavor.Sync), renderer: this.isFlavor(Flavor.Renderer), doc: this.isFlavor(Flavor.Doc), - script: this.isFlavor(Flavor.Script), + // Script in a special flavor, return true only when it is set explicitly + script: this.FLAVOR === Flavor.Script, }; } diff --git a/packages/backend/server/src/index.ts b/packages/backend/server/src/index.ts index a0354c8b0..42806bd65 100644 --- a/packages/backend/server/src/index.ts +++ b/packages/backend/server/src/index.ts @@ -1,20 +1,11 @@ /// import './prelude'; -import { Logger } from '@nestjs/common'; +import { run as runCli } from './cli'; +import { run as runServer } from './server'; -import { createApp } from './app'; -import { Config, URLHelper } from './base'; - -const app = await createApp(); -const config = app.get(Config); -const url = app.get(URLHelper); -const listeningHost = '0.0.0.0'; - -await app.listen(config.server.port, listeningHost); - -const logger = new Logger('App'); - -logger.log(`AFFiNE Server is running in [${env.DEPLOYMENT_TYPE}] mode`); -logger.log(`Listening on http://${listeningHost}:${config.server.port}`); -logger.log(`And the public server should be recognized as ${url.home}`); +if (env.flavors.script) { + await runCli(); +} else { + await runServer(); +} diff --git a/packages/backend/server/src/native.ts b/packages/backend/server/src/native.ts index c41618b78..0bb8ebefa 100644 --- a/packages/backend/server/src/native.ts +++ b/packages/backend/server/src/native.ts @@ -1,17 +1,4 @@ -import { createRequire } from 'node:module'; - -let serverNativeModule: typeof import('@affine/server-native'); -try { - serverNativeModule = await import('@affine/server-native'); -} catch { - const require = createRequire(import.meta.url); - serverNativeModule = - process.arch === 'arm64' - ? require('../server-native.arm64.node') - : process.arch === 'arm' - ? require('../server-native.armv7.node') - : require('../server-native.node'); -} +import serverNativeModule from '@affine/server-native'; export const mergeUpdatesInApplyWay = serverNativeModule.mergeUpdatesInApplyWay; diff --git a/packages/backend/server/src/app.ts b/packages/backend/server/src/server.ts similarity index 77% rename from packages/backend/server/src/app.ts rename to packages/backend/server/src/server.ts index 50df53b98..35a6b4c1e 100644 --- a/packages/backend/server/src/app.ts +++ b/packages/backend/server/src/server.ts @@ -9,6 +9,7 @@ import { CloudThrottlerGuard, Config, GlobalExceptionFilter, + URLHelper, } from './base'; import { SocketIoAdapter } from './base/websocket'; import { AuthGuard } from './core/auth'; @@ -16,7 +17,7 @@ import { serverTimingAndCache } from './middleware/timing'; const OneMB = 1024 * 1024; -export async function createApp() { +export async function run() { const { AppModule } = await import('./app.module'); const app = await NestFactory.create(AppModule, { @@ -28,7 +29,8 @@ export async function createApp() { app.useBodyParser('raw', { limit: 100 * OneMB }); - app.useLogger(app.get(AFFiNELogger)); + const logger = app.get(AFFiNELogger); + app.useLogger(logger); const config = app.get(Config); if (config.server.path) { @@ -57,5 +59,12 @@ export async function createApp() { const adapter = new SocketIoAdapter(app); app.useWebSocketAdapter(adapter); - return app; + const url = app.get(URLHelper); + const listeningHost = '0.0.0.0'; + + await app.listen(config.server.port, listeningHost); + + logger.log(`AFFiNE Server is running in [${env.DEPLOYMENT_TYPE}] mode`); + logger.log(`Listening on http://${listeningHost}:${config.server.port}`); + logger.log(`And the public server should be recognized as ${url.home}`); } diff --git a/packages/backend/server/tsconfig.json b/packages/backend/server/tsconfig.json index 26fc078e1..6997a939f 100644 --- a/packages/backend/server/tsconfig.json +++ b/packages/backend/server/tsconfig.json @@ -12,9 +12,9 @@ }, "include": ["./src"], "references": [ + { "path": "../native" }, { "path": "../../../tools/cli" }, { "path": "../../../tools/utils" }, - { "path": "../../common/graphql" }, - { "path": "../native" } + { "path": "../../common/graphql" } ] } diff --git a/packages/common/infra/package.json b/packages/common/infra/package.json index b048806b3..db90fd0aa 100644 --- a/packages/common/infra/package.json +++ b/packages/common/infra/package.json @@ -34,7 +34,6 @@ "devDependencies": { "@affine/templates": "workspace:*", "@emotion/react": "^11.14.0", - "@swc/core": "^1.10.1", "@testing-library/dom": "^10.4.0", "@testing-library/react": "^16.1.0", "@types/react": "^19.0.1", diff --git a/packages/frontend/core/package.json b/packages/frontend/core/package.json index a0847b362..34fc18f3f 100644 --- a/packages/frontend/core/package.json +++ b/packages/frontend/core/package.json @@ -84,7 +84,6 @@ "zod": "^3.24.1" }, "devDependencies": { - "@swc/core": "^1.10.1", "@testing-library/dom": "^10.4.0", "@testing-library/react": "^16.1.0", "@types/animejs": "^3.1.12", diff --git a/tools/cli/bin/cli.js b/tools/cli/bin/cli.js new file mode 100755 index 000000000..be4c9ecf2 --- /dev/null +++ b/tools/cli/bin/cli.js @@ -0,0 +1,5 @@ +import { spawnSync } from 'node:child_process'; + +spawnSync('yarn', ['r', 'affine.ts', ...process.argv.slice(2)], { + stdio: 'inherit', +}); diff --git a/tools/cli/package.json b/tools/cli/package.json index 6b444993b..37988114f 100644 --- a/tools/cli/package.json +++ b/tools/cli/package.json @@ -4,6 +4,7 @@ "type": "module", "private": true, "bin": { + "affine": "./bin/cli.js", "r": "./bin/runner.js" }, "exports": { @@ -21,6 +22,7 @@ "@napi-rs/simple-git": "^0.1.19", "@perfsee/webpack": "^1.13.0", "@sentry/webpack-plugin": "^3.0.0", + "@swc/core": "^1.10.1", "@tailwindcss/postcss": "^4.0.0", "@vanilla-extract/webpack-plugin": "^2.3.15", "autoprefixer": "^10.4.20", @@ -34,6 +36,7 @@ "lodash-es": "^4.17.21", "mime-types": "^3.0.0", "mini-css-extract-plugin": "^2.9.2", + "node-loader": "^2.1.0", "postcss": "^8.4.49", "postcss-loader": "^8.1.1", "prettier": "^3.4.2", diff --git a/tools/cli/src/bundle.ts b/tools/cli/src/bundle.ts index df3194f55..ed786b360 100644 --- a/tools/cli/src/bundle.ts +++ b/tools/cli/src/bundle.ts @@ -10,12 +10,16 @@ import WebpackDevServer, { } from 'webpack-dev-server'; import { Option, PackageCommand } from './command'; -import { createHTMLTargetConfig, createWorkerTargetConfig } from './webpack'; +import { + createHTMLTargetConfig, + createNodeTargetConfig, + createWorkerTargetConfig, +} from './webpack'; -function getBundleConfigs(pkg: Package) { +function getBaseWorkerConfigs(pkg: Package) { const core = new Package('@affine/core'); - const workerConfigs = [ + return [ createWorkerTargetConfig( pkg, core.srcPath.join( @@ -31,7 +35,9 @@ function getBundleConfigs(pkg: Package) { core.srcPath.join('blocksuite/extensions/turbo-painter.worker.ts').value ), ]; +} +function getBundleConfigs(pkg: Package) { switch (pkg.name) { case '@affine/admin': { return [createHTMLTargetConfig(pkg, pkg.srcPath.join('index.tsx').value)]; @@ -40,6 +46,7 @@ function getBundleConfigs(pkg: Package) { case '@affine/mobile': case '@affine/ios': case '@affine/android': { + const workerConfigs = getBaseWorkerConfigs(pkg); workerConfigs.push( createWorkerTargetConfig( pkg, @@ -58,6 +65,8 @@ function getBundleConfigs(pkg: Package) { ]; } case '@affine/electron-renderer': { + const workerConfigs = getBaseWorkerConfigs(pkg); + return [ createHTMLTargetConfig( pkg, @@ -78,10 +87,14 @@ function getBundleConfigs(pkg: Package) { ...workerConfigs, ]; } + case '@affine/server': { + return [createNodeTargetConfig(pkg, pkg.srcPath.join('index.ts').value)]; + } } throw new Error(`Unsupported package: ${pkg.name}`); } + const IN_CI = !!process.env.CI; const httpProxyMiddlewareLogLevel = IN_CI ? 'silent' : 'error'; diff --git a/tools/cli/src/webpack/index.ts b/tools/cli/src/webpack/index.ts index 5d8f5908e..5f45c6a0f 100644 --- a/tools/cli/src/webpack/index.ts +++ b/tools/cli/src/webpack/index.ts @@ -2,7 +2,7 @@ import { createRequire } from 'node:module'; import path from 'node:path'; import { getBuildConfig } from '@affine-tools/utils/build-config'; -import { ProjectRoot } from '@affine-tools/utils/path'; +import { Path, ProjectRoot } from '@affine-tools/utils/path'; import { Package } from '@affine-tools/utils/workspace'; import { PerfseePlugin } from '@perfsee/webpack'; import { sentryWebpackPlugin } from '@sentry/webpack-plugin'; @@ -75,10 +75,7 @@ export function createHTMLTargetConfig( }, entry, output: { - environment: { - module: true, - dynamicImport: true, - }, + environment: { module: true, dynamicImport: true }, filename: buildConfig.debug ? 'js/[name].js' : 'js/[name].[contenthash:8].js', @@ -127,12 +124,7 @@ export function createHTMLTargetConfig( }, //#region rules rules: [ - { - test: /\.m?js?$/, - resolve: { - fullySpecified: false, - }, - }, + { test: /\.m?js?$/, resolve: { fullySpecified: false } }, { test: /\.js$/, enforce: 'pre', @@ -185,9 +177,7 @@ export function createHTMLTargetConfig( target: 'es2022', externalHelpers: false, transform: { - react: { - runtime: 'automatic', - }, + react: { runtime: 'automatic' }, useDefineForClassFields: false, decoratorVersion: '2022-03', }, @@ -200,18 +190,9 @@ export function createHTMLTargetConfig( test: /\.(png|jpg|gif|svg|webp|mp4|zip)$/, type: 'asset/resource', }, - { - test: /\.(ttf|eot|woff|woff2)$/, - type: 'asset/resource', - }, - { - test: /\.txt$/, - type: 'asset/source', - }, - { - test: /\.inline\.svg$/, - type: 'asset/inline', - }, + { test: /\.(ttf|eot|woff|woff2)$/, type: 'asset/resource' }, + { test: /\.txt$/, type: 'asset/source' }, + { test: /\.inline\.svg$/, type: 'asset/inline' }, { test: /\.css$/, use: [ @@ -242,12 +223,7 @@ export function createHTMLTargetConfig( ] : [ cssnano({ - preset: [ - 'default', - { - convertValues: false, - }, - ], + preset: ['default', { convertValues: false }], }), ], }, @@ -298,9 +274,7 @@ export function createHTMLTargetConfig( new WebpackS3Plugin(), !buildConfig.debug && process.env.PERFSEE_TOKEN && - new PerfseePlugin({ - project: 'affine-toeverything', - }), + new PerfseePlugin({ project: 'affine-toeverything' }), process.env.SENTRY_AUTH_TOKEN && process.env.SENTRY_ORG && process.env.SENTRY_PROJECT && @@ -325,9 +299,7 @@ export function createHTMLTargetConfig( ]), //#endregion - stats: { - errorDetails: true, - }, + stats: { errorDetails: true }, //#region optimization optimization: { @@ -339,12 +311,8 @@ export function createHTMLTargetConfig( extractComments: true, terserOptions: { ecma: 2020, - compress: { - unused: true, - }, - mangle: { - keep_classnames: true, - }, + compress: { unused: true }, + mangle: { keep_classnames: true }, }, }), ], @@ -353,9 +321,7 @@ export function createHTMLTargetConfig( usedExports: true, sideEffects: true, removeAvailableModules: true, - runtimeChunk: { - name: 'runtime', - }, + runtimeChunk: { name: 'runtime' }, splitChunks: { chunks: 'all', minSize: 1, @@ -382,11 +348,7 @@ export function createHTMLTargetConfig( priority: -10, reuseExistingChunk: true, }, - default: { - minChunks: 2, - priority: -20, - reuseExistingChunk: true, - }, + default: { minChunks: 2, priority: -20, reuseExistingChunk: true }, styles: { name: 'styles', type: 'css/mini-extract', @@ -416,9 +378,7 @@ export function createWorkerTargetConfig( outputModule: false, syncWebAssembly: true, }, - entry: { - [workerName]: entry, - }, + entry: { [workerName]: entry }, output: { filename: `js/${workerName}-${buildConfig.appVersion}.worker.js`, path: pkg.distPath.value, @@ -432,14 +392,9 @@ export function createWorkerTargetConfig( devtool: buildConfig.debug ? 'cheap-module-source-map' : 'source-map', resolve: { symlinks: true, - extensionAlias: { - '.js': ['.js', '.ts'], - '.mjs': ['.mjs', '.mts'], - }, + extensionAlias: { '.js': ['.js', '.ts'], '.mjs': ['.mjs', '.mts'] }, extensions: ['.js', '.ts'], - alias: { - yjs: ProjectRoot.join('node_modules', 'yjs').value, - }, + alias: { yjs: ProjectRoot.join('node_modules', 'yjs').value }, }, module: { @@ -454,12 +409,7 @@ export function createWorkerTargetConfig( }, }, rules: [ - { - test: /\.m?js?$/, - resolve: { - fullySpecified: false, - }, - }, + { test: /\.m?js?$/, resolve: { fullySpecified: false } }, { test: /\.js$/, enforce: 'pre', @@ -508,9 +458,7 @@ export function createWorkerTargetConfig( {} as Record ) ), - new webpack.optimize.LimitChunkCountPlugin({ - maxChunks: 1, - }), + new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }), process.env.SENTRY_AUTH_TOKEN && process.env.SENTRY_ORG && process.env.SENTRY_PROJECT && @@ -520,9 +468,7 @@ export function createWorkerTargetConfig( authToken: process.env.SENTRY_AUTH_TOKEN, }), ]), - stats: { - errorDetails: true, - }, + stats: { errorDetails: true }, optimization: { minimize: !buildConfig.debug, minimizer: [ @@ -532,12 +478,8 @@ export function createWorkerTargetConfig( extractComments: true, terserOptions: { ecma: 2020, - compress: { - unused: true, - }, - mangle: { - keep_classnames: true, - }, + compress: { unused: true }, + mangle: { keep_classnames: true }, }, }), ], @@ -549,8 +491,120 @@ export function createWorkerTargetConfig( runtimeChunk: false, splitChunks: false, }, - performance: { - hints: false, - }, + performance: { hints: false }, + }; +} + +export function createNodeTargetConfig( + pkg: Package, + entry: string +): Omit & { name: string } { + return { + name: entry, + context: ProjectRoot.value, + experiments: { + topLevelAwait: true, + outputModule: pkg.packageJson.type === 'module', + syncWebAssembly: true, + }, + entry: { index: entry }, + output: { + filename: `main.js`, + path: pkg.distPath.value, + clean: true, + globalObject: 'globalThis', + }, + target: ['node', 'es2022'], + externals: (data, callback) => { + if ( + data.request && + // import ... from 'module' + /^[a-zA-Z@]/.test(data.request) && + // not workspace deps + !pkg.deps.some(dep => data.request!.startsWith(dep.name)) + ) { + callback(null, true); + } else { + callback(null, false); + } + }, + externalsPresets: { node: true }, + node: { __dirname: false, __filename: false }, + mode: 'none', + devtool: 'source-map', + resolve: { + symlinks: true, + extensionAlias: { '.js': ['.js', '.ts'], '.mjs': ['.mjs', '.mts'] }, + extensions: ['.js', '.ts', '.tsx', '.node'], + alias: { yjs: ProjectRoot.join('node_modules', 'yjs').value }, + }, + module: { + parser: { + javascript: { url: false, importMeta: false, createRequire: false }, + }, + rules: [ + { + test: /\.js$/, + enforce: 'pre', + include: /@blocksuite/, + use: ['source-map-loader'], + }, + { + test: /\.node$/, + loader: Path.dir(import.meta.url).join('node-loader.js').value, + }, + { + test: /\.tsx?$/, + exclude: /node_modules/, + loader: 'swc-loader', + options: { + // https://swc.rs/docs/configuring-swc/ + jsc: { + preserveAllComments: true, + parser: { + syntax: 'typescript', + dynamicImport: true, + topLevelAwait: true, + tsx: true, + decorators: true, + }, + target: 'es2022', + externalHelpers: false, + transform: { + legacyDecorator: true, + decoratorMetadata: true, + react: { runtime: 'automatic' }, + }, + }, + sourceMaps: true, + inlineSourcesContent: true, + }, + }, + ], + }, + plugins: compact([ + new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }), + new webpack.IgnorePlugin({ + checkResource(resource) { + const lazyImports = [ + '@nestjs/microservices', + '@nestjs/websockets/socket-module', + '@apollo/subgraph', + '@apollo/gateway', + '@as-integrations/fastify', + 'ts-morph', + 'class-validator', + 'class-transformer', + ]; + return lazyImports.some(lazyImport => + resource.startsWith(lazyImport) + ); + }, + }), + ]), + stats: { errorDetails: true }, + optimization: { nodeEnv: false }, + performance: { hints: false }, + ignoreWarnings: [/^(?!CriticalDependenciesWarning$)/], }; } diff --git a/tools/cli/src/webpack/node-loader.js b/tools/cli/src/webpack/node-loader.js new file mode 100644 index 000000000..124a4cb50 --- /dev/null +++ b/tools/cli/src/webpack/node-loader.js @@ -0,0 +1,18 @@ +import { parse } from 'node:path'; + +export const raw = true; +/** + * @type {import('webpack').LoaderDefinitionFunction} + */ +export default function loader(content) { + const name = parse(this.resourcePath).base; + this.emitFile(name, content); + + return ` + import { createRequire } from 'node:module' + + const require = createRequire(import.meta.url) + const binding = require('./${name}') + export default binding + `; +} diff --git a/tools/utils/src/types.ts b/tools/utils/src/types.ts index 9a7fd1cc2..0a72e4641 100644 --- a/tools/utils/src/types.ts +++ b/tools/utils/src/types.ts @@ -8,13 +8,12 @@ export interface YarnWorkspaceItem { export interface CommonPackageJsonContent { name: string; + type?: 'module' | 'commonjs'; version: string; private?: boolean; dependencies?: { [key: string]: string }; devDependencies?: { [key: string]: string }; scripts?: { [key: string]: string }; main?: string; - exports?: { - [key: string]: string; - }; + exports?: { [key: string]: string | { [key: string]: string } }; } diff --git a/tools/utils/src/workspace.gen.ts b/tools/utils/src/workspace.gen.ts index 96ec62f35..1344ca7ad 100644 --- a/tools/utils/src/workspace.gen.ts +++ b/tools/utils/src/workspace.gen.ts @@ -963,10 +963,10 @@ export const PackageList = [ location: 'packages/backend/server', name: '@affine/server', workspaceDependencies: [ + 'packages/backend/native', 'tools/cli', 'tools/utils', 'packages/common/graphql', - 'packages/backend/native', ], }, { diff --git a/yarn.lock b/yarn.lock index c930f805b..b449b7d2e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -122,6 +122,7 @@ __metadata: "@napi-rs/simple-git": "npm:^0.1.19" "@perfsee/webpack": "npm:^1.13.0" "@sentry/webpack-plugin": "npm:^3.0.0" + "@swc/core": "npm:^1.10.1" "@tailwindcss/postcss": "npm:^4.0.0" "@types/lodash-es": "npm:^4.17.12" "@types/mime-types": "npm:^2.1.4" @@ -139,6 +140,7 @@ __metadata: lodash-es: "npm:^4.17.21" mime-types: "npm:^3.0.0" mini-css-extract-plugin: "npm:^2.9.2" + node-loader: "npm:^2.1.0" postcss: "npm:^8.4.49" postcss-loader: "npm:^8.1.1" prettier: "npm:^3.4.2" @@ -155,6 +157,7 @@ __metadata: webpack-dev-server: "npm:^5.2.0" webpack-merge: "npm:^6.0.1" bin: + affine: ./bin/cli.js r: ./bin/runner.js languageName: unknown linkType: soft @@ -417,7 +420,6 @@ __metadata: "@radix-ui/react-slot": "npm:^1.1.1" "@radix-ui/react-toolbar": "npm:^1.1.1" "@sentry/react": "npm:^9.2.0" - "@swc/core": "npm:^1.10.1" "@testing-library/dom": "npm:^10.4.0" "@testing-library/react": "npm:^16.1.0" "@toeverything/infra": "workspace:*" @@ -760,7 +762,6 @@ __metadata: "@magic-works/i18n-codegen": "npm:^0.6.1" "@playwright/test": "npm:=1.51.1" "@smarttools/eslint-plugin-rxjs": "npm:^1.0.8" - "@swc/core": "npm:^1.10.1" "@taplo/cli": "npm:^0.7.0" "@toeverything/infra": "workspace:*" "@types/eslint": "npm:^9.6.1" @@ -14286,7 +14287,6 @@ __metadata: "@affine/templates": "workspace:*" "@emotion/react": "npm:^11.14.0" "@preact/signals-core": "npm:^1.8.0" - "@swc/core": "npm:^1.10.1" "@testing-library/dom": "npm:^10.4.0" "@testing-library/react": "npm:^16.1.0" "@types/react": "npm:^19.0.1" @@ -25152,7 +25152,7 @@ __metadata: languageName: node linkType: hard -"loader-utils@npm:^2.0.0": +"loader-utils@npm:^2.0.0, loader-utils@npm:^2.0.3": version: 2.0.4 resolution: "loader-utils@npm:2.0.4" dependencies: @@ -27414,6 +27414,17 @@ __metadata: languageName: node linkType: hard +"node-loader@npm:^2.1.0": + version: 2.1.0 + resolution: "node-loader@npm:2.1.0" + dependencies: + loader-utils: "npm:^2.0.3" + peerDependencies: + webpack: ^5.0.0 + checksum: 10/d2f20b1e0f946055fcbbf365c3927ffecfff9aee3b5cc2d71e45229ca27010267d3d6fdea04dcb7c0bc7ce9b87878105b8c1d15c05065c813b5c8ec5ef1fb4d1 + languageName: node + linkType: hard + "node-releases@npm:^2.0.19": version: 2.0.19 resolution: "node-releases@npm:2.0.19"