Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate Angular production vs non-production entries #887

Merged
merged 1 commit into from
Jan 6, 2025
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
46 changes: 46 additions & 0 deletions packages/knip/fixtures/plugins/angular3/angular.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
}
}
14 changes: 14 additions & 0 deletions packages/knip/fixtures/plugins/angular3/package.json
Original file line number Diff line number Diff line change
@@ -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": "*"
}
}
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
111 changes: 70 additions & 41 deletions packages/knip/src/plugins/angular/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -24,48 +29,40 @@ const resolveConfig: ResolveConfig<AngularCLIWorkspaceConfiguration> = 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<string, EntriesByOption> = 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) {
Expand Down Expand Up @@ -102,16 +99,48 @@ const resolveConfig: ResolveConfig<AngularCLIWorkspaceConfiguration> = async (co
return Array.from(inputs);
};

type AngularScriptsBuildOption = Exclude<WebpackBrowserSchemaForBuildFacade['scripts'], undefined>;

const filesReplacedBy = (
//👇 Using Webpack-based browser schema to support old `replaceWith` file replacements
fileReplacements: Exclude<WebpackBrowserSchemaForBuildFacade['fileReplacements'], undefined>
): 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<Target['options'], undefined>;
type Target = Architect[string];
type Architect = Exclude<Project['architect'], undefined>;

type EntriesByOption = Map<string, readonly string[]>;

//👇 Using Webpack-based browser schema to support old `replaceWith` file replacements
type FileReplacementsBuildOption = Exclude<WebpackBrowserSchemaForBuildFacade['fileReplacements'], undefined>;
type ScriptsBuildOption = Exclude<WebpackBrowserSchemaForBuildFacade['scripts'], undefined>;

const PRODUCTION_CONFIG_NAME = 'production';
const BUILD_TARGET_NAME = 'build';

export default {
title,
enablers,
Expand Down
2 changes: 1 addition & 1 deletion packages/knip/src/plugins/angular/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
36 changes: 36 additions & 0 deletions packages/knip/test/plugins/angular3.test.ts
Original file line number Diff line number Diff line change
@@ -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({
davidlj95 marked this conversation as resolved.
Show resolved Hide resolved
...baseArguments,
isProduction: true,
cwd,
});

assert.deepEqual(prodCounters, {
...baseCounters,
processed: 4,
total: 4,
});
});
Loading