mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
build: enforce split project reference faces
This commit is contained in:
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||||
# after editing either side, bring the other along and re-record with:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-08-api-remotes-generated-contract-build.md
|
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-08-api-remotes-generated-contract-build.md
|
||||||
2026-08-08-api-remotes-generated-contract-build.md: 83848290400441f0272b220ed0d396570e1ce2dc
|
2026-08-08-api-remotes-generated-contract-build.md: 947465b19a7c399038ae8a3106f7563592365a8d
|
||||||
2026-08-08-api-remotes-generated-contract-build.zh.md: 4f9760078c209a22b9e03837fd81769e156b5df9
|
2026-08-08-api-remotes-generated-contract-build.zh.md: 7b559cd966c4acc055d41379c15f46fdfc649a00
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ packages/api/remotes/
|
|||||||
└─ index.ts
|
└─ index.ts
|
||||||
~~~
|
~~~
|
||||||
|
|
||||||
The package-root `tsconfig.json` is a solution that only references the two concrete projects; it enters neither aggregate nor any direct consumer's dependency graph. The root Host aggregate and `host/apiproxy` reference `api/remotes/tsconfig.host.json`, while the root Client aggregate and `client/ui-goal` reference `api/remotes/tsconfig.client.json`. `ui-goal` itself remains an ordinary single Client project.
|
The package-root `tsconfig.json` is a solution that only references the two concrete projects; it enters neither aggregate nor any direct consumer's dependency graph. The root Host aggregate and `host/apiproxy` reference `api/remotes/tsconfig.host.json`, while the root Client aggregate and `client/ui-goal` reference `api/remotes/tsconfig.client.json`. `ui-goal` itself remains an ordinary single Client project. The workspace constraints gate walks the reachable Project Reference graph and rejects any face-declared project that references a split package's solution root or opposite leaf; targets with only `tsconfig.json` remain valid from either face.
|
||||||
|
|
||||||
The two projects use disjoint `files` and separate `.tsbuildinfo` files, so they can share `lib/types` without emitting any source file twice. If both sides later need a shared implementation, move that implementation into a neutral package instead of giving the same source to two emitting projects.
|
The two projects use disjoint `files` and separate `.tsbuildinfo` files, so they can share `lib/types` without emitting any source file twice. If both sides later need a shared implementation, move that implementation into a neutral package instead of giving the same source to two emitting projects.
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ packages/api/remotes/
|
|||||||
└─ index.ts
|
└─ index.ts
|
||||||
~~~
|
~~~
|
||||||
|
|
||||||
包根 `tsconfig.json` 是只引用两个具体 project 的 solution,不进入任何 aggregate 或直接消费方的依赖图。根 Host aggregate 与 `host/apiproxy` 引用 `api/remotes/tsconfig.host.json`;根 Client aggregate 与 `client/ui-goal` 引用 `api/remotes/tsconfig.client.json`。`ui-goal` 本身仍是普通的单一 Client project。
|
包根 `tsconfig.json` 是只引用两个具体 project 的 solution,不进入任何 aggregate 或直接消费方的依赖图。根 Host aggregate 与 `host/apiproxy` 引用 `api/remotes/tsconfig.host.json`;根 Client aggregate 与 `client/ui-goal` 引用 `api/remotes/tsconfig.client.json`。`ui-goal` 本身仍是普通的单一 Client project。workspace constraints 门禁遍历可达的 Project Reference 图;凡已声明 face 的 project 引用了拆分包的 solution 根或另一侧 leaf,门禁都会拒绝,而只有 `tsconfig.json` 的目标仍可由任一 face 引用。
|
||||||
|
|
||||||
两个 project 使用互不重叠的 `files` 和不同的 `.tsbuildinfo`,因此可以共享 `lib/types` 而不重复发射任何源码。若未来需要两侧共用一份实现,应把实现移入中立 package,不能把同一源码同时交给两个 emitting project。
|
两个 project 使用互不重叠的 `files` 和不同的 `.tsbuildinfo`,因此可以共享 `lib/types` 而不重复发射任何源码。若未来需要两侧共用一份实现,应把实现移入中立 package,不能把同一源码同时交给两个 emitting project。
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||||
# after editing either side, bring the other along and re-record with:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write docs/development.md
|
# pnpm run verify-translation-pairing --write docs/development.md
|
||||||
development.md: acf279d182ca580c6e372be6fbdca8f46afdc445
|
development.md: 60a7ccc87e2c33e66b3d966a2907d31bb0b1efd8
|
||||||
development.zh.md: 927c72be2de78f9e7f67565b9524db85c5aa1669
|
development.zh.md: 6607705be7e9f548b4f44555ad8c6cc8c2d34964
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ Host and Client stay two aggregate programs because both sides declaration-merge
|
|||||||
- A script that builds a repo-wide `ts.Program` seeds `tsconfig.host.json` or `tsconfig.client.json` explicitly — never the root solution, because flattening both aggregates into one program collides the `Context` merges.
|
- A script that builds a repo-wide `ts.Program` seeds `tsconfig.host.json` or `tsconfig.client.json` explicitly — never the root solution, because flattening both aggregates into one program collides the `Context` merges.
|
||||||
- A new package is registered in exactly one aggregate. Having both a Node loader entry and a browser entry is not a reason to split a package; an ordinary Client plugin produces both runtime artifacts during the Client build phase.
|
- A new package is registered in exactly one aggregate. Having both a Node loader entry and a browser entry is not a reason to split a package; an ordinary Client plugin produces both runtime artifacts during the Client build phase.
|
||||||
|
|
||||||
`api/remotes` is the repository's only package with split Host and Client tsconfigs. Its Host entry must participate in the Host TypeRT graph, while its Client entry imports `/remote` declarations that Host tsdown must generate first. The package-root `tsconfig.json` is therefore only a solution, and the two aggregates and direct consumers reference `tsconfig.host.json` or `tsconfig.client.json` respectively. Do not copy this structure to other packages; see the [`api-remotes` README](../packages/api/remotes/README.md) for the complete boundary.
|
`api/remotes` is the repository's only package with split Host and Client tsconfigs. Its Host entry must participate in the Host TypeRT graph, while its Client entry imports `/remote` declarations that Host tsdown must generate first. The package-root `tsconfig.json` is therefore only a solution, and the two aggregates and direct consumers reference `tsconfig.host.json` or `tsconfig.client.json` respectively. The workspace `constraints` gate walks the reachable Project Reference graph and checks each referencing project's own compiler face: a single-config target remains valid from either face, while a split target must name the matching leaf rather than its solution root or opposite leaf. Do not copy this structure to other packages; see the [`api-remotes` README](../packages/api/remotes/README.md) for the complete boundary.
|
||||||
|
|
||||||
The root build follows the generated dependency order:
|
The root build follows the generated dependency order:
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ Host 与 Client 保持两个 aggregate program,是因为两侧在相同键下
|
|||||||
- 构造全仓 `ts.Program` 的脚本显式种子 `tsconfig.host.json` 或 `tsconfig.client.json`——永不种子根 solution,因为把两个 aggregate 展平进一个 program 会撞上 `Context` 合并冲突。
|
- 构造全仓 `ts.Program` 的脚本显式种子 `tsconfig.host.json` 或 `tsconfig.client.json`——永不种子根 solution,因为把两个 aggregate 展平进一个 program 会撞上 `Context` 合并冲突。
|
||||||
- 新 package 只登记进一个 aggregate。包同时具有 Node loader 入口和 browser 入口并不构成拆分理由;普通 Client plugin 的两份运行时产物都在 Client 构建阶段生成。
|
- 新 package 只登记进一个 aggregate。包同时具有 Node loader 入口和 browser 入口并不构成拆分理由;普通 Client plugin 的两份运行时产物都在 Client 构建阶段生成。
|
||||||
|
|
||||||
`api/remotes` 是唯一拆分 Host/Client tsconfig 的仓库特例。它的 Host 入口必须进入 Host TypeRT 图,而 Client 入口导入 Host tsdown 才会生成的 `/remote` 声明,因此本包根 `tsconfig.json` 只作为 solution,两个 aggregate 和直接消费方分别引用 `tsconfig.host.json` 或 `tsconfig.client.json`。不要把该结构推广到其他包;完整边界见 [`api-remotes` README](../packages/api/remotes/README.md)。
|
`api/remotes` 是唯一拆分 Host/Client tsconfig 的仓库特例。它的 Host 入口必须进入 Host TypeRT 图,而 Client 入口导入 Host tsdown 才会生成的 `/remote` 声明,因此本包根 `tsconfig.json` 只作为 solution,两个 aggregate 和直接消费方分别引用 `tsconfig.host.json` 或 `tsconfig.client.json`。workspace `constraints` 门禁遍历可达的 Project Reference 图,并按各引用 project 自身的 compiler face 检查:只有单一配置的目标可由任一 face 引用,拆分配置的目标则必须引用匹配的 leaf,不得引用 solution 根或另一侧 leaf。不要把该结构推广到其他包;完整边界见 [`api-remotes` README](../packages/api/remotes/README.md)。
|
||||||
|
|
||||||
根构建按生成依赖排序:
|
根构建按生成依赖排序:
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
||||||
import { join, relative, resolve } from 'node:path'
|
import { join, relative, resolve } from 'node:path'
|
||||||
import { hasTypeRTRemoteNavigation, isForbiddenPublicationFile } from './publication-payload.ts'
|
import { hasTypeRTRemoteNavigation, isForbiddenPublicationFile } from './publication-payload.ts'
|
||||||
|
import { collectProjectReferenceFaceViolations } from './project-reference-faces.ts'
|
||||||
|
|
||||||
const root = resolve(import.meta.dirname, '..')
|
const root = resolve(import.meta.dirname, '..')
|
||||||
// vendor/* is single-level; packages/<group>/<pkg> nests one level deeper
|
// vendor/* is single-level; packages/<group>/<pkg> nests one level deeper
|
||||||
@@ -305,6 +306,7 @@ const errors = [
|
|||||||
...checkRepositoryVersion(),
|
...checkRepositoryVersion(),
|
||||||
...workspaceManifests().flatMap(checkWorkspace),
|
...workspaceManifests().flatMap(checkWorkspace),
|
||||||
...checkHierarchyShape(),
|
...checkHierarchyShape(),
|
||||||
|
...collectProjectReferenceFaceViolations(root),
|
||||||
]
|
]
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
console.error(errors.join('\n'))
|
console.error(errors.join('\n'))
|
||||||
|
|||||||
100
scripts/project-reference-faces.spec.ts
Normal file
100
scripts/project-reference-faces.spec.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
import { afterEach, describe, expect, it } from 'vitest'
|
||||||
|
import { collectProjectReferenceFaceViolations } from './project-reference-faces.ts'
|
||||||
|
|
||||||
|
const roots: string[] = []
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
function writeJson(path: string, value: unknown): void {
|
||||||
|
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function workspaceFixture(options: {
|
||||||
|
readonly host: readonly string[]
|
||||||
|
readonly client: readonly string[]
|
||||||
|
}): string {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'dsh-project-reference-faces-'))
|
||||||
|
roots.push(root)
|
||||||
|
const shared = join(root, 'packages/core/shared')
|
||||||
|
const split = join(root, 'packages/api/split')
|
||||||
|
mkdirSync(shared, { recursive: true })
|
||||||
|
mkdirSync(split, { recursive: true })
|
||||||
|
writeJson(join(root, 'tsconfig.base.json'), {})
|
||||||
|
writeJson(join(root, 'tsconfig.base.client.json'), { extends: './tsconfig.base.json' })
|
||||||
|
writeJson(join(shared, 'package.json'), { name: '@deepseek-ai/dsh-shared' })
|
||||||
|
writeJson(join(shared, 'tsconfig.json'), {
|
||||||
|
extends: '../../../tsconfig.base.json',
|
||||||
|
references: [],
|
||||||
|
})
|
||||||
|
writeJson(join(split, 'package.json'), { name: '@deepseek-ai/dsh-split' })
|
||||||
|
writeJson(join(split, 'tsconfig.json'), {
|
||||||
|
files: [],
|
||||||
|
references: [{ path: './tsconfig.host.json' }, { path: './tsconfig.client.json' }],
|
||||||
|
})
|
||||||
|
writeJson(join(split, 'tsconfig.host.json'), { references: [{ path: '../../core/shared' }] })
|
||||||
|
writeJson(join(split, 'tsconfig.client.json'), { references: [{ path: '../../core/shared' }] })
|
||||||
|
writeJson(join(root, 'tsconfig.host.json'), {
|
||||||
|
references: options.host.map(path => ({ path })),
|
||||||
|
})
|
||||||
|
writeJson(join(root, 'tsconfig.client.json'), {
|
||||||
|
references: options.client.map(path => ({ path })),
|
||||||
|
})
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Project Reference compiler faces', () => {
|
||||||
|
it('allows neutral projects in either graph and matching split leaves', () => {
|
||||||
|
const root = workspaceFixture({
|
||||||
|
host: ['./packages/core/shared', './packages/api/split/tsconfig.host.json'],
|
||||||
|
client: ['./packages/core/shared', './packages/api/split/tsconfig.client.json'],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(collectProjectReferenceFaceViolations(root)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects the opposite leaf and the solution root of a split project', () => {
|
||||||
|
const root = workspaceFixture({
|
||||||
|
host: [
|
||||||
|
'./packages/api/split/tsconfig.host.json',
|
||||||
|
'./packages/api/split/tsconfig.client.json',
|
||||||
|
],
|
||||||
|
client: ['./packages/api/split'],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(collectProjectReferenceFaceViolations(root)).toEqual([
|
||||||
|
'tsconfig.client.json: Project Reference "./packages/api/split" enters split project packages/api/split from a Client config; reference "packages/api/split/tsconfig.client.json" instead',
|
||||||
|
'tsconfig.host.json: Project Reference "./packages/api/split/tsconfig.client.json" enters split project packages/api/split from a Host config; reference "packages/api/split/tsconfig.host.json" instead',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses the referencing project face throughout the reachable graph', () => {
|
||||||
|
const root = workspaceFixture({
|
||||||
|
host: ['./packages/core/host-consumer'],
|
||||||
|
client: ['./packages/core/client-consumer'],
|
||||||
|
})
|
||||||
|
const hostConsumer = join(root, 'packages/core/host-consumer')
|
||||||
|
mkdirSync(hostConsumer, { recursive: true })
|
||||||
|
writeJson(join(hostConsumer, 'package.json'), { name: '@deepseek-ai/dsh-host-consumer' })
|
||||||
|
writeJson(join(hostConsumer, 'tsconfig.json'), {
|
||||||
|
extends: '../../../tsconfig.base.json',
|
||||||
|
references: [{ path: '../../api/split/tsconfig.client.json' }],
|
||||||
|
})
|
||||||
|
const clientConsumer = join(root, 'packages/core/client-consumer')
|
||||||
|
mkdirSync(clientConsumer, { recursive: true })
|
||||||
|
writeJson(join(clientConsumer, 'package.json'), { name: '@deepseek-ai/dsh-client-consumer' })
|
||||||
|
writeJson(join(clientConsumer, 'tsconfig.json'), {
|
||||||
|
extends: '../../../tsconfig.base.client.json',
|
||||||
|
references: [{ path: '../../api/split/tsconfig.host.json' }],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(collectProjectReferenceFaceViolations(root)).toEqual([
|
||||||
|
'packages/core/client-consumer/tsconfig.json: Project Reference "../../api/split/tsconfig.host.json" enters split project packages/api/split from a Client config; reference "packages/api/split/tsconfig.client.json" instead',
|
||||||
|
'packages/core/host-consumer/tsconfig.json: Project Reference "../../api/split/tsconfig.client.json" enters split project packages/api/split from a Host config; reference "packages/api/split/tsconfig.host.json" instead',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
129
scripts/project-reference-faces.ts
Normal file
129
scripts/project-reference-faces.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/** Validate compiler-face isolation across workspace Project Reference graphs. */
|
||||||
|
|
||||||
|
import { existsSync, globSync } from 'node:fs'
|
||||||
|
import { basename, dirname, isAbsolute, relative, resolve, sep } from 'node:path'
|
||||||
|
import ts from 'typescript'
|
||||||
|
|
||||||
|
type ProjectFace = 'host' | 'client'
|
||||||
|
|
||||||
|
interface ProjectReferenceConfig {
|
||||||
|
readonly extends?: unknown
|
||||||
|
readonly references?: ReadonlyArray<{ readonly path?: unknown }>
|
||||||
|
}
|
||||||
|
|
||||||
|
const WORKSPACE_MANIFESTS = [
|
||||||
|
'packages/*/*/package.json',
|
||||||
|
'apps/*/package.json',
|
||||||
|
'vendor/*/package.json',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find references that enter the wrong leaf of a split Host/Client project.
|
||||||
|
*
|
||||||
|
* A single-config project is neutral and may participate in either graph. Once
|
||||||
|
* a package declares both face configs, every reachable reference must name
|
||||||
|
* the leaf matching the aggregate from which traversal began.
|
||||||
|
*
|
||||||
|
* @param root - Repository root containing both aggregate tsconfigs.
|
||||||
|
* @returns Repo-relative diagnostics for every mismatched reference edge.
|
||||||
|
*/
|
||||||
|
export function collectProjectReferenceFaceViolations(root: string): string[] {
|
||||||
|
const splitRoots = splitProjectRoots(root)
|
||||||
|
const violations: string[] = []
|
||||||
|
const pending = [resolve(root, 'tsconfig.host.json'), resolve(root, 'tsconfig.client.json')]
|
||||||
|
const visited = new Set<string>()
|
||||||
|
for (let configPath = pending.pop(); configPath !== undefined; configPath = pending.pop()) {
|
||||||
|
if (visited.has(configPath) || !existsSync(configPath)) continue
|
||||||
|
visited.add(configPath)
|
||||||
|
const config = projectConfig(root, configPath)
|
||||||
|
const face = projectFace(root, configPath, config)
|
||||||
|
for (const reference of projectReferences(config)) {
|
||||||
|
const targetConfig = referenceConfigPath(configPath, reference)
|
||||||
|
const splitRoot = containingSplitRoot(splitRoots, targetConfig)
|
||||||
|
if (splitRoot !== undefined) {
|
||||||
|
if (face === undefined) {
|
||||||
|
violations.push(
|
||||||
|
`${repoPath(root, configPath)}: Project Reference ${JSON.stringify(reference)} enters split project ${repoPath(root, splitRoot)} from a config with no Host/Client face`,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const expected = resolve(splitRoot, `tsconfig.${face}.json`)
|
||||||
|
if (targetConfig !== expected) {
|
||||||
|
violations.push(
|
||||||
|
`${repoPath(root, configPath)}: Project Reference ${JSON.stringify(reference)} enters split project ${repoPath(root, splitRoot)} from a ${faceLabel(face)} config; reference ${JSON.stringify(repoPath(root, expected))} instead`,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pending.push(targetConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return violations.sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitProjectRoots(root: string): string[] {
|
||||||
|
return globSync(WORKSPACE_MANIFESTS, { cwd: root })
|
||||||
|
.map(manifest => resolve(root, dirname(manifest)))
|
||||||
|
.filter(dir => existsSync(resolve(dir, 'tsconfig.host.json'))
|
||||||
|
&& existsSync(resolve(dir, 'tsconfig.client.json')))
|
||||||
|
.sort((left, right) => right.length - left.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
function projectConfig(root: string, configPath: string): ProjectReferenceConfig {
|
||||||
|
const read = ts.readConfigFile(configPath, path => ts.sys.readFile(path))
|
||||||
|
if (read.error !== undefined) {
|
||||||
|
const message = ts.flattenDiagnosticMessageText(read.error.messageText, '\n')
|
||||||
|
throw new Error(`${repoPath(root, configPath)}: ${message}`)
|
||||||
|
}
|
||||||
|
return read.config as ProjectReferenceConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
function projectReferences(config: ProjectReferenceConfig): string[] {
|
||||||
|
return (config.references ?? [])
|
||||||
|
.map(reference => reference.path)
|
||||||
|
.filter((path): path is string => typeof path === 'string')
|
||||||
|
}
|
||||||
|
|
||||||
|
function projectFace(
|
||||||
|
root: string,
|
||||||
|
configPath: string,
|
||||||
|
config: ProjectReferenceConfig,
|
||||||
|
seen = new Set<string>(),
|
||||||
|
): ProjectFace | undefined {
|
||||||
|
if (basename(configPath) === 'tsconfig.host.json') return 'host'
|
||||||
|
if (basename(configPath) === 'tsconfig.client.json') return 'client'
|
||||||
|
if (configPath === resolve(root, 'tsconfig.base.json')) return 'host'
|
||||||
|
if (configPath === resolve(root, 'tsconfig.base.client.json')) return 'client'
|
||||||
|
if (seen.has(configPath)) return undefined
|
||||||
|
seen.add(configPath)
|
||||||
|
const parent = localExtendsConfig(configPath, config.extends)
|
||||||
|
if (parent === undefined || !existsSync(parent)) return undefined
|
||||||
|
return projectFace(root, parent, projectConfig(root, parent), seen)
|
||||||
|
}
|
||||||
|
|
||||||
|
function localExtendsConfig(configPath: string, value: unknown): string | undefined {
|
||||||
|
if (typeof value !== 'string' || !value.startsWith('.')) return undefined
|
||||||
|
const target = resolve(dirname(configPath), value)
|
||||||
|
return target.endsWith('.json') ? target : `${target}.json`
|
||||||
|
}
|
||||||
|
|
||||||
|
function referenceConfigPath(sourceConfig: string, reference: string): string {
|
||||||
|
const target = resolve(dirname(sourceConfig), reference)
|
||||||
|
return target.endsWith('.json') ? target : resolve(target, 'tsconfig.json')
|
||||||
|
}
|
||||||
|
|
||||||
|
function containingSplitRoot(splitRoots: readonly string[], targetConfig: string): string | undefined {
|
||||||
|
return splitRoots.find((root) => {
|
||||||
|
const path = relative(root, targetConfig)
|
||||||
|
return path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function repoPath(root: string, path: string): string {
|
||||||
|
return relative(root, path).split(sep).join('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
function faceLabel(face: ProjectFace): string {
|
||||||
|
return face === 'host' ? 'Host' : 'Client'
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user