Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/schema-data-root-consolidation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@adcp/sdk": patch
---

Internal: consolidated the duplicated `__dirname`-relative schema/data-root resolution across 7 loaders (`validation/schema-loader.ts`, `conformance/schemaLoader.ts`, `server/error-arm-tools.ts`, `v2/projection/{catalog,registry,canonical-properties}.ts`, `testing/storyboard/compliance.ts`) into shared `getPackageRoot()`/`getSchemaDataRoots()` helpers (`src/lib/internal/`). Resolution now anchors via `require.resolve('@adcp/sdk/package.json')` self-reference instead of hand-tuned `path.join(__dirname, '..', ...)` arithmetic tied to each file's own directory depth. No public API changes.
5 changes: 5 additions & 0 deletions .changeset/tsup-conditional-node-shim-banner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@adcp/sdk": patch
---

Fix: the ESM build no longer injects the `node:url`/`node:path`/`node:module` shim banner into every `.mjs` file. Only the (11) files that actually reference `__dirname`, `__filename`, or `require` in their body get it now; pure-data and pure-logic modules — including `enums.generated.mjs`, `inline-enums.generated.mjs`, and everything reachable from the zod-free `./enums` entry point — no longer carry dead Node-only imports that broke strict browser bundlers (Vite, `esbuild --platform=browser`).
6 changes: 4 additions & 2 deletions src/lib/conformance/schemaLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as path from 'path';
import type { ConformanceToolName } from './types';
import { ADCP_VERSION } from '../version';
import { resolveBundleKey } from '../validation/schema-loader';
import { getSchemaDataRoots } from '../internal/schema-data-roots';

type JsonSchema = Record<string, unknown>;

