Skip to content

Commit 06246df

Browse files
bloveclaude
andcommitted
feat(telemetry): assemble-dist script + corrected exports map for publish
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 <noreply@anthropic.com>
1 parent a00eba3 commit 06246df

3 files changed

Lines changed: 185 additions & 9 deletions

File tree

libs/telemetry/package.json

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,25 @@
1414
"exports": {
1515
".": {
1616
"types": "./index.d.ts",
17-
"default": "./fesm2022/ngaf-telemetry.mjs"
17+
"default": "./index.js"
18+
},
19+
"./shared": {
20+
"types": "./shared/events.d.ts",
21+
"default": "./shared/events.js"
1822
},
1923
"./node": {
2024
"types": "./node/index.d.ts",
21-
"default": "./node/index.mjs"
25+
"default": "./node/index.js"
26+
},
27+
"./node/postinstall": {
28+
"types": "./node/postinstall.d.ts",
29+
"default": "./node/postinstall.js"
2230
},
2331
"./browser": {
2432
"types": "./browser/index.d.ts",
25-
"default": "./fesm2022/ngaf-telemetry-browser.mjs"
33+
"default": "./browser/fesm2022/ngaf-telemetry.mjs"
2634
},
27-
"./postinstall": {
28-
"types": "./node/postinstall.d.ts",
29-
"default": "./node/postinstall.mjs"
30-
}
35+
"./README.md": "./README.md"
3136
},
3237
"peerDependencies": {
3338
"@angular/core": "^20.0.0 || ^21.0.0"
@@ -40,6 +45,6 @@
4045
"posthog-node": "^5.20.0"
4146
},
4247
"scripts": {
43-
"postinstall": "node ./node/postinstall.mjs || true"
48+
"postinstall": "node ./node/postinstall.js || true"
4449
}
4550
}

