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
34 changes: 34 additions & 0 deletions .changeset/aliased-install-host-importer.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
---
"@objectstack/types": patch
---

fix(types): an aliased install (`"foo": "npm:bar@1"`) is now found by the host importer's ESM-only fallback

`createHostImporter`'s #14041 fallback finder verifies the one directory it
consults — `<hostRoot>/node_modules/<key>` — by matching that directory's
`package.json` `name` against the declared package name. An aliased install
fails that check by construction: `{ "dependencies": { "foo": "npm:bar@1" } }`
puts a manifest named `bar` at `node_modules/foo`. The finder answered
`absent`, and an ESM-only aliased package therefore kept the pre-#14041 INSTALL
wording — a confidently-wrong remedy sending an operator to run `pnpm install`
against an install that is already correct, on a declaration shape
`packageNameFromSpecifier`'s own documentation blesses.

The declaration is now parsed for the name it promises: `npm:bar@1`,
`npm:@acme/x@^2` and the aliased `workspace:bar@*` name the package installed
under the key, so that is the manifest name the finder expects there. An
aliased ESM-only package is rescued exactly as a plain one is, and an aliased
install publishing nothing loadable gets the message about the PACKAGE's own
shape instead of the INSTALL message.

⚠️ The manifest-name check itself is NOT loosened — that check is what keeps
the fallback strictly tighter than the CJS resolution it backs up (#4719's
declaration gate, from the fallback side). What moved is the EXPECTATION, still
authored by the host and still read out of the host's own `package.json`: an
alias naming one package refuses a directory holding another, a non-aliased
declaration is unchanged, and a value that is not a bare package name — a
`workspace:` range, an alias carrying a subpath — yields no expectation to move
to, so the key stays and today's refusal is kept. `link:` and `file:` name a
LOCATION rather than a package, so no name is derivable from them at all; they
keep the key expectation, and with it the conservative direction the finder had
before.
271 changes: 271 additions & 0 deletions packages/types/src/node.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1243,3 +1243,274 @@ describe('the declared leg loads an ESM-only package via a hostRoot node_modules
expect((err as Error).message).toMatch(/INSTALL problem/);
});
});

/**
* ── #14278: an ALIASED install names its own package, and the finder must know ─
*
* `{ "dependencies": { "foo": "npm:bar@1" } }` installs the package `bar` at
* `<hostRoot>/node_modules/foo`: the manifest there is named `bar`, while the
* importable specifier — and the declaration key — is `foo`. The #14041
* fallback finder verifies the directory it consults by matching that
* manifest's `name` against the declared name, so it refused every aliased
* install BY CONSTRUCTION, and an ESM-only aliased package kept the
* pre-#14041 INSTALL wording: a confidently-wrong remedy against an install
* that is already correct.
*
* The fix parses the DECLARATION, never the directory. The host's own
* `package.json` says which package `foo` is an alias for, so the expectation
* is still authored by the host and the check is exactly as tight as it was —
* what moves is the EXPECTED NAME, never the comparison. The TIGHTNESS cases
* below are that proof: an alias naming one package does not license a
* directory holding another, and a NON-aliased declaration is untouched (the
* `manifest NAMES the declared package` case above is that control, and it
* stays green).
*
* `link:` / `file:` name a LOCATION rather than a package, so no name can be
* derived from them at all; they keep the key expectation, and with it today's
* conservative refusal.
*/
describe('an aliased install is verified against the name its DECLARATION names (#14278)', () => {
/** The card's exact shape: `import` condition only, no `require`, no `main`. */
const ESM_ONLY_EXPORTS = { '.': { import: './dist/index.js' } };

const roots: string[] = [];

afterAll(() => {
for (const dir of roots) rmSync(dir, { recursive: true, force: true });
});

/** A fresh host app declaring `key` with the literal specifier under test. */
function app(tag: string, key: string, specifier: string): string {
const root = mkdtempSync(join(tmpdir(), `os-aliased-${tag}-`));
roots.push(root);
writeFileSync(
join(root, 'package.json'),
JSON.stringify({
name: 'aliased-host-fixture',
type: 'module',
dependencies: { [key]: specifier },
}),
'utf8',
);
return root;
}

/**
* Install a package NAMED `manifestName` at `node_modules/<key>` — the
* on-disk shape every aliasing package manager produces. (`link:` /
* `workspace:` installs put a SYMLINK there instead; the finder reads
* `node_modules/<key>` either way and realpaths only afterwards, so a plain
* directory exercises the same code.)
*/
function installAs(
root: string,
key: string,
manifestName: string,
manifest: Record<string, unknown>,
files: Record<string, string>,
): void {
const dir = join(root, 'node_modules', ...key.split('/'));
mkdirSync(dir, { recursive: true });
writeFileSync(
join(dir, 'package.json'),
JSON.stringify({
name: manifestName,
version: '0.0.0-fixture',
type: 'module',
...manifest,
}),
'utf8',
);
for (const rel of Object.keys(files)) {
const target = join(dir, rel);
mkdirSync(dirname(target), { recursive: true });
writeFileSync(target, files[rel] as string, 'utf8');
}
}

it('PRECONDITION: an aliased ESM-only install reaches the fallback at all', () => {
// Same precondition the #14041 suite pins, re-measured through an alias:
// the CJS resolver FINDS `node_modules/aliased` and refuses on the
// CONDITION, so everything below is decided inside that throw's catch —
// the fallback is the only thing that can answer, and before this fix it
// answered `absent`.
const root = app('precondition', 'aliased', 'npm:@fixture/alias-target@1');
installAs(root, 'aliased', '@fixture/alias-target', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'aliased-esm-only';\n",
});
let code: string | undefined;
try {
createHostRequire(root).resolve('aliased');
} catch (e) {
code = (e as { code?: string }).code;
}
expect(code).toBe('ERR_PACKAGE_PATH_NOT_EXPORTED');
});

it('THE CARD: an aliased ESM-only package is rescued, not reported as an INSTALL problem', async () => {
const root = app('loads', 'aliased-esm', 'npm:@fixture/alias-esm-only@1');
installAs(root, 'aliased-esm', '@fixture/alias-esm-only', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'aliased-esm-only';\n",
});
expect((await createHostImporter(root)('aliased-esm')).BUILD).toBe('aliased-esm-only');
});

it('THE CARD (wording): an aliased install with no loadable entry gets the PACKAGE message', async () => {
// The card's named deliverable: the aliased install answers with the
// ESM-only wording (`declared-no-loadable-entry`) instead of the INSTALL
// wording, because the install is fine and no install action can help.
const root = app('types-only', 'aliased-types', 'npm:@fixture/alias-types-only@1');
installAs(
root,
'aliased-types',
'@fixture/alias-types-only',
{ exports: { '.': { types: './dist/index.d.ts' } } },
{ 'dist/index.d.ts': 'export declare const BUILD: string;\n' },
);
const err = await createHostImporter(root)('aliased-types').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-no-loadable-entry');
expect((err as Error).message).toMatch(/publishes no entry/);
expect((err as Error).message).not.toMatch(/INSTALL problem/);
});

it('a SCOPED key aliasing an unscoped package is rescued too', async () => {
// Both halves of the mapping are free to be scoped or not: the key is a
// directory path under `node_modules`, the alias target is a package name.
const root = app('scoped-key', '@app/aliased', 'npm:alias-unscoped@^2.0.0');
installAs(root, '@app/aliased', 'alias-unscoped', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'alias-unscoped';\n",
});
expect((await createHostImporter(root)('@app/aliased')).BUILD).toBe('alias-unscoped');
});

it('an aliased SUBPATH resolves against the aliased package', async () => {
const root = app('subpath', 'aliased-sub', 'npm:@fixture/alias-subpaths@1');
installAs(
root,
'aliased-sub',
'@fixture/alias-subpaths',
{ exports: { '.': { import: './dist/index.js' }, './plugin': { import: './dist/plugin.js' } } },
{
'dist/index.js': "export const WHERE = 'root';\n",
'dist/plugin.js': "export const WHERE = 'plugin';\n",
},
);
expect((await createHostImporter(root)('aliased-sub/plugin')).WHERE).toBe('plugin');
});

it('an alias with no version range names its target just the same', async () => {
const root = app('no-range', 'aliased-bare', 'npm:@fixture/alias-bare');
installAs(root, 'aliased-bare', '@fixture/alias-bare', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'alias-bare';\n",
});
expect((await createHostImporter(root)('aliased-bare')).BUILD).toBe('alias-bare');
});

it('a `workspace:` ALIAS names its target; a plain `workspace:` range does not', async () => {
// pnpm spells an aliased workspace dependency `workspace:<name>@<range>`;
// `workspace:*` / `workspace:^1.2.3` carry a RANGE only, so the key stays
// the expected name.
const aliased = app('workspace-alias', 'ws-aliased', 'workspace:@fixture/ws-target@*');
installAs(aliased, 'ws-aliased', '@fixture/ws-target', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'ws-target';\n",
});
expect((await createHostImporter(aliased)('ws-aliased')).BUILD).toBe('ws-target');

const plain = app('workspace-plain', '@fixture/ws-plain', 'workspace:*');
installAs(plain, '@fixture/ws-plain', '@fixture/ws-plain', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'ws-plain';\n",
});
expect((await createHostImporter(plain)('@fixture/ws-plain')).BUILD).toBe('ws-plain');
});

it('a `link:` specifier names a LOCATION, so the KEY stays the expected name', async () => {
// The linked package installed under its own key loads, exactly as before.
const root = app('link-ok', 'linked', 'link:../linked');
installAs(root, 'linked', 'linked', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'linked';\n",
});
expect((await createHostImporter(root)('linked')).BUILD).toBe('linked');
});

it('BOUNDARY: a `link:` target whose manifest names something else keeps the refusal', async () => {
// Deliberate, and the reason `link:` is not "parsed" into a name: a path
// specifier carries no package name for the finder to expect, so there is
// nothing to verify a differing manifest against. The conservative
// direction (refuse, never load the wrong thing) is kept rather than
// guessed at — widening it here would make the finder looser than the
// manifest-name check exists to be.
const root = app('link-mismatch', 'linked-other', 'link:../elsewhere');
installAs(root, 'linked-other', '@fixture/some-other-name', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'other';\n",
});
const err = await createHostImporter(root)('linked-other').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: an alias naming one package does not license a directory holding another', async () => {
// The check moved its EXPECTATION, not its strictness. The declaration
// says this directory holds `@fixture/alias-declared`; it holds
// `@fixture/alias-installed`, so it is not the declared package's install
// and must not be rescued from.
const root = app('alias-mismatch', 'aliased-wrong', 'npm:@fixture/alias-declared@1');
installAs(root, 'aliased-wrong', '@fixture/alias-installed', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'imposter';\n",
});
const err = await createHostImporter(root)('aliased-wrong').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: a NON-aliased declaration is unchanged — the key is still the expected name', async () => {
// The control the card names: an aliased-install red that also reddens
// this one would mean the finder got looser, not smarter. A plain range
// declares no alias, so a directory holding a different package is refused
// exactly as it was before #14278.
const root = app('plain-range', '@fixture/plain-range', '^1.0.0');
installAs(root, '@fixture/plain-range', '@fixture/somebody-else', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'imposter';\n",
});
const err = await createHostImporter(root)('@fixture/plain-range').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: an alias target carrying a SUBPATH is not a package name, and is refused', async () => {
// `npm:` values are `<name>[@<range>]` — never a subpath. A value that is
// not a bare package name yields no expectation to move to, so the key
// stays, and this directory (named for the subpath's package) is refused.
const root = app('alias-subpath-value', 'aliased-bad', 'npm:@fixture/alias-bad/deep@1');
installAs(root, 'aliased-bad', '@fixture/alias-bad', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'imposter';\n",
});
const err = await createHostImporter(root)('aliased-bad').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: an alias does not reopen the hostRoot boundary', async () => {
// Every other axis of the finder's tightness is unaffected by the alias:
// the one directory consulted is still `<hostRoot>/node_modules/<key>`,
// never a parent's. Installed one level up, under the same key and the
// aliased name, it is still not this app's install.
const parent = mkdtempSync(join(tmpdir(), 'os-aliased-parent-'));
roots.push(parent);
installAs(parent, 'aliased-up', '@fixture/alias-parent', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'from-parent';\n",
});
const root = join(parent, 'app');
mkdirSync(root, { recursive: true });
writeFileSync(
join(root, 'package.json'),
JSON.stringify({
name: 'nested-aliased-host-fixture',
type: 'module',
dependencies: { 'aliased-up': 'npm:@fixture/alias-parent@1' },
}),
'utf8',
);
const err = await createHostImporter(root)('aliased-up').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
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
34 changes: 34 additions & 0 deletions .changeset/aliased-install-host-importer.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
---
"@objectstack/types": patch
---

