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
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ jobs:
- name: Build the library
run: npm run build:lib

# 'Zero dependencies' is checkable in ten seconds on Bundlephobia,
# so it has to be exactly true. 0.0.2 shipped declaring tslib.
- name: No runtime dependencies
run: node scripts/verify-deps.mjs

# Also exercises the command Vercel runs, and the llms.txt sync step.
- name: Build the demo
run: npm run build
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"prebuild": "node scripts/sync-llms.mjs",
"start": "npm run build:lib && ng serve demo",
"build": "npm run build:lib && ng build demo",
"build:lib": "ng build masonry-angular",
"build:lib": "ng build masonry-angular && node scripts/strip-tslib.mjs",
"watch:lib": "ng build masonry-angular --watch",
"test": "ng test masonry-angular --watch=false",
"test:watch": "ng test masonry-angular --watch",
Expand All @@ -21,7 +21,8 @@
"verify:ssr": "npm run build:lib && node scripts/verify-ssr.mjs",
"verify:compat": "node scripts/verify-compat.mjs",
"verify:docs": "npm run build:lib && node scripts/verify-docs.mjs",
"postbuild": "node scripts/inject-meta.mjs"
"postbuild": "node scripts/inject-meta.mjs",
"verify:deps": "npm run build:lib && node scripts/verify-deps.mjs"
},
"packageManager": "npm@11.12.1",
"engines": {
Expand Down
3 changes: 0 additions & 3 deletions projects/masonry-angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,5 @@
"peerDependencies": {
"@angular/common": ">=17.1.0",
"@angular/core": ">=17.1.0"
},
"dependencies": {
"tslib": "^2.3.0"
}
}
4 changes: 4 additions & 0 deletions projects/masonry-angular/tsconfig.lib.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
"outDir": "../../out-tsc/lib",
"declaration": true,
"declarationMap": true,
/* Inline any TypeScript helper instead of importing it from tslib: a library
that claims no dependencies must not grow one when some syntax needs a
helper. scripts/verify-deps.mjs checks the built output. */
"importHelpers": false,
"types": []
},
"include": ["src/**/*.ts", "testing/**/*.ts"],
Expand Down
39 changes: 39 additions & 0 deletions scripts/strip-tslib.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Removes the `tslib` dependency ng-packagr writes into the built package.json.
*
* ng-packagr adds `tslib` to `dependencies` unconditionally when a library does
* not declare it, reading the version from @angular/compiler — there is no
* option to turn that off, so leaving it out of the source package.json cannot
* work. For most Angular libraries that is harmless, because their compiled
* code imports tslib's helpers.
*
* This one does not. The library tsconfig sets `importHelpers: false`, so any
* helper TypeScript needs is inlined, and the built code imports nothing but
* @angular/core. Declaring tslib anyway is metadata that does not match the
* code: Bundlephobia reported "1 dependency" for 0.0.2 beside a README that
* said none.
*
* Stripping it is safe only because it is checked. `npm run verify:deps` fails
* if the built code imports any module that is not a declared peer, so the day
* a build does need tslib, CI says so instead of consumers finding out.
*
* Runs as part of `npm run build:lib`, so `npm run release` publishes the
* corrected file.
*/
import { readFileSync, writeFileSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const PKG = join(ROOT, 'dist/masonry-angular/package.json');

const pkg = JSON.parse(readFileSync(PKG, 'utf8'));

if (pkg.dependencies?.tslib) {
delete pkg.dependencies.tslib;
if (Object.keys(pkg.dependencies).length === 0) delete pkg.dependencies;
writeFileSync(PKG, JSON.stringify(pkg, null, 2) + '\n');
console.log('strip-tslib — removed the tslib dependency ng-packagr added.');
} else {
console.log('strip-tslib — nothing to remove.');
}
69 changes: 69 additions & 0 deletions scripts/verify-deps.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Checks that the published package has no runtime dependencies.
*
* "Zero dependencies" is a claim a stranger can verify in ten seconds on
* Bundlephobia, so it has to be exactly true. It was not: ng-packagr's default
* template declares `tslib`, and version 0.0.2 shipped declaring it even though
* the built code never imported it. Bundlephobia reported "1 dependency" beside
* a README that said none.
*
* Two things can make the claim false, so this checks both:
*
* 1. package.json declares something under `dependencies`.
* 2. The built code imports a module that is not a declared peer — which is
* how a dependency sneaks in without anyone adding it on purpose, for
* instance a TypeScript helper imported from tslib.
*
* Run it after `npm run build:lib`:
*
* npm run verify:deps
*/
import { readFileSync, readdirSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const DIST = join(ROOT, 'dist/masonry-angular');

const pkg = JSON.parse(readFileSync(join(DIST, 'package.json'), 'utf8'));
const peers = Object.keys(pkg.peerDependencies ?? {});
const problems = [];

// 1. Declared dependencies.
const declared = Object.keys(pkg.dependencies ?? {});
if (declared.length > 0) {
problems.push(`package.json declares dependencies: ${declared.join(', ')}`);
}

// 2. What the shipped code actually imports.
const fesm = join(DIST, 'fesm2022');
const imported = new Map();
for (const file of readdirSync(fesm).filter((f) => f.endsWith('.mjs'))) {
const code = readFileSync(join(fesm, file), 'utf8');
for (const [, spec] of code.matchAll(/(?:from|import)\s*\(?\s*['"]([^'"]+)['"]/g)) {
if (spec.startsWith('.')) continue; // internal
// `@scope/name/sub` and `name/sub` both belong to their package.
const name = spec.startsWith('@') ? spec.split('/').slice(0, 2).join('/') : spec.split('/')[0];
if (name === pkg.name) continue; // a secondary entry point importing the primary
if (!imported.has(name)) imported.set(name, new Set());
imported.get(name).add(file);
}
}

for (const [name, files] of imported) {
if (!peers.includes(name)) {
problems.push(`${[...files].join(', ')} imports '${name}', which is not a declared peer`);
}
}

console.log('verify:deps — the published package must have no runtime dependencies.\n');
console.log(` declared dependencies ${declared.length ? declared.join(', ') : 'none'}`);
console.log(` modules imported ${[...imported.keys()].join(', ') || 'none'}`);
console.log(` declared peers ${peers.join(', ')}`);

if (problems.length > 0) {
console.log('\nFAIL');
for (const p of problems) console.log(` - ${p}`);
process.exit(1);
}
console.log('\nOK — every import is a declared peer, and nothing else is required.');
Loading