libs/telemetry/project.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@
3131
"build": {
3232
"dependsOn": ["build:node", "build:browser"],
3333
"executor": "nx:run-commands",
34-
"options": { "command": "true" }
34+
"outputs": ["{workspaceRoot}/dist/libs/telemetry"],
35+
"options": {
36+
"command": "node libs/telemetry/scripts/assemble-dist.mjs"
37+
}
3538
},
3639
"test": {
3740
"executor": "@nx/vitest:test",
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Assembles the publishable @ngaf/telemetry package after `nx run telemetry:build`.
4+
*
5+
* The build produces two disjoint outputs:
6+
* - @nx/js:tsc → dist/libs/telemetry/src/{index,shared,node}/*
7+
* - @nx/angular:package → dist/libs/telemetry/browser/{fesm2022,types}/*
8+
*
9+
* Neither shape matches what the exports map needs. This script:
10+
* 1. Flattens dist/libs/telemetry/src/* → dist/libs/telemetry/*
11+
* 2. Removes the conflicting browser/package.json from ng-packagr.
12+
* 3. Re-emits a canonical package.json with the corrected exports map.
13+
* 4. Adds a browser/index.d.ts that re-exports from fesm2022 types.
14+
*
15+
* Idempotent — re-running on an assembled dist is a no-op.
16+
*/
17+
import { readFile, writeFile, rm, rename, mkdir, access, readdir, stat } from 'node:fs/promises';
18+
import { existsSync } from 'node:fs';
19+
import { fileURLToPath } from 'node:url';
20+
import { dirname, join, relative } from 'node:path';
21+
22+
const HERE = dirname(fileURLToPath(import.meta.url));
23+
const LIB_ROOT = join(HERE, '..');
24+
const DIST = join(LIB_ROOT, '..', '..', 'dist', 'libs', 'telemetry');
25+
26+
async function exists(p) {
27+
try { await access(p); return true; } catch { return false; }
28+
}
29+
30+
/**
31+
* Recursively moves entries from src into dest, overwriting existing files.
32+
* Avoids `rename` cross-device issues by copying via read+write when possible,
33+
* but for same-filesystem moves a simple loop of renames is fastest.
34+
*/
35+
async function moveDirContentsUp(src, dest) {
36+
if (!(await exists(src))) return;
37+
await mkdir(dest, { recursive: true });
38+
const entries = await readdir(src, { withFileTypes: true });
39+
for (const entry of entries) {
40+
const from = join(src, entry.name);
41+
const to = join(dest, entry.name);
42+
if (entry.isDirectory()) {
43+
await moveDirContentsUp(from, to);
44+
await rm(from, { recursive: true, force: true });
45+
} else {
46+
await rename(from, to);
47+
}
48+
}
49+
}
50+
51+
async function flattenSrc() {
52+
const srcDir = join(DIST, 'src');
53+
if (!(await exists(srcDir))) {
54+
console.log('[assemble-dist] dist/libs/telemetry/src not present — already assembled or build skipped');
55+
return;
56+
}
57+
console.log('[assemble-dist] flattening dist/libs/telemetry/src/* → dist/libs/telemetry/*');
58+
await moveDirContentsUp(srcDir, DIST);
59+
await rm(srcDir, { recursive: true, force: true });
60+
}
61+
62+
async function removeNgPackagrManifest() {
63+
const ngPkg = join(DIST, 'browser', 'package.json');
64+
if (await exists(ngPkg)) {
65+
console.log('[assemble-dist] removing ng-packagr browser/package.json (replaced by canonical root manifest)');
66+
await rm(ngPkg);
67+
}
68+
const ngIgnore = join(DIST, 'browser', '.npmignore');
69+
if (await exists(ngIgnore)) await rm(ngIgnore);
70+
}
71+
72+
async function writeBrowserIndexReexport() {
73+
// Make `./browser` resolve a clean type entry path.
74+
const fesmTypes = join(DIST, 'browser', 'types', 'ngaf-telemetry.d.ts');
75+
const indexDts = join(DIST, 'browser', 'index.d.ts');
76+
if (!(await exists(fesmTypes))) {
77+
console.warn('[assemble-dist] no browser/types/ngaf-telemetry.d.ts — skipping browser/index.d.ts');
78+
return;
79+
}
80+
if (await exists(indexDts)) return; // idempotent
81+
const rel = relative(dirname(indexDts), fesmTypes).replace(/\\/g, '/').replace(/\.d\.ts$/, '');
82+
const content = `export * from '${rel.startsWith('.') ? rel : './' + rel}';\n`;
83+
await writeFile(indexDts, content, 'utf8');
84+
console.log('[assemble-dist] wrote browser/index.d.ts re-exporting from fesm2022 types');
85+
}
86+
87+
async function writeCanonicalPackageJson() {
88+
const srcPkg = JSON.parse(await readFile(join(LIB_ROOT, 'package.json'), 'utf8'));
89+
// Strip dev fields, lock to a clean publishable shape.
90+
const out = {
91+
name: srcPkg.name,
92+
version: srcPkg.version,
93+
license: srcPkg.license,
94+
repository: srcPkg.repository,
95+
homepage: srcPkg.homepage,
96+
bugs: srcPkg.bugs,
97+
sideEffects: false,
98+
type: 'module',
99+
exports: {
100+
'.': {
101+
types: './index.d.ts',
102+
default: './index.js',
103+
},
104+
'./shared': {
105+
types: './shared/events.d.ts', // shared has no aggregating index; events is the only type-only public artifact
106+
default: './shared/events.js',
107+
},
108+
'./node': {
109+
types: './node/index.d.ts',
110+
default: './node/index.js',
111+
},
112+
'./node/postinstall': {
113+
types: './node/postinstall.d.ts',
114+
default: './node/postinstall.js',
115+
},
116+
'./browser': {
117+
types: './browser/index.d.ts',
118+
default: './browser/fesm2022/ngaf-telemetry.mjs',
119+
},
120+
'./README.md': './README.md',
121+
},
122+
peerDependencies: srcPkg.peerDependencies,
123+
peerDependenciesMeta: srcPkg.peerDependenciesMeta,
124+
dependencies: srcPkg.dependencies,
125+
scripts: {
126+
postinstall: 'node ./node/postinstall.js || true',
127+
},
128+
};
129+
// Strip undefined.
130+
for (const k of Object.keys(out)) if (out[k] === undefined) delete out[k];
131+
await writeFile(join(DIST, 'package.json'), JSON.stringify(out, null, 2) + '\n', 'utf8');
132+
console.log('[assemble-dist] wrote canonical dist/libs/telemetry/package.json with corrected exports map');
133+
}
134+
135+
async function verifyExports() {
136+
const pkg = JSON.parse(await readFile(join(DIST, 'package.json'), 'utf8'));
137+
const missing = [];
138+
for (const [key, value] of Object.entries(pkg.exports ?? {})) {
139+
const paths = typeof value === 'string' ? [value] : Object.values(value);
140+
for (const p of paths) {
141+
const abs = join(DIST, p);
142+
if (!(await exists(abs))) missing.push(`${key}${p}`);
143+
}
144+
}
145+
if (missing.length > 0) {
146+
console.error('[assemble-dist] FAIL: exports map references missing files:');
147+
for (const m of missing) console.error(` - ${m}`);
148+
process.exit(1);
149+
}
150+
console.log(`[assemble-dist] OK: all ${Object.keys(pkg.exports).length} exports map paths resolve`);
151+
}
152+
153+
async function main() {
154+
if (!(await exists(DIST))) {
155+
console.error(`[assemble-dist] ${DIST} does not exist — run \`nx run telemetry:build\` first`);
156+
process.exit(1);
157+
}
158+
await flattenSrc();
159+
await removeNgPackagrManifest();
160+
await writeBrowserIndexReexport();
161+
await writeCanonicalPackageJson();
162+
await verifyExports();
163+
}
164+
165+
main().catch((err) => {
166+
console.error('[assemble-dist] failed:', err);
167+
process.exit(1);
168+
});

0 commit comments

Comments
 (0)