fix(types): an aliased install (`"foo": "npm:bar@1"`) is now found by the host importer's ESM-only fallback

`createHostImporter`'s #14041 fallback finder verifies the one directory it
consults — `<hostRoot>/node_modules/<key>` — by matching that directory's
`package.json` `name` against the declared package name. An aliased install
fails that check by construction: `{ "dependencies": { "foo": "npm:bar@1" } }`
puts a manifest named `bar` at `node_modules/foo`. The finder answered
`absent`, and an ESM-only aliased package therefore kept the pre-#14041 INSTALL
wording — a confidently-wrong remedy sending an operator to run `pnpm install`
against an install that is already correct, on a declaration shape
`packageNameFromSpecifier`'s own documentation blesses.

The declaration is now parsed for the name it promises: `npm:bar@1`,
`npm:@acme/x@^2` and the aliased `workspace:bar@*` name the package installed
under the key, so that is the manifest name the finder expects there. An
aliased ESM-only package is rescued exactly as a plain one is, and an aliased
install publishing nothing loadable gets the message about the PACKAGE's own
shape instead of the INSTALL message.

⚠️ The manifest-name check itself is NOT loosened — that check is what keeps
the fallback strictly tighter than the CJS resolution it backs up (#4719's
declaration gate, from the fallback side). What moved is the EXPECTATION, still
authored by the host and still read out of the host's own `package.json`: an
alias naming one package refuses a directory holding another, a non-aliased
declaration is unchanged, and a value that is not a bare package name — a
`workspace:` range, an alias carrying a subpath — yields no expectation to move
to, so the key stays and today's refusal is kept. `link:` and `file:` name a
LOCATION rather than a package, so no name is derivable from them at all; they
keep the key expectation, and with it the conservative direction the finder had
before.
271 changes: 271 additions & 0 deletions packages/types/src/node.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1243,3 +1243,274 @@ describe('the declared leg loads an ESM-only package via a hostRoot node_modules
expect((err as Error).message).toMatch(/INSTALL problem/);
});
});

/**
* ── #14278: an ALIASED install names its own package, and the finder must know ─
*
* `{ "dependencies": { "foo": "npm:bar@1" } }` installs the package `bar` at
* `<hostRoot>/node_modules/foo`: the manifest there is named `bar`, while the
* importable specifier — and the declaration key — is `foo`. The #14041
* fallback finder verifies the directory it consults by matching that
* manifest's `name` against the declared name, so it refused every aliased
* install BY CONSTRUCTION, and an ESM-only aliased package kept the
* pre-#14041 INSTALL wording: a confidently-wrong remedy against an install
* that is already correct.
*
* The fix parses the DECLARATION, never the directory. The host's own
* `package.json` says which package `foo` is an alias for, so the expectation
* is still authored by the host and the check is exactly as tight as it was —
* what moves is the EXPECTED NAME, never the comparison. The TIGHTNESS cases
* below are that proof: an alias naming one package does not license a
* directory holding another, and a NON-aliased declaration is untouched (the
* `manifest NAMES the declared package` case above is that control, and it
* stays green).
*
* `link:` / `file:` name a LOCATION rather than a package, so no name can be
* derived from them at all; they keep the key expectation, and with it today's
* conservative refusal.
*/
describe('an aliased install is verified against the name its DECLARATION names (#14278)', () => {
/** The card's exact shape: `import` condition only, no `require`, no `main`. */
const ESM_ONLY_EXPORTS = { '.': { import: './dist/index.js' } };

const roots: string[] = [];

afterAll(() => {
for (const dir of roots) rmSync(dir, { recursive: true, force: true });
});

/** A fresh host app declaring `key` with the literal specifier under test. */
function app(tag: string, key: string, specifier: string): string {
const root = mkdtempSync(join(tmpdir(), `os-aliased-${tag}-`));
roots.push(root);
writeFileSync(
join(root, 'package.json'),
JSON.stringify({
name: 'aliased-host-fixture',
type: 'module',
dependencies: { [key]: specifier },
}),
'utf8',
);
return root;
}

/**
* Install a package NAMED `manifestName` at `node_modules/<key>` — the
* on-disk shape every aliasing package manager produces. (`link:` /
* `workspace:` installs put a SYMLINK there instead; the finder reads
* `node_modules/<key>` either way and realpaths only afterwards, so a plain
* directory exercises the same code.)
*/
function installAs(
root: string,
key: string,
manifestName: string,
manifest: Record<string, unknown>,
files: Record<string, string>,
): void {
const dir = join(root, 'node_modules', ...key.split('/'));
mkdirSync(dir, { recursive: true });
writeFileSync(
join(dir, 'package.json'),
JSON.stringify({
name: manifestName,
version: '0.0.0-fixture',
type: 'module',
...manifest,
}),
'utf8',
);
for (const rel of Object.keys(files)) {
const target = join(dir, rel);
mkdirSync(dirname(target), { recursive: true });
writeFileSync(target, files[rel] as string, 'utf8');
}
}

it('PRECONDITION: an aliased ESM-only install reaches the fallback at all', () => {
// Same precondition the #14041 suite pins, re-measured through an alias:
// the CJS resolver FINDS `node_modules/aliased` and refuses on the
// CONDITION, so everything below is decided inside that throw's catch —
// the fallback is the only thing that can answer, and before this fix it
// answered `absent`.
const root = app('precondition', 'aliased', 'npm:@fixture/alias-target@1');
installAs(root, 'aliased', '@fixture/alias-target', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'aliased-esm-only';\n",
});
let code: string | undefined;
try {
createHostRequire(root).resolve('aliased');
} catch (e) {
code = (e as { code?: string }).code;
}
expect(code).toBe('ERR_PACKAGE_PATH_NOT_EXPORTED');
});

it('THE CARD: an aliased ESM-only package is rescued, not reported as an INSTALL problem', async () => {
const root = app('loads', 'aliased-esm', 'npm:@fixture/alias-esm-only@1');
installAs(root, 'aliased-esm', '@fixture/alias-esm-only', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'aliased-esm-only';\n",
});
expect((await createHostImporter(root)('aliased-esm')).BUILD).toBe('aliased-esm-only');
});

it('THE CARD (wording): an aliased install with no loadable entry gets the PACKAGE message', async () => {
// The card's named deliverable: the aliased install answers with the
// ESM-only wording (`declared-no-loadable-entry`) instead of the INSTALL
// wording, because the install is fine and no install action can help.
const root = app('types-only', 'aliased-types', 'npm:@fixture/alias-types-only@1');
installAs(
root,
'aliased-types',
'@fixture/alias-types-only',
{ exports: { '.': { types: './dist/index.d.ts' } } },
{ 'dist/index.d.ts': 'export declare const BUILD: string;\n' },
);
const err = await createHostImporter(root)('aliased-types').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-no-loadable-entry');
expect((err as Error).message).toMatch(/publishes no entry/);
expect((err as Error).message).not.toMatch(/INSTALL problem/);
});

it('a SCOPED key aliasing an unscoped package is rescued too', async () => {
// Both halves of the mapping are free to be scoped or not: the key is a
// directory path under `node_modules`, the alias target is a package name.
const root = app('scoped-key', '@app/aliased', 'npm:alias-unscoped@^2.0.0');
installAs(root, '@app/aliased', 'alias-unscoped', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'alias-unscoped';\n",
});
expect((await createHostImporter(root)('@app/aliased')).BUILD).toBe('alias-unscoped');
});

it('an aliased SUBPATH resolves against the aliased package', async () => {
const root = app('subpath', 'aliased-sub', 'npm:@fixture/alias-subpaths@1');
installAs(
root,
'aliased-sub',
'@fixture/alias-subpaths',
{ exports: { '.': { import: './dist/index.js' }, './plugin': { import: './dist/plugin.js' } } },
{
'dist/index.js': "export const WHERE = 'root';\n",
'dist/plugin.js': "export const WHERE = 'plugin';\n",
},
);
expect((await createHostImporter(root)('aliased-sub/plugin')).WHERE).toBe('plugin');
});

it('an alias with no version range names its target just the same', async () => {
const root = app('no-range', 'aliased-bare', 'npm:@fixture/alias-bare');
installAs(root, 'aliased-bare', '@fixture/alias-bare', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'alias-bare';\n",
});
expect((await createHostImporter(root)('aliased-bare')).BUILD).toBe('alias-bare');
});

it('a `workspace:` ALIAS names its target; a plain `workspace:` range does not', async () => {
// pnpm spells an aliased workspace dependency `workspace:<name>@<range>`;
// `workspace:*` / `workspace:^1.2.3` carry a RANGE only, so the key stays
// the expected name.
const aliased = app('workspace-alias', 'ws-aliased', 'workspace:@fixture/ws-target@*');
installAs(aliased, 'ws-aliased', '@fixture/ws-target', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'ws-target';\n",
});
expect((await createHostImporter(aliased)('ws-aliased')).BUILD).toBe('ws-target');

const plain = app('workspace-plain', '@fixture/ws-plain', 'workspace:*');
installAs(plain, '@fixture/ws-plain', '@fixture/ws-plain', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'ws-plain';\n",
});
expect((await createHostImporter(plain)('@fixture/ws-plain')).BUILD).toBe('ws-plain');
});

it('a `link:` specifier names a LOCATION, so the KEY stays the expected name', async () => {
// The linked package installed under its own key loads, exactly as before.
const root = app('link-ok', 'linked', 'link:../linked');
installAs(root, 'linked', 'linked', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'linked';\n",
});
expect((await createHostImporter(root)('linked')).BUILD).toBe('linked');
});

it('BOUNDARY: a `link:` target whose manifest names something else keeps the refusal', async () => {
// Deliberate, and the reason `link:` is not "parsed" into a name: a path
// specifier carries no package name for the finder to expect, so there is
// nothing to verify a differing manifest against. The conservative
// direction (refuse, never load the wrong thing) is kept rather than
// guessed at — widening it here would make the finder looser than the
// manifest-name check exists to be.
const root = app('link-mismatch', 'linked-other', 'link:../elsewhere');
installAs(root, 'linked-other', '@fixture/some-other-name', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'other';\n",
});
const err = await createHostImporter(root)('linked-other').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: an alias naming one package does not license a directory holding another', async () => {
// The check moved its EXPECTATION, not its strictness. The declaration
// says this directory holds `@fixture/alias-declared`; it holds
// `@fixture/alias-installed`, so it is not the declared package's install
// and must not be rescued from.
const root = app('alias-mismatch', 'aliased-wrong', 'npm:@fixture/alias-declared@1');
installAs(root, 'aliased-wrong', '@fixture/alias-installed', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'imposter';\n",
});
const err = await createHostImporter(root)('aliased-wrong').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: a NON-aliased declaration is unchanged — the key is still the expected name', async () => {
// The control the card names: an aliased-install red that also reddens
// this one would mean the finder got looser, not smarter. A plain range
// declares no alias, so a directory holding a different package is refused
// exactly as it was before #14278.
const root = app('plain-range', '@fixture/plain-range', '^1.0.0');
installAs(root, '@fixture/plain-range', '@fixture/somebody-else', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'imposter';\n",
});
const err = await createHostImporter(root)('@fixture/plain-range').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: an alias target carrying a SUBPATH is not a package name, and is refused', async () => {
// `npm:` values are `<name>[@<range>]` — never a subpath. A value that is
// not a bare package name yields no expectation to move to, so the key
// stays, and this directory (named for the subpath's package) is refused.
const root = app('alias-subpath-value', 'aliased-bad', 'npm:@fixture/alias-bad/deep@1');
installAs(root, 'aliased-bad', '@fixture/alias-bad', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'imposter';\n",
});
const err = await createHostImporter(root)('aliased-bad').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: an alias does not reopen the hostRoot boundary', async () => {
// Every other axis of the finder's tightness is unaffected by the alias:
// the one directory consulted is still `<hostRoot>/node_modules/<key>`,
// never a parent's. Installed one level up, under the same key and the
// aliased name, it is still not this app's install.
const parent = mkdtempSync(join(tmpdir(), 'os-aliased-parent-'));
roots.push(parent);
installAs(parent, 'aliased-up', '@fixture/alias-parent', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'from-parent';\n",
});
const root = join(parent, 'app');
mkdirSync(root, { recursive: true });
writeFileSync(
join(root, 'package.json'),
JSON.stringify({
name: 'nested-aliased-host-fixture',
type: 'module',
dependencies: { 'aliased-up': 'npm:@fixture/alias-parent@1' },
}),
'utf8',
);
const err = await createHostImporter(root)('aliased-up').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
34 changes: 34 additions & 0 deletions .changeset/aliased-install-host-importer.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
---
"@objectstack/types": patch
---

