From af3b28c42cf4250fb88d2635cf138069ae96fd53 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 15 May 2026 09:25:37 -0700 Subject: [PATCH] feat(telemetry): assemble-dist script + corrected exports map for publish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spec 1B left a known follow-up: the hybrid build produced disjoint outputs that didn't match the source package.json exports map. @nx/js:tsc wrote to dist/libs/telemetry/src/* and @nx/angular:package wrote to dist/libs/telemetry/browser/*. This PR adds libs/telemetry/scripts/assemble-dist.mjs which the telemetry:build target now runs as its final step. The script: 1. Flattens dist/libs/telemetry/src/* up one level so paths in ./node/* and ./shared/* resolve. 2. Removes the conflicting ng-packagr-generated browser/package.json. 3. Writes browser/index.d.ts as a clean re-export from fesm2022/types/. 4. Re-emits a canonical dist/libs/telemetry/package.json with a corrected exports map. 5. Verifies every exports map path resolves to an actual file (fails the build if not). Also corrects the SOURCE libs/telemetry/package.json exports map to match what the script writes, and fixes scripts.postinstall to use .js (the actual @nx/js:tsc output extension), not .mjs. After this PR, @ngaf/telemetry is publish-ready. npm pack produces a 41-file 9.4 KB tarball with all six subpath exports resolving: . → ./index.js ./shared → ./shared/events.js ./node → ./node/index.js ./node/postinstall → ./node/postinstall.js ./browser → ./browser/fesm2022/ngaf-telemetry.mjs ./README.md → ./README.md Unblocks Spec 1C (cockpit instrumentation) which needs to consume @ngaf/telemetry/browser via the published package shape. Co-Authored-By: Claude Opus 4.7 --- libs/telemetry/package.json | 21 +-- libs/telemetry/project.json | 5 +- libs/telemetry/scripts/assemble-dist.mjs | 168 +++++++++++++++++++++++ 3 files changed, 185 insertions(+), 9 deletions(-) create mode 100644 libs/telemetry/scripts/assemble-dist.mjs diff --git a/libs/telemetry/package.json b/libs/telemetry/package.json index 3858f6712..9490e8f51 100644 --- a/libs/telemetry/package.json +++ b/libs/telemetry/package.json @@ -14,20 +14,25 @@ "exports": { ".": { "types": "./index.d.ts", - "default": "./fesm2022/ngaf-telemetry.mjs" + "default": "./index.js" + }, + "./shared": { + "types": "./shared/events.d.ts", + "default": "./shared/events.js" }, "./node": { "types": "./node/index.d.ts", - "default": "./node/index.mjs" + "default": "./node/index.js" + }, + "./node/postinstall": { + "types": "./node/postinstall.d.ts", + "default": "./node/postinstall.js" }, "./browser": { "types": "./browser/index.d.ts", - "default": "./fesm2022/ngaf-telemetry-browser.mjs" + "default": "./browser/fesm2022/ngaf-telemetry.mjs" }, - "./postinstall": { - "types": "./node/postinstall.d.ts", - "default": "./node/postinstall.mjs" - } + "./README.md": "./README.md" }, "peerDependencies": { "@angular/core": "^20.0.0 || ^21.0.0" @@ -40,6 +45,6 @@ "posthog-node": "^5.20.0" }, "scripts": { - "postinstall": "node ./node/postinstall.mjs || true" + "postinstall": "node ./node/postinstall.js || true" } } diff --git a/libs/telemetry/project.json b/libs/telemetry/project.json index 1fc43b254..ac61e14e5 100644 --- a/libs/telemetry/project.json +++ b/libs/telemetry/project.json @@ -31,7 +31,10 @@ "build": { "dependsOn": ["build:node", "build:browser"], "executor": "nx:run-commands", - "options": { "command": "true" } + "outputs": ["{workspaceRoot}/dist/libs/telemetry"], + "options": { + "command": "node libs/telemetry/scripts/assemble-dist.mjs" + } }, "test": { "executor": "@nx/vitest:test", diff --git a/libs/telemetry/scripts/assemble-dist.mjs b/libs/telemetry/scripts/assemble-dist.mjs new file mode 100644 index 000000000..9ff257f4a --- /dev/null +++ b/libs/telemetry/scripts/assemble-dist.mjs @@ -0,0 +1,168 @@ +#!/usr/bin/env node +/** + * Assembles the publishable @ngaf/telemetry package after `nx run telemetry:build`. + * + * The build produces two disjoint outputs: + * - @nx/js:tsc → dist/libs/telemetry/src/{index,shared,node}/* + * - @nx/angular:package → dist/libs/telemetry/browser/{fesm2022,types}/* + * + * Neither shape matches what the exports map needs. This script: + * 1. Flattens dist/libs/telemetry/src/* → dist/libs/telemetry/* + * 2. Removes the conflicting browser/package.json from ng-packagr. + * 3. Re-emits a canonical package.json with the corrected exports map. + * 4. Adds a browser/index.d.ts that re-exports from fesm2022 types. + * + * Idempotent — re-running on an assembled dist is a no-op. + */ +import { readFile, writeFile, rm, rename, mkdir, access, readdir, stat } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join, relative } from 'node:path'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const LIB_ROOT = join(HERE, '..'); +const DIST = join(LIB_ROOT, '..', '..', 'dist', 'libs', 'telemetry'); + +async function exists(p) { + try { await access(p); return true; } catch { return false; } +} + +/** + * Recursively moves entries from src into dest, overwriting existing files. + * Avoids `rename` cross-device issues by copying via read+write when possible, + * but for same-filesystem moves a simple loop of renames is fastest. + */ +async function moveDirContentsUp(src, dest) { + if (!(await exists(src))) return; + await mkdir(dest, { recursive: true }); + const entries = await readdir(src, { withFileTypes: true }); + for (const entry of entries) { + const from = join(src, entry.name); + const to = join(dest, entry.name); + if (entry.isDirectory()) { + await moveDirContentsUp(from, to); + await rm(from, { recursive: true, force: true }); + } else { + await rename(from, to); + } + } +} + +async function flattenSrc() { + const srcDir = join(DIST, 'src'); + if (!(await exists(srcDir))) { + console.log('[assemble-dist] dist/libs/telemetry/src not present — already assembled or build skipped'); + return; + } + console.log('[assemble-dist] flattening dist/libs/telemetry/src/* → dist/libs/telemetry/*'); + await moveDirContentsUp(srcDir, DIST); + await rm(srcDir, { recursive: true, force: true }); +} + +async function removeNgPackagrManifest() { + const ngPkg = join(DIST, 'browser', 'package.json'); + if (await exists(ngPkg)) { + console.log('[assemble-dist] removing ng-packagr browser/package.json (replaced by canonical root manifest)'); + await rm(ngPkg); + } + const ngIgnore = join(DIST, 'browser', '.npmignore'); + if (await exists(ngIgnore)) await rm(ngIgnore); +} + +async function writeBrowserIndexReexport() { + // Make `./browser` resolve a clean type entry path. + const fesmTypes = join(DIST, 'browser', 'types', 'ngaf-telemetry.d.ts'); + const indexDts = join(DIST, 'browser', 'index.d.ts'); + if (!(await exists(fesmTypes))) { + console.warn('[assemble-dist] no browser/types/ngaf-telemetry.d.ts — skipping browser/index.d.ts'); + return; + } + if (await exists(indexDts)) return; // idempotent + const rel = relative(dirname(indexDts), fesmTypes).replace(/\\/g, '/').replace(/\.d\.ts$/, ''); + const content = `export * from '${rel.startsWith('.') ? rel : './' + rel}';\n`; + await writeFile(indexDts, content, 'utf8'); + console.log('[assemble-dist] wrote browser/index.d.ts re-exporting from fesm2022 types'); +} + +async function writeCanonicalPackageJson() { + const srcPkg = JSON.parse(await readFile(join(LIB_ROOT, 'package.json'), 'utf8')); + // Strip dev fields, lock to a clean publishable shape. + const out = { + name: srcPkg.name, + version: srcPkg.version, + license: srcPkg.license, + repository: srcPkg.repository, + homepage: srcPkg.homepage, + bugs: srcPkg.bugs, + sideEffects: false, + type: 'module', + exports: { + '.': { + types: './index.d.ts', + default: './index.js', + }, + './shared': { + types: './shared/events.d.ts', // shared has no aggregating index; events is the only type-only public artifact + default: './shared/events.js', + }, + './node': { + types: './node/index.d.ts', + default: './node/index.js', + }, + './node/postinstall': { + types: './node/postinstall.d.ts', + default: './node/postinstall.js', + }, + './browser': { + types: './browser/index.d.ts', + default: './browser/fesm2022/ngaf-telemetry.mjs', + }, + './README.md': './README.md', + }, + peerDependencies: srcPkg.peerDependencies, + peerDependenciesMeta: srcPkg.peerDependenciesMeta, + dependencies: srcPkg.dependencies, + scripts: { + postinstall: 'node ./node/postinstall.js || true', + }, + }; + // Strip undefined. + for (const k of Object.keys(out)) if (out[k] === undefined) delete out[k]; + await writeFile(join(DIST, 'package.json'), JSON.stringify(out, null, 2) + '\n', 'utf8'); + console.log('[assemble-dist] wrote canonical dist/libs/telemetry/package.json with corrected exports map'); +} + +async function verifyExports() { + const pkg = JSON.parse(await readFile(join(DIST, 'package.json'), 'utf8')); + const missing = []; + for (const [key, value] of Object.entries(pkg.exports ?? {})) { + const paths = typeof value === 'string' ? [value] : Object.values(value); + for (const p of paths) { + const abs = join(DIST, p); + if (!(await exists(abs))) missing.push(`${key} → ${p}`); + } + } + if (missing.length > 0) { + console.error('[assemble-dist] FAIL: exports map references missing files:'); + for (const m of missing) console.error(` - ${m}`); + process.exit(1); + } + console.log(`[assemble-dist] OK: all ${Object.keys(pkg.exports).length} exports map paths resolve`); +} + +async function main() { + if (!(await exists(DIST))) { + console.error(`[assemble-dist] ${DIST} does not exist — run \`nx run telemetry:build\` first`); + process.exit(1); + } + await flattenSrc(); + await removeNgPackagrManifest(); + await writeBrowserIndexReexport(); + await writeCanonicalPackageJson(); + await verifyExports(); +} + +main().catch((err) => { + console.error('[assemble-dist] failed:', err); + process.exit(1); +});