빌드 선언 생성 병목과 Vue 중복 타입 검사를 개선

This commit is contained in:
2026-10-23 01:33:34 +09:00
parent b377250fb9
commit af51968782
31 changed files with 436 additions and 109 deletions
+4 -1
View File
@@ -204,10 +204,13 @@ HTTPS 검증과 구분합니다.
## 코드 스타일
- Vite, Vue/Volar와 compiler API 소비자는 workspace 전체에서 정확히
- Vite, ESLint와 일반 compiler API 소비자는 workspace 전체에서 정확히
TypeScript `6.0.3`을 사용해 주세요. CLI project build/typecheck는 root의
`@typescript/native` alias로 고정한 TypeScript 7 `tsc`를 사용하며 package-local
다른 compiler version이나 직접 `tsc` 호출을 추가하지 말아 주세요.
Vue CLI만 `tools/vue-typecheck.mjs`로 고정한
`typescript-native-bridge@6.0.3-bridge.18.tsgo.7.0.2`를 사용합니다. 이 예외는
Vue 타입 검사에 한하며 workspace `typescript` override를 교체하지 않습니다.
- TypeScript/JSON/Vue SFC는 기존 4-space 스타일을 유지해 주세요.
- public API는 명시적 타입을 사용하고 `any`, 불필요하게 넓은 `unknown`,
`as unknown as` 우회를 피해 주세요.
+3 -1
View File
@@ -89,6 +89,7 @@ input-event 결과를 transaction으로 반영합니다. Redis pub/sub과 SSE는
- Node.js `24.21.0`, pnpm `12.5.1`, Turbo
- TypeScript `6.0.3` compiler API와 TypeScript `7.0.2` native `tsc`
- Vue CLI 전용 `typescript-native-bridge` (`6.0.3-bridge.18.tsgo.7.0.2`)
- Fastify, tRPC, zod
- Vue 3, Pinia, Vue Router, Vite
- PostgreSQL, Prisma, Redis
@@ -99,7 +100,8 @@ Node.js는 `.nvmrc`에서 24.x로 고정합니다. 의존성 설치와 검증에
`package.json`과 `pnpm-lock.yaml`을 함께 사용해 주세요. 모든 workspace package는
내부 전용(`private`)이므로 manifest의 `0.0.0`은 배포 버전이 아닙니다. 배포 source는
full Git commit으로 고정하고, 실험 릴리스 같은 milestone은 annotated Git tag로
식별합니다. Profile frontend는 같은 commit의 정적 버전 문서를 고정 URL과 ETag로
식별합니다. 선언 파일 생성·Vue 검사 범위와 캐시 계약은 [빌드 도구](docs/build-performance.md)를
확인해 주세요. Profile frontend는 같은 commit의 정적 버전 문서를 고정 URL과 ETag로
주기적으로 재검증하고, 현재 열린 bundle과 달라졌을 때만 강제 reload 없이 공용
toast로 한 번 안내합니다.
+5 -1
View File
@@ -3,7 +3,11 @@
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"composite": false
"composite": false,
"paths": {
"@sammo-ts/game-api": ["./src/index.ts"],
"@sammo-ts/game-api/*": ["./src/*"]
}
},
"include": ["src"]
}
+6 -3
View File
@@ -3,8 +3,11 @@
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"composite": false
"composite": false,
"paths": {
"@sammo-ts/game-engine": ["./src/index.ts"],
"@sammo-ts/game-engine/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "../../packages/common" }, { "path": "../../packages/logic" }]
"include": ["src"]
}
+3 -3
View File
@@ -1,4 +1,5 @@
import { defineConfig } from 'tsdown';
import { emitDeclarations } from '../../tools/build-scripts/emit-declarations.mjs';
export default defineConfig({
entry: {
@@ -30,9 +31,8 @@ export default defineConfig({
},
format: 'es',
outDir: 'dist',
dts: {
build: true,
},
dts: false,
hooks: { 'build:done': emitDeclarations },
sourcemap: true,
target: 'node22',
platform: 'node',
+12 -6
View File
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
import { expect, test, type Browser, type Page } from '@playwright/test';
import { encryptGameSessionToken } from '../../../packages/common/dist/auth/gameToken.js';
import { createGamePostgresConnector } from '../../../packages/infra/dist/index.js';
import { createGamePostgresConnector, GamePrisma } from '../../../packages/infra/dist/index.js';
const databaseUrl = process.env.CHIEF_CENTER_LIVE_DATABASE_URL;
const gameTokenSecret = process.env.CHIEF_CENTER_LIVE_GAME_SECRET;
@@ -174,7 +174,13 @@ test('persists one chief command and exposes it to a normal nation user and anot
await transaction.nationTurn.deleteMany({
where: { nationId: editor.nationId, officerLevel: editor.officerLevel },
});
if (originalTurns.length) await transaction.nationTurn.createMany({ data: originalTurns });
if (originalTurns.length)
await transaction.nationTurn.createMany({
data: originalTurns.map((row: (typeof originalTurns)[number]) => ({
...row,
arg: row.arg ?? GamePrisma.JsonNull,
})),
});
await transaction.nationTurnRevision.deleteMany({
where: { nationId: editor.nationId, officerLevel: editor.officerLevel },
});
@@ -185,8 +191,8 @@ test('persists one chief command and exposes it to a normal nation user and anot
userId: viewer.userId,
officerLevel: viewer.officerLevel,
npcState: viewer.npcState,
meta: viewer.meta,
penalty: viewer.penalty,
meta: viewer.meta ?? GamePrisma.JsonNull,
penalty: viewer.penalty ?? GamePrisma.JsonNull,
},
});
await transaction.general.update({
@@ -195,8 +201,8 @@ test('persists one chief command and exposes it to a normal nation user and anot
userId: otherChief.userId,
officerLevel: otherChief.officerLevel,
npcState: otherChief.npcState,
meta: otherChief.meta,
penalty: otherChief.penalty,
meta: otherChief.meta ?? GamePrisma.JsonNull,
penalty: otherChief.penalty ?? GamePrisma.JsonNull,
},
});
});
@@ -1,6 +1,6 @@
import { readFile } from 'node:fs/promises';
import { expect, test, type Locator, type Page } from '@playwright/test';
import { createGamePostgresConnector } from '../../../packages/infra/dist/index.js';
import { createGamePostgresConnector, GamePrisma } from '../../../packages/infra/dist/index.js';
const bootstrapFile = process.env.COMMAND_PANEL_LIVE_BOOTSTRAP_FILE;
const databaseUrl = process.env.DATABASE_URL;
@@ -166,14 +166,26 @@ test('reserves every requested general and chief command through Chromium and re
} finally {
await db.$transaction(async (transaction) => {
await transaction.generalTurn.deleteMany({ where: { generalId: general.id } });
if (originalGeneralTurns.length) await transaction.generalTurn.createMany({ data: originalGeneralTurns });
if (originalGeneralTurns.length)
await transaction.generalTurn.createMany({
data: originalGeneralTurns.map((row: (typeof originalGeneralTurns)[number]) => ({
...row,
arg: row.arg ?? GamePrisma.JsonNull,
})),
});
await transaction.generalTurnRevision.deleteMany({ where: { generalId: general.id } });
if (originalGeneralRevision)
await transaction.generalTurnRevision.create({ data: originalGeneralRevision });
await transaction.nationTurn.deleteMany({
where: { nationId: general.nationId, officerLevel: general.officerLevel },
});
if (originalNationTurns.length) await transaction.nationTurn.createMany({ data: originalNationTurns });
if (originalNationTurns.length)
await transaction.nationTurn.createMany({
data: originalNationTurns.map((row: (typeof originalNationTurns)[number]) => ({
...row,
arg: row.arg ?? GamePrisma.JsonNull,
})),
});
await transaction.nationTurnRevision.deleteMany({
where: { nationId: general.nationId, officerLevel: general.officerLevel },
});
+3 -3
View File
@@ -5,9 +5,9 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"build": "node ../../tools/vue-typecheck.mjs && vite build",
"build:release": "vite build --outDir .release-build",
"typecheck:release": "vue-tsc --noEmit",
"typecheck:release": "node ../../tools/vue-typecheck.mjs --noEmit",
"preview": "vite preview",
"test": "pnpm test:unit",
"test:unit": "node --test test/**/*.test.ts",
@@ -30,7 +30,7 @@
"test:e2e:default-recruit-commands-live": "pnpm --filter @sammo-ts/infra prisma:generate && pnpm --filter @sammo-ts/common build && pnpm --filter @sammo-ts/logic build && pnpm --filter @sammo-ts/infra build && pnpm --filter @sammo-ts/game-engine build && pnpm --filter @sammo-ts/game-api build && playwright test --config e2e/defaultRecruitCommands.live.playwright.config.mjs --tsconfig e2e/playwright.live.tsconfig.json",
"lint": "pnpm -w lint:target app/game-frontend",
"lint:fix": "pnpm -w lint:target:fix app/game-frontend",
"typecheck": "vue-tsc --noEmit"
"typecheck": "node ../../tools/vue-typecheck.mjs --noEmit"
},
"dependencies": {
"@sammo-ts/common": "workspace:*",
+4 -9
View File
@@ -30,16 +30,11 @@
"@sammo-ts/game-api/*": ["../../app/game-api/src/*"],
"@sammo-ts/gateway-api": ["../../app/gateway-api/src/index.ts"],
"@sammo-ts/gateway-api/*": ["../../app/gateway-api/src/*"]
}
},
"declaration": false,
"declarationMap": false
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"test/**/*.ts",
"e2e/**/*.ts"
],
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "test/**/*.ts", "e2e/**/*.ts"],
"references": [
{
"path": "./tsconfig.node.json"
+5 -1
View File
@@ -3,7 +3,11 @@
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"composite": false
"composite": false,
"paths": {
"@sammo-ts/gateway-api": ["./src/index.ts"],
"@sammo-ts/gateway-api/*": ["./src/*"]
}
},
"include": ["src"]
}
@@ -152,6 +152,7 @@ const installGameFixture = async (page: Page, profile: ProfileFixture, userCnt:
}
if (operation === 'lobby.info') {
return response({
scenarioTitle: '공개 지도 테스트 시나리오',
year: 200,
month: profile.profile === 'che' ? 1 : 2,
userCnt,
@@ -448,8 +449,7 @@ test('renders the Gateway profile order returned by the API', async ({ page }, t
await page.setViewportSize({ width: 1200, height: 900 });
await page.goto('lobby');
const renderedProfiles = await page.locator('tbody tr td:first-child > div:first-child').allTextContents();
expect(renderedProfiles.map((name) => name.trim())).toEqual([
await expect(page.locator('tbody tr td:first-child > div:first-child')).toHaveText([
'체섭',
'퀘섭',
'풰섭',
+3 -3
View File
@@ -9,13 +9,13 @@
"test:e2e:operations": "playwright test --config e2e/playwright.config.mjs",
"test:e2e:hwe-lifecycle": "playwright test --config e2e/hwe-lifecycle.playwright.config.mjs",
"test:e2e:general-icons": "playwright test --config e2e/general-icon-lifecycle.playwright.config.mjs",
"build": "vue-tsc && vite build",
"build": "node ../../tools/vue-typecheck.mjs && vite build",
"build:release": "vite build",
"typecheck:release": "vue-tsc --noEmit",
"typecheck:release": "node ../../tools/vue-typecheck.mjs --noEmit",
"preview": "vite preview",
"lint": "pnpm -w lint:target app/gateway-frontend",
"lint:fix": "pnpm -w lint:target:fix app/gateway-frontend",
"typecheck": "vue-tsc --noEmit"
"typecheck": "node ../../tools/vue-typecheck.mjs --noEmit"
},
"dependencies": {
"@sammo-ts/common": "workspace:*",
+18 -53
View File
@@ -4,11 +4,7 @@
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": [
"ESNext",
"DOM",
"DOM.Iterable"
],
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
@@ -21,55 +17,24 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": [
"./src/*"
],
"@sammo-ts/common": [
"../../packages/common/src/index.ts"
],
"@sammo-ts/common/*": [
"../../packages/common/src/*"
],
"@sammo-ts/infra": [
"../../packages/infra/src/index.ts"
],
"@sammo-ts/infra/*": [
"../../packages/infra/src/*"
],
"@sammo-ts/logic": [
"../../packages/logic/src/index.ts"
],
"@sammo-ts/logic/*": [
"../../packages/logic/src/*"
],
"@sammo-ts/game-engine": [
"../../app/game-engine/src/index.ts"
],
"@sammo-ts/game-engine/*": [
"../../app/game-engine/src/*"
],
"@sammo-ts/game-api": [
"../../app/game-api/src/index.ts"
],
"@sammo-ts/game-api/*": [
"../../app/game-api/src/*"
],
"@sammo-ts/gateway-api": [
"../../app/gateway-api/src/index.ts"
],
"@sammo-ts/gateway-api/*": [
"../../app/gateway-api/src/*"
]
}
"@/*": ["./src/*"],
"@sammo-ts/common": ["../../packages/common/src/index.ts"],
"@sammo-ts/common/*": ["../../packages/common/src/*"],
"@sammo-ts/infra": ["../../packages/infra/src/index.ts"],
"@sammo-ts/infra/*": ["../../packages/infra/src/*"],
"@sammo-ts/logic": ["../../packages/logic/src/index.ts"],
"@sammo-ts/logic/*": ["../../packages/logic/src/*"],
"@sammo-ts/game-engine": ["../../app/game-engine/src/index.ts"],
"@sammo-ts/game-engine/*": ["../../app/game-engine/src/*"],
"@sammo-ts/game-api": ["../../app/game-api/src/index.ts"],
"@sammo-ts/game-api/*": ["../../app/game-api/src/*"],
"@sammo-ts/gateway-api": ["../../app/gateway-api/src/index.ts"],
"@sammo-ts/gateway-api/*": ["../../app/gateway-api/src/*"]
},
"declaration": false,
"declarationMap": false
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"test/**/*.ts",
"e2e/**/*.ts"
],
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "test/**/*.ts", "e2e/**/*.ts"],
"references": [
{
"path": "./tsconfig.node.json"
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"composite": false,
"paths": {
"@sammo-ts/release-controller": ["./src/index.ts"],
"@sammo-ts/release-controller/*": ["./src/*"]
}
},
"include": ["src"]
}
+75
View File
@@ -0,0 +1,75 @@
# 빌드 도구와 캐시 계약
## 타입 검사기 분리
- Vite, ESLint와 일반 compiler API는 workspace의 TypeScript `6.0.3`을 사용한다.
- server project 검사와 선언 생성은 root `pnpm tsc7`의 TypeScript `7.0.2`를 사용한다.
- 두 frontend의 Vue CLI만 `tools/vue-typecheck.mjs`가
`typescript-native-bridge@6.0.3-bridge.18.tsgo.7.0.2`의 compiler 경로를
`vue-tsc.run()`에 전달한다. 전역 `typescript` override를 bridge로 바꾸지 않는다.
- frontend는 `noEmit`이며 `declaration`과 `declarationMap`을 명시적으로 끈다.
선언 파일을 내지 않는 검사에서 선언 이식성 진단을 요구하지 않으며, 일반 TS와
Vue template의 타입 검사는 유지한다.
`pnpm check:typescript-toolchain`은 API, native CLI와 bridge의 정확한 버전을 검사한다.
bridge의 Linux native binary는 glibc 환경을 요구한다. 운영 builder의 Debian
Bookworm과 같은 환경을 사용하며 Alpine/musl은 지원 대상으로 간주하지 않는다.
platform binary가 없으면 검사를 건너뛰거나 성공으로 처리하지 않고 실패한다.
일반 `pnpm --filter <frontend> build`는 Vue 검사 후 Vite bundle을 생성한다.
Turbo의 frontend `build` override는 `^build`만 선행하므로 이 내장 검사와 별도
`typecheck`를 중복 실행하지 않는다. `pnpm typecheck`는 기존처럼 두 frontend를
검사한다. 운영 `build:release`는 cached `typecheck:release`를 선행한다.
## 서버 선언 파일
tsdown은 기존 entry, target, external dependency와 JS source map 설정으로 JS를
bundle한다. `build:done` hook은 `tools/build-scripts/emit-declarations.mjs`를 호출한다.
이 hook은 package의 `tsconfig.build.json`과 root의 `pnpm tsc7`로 선언 파일과 선언
source map을 생성한다. compiler 실패와 signal은 전체 build 실패로 전파한다.
Prisma와 tRPC의 큰 inferred type graph를 한 `.d.ts`로 다시 묶지 않는다. 각
package의 공개 `dist/index.d.ts`와 명시된 subpath는 유지하고, 그 선언이 참조하는
파일도 `dist`에 함께 둔다. 이 저장소의 package는 private이며 배포 시 선언 파일
하나만 복사해서는 안 된다.
`logic`의 기존 `./* → ./dist/src/*` export는 별도 계약이다. 이 package는 native
compiler가 `dist/src`에 JS, 선언과 양쪽 source map을 함께 생성한다.
`dist/index.d.ts`는 `./src/index.js`를 재수출하는 작은 진입점이다. 루트와 하위
경로의 class가 같은 선언을 참조하므로 private member의 타입 정체성이 갈라지지
않는다. 실제 선언과 source map은 `dist/src`에 있다.
선언 생성용 config는 test를 포함하는 project-reference 검사와 분리한다. 자체
package alias는 source로, 선행 package는 공개 dist로 해석한다. 기존 타입 검사
config의 references와 test 포함 범위는 유지한다. Turbo가 dependency build 순서를
보장하며 직접 package를 빌드하기 전에는 [테스트 준비](testing-policy.md#새-worktree-준비)를
완료해야 한다.
## 캐시와 검증
일반 build key에는 root tsconfig, tsdown config와 선언/Vue 검사 도구가 포함된다.
frontend build key에는 모든 `VITE_*` 공개 입력이 포함된다. 타입 검사 task는
`!VITE_*`로 [자동 framework 환경 추론](https://turborepo.dev/docs/crafting-your-repository/using-environment-variables)을
제외한다. prefix나 배포 SHA만 바뀌어도 같은 source를 다시 타입 검사하지 않으며,
실제 Vite bundle의 공개 입력은 계속 cache key에 포함한다. 운영 frontend의 source
입력, 고정 commit, prefix와 공유 artifact 계약은
[릴리스 운영](release-operations.md)을 따른다. source map과 타입 검사를 끄는 방식으로
속도를 측정하지 않는다.
```sh
pnpm install --frozen-lockfile
pnpm check:typescript-toolchain
pnpm --filter @sammo-ts/build-scripts test
CI=1 pnpm typecheck
CI=1 RAYON_NUM_THREADS=2 NODE_OPTIONS=--max-old-space-size=8192 pnpm build --force --concurrency=1
pnpm test
```
`--force`는 Turbo cache를 우회하지만 TypeScript의 기존 `.tsbuildinfo`까지 지우지
않는다. 콜드 비교에서는 격리된 checkout의 generated dist와 `.tsbuildinfo` 조건도
맞추고, 같은 Node/pnpm, CPU·메모리 상한과 환경을 사용한다. 반복 빌드와 source
수정 후 빌드를 구분하고 cache hit 수, compiler 실패와 peak RSS를 함께 기록한다.
운영 적용은 로컬 측정과 별도다. frozen install, 실제 builder 메모리 제한, 배포
operation, 공개 prefix의 API·asset·Chromium과 기존 profile 상태를 확인한 뒤
완료로 기록한다.
+4 -1
View File
@@ -90,7 +90,10 @@ server build는 기존 Turbo cache를 재사용할 수 있지만, 자원 보호
source와 공개 build-time 값이 다른 frontend artifact를 cache hit로 잘못 복원하지
않습니다. Profile base path와 API/SSE URL은 공용 bundle 입력이 아니라 게시 시점의
runtime JSON config이므로 profile 사이에서 의도적으로 같은 cache key를 사용합니다.
일반 개발용 `pnpm --filter <frontend> build`의 typecheck 계약은 그대로입니다.
일반 개발용 `pnpm --filter <frontend> build`도 Vue 타입 검사를 한 번 실행합니다.
일반 Turbo build는 이 내장 검사와 별도 frontend `typecheck`를 중복 실행하지 않습니다.
Vue CLI는 [빌드 도구](build-performance.md)의 전용 bridge를 사용하고, server 선언 파일은
고정 native TypeScript가 파일별로 생성합니다. 선언·JS source map은 유지합니다.
`NODE_OPTIONS`와 `RAYON_NUM_THREADS`는 출력에는 영향을 주지 않는 resource 제한으로
build child에 전달됩니다.
- `TURN_DAEMON_NODE_OPTIONS`가 설정되어 있으면 Gateway orchestrator는 그 값을
+1 -1
View File
@@ -43,7 +43,7 @@ typecheck·unit·Chromium 검증을 실행한 것으로 설명하지 않습니
## 새 worktree 준비
Playwright 설정의 `webServer`는 frontend package의 `dev` 또는
`vue-tsc && vite build`를 직접 실행합니다. 이 경로는 root Turbo task가
Vue 타입 검사와 `vite build`를 직접 실행합니다. 이 경로는 root Turbo task가
보장하는 Prisma 생성과 upstream package build를 거치지 않으므로, 새 worktree에서
브라우저 test를 먼저 실행하면 제품 코드와 무관한 module/type 오류가 발생할 수
있습니다.
+2 -1
View File
@@ -56,6 +56,7 @@
"@types/node": "^26.2.0",
"@typescript-eslint/eslint-plugin": "^8.67.0",
"@typescript-eslint/parser": "^8.67.0",
"@typescript/native": "npm:typescript@7.0.2",
"eslint": "^10.8.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.6",
@@ -67,9 +68,9 @@
"prisma": "^7.10.0",
"tsdown": "^0.23.0",
"turbo": "^2.11.3",
"@typescript/native": "npm:typescript@7.0.2",
"typescript": "6.0.3",
"typescript-eslint": "^8.67.0",
"typescript-native-bridge": "6.0.3-bridge.18.tsgo.7.0.2",
"vitepress": "1.6.4",
"vue-eslint-parser": "^10.4.1"
}
+5 -1
View File
@@ -3,7 +3,11 @@
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"composite": false
"composite": false,
"paths": {
"@sammo-ts/common": ["./src/index.ts"],
"@sammo-ts/common/*": ["./src/*"]
}
},
"include": ["src"]
}
+3 -3
View File
@@ -1,4 +1,5 @@
import { defineConfig } from 'tsdown';
import { emitDeclarations } from '../../tools/build-scripts/emit-declarations.mjs';
export default defineConfig({
entry: {
@@ -18,9 +19,8 @@ export default defineConfig({
},
format: 'es',
outDir: 'dist',
dts: {
build: true,
},
dts: false,
hooks: { 'build:done': emitDeclarations },
sourcemap: true,
target: 'node22',
platform: 'node',
+5 -1
View File
@@ -3,7 +3,11 @@
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"composite": false
"composite": false,
"paths": {
"@sammo-ts/infra": ["./src/index.ts"],
"@sammo-ts/infra/*": ["./src/*"]
}
},
"include": ["src"]
}
+6 -3
View File
@@ -16,8 +16,11 @@
"noFallthroughCasesInSwitch": true,
"useUnknownInCatchVariables": true,
"noUnusedLocals": true,
"noUnusedParameters": true
"noUnusedParameters": true,
"paths": {
"@sammo-ts/logic": ["./src/index.ts"],
"@sammo-ts/logic/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "../common" }]
"include": ["src"]
}
+74
View File
@@ -241,6 +241,9 @@ importers:
typescript-eslint:
specifier: ^8.67.0
version: 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
typescript-native-bridge:
specifier: 6.0.3-bridge.18.tsgo.7.0.2
version: 6.0.3-bridge.18.tsgo.7.0.2
vitepress:
specifier: 1.6.4
version: 1.6.4(@algolia/client-search@5.56.0)(@types/node@26.2.0)(@types/react@19.2.18)(jiti@2.7.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(search-insights@2.17.3)(sortablejs@1.14.0)(tsx@4.23.12)(typescript@6.0.3)
@@ -2710,6 +2713,41 @@ packages:
resolution: {integrity: sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-native-bridge/darwin-arm64@6.0.3-bridge.18.tsgo.7.0.2':
resolution: {integrity: sha512-cuLVc0OVh1u4xyo9fQcJAxMca3MJqEJs3TSN0QhGPbYv5T4gYBiHSe69HWL7A27PyaSFuPMTgzN9RqoqlYALAw==}
cpu: [arm64]
os: [darwin]
'@typescript-native-bridge/darwin-x64@6.0.3-bridge.18.tsgo.7.0.2':
resolution: {integrity: sha512-Wj/DkI9BcWESykr2DhWqxxwhQBzaKaRcoRGxa67gD/VkJWpVkbbeLyxobjnNDF7e6xnGleHKAm/3fPKzfbqMWg==}
cpu: [x64]
os: [darwin]
'@typescript-native-bridge/linux-arm64@6.0.3-bridge.18.tsgo.7.0.2':
resolution: {integrity: sha512-4oxA689R+Ht1ikOCAGCIqIdZSk7orbNiSGV0eqGw7h9Q6+xrqYlnvcPtETayqLp6Q9gzEP3wCraDV9y+tSrA3Q==}
cpu: [arm64]
os: [linux]
'@typescript-native-bridge/linux-arm@6.0.3-bridge.18.tsgo.7.0.2':
resolution: {integrity: sha512-PuvMjc/pq41L28jUMpODJdknKSEqL1MfBT2mg8QiB1SAGTzuGd38Zsy1h6a+eGHqKU8M4laEZEHdkM39U54pTw==}
cpu: [arm]
os: [linux]
'@typescript-native-bridge/linux-x64@6.0.3-bridge.18.tsgo.7.0.2':
resolution: {integrity: sha512-XWAX1N0n1lV6pfMT0Bu7m12SN2iyMNCuNXD5LvjMzErzFR22goNNMG+91jm5HeCSNnZzLA6nvrHXv84/XAVi0g==}
cpu: [x64]
os: [linux]
'@typescript-native-bridge/win32-arm64@6.0.3-bridge.18.tsgo.7.0.2':
resolution: {integrity: sha512-agYZUX12eAPBFXP5mihuOQ1liH5Yrdt98uGe2kMY2+slinsFOdf1gjoGCHIykCZ8862Xtz92SkmIr9o2VDZRPw==}
cpu: [arm64]
os: [win32]
'@typescript-native-bridge/win32-x64@6.0.3-bridge.18.tsgo.7.0.2':
resolution: {integrity: sha512-nJu277PWZIb4/bmWN9ZREgZG+BwEVRcnnfVwpD5QI0n32G9ZgU7x0Q4PpvvxLmZz9xB4XcBAyoV3grw4RtRRNQ==}
cpu: [x64]
os: [win32]
'@typescript/typescript-aix-ppc64@7.0.2':
resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==}
engines: {node: '>=16.20.0'}
@@ -5021,6 +5059,11 @@ packages:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: 6.0.3
typescript-native-bridge@6.0.3-bridge.18.tsgo.7.0.2:
resolution: {integrity: sha512-otfFv4X9MRIBjwFAcPJYtr+S79xd/eE0nJjJ5CoU4KfYDh7+ve5nDfuu1tiGETPYUUnlPvWxGKDNqdv2w34ECg==}
engines: {node: '>=20.19'}
hasBin: true
typescript@6.0.3:
resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
engines: {node: '>=14.17'}
@@ -6906,6 +6949,27 @@ snapshots:
'@typescript-eslint/types': 8.67.0
eslint-visitor-keys: 5.0.1
'@typescript-native-bridge/darwin-arm64@6.0.3-bridge.18.tsgo.7.0.2':
optional: true
'@typescript-native-bridge/darwin-x64@6.0.3-bridge.18.tsgo.7.0.2':
optional: true
'@typescript-native-bridge/linux-arm64@6.0.3-bridge.18.tsgo.7.0.2':
optional: true
'@typescript-native-bridge/linux-arm@6.0.3-bridge.18.tsgo.7.0.2':
optional: true
'@typescript-native-bridge/linux-x64@6.0.3-bridge.18.tsgo.7.0.2':
optional: true
'@typescript-native-bridge/win32-arm64@6.0.3-bridge.18.tsgo.7.0.2':
optional: true
'@typescript-native-bridge/win32-x64@6.0.3-bridge.18.tsgo.7.0.2':
optional: true
'@typescript/typescript-aix-ppc64@7.0.2':
optional: true
@@ -9208,6 +9272,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
typescript-native-bridge@6.0.3-bridge.18.tsgo.7.0.2:
optionalDependencies:
'@typescript-native-bridge/darwin-arm64': 6.0.3-bridge.18.tsgo.7.0.2
'@typescript-native-bridge/darwin-x64': 6.0.3-bridge.18.tsgo.7.0.2
'@typescript-native-bridge/linux-arm': 6.0.3-bridge.18.tsgo.7.0.2
'@typescript-native-bridge/linux-arm64': 6.0.3-bridge.18.tsgo.7.0.2
'@typescript-native-bridge/linux-x64': 6.0.3-bridge.18.tsgo.7.0.2
'@typescript-native-bridge/win32-arm64': 6.0.3-bridge.18.tsgo.7.0.2
'@typescript-native-bridge/win32-x64': 6.0.3-bridge.18.tsgo.7.0.2
typescript@6.0.3: {}
typescript@7.0.2:
@@ -0,0 +1,3 @@
import type { BuildContext } from 'tsdown';
export declare const emitDeclarations: (context: BuildContext) => Promise<void>;
+39
View File
@@ -0,0 +1,39 @@
import { spawn } from 'node:child_process';
import path from 'node:path';
import { readFile, writeFile } from 'node:fs/promises';
// JS bundling stays in tsdown. Native TypeScript emits the complete declaration
// tree instead of repeatedly bundling large inferred Prisma/tRPC type graphs.
/**
* @param {import('tsdown').BuildContext} context
* @returns {Promise<void>}
*/
export const emitDeclarations = async ({ options }) => {
const manifest = JSON.parse(await readFile(path.join(options.cwd, 'package.json'), 'utf8'));
// logic's existing wildcard export exposes dist/src/*. Keep one canonical
// declaration tree so private class members retain their identity across
// root and subpath imports, and keep the wildcard JS entry points working.
const sourceExports = manifest.exports?.['./*'] === './dist/src/*';
const args = [
'-w',
'tsc7',
'-p',
path.join(options.cwd, 'tsconfig.build.json'),
...(sourceExports ? [] : ['--emitDeclarationOnly']),
'--types',
'node',
'--outDir',
sourceExports ? path.join(options.outDir, 'src') : options.outDir,
];
await new Promise((resolve, reject) => {
const child = spawn('pnpm', args, { cwd: options.cwd, stdio: 'inherit' });
child.once('error', reject);
child.once('close', (code, signal) => {
if (code === 0) resolve(undefined);
else reject(new Error(`Declaration emission failed (${signal ?? code ?? 'unknown'}): ${options.cwd}`));
});
});
if (sourceExports) {
await writeFile(path.join(options.outDir, 'index.d.ts'), "export * from './src/index.js';\n");
}
};
@@ -0,0 +1,73 @@
import assert from 'node:assert/strict';
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import { execFileSync } from 'node:child_process';
import { pathToFileURL } from 'node:url';
import { emitDeclarations } from './emit-declarations.mjs';
const workspaceRoot = path.resolve(import.meta.dirname, '../..');
test('native declarations preserve relative imports, subpaths and maps, and fail on type errors', async () => {
const temporaryRoot = path.join(workspaceRoot, '.turbo');
await mkdir(temporaryRoot, { recursive: true });
const fixture = await mkdtemp(path.join(temporaryRoot, 'declaration-test-'));
const outDir = path.join(fixture, 'dist');
try {
await mkdir(path.join(fixture, 'src'));
await writeFile(path.join(fixture, 'package.json'), JSON.stringify({ type: 'module' }));
await writeFile(
path.join(fixture, 'tsconfig.build.json'),
JSON.stringify({
extends: path.join(workspaceRoot, 'tsconfig.base.json'),
compilerOptions: { rootDir: 'src', outDir: 'dist' },
include: ['src'],
})
);
await writeFile(
path.join(fixture, 'src/model.ts'),
'export class Model { private readonly id = 1; getId(): number { return this.id; } }\n'
);
await writeFile(path.join(fixture, 'src/index.ts'), "export { Model } from './model.js';\n");
await emitDeclarations({ options: { cwd: fixture, outDir } });
assert.match(await readFile(path.join(outDir, 'index.d.ts'), 'utf8'), /from ['"]\.\/model\.js['"]/u);
assert.match(await readFile(path.join(outDir, 'model.d.ts'), 'utf8'), /getId\(\): number/u);
const map = JSON.parse(await readFile(path.join(outDir, 'model.d.ts.map'), 'utf8'));
assert.deepEqual(map.sources, ['../src/model.ts']);
await assert.rejects(readFile(path.join(outDir, 'index.js')), { code: 'ENOENT' });
await writeFile(
path.join(fixture, 'package.json'),
JSON.stringify({ type: 'module', exports: { './*': './dist/src/*' } })
);
await emitDeclarations({ options: { cwd: fixture, outDir } });
assert.equal(await readFile(path.join(outDir, 'index.d.ts'), 'utf8'), "export * from './src/index.js';\n");
const { Model } = await import(pathToFileURL(path.join(outDir, 'src/model.js')).href);
assert.equal(new Model().getId(), 1);
await readFile(path.join(outDir, 'src/model.d.ts.map'));
await readFile(path.join(outDir, 'src/model.js.map'));
await writeFile(
path.join(fixture, 'consumer.ts'),
"import { Model } from './dist/index.js';\n" +
"import { Model as SubpathModel } from './dist/src/model.js';\n" +
'export const same: Model = new SubpathModel();\n'
);
await writeFile(
path.join(fixture, 'tsconfig.consumer.json'),
JSON.stringify({ extends: path.join(workspaceRoot, 'tsconfig.base.json'), files: ['consumer.ts'] })
);
execFileSync(
'pnpm',
['-w', 'tsc7', '-p', path.join(fixture, 'tsconfig.consumer.json'), '--noEmit', '--types', 'node'],
{
cwd: fixture,
stdio: 'pipe',
}
);
await writeFile(path.join(fixture, 'src/model.ts'), 'export const id: number = "invalid";\n');
await assert.rejects(emitDeclarations({ options: { cwd: fixture, outDir } }), /Declaration emission failed/u);
} finally {
await rm(fixture, { recursive: true, force: true });
}
});
+1 -1
View File
@@ -7,7 +7,7 @@
"build": "pnpm -w tsc7 -p tools/build-scripts/tsconfig.json",
"dev": "node -e \"console.log('dev not configured')\"",
"lint": "node -e \"console.log('lint not configured')\"",
"test": "node -e \"console.log('test not configured')\"",
"test": "node --test *.test.mjs",
"typecheck": "pnpm -w tsc7 -b tools/build-scripts/tsconfig.json",
"build:server": "node ./build-server.mjs"
}
+8 -1
View File
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
const workspaceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const expectedApiVersion = '6.0.3';
const expectedCliVersion = '7.0.2';
const expectedVueBridgeVersion = '6.0.3-bridge.18.tsgo.7.0.2';
const consumers = [
['root', 'package.json'],
@@ -35,4 +36,10 @@ if (cliVersion !== expectedCliVersion) {
throw new Error(`native tsc resolved ${cliVersion}; expected ${expectedCliVersion}`);
}
console.log(`TypeScript API ${expectedApiVersion}; native tsc ${expectedCliVersion}`);
const requireFromRoot = createRequire(path.join(workspaceRoot, 'package.json'));
const vueBridgeVersion = requireFromRoot('typescript-native-bridge/package.json').version;
if (vueBridgeVersion !== expectedVueBridgeVersion) {
throw new Error(`Vue bridge resolved ${vueBridgeVersion}; expected ${expectedVueBridgeVersion}`);
}
console.log(`TypeScript API ${expectedApiVersion}; native tsc ${expectedCliVersion}; Vue bridge ${vueBridgeVersion}`);
+9
View File
@@ -0,0 +1,9 @@
import { createRequire } from 'node:module';
import path from 'node:path';
// Scope the bridge to the Vue CLI. Vite, ESLint and other compiler API
// consumers keep resolving the workspace's stock TypeScript 6 installation.
const requireFromFrontend = createRequire(path.resolve('package.json'));
const requireFromToolchain = createRequire(import.meta.url);
const { run } = requireFromFrontend('vue-tsc');
run(requireFromToolchain.resolve('typescript-native-bridge/lib/tsc.js'));
+3 -3
View File
@@ -1,4 +1,5 @@
import { defineConfig } from 'tsdown';
import { emitDeclarations } from './tools/build-scripts/emit-declarations.mjs';
export default defineConfig({
workspace: {
@@ -8,9 +9,8 @@ export default defineConfig({
entry: 'src/index.ts',
format: 'es',
outDir: 'dist',
dts: {
build: true
},
dts: false,
hooks: { 'build:done': emitDeclarations },
sourcemap: true,
target: 'node22',
platform: 'node',
+29 -4
View File
@@ -6,7 +6,15 @@
"build": {
"dependsOn": ["^build", "typecheck"],
"env": ["NODE_ENV"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"inputs": [
"$TURBO_DEFAULT$",
".env*",
"$TURBO_ROOT$/tsconfig*.json",
"$TURBO_ROOT$/tsdown.config.ts",
"$TURBO_ROOT$/tools/build-scripts/emit-declarations.mjs",
"$TURBO_ROOT$/tools/build-scripts/emit-declarations.d.mts",
"$TURBO_ROOT$/tools/vue-typecheck.mjs"
],
"outputs": ["dist/**"]
},
"build:release": {
@@ -27,13 +35,22 @@
"$TURBO_ROOT$/packages/logic/src/**",
"$TURBO_ROOT$/app/game-engine/src/**",
"$TURBO_ROOT$/app/game-api/src/**",
"$TURBO_ROOT$/app/gateway-api/src/**"
"$TURBO_ROOT$/app/gateway-api/src/**",
"$TURBO_ROOT$/tools/vue-typecheck.mjs"
],
"cache": true
"cache": true,
"env": ["!VITE_*"]
},
"typecheck": {
"dependsOn": ["^build", "prisma:generate"],
"cache": true
"cache": true,
"inputs": [
"$TURBO_DEFAULT$",
"$TURBO_ROOT$/tsconfig*.json",
"$TURBO_ROOT$/tools/build-scripts/emit-declarations.d.mts",
"$TURBO_ROOT$/tools/vue-typecheck.mjs"
],
"env": ["!VITE_*"]
},
"prisma:generate": {
"inputs": ["$TURBO_DEFAULT$"],
@@ -53,6 +70,14 @@
"dev": {
"cache": false,
"persistent": true
},
"@sammo-ts/game-frontend#build": {
"dependsOn": ["^build"],
"env": ["NODE_ENV", "VITE_*"]
},
"@sammo-ts/gateway-frontend#build": {
"dependsOn": ["^build"],
"env": ["NODE_ENV", "VITE_*"]
}
}
}