fix(types): an aliased install (`"foo": "npm:bar@1"`) is now found by the host importer's ESM-only fallback

`createHostImporter`'s #14041 fallback finder verifies the one directory it
consults — `<hostRoot>/node_modules/<key>` — by matching that directory's
`package.json` `name` against the declared package name. An aliased install
fails that check by construction: `{ "dependencies": { "foo": "npm:bar@1" } }`
puts a manifest named `bar` at `node_modules/foo`. The finder answered
`absent`, and an ESM-only aliased package therefore kept the pre-#14041 INSTALL
wording — a confidently-wrong remedy sending an operator to run `pnpm install`
against an install that is already correct, on a declaration shape
`packageNameFromSpecifier`'s own documentation blesses.

The declaration is now parsed for the name it promises: `npm:bar@1`,
`npm:@acme/x@^2` and the aliased `workspace:bar@*` name the package installed
under the key, so that is the manifest name the finder expects there. An
aliased ESM-only package is rescued exactly as a plain one is, and an aliased
install publishing nothing loadable gets the message about the PACKAGE's own
shape instead of the INSTALL message.

⚠️ The manifest-name check itself is NOT loosened — that check is what keeps
the fallback strictly tighter than the CJS resolution it backs up (#4719's
declaration gate, from the fallback side). What moved is the EXPECTATION, still
authored by the host and still read out of the host's own `package.json`: an
alias naming one package refuses a directory holding another, a non-aliased
declaration is unchanged, and a value that is not a bare package name — a
`workspace:` range, an alias carrying a subpath — yields no expectation to move
to, so the key stays and today's refusal is kept. `link:` and `file:` name a
LOCATION rather than a package, so no name is derivable from them at all; they
keep the key expectation, and with it the conservative direction the finder had
before.
271 changes: 271 additions & 0 deletions packages/types/src/node.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1243,3 +1243,274 @@ describe('the declared leg loads an ESM-only package via a hostRoot node_modules
expect((err as Error).message).toMatch(/INSTALL problem/);
});
});

/**
* ── #14278: an ALIASED install names its own package, and the finder must know ─
*
* `{ "dependencies": { "foo": "npm:bar@1" } }` installs the package `bar` at
* `<hostRoot>/node_modules/foo`: the manifest there is named `bar`, while the
* importable specifier — and the declaration key — is `foo`. The #14041
* fallback finder verifies the directory it consults by matching that
* manifest's `name` against the declared name, so it refused every aliased
* install BY CONSTRUCTION, and an ESM-only aliased package kept the
* pre-#14041 INSTALL wording: a confidently-wrong remedy against an install
* that is already correct.
*
* The fix parses the DECLARATION, never the directory. The host's own
* `package.json` says which package `foo` is an alias for, so the expectation
* is still authored by the host and the check is exactly as tight as it was —
* what moves is the EXPECTED NAME, never the comparison. The TIGHTNESS cases
* below are that proof: an alias naming one package does not license a
* directory holding another, and a NON-aliased declaration is untouched (the
* `manifest NAMES the declared package` case above is that control, and it
* stays green).
*
* `link:` / `file:` name a LOCATION rather than a package, so no name can be
* derived from them at all; they keep the key expectation, and with it today's
* conservative refusal.
*/
describe('an aliased install is verified against the name its DECLARATION names (#14278)', () => {
/** The card's exact shape: `import` condition only, no `require`, no `main`. */
const ESM_ONLY_EXPORTS = { '.': { import: './dist/index.js' } };

const roots: string[] = [];

afterAll(() => {
for (const dir of roots) rmSync(dir, { recursive: true, force: true });
});

/** A fresh host app declaring `key` with the literal specifier under test. */
function app(tag: string, key: string, specifier: string): string {
const root = mkdtempSync(join(tmpdir(), `os-aliased-${tag}-`));
roots.push(root);
writeFileSync(
join(root, 'package.json'),
JSON.stringify({
name: 'aliased-host-fixture',
type: 'module',
dependencies: { [key]: specifier },
}),
'utf8',
);
return root;
}

/**
* Install a package NAMED `manifestName` at `node_modules/<key>` — the
* on-disk shape every aliasing package manager produces. (`link:` /
* `workspace:` installs put a SYMLINK there instead; the finder reads
* `node_modules/<key>` either way and realpaths only afterwards, so a plain
* directory exercises the same code.)
*/
function installAs(
root: string,
key: string,
manifestName: string,
manifest: Record<string, unknown>,
files: Record<string, string>,
): void {
const dir = join(root, 'node_modules', ...key.split('/'));
mkdirSync(dir, { recursive: true });
writeFileSync(
join(dir, 'package.json'),
JSON.stringify({
name: manifestName,
version: '0.0.0-fixture',
type: 'module',
...manifest,
}),
'utf8',
);
for (const rel of Object.keys(files)) {
const target = join(dir, rel);
mkdirSync(dirname(target), { recursive: true });
writeFileSync(target, files[rel] as string, 'utf8');
}
}

it('PRECONDITION: an aliased ESM-only install reaches the fallback at all', () => {
// Same precondition the #14041 suite pins, re-measured through an alias:
// the CJS resolver FINDS `node_modules/aliased` and refuses on the
// CONDITION, so everything below is decided inside that throw's catch —
// the fallback is the only thing that can answer, and before this fix it
// answered `absent`.
const root = app('precondition', 'aliased', 'npm:@fixture/alias-target@1');
installAs(root, 'aliased', '@fixture/alias-target', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'aliased-esm-only';\n",
});
let code: string | undefined;
try {
createHostRequire(root).resolve('aliased');
} catch (e) {
code = (e as { code?: string }).code;
}
expect(code).toBe('ERR_PACKAGE_PATH_NOT_EXPORTED');
});

it('THE CARD: an aliased ESM-only package is rescued, not reported as an INSTALL problem', async () => {
const root = app('loads', 'aliased-esm', 'npm:@fixture/alias-esm-only@1');
installAs(root, 'aliased-esm', '@fixture/alias-esm-only', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'aliased-esm-only';\n",
});
expect((await createHostImporter(root)('aliased-esm')).BUILD).toBe('aliased-esm-only');
});

it('THE CARD (wording): an aliased install with no loadable entry gets the PACKAGE message', async () => {
// The card's named deliverable: the aliased install answers with the
// ESM-only wording (`declared-no-loadable-entry`) instead of the INSTALL
// wording, because the install is fine and no install action can help.
const root = app('types-only', 'aliased-types', 'npm:@fixture/alias-types-only@1');
installAs(
root,
'aliased-types',
'@fixture/alias-types-only',
{ exports: { '.': { types: './dist/index.d.ts' } } },
{ 'dist/index.d.ts': 'export declare const BUILD: string;\n' },
);
const err = await createHostImporter(root)('aliased-types').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-no-loadable-entry');
expect((err as Error).message).toMatch(/publishes no entry/);
expect((err as Error).message).not.toMatch(/INSTALL problem/);
});

it('a SCOPED key aliasing an unscoped package is rescued too', async () => {
// Both halves of the mapping are free to be scoped or not: the key is a
// directory path under `node_modules`, the alias target is a package name.
const root = app('scoped-key', '@app/aliased', 'npm:alias-unscoped@^2.0.0');
installAs(root, '@app/aliased', 'alias-unscoped', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'alias-unscoped';\n",
});
expect((await createHostImporter(root)('@app/aliased')).BUILD).toBe('alias-unscoped');
});

it('an aliased SUBPATH resolves against the aliased package', async () => {
const root = app('subpath', 'aliased-sub', 'npm:@fixture/alias-subpaths@1');
installAs(
root,
'aliased-sub',
'@fixture/alias-subpaths',
{ exports: { '.': { import: './dist/index.js' }, './plugin': { import: './dist/plugin.js' } } },
{
'dist/index.js': "export const WHERE = 'root';\n",
'dist/plugin.js': "export const WHERE = 'plugin';\n",
},
);
expect((await createHostImporter(root)('aliased-sub/plugin')).WHERE).toBe('plugin');
});

it('an alias with no version range names its target just the same', async () => {
const root = app('no-range', 'aliased-bare', 'npm:@fixture/alias-bare');
installAs(root, 'aliased-bare', '@fixture/alias-bare', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'alias-bare';\n",
});
expect((await createHostImporter(root)('aliased-bare')).BUILD).toBe('alias-bare');
});

it('a `workspace:` ALIAS names its target; a plain `workspace:` range does not', async () => {
// pnpm spells an aliased workspace dependency `workspace:<name>@<range>`;
// `workspace:*` / `workspace:^1.2.3` carry a RANGE only, so the key stays
// the expected name.
const aliased = app('workspace-alias', 'ws-aliased', 'workspace:@fixture/ws-target@*');
installAs(aliased, 'ws-aliased', '@fixture/ws-target', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'ws-target';\n",
});
expect((await createHostImporter(aliased)('ws-aliased')).BUILD).toBe('ws-target');

const plain = app('workspace-plain', '@fixture/ws-plain', 'workspace:*');
installAs(plain, '@fixture/ws-plain', '@fixture/ws-plain', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'ws-plain';\n",
});
expect((await createHostImporter(plain)('@fixture/ws-plain')).BUILD).toBe('ws-plain');
});

it('a `link:` specifier names a LOCATION, so the KEY stays the expected name', async () => {
// The linked package installed under its own key loads, exactly as before.
const root = app('link-ok', 'linked', 'link:../linked');
installAs(root, 'linked', 'linked', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'linked';\n",
});
expect((await createHostImporter(root)('linked')).BUILD).toBe('linked');
});

it('BOUNDARY: a `link:` target whose manifest names something else keeps the refusal', async () => {
// Deliberate, and the reason `link:` is not "parsed" into a name: a path
// specifier carries no package name for the finder to expect, so there is
// nothing to verify a differing manifest against. The conservative
// direction (refuse, never load the wrong thing) is kept rather than
// guessed at — widening it here would make the finder looser than the
// manifest-name check exists to be.
const root = app('link-mismatch', 'linked-other', 'link:../elsewhere');
installAs(root, 'linked-other', '@fixture/some-other-name', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'other';\n",
});
const err = await createHostImporter(root)('linked-other').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: an alias naming one package does not license a directory holding another', async () => {
// The check moved its EXPECTATION, not its strictness. The declaration
// says this directory holds `@fixture/alias-declared`; it holds
// `@fixture/alias-installed`, so it is not the declared package's install
// and must not be rescued from.
const root = app('alias-mismatch', 'aliased-wrong', 'npm:@fixture/alias-declared@1');
installAs(root, 'aliased-wrong', '@fixture/alias-installed', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'imposter';\n",
});
const err = await createHostImporter(root)('aliased-wrong').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: a NON-aliased declaration is unchanged — the key is still the expected name', async () => {
// The control the card names: an aliased-install red that also reddens
// this one would mean the finder got looser, not smarter. A plain range
// declares no alias, so a directory holding a different package is refused
// exactly as it was before #14278.
const root = app('plain-range', '@fixture/plain-range', '^1.0.0');
installAs(root, '@fixture/plain-range', '@fixture/somebody-else', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'imposter';\n",
});
const err = await createHostImporter(root)('@fixture/plain-range').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: an alias target carrying a SUBPATH is not a package name, and is refused', async () => {
// `npm:` values are `<name>[@<range>]` — never a subpath. A value that is
// not a bare package name yields no expectation to move to, so the key
// stays, and this directory (named for the subpath's package) is refused.
const root = app('alias-subpath-value', 'aliased-bad', 'npm:@fixture/alias-bad/deep@1');
installAs(root, 'aliased-bad', '@fixture/alias-bad', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'imposter';\n",
});
const err = await createHostImporter(root)('aliased-bad').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: an alias does not reopen the hostRoot boundary', async () => {
// Every other axis of the finder's tightness is unaffected by the alias:
// the one directory consulted is still `<hostRoot>/node_modules/<key>`,
// never a parent's. Installed one level up, under the same key and the
// aliased name, it is still not this app's install.
const parent = mkdtempSync(join(tmpdir(), 'os-aliased-parent-'));
roots.push(parent);
installAs(parent, 'aliased-up', '@fixture/alias-parent', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'from-parent';\n",
});
const root = join(parent, 'app');
mkdirSync(root, { recursive: true });
writeFileSync(
join(root, 'package.json'),
JSON.stringify({
name: 'nested-aliased-host-fixture',
type: 'module',
dependencies: { 'aliased-up': 'npm:@fixture/alias-parent@1' },
}),
'utf8',
);
const err = await createHostImporter(root)('aliased-up').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
34 changes: 34 additions & 0 deletions .changeset/aliased-install-host-importer.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
---
"@objectstack/types": patch
---

