Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/link-finder-declared-location-axis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@objectstack/types': minor
---

Host importer: a `link:` / `file:` install is now verified by the LOCATION the app declared, so a correctly linked package loads instead of being refused.

The ESM fallback finder (`createHostImporter`) verifies the one directory it consults — `<hostRoot>/node_modules/<key>` — against what the host's own `package.json` declares. Until now it could only do that by NAME, and a `link:` / `file:` value promises no name, so the KEY stood in for one: a package linked exactly as the app asked, whose own manifest happens to be named something else, was refused with `declared-unresolvable` / `MODULE_NOT_FOUND`. Nothing was broken, and the only way out was to stop using a supported linking mode.

Such a declaration does name something checkable — a directory — so the finder now checks that too: `realpath(node_modules/<key>)` against `realpath(resolve(hostRoot, <declared path>))`, both sides canonicalised, compared exactly (no basename matching, no case folding). If they are the same directory, the host declared it and it loads.

This is a second verification axis, not a looser first one. A directory the app declared neither by name nor by path is refused exactly as before, and the finder stays strictly tighter than the CommonJS resolution it backs up, which asks neither question. Unchanged: a plain version range licenses no path; an `npm:` alias is still checked by name; `github:` / tarball URLs and the bare `owner/repo` shorthand name no on-disk location, so they gain nothing; a package that publishes a `require` condition never reaches this fallback at all, so no load that succeeds today changes.

Measured on pnpm 10.33: `link:` symlinks the key at the declared directory and verifies; a `file:` directory install routes through pnpm's virtual store (a copy), so it does not, and keeps today's refusal. The refusal's text now states what the location check compared instead of asserting a limit the finder no longer has.
9 changes: 9 additions & 0 deletions .changeset/serve-org-remedy-defers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@objectstack/cli': patch
---

`serve`: the multi-org runtime's stage-1 refusal no longer prints its own install remedy for a `declared-unresolvable` failure — it defers to the importer's message, which the same refusal already prints as its `cause:` line.

Driven on both shapes that kind covers, the minted bullet ("Repair the INSTALL … run `pnpm install`, check that a production prune did not drop it, and that its dist is actually built") was wrong twice over. For a genuinely broken install it repeated, word for word, the three remedies the cause line four lines below already carried. For a location install the finder cannot tie to the declaration, the cause says outright that re-running `pnpm install`, un-pruning a deploy and rebuilding a dist all change nothing — so one screen contradicted itself.

The arm now says only what it uniquely knows (the app DOES declare the package, so re-reading `package.json` will not help) and names the cause as the authority on the remedy — the same deferral the `declared-no-loadable-entry` arm has had since it landed.
22 changes: 20 additions & 2 deletions content/docs/deployment/tenancy-modes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,26 @@ So, three ways out:
- **set `OS_ALLOW_DEGRADED_TENANCY=1`** to boot anyway in an explicitly degraded
single-org state.

If the package is already declared but the install is broken, the message says so
and tells you to repair the install rather than re-read `package.json`.
If the package is already declared, the remedy bullet says only that — the
declaration is not the problem — and hands the remedy to the `cause:` line
below it, which is the one thing that measured what actually went wrong. There
are two shapes of that, and they want opposite actions:

- **the install is broken** — never installed, pruned away by a filtered deploy,
or pointing at a dist that was never built. The cause names all three and
tells you to repair the install rather than re-read `package.json`.
- **the install is fine and the runtime still could not be verified** — the app
declares the package under a `link:` / `file:` path, or a git or tarball URL,
and the package sitting at that key carries a different `name` in its own
`package.json`. This is neither an install nor a declaration fault, and the
cause says so outright: re-running `pnpm install` changes nothing. A `link:`
install pointing at exactly the directory the declaration names is verified by
that path and loads normally; what reaches this message is the residue where
neither the name nor the path ties the directory to the declaration — a
`file:` directory install under pnpm (which lands in the virtual store rather
than at the declared path), or a URL that names no on-disk location at all.
The remedy the cause prints is to make the two names agree, from whichever end
you own.

### A present-but-refusing runtime is a different failure

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,20 +157,49 @@ const DECLARED: HostDeclaration = {
};
const UNDECLARED: HostDeclaration = { packageName: PKG, hostRoot: '/srv/app', declared: false };

const remedyDeclared = () => formatOrganizationsInstallRemedy('declared-unresolvable', DECLARED, '/srv/app');
const remedyUndeclared = () => formatOrganizationsInstallRemedy('undeclared', UNDECLARED, '/srv/app');
const remedyNoLoadableEntry = () =>
formatOrganizationsInstallRemedy('declared-no-loadable-entry', DECLARED, '/srv/app');
const remedyDeclared = () => formatOrganizationsInstallRemedy('declared-unresolvable', DECLARED);
const remedyUndeclared = () => formatOrganizationsInstallRemedy('undeclared', UNDECLARED);
const remedyNoLoadableEntry = () => formatOrganizationsInstallRemedy('declared-no-loadable-entry', DECLARED);

