mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
chore(workspace): enforce dependency declarations
This commit is contained in:
1 parent
756daf04bf
commit
c34f71ed2d
7 files changed
+249
-2
No files matched your search
@@ -58,6 +58,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Check workspace dependency declarations
|
||||
run: pnpm run check:workspace-deps
|
||||
|
||||
# 共享 node_modules 的 better-sqlite3 由 pnpm install 保持 Node ABI
|
||||
# (desktop 不再有 electron-rebuild postinstall),这里只需准备 CLI 专属副本。
|
||||
- name: Prepare better-sqlite3 for Node tests
|
||||
|
||||
@@ -31,6 +31,14 @@
|
||||
"undici": "^6.25.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openchatlab/config": "workspace:*",
|
||||
"@openchatlab/core": "workspace:*",
|
||||
"@openchatlab/http-routes": "workspace:*",
|
||||
"@openchatlab/node-runtime": "workspace:*",
|
||||
"@openchatlab/parser": "workspace:*",
|
||||
"@openchatlab/shared-types": "workspace:*",
|
||||
"@openchatlab/sync": "workspace:*",
|
||||
"@openchatlab/tools": "workspace:*",
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@electron/rebuild": "^4.0.4",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"type-check:node": "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p apps/desktop/tsconfig.node.json",
|
||||
"type-check:all": "pnpm run type-check:web && pnpm run type-check:node",
|
||||
"type-check": "vue-tsc --noEmit -p tsconfig.web.json",
|
||||
"check:workspace-deps": "node scripts/check-workspace-dependencies.mjs",
|
||||
"test": "node scripts/run-tests.mjs",
|
||||
"test:unit": "node scripts/run-tests.mjs",
|
||||
"test:auth": "tsx --test apps/cli/src/http/auth.test.ts src/services/utils/http.test.ts",
|
||||
@@ -63,6 +64,10 @@
|
||||
"vue-i18n": "^11.2.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openchatlab/core": "workspace:*",
|
||||
"@openchatlab/parser": "workspace:*",
|
||||
"@openchatlab/shared-types": "workspace:*",
|
||||
"@openchatlab/tools": "workspace:*",
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@nuxt/ui": "^4.9.0",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
|
||||
@@ -5,5 +5,8 @@
|
||||
"description": "ChatLab 平台无关的共享核心:抽象接口、查询逻辑、分析算法",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts"
|
||||
"types": "./src/index.ts",
|
||||
"dependencies": {
|
||||
"@openchatlab/shared-types": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@
|
||||
"@huggingface/transformers": "^4.2.0",
|
||||
"@openchatlab/config": "workspace:*",
|
||||
"@openchatlab/core": "workspace:*",
|
||||
"@openchatlab/parser": "workspace:*",
|
||||
"@openchatlab/shared-types": "workspace:*",
|
||||
"better-sqlite3": "^12.4.6",
|
||||
"gray-matter": "^4.0.3",
|
||||
"js-tiktoken": "^1.0.21",
|
||||
|
||||
Generated
+47
-1
@@ -42,6 +42,18 @@ importers:
|
||||
'@nuxt/ui':
|
||||
specifier: ^4.9.0
|
||||
version: 4.9.0(s6g7ddwkwqprfo6zxngcqjtvte)
|
||||
'@openchatlab/core':
|
||||
specifier: workspace:*
|
||||
version: link:packages/core
|
||||
'@openchatlab/parser':
|
||||
specifier: workspace:*
|
||||
version: link:packages/parser
|
||||
'@openchatlab/shared-types':
|
||||
specifier: workspace:*
|
||||
version: link:packages/shared-types
|
||||
'@openchatlab/tools':
|
||||
specifier: workspace:*
|
||||
version: link:packages/tools
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.0.0
|
||||
version: 4.1.18(vite@7.3.3(@types/node@25.2.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
@@ -231,6 +243,30 @@ importers:
|
||||
'@electron/rebuild':
|
||||
specifier: ^4.0.4
|
||||
version: 4.0.4
|
||||
'@openchatlab/config':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/config
|
||||
'@openchatlab/core':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/core
|
||||
'@openchatlab/http-routes':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/http-routes
|
||||
'@openchatlab/node-runtime':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/node-runtime
|
||||
'@openchatlab/parser':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/parser
|
||||
'@openchatlab/shared-types':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/shared-types
|
||||
'@openchatlab/sync':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/sync
|
||||
'@openchatlab/tools':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/tools
|
||||
'@types/better-sqlite3':
|
||||
specifier: ^7.6.13
|
||||
version: 7.6.13
|
||||
@@ -262,7 +298,11 @@ importers:
|
||||
specifier: ^3.24.4
|
||||
version: 3.25.76
|
||||
|
||||
packages/core: {}
|
||||
packages/core:
|
||||
dependencies:
|
||||
'@openchatlab/shared-types':
|
||||
specifier: workspace:*
|
||||
version: link:../shared-types
|
||||
|
||||
packages/http-routes:
|
||||
dependencies:
|
||||
@@ -337,6 +377,12 @@ importers:
|
||||
'@openchatlab/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
'@openchatlab/parser':
|
||||
specifier: workspace:*
|
||||
version: link:../parser
|
||||
'@openchatlab/shared-types':
|
||||
specifier: workspace:*
|
||||
version: link:../shared-types
|
||||
better-sqlite3:
|
||||
specifier: ^12.4.6
|
||||
version: 12.6.2
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import ts from 'typescript'
|
||||
|
||||
const repoRoot = path.resolve(import.meta.dirname, '..')
|
||||
const sourceExtensions = new Set(['.cjs', '.cts', '.js', '.jsx', '.mjs', '.mts', '.ts', '.tsx', '.vue'])
|
||||
const ignoredDirectories = new Set(['.docs', '.git', 'coverage', 'dist', 'dist-web', 'node_modules', 'out', 'target'])
|
||||
const ignoredPaths = new Set([
|
||||
path.join(repoRoot, 'apps', 'desktop', 'native'),
|
||||
path.join(repoRoot, 'docs', '.vitepress', 'cache'),
|
||||
])
|
||||
|
||||
function readJson(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
||||
}
|
||||
|
||||
function findWorkspaceManifests() {
|
||||
const pnpmCommand = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
|
||||
const result = spawnSync(pnpmCommand, ['-r', 'list', '--depth', '-1', '--json'], {
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8',
|
||||
})
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`Failed to list pnpm workspace projects: ${result.stderr.trim()}`)
|
||||
}
|
||||
return JSON.parse(result.stdout).map((project) => path.join(project.path, 'package.json'))
|
||||
}
|
||||
|
||||
function parseModuleSpecifiers(sourceText, filePath) {
|
||||
const scriptKind = filePath.endsWith('.tsx') || filePath.endsWith('.jsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS
|
||||
const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, scriptKind)
|
||||
const specifiers = new Set()
|
||||
|
||||
function addStringLiteral(node) {
|
||||
if (node && ts.isStringLiteralLike(node)) specifiers.add(node.text)
|
||||
}
|
||||
|
||||
function visit(node) {
|
||||
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
|
||||
addStringLiteral(node.moduleSpecifier)
|
||||
} else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) {
|
||||
addStringLiteral(node.moduleReference.expression)
|
||||
} else if (ts.isCallExpression(node) && node.arguments.length === 1) {
|
||||
const isDynamicImport = node.expression.kind === ts.SyntaxKind.ImportKeyword
|
||||
const isRequire = ts.isIdentifier(node.expression) && node.expression.text === 'require'
|
||||
if (isDynamicImport || isRequire) addStringLiteral(node.arguments[0])
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
|
||||
visit(sourceFile)
|
||||
return specifiers
|
||||
}
|
||||
|
||||
function extractModuleSpecifiers(filePath) {
|
||||
const sourceText = fs.readFileSync(filePath, 'utf8')
|
||||
if (!filePath.endsWith('.vue')) return parseModuleSpecifiers(sourceText, filePath)
|
||||
|
||||
const specifiers = new Set()
|
||||
const scriptBlockPattern = /<script\b[^>]*>([\s\S]*?)<\/script>/gi
|
||||
for (const match of sourceText.matchAll(scriptBlockPattern)) {
|
||||
for (const specifier of parseModuleSpecifiers(match[1], filePath)) specifiers.add(specifier)
|
||||
}
|
||||
return specifiers
|
||||
}
|
||||
|
||||
function collectSourceFiles(projectRoot, nestedProjectRoots) {
|
||||
const files = []
|
||||
|
||||
function walk(directory) {
|
||||
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
||||
const child = path.join(directory, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
if (ignoredDirectories.has(entry.name) || ignoredPaths.has(child) || nestedProjectRoots.has(child)) continue
|
||||
walk(child)
|
||||
} else if (entry.isFile() && sourceExtensions.has(path.extname(entry.name))) {
|
||||
files.push(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(projectRoot)
|
||||
return files
|
||||
}
|
||||
|
||||
function resolveWorkspacePackage(specifier, workspaceNames) {
|
||||
return workspaceNames.find((name) => specifier === name || specifier.startsWith(`${name}/`))
|
||||
}
|
||||
|
||||
function collectDeclaredDependencies(manifest) {
|
||||
return new Map(
|
||||
['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'].flatMap((field) =>
|
||||
Object.entries(manifest[field] ?? {}).map(([name, version]) => [name, { field, version }])
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function relativePath(filePath) {
|
||||
return path.relative(repoRoot, filePath) || '.'
|
||||
}
|
||||
|
||||
function auditWorkspaceDependencies() {
|
||||
const projects = findWorkspaceManifests().map((manifestPath) => ({
|
||||
manifestPath,
|
||||
root: path.dirname(manifestPath),
|
||||
manifest: readJson(manifestPath),
|
||||
}))
|
||||
const workspaceNames = projects
|
||||
.map((project) => project.manifest.name)
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => b.length - a.length)
|
||||
const projectRoots = new Set(projects.map((project) => project.root))
|
||||
const missing = []
|
||||
const invalidProtocols = []
|
||||
|
||||
for (const project of projects) {
|
||||
const declared = collectDeclaredDependencies(project.manifest)
|
||||
const nestedProjectRoots = new Set(
|
||||
[...projectRoots].filter((root) => root !== project.root && root.startsWith(`${project.root}${path.sep}`))
|
||||
)
|
||||
const references = new Map()
|
||||
|
||||
for (const filePath of collectSourceFiles(project.root, nestedProjectRoots)) {
|
||||
for (const specifier of extractModuleSpecifiers(filePath)) {
|
||||
const dependency = resolveWorkspacePackage(specifier, workspaceNames)
|
||||
if (!dependency || dependency === project.manifest.name) continue
|
||||
const files = references.get(dependency) ?? new Set()
|
||||
files.add(relativePath(filePath))
|
||||
references.set(dependency, files)
|
||||
}
|
||||
}
|
||||
|
||||
for (const [dependency, files] of references) {
|
||||
if (!declared.has(dependency)) {
|
||||
missing.push({ project: project.manifest.name, dependency, files: [...files].sort() })
|
||||
}
|
||||
}
|
||||
|
||||
for (const [dependency, declaration] of declared) {
|
||||
if (!workspaceNames.includes(dependency)) continue
|
||||
if (declaration.version !== 'workspace:*') {
|
||||
invalidProtocols.push({
|
||||
project: project.manifest.name,
|
||||
dependency,
|
||||
field: declaration.field,
|
||||
version: declaration.version,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { projects, missing, invalidProtocols }
|
||||
}
|
||||
|
||||
const result = auditWorkspaceDependencies()
|
||||
|
||||
if (result.missing.length === 0 && result.invalidProtocols.length === 0) {
|
||||
console.log(
|
||||
`[workspace-deps] Checked ${result.projects.length} projects; all direct workspace dependencies are declared.`
|
||||
)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
for (const issue of result.missing) {
|
||||
console.error(`[workspace-deps] ${issue.project} imports ${issue.dependency} but does not declare it:`)
|
||||
for (const file of issue.files.slice(0, 8)) console.error(` - ${file}`)
|
||||
if (issue.files.length > 8) console.error(` - ...and ${issue.files.length - 8} more`)
|
||||
}
|
||||
|
||||
for (const issue of result.invalidProtocols) {
|
||||
console.error(
|
||||
`[workspace-deps] ${issue.project} declares ${issue.dependency} in ${issue.field} as ${String(issue.version)}; use workspace:*.`
|
||||
)
|
||||
}
|
||||
|
||||
process.exitCode = 1
|
||||
Reference in new issue
Block a user