fix(types): an aliased install (`"foo": "npm:bar@1"`) is now found by the host importer's ESM-only fallback

`createHostImporter`'s #14041 fallback finder verifies the one directory it
consults — `<hostRoot>/node_modules/<key>` — by matching that directory's
`package.json` `name` against the declared package name. An aliased install
fails that check by construction: `{ "dependencies": { "foo": "npm:bar@1" } }`
puts a manifest named `bar` at `node_modules/foo`. The finder answered
`absent`, and an ESM-only aliased package therefore kept the pre-#14041 INSTALL
wording — a confidently-wrong remedy sending an operator to run `pnpm install`
against an install that is already correct, on a declaration shape
`packageNameFromSpecifier`'s own documentation blesses.

The declaration is now parsed for the name it promises: `npm:bar@1`,
`npm:@acme/x@^2` and the aliased `workspace:bar@*` name the package installed
under the key, so that is the manifest name the finder expects there. An
aliased ESM-only package is rescued exactly as a plain one is, and an aliased
install publishing nothing loadable gets the message about the PACKAGE's own
shape instead of the INSTALL message.

⚠️ The manifest-name check itself is NOT loosened — that check is what keeps
the fallback strictly tighter than the CJS resolution it backs up (#4719's
declaration gate, from the fallback side). What moved is the EXPECTATION, still
authored by the host and still read out of the host's own `package.json`: an
alias naming one package refuses a directory holding another, a non-aliased
declaration is unchanged, and a value that is not a bare package name — a
`workspace:` range, an alias carrying a subpath — yields no expectation to move
to, so the key stays and today's refusal is kept. `link:` and `file:` name a
LOCATION rather than a package, so no name is derivable from them at all; they
keep the key expectation, and with it the conservative direction the finder had
before.
271 changes: 271 additions & 0 deletions packages/types/src/node.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1243,3 +1243,274 @@ describe('the declared leg loads an ESM-only package via a hostRoot node_modules
expect((err as Error).message).toMatch(/INSTALL problem/);
});
});

/**
* ── #14278: an ALIASED install names its own package, and the finder must know ─
*
* `{ "dependencies": { "foo": "npm:bar@1" } }` installs the package `bar` at
* `<hostRoot>/node_modules/foo`: the manifest there is named `bar`, while the
* importable specifier — and the declaration key — is `foo`. The #14041
* fallback finder verifies the directory it consults by matching that
* manifest's `name` against the declared name, so it refused every aliased
* install BY CONSTRUCTION, and an ESM-only aliased package kept the
* pre-#14041 INSTALL wording: a confidently-wrong remedy against an install
* that is already correct.
*
* The fix parses the DECLARATION, never the directory. The host's own
* `package.json` says which package `foo` is an alias for, so the expectation
* is still authored by the host and the check is exactly as tight as it was —
* what moves is the EXPECTED NAME, never the comparison. The TIGHTNESS cases
* below are that proof: an alias naming one package does not license a
* directory holding another, and a NON-aliased declaration is untouched (the
* `manifest NAMES the declared package` case above is that control, and it
* stays green).
*
* `link:` / `file:` name a LOCATION rather than a package, so no name can be
* derived from them at all; they keep the key expectation, and with it today's
* conservative refusal.
*/
describe('an aliased install is verified against the name its DECLARATION names (#14278)', () => {
/** The card's exact shape: `import` condition only, no `require`, no `main`. */
const ESM_ONLY_EXPORTS = { '.': { import: './dist/index.js' } };

const roots: string[] = [];

afterAll(() => {
for (const dir of roots) rmSync(dir, { recursive: true, force: true });
});

/** A fresh host app declaring `key` with the literal specifier under test. */
function app(tag: string, key: string, specifier: string): string {
const root = mkdtempSync(join(tmpdir(), `os-aliased-${tag}-`));
roots.push(root);
writeFileSync(
join(root, 'package.json'),
JSON.stringify({
name: 'aliased-host-fixture',
type: 'module',
dependencies: { [key]: specifier },
}),
'utf8',
);
return root;
}

/**
* Install a package NAMED `manifestName` at `node_modules/<key>` — the
* on-disk shape every aliasing package manager produces. (`link:` /
* `workspace:` installs put a SYMLINK there instead; the finder reads
* `node_modules/<key>` either way and realpaths only afterwards, so a plain
* directory exercises the same code.)
*/
function installAs(
root: string,
key: string,
manifestName: string,
manifest: Record<string, unknown>,
files: Record<string, string>,
): void {
const dir = join(root, 'node_modules', ...key.split('/'));
mkdirSync(dir, { recursive: true });
writeFileSync(
join(dir, 'package.json'),
JSON.stringify({
name: manifestName,
version: '0.0.0-fixture',
type: 'module',
...manifest,
}),
'utf8',
);
for (const rel of Object.keys(files)) {
const target = join(dir, rel);
mkdirSync(dirname(target), { recursive: true });
writeFileSync(target, files[rel] as string, 'utf8');
}
}

it('PRECONDITION: an aliased ESM-only install reaches the fallback at all', () => {
// Same precondition the #14041 suite pins, re-measured through an alias:
// the CJS resolver FINDS `node_modules/aliased` and refuses on the
// CONDITION, so everything below is decided inside that throw's catch —
// the fallback is the only thing that can answer, and before this fix it
// answered `absent`.
const root = app('precondition', 'aliased', 'npm:@fixture/alias-target@1');
installAs(root, 'aliased', '@fixture/alias-target', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'aliased-esm-only';\n",
});
let code: string | undefined;
try {
createHostRequire(root).resolve('aliased');
} catch (e) {
code = (e as { code?: string }).code;
}
expect(code).toBe('ERR_PACKAGE_PATH_NOT_EXPORTED');
});

it('THE CARD: an aliased ESM-only package is rescued, not reported as an INSTALL problem', async () => {
const root = app('loads', 'aliased-esm', 'npm:@fixture/alias-esm-only@1');
installAs(root, 'aliased-esm', '@fixture/alias-esm-only', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'aliased-esm-only';\n",
});
expect((await createHostImporter(root)('aliased-esm')).BUILD).toBe('aliased-esm-only');
});

it('THE CARD (wording): an aliased install with no loadable entry gets the PACKAGE message', async () => {
// The card's named deliverable: the aliased install answers with the
// ESM-only wording (`declared-no-loadable-entry`) instead of the INSTALL
// wording, because the install is fine and no install action can help.
const root = app('types-only', 'aliased-types', 'npm:@fixture/alias-types-only@1');
installAs(
root,
'aliased-types',
'@fixture/alias-types-only',
{ exports: { '.': { types: './dist/index.d.ts' } } },
{ 'dist/index.d.ts': 'export declare const BUILD: string;\n' },
);
const err = await createHostImporter(root)('aliased-types').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-no-loadable-entry');
expect((err as Error).message).toMatch(/publishes no entry/);
expect((err as Error).message).not.toMatch(/INSTALL problem/);
});

it('a SCOPED key aliasing an unscoped package is rescued too', async () => {
// Both halves of the mapping are free to be scoped or not: the key is a
// directory path under `node_modules`, the alias target is a package name.
const root = app('scoped-key', '@app/aliased', 'npm:alias-unscoped@^2.0.0');
installAs(root, '@app/aliased', 'alias-unscoped', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'alias-unscoped';\n",
});
expect((await createHostImporter(root)('@app/aliased')).BUILD).toBe('alias-unscoped');
});

it('an aliased SUBPATH resolves against the aliased package', async () => {
const root = app('subpath', 'aliased-sub', 'npm:@fixture/alias-subpaths@1');
installAs(
root,
'aliased-sub',
'@fixture/alias-subpaths',
{ exports: { '.': { import: './dist/index.js' }, './plugin': { import: './dist/plugin.js' } } },
{
'dist/index.js': "export const WHERE = 'root';\n",
'dist/plugin.js': "export const WHERE = 'plugin';\n",
},
);
expect((await createHostImporter(root)('aliased-sub/plugin')).WHERE).toBe('plugin');
});

it('an alias with no version range names its target just the same', async () => {
const root = app('no-range', 'aliased-bare', 'npm:@fixture/alias-bare');
installAs(root, 'aliased-bare', '@fixture/alias-bare', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'alias-bare';\n",
});
expect((await createHostImporter(root)('aliased-bare')).BUILD).toBe('alias-bare');
});

it('a `workspace:` ALIAS names its target; a plain `workspace:` range does not', async () => {
// pnpm spells an aliased workspace dependency `workspace:<name>@<range>`;
// `workspace:*` / `workspace:^1.2.3` carry a RANGE only, so the key stays
// the expected name.
const aliased = app('workspace-alias', 'ws-aliased', 'workspace:@fixture/ws-target@*');
installAs(aliased, 'ws-aliased', '@fixture/ws-target', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'ws-target';\n",
});
expect((await createHostImporter(aliased)('ws-aliased')).BUILD).toBe('ws-target');

const plain = app('workspace-plain', '@fixture/ws-plain', 'workspace:*');
installAs(plain, '@fixture/ws-plain', '@fixture/ws-plain', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'ws-plain';\n",
});
expect((await createHostImporter(plain)('@fixture/ws-plain')).BUILD).toBe('ws-plain');
});

it('a `link:` specifier names a LOCATION, so the KEY stays the expected name', async () => {
// The linked package installed under its own key loads, exactly as before.
const root = app('link-ok', 'linked', 'link:../linked');
installAs(root, 'linked', 'linked', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'linked';\n",
});
expect((await createHostImporter(root)('linked')).BUILD).toBe('linked');
});

it('BOUNDARY: a `link:` target whose manifest names something else keeps the refusal', async () => {
// Deliberate, and the reason `link:` is not "parsed" into a name: a path
// specifier carries no package name for the finder to expect, so there is
// nothing to verify a differing manifest against. The conservative
// direction (refuse, never load the wrong thing) is kept rather than
// guessed at — widening it here would make the finder looser than the
// manifest-name check exists to be.
const root = app('link-mismatch', 'linked-other', 'link:../elsewhere');
installAs(root, 'linked-other', '@fixture/some-other-name', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'other';\n",
});
const err = await createHostImporter(root)('linked-other').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: an alias naming one package does not license a directory holding another', async () => {
// The check moved its EXPECTATION, not its strictness. The declaration
// says this directory holds `@fixture/alias-declared`; it holds
// `@fixture/alias-installed`, so it is not the declared package's install
// and must not be rescued from.
const root = app('alias-mismatch', 'aliased-wrong', 'npm:@fixture/alias-declared@1');
installAs(root, 'aliased-wrong', '@fixture/alias-installed', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'imposter';\n",
});
const err = await createHostImporter(root)('aliased-wrong').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: a NON-aliased declaration is unchanged — the key is still the expected name', async () => {
// The control the card names: an aliased-install red that also reddens
// this one would mean the finder got looser, not smarter. A plain range
// declares no alias, so a directory holding a different package is refused
// exactly as it was before #14278.
const root = app('plain-range', '@fixture/plain-range', '^1.0.0');
installAs(root, '@fixture/plain-range', '@fixture/somebody-else', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'imposter';\n",
});
const err = await createHostImporter(root)('@fixture/plain-range').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: an alias target carrying a SUBPATH is not a package name, and is refused', async () => {
// `npm:` values are `<name>[@<range>]` — never a subpath. A value that is
// not a bare package name yields no expectation to move to, so the key
// stays, and this directory (named for the subpath's package) is refused.
const root = app('alias-subpath-value', 'aliased-bad', 'npm:@fixture/alias-bad/deep@1');
installAs(root, 'aliased-bad', '@fixture/alias-bad', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'imposter';\n",
});
const err = await createHostImporter(root)('aliased-bad').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: an alias does not reopen the hostRoot boundary', async () => {
// Every other axis of the finder's tightness is unaffected by the alias:
// the one directory consulted is still `<hostRoot>/node_modules/<key>`,
// never a parent's. Installed one level up, under the same key and the
// aliased name, it is still not this app's install.
const parent = mkdtempSync(join(tmpdir(), 'os-aliased-parent-'));
roots.push(parent);
installAs(parent, 'aliased-up', '@fixture/alias-parent', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'from-parent';\n",
});
const root = join(parent, 'app');
mkdirSync(root, { recursive: true });
writeFileSync(
join(root, 'package.json'),
JSON.stringify({
name: 'nested-aliased-host-fixture',
type: 'module',
dependencies: { 'aliased-up': 'npm:@fixture/alias-parent@1' },
}),
'utf8',
);
const err = await createHostImporter(root)('aliased-up').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
34 changes: 34 additions & 0 deletions .changeset/aliased-install-host-importer.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
---
"@objectstack/types": patch
---

