diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c417439..44b92ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/README.md b/README.md index fda3ec7..aecf515 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/package.json b/package.json index 08792cb..5301eb9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/projects/ngx-source-inspector/node/enable-source-locations.mjs b/projects/ngx-source-inspector/node/enable-source-locations.mjs new file mode 100644 index 0000000..520ba40 --- /dev/null +++ b/projects/ngx-source-inspector/node/enable-source-locations.mjs @@ -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; + } +} diff --git a/projects/ngx-source-inspector/node/enable-source-locations.spec.mjs b/projects/ngx-source-inspector/node/enable-source-locations.spec.mjs new file mode 100644 index 0000000..f29f8f0 --- /dev/null +++ b/projects/ngx-source-inspector/node/enable-source-locations.spec.mjs @@ -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 }); + } + }); +}); diff --git a/projects/ngx-source-inspector/package.json b/projects/ngx-source-inspector/package.json index cd4925c..c35e616 100644 --- a/projects/ngx-source-inspector/package.json +++ b/projects/ngx-source-inspector/package.json @@ -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", @@ -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 } diff --git a/projects/ngx-source-inspector/schematics/ng-add/index.spec.ts b/projects/ngx-source-inspector/schematics/ng-add/index.spec.ts index 9b45841..aa2c124 100644 --- a/projects/ngx-source-inspector/schematics/ng-add/index.spec.ts +++ b/projects/ngx-source-inspector/schematics/ng-add/index.spec.ts @@ -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); + }); + }); }); diff --git a/projects/ngx-source-inspector/schematics/ng-add/index.ts b/projects/ngx-source-inspector/schematics/ng-add/index.ts index 296d55b..a6a7b3b 100644 --- a/projects/ngx-source-inspector/schematics/ng-add/index.ts +++ b/projects/ngx-source-inspector/schematics/ng-add/index.ts @@ -1,4 +1,10 @@ -import { chain, Rule, SchematicsException, Tree } from '@angular-devkit/schematics'; +import { + chain, + Rule, + SchematicContext, + SchematicsException, + Tree, +} from '@angular-devkit/schematics'; import { addRootProvider, readWorkspace, updateWorkspace } from '@schematics/angular/utility'; import { JSONFile } from '@schematics/angular/utility/json-file'; import { posix } from 'node:path'; @@ -10,10 +16,14 @@ export interface NgAddOptions { const PACKAGE_NAME = 'ngx-source-inspector'; const DEV_TSCONFIG_NAME = 'tsconfig.dev.json'; const COMPILER_OPTION = ['angularCompilerOptions', 'enableTemplateSourceLocations']; +// First Angular version that has the compiler option. +const COMPILER_OPTION_SINCE = [22, 1, 4]; +const PRELOAD = `NODE_OPTIONS="--import ${PACKAGE_NAME}/enable-source-locations" ng serve`; /** * - Turns on the `enableTemplateSourceLocations` compiler option for the `development` build - * configuration only, so template paths never end up in production bundles. + * configuration only, so template paths never end up in production bundles. Angular versions + * that don't have the option need a preload when starting `ng serve` instead. * - Adds `provideSourceInspector()` to the application providers. */ export function ngAdd(options: NgAddOptions): Rule { @@ -24,43 +34,10 @@ export function ngAdd(options: NgAddOptions): Rule { if (!project || project.extensions['projectType'] !== 'application') { throw new SchematicsException(`"${projectName}" isn't an application in this workspace.`); } - const build = project.targets.get('build'); - const appTsConfig = build?.options?.['tsConfig']; - if (!build || typeof appTsConfig !== 'string') { - throw new SchematicsException( - `Project "${projectName}" has no build target with a "tsConfig" option.`, - ); - } - const development = build.configurations?.['development']; - if (!development) { - throw new SchematicsException( - `Project "${projectName}" has no "development" build configuration. ` + - 'The compiler option has to be limited to development builds, add that configuration first.', - ); - } - - const rules: Rule[] = []; - const currentDevTsConfig = development['tsConfig']; - if (typeof currentDevTsConfig === 'string' && currentDevTsConfig !== appTsConfig) { - // The development configuration already has its own tsconfig: turn the option on there. - rules.push(enableCompilerOption(currentDevTsConfig)); - } else { - const devTsConfig = posix.join(posix.dirname(appTsConfig), DEV_TSCONFIG_NAME); - if (tree.exists(devTsConfig)) { - throw new SchematicsException( - `${devTsConfig} already exists but isn't used by the "development" configuration. ` + - `Set "tsConfig" to it there and run ng add again.`, - ); - } - tree.create(devTsConfig, devTsConfigContent(posix.basename(appTsConfig))); - rules.push( - updateWorkspace((updated) => { - const target = updated.projects.get(projectName)!.targets.get('build')!; - target.configurations!['development']!['tsConfig'] = devTsConfig; - }), - ); - context.logger.info(`Created ${devTsConfig}, used by the "development" build configuration.`); - } + const usePreload = !hasCompilerOption(tree); + const rules: Rule[] = usePreload + ? [] + : enableForDevelopment(tree, projectName, project, context); const sourceRoot = project.sourceRoot ?? posix.join(project.root, 'src'); if (containsProviderCall(tree, sourceRoot)) { @@ -76,17 +53,92 @@ export function ngAdd(options: NgAddOptions): Rule { rules.push(() => { context.logger.info( - 'Done. Run `ng serve`, hold Alt (Option on macOS) and hover the page, Alt+click to open ' + - 'the element in your editor.\n' + - 'Keep `enableTemplateSourceLocations` out of the tsconfig used for production builds, ' + - 'or template paths will end up in your bundles.', + usePreload + ? 'Done. This version of Angular has no compiler option for template source locations, ' + + 'start the dev server with the preload that turns them on:\n' + + ` ${PRELOAD}\n` + + 'Then hold Alt (Option on macOS) and hover the page, Alt+click to open the element ' + + 'in your editor.' + : 'Done. Run `ng serve`, hold Alt (Option on macOS) and hover the page, Alt+click to ' + + 'open the element in your editor.\n' + + 'Keep `enableTemplateSourceLocations` out of the tsconfig used for production ' + + 'builds, or template paths will end up in your bundles.', ); }); return chain(rules); }; } -function singleApplication(workspace: Awaited>): string { +type Workspace = Awaited>; +type Project = NonNullable>; + +/** Whether the workspace's Angular has the compiler option. */ +function hasCompilerOption(tree: Tree): boolean { + // The installed version, or else the lowest one that package.json allows. + const installed = 'node_modules/@angular/core/package.json'; + const version = tree.exists(installed) + ? new JSONFile(tree, installed).get(['version']) + : new JSONFile(tree, 'package.json').get(['dependencies', '@angular/core']); + const match = /(\d+)\.(\d+)\.(\d+)/.exec(String(version)); + if (!match) { + return true; + } + const [major, minor, patch] = match.slice(1).map(Number); + const [sinceMajor, sinceMinor, sincePatch] = COMPILER_OPTION_SINCE; + if (major !== sinceMajor) { + return major > sinceMajor; + } + return minor !== sinceMinor ? minor > sinceMinor : patch >= sincePatch; +} + +/** Turns the compiler option on in the tsconfig of the `development` build configuration. */ +function enableForDevelopment( + tree: Tree, + projectName: string, + project: Project, + context: SchematicContext, +): Rule[] { + const build = project.targets.get('build'); + const appTsConfig = build?.options?.['tsConfig']; + if (!build || typeof appTsConfig !== 'string') { + throw new SchematicsException( + `Project "${projectName}" has no build target with a "tsConfig" option.`, + ); + } + const development = build.configurations?.['development']; + if (!development) { + throw new SchematicsException( + `Project "${projectName}" has no "development" build configuration. ` + + 'The compiler option has to be limited to development builds, add that configuration first.', + ); + } + + const rules: Rule[] = []; + const currentDevTsConfig = development['tsConfig']; + if (typeof currentDevTsConfig === 'string' && currentDevTsConfig !== appTsConfig) { + // The development configuration already has its own tsconfig: turn the option on there. + rules.push(enableCompilerOption(currentDevTsConfig)); + } else { + const devTsConfig = posix.join(posix.dirname(appTsConfig), DEV_TSCONFIG_NAME); + if (tree.exists(devTsConfig)) { + throw new SchematicsException( + `${devTsConfig} already exists but isn't used by the "development" configuration. ` + + `Set "tsConfig" to it there and run ng add again.`, + ); + } + tree.create(devTsConfig, devTsConfigContent(posix.basename(appTsConfig))); + rules.push( + updateWorkspace((updated) => { + const target = updated.projects.get(projectName)!.targets.get('build')!; + target.configurations!['development']!['tsConfig'] = devTsConfig; + }), + ); + context.logger.info(`Created ${devTsConfig}, used by the "development" build configuration.`); + } + return rules; +} + +function singleApplication(workspace: Workspace): string { const applications = [...workspace.projects] .filter(([, project]) => project.extensions['projectType'] === 'application') .map(([name]) => name); diff --git a/projects/ngx-source-inspector/src/lib/inspector.ts b/projects/ngx-source-inspector/src/lib/inspector.ts index 95a5567..9a7bc37 100644 --- a/projects/ngx-source-inspector/src/lib/inspector.ts +++ b/projects/ngx-source-inspector/src/lib/inspector.ts @@ -104,7 +104,8 @@ export function install(doc: Document, options: SourceInspectorOptions = {}): () warnedMissingLocations = true; console.warn( '[ngx-source-inspector] No element has a `data-ng-source-location` attribute. Is the ' + - '`enableTemplateSourceLocations` compiler option on for this build? `ng add ngx-source-inspector` sets it up.', + '`enableTemplateSourceLocations` compiler option on for this build? `ng add ngx-source-inspector` sets it up. ' + + 'Before Angular 22.1.4, start `ng serve` with the `ngx-source-inspector/enable-source-locations` preload instead.', ); } hide(); diff --git a/scripts/check-angular-21.mjs b/scripts/check-angular-21.mjs new file mode 100644 index 0000000..3dc417c --- /dev/null +++ b/scripts/check-angular-21.mjs @@ -0,0 +1,90 @@ +// Checks the package with Angular 21, which doesn't have the compiler option yet: sets up a new +// Angular 21 app with `ng add`, then checks that `ng serve` with the preload produces source +// locations and that a production build doesn't. Run after `npm run build:lib`. +import { execFileSync, spawn } from 'node:child_process'; +import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { stripVTControlCharacters } from 'node:util'; + +const PRELOAD = '--import ngx-source-inspector/enable-source-locations'; +const repository = process.cwd(); +const workspace = mkdtempSync(join(tmpdir(), 'ngx-source-inspector-')); +const app = join(workspace, 'app'); +const env = { ...process.env, NODE_OPTIONS: PRELOAD, NG_CLI_ANALYTICS: 'false' }; + +const run = (command, args, options = {}) => + execFileSync(command, args, { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], + ...options, + }); + +function check(condition, message) { + if (!condition) { + throw new Error(message); + } +} + +/** Starts `ng serve` with the preload and returns the `main.js` that it serves. */ +async function servedMainBundle() { + const server = spawn('npx', ['ng', 'serve', '--port', '0'], { cwd: app, env, detached: true }); + try { + const url = await new Promise((resolveUrl, reject) => { + let output = ''; + server.stdout.on('data', (chunk) => { + // The CLI colors its output in CI, in the middle of the URL too. + output += stripVTControlCharacters(String(chunk)); + const match = /Local:\s+(http:\/\/[\w.:]+)/.exec(output); + if (match) { + resolveUrl(match[1]); + } + }); + server.stderr.pipe(process.stderr); + server.on('exit', () => reject(new Error(`ng serve stopped:\n${output}`))); + setTimeout(() => reject(new Error(`ng serve didn't start:\n${output}`)), 120_000).unref(); + }); + return await (await fetch(`${url}/main.js`)).text(); + } finally { + // The whole process group: npx, the CLI and its workers. + process.kill(-server.pid); + } +} + +try { + const [{ filename }] = JSON.parse( + run('npm', ['pack', './dist/ngx-source-inspector', '--pack-destination', workspace, '--json']), + ); + run('npx', ['-y', '@angular/cli@21', 'new', 'app', '--defaults', '--skip-git', '--skip-tests'], { + cwd: workspace, + }); + const { version } = JSON.parse( + readFileSync(join(app, 'node_modules/@angular/core/package.json'), 'utf8'), + ); + console.log(`Checking with Angular ${version}.`); + + run('npx', ['ng', 'add', join(workspace, filename), '--skip-confirmation'], { cwd: app }); + check( + readFileSync(join(app, 'src/app/app.config.ts'), 'utf8').includes('provideSourceInspector()'), + "ng add didn't add provideSourceInspector().", + ); + check( + !existsSync(join(app, 'tsconfig.dev.json')), + "ng add set up the compiler option, which this version of Angular doesn't have.", + ); + + check( + (await servedMainBundle()).includes('attachSourceLocations("src/app/app.html"'), + "ng serve with the preload didn't produce source locations.", + ); + + run('npx', ['ng', 'build'], { cwd: app, env }); + run( + process.execPath, + [resolve(repository, 'scripts/check-production-bundle.mjs'), 'dist/app/browser'], + { cwd: app, stdio: 'inherit' }, + ); + console.log(`OK: ng add, source locations in ng serve and a clean production build.`); +} finally { + rmSync(workspace, { recursive: true, force: true }); +} diff --git a/scripts/check-production-bundle.mjs b/scripts/check-production-bundle.mjs index a480c42..bcedda1 100644 --- a/scripts/check-production-bundle.mjs +++ b/scripts/check-production-bundle.mjs @@ -1,8 +1,9 @@ // Fails if anything from the inspector or the template source locations made it into the // production build of the demo app. Run after `ng build inspector-demo --configuration production`. +// Another app's output directory can be given as an argument. import { globSync, readFileSync } from 'node:fs'; -const outputDir = 'dist/inspector-demo/browser'; +const outputDir = process.argv[2] ?? 'dist/inspector-demo/browser'; const forbidden = [ // The inspector itself. 'ngx-source-inspector', diff --git a/scripts/copy-package-files.mjs b/scripts/copy-package-files.mjs index 37e5870..9fcdca1 100644 --- a/scripts/copy-package-files.mjs +++ b/scripts/copy-package-files.mjs @@ -1,6 +1,12 @@ // The repository README and license are also the package's. import { cpSync } from 'node:fs'; +const destination = 'dist/ngx-source-inspector'; + for (const file of ['README.md', 'LICENSE']) { - cpSync(file, `dist/ngx-source-inspector/${file}`); + cpSync(file, `${destination}/${file}`); } + +// The preload for `ng serve` is plain JavaScript for Node.js, there's nothing to compile. +const preload = 'node/enable-source-locations.mjs'; +cpSync(`projects/ngx-source-inspector/${preload}`, `${destination}/${preload}`);