refactor(cli): simplify profile composition and dump paths
- composeProfile keeps layers as bundle/user/overlay+flags segments instead of one flat list later re-sliced by index arithmetic; the row index drops the group-walk (profile trees are flat patch compositions) and the double composition. - The config dump anchors on the profile's real empty root (written by the shared prepareProfile) instead of materializing a temp file, so dump and boot compose over the identical base by construction. - dsh-base drops its patchPath export: the dsh.patch manifest field is the one contract; the package carries no runtime API. - packageDirFromAnchor is paths-probe only (the require.resolve fast path duplicated the probe's outcome); basename() replaces hand-rolled path splitting; verify-cordis-config stops re-reading bundle manifests in-loop.
This commit is contained in:
@@ -6,17 +6,14 @@
|
||||
* @module @deepseek-ai/dsh/dump-config
|
||||
*/
|
||||
|
||||
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import {
|
||||
healProfilesModuleFallback,
|
||||
loadOverlayPatches,
|
||||
loadProfile,
|
||||
renderConfigDump,
|
||||
type ConfigDumpLayer,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { INSTALL_ANCHOR } from './profile-boot.ts'
|
||||
import { prepareProfile, PROFILE_ROOT_FILENAME } from './profile-boot.ts'
|
||||
|
||||
const NAME = 'dsh'
|
||||
|
||||
@@ -24,15 +21,13 @@ const NAME = 'dsh'
|
||||
/**
|
||||
* Print a profile composition with provenance comments.
|
||||
* @param profile - the profile name.
|
||||
* @param defaultOnly - omit the profile's user layer and `--patch` overlays.
|
||||
* @param defaultOnly - omit the profile's user layer and `--patch` overlays
|
||||
* (the recovery diagnostic for a broken `cordis.patch.yml`, which is then
|
||||
* never parsed).
|
||||
* @param patches - `--patch` overlay paths, in argv order.
|
||||
*/
|
||||
export function runDumpConfig(profile: string, defaultOnly: boolean, patches: readonly string[]): void {
|
||||
healProfilesModuleFallback(INSTALL_ANCHOR)
|
||||
// The default dump never reads the user layer: it doubles as the recovery
|
||||
// diagnostic for a broken cordis.patch.yml, so parsing that file here would
|
||||
// defeat its purpose.
|
||||
const loaded = loadProfile(NAME, profile, INSTALL_ANCHOR, undefined, { userLayer: !defaultOnly })
|
||||
const loaded = prepareProfile(profile, !defaultOnly)
|
||||
const layers: ConfigDumpLayer[] = loaded.layers.map(layer => ({
|
||||
label: layer.packageName,
|
||||
patches: layer.patches,
|
||||
@@ -46,15 +41,7 @@ export function runDumpConfig(profile: string, defaultOnly: boolean, patches: re
|
||||
layers.push({ label: absolute, patches: loadOverlayPatches(NAME, absolute) })
|
||||
}
|
||||
}
|
||||
// renderConfigDump anchors on a base entry-list file; a profile's base is
|
||||
// the empty list, materialized as a temp document.
|
||||
const emptyRoot = mkdtempSync(join(tmpdir(), 'dsh-dump-'))
|
||||
const emptyRootFile = join(emptyRoot, 'profile-root.yml')
|
||||
writeFileSync(emptyRootFile, '[]\n')
|
||||
try {
|
||||
process.stdout.write(renderConfigDump(NAME, emptyRootFile, layers))
|
||||
} finally {
|
||||
rmSync(emptyRoot, { recursive: true, force: true })
|
||||
}
|
||||
// The dump anchors on the same empty root file the boot includes.
|
||||
process.stdout.write(renderConfigDump(NAME, join(loaded.dir, PROFILE_ROOT_FILENAME), layers))
|
||||
}
|
||||
/* v8 ignore stop */
|
||||
@@ -44,7 +44,7 @@ const PROFILE_ROOT_CONFIG = `# dsh profile root — an empty entry list. The tre
|
||||
`
|
||||
|
||||
/** Root config filename inside a profile directory. */
|
||||
const PROFILE_ROOT_FILENAME = 'cordis.yml'
|
||||
export const PROFILE_ROOT_FILENAME = 'cordis.yml'
|
||||
|
||||
/**
|
||||
* Resolve the telemetry opt-out switch into its boot patch. ANY non-empty
|
||||
@@ -62,39 +62,54 @@ export function resolveTelemetryPatch(disabledEnv: string | undefined, hasRow: b
|
||||
return { id: TELEMETRY_ROW_ID, disabled: true }
|
||||
}
|
||||
|
||||
/** Load a resolved profile for `name`, healing the shared module fallback first. */
|
||||
function prepareProfile(name: string): Profile {
|
||||
/**
|
||||
* Load a resolved profile for `name`: heal the shared module fallback, then
|
||||
* (re)write the empty root config. The root is always rewritten: the whole
|
||||
* composition is patch layers, and the vendored Loader's tree write-back (a
|
||||
* plugin self-disposing persists the current tree) can bake composed rows
|
||||
* into this file — which would duplicate every bundle insert on the next
|
||||
* boot. The file exists on disk only because the Loader needs a real include
|
||||
* root to anchor `baseUrl` at the profile directory (the config dump anchors
|
||||
* on the same file, so both compose over the identical base).
|
||||
* @param name - the profile name.
|
||||
* @param userLayer - `false` skips parsing `cordis.patch.yml` (the default dump).
|
||||
* @returns the loaded profile.
|
||||
*/
|
||||
export function prepareProfile(name: string, userLayer = true): Profile {
|
||||
healProfilesModuleFallback(INSTALL_ANCHOR)
|
||||
const profile = loadProfile(NAME, name, INSTALL_ANCHOR)
|
||||
const rootConfig = join(profile.dir, PROFILE_ROOT_FILENAME)
|
||||
// The root is always rewritten to the empty list: the whole composition is
|
||||
// patch layers, and the vendored Loader's tree write-back (a plugin
|
||||
// self-disposing persists the current tree) can bake composed rows into
|
||||
// this file — which would duplicate every bundle insert on the next boot.
|
||||
// The file stays a real on-disk include root only because the Loader needs
|
||||
// one to anchor `baseUrl` at the profile directory.
|
||||
writeFileSync(rootConfig, PROFILE_ROOT_CONFIG)
|
||||
const profile = loadProfile(NAME, name, INSTALL_ANCHOR, undefined, { userLayer })
|
||||
writeFileSync(join(profile.dir, PROFILE_ROOT_FILENAME), PROFILE_ROOT_CONFIG)
|
||||
return profile
|
||||
}
|
||||
|
||||
/** One profile's full patch stack and the row index of its composed tree. */
|
||||
/** One profile's patch layers (application order) and the row index of its pre-flag composition. */
|
||||
interface ComposedProfile {
|
||||
profile: Profile
|
||||
/** Bundle + profile + --patch + flag layers, in application order. */
|
||||
patches: PatchOptions[]
|
||||
/** id → composed row (post-composition), for flag merges and row checks. */
|
||||
/** Bundle layers concatenated — the part below the user layer on a live reload. */
|
||||
bundlePatches: PatchOptions[]
|
||||
/** Layers above the user layer on a live reload: --patch overlays, flag patches, the telemetry switch. */
|
||||
overlayAndFlags: PatchOptions[]
|
||||
/**
|
||||
* id → row of the pre-flag composition (bundles + user layer + overlays),
|
||||
* for flag merges and row checks. Flag patches must not insert rows the
|
||||
* launcher consults here (they only override values and insert dev glue).
|
||||
*/
|
||||
rows: Map<string, { name?: string; config?: unknown }>
|
||||
}
|
||||
|
||||
/** The full patch stack of one composed profile, in application order. */
|
||||
function allPatches(composed: ComposedProfile): PatchOptions[] {
|
||||
return [...composed.bundlePatches, ...composed.profile.patches, ...composed.overlayAndFlags]
|
||||
}
|
||||
|
||||
/**
|
||||
* Load `name` and compose its effective patch stack. Flag patches derive from
|
||||
* the pre-flag composition (`deriveFlagPatches` receives the row index of
|
||||
* bundle + profile + overlay layers), then apply last, then the telemetry
|
||||
* switch.
|
||||
* Load `name` and compose its effective patch stack: bundle layers in
|
||||
* `dsh.plugins` order, the profile's user layer, `--patch` overlays, then
|
||||
* flag patches derived from the composed rows, then the telemetry switch.
|
||||
* @param name - the profile name.
|
||||
* @param patchFiles - `--patch` overlay paths, in argv order.
|
||||
* @param deriveFlagPatches - launcher hook turning composed rows into flag patches.
|
||||
* @returns the profile, its patch stack, and the composed row index (flags included).
|
||||
* @returns the profile, its patch layers, and the composed row index.
|
||||
*/
|
||||
function composeProfile(
|
||||
name: string,
|
||||
@@ -102,30 +117,16 @@ function composeProfile(
|
||||
deriveFlagPatches: (rows: ComposedProfile['rows']) => PatchOptions[] = () => [],
|
||||
): ComposedProfile {
|
||||
const profile = prepareProfile(name)
|
||||
const overlayLayers = patchFiles.map(file => loadOverlayPatches(NAME, resolve(file)))
|
||||
const layers = [
|
||||
...profile.layers.map(layer => layer.patches),
|
||||
profile.patches,
|
||||
...overlayLayers,
|
||||
]
|
||||
const indexRows = (composedEntries: { id?: string; name?: string; config?: unknown; group?: unknown }[]): ComposedProfile['rows'] => {
|
||||
const rows = new Map<string, { name?: string; config?: unknown }>()
|
||||
const walk = (entries: typeof composedEntries): void => {
|
||||
for (const row of entries) {
|
||||
if (typeof row.id === 'string') rows.set(row.id, row)
|
||||
if (row.group === true && Array.isArray(row.config)) walk(row.config as typeof composedEntries)
|
||||
}
|
||||
}
|
||||
walk(composedEntries)
|
||||
return rows
|
||||
const overlays = patchFiles.flatMap(file => loadOverlayPatches(NAME, resolve(file)))
|
||||
const bundlePatches = profile.layers.flatMap(layer => layer.patches)
|
||||
const rows = new Map<string, { name?: string; config?: unknown }>()
|
||||
for (const row of composeEntries([bundlePatches, profile.patches, overlays])) {
|
||||
if (typeof row.id === 'string') rows.set(row.id, row)
|
||||
}
|
||||
const flagPatches = deriveFlagPatches(indexRows(composeEntries(layers)))
|
||||
layers.push(flagPatches)
|
||||
const rows = indexRows(composeEntries(layers))
|
||||
const patches = layers.flat()
|
||||
const overlayAndFlags = [...overlays, ...deriveFlagPatches(rows)]
|
||||
const telemetryPatch = resolveTelemetryPatch(process.env.DSH_TELEMETRY_DISABLED, rows.has(TELEMETRY_ROW_ID))
|
||||
if (telemetryPatch !== undefined) patches.push(telemetryPatch)
|
||||
return { profile, patches, rows }
|
||||
if (telemetryPatch !== undefined) overlayAndFlags.push(telemetryPatch)
|
||||
return { profile, bundlePatches, overlayAndFlags, rows }
|
||||
}
|
||||
|
||||
/** Options for {@link runProfile}. */
|
||||
@@ -157,7 +158,7 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
+ '(the headless profile does)',
|
||||
)
|
||||
}
|
||||
composed.patches.push({ id: HEADLESS_ROW_ID, config: { task: options.task } })
|
||||
composed.overlayAndFlags.push({ id: HEADLESS_ROW_ID, config: { task: options.task } })
|
||||
} else if (composed.rows.has(HEADLESS_ROW_ID)) {
|
||||
// The inverse misuse: a one-shot composition booted without its task
|
||||
// would otherwise die in the runner row's schema with a raw "required"
|
||||
@@ -182,26 +183,22 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
const rootConfig = join(composed.profile.dir, PROFILE_ROOT_FILENAME)
|
||||
// Recomposition for the live profile layer: bundle layers below, overlays
|
||||
// and flag patches above, so a profile edit can never displace them.
|
||||
const overlayAndFlags = composed.patches.slice(
|
||||
composed.profile.layers.reduce((n, layer) => n + layer.patches.length, 0)
|
||||
+ composed.profile.patches.length,
|
||||
)
|
||||
// Fresh clones per generation: the include pushes `insert` rows into the
|
||||
// mounted tree BY REFERENCE and later id-targeted patches mutate those
|
||||
// objects in place. Reusing one parsed patch object across applications
|
||||
// would bake a user override into the bundle's in-memory insert row, so
|
||||
// removing the override could never revert the row to the bundle default.
|
||||
const composeLive = (profilePatches: PatchOptions[]): PatchOptions[] => structuredClone([
|
||||
...composed.profile.layers.flatMap(layer => layer.patches),
|
||||
...composed.bundlePatches,
|
||||
...profilePatches,
|
||||
...overlayAndFlags,
|
||||
...composed.overlayAndFlags,
|
||||
])
|
||||
// One-shot runs exit through the runner; watching would only hold the
|
||||
// process open after its exit request.
|
||||
const watchProfilePatch = options.task === undefined
|
||||
// Cloned for the same insert-aliasing reason as composeLive: the boot
|
||||
// application must not mutate the objects later reloads recompose from.
|
||||
const ctx = await boot(NAME, rootConfig, structuredClone(composed.patches), async (hostCtx) => {
|
||||
const ctx = await boot(NAME, rootConfig, structuredClone(allPatches(composed)), async (hostCtx) => {
|
||||
app.current = hostCtx
|
||||
if (options.task !== undefined) {
|
||||
const io: HeadlessIo = {
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# 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:
|
||||
# pnpm run verify-translation-pairing --write packages/bundle/base/README.md
|
||||
README.md: dd44e825f9a62c8b5e49a6af31c17b242a1927d7
|
||||
README.zh.md: 7227345591b5ddf6d27a88038074ed3541b01102
|
||||
README.md: 627dddc3808f67a2624e6e5b4d7f71c1617f227a
|
||||
README.zh.md: 84f48357d7b66df334d9f78eff64b0c7de3080e1
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The shared dsh core as a profile bundle: [`cordis.patch.yml`](cordis.patch.yml) inserts every base plugin row — model adapters, tools, persistence, policy, settings/credentials, repository Plugins, telemetry — over the empty profile root, as the first layer of every profile's `dsh.plugins` list. Later bundle layers (e.g. [`dsh-web-app`](../web-app/README.md)) and the user's profile `cordis.patch.yml` override these rows by id; a patch replaces a row's whole `config`, so mode-specific values live in mode bundles, not here. The package's TypeScript surface is a single `patchPath` convenience export; the profile composer resolves the patch through the `dsh.patch` manifest field, never through code.
|
||||
The shared dsh core as a profile bundle: [`cordis.patch.yml`](cordis.patch.yml) inserts every base plugin row — model adapters, tools, persistence, policy, settings/credentials, repository Plugins, telemetry — over the empty profile root, as the first layer of every profile's `dsh.plugins` list. Later bundle layers (e.g. [`dsh-web-app`](../web-app/README.md)) and the user's profile `cordis.patch.yml` override these rows by id; a patch replaces a row's whole `config`, so mode-specific values live in mode bundles, not here. The package has no runtime API; the profile composer resolves the patch through the `dsh.patch` manifest field, never through code.
|
||||
|
||||
The row set and its rationale are documented inline in the patch file; the [generated composition graph](../../../apps/cli/composition.md) renders it.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、工具、持久化、策略、settings/credentials、repository 插件、遥测——作为每个 profile 的 `dsh.plugins` 列表中的第一层。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行;patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包的 TypeScript 表层只有一个便利导出 `patchPath`;profile 组合器通过 manifest(元数据清单)的 `dsh.patch` 字段解析 patch,绝不通过代码。
|
||||
以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、工具、持久化、策略、settings/credentials、repository 插件、遥测——作为每个 profile 的 `dsh.plugins` 列表中的第一层。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行;patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 API;profile 组合器通过 manifest(元数据清单)的 `dsh.patch` 字段解析 patch,绝不通过代码。
|
||||
|
||||
行集合及其设计依据以行内注释写在 patch 文件里;[生成的组合图](../../../apps/cli/composition.md)负责渲染它。
|
||||
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
/**
|
||||
* @deepseek-ai/dsh-base — the shared dsh core as a profile bundle. The
|
||||
* package's substance is `cordis.patch.yml` (declared by the `dsh.patch`
|
||||
* manifest field): every profile's first patch layer, inserting the base
|
||||
* plugin rows over the empty profile root. This module only names the patch
|
||||
* for consumers that need the path programmatically (the profile composer
|
||||
* resolves it through the manifest field, not through this export).
|
||||
* package's substance is `cordis.patch.yml`, declared by the `dsh.patch`
|
||||
* manifest field and resolved by the profile composer through that field;
|
||||
* this module carries no runtime API.
|
||||
* @module @deepseek-ai/dsh-base
|
||||
*/
|
||||
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
/** Absolute path of this bundle's profile patch. */
|
||||
export const patchPath: string = fileURLToPath(new URL('../cordis.patch.yml', import.meta.url))
|
||||
export {}
|
||||
@@ -1,19 +1,21 @@
|
||||
/**
|
||||
* The bundle's substance is its patch file: the convenience export must point
|
||||
* at the real, parseable patch list the `dsh.patch` manifest field declares.
|
||||
* The bundle's substance is its patch file: the `dsh.patch` manifest field
|
||||
* must name a real, parseable patch list.
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import * as yaml from 'js-yaml'
|
||||
import { entryListSchema } from '@cordisjs/plugin-include'
|
||||
import { patchPath } from '../src/index.ts'
|
||||
|
||||
describe('dsh-base bundle', () => {
|
||||
it('exports the path of a parseable patch list matching the manifest declaration', () => {
|
||||
const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { dsh?: { patch?: string } }
|
||||
it('declares a parseable patch list through the dsh.patch manifest field', () => {
|
||||
const root = fileURLToPath(new URL('..', import.meta.url))
|
||||
const manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as { dsh?: { patch?: string } }
|
||||
expect(manifest.dsh?.patch).toBe('./cordis.patch.yml')
|
||||
const parsed = yaml.load(readFileSync(patchPath, 'utf8'), { schema: entryListSchema })
|
||||
const parsed = yaml.load(readFileSync(resolve(root, manifest.dsh!.patch!), 'utf8'), { schema: entryListSchema })
|
||||
expect(Array.isArray(parsed)).toBe(true)
|
||||
// The base layer is one insert list over the empty profile root.
|
||||
const rows = (parsed as { insert?: { id?: string }[] }[]).flatMap(patch => patch.insert ?? [])
|
||||
|
||||
@@ -25,7 +25,7 @@ import { createRequire } from 'node:module'
|
||||
import {
|
||||
existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync,
|
||||
} from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { basename, dirname, join } from 'node:path'
|
||||
import type { EntryOptions } from '@cordisjs/plugin-loader'
|
||||
import { applyEntryPatches, type PatchOptions } from '@cordisjs/plugin-include'
|
||||
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
|
||||
@@ -133,10 +133,7 @@ export function initProfile(dir: string, plugins: readonly string[]): void {
|
||||
const manifestPath = join(dir, 'package.json')
|
||||
if (!existsSync(manifestPath)) {
|
||||
const manifest: ProfileManifest & { private: boolean } = {
|
||||
// `dir` always carries at least one segment, so at(-1) cannot miss;
|
||||
// the fallback only satisfies the type.
|
||||
/* v8 ignore next */
|
||||
name: `dsh-profile-${join(dir).split(/[/\\]/).at(-1) ?? 'profile'}`,
|
||||
name: `dsh-profile-${basename(dir)}`,
|
||||
private: true,
|
||||
dependencies: {},
|
||||
dsh: { plugins: [...plugins] },
|
||||
@@ -267,21 +264,16 @@ export function writeProfileManifest(dir: string, manifest: ProfileManifest): vo
|
||||
|
||||
/**
|
||||
* Resolve a package's root directory from one anchor without depending on the
|
||||
* package exporting `./package.json`: probe the require resolution paths for
|
||||
* a directory holding the named manifest. This is Node's own lookup order, so
|
||||
* the result matches what the Loader would import from the same anchor.
|
||||
* package exporting `./package.json` (`require.resolve` would need that):
|
||||
* probe the require resolution paths for a directory holding the named
|
||||
* manifest. This is Node's own node_modules lookup order, so the result
|
||||
* matches what the Loader would import from the same anchor, and
|
||||
* `existsSync` follows the symlinks pnpm's isolated layout uses.
|
||||
*/
|
||||
function packageDirFromAnchor(anchor: string, packageName: string): string | undefined {
|
||||
const require = createRequire(anchor)
|
||||
// Fast path: the package exports its manifest (every in-box package does).
|
||||
try {
|
||||
return dirname(require.resolve(`${packageName}/package.json`))
|
||||
} catch {
|
||||
// Exports-encapsulated package — fall through to the paths probe.
|
||||
}
|
||||
// resolve.paths returns null only for builtins, which no bundle name is.
|
||||
/* v8 ignore next */
|
||||
for (const searchPath of require.resolve.paths(packageName) ?? []) {
|
||||
for (const searchPath of createRequire(anchor).resolve.paths(packageName) ?? []) {
|
||||
const candidate = join(searchPath, packageName)
|
||||
if (existsSync(join(candidate, 'package.json'))) return candidate
|
||||
}
|
||||
@@ -307,11 +299,9 @@ export function resolveBundleDir(
|
||||
const dir = packageDirFromAnchor(anchor, packageName)
|
||||
if (dir !== undefined) return dir
|
||||
}
|
||||
// profileDir always carries at least one segment; String() only satisfies the type.
|
||||
const profileName = String(join(profileDir).split(/[/\\]/).at(-1))
|
||||
throw new Error(
|
||||
`${binName}: cannot resolve profile bundle ${JSON.stringify(packageName)} from the dsh installation or ${profileDir}; `
|
||||
+ `run 'dsh plugin --profile ${profileName} install' if its dependency is not installed`,
|
||||
+ `run 'dsh plugin --profile ${basename(profileDir)} install' if its dependency is not installed`,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -166,12 +166,12 @@ function validateAppResolution(): string[] {
|
||||
// per-layer resolution anchors on the bundle package directory.
|
||||
for (const manifestPath of globSync('packages/bundle/*/package.json', { cwd: root })) {
|
||||
const bundleDir = manifestPath.replace(/\/package\.json$/, '')
|
||||
const dependencies = readManifest(manifestPath).dependencies ?? {}
|
||||
const manifest = readManifest(manifestPath)
|
||||
const references = pluginReferences.filter(reference => reference.file.startsWith(`${bundleDir}/`))
|
||||
violations.push(...missingPluginDependencies(
|
||||
// A bundle may mount its own package (the web-app runtime row).
|
||||
references.filter(reference => packageNameFromSpecifier(reference.name) !== readManifest(manifestPath).name),
|
||||
dependencies,
|
||||
references.filter(reference => packageNameFromSpecifier(reference.name) !== manifest.name),
|
||||
manifest.dependencies ?? {},
|
||||
manifestPath,
|
||||
))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user