fix(types): an aliased install (`"foo": "npm:bar@1"`) is now found by the host importer's ESM-only fallback

`createHostImporter`'s #14041 fallback finder verifies the one directory it
consults — `<hostRoot>/node_modules/<key>` — by matching that directory's
`package.json` `name` against the declared package name. An aliased install
fails that check by construction: `{ "dependencies": { "foo": "npm:bar@1" } }`
puts a manifest named `bar` at `node_modules/foo`. The finder answered
`absent`, and an ESM-only aliased package therefore kept the pre-#14041 INSTALL
wording — a confidently-wrong remedy sending an operator to run `pnpm install`
against an install that is already correct, on a declaration shape
`packageNameFromSpecifier`'s own documentation blesses.

The declaration is now parsed for the name it promises: `npm:bar@1`,
`npm:@acme/x@^2` and the aliased `workspace:bar@*` name the package installed
under the key, so that is the manifest name the finder expects there. An
aliased ESM-only package is rescued exactly as a plain one is, and an aliased
install publishing nothing loadable gets the message about the PACKAGE's own
shape instead of the INSTALL message.

⚠️ The manifest-name check itself is NOT loosened — that check is what keeps
the fallback strictly tighter than the CJS resolution it backs up (#4719's
declaration gate, from the fallback side). What moved is the EXPECTATION, still
authored by the host and still read out of the host's own `package.json`: an
alias naming one package refuses a directory holding another, a non-aliased
declaration is unchanged, and a value that is not a bare package name — a
`workspace:` range, an alias carrying a subpath — yields no expectation to move
to, so the key stays and today's refusal is kept. `link:` and `file:` name a
LOCATION rather than a package, so no name is derivable from them at all; they
keep the key expectation, and with it the conservative direction the finder had
before.
271 changes: 271 additions & 0 deletions packages/types/src/node.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1243,3 +1243,274 @@ describe('the declared leg loads an ESM-only package via a hostRoot node_modules
expect((err as Error).message).toMatch(/INSTALL problem/);
});
});

/**
* ── #14278: an ALIASED install names its own package, and the finder must know ─
*
* `{ "dependencies": { "foo": "npm:bar@1" } }` installs the package `bar` at
* `<hostRoot>/node_modules/foo`: the manifest there is named `bar`, while the
* importable specifier — and the declaration key — is `foo`. The #14041
* fallback finder verifies the directory it consults by matching that
* manifest's `name` against the declared name, so it refused every aliased
* install BY CONSTRUCTION, and an ESM-only aliased package kept the
* pre-#14041 INSTALL wording: a confidently-wrong remedy against an install
* that is already correct.
*
* The fix parses the DECLARATION, never the directory. The host's own
* `package.json` says which package `foo` is an alias for, so the expectation
* is still authored by the host and the check is exactly as tight as it was —
* what moves is the EXPECTED NAME, never the comparison. The TIGHTNESS cases
* below are that proof: an alias naming one package does not license a
* directory holding another, and a NON-aliased declaration is untouched (the
* `manifest NAMES the declared package` case above is that control, and it
* stays green).
*
* `link:` / `file:` name a LOCATION rather than a package, so no name can be
* derived from them at all; they keep the key expectation, and with it today's
* conservative refusal.
*/
describe('an aliased install is verified against the name its DECLARATION names (#14278)', () => {
/** The card's exact shape: `import` condition only, no `require`, no `main`. */
const ESM_ONLY_EXPORTS = { '.': { import: './dist/index.js' } };

const roots: string[] = [];

afterAll(() => {
for (const dir of roots) rmSync(dir, { recursive: true, force: true });
});

/** A fresh host app declaring `key` with the literal specifier under test. */
function app(tag: string, key: string, specifier: string): string {
const root = mkdtempSync(join(tmpdir(), `os-aliased-${tag}-`));
roots.push(root);
writeFileSync(
join(root, 'package.json'),
JSON.stringify({
name: 'aliased-host-fixture',
type: 'module',
dependencies: { [key]: specifier },
}),
'utf8',
);
return root;
}

/**
* Install a package NAMED `manifestName` at `node_modules/<key>` — the
* on-disk shape every aliasing package manager produces. (`link:` /
* `workspace:` installs put a SYMLINK there instead; the finder reads
* `node_modules/<key>` either way and realpaths only afterwards, so a plain
* directory exercises the same code.)
*/
function installAs(
root: string,
key: string,
manifestName: string,
manifest: Record<string, unknown>,
files: Record<string, string>,
): void {
const dir = join(root, 'node_modules', ...key.split('/'));
mkdirSync(dir, { recursive: true });
writeFileSync(
join(dir, 'package.json'),
JSON.stringify({
name: manifestName,
version: '0.0.0-fixture',
type: 'module',
...manifest,
}),
'utf8',
);
for (const rel of Object.keys(files)) {
const target = join(dir, rel);
mkdirSync(dirname(target), { recursive: true });
writeFileSync(target, files[rel] as string, 'utf8');
}
}

it('PRECONDITION: an aliased ESM-only install reaches the fallback at all', () => {
// Same precondition the #14041 suite pins, re-measured through an alias:
// the CJS resolver FINDS `node_modules/aliased` and refuses on the
// CONDITION, so everything below is decided inside that throw's catch —
// the fallback is the only thing that can answer, and before this fix it
// answered `absent`.
const root = app('precondition', 'aliased', 'npm:@fixture/alias-target@1');
installAs(root, 'aliased', '@fixture/alias-target', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'aliased-esm-only';\n",
});
let code: string | undefined;
try {
createHostRequire(root).resolve('aliased');
} catch (e) {
code = (e as { code?: string }).code;
}
expect(code).toBe('ERR_PACKAGE_PATH_NOT_EXPORTED');
});

it('THE CARD: an aliased ESM-only package is rescued, not reported as an INSTALL problem', async () => {
const root = app('loads', 'aliased-esm', 'npm:@fixture/alias-esm-only@1');
installAs(root, 'aliased-esm', '@fixture/alias-esm-only', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'aliased-esm-only';\n",
});
expect((await createHostImporter(root)('aliased-esm')).BUILD).toBe('aliased-esm-only');
});

it('THE CARD (wording): an aliased install with no loadable entry gets the PACKAGE message', async () => {
// The card's named deliverable: the aliased install answers with the
// ESM-only wording (`declared-no-loadable-entry`) instead of the INSTALL
// wording, because the install is fine and no install action can help.
const root = app('types-only', 'aliased-types', 'npm:@fixture/alias-types-only@1');
installAs(
root,
'aliased-types',
'@fixture/alias-types-only',
{ exports: { '.': { types: './dist/index.d.ts' } } },
{ 'dist/index.d.ts': 'export declare const BUILD: string;\n' },
);
const err = await createHostImporter(root)('aliased-types').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-no-loadable-entry');
expect((err as Error).message).toMatch(/publishes no entry/);
expect((err as Error).message).not.toMatch(/INSTALL problem/);
});

it('a SCOPED key aliasing an unscoped package is rescued too', async () => {
// Both halves of the mapping are free to be scoped or not: the key is a
// directory path under `node_modules`, the alias target is a package name.
const root = app('scoped-key', '@app/aliased', 'npm:alias-unscoped@^2.0.0');
installAs(root, '@app/aliased', 'alias-unscoped', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'alias-unscoped';\n",
});
expect((await createHostImporter(root)('@app/aliased')).BUILD).toBe('alias-unscoped');
});

it('an aliased SUBPATH resolves against the aliased package', async () => {
const root = app('subpath', 'aliased-sub', 'npm:@fixture/alias-subpaths@1');
installAs(
root,
'aliased-sub',
'@fixture/alias-subpaths',
{ exports: { '.': { import: './dist/index.js' }, './plugin': { import: './dist/plugin.js' } } },
{
'dist/index.js': "export const WHERE = 'root';\n",
'dist/plugin.js': "export const WHERE = 'plugin';\n",
},
);
expect((await createHostImporter(root)('aliased-sub/plugin')).WHERE).toBe('plugin');
});

it('an alias with no version range names its target just the same', async () => {
const root = app('no-range', 'aliased-bare', 'npm:@fixture/alias-bare');
installAs(root, 'aliased-bare', '@fixture/alias-bare', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'alias-bare';\n",
});
expect((await createHostImporter(root)('aliased-bare')).BUILD).toBe('alias-bare');
});

it('a `workspace:` ALIAS names its target; a plain `workspace:` range does not', async () => {
// pnpm spells an aliased workspace dependency `workspace:<name>@<range>`;
// `workspace:*` / `workspace:^1.2.3` carry a RANGE only, so the key stays
// the expected name.
const aliased = app('workspace-alias', 'ws-aliased', 'workspace:@fixture/ws-target@*');
installAs(aliased, 'ws-aliased', '@fixture/ws-target', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'ws-target';\n",
});
expect((await createHostImporter(aliased)('ws-aliased')).BUILD).toBe('ws-target');

const plain = app('workspace-plain', '@fixture/ws-plain', 'workspace:*');
installAs(plain, '@fixture/ws-plain', '@fixture/ws-plain', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'ws-plain';\n",
});
expect((await createHostImporter(plain)('@fixture/ws-plain')).BUILD).toBe('ws-plain');
});

it('a `link:` specifier names a LOCATION, so the KEY stays the expected name', async () => {
// The linked package installed under its own key loads, exactly as before.
const root = app('link-ok', 'linked', 'link:../linked');
installAs(root, 'linked', 'linked', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'linked';\n",
});
expect((await createHostImporter(root)('linked')).BUILD).toBe('linked');
});

it('BOUNDARY: a `link:` target whose manifest names something else keeps the refusal', async () => {
// Deliberate, and the reason `link:` is not "parsed" into a name: a path
// specifier carries no package name for the finder to expect, so there is
// nothing to verify a differing manifest against. The conservative
// direction (refuse, never load the wrong thing) is kept rather than
// guessed at — widening it here would make the finder looser than the
// manifest-name check exists to be.
const root = app('link-mismatch', 'linked-other', 'link:../elsewhere');
installAs(root, 'linked-other', '@fixture/some-other-name', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'other';\n",
});
const err = await createHostImporter(root)('linked-other').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: an alias naming one package does not license a directory holding another', async () => {
// The check moved its EXPECTATION, not its strictness. The declaration
// says this directory holds `@fixture/alias-declared`; it holds
// `@fixture/alias-installed`, so it is not the declared package's install
// and must not be rescued from.
const root = app('alias-mismatch', 'aliased-wrong', 'npm:@fixture/alias-declared@1');
installAs(root, 'aliased-wrong', '@fixture/alias-installed', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'imposter';\n",
});
const err = await createHostImporter(root)('aliased-wrong').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: a NON-aliased declaration is unchanged — the key is still the expected name', async () => {
// The control the card names: an aliased-install red that also reddens
// this one would mean the finder got looser, not smarter. A plain range
// declares no alias, so a directory holding a different package is refused
// exactly as it was before #14278.
const root = app('plain-range', '@fixture/plain-range', '^1.0.0');
installAs(root, '@fixture/plain-range', '@fixture/somebody-else', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'imposter';\n",
});
const err = await createHostImporter(root)('@fixture/plain-range').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: an alias target carrying a SUBPATH is not a package name, and is refused', async () => {
// `npm:` values are `<name>[@<range>]` — never a subpath. A value that is
// not a bare package name yields no expectation to move to, so the key
// stays, and this directory (named for the subpath's package) is refused.
const root = app('alias-subpath-value', 'aliased-bad', 'npm:@fixture/alias-bad/deep@1');
installAs(root, 'aliased-bad', '@fixture/alias-bad', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'imposter';\n",
});
const err = await createHostImporter(root)('aliased-bad').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: an alias does not reopen the hostRoot boundary', async () => {
// Every other axis of the finder's tightness is unaffected by the alias:
// the one directory consulted is still `<hostRoot>/node_modules/<key>`,
// never a parent's. Installed one level up, under the same key and the
// aliased name, it is still not this app's install.
const parent = mkdtempSync(join(tmpdir(), 'os-aliased-parent-'));
roots.push(parent);
installAs(parent, 'aliased-up', '@fixture/alias-parent', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'from-parent';\n",
});
const root = join(parent, 'app');
mkdirSync(root, { recursive: true });
writeFileSync(
join(root, 'package.json'),
JSON.stringify({
name: 'nested-aliased-host-fixture',
type: 'module',
dependencies: { 'aliased-up': 'npm:@fixture/alias-parent@1' },
}),
'utf8',
);
const err = await createHostImporter(root)('aliased-up').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
34 changes: 34 additions & 0 deletions .changeset/aliased-install-host-importer.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
---
"@objectstack/types": patch
---

fix(types): an aliased install (`"foo": "npm:bar@1"`) is now found by the host importer's ESM-only fallback