describe('serve — the multi-org runtime name an operator READS comes from the declaration (#12151)', () => {
it('site 1 — the "install is broken" remedy names it, with the spacing intact', () => {
// The `declared-unresolvable` branch: the app's package.json IS correct and
// the install is what broke (#4719). One of the two an operator ACTS on.
it('site 1 — the "the declaration is not the problem" remedy names it, with the spacing intact', () => {
// The `declared-unresolvable` branch: the app's package.json IS correct
// (#4719). One of the two an operator ACTS on.
expect(lines(remedyDeclared())[0]).toBe(
` • this app DECLARES ${PKG} (dependencies: "^1.2.3") — the`,
);
});

it('site 1b — the `declared-unresolvable` remedy DEFERS too; it mints no install advice (#17046)', () => {
// ── The arm that used to fire, quoted so the flip is legible ──────────
//
// Repair the INSTALL in /srv/app: run `pnpm install`, check that a
// production prune did not drop it, and that its dist is actually built — or
//
// DRIVEN on both shapes this one kind covers, that text was wrong twice:
// for a genuinely broken install it repeats, word for word, the three
// remedies `unresolvableMessage` already prints in the `cause:` line four
// lines below; and for #15045's location sub-case — narrowed by #17046 but
// NOT removed, since pnpm's `file:` virtual-store copy and every git /
// tarball declaration still reach it — the same `cause:` says outright
// that re-running `pnpm install`, un-pruning and rebuilding change
// nothing. One screen contradicting itself, the #14270 class.
const rendered = plain(remedyDeclared());
expect(rendered).not.toContain('Repair the INSTALL');
expect(rendered).not.toContain('pnpm install');
expect(rendered).not.toContain('production prune');
expect(rendered).not.toContain('dist is actually built');
// ⛔ Nor may it drift into the OTHER arm's instruction.
expect(rendered).not.toContain("declare it in the app's package.json");
// What it says instead: the declaration is fine, and the cause below owns
// the remedy — the same DEFERRAL site 2b pins for the sibling kind.
expect(rendered).toContain('declaration is NOT the problem');
expect(rendered).toContain('the cause below names the');
expect(rendered).toContain('authority on');
// It still chains into the `Fix one of:` list the fatal assembles.
expect(rendered.endsWith(' — or\n')).toBe(true);
});

it('site 2 — the "add it to THIS APP" remedy names it, with the spacing intact', () => {
// The other instruction an operator acts on: the app never declared it.
expect(lines(remedyUndeclared())[0]).toBe(
Expand Down
28 changes: 22 additions & 6 deletions packages/cli/src/commands/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3770,7 +3770,6 @@ export default class Serve extends Command {
const remedy = formatOrganizationsInstallRemedy(
hostImportFailureKind(orgErr),
declaration,
hostRoot,
);
console.error(
chalk.red(formatOrganizationsAbsentFatal(tenancyPosture, remedy, cause)),
Expand Down Expand Up @@ -5404,9 +5403,26 @@ export function formatI18nLoadDiagnostic(pkg: string, err: unknown): string {
*
* The question each arm answers is **"is the declaration the problem?"**:
*
* - `declared-unresolvable` — the app's `package.json` DOES name the package
* and the INSTALL is what is broken. Telling that operator to re-read a
* - `declared-unresolvable` — the app's `package.json` DOES name the package,
* so the DECLARATION is not the problem. Telling that operator to re-read a
* file that is already correct is the defect this branch exists to avoid.
* ⚠️ It used to mint an install remedy here too — "Repair the INSTALL: run
* `pnpm install`, check that a production prune did not drop it, and that
* its dist is actually built". DRIVEN, on both shapes this kind covers, it
* was wrong twice over (#17046):
* · a genuinely broken install already gets those three remedies, word
* for word, from `unresolvableMessage` in the `cause:` line printed
* four lines below — so the bullet was a second copy to drift;
* · the #15045 sub-case — a location install this finder cannot tie to
* the declaration — prints a `cause:` that says outright "This is NOT
* an install problem … re-running `pnpm install`, un-pruning a deploy
* and rebuilding a dist all change nothing here", so the two halves of
* one screen contradicted each other. #17046 narrowed that sub-case
* (a correctly linked package now LOADS) but did NOT remove it: pnpm's
* `file:` virtual-store copy and every git / tarball declaration still
* land here.
* So this arm now says only what it uniquely knows — the declaration — and
* defers the remedy, exactly as the next one does.
* - `declared-no-loadable-entry` (#14041) — the app declares it, the install
* DELIVERED it, and the package's own `exports` names no runtime entry
* Node can load. Neither absence applies: there is nothing to declare and
Expand Down Expand Up @@ -5434,15 +5450,15 @@ export function formatI18nLoadDiagnostic(pkg: string, err: unknown): string {
export function formatOrganizationsInstallRemedy(
kind: HostImportFailureKind | undefined,
declaration: HostDeclaration,
hostRoot: string,
): string {
const pkg = Serve.ORGANIZATIONS_RUNTIME_PKG;
if (kind === 'declared-unresolvable') {
return ` • this app DECLARES ${pkg} ` +
`(${declaration.field}: ${JSON.stringify(declaration.specifier)}) — the\n` +
' declaration is NOT the problem and re-reading package.json will not help.\n' +
` Repair the INSTALL in ${hostRoot}: run \`pnpm install\`, check that a\n` +
' production prune did not drop it, and that its dist is actually built — or\n';
' What IS wrong was measured by the importer: the cause below names the\n' +
' directory it consulted and what it found there, and is the authority on\n' +
' whether any install action can change it — or\n';
}
if (kind === 'declared-no-loadable-entry') {
return ` • this app DECLARES ${pkg} ` +
Expand Down
Loading
Loading