fix(scaffold): resolve the framework peer from this repository, not a registry

Two sites reached a registry for the vendored framework, which the rescope
turns from a silent second copy into a hard failure.

Live-link mode relinked only the root manifest, so a generated workspace member
— `plugins/*/package.json` — resolved its own dependencies from the registry
and installed upstream cordis beside this repository's vendored copy.
`LinkWorkspace.relinkNestedManifest()` relinks every nested generated manifest;
`peerDependencies` keeps its range because package managers reject a link spec
there.

The sandbox publish-path rehearsal installs this repository's vendored cordis
and cosmokit tarballs instead of naming a registry version.
This commit is contained in:
imccyu
2026-08-10 11:26:59 +08:00
parent 194828e8b8
commit 78e9b8bec5
4 changed files with 78 additions and 2 deletions

View File

@@ -134,4 +134,37 @@ export class LinkWorkspace {
? resolve(dirname(directory), directory.split(sep).at(-1) as string)
: undefined
}
/**
* Rewrite one nested generated manifest's local dependencies to live-link specs.
*
* A generated workspace member resolves its own dependencies, so every local
* name it declares must point into this repository as well: none of them —
* the harness packages or the rescoped framework — exists on a public
* registry, so a semver spec there fails the install outright.
* `peerDependencies` keeps its range because a peer states what the consumer
* must supply, and package managers reject a link spec in that section.
* @param projectRoot - Absolute root of the generated project.
* @param manifestPath - The nested manifest's project-relative POSIX path.
* @param text - The nested manifest's complete current text.
* @param manager - Package manager whose link-spec form applies.
* @returns The manifest text with every resolved local dependency relinked.
*/
relinkNestedManifest(projectRoot: string, manifestPath: string, text: string, manager: PackageManager): string {
const manifest = JSON.parse(text) as Record<string, unknown>
const manifestDirectory = resolve(canonicalPath(projectRoot), dirname(manifestPath))
let changed = false
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
const dependencies = manifest[section]
if (typeof dependencies !== 'object' || dependencies === null) continue
for (const [name] of Object.entries(dependencies as Record<string, string>)) {
const pkg = this.packages.get(name)
if (!pkg) continue
const relativePath = posixPath(relative(manifestDirectory, realpathSync(pkg.directory)))
;(dependencies as Record<string, string>)[name] = manager.linkSpec(relativePath)
changed = true
}
}
return changed ? `${JSON.stringify(manifest, null, 2)}\n` : text
}
}

View File

@@ -17,7 +17,7 @@ import type { ProjectResource } from '../features/resources.ts'
import { CordisYamlFile, type CordisConfigEntry } from '../documents/cordis-yaml-file.ts'
import { EnvFile } from '../documents/env-file.ts'
import { PackageJsonFile, type PackageManifest } from '../documents/package-json-file.ts'
import { ProjectFile } from '../documents/project-file.ts'
import { ProjectFile, TextProjectFile } from '../documents/project-file.ts'
import { TsConfigFile } from '../documents/tsconfig-file.ts'
import { featureId, type FeatureId, type ResourceKey } from '../ids.ts'
import { LinkWorkspace } from '../package-managers/link-workspace.ts'
@@ -288,6 +288,18 @@ export class ProjectEditSession implements FeatureProjectView {
this.profile.packageManager,
[...this.documents.values()],
)
// Generated workspace members resolve their own dependencies, so the root
// manifest's links are not enough: relink every nested manifest as well.
for (const [path, document] of this.documents) {
if (path === 'package.json' || !path.endsWith('/package.json')) continue
const relinked = workspace.relinkNestedManifest(
this.source.root,
path,
document.serialize(),
this.profile.packageManager,
)
this.documents.set(path, new TextProjectFile(path, relinked, document.originalText))
}
}
this.validateFinalState()
const changes = this.changes()