`createHostImporter`'s #14041 fallback finder verifies the one directory it
consults — `<hostRoot>/node_modules/<key>` — by matching that directory's
`package.json` `name` against the declared package name. An aliased install
fails that check by construction: `{ "dependencies": { "foo": "npm:bar@1" } }`
puts a manifest named `bar` at `node_modules/foo`. The finder answered
`absent`, and an ESM-only aliased package therefore kept the pre-#14041 INSTALL
wording — a confidently-wrong remedy sending an operator to run `pnpm install`
against an install that is already correct, on a declaration shape
`packageNameFromSpecifier`'s own documentation blesses.

The declaration is now parsed for the name it promises: `npm:bar@1`,
`npm:@acme/x@^2` and the aliased `workspace:bar@*` name the package installed
under the key, so that is the manifest name the finder expects there. An
aliased ESM-only package is rescued exactly as a plain one is, and an aliased
install publishing nothing loadable gets the message about the PACKAGE's own
shape instead of the INSTALL message.

⚠️ The manifest-name check itself is NOT loosened — that check is what keeps
the fallback strictly tighter than the CJS resolution it backs up (#4719's
declaration gate, from the fallback side). What moved is the EXPECTATION, still
authored by the host and still read out of the host's own `package.json`: an
alias naming one package refuses a directory holding another, a non-aliased
declaration is unchanged, and a value that is not a bare package name — a
`workspace:` range, an alias carrying a subpath — yields no expectation to move
to, so the key stays and today's refusal is kept. `link:` and `file:` name a
LOCATION rather than a package, so no name is derivable from them at all; they
keep the key expectation, and with it the conservative direction the finder had
before.
271 changes: 271 additions & 0 deletions packages/types/src/node.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1243,3 +1243,274 @@ describe('the declared leg loads an ESM-only package via a hostRoot node_modules
expect((err as Error).message).toMatch(/INSTALL problem/);
});
});

/**
* ── #14278: an ALIASED install names its own package, and the finder must know ─
*
* `{ "dependencies": { "foo": "npm:bar@1" } }` installs the package `bar` at
* `<hostRoot>/node_modules/foo`: the manifest there is named `bar`, while the
* importable specifier — and the declaration key — is `foo`. The #14041
* fallback finder verifies the directory it consults by matching that
* manifest's `name` against the declared name, so it refused every aliased
* install BY CONSTRUCTION, and an ESM-only aliased package kept the
* pre-#14041 INSTALL wording: a confidently-wrong remedy against an install
* that is already correct.
*
* The fix parses the DECLARATION, never the directory. The host's own
* `package.json` says which package `foo` is an alias for, so the expectation
* is still authored by the host and the check is exactly as tight as it was —
* what moves is the EXPECTED NAME, never the comparison. The TIGHTNESS cases
* below are that proof: an alias naming one package does not license a
* directory holding another, and a NON-aliased declaration is untouched (the
* `manifest NAMES the declared package` case above is that control, and it
* stays green).
*
* `link:` / `file:` name a LOCATION rather than a package, so no name can be
* derived from them at all; they keep the key expectation, and with it today's
* conservative refusal.
*/
describe('an aliased install is verified against the name its DECLARATION names (#14278)', () => {
/** The card's exact shape: `import` condition only, no `require`, no `main`. */
const ESM_ONLY_EXPORTS = { '.': { import: './dist/index.js' } };

const roots: string[] = [];

afterAll(() => {
for (const dir of roots) rmSync(dir, { recursive: true, force: true });
});

/** A fresh host app declaring `key` with the literal specifier under test. */
function app(tag: string, key: string, specifier: string): string {
const root = mkdtempSync(join(tmpdir(), `os-aliased-${tag}-`));
roots.push(root);
writeFileSync(
join(root, 'package.json'),
JSON.stringify({
name: 'aliased-host-fixture',
type: 'module',
dependencies: { [key]: specifier },
}),
'utf8',
);
return root;
}

/**
* Install a package NAMED `manifestName` at `node_modules/<key>` — the
* on-disk shape every aliasing package manager produces. (`link:` /
* `workspace:` installs put a SYMLINK there instead; the finder reads
* `node_modules/<key>` either way and realpaths only afterwards, so a plain
* directory exercises the same code.)
*/
function installAs(
root: string,
key: string,
manifestName: string,
manifest: Record<string, unknown>,
files: Record<string, string>,
): void {
const dir = join(root, 'node_modules', ...key.split('/'));
mkdirSync(dir, { recursive: true });
writeFileSync(
join(dir, 'package.json'),
JSON.stringify({
name: manifestName,
version: '0.0.0-fixture',
type: 'module',
...manifest,
}),
'utf8',
);
for (const rel of Object.keys(files)) {
const target = join(dir, rel);
mkdirSync(dirname(target), { recursive: true });
writeFileSync(target, files[rel] as string, 'utf8');
}
}

it('PRECONDITION: an aliased ESM-only install reaches the fallback at all', () => {
// Same precondition the #14041 suite pins, re-measured through an alias:
// the CJS resolver FINDS `node_modules/aliased` and refuses on the
// CONDITION, so everything below is decided inside that throw's catch —
// the fallback is the only thing that can answer, and before this fix it
// answered `absent`.
const root = app('precondition', 'aliased', 'npm:@fixture/alias-target@1');
installAs(root, 'aliased', '@fixture/alias-target', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'aliased-esm-only';\n",
});
let code: string | undefined;
try {
createHostRequire(root).resolve('aliased');
} catch (e) {
code = (e as { code?: string }).code;
}
expect(code).toBe('ERR_PACKAGE_PATH_NOT_EXPORTED');
});

it('THE CARD: an aliased ESM-only package is rescued, not reported as an INSTALL problem', async () => {
const root = app('loads', 'aliased-esm', 'npm:@fixture/alias-esm-only@1');
installAs(root, 'aliased-esm', '@fixture/alias-esm-only', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'aliased-esm-only';\n",
});
expect((await createHostImporter(root)('aliased-esm')).BUILD).toBe('aliased-esm-only');
});

it('THE CARD (wording): an aliased install with no loadable entry gets the PACKAGE message', async () => {
// The card's named deliverable: the aliased install answers with the
// ESM-only wording (`declared-no-loadable-entry`) instead of the INSTALL
// wording, because the install is fine and no install action can help.
const root = app('types-only', 'aliased-types', 'npm:@fixture/alias-types-only@1');
installAs(
root,
'aliased-types',
'@fixture/alias-types-only',
{ exports: { '.': { types: './dist/index.d.ts' } } },
{ 'dist/index.d.ts': 'export declare const BUILD: string;\n' },
);
const err = await createHostImporter(root)('aliased-types').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-no-loadable-entry');
expect((err as Error).message).toMatch(/publishes no entry/);
expect((err as Error).message).not.toMatch(/INSTALL problem/);
});

it('a SCOPED key aliasing an unscoped package is rescued too', async () => {
// Both halves of the mapping are free to be scoped or not: the key is a
// directory path under `node_modules`, the alias target is a package name.
const root = app('scoped-key', '@app/aliased', 'npm:alias-unscoped@^2.0.0');
installAs(root, '@app/aliased', 'alias-unscoped', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'alias-unscoped';\n",
});
expect((await createHostImporter(root)('@app/aliased')).BUILD).toBe('alias-unscoped');
});

it('an aliased SUBPATH resolves against the aliased package', async () => {
const root = app('subpath', 'aliased-sub', 'npm:@fixture/alias-subpaths@1');
installAs(
root,
'aliased-sub',
'@fixture/alias-subpaths',
{ exports: { '.': { import: './dist/index.js' }, './plugin': { import: './dist/plugin.js' } } },
{
'dist/index.js': "export const WHERE = 'root';\n",
'dist/plugin.js': "export const WHERE = 'plugin';\n",
},
);
expect((await createHostImporter(root)('aliased-sub/plugin')).WHERE).toBe('plugin');
});

it('an alias with no version range names its target just the same', async () => {
const root = app('no-range', 'aliased-bare', 'npm:@fixture/alias-bare');
installAs(root, 'aliased-bare', '@fixture/alias-bare', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'alias-bare';\n",
});
expect((await createHostImporter(root)('aliased-bare')).BUILD).toBe('alias-bare');
});

it('a `workspace:` ALIAS names its target; a plain `workspace:` range does not', async () => {
// pnpm spells an aliased workspace dependency `workspace:<name>@<range>`;
// `workspace:*` / `workspace:^1.2.3` carry a RANGE only, so the key stays
// the expected name.
const aliased = app('workspace-alias', 'ws-aliased', 'workspace:@fixture/ws-target@*');
installAs(aliased, 'ws-aliased', '@fixture/ws-target', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'ws-target';\n",
});
expect((await createHostImporter(aliased)('ws-aliased')).BUILD).toBe('ws-target');

const plain = app('workspace-plain', '@fixture/ws-plain', 'workspace:*');
installAs(plain, '@fixture/ws-plain', '@fixture/ws-plain', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'ws-plain';\n",
});
expect((await createHostImporter(plain)('@fixture/ws-plain')).BUILD).toBe('ws-plain');
});

it('a `link:` specifier names a LOCATION, so the KEY stays the expected name', async () => {
// The linked package installed under its own key loads, exactly as before.
const root = app('link-ok', 'linked', 'link:../linked');
installAs(root, 'linked', 'linked', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'linked';\n",
});
expect((await createHostImporter(root)('linked')).BUILD).toBe('linked');
});

it('BOUNDARY: a `link:` target whose manifest names something else keeps the refusal', async () => {
// Deliberate, and the reason `link:` is not "parsed" into a name: a path
// specifier carries no package name for the finder to expect, so there is
// nothing to verify a differing manifest against. The conservative
// direction (refuse, never load the wrong thing) is kept rather than
// guessed at — widening it here would make the finder looser than the
// manifest-name check exists to be.
const root = app('link-mismatch', 'linked-other', 'link:../elsewhere');
installAs(root, 'linked-other', '@fixture/some-other-name', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'other';\n",
});
const err = await createHostImporter(root)('linked-other').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: an alias naming one package does not license a directory holding another', async () => {
// The check moved its EXPECTATION, not its strictness. The declaration
// says this directory holds `@fixture/alias-declared`; it holds
// `@fixture/alias-installed`, so it is not the declared package's install
// and must not be rescued from.
const root = app('alias-mismatch', 'aliased-wrong', 'npm:@fixture/alias-declared@1');
installAs(root, 'aliased-wrong', '@fixture/alias-installed', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'imposter';\n",
});
const err = await createHostImporter(root)('aliased-wrong').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: a NON-aliased declaration is unchanged — the key is still the expected name', async () => {
// The control the card names: an aliased-install red that also reddens
// this one would mean the finder got looser, not smarter. A plain range
// declares no alias, so a directory holding a different package is refused
// exactly as it was before #14278.
const root = app('plain-range', '@fixture/plain-range', '^1.0.0');
installAs(root, '@fixture/plain-range', '@fixture/somebody-else', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'imposter';\n",
});
const err = await createHostImporter(root)('@fixture/plain-range').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: an alias target carrying a SUBPATH is not a package name, and is refused', async () => {
// `npm:` values are `<name>[@<range>]` — never a subpath. A value that is
// not a bare package name yields no expectation to move to, so the key
// stays, and this directory (named for the subpath's package) is refused.
const root = app('alias-subpath-value', 'aliased-bad', 'npm:@fixture/alias-bad/deep@1');
installAs(root, 'aliased-bad', '@fixture/alias-bad', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'imposter';\n",
});
const err = await createHostImporter(root)('aliased-bad').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: an alias does not reopen the hostRoot boundary', async () => {
// Every other axis of the finder's tightness is unaffected by the alias:
// the one directory consulted is still `<hostRoot>/node_modules/<key>`,
// never a parent's. Installed one level up, under the same key and the
// aliased name, it is still not this app's install.
const parent = mkdtempSync(join(tmpdir(), 'os-aliased-parent-'));
roots.push(parent);
installAs(parent, 'aliased-up', '@fixture/alias-parent', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'from-parent';\n",
});
const root = join(parent, 'app');
mkdirSync(root, { recursive: true });
writeFileSync(
join(root, 'package.json'),
JSON.stringify({
name: 'nested-aliased-host-fixture',
type: 'module',
dependencies: { 'aliased-up': 'npm:@fixture/alias-parent@1' },
}),
'utf8',
);
const err = await createHostImporter(root)('aliased-up').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
34 changes: 34 additions & 0 deletions .changeset/aliased-install-host-importer.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
---
"@objectstack/types": patch
---

fix(types): an aliased install (`"foo": "npm:bar@1"`) is now found by the host importer's ESM-only fallback

