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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ jobs:
- run: npm test
- name: Check that nothing ends up in production bundles
run: npm run check:production
- name: Check the package with Angular 21
run: npm run check:angular-21
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ It only runs in development builds.

> **Experimental.** This relies on `enableTemplateSourceLocations`, a compiler option that Angular
> doesn't document and doesn't cover by semver. It could change or go away in any release. Tested
> with Angular 22.2. This isn't an official Angular package. The proposal to support this in Angular
> itself is [angular/angular#70927](https://github.com/angular/angular/issues/70927).
> with Angular 21.2 and 22.2. This isn't an official Angular package. The proposal to support this
> in Angular itself is [angular/angular#70927](https://github.com/angular/angular/issues/70927).

## Install

Expand Down Expand Up @@ -58,6 +58,25 @@ Then run `ng serve`, hold Alt and move the mouse over the page.
};
```

### Angular 21

The compiler option exists since Angular 22.1.4. Before that, `ng add` only adds the provider (skip
steps 1 and 2 of the manual setup), and a preload turns the source locations on when you start the
dev server:

```sh
NODE_OPTIONS="--import ngx-source-inspector/enable-source-locations" ng serve
```

You can put that in the `start` script of your `package.json`. On Windows, set the variable with
[cross-env](https://www.npmjs.com/package/cross-env).

The preload calls `setEnableTemplateSourceLocations()`, which `@angular/compiler` exports for
Angular's own tests, in the process that compiles your templates. It only does something for the dev
server (`ng serve`, or a `serve` target): `ng build` and the other commands ignore it.

Once you're on Angular 22.2 or later, remove it and run `ng add ngx-source-inspector` again.

## Options

```ts
Expand Down Expand Up @@ -92,6 +111,8 @@ for production:
becomes an empty provider list and the inspector code is dropped from the bundle.
- The template paths come from the compiler option. If you turn it on in `tsconfig.app.json`, every
template path ships in your production bundle. That's why it goes in a development-only tsconfig.
- In Angular 21, the preload only turns the source locations on for the dev server, so a production
build stays clean even if `NODE_OPTIONS` is set for it too.

This repository's CI builds the demo app for production and fails if the bundle contains any
inspector code, `data-ng-source-location` attribute or template path. To check your own app, build
Expand Down Expand Up @@ -129,6 +150,7 @@ npm run build:lib # library and ng-add schematic, in dist/ngx-source-ins
npm start # demo app with the inspector
npm test # unit tests and schematic tests
npm run check:production # production build of the demo, checked for leftovers
npm run check:angular-21 # ng add, ng serve and a production build in a new Angular 21 app
```

The first prototype, linked from the Angular issue, is tagged
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
"build": "npm run build:lib && ng build inspector-demo",
"build:lib": "ng build ngx-source-inspector && npm run build:schematics && node scripts/copy-package-files.mjs",
"build:schematics": "tsc -p projects/ngx-source-inspector/tsconfig.schematics.json && node scripts/copy-schematics-assets.mjs",
"test": "ng test ngx-source-inspector --watch=false && npm run test:schematics",
"test": "ng test ngx-source-inspector --watch=false && npm run test:schematics && npm run test:preload",
"test:schematics": "tsc -p projects/ngx-source-inspector/tsconfig.schematics.spec.json && node --test out-tsc/schematics/ng-add/index.spec.js",
"check:production": "ng build inspector-demo --configuration production && node scripts/check-production-bundle.mjs"
"test:preload": "node --test projects/ngx-source-inspector/node/enable-source-locations.spec.mjs",
"check:production": "ng build inspector-demo --configuration production && node scripts/check-production-bundle.mjs",
"check:angular-21": "node scripts/check-angular-21.mjs"
},
"private": true,
"packageManager": "npm@11.19.0",
Expand Down
49 changes: 49 additions & 0 deletions projects/ngx-source-inspector/node/enable-source-locations.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Turns on template source locations in Angular versions that don't have the
// `enableTemplateSourceLocations` compiler option yet (before 22.1.4). Preload it into `ng serve`:
//
// NODE_OPTIONS="--import ngx-source-inspector/enable-source-locations" ng serve
//
// In those versions the only switch is `setEnableTemplateSourceLocations()`, a function that
// `@angular/compiler` exports for Angular's own tests. It has to be called in the process, or the
// worker thread, that compiles the templates, which is why this is a preload.
import { createRequire } from 'node:module';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { isMainThread } from 'node:worker_threads';

// Set by the process that runs the dev server, read by its worker threads and child processes,
// which don't get the command line.
const SERVING = 'NGX_SOURCE_INSPECTOR_SERVING';

// `ng serve`, `ng s`, `ng dev`, `ng run app:serve`, `nx serve app`...
const isServeCommand = (arg) => /^(serve|s|dev)$|(^|:)serve(:|$)/.test(arg);

// NODE_OPTIONS reaches every Node.js process started with it, `ng build` included. Template paths
// must not end up in production bundles, so this only does something for the dev server.
const startsDevServer = isMainThread && process.argv.slice(2).some(isServeCommand);
const alreadyServing = process.env[SERVING] === '1';

if (startsDevServer || alreadyServing) {
process.env[SERVING] = '1';
const compiler = await importWorkspaceCompiler();
if (typeof compiler?.setEnableTemplateSourceLocations === 'function') {
compiler.setEnableTemplateSourceLocations(true);
} else if (compiler && !alreadyServing) {
console.warn(
'[ngx-source-inspector] This version of Angular has the `enableTemplateSourceLocations` ' +
'compiler option, the `enable-source-locations` preload does nothing. Remove it and run ' +
'`ng add ngx-source-inspector` to turn the option on for development builds.',
);
}
}

/** The copy of `@angular/compiler` that the CLI of the current workspace compiles with. */
async function importWorkspaceCompiler() {
try {
const require = createRequire(join(process.cwd(), 'package.json'));
return await import(pathToFileURL(require.resolve('@angular/compiler')).href);
} catch {
// Not in an Angular workspace: another process that inherited NODE_OPTIONS.
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { after, before, describe, it } from 'node:test';
import { fileURLToPath } from 'node:url';

const preload = fileURLToPath(new URL('./enable-source-locations.mjs', import.meta.url));

// Stands in for the CLI: prints whether source locations are on in the main thread and in a worker
// thread, where the CLI compiles templates.
const CLI = `
import { Worker } from 'node:worker_threads';
const { enabled } = await import('@angular/compiler');
const worker = new Worker(new URL('./worker.mjs', import.meta.url));
worker.once('message', (inWorker) => console.log(JSON.stringify({ main: enabled(), worker: inWorker })));
`;
const WORKER = `
import { parentPort } from 'node:worker_threads';
const { enabled } = await import('@angular/compiler');
parentPort.postMessage(enabled());
`;

const OLD_COMPILER = `
let on = false;
export const setEnableTemplateSourceLocations = (value) => (on = value);
export const enabled = () => on;
`;
// Since 22.1.4 the setter is gone, replaced by the compiler option.
const NEW_COMPILER = 'export const enabled = () => false;';

describe('enable-source-locations preload', () => {
let workspace;

const useCompiler = (code) =>
writeFileSync(join(workspace, 'node_modules/@angular/compiler/index.mjs'), code);

const ng = (...args) => {
const { NGX_SOURCE_INSPECTOR_SERVING, ...env } = process.env;
const output = execFileSync(process.execPath, ['--import', preload, 'ng.mjs', ...args], {
cwd: workspace,
env,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
return JSON.parse(output);
};

before(() => {
workspace = mkdtempSync(join(tmpdir(), 'ngx-source-inspector-'));
mkdirSync(join(workspace, 'node_modules/@angular/compiler'), { recursive: true });
writeFileSync(
join(workspace, 'node_modules/@angular/compiler/package.json'),
JSON.stringify({ name: '@angular/compiler', exports: { '.': { default: './index.mjs' } } }),
);
writeFileSync(join(workspace, 'package.json'), '{}');
writeFileSync(join(workspace, 'ng.mjs'), CLI);
writeFileSync(join(workspace, 'worker.mjs'), WORKER);
});

after(() => rmSync(workspace, { recursive: true, force: true }));

it('turns source locations on for the dev server, worker threads included', () => {
useCompiler(OLD_COMPILER);
for (const args of [['serve'], ['s', '--port', '4201'], ['dev'], ['run', 'app:serve']]) {
assert.deepEqual(ng(...args), { main: true, worker: true }, args.join(' '));
}
});

it('leaves every other command alone', () => {
useCompiler(OLD_COMPILER);
for (const args of [['build'], ['build', '--configuration', 'production'], ['test'], []]) {
assert.deepEqual(ng(...args), { main: false, worker: false }, args.join(' '));
}
});

it('does nothing in Angular versions that have the compiler option', () => {
useCompiler(NEW_COMPILER);
assert.deepEqual(ng('serve'), { main: false, worker: false });
});

it('does nothing outside of an Angular workspace', () => {
const elsewhere = mkdtempSync(join(tmpdir(), 'ngx-source-inspector-'));
try {
writeFileSync(join(elsewhere, 'ng.mjs'), 'console.log("started")');
const output = execFileSync(process.execPath, ['--import', preload, 'ng.mjs', 'serve'], {
cwd: elsewhere,
encoding: 'utf8',
});
assert.equal(output.trim(), 'started');
} finally {
rmSync(elsewhere, { recursive: true, force: true });
}
});
});
11 changes: 8 additions & 3 deletions projects/ngx-source-inspector/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ngx-source-inspector",
"version": "0.1.0",
"version": "0.2.0",
"description": "Hold Alt, hover an element of your Angular app to see which template line it comes from, Alt+click to open it in your editor. Development only.",
"keywords": [
"angular",
Expand All @@ -21,12 +21,17 @@
"url": "https://github.com/aminesbdev/angular-source-inspector/issues"
},
"peerDependencies": {
"@angular/common": "^22.2.0",
"@angular/core": "^22.2.0"
"@angular/common": ">=21.0.0 <23.0.0",
"@angular/core": ">=21.0.0 <23.0.0"
},
"dependencies": {
"tslib": "^2.3.0"
},
"exports": {
"./enable-source-locations": {
"default": "./node/enable-source-locations.mjs"
}
},
"schematics": "./schematics/collection.json",
"sideEffects": false
}
50 changes: 50 additions & 0 deletions projects/ngx-source-inspector/schematics/ng-add/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,54 @@ describe('ng-add', () => {
/no "development" build configuration/,
);
});

describe('before the compiler option exists', () => {
const logs: string[] = [];
runner.logger.subscribe((entry) => logs.push(entry.message));

beforeEach(() => {
const packageJson = readJson(tree, 'package.json');
packageJson.dependencies['@angular/core'] = '^21.2.0';
tree.overwrite('package.json', JSON.stringify(packageJson, null, 2));
logs.length = 0;
});

it('leaves the build configuration alone and points to the preload', async () => {
const angularJson = tree.readText('angular.json');

const result = await runner.runSchematic('ng-add', { project: 'app' }, tree);

assert.equal(result.readText('angular.json'), angularJson);
assert.equal(result.exists('projects/app/tsconfig.dev.json'), false);
assert.match(
logs.join('\n'),
/NODE_OPTIONS="--import ngx-source-inspector\/enable-source-locations" ng serve/,
);
});

it('adds provideSourceInspector() to the application providers', async () => {
const result = await runner.runSchematic('ng-add', { project: 'app' }, tree);

assert.match(
result.readText('projects/app/src/app/app.config.ts'),
/provideSourceInspector\(\)/,
);
});

it('goes by the installed version rather than the one in package.json', async () => {
tree.create('node_modules/@angular/core/package.json', '{"version": "22.1.4"}');

const result = await runner.runSchematic('ng-add', { project: 'app' }, tree);

assert.equal(result.exists('projects/app/tsconfig.dev.json'), true);
});

it('still needs the preload in the 22 releases that came before the option', async () => {
tree.create('node_modules/@angular/core/package.json', '{"version": "22.1.3"}');

const result = await runner.runSchematic('ng-add', { project: 'app' }, tree);

assert.equal(result.exists('projects/app/tsconfig.dev.json'), false);
});
});
});
Loading
Loading