From c9b3416dcffe2ee69820a8617911f1c3006a7e65 Mon Sep 17 00:00:00 2001 From: David LJ Date: Fri, 20 Dec 2024 18:02:53 +0100 Subject: [PATCH] Separate Angular production vs non-production entries --- .../fixtures/plugins/angular3/angular.json | 46 ++++++++ .../fixtures/plugins/angular3/package.json | 14 +++ .../plugins/angular3/src/main-for-non-prod.ts | 0 .../plugins/angular3/src/main-for-testing.ts | 0 .../angular3/src/main.server-for-non-prod.ts | 0 .../plugins/angular3/src/main.server.ts | 0 .../fixtures/plugins/angular3/src/main.ts | 0 .../angular3/src/script-for-non-prod.js | 0 .../fixtures/plugins/angular3/src/script.js | 0 .../fixtures/plugins/angular3/src/server.ts | 0 packages/knip/src/plugins/angular/index.ts | 111 +++++++++++------- packages/knip/src/plugins/angular/types.ts | 2 +- packages/knip/test/plugins/angular3.test.ts | 36 ++++++ 13 files changed, 167 insertions(+), 42 deletions(-) create mode 100644 packages/knip/fixtures/plugins/angular3/angular.json create mode 100644 packages/knip/fixtures/plugins/angular3/package.json create mode 100644 packages/knip/fixtures/plugins/angular3/src/main-for-non-prod.ts create mode 100644 packages/knip/fixtures/plugins/angular3/src/main-for-testing.ts create mode 100644 packages/knip/fixtures/plugins/angular3/src/main.server-for-non-prod.ts create mode 100644 packages/knip/fixtures/plugins/angular3/src/main.server.ts create mode 100644 packages/knip/fixtures/plugins/angular3/src/main.ts create mode 100644 packages/knip/fixtures/plugins/angular3/src/script-for-non-prod.js create mode 100644 packages/knip/fixtures/plugins/angular3/src/script.js create mode 100644 packages/knip/fixtures/plugins/angular3/src/server.ts create mode 100644 packages/knip/test/plugins/angular3.test.ts diff --git a/packages/knip/fixtures/plugins/angular3/angular.json b/packages/knip/fixtures/plugins/angular3/angular.json new file mode 100644 index 000000000..006505631 --- /dev/null +++ b/packages/knip/fixtures/plugins/angular3/angular.json @@ -0,0 +1,46 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "knip-angular-example": { + "projectType": "application", + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "browser": "src/main.ts", + "ssr": { + "entry": "src/server.ts" + }, + "server": "src/main.server-for-non-prod.ts" + }, + "configurations": { + "production": { + "server": "src/main.server.ts", + "scripts": ["src/script.js"] + }, + "development": { + "scripts": ["src/script-for-non-prod.js"] + } + } + }, + "a-non-prod-target": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "main": "src/main-for-non-prod.ts" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/main-for-testing.ts" + } + } + } + } + } +} diff --git a/packages/knip/fixtures/plugins/angular3/package.json b/packages/knip/fixtures/plugins/angular3/package.json new file mode 100644 index 000000000..e87ba142a --- /dev/null +++ b/packages/knip/fixtures/plugins/angular3/package.json @@ -0,0 +1,14 @@ +{ + "name": "@fixtures/angular3", + "version": "*", + "devDependencies": { + "@angular-devkit/build-angular": "*", + "@angular/cli": "*", + "jasmine-core": "*", + "karma-chrome-launcher": "*", + "karma-coverage": "*", + "karma-jasmine": "*", + "karma-jasmine-html-reporter": "*", + "typescript": "*" + } +} diff --git a/packages/knip/fixtures/plugins/angular3/src/main-for-non-prod.ts b/packages/knip/fixtures/plugins/angular3/src/main-for-non-prod.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/knip/fixtures/plugins/angular3/src/main-for-testing.ts b/packages/knip/fixtures/plugins/angular3/src/main-for-testing.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/knip/fixtures/plugins/angular3/src/main.server-for-non-prod.ts b/packages/knip/fixtures/plugins/angular3/src/main.server-for-non-prod.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/knip/fixtures/plugins/angular3/src/main.server.ts b/packages/knip/fixtures/plugins/angular3/src/main.server.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/knip/fixtures/plugins/angular3/src/main.ts b/packages/knip/fixtures/plugins/angular3/src/main.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/knip/fixtures/plugins/angular3/src/script-for-non-prod.js b/packages/knip/fixtures/plugins/angular3/src/script-for-non-prod.js new file mode 100644 index 000000000..e69de29bb diff --git a/packages/knip/fixtures/plugins/angular3/src/script.js b/packages/knip/fixtures/plugins/angular3/src/script.js new file mode 100644 index 000000000..e69de29bb diff --git a/packages/knip/fixtures/plugins/angular3/src/server.ts b/packages/knip/fixtures/plugins/angular3/src/server.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/knip/src/plugins/angular/index.ts b/packages/knip/src/plugins/angular/index.ts index be55fe90f..f1e6f3c6b 100644 --- a/packages/knip/src/plugins/angular/index.ts +++ b/packages/knip/src/plugins/angular/index.ts @@ -3,7 +3,12 @@ import { type Input, toConfig, toDependency, toEntry, toProductionEntry } from ' import { join } from '../../util/path.js'; import { hasDependency } from '../../util/plugin.js'; import * as karma from '../karma/helpers.js'; -import type { AngularCLIWorkspaceConfiguration, KarmaTarget, WebpackBrowserSchemaForBuildFacade } from './types.js'; +import type { + AngularCLIWorkspaceConfiguration, + KarmaTarget, + Project, + WebpackBrowserSchemaForBuildFacade, +} from './types.js'; // https://angular.io/guide/workspace-config @@ -24,48 +29,40 @@ const resolveConfig: ResolveConfig = async (co for (const project of Object.values(config.projects)) { if (!project.architect) return []; - for (const target of Object.values(project.architect)) { + for (const [targetName, target] of Object.entries(project.architect)) { const { options: opts, configurations: configs } = target; const [packageName] = typeof target.builder === 'string' ? target.builder.split(':') : []; if (typeof packageName === 'string') inputs.add(toDependency(packageName)); if (opts) { - if ('main' in opts && typeof opts.main === 'string') { - inputs.add(toProductionEntry(join(cwd, opts.main))); - } - if ('browser' in opts && typeof opts.browser === 'string') { - inputs.add(toProductionEntry(join(cwd, opts.browser))); - } - if ('ssr' in opts && opts.ssr && typeof opts.ssr === 'object') { - if ('entry' in opts.ssr && typeof opts.ssr.entry === 'string') { - inputs.add(toProductionEntry(join(cwd, opts.ssr.entry))); - } - } if ('tsConfig' in opts && typeof opts.tsConfig === 'string') { inputs.add(toConfig('typescript', opts.tsConfig, configFilePath)); } - if ('server' in opts && opts.server && typeof opts.server === 'string') { - inputs.add(toProductionEntry(join(cwd, opts.server))); - } - if ('fileReplacements' in opts && opts.fileReplacements && Array.isArray(opts.fileReplacements)) { - for (const fileReplacedBy of filesReplacedBy(opts.fileReplacements)) { - inputs.add(toEntry(fileReplacedBy)); - } - } - if ('scripts' in opts && opts.scripts && Array.isArray(opts.scripts)) { - for (const scriptStringOrObject of opts.scripts as AngularScriptsBuildOption) { - const script = typeof scriptStringOrObject === 'string' ? scriptStringOrObject : scriptStringOrObject.input; - inputs.add(toProductionEntry(script)); + } + const defaultEntriesByOption: EntriesByOption = opts ? entriesByOption(opts) : new Map(); + const entriesByOptionByConfig: Map = new Map( + configs ? Object.entries(configs).map(([name, opts]) => [name, entriesByOption(opts)]) : [] + ); + const productionEntriesByOption: EntriesByOption = + entriesByOptionByConfig.get(PRODUCTION_CONFIG_NAME) ?? new Map(); + const normalizePath = (path: string) => join(cwd, path); + for (const [configName, entriesByOption] of entriesByOptionByConfig.entries()) { + for (const entries of entriesByOption.values()) { + for (const entry of entries) { + inputs.add( + targetName === BUILD_TARGET_NAME && configName === PRODUCTION_CONFIG_NAME + ? toProductionEntry(normalizePath(entry)) + : toEntry(normalizePath(entry)) + ); } } } - if (configs) { - for (const [configName, config] of Object.entries(configs)) { - const isProductionConfig = configName === 'production'; - if ('fileReplacements' in config && config.fileReplacements && Array.isArray(config.fileReplacements)) { - for (const fileReplacedBy of filesReplacedBy(config.fileReplacements)) { - inputs.add(isProductionConfig ? toProductionEntry(fileReplacedBy) : toEntry(fileReplacedBy)); - } - } + for (const [option, entries] of defaultEntriesByOption.entries()) { + for (const entry of entries) { + inputs.add( + targetName === BUILD_TARGET_NAME && !productionEntriesByOption.get(option)?.length + ? toProductionEntry(normalizePath(entry)) + : toEntry(normalizePath(entry)) + ); } } if (target.builder === '@angular-devkit/build-angular:karma' && opts) { @@ -102,16 +99,48 @@ const resolveConfig: ResolveConfig = async (co return Array.from(inputs); }; -type AngularScriptsBuildOption = Exclude; - -const filesReplacedBy = ( - //👇 Using Webpack-based browser schema to support old `replaceWith` file replacements - fileReplacements: Exclude -): readonly string[] => - fileReplacements.map(fileReplacement => - 'with' in fileReplacement ? fileReplacement.with : fileReplacement.replaceWith +const entriesByOption = (opts: TargetOptions): EntriesByOption => + new Map( + Object.entries({ + main: 'main' in opts && opts.main && typeof opts.main === 'string' ? [opts.main] : [], + scripts: + 'scripts' in opts && opts.scripts && Array.isArray(opts.scripts) + ? (opts.scripts as ScriptsBuildOption).map(scriptStringOrObject => + typeof scriptStringOrObject === 'string' ? scriptStringOrObject : scriptStringOrObject.input + ) + : [], + fileReplacements: + 'fileReplacements' in opts && opts.fileReplacements && Array.isArray(opts.fileReplacements) + ? (opts.fileReplacements as FileReplacementsBuildOption).map(fileReplacement => + 'with' in fileReplacement ? fileReplacement.with : fileReplacement.replaceWith + ) + : [], + browser: 'browser' in opts && opts.browser && typeof opts.browser === 'string' ? [opts.browser] : [], + server: 'server' in opts && opts.server && typeof opts.server === 'string' ? [opts.server] : [], + ssrEntry: + 'ssr' in opts && + opts.ssr && + typeof opts.ssr === 'object' && + 'entry' in opts.ssr && + typeof opts.ssr.entry === 'string' + ? [opts.ssr.entry] + : [], + }) ); +type TargetOptions = Exclude; +type Target = Architect[string]; +type Architect = Exclude; + +type EntriesByOption = Map; + +//👇 Using Webpack-based browser schema to support old `replaceWith` file replacements +type FileReplacementsBuildOption = Exclude; +type ScriptsBuildOption = Exclude; + +const PRODUCTION_CONFIG_NAME = 'production'; +const BUILD_TARGET_NAME = 'build'; + export default { title, enablers, diff --git a/packages/knip/src/plugins/angular/types.ts b/packages/knip/src/plugins/angular/types.ts index d753b396a..f07f7ab87 100644 --- a/packages/knip/src/plugins/angular/types.ts +++ b/packages/knip/src/plugins/angular/types.ts @@ -13,7 +13,7 @@ type FileVersion = number; * This interface was referenced by `undefined`'s JSON-Schema definition * via the `patternProperty` "^(?:@[a-zA-Z0-9._-]+/)?[a-zA-Z0-9._-]+$". */ -type Project = Project1 & { +export type Project = Project1 & { cli?: { [k: string]: unknown; }; diff --git a/packages/knip/test/plugins/angular3.test.ts b/packages/knip/test/plugins/angular3.test.ts new file mode 100644 index 000000000..a15da33f6 --- /dev/null +++ b/packages/knip/test/plugins/angular3.test.ts @@ -0,0 +1,36 @@ +import { test } from 'bun:test'; +import assert from 'node:assert/strict'; +import { main } from '../../src/index.js'; +import { resolve } from '../../src/util/path.js'; +import baseArguments from '../helpers/baseArguments.js'; +import baseCounters from '../helpers/baseCounters.js'; + +const cwd = resolve('fixtures/plugins/angular3'); + +test('Find dependencies with the Angular plugin (production vs non-production)', async () => { + const { issues: nonProdIssues, counters: nonProdCounters } = await main({ + ...baseArguments, + cwd, + }); + + assert(nonProdIssues.devDependencies['package.json']['@angular/cli']); + + assert.deepEqual(nonProdCounters, { + ...baseCounters, + devDependencies: 1, + processed: 8, + total: 8, + }); + + const { counters: prodCounters } = await main({ + ...baseArguments, + isProduction: true, + cwd, + }); + + assert.deepEqual(prodCounters, { + ...baseCounters, + processed: 4, + total: 4, + }); +});