`createHostImporter`'s #14041 fallback finder verifies the one directory it
consults — `<hostRoot>/node_modules/<key>` — by matching that directory's
`package.json` `name` against the declared package name. An aliased install
fails that check by construction: `{ "dependencies": { "foo": "npm:bar@1" } }`
puts a manifest named `bar` at `node_modules/foo`. The finder answered
`absent`, and an ESM-only aliased package therefore kept the pre-#14041 INSTALL
wording — a confidently-wrong remedy sending an operator to run `pnpm install`
against an install that is already correct, on a declaration shape
`packageNameFromSpecifier`'s own documentation blesses.

The declaration is now parsed for the name it promises: `npm:bar@1`,
`npm:@acme/x@^2` and the aliased `workspace:bar@*` name the package installed
under the key, so that is the manifest name the finder expects there. An
aliased ESM-only package is rescued exactly as a plain one is, and an aliased
install publishing nothing loadable gets the message about the PACKAGE's own
shape instead of the INSTALL message.

⚠️ The manifest-name check itself is NOT loosened — that check is what keeps
the fallback strictly tighter than the CJS resolution it backs up (#4719's
declaration gate, from the fallback side). What moved is the EXPECTATION, still
authored by the host and still read out of the host's own `package.json`: an
alias naming one package refuses a directory holding another, a non-aliased
declaration is unchanged, and a value that is not a bare package name — a
`workspace:` range, an alias carrying a subpath — yields no expectation to move
to, so the key stays and today's refusal is kept. `link:` and `file:` name a
LOCATION rather than a package, so no name is derivable from them at all; they
keep the key expectation, and with it the conservative direction the finder had
before.
271 changes: 271 additions & 0 deletions packages/types/src/node.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1243,3 +1243,274 @@ describe('the declared leg loads an ESM-only package via a hostRoot node_modules
expect((err as Error).message).toMatch(/INSTALL problem/);
});
});

/**
* ── #14278: an ALIASED install names its own package, and the finder must know ─
*
* `{ "dependencies": { "foo": "npm:bar@1" } }` installs the package `bar` at
* `<hostRoot>/node_modules/foo`: the manifest there is named `bar`, while the
* importable specifier — and the declaration key — is `foo`. The #14041
* fallback finder verifies the directory it consults by matching that
* manifest's `name` against the declared name, so it refused every aliased
* install BY CONSTRUCTION, and an ESM-only aliased package kept the
* pre-#14041 INSTALL wording: a confidently-wrong remedy against an install
* that is already correct.
*
* The fix parses the DECLARATION, never the directory. The host's own
* `package.json` says which package `foo` is an alias for, so the expectation
* is still authored by the host and the check is exactly as tight as it was —
* what moves is the EXPECTED NAME, never the comparison. The TIGHTNESS cases
* below are that proof: an alias naming one package does not license a
* directory holding another, and a NON-aliased declaration is untouched (the
* `manifest NAMES the declared package` case above is that control, and it
* stays green).
*
* `link:` / `file:` name a LOCATION rather than a package, so no name can be
* derived from them at all; they keep the key expectation, and with it today's
* conservative refusal.
*/
describe('an aliased install is verified against the name its DECLARATION names (#14278)', () => {
/** The card's exact shape: `import` condition only, no `require`, no `main`. */
const ESM_ONLY_EXPORTS = { '.': { import: './dist/index.js' } };

const roots: string[] = [];

afterAll(() => {
for (const dir of roots) rmSync(dir, { recursive: true, force: true });
});

/** A fresh host app declaring `key` with the literal specifier under test. */
function app(tag: string, key: string, specifier: string): string {
const root = mkdtempSync(join(tmpdir(), `os-aliased-${tag}-`));
roots.push(root);
writeFileSync(
join(root, 'package.json'),
JSON.stringify({
name: 'aliased-host-fixture',
type: 'module',
dependencies: { [key]: specifier },
}),
'utf8',
);
return root;
}

/**
* Install a package NAMED `manifestName` at `node_modules/<key>` — the
* on-disk shape every aliasing package manager produces. (`link:` /
* `workspace:` installs put a SYMLINK there instead; the finder reads
* `node_modules/<key>` either way and realpaths only afterwards, so a plain
* directory exercises the same code.)
*/
function installAs(
root: string,
key: string,
manifestName: string,
manifest: Record<string, unknown>,
files: Record<string, string>,
): void {
const dir = join(root, 'node_modules', ...key.split('/'));
mkdirSync(dir, { recursive: true });
writeFileSync(
join(dir, 'package.json'),
JSON.stringify({
name: manifestName,
version: '0.0.0-fixture',
type: 'module',
...manifest,
}),
'utf8',
);
for (const rel of Object.keys(files)) {
const target = join(dir, rel);
mkdirSync(dirname(target), { recursive: true });
writeFileSync(target, files[rel] as string, 'utf8');
}
}

it('PRECONDITION: an aliased ESM-only install reaches the fallback at all', () => {
// Same precondition the #14041 suite pins, re-measured through an alias:
// the CJS resolver FINDS `node_modules/aliased` and refuses on the
// CONDITION, so everything below is decided inside that throw's catch —
// the fallback is the only thing that can answer, and before this fix it
// answered `absent`.
const root = app('precondition', 'aliased', 'npm:@fixture/alias-target@1');
installAs(root, 'aliased', '@fixture/alias-target', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'aliased-esm-only';\n",
});
let code: string | undefined;
try {
createHostRequire(root).resolve('aliased');
} catch (e) {
code = (e as { code?: string }).code;
}
expect(code).toBe('ERR_PACKAGE_PATH_NOT_EXPORTED');
});

it('THE CARD: an aliased ESM-only package is rescued, not reported as an INSTALL problem', async () => {
const root = app('loads', 'aliased-esm', 'npm:@fixture/alias-esm-only@1');
installAs(root, 'aliased-esm', '@fixture/alias-esm-only', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'aliased-esm-only';\n",
});
expect((await createHostImporter(root)('aliased-esm')).BUILD).toBe('aliased-esm-only');
});

it('THE CARD (wording): an aliased install with no loadable entry gets the PACKAGE message', async () => {
// The card's named deliverable: the aliased install answers with the
// ESM-only wording (`declared-no-loadable-entry`) instead of the INSTALL
// wording, because the install is fine and no install action can help.
const root = app('types-only', 'aliased-types', 'npm:@fixture/alias-types-only@1');
installAs(
root,
'aliased-types',
'@fixture/alias-types-only',
{ exports: { '.': { types: './dist/index.d.ts' } } },
{ 'dist/index.d.ts': 'export declare const BUILD: string;\n' },
);
const err = await createHostImporter(root)('aliased-types').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-no-loadable-entry');
expect((err as Error).message).toMatch(/publishes no entry/);
expect((err as Error).message).not.toMatch(/INSTALL problem/);
});

it('a SCOPED key aliasing an unscoped package is rescued too', async () => {
// Both halves of the mapping are free to be scoped or not: the key is a
// directory path under `node_modules`, the alias target is a package name.
const root = app('scoped-key', '@app/aliased', 'npm:alias-unscoped@^2.0.0');
installAs(root, '@app/aliased', 'alias-unscoped', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'alias-unscoped';\n",
});
expect((await createHostImporter(root)('@app/aliased')).BUILD).toBe('alias-unscoped');
});

it('an aliased SUBPATH resolves against the aliased package', async () => {
const root = app('subpath', 'aliased-sub', 'npm:@fixture/alias-subpaths@1');
installAs(
root,
'aliased-sub',
'@fixture/alias-subpaths',
{ exports: { '.': { import: './dist/index.js' }, './plugin': { import: './dist/plugin.js' } } },
{
'dist/index.js': "export const WHERE = 'root';\n",
'dist/plugin.js': "export const WHERE = 'plugin';\n",
},
);
expect((await createHostImporter(root)('aliased-sub/plugin')).WHERE).toBe('plugin');
});

it('an alias with no version range names its target just the same', async () => {
const root = app('no-range', 'aliased-bare', 'npm:@fixture/alias-bare');
installAs(root, 'aliased-bare', '@fixture/alias-bare', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'alias-bare';\n",
});
expect((await createHostImporter(root)('aliased-bare')).BUILD).toBe('alias-bare');
});

it('a `workspace:` ALIAS names its target; a plain `workspace:` range does not', async () => {
// pnpm spells an aliased workspace dependency `workspace:<name>@<range>`;
// `workspace:*` / `workspace:^1.2.3` carry a RANGE only, so the key stays
// the expected name.
const aliased = app('workspace-alias', 'ws-aliased', 'workspace:@fixture/ws-target@*');
installAs(aliased, 'ws-aliased', '@fixture/ws-target', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'ws-target';\n",
});
expect((await createHostImporter(aliased)('ws-aliased')).BUILD).toBe('ws-target');

const plain = app('workspace-plain', '@fixture/ws-plain', 'workspace:*');
installAs(plain, '@fixture/ws-plain', '@fixture/ws-plain', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'ws-plain';\n",
});
expect((await createHostImporter(plain)('@fixture/ws-plain')).BUILD).toBe('ws-plain');
});

it('a `link:` specifier names a LOCATION, so the KEY stays the expected name', async () => {
// The linked package installed under its own key loads, exactly as before.
const root = app('link-ok', 'linked', 'link:../linked');
installAs(root, 'linked', 'linked', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'linked';\n",
});
expect((await createHostImporter(root)('linked')).BUILD).toBe('linked');
});

it('BOUNDARY: a `link:` target whose manifest names something else keeps the refusal', async () => {
// Deliberate, and the reason `link:` is not "parsed" into a name: a path
// specifier carries no package name for the finder to expect, so there is
// nothing to verify a differing manifest against. The conservative
// direction (refuse, never load the wrong thing) is kept rather than
// guessed at — widening it here would make the finder looser than the
// manifest-name check exists to be.
const root = app('link-mismatch', 'linked-other', 'link:../elsewhere');
installAs(root, 'linked-other', '@fixture/some-other-name', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'other';\n",
});
const err = await createHostImporter(root)('linked-other').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: an alias naming one package does not license a directory holding another', async () => {
// The check moved its EXPECTATION, not its strictness. The declaration
// says this directory holds `@fixture/alias-declared`; it holds
// `@fixture/alias-installed`, so it is not the declared package's install
// and must not be rescued from.
const root = app('alias-mismatch', 'aliased-wrong', 'npm:@fixture/alias-declared@1');
installAs(root, 'aliased-wrong', '@fixture/alias-installed', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'imposter';\n",
});
const err = await createHostImporter(root)('aliased-wrong').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: a NON-aliased declaration is unchanged — the key is still the expected name', async () => {
// The control the card names: an aliased-install red that also reddens
// this one would mean the finder got looser, not smarter. A plain range
// declares no alias, so a directory holding a different package is refused
// exactly as it was before #14278.
const root = app('plain-range', '@fixture/plain-range', '^1.0.0');
installAs(root, '@fixture/plain-range', '@fixture/somebody-else', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'imposter';\n",
});
const err = await createHostImporter(root)('@fixture/plain-range').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: an alias target carrying a SUBPATH is not a package name, and is refused', async () => {
// `npm:` values are `<name>[@<range>]` — never a subpath. A value that is
// not a bare package name yields no expectation to move to, so the key
// stays, and this directory (named for the subpath's package) is refused.
const root = app('alias-subpath-value', 'aliased-bad', 'npm:@fixture/alias-bad/deep@1');
installAs(root, 'aliased-bad', '@fixture/alias-bad', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'imposter';\n",
});
const err = await createHostImporter(root)('aliased-bad').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: an alias does not reopen the hostRoot boundary', async () => {
// Every other axis of the finder's tightness is unaffected by the alias:
// the one directory consulted is still `<hostRoot>/node_modules/<key>`,
// never a parent's. Installed one level up, under the same key and the
// aliased name, it is still not this app's install.
const parent = mkdtempSync(join(tmpdir(), 'os-aliased-parent-'));
roots.push(parent);
installAs(parent, 'aliased-up', '@fixture/alias-parent', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'from-parent';\n",
});
const root = join(parent, 'app');
mkdirSync(root, { recursive: true });
writeFileSync(
join(root, 'package.json'),
JSON.stringify({
name: 'nested-aliased-host-fixture',
type: 'module',
dependencies: { 'aliased-up': 'npm:@fixture/alias-parent@1' },
}),
'utf8',
);
const err = await createHostImporter(root)('aliased-up').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
34 changes: 34 additions & 0 deletions .changeset/aliased-install-host-importer.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
---
"@objectstack/types": patch
---

fix(types): an aliased install (`"foo": "npm:bar@1"`) is now found by the host importer's ESM-only fallback