Expand Down Expand Up @@ -70,10 +71,11 @@ function findBundledDir(options: ConformanceSchemaOptions = {}): string {

const version = options.version ?? ADCP_VERSION;
const bundleKey = resolveBundleKey(version);
const distCandidate = path.resolve(__dirname, '..', 'schemas-data', bundleKey, 'bundled');
const { builtSchemasDataRoot, sourceSchemasCacheRoot } = getSchemaDataRoots();
const distCandidate = path.join(builtSchemasDataRoot, bundleKey, 'bundled');
if (fs.existsSync(distCandidate)) return distCandidate;

const srcCandidate = path.resolve(__dirname, '..', '..', '..', 'schemas', 'cache', version, 'bundled');
const srcCandidate = path.join(sourceSchemasCacheRoot, version, 'bundled');
if (fs.existsSync(srcCandidate)) return srcCandidate;

throw new Error(
Expand Down
82 changes: 82 additions & 0 deletions src/lib/internal/package-root.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Resolve the @adcp/sdk package root — the directory containing this
* package's own `package.json` — independent of the calling module's own
* directory depth or bundling shape.
*
* Replaces the per-file `__dirname` arithmetic that used to be hand-tuned to
* each schema/data loader's own depth under `src/lib/` (one `..` for
* `validation/`, two for `v2/projection/`, four for `testing/storyboard/`),
* which silently breaks if any of those files ever moves.
*/

import { existsSync, readFileSync } from 'fs';
import path from 'path';

let cachedRoot: string | undefined;

/**
* Primary: self-reference through this package's own `exports` map
* (`"./package.json": "./package.json"`). Node resolves `require(...)` calls
* (bare form, not `.resolve` chained) so tsup's conditional ESM shim still
* detects this file needs `require` injected under `.mjs` output.
*/
function resolveViaModuleResolution(): string | undefined {
try {
const pkg = require('@adcp/sdk/package.json') as { name?: string };
if (pkg.name !== '@adcp/sdk') return undefined;
return path.dirname(require.resolve('@adcp/sdk/package.json'));
} catch {
return undefined;
}
}

/**
* Fallback: walk up from a starting directory looking for an ancestor
* `package.json` whose `name` is `@adcp/sdk`. Covers environments where
* module self-resolution is unavailable (e.g. an aggressive downstream
* bundler that strips/rewrites `require.resolve`).
*
* Exported for direct testing; not part of the public API surface.
*/
export function _resolvePackageRootViaDirectoryWalk(start: string): string | undefined {
let dir = start;
for (;;) {
const candidate = path.join(dir, 'package.json');
if (existsSync(candidate)) {
try {
const pkg = JSON.parse(readFileSync(candidate, 'utf-8')) as { name?: string };
if (pkg.name === '@adcp/sdk') return dir;
} catch {
// Malformed/unrelated package.json — keep walking past it.
}
}
const parent = path.dirname(dir);
if (parent === dir) return undefined;
dir = parent;
}
}

/**
* Resolve the package root, memoized after the first successful call.
*
* Throws if both resolution strategies fail — this indicates a broken
* install or an aggressive bundler that stripped `package.json`, not a
* recoverable runtime condition.
*/
export function getPackageRoot(): string {
if (cachedRoot) return cachedRoot;
const resolved = resolveViaModuleResolution() ?? _resolvePackageRootViaDirectoryWalk(__dirname);
if (!resolved) {
throw new Error(
'@adcp/sdk: could not resolve the package root (require.resolve self-reference and directory walk ' +
'both failed). This indicates a broken install or an aggressive bundler that stripped package.json.'
);
}
cachedRoot = resolved;
return cachedRoot;
}

/** Test hook: clear the memoized package root. */
export function _resetPackageRootCache(): void {
cachedRoot = undefined;
}
17 changes: 17 additions & 0 deletions src/lib/internal/schema-data-roots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import path from 'path';
import { getPackageRoot } from './package-root';

export interface SchemaDataRoots {
/** dist/lib/schemas-data — populated by scripts/copy-schemas-to-dist.ts. Only exists post-build. */
builtSchemasDataRoot: string;
/** schemas/cache — populated by scripts/sync-schemas.ts. Only exists in a source checkout. */
sourceSchemasCacheRoot: string;
}

export function getSchemaDataRoots(): SchemaDataRoots {
const root = getPackageRoot();
return {
builtSchemasDataRoot: path.join(root, 'dist', 'lib', 'schemas-data'),
sourceSchemasCacheRoot: path.join(root, 'schemas', 'cache'),
};
}
6 changes: 4 additions & 2 deletions src/lib/server/error-arm-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { readdirSync, readFileSync, existsSync } from 'fs';
import path from 'path';
import { ADCP_VERSION } from '../version';
import { resolveBundleKey } from '../validation/schema-loader';
import { getSchemaDataRoots } from '../internal/schema-data-roots';

/**
* Resolve the schema-cache root for a given AdCP version.
Expand All @@ -43,12 +44,13 @@ import { resolveBundleKey } from '../validation/schema-loader';
*/
function resolveBundledRoot(version: string): string | undefined {
const key = resolveBundleKey(version);
const { builtSchemasDataRoot, sourceSchemasCacheRoot } = getSchemaDataRoots();
// Built layout (dist): dist/lib/schemas-data/<bundle-key>/bundled
const distCandidate = path.join(__dirname, '..', 'schemas-data', key, 'bundled');
const distCandidate = path.join(builtSchemasDataRoot, key, 'bundled');
if (existsSync(distCandidate)) return distCandidate;

// Source-tree layout (dev): schemas/cache/<exact-version>/bundled
const cacheRoot = path.join(__dirname, '..', '..', '..', 'schemas', 'cache');
const cacheRoot = sourceSchemasCacheRoot;
const exactCandidate = path.join(cacheRoot, version, 'bundled');
if (existsSync(exactCandidate)) return exactCandidate;

Expand Down
8 changes: 2 additions & 6 deletions src/lib/testing/storyboard/compliance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ADCPError } from '../../errors';
import { isAdcpVersionSupported } from '../../utils/adcp-version-config';
import { hasSchemaBundle, resolveBundleKey } from '../../validation/schema-loader';
import { synthesizeRequestSigningSteps } from './request-signing/synthesize';
import { getPackageRoot } from '../../internal/package-root';
import type { RunnerSelectionResult, Storyboard } from './types';

/**
Expand Down Expand Up @@ -207,11 +208,6 @@ export interface ResolvedStoryboards {
not_applicable: NotApplicableStoryboard[];
}

function getRepoRoot(): string {
// Walks from src/lib/testing/storyboard/ (or dist/lib/testing/storyboard/) → package root.
return resolve(__dirname, '..', '..', '..', '..');
}

/**
* Resolve the compliance cache directory.
*
Expand All @@ -224,7 +220,7 @@ export function getComplianceCacheDir(options: ResolveOptions = {}): string {
const configured = getConfiguredComplianceDir(options);
if (configured) return configured;
const version = options.version || readAdcpVersion();
return join(getRepoRoot(), 'compliance', 'cache', version);
return join(getPackageRoot(), 'compliance', 'cache', version);
}

function readAdcpVersion(): string {
Expand Down
6 changes: 4 additions & 2 deletions src/lib/v2/projection/canonical-properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { readFileSync, existsSync } from 'fs';
import path from 'path';
import type { CanonicalFormatKind } from './types';
import { BETA_VERSIONS_TO_TRY } from './cache-versions';
import { getSchemaDataRoots } from '../../internal/schema-data-roots';

interface CanonicalSchema {
properties?: {
Expand Down Expand Up @@ -55,9 +56,10 @@ function findCacheRoot(): string {
// is intentionally not copied — adopters pinned to 3.0.x GA hit the
// throw below rather than silently picking up a v3.0 cache that
// lacks canonical-format schemas).
const { builtSchemasDataRoot, sourceSchemasCacheRoot } = getSchemaDataRoots();
const candidates = [
...BETA_VERSIONS_TO_TRY.map(v => path.join(__dirname, '..', '..', 'schemas-data', v)),
...BETA_VERSIONS_TO_TRY.map(v => path.join(__dirname, '..', '..', '..', '..', 'schemas', 'cache', v)),
...BETA_VERSIONS_TO_TRY.map(v => path.join(builtSchemasDataRoot, v)),
...BETA_VERSIONS_TO_TRY.map(v => path.join(sourceSchemasCacheRoot, v)),
];
for (const c of candidates) {
if (existsSync(c)) return c;
Expand Down
16 changes: 4 additions & 12 deletions src/lib/v2/projection/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import { readFileSync, existsSync } from 'fs';
import path from 'path';
import type { CanonicalFormatKind, V1FormatId } from './types';
import { getPackageRoot } from '../../internal/package-root';

/**
* Canonical projection reference (`canonical-projection-ref.json` in the
Expand Down Expand Up @@ -120,21 +121,12 @@ function indexKey(agentUrl: string, id: string): string {
export function loadCatalog(explicitPath?: string): CatalogIndex {
if (cached) return cached;

const packageRoot = getPackageRoot();
const candidates = explicitPath
? [explicitPath]
: [
path.join(__dirname, 'aao-reference-formats.json'),
path.join(
__dirname,
'..',
'..',
'..',
'..',
'test',
'lib',
'v2-projection-fixtures',
'aao-reference-formats.json'
),
path.join(packageRoot, 'dist', 'lib', 'v2', 'projection', 'aao-reference-formats.json'),
path.join(packageRoot, 'test', 'lib', 'v2-projection-fixtures', 'aao-reference-formats.json'),
];

for (const file of candidates) {
Expand Down
8 changes: 4 additions & 4 deletions src/lib/v2/projection/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import path from 'path';
import type { CanonicalFormatKind, V1FormatId } from './types';
import { AAO_CANONICAL_AGENT_URL } from './constants';
import { BETA_VERSIONS_TO_TRY } from './cache-versions';
import { getSchemaDataRoots } from '../../internal/schema-data-roots';

interface RegistryEntryV1Pattern {
format_id_glob?: string;
Expand Down Expand Up @@ -78,14 +79,13 @@ let cached: CanonicalMappingRegistry | null = null;
*/
export function loadRegistry(cacheRoot?: string): CanonicalMappingRegistry {
if (cached) return cached;
const { builtSchemasDataRoot, sourceSchemasCacheRoot } = getSchemaDataRoots();
const candidates = cacheRoot
? [path.join(cacheRoot, 'registries', 'v1-canonical-mapping.json')]
: [
...BETA_VERSIONS_TO_TRY.map(v => path.join(builtSchemasDataRoot, v, 'registries', 'v1-canonical-mapping.json')),
...BETA_VERSIONS_TO_TRY.map(v =>
path.join(__dirname, '..', '..', 'schemas-data', v, 'registries', 'v1-canonical-mapping.json')
),
...BETA_VERSIONS_TO_TRY.map(v =>
path.join(__dirname, '..', '..', '..', '..', 'schemas', 'cache', v, 'registries', 'v1-canonical-mapping.json')
path.join(sourceSchemasCacheRoot, v, 'registries', 'v1-canonical-mapping.json')
),
];
for (const file of candidates) {
Expand Down
8 changes: 5 additions & 3 deletions src/lib/validation/schema-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { readdirSync, readFileSync, existsSync } from 'fs';
import path from 'path';
import { ADCP_VERSION } from '../version';
import { ConfigurationError } from '../errors';
import { getSchemaDataRoots } from '../internal/schema-data-roots';

export type ResponseVariant = 'sync' | 'submitted' | 'working' | 'input-required';
export type Direction = 'request' | ResponseVariant;
Expand Down Expand Up @@ -239,22 +240,23 @@ function resolveSchemaRoot(version: string): string {
const externalRoot = externalSchemaRoots.get(key);
if (externalRoot && existsSync(externalRoot)) return externalRoot;

const distCandidate = path.join(__dirname, '..', 'schemas-data', key);
const { builtSchemasDataRoot, sourceSchemasCacheRoot } = getSchemaDataRoots();
const distCandidate = path.join(builtSchemasDataRoot, key);
if (existsSync(distCandidate)) return distCandidate;

// Published builds carry exact prerelease bundle directories
// (`3.1.0-rc.1/`), while callers may pin the release-precision family
// alias (`3.1-rc`). Resolve that alias before falling back to source cache.
const releasePrecisionMatch = /^\d+\.\d+-/.test(key);
if (releasePrecisionMatch) {
const distPrereleaseCandidate = findPrereleaseBundle(path.join(__dirname, '..', 'schemas-data'), key);
const distPrereleaseCandidate = findPrereleaseBundle(builtSchemasDataRoot, key);
if (distPrereleaseCandidate) return distPrereleaseCandidate;
}

// Source-tree fallback: cache stays exact-version-named, so map the key
// back to the highest-patch cache directory in the same minor for stable
// pins. Prereleases need an exact match.
const cacheRoot = path.join(__dirname, '..', '..', '..', 'schemas', 'cache');
const cacheRoot = sourceSchemasCacheRoot;
const exactCandidate = path.join(cacheRoot, version);
if (existsSync(exactCandidate)) return exactCandidate;

Expand Down
Loading
Loading