`createHostImporter`'s #14041 fallback finder verifies the one directory it
consults — `<hostRoot>/node_modules/<key>` — by matching that directory's
`package.json` `name` against the declared package name. An aliased install
fails that check by construction: `{ "dependencies": { "foo": "npm:bar@1" } }`
puts a manifest named `bar` at `node_modules/foo`. The finder answered
`absent`, and an ESM-only aliased package therefore kept the pre-#14041 INSTALL
wording — a confidently-wrong remedy sending an operator to run `pnpm install`
against an install that is already correct, on a declaration shape
`packageNameFromSpecifier`'s own documentation blesses.

The declaration is now parsed for the name it promises: `npm:bar@1`,
`npm:@acme/x@^2` and the aliased `workspace:bar@*` name the package installed
under the key, so that is the manifest name the finder expects there. An
aliased ESM-only package is rescued exactly as a plain one is, and an aliased
install publishing nothing loadable gets the message about the PACKAGE's own
shape instead of the INSTALL message.

⚠️ The manifest-name check itself is NOT loosened — that check is what keeps
the fallback strictly tighter than the CJS resolution it backs up (#4719's
declaration gate, from the fallback side). What moved is the EXPECTATION, still
authored by the host and still read out of the host's own `package.json`: an
alias naming one package refuses a directory holding another, a non-aliased
declaration is unchanged, and a value that is not a bare package name — a
`workspace:` range, an alias carrying a subpath — yields no expectation to move
to, so the key stays and today's refusal is kept. `link:` and `file:` name a
LOCATION rather than a package, so no name is derivable from them at all; they
keep the key expectation, and with it the conservative direction the finder had
before.
271 changes: 271 additions & 0 deletions packages/types/src/node.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1243,3 +1243,274 @@ describe('the declared leg loads an ESM-only package via a hostRoot node_modules
expect((err as Error).message).toMatch(/INSTALL problem/);
});
});

/**
* ── #14278: an ALIASED install names its own package, and the finder must know ─
*
* `{ "dependencies": { "foo": "npm:bar@1" } }` installs the package `bar` at
* `<hostRoot>/node_modules/foo`: the manifest there is named `bar`, while the
* importable specifier — and the declaration key — is `foo`. The #14041
* fallback finder verifies the directory it consults by matching that
* manifest's `name` against the declared name, so it refused every aliased
* install BY CONSTRUCTION, and an ESM-only aliased package kept the
* pre-#14041 INSTALL wording: a confidently-wrong remedy against an install
* that is already correct.
*
* The fix parses the DECLARATION, never the directory. The host's own
* `package.json` says which package `foo` is an alias for, so the expectation
* is still authored by the host and the check is exactly as tight as it was —
* what moves is the EXPECTED NAME, never the comparison. The TIGHTNESS cases
* below are that proof: an alias naming one package does not license a
* directory holding another, and a NON-aliased declaration is untouched (the
* `manifest NAMES the declared package` case above is that control, and it
* stays green).
*
* `link:` / `file:` name a LOCATION rather than a package, so no name can be
* derived from them at all; they keep the key expectation, and with it today's
* conservative refusal.
*/
describe('an aliased install is verified against the name its DECLARATION names (#14278)', () => {
/** The card's exact shape: `import` condition only, no `require`, no `main`. */
const ESM_ONLY_EXPORTS = { '.': { import: './dist/index.js' } };

const roots: string[] = [];

afterAll(() => {
for (const dir of roots) rmSync(dir, { recursive: true, force: true });
});

/** A fresh host app declaring `key` with the literal specifier under test. */
function app(tag: string, key: string, specifier: string): string {
const root = mkdtempSync(join(tmpdir(), `os-aliased-${tag}-`));
roots.push(root);
writeFileSync(
join(root, 'package.json'),
JSON.stringify({
name: 'aliased-host-fixture',
type: 'module',
dependencies: { [key]: specifier },
}),
'utf8',
);
return root;
}

/**
* Install a package NAMED `manifestName` at `node_modules/<key>` — the
* on-disk shape every aliasing package manager produces. (`link:` /
* `workspace:` installs put a SYMLINK there instead; the finder reads
* `node_modules/<key>` either way and realpaths only afterwards, so a plain
* directory exercises the same code.)
*/
function installAs(
root: string,
key: string,
manifestName: string,
manifest: Record<string, unknown>,
files: Record<string, string>,
): void {
const dir = join(root, 'node_modules', ...key.split('/'));
mkdirSync(dir, { recursive: true });
writeFileSync(
join(dir, 'package.json'),
JSON.stringify({
name: manifestName,
version: '0.0.0-fixture',
type: 'module',
...manifest,
}),
'utf8',
);
for (const rel of Object.keys(files)) {
const target = join(dir, rel);
mkdirSync(dirname(target), { recursive: true });
writeFileSync(target, files[rel] as string, 'utf8');
}
}

it('PRECONDITION: an aliased ESM-only install reaches the fallback at all', () => {
// Same precondition the #14041 suite pins, re-measured through an alias:
// the CJS resolver FINDS `node_modules/aliased` and refuses on the
// CONDITION, so everything below is decided inside that throw's catch —
// the fallback is the only thing that can answer, and before this fix it
// answered `absent`.
const root = app('precondition', 'aliased', 'npm:@fixture/alias-target@1');
installAs(root, 'aliased', '@fixture/alias-target', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'aliased-esm-only';\n",
});
let code: string | undefined;
try {
createHostRequire(root).resolve('aliased');
} catch (e) {
code = (e as { code?: string }).code;
}
expect(code).toBe('ERR_PACKAGE_PATH_NOT_EXPORTED');
});

it('THE CARD: an aliased ESM-only package is rescued, not reported as an INSTALL problem', async () => {
const root = app('loads', 'aliased-esm', 'npm:@fixture/alias-esm-only@1');
installAs(root, 'aliased-esm', '@fixture/alias-esm-only', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'aliased-esm-only';\n",
});
expect((await createHostImporter(root)('aliased-esm')).BUILD).toBe('aliased-esm-only');
});

it('THE CARD (wording): an aliased install with no loadable entry gets the PACKAGE message', async () => {
// The card's named deliverable: the aliased install answers with the
// ESM-only wording (`declared-no-loadable-entry`) instead of the INSTALL
// wording, because the install is fine and no install action can help.
const root = app('types-only', 'aliased-types', 'npm:@fixture/alias-types-only@1');
installAs(
root,
'aliased-types',
'@fixture/alias-types-only',
{ exports: { '.': { types: './dist/index.d.ts' } } },
{ 'dist/index.d.ts': 'export declare const BUILD: string;\n' },
);
const err = await createHostImporter(root)('aliased-types').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-no-loadable-entry');
expect((err as Error).message).toMatch(/publishes no entry/);
expect((err as Error).message).not.toMatch(/INSTALL problem/);
});

it('a SCOPED key aliasing an unscoped package is rescued too', async () => {
// Both halves of the mapping are free to be scoped or not: the key is a
// directory path under `node_modules`, the alias target is a package name.
const root = app('scoped-key', '@app/aliased', 'npm:alias-unscoped@^2.0.0');
installAs(root, '@app/aliased', 'alias-unscoped', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'alias-unscoped';\n",
});
expect((await createHostImporter(root)('@app/aliased')).BUILD).toBe('alias-unscoped');
});

it('an aliased SUBPATH resolves against the aliased package', async () => {
const root = app('subpath', 'aliased-sub', 'npm:@fixture/alias-subpaths@1');
installAs(
root,
'aliased-sub',
'@fixture/alias-subpaths',
{ exports: { '.': { import: './dist/index.js' }, './plugin': { import: './dist/plugin.js' } } },
{
'dist/index.js': "export const WHERE = 'root';\n",
'dist/plugin.js': "export const WHERE = 'plugin';\n",
},
);
expect((await createHostImporter(root)('aliased-sub/plugin')).WHERE).toBe('plugin');
});

it('an alias with no version range names its target just the same', async () => {
const root = app('no-range', 'aliased-bare', 'npm:@fixture/alias-bare');
installAs(root, 'aliased-bare', '@fixture/alias-bare', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'alias-bare';\n",
});
expect((await createHostImporter(root)('aliased-bare')).BUILD).toBe('alias-bare');
});

it('a `workspace:` ALIAS names its target; a plain `workspace:` range does not', async () => {
// pnpm spells an aliased workspace dependency `workspace:<name>@<range>`;
// `workspace:*` / `workspace:^1.2.3` carry a RANGE only, so the key stays
// the expected name.
const aliased = app('workspace-alias', 'ws-aliased', 'workspace:@fixture/ws-target@*');
installAs(aliased, 'ws-aliased', '@fixture/ws-target', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'ws-target';\n",
});
expect((await createHostImporter(aliased)('ws-aliased')).BUILD).toBe('ws-target');

const plain = app('workspace-plain', '@fixture/ws-plain', 'workspace:*');
installAs(plain, '@fixture/ws-plain', '@fixture/ws-plain', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'ws-plain';\n",
});
expect((await createHostImporter(plain)('@fixture/ws-plain')).BUILD).toBe('ws-plain');
});

it('a `link:` specifier names a LOCATION, so the KEY stays the expected name', async () => {
// The linked package installed under its own key loads, exactly as before.
const root = app('link-ok', 'linked', 'link:../linked');
installAs(root, 'linked', 'linked', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'linked';\n",
});
expect((await createHostImporter(root)('linked')).BUILD).toBe('linked');
});

it('BOUNDARY: a `link:` target whose manifest names something else keeps the refusal', async () => {
// Deliberate, and the reason `link:` is not "parsed" into a name: a path
// specifier carries no package name for the finder to expect, so there is
// nothing to verify a differing manifest against. The conservative
// direction (refuse, never load the wrong thing) is kept rather than
// guessed at — widening it here would make the finder looser than the
// manifest-name check exists to be.
const root = app('link-mismatch', 'linked-other', 'link:../elsewhere');
installAs(root, 'linked-other', '@fixture/some-other-name', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'other';\n",
});
const err = await createHostImporter(root)('linked-other').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: an alias naming one package does not license a directory holding another', async () => {
// The check moved its EXPECTATION, not its strictness. The declaration
// says this directory holds `@fixture/alias-declared`; it holds
// `@fixture/alias-installed`, so it is not the declared package's install
// and must not be rescued from.
const root = app('alias-mismatch', 'aliased-wrong', 'npm:@fixture/alias-declared@1');
installAs(root, 'aliased-wrong', '@fixture/alias-installed', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'imposter';\n",
});
const err = await createHostImporter(root)('aliased-wrong').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: a NON-aliased declaration is unchanged — the key is still the expected name', async () => {
// The control the card names: an aliased-install red that also reddens
// this one would mean the finder got looser, not smarter. A plain range
// declares no alias, so a directory holding a different package is refused
// exactly as it was before #14278.
const root = app('plain-range', '@fixture/plain-range', '^1.0.0');
installAs(root, '@fixture/plain-range', '@fixture/somebody-else', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'imposter';\n",
});
const err = await createHostImporter(root)('@fixture/plain-range').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: an alias target carrying a SUBPATH is not a package name, and is refused', async () => {
// `npm:` values are `<name>[@<range>]` — never a subpath. A value that is
// not a bare package name yields no expectation to move to, so the key
// stays, and this directory (named for the subpath's package) is refused.
const root = app('alias-subpath-value', 'aliased-bad', 'npm:@fixture/alias-bad/deep@1');
installAs(root, 'aliased-bad', '@fixture/alias-bad', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'imposter';\n",
});
const err = await createHostImporter(root)('aliased-bad').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
expect((err as Error).message).toMatch(/INSTALL problem/);
});

it('TIGHTNESS: an alias does not reopen the hostRoot boundary', async () => {
// Every other axis of the finder's tightness is unaffected by the alias:
// the one directory consulted is still `<hostRoot>/node_modules/<key>`,
// never a parent's. Installed one level up, under the same key and the
// aliased name, it is still not this app's install.
const parent = mkdtempSync(join(tmpdir(), 'os-aliased-parent-'));
roots.push(parent);
installAs(parent, 'aliased-up', '@fixture/alias-parent', { exports: ESM_ONLY_EXPORTS }, {
'dist/index.js': "export const BUILD = 'from-parent';\n",
});
const root = join(parent, 'app');
mkdirSync(root, { recursive: true });
writeFileSync(
join(root, 'package.json'),
JSON.stringify({
name: 'nested-aliased-host-fixture',
type: 'module',
dependencies: { 'aliased-up': 'npm:@fixture/alias-parent@1' },
}),
'utf8',
);
const err = await createHostImporter(root)('aliased-up').catch((e: unknown) => e);
expect(hostImportFailureKind(err)).toBe('declared-unresolvable');
});
});
Loading
Loading