Skip to content

Commit 91130f8

Browse files
authored
Separate Angular production vs non-production entries (#887)
1 parent b106a5f commit 91130f8

File tree

13 files changed

+167
-42
lines changed

13 files changed

+167
-42
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3+
"version": 1,
4+
"newProjectRoot": "projects",
5+
"projects": {
6+
"knip-angular-example": {
7+
"projectType": "application",
8+
"root": "",
9+
"sourceRoot": "src",
10+
"prefix": "app",
11+
"architect": {
12+
"build": {
13+
"builder": "@angular-devkit/build-angular:application",
14+
"options": {
15+
"browser": "src/main.ts",
16+
"ssr": {
17+
"entry": "src/server.ts"
18+
},
19+
"server": "src/main.server-for-non-prod.ts"
20+
},
21+
"configurations": {
22+
"production": {
23+
"server": "src/main.server.ts",
24+
"scripts": ["src/script.js"]
25+
},
26+
"development": {
27+
"scripts": ["src/script-for-non-prod.js"]
28+
}
29+
}
30+
},
31+
"a-non-prod-target": {
32+
"builder": "@angular-devkit/build-angular:browser",
33+
"options": {
34+
"main": "src/main-for-non-prod.ts"
35+
}
36+
},
37+
"test": {
38+
"builder": "@angular-devkit/build-angular:karma",
39+
"options": {
40+
"main": "src/main-for-testing.ts"
41+
}
42+
}
43+
}
44+
}
45+
}
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "@fixtures/angular3",
3+
"version": "*",
4+
"devDependencies": {
5+
"@angular-devkit/build-angular": "*",
6+
"@angular/cli": "*",
7+
"jasmine-core": "*",
8+
"karma-chrome-launcher": "*",
9+
"karma-coverage": "*",
10+
"karma-jasmine": "*",
11+
"karma-jasmine-html-reporter": "*",
12+
"typescript": "*"
13+
}
14+
}

packages/knip/fixtures/plugins/angular3/src/main-for-non-prod.ts

Whitespace-only changes.

packages/knip/fixtures/plugins/angular3/src/main-for-testing.ts

Whitespace-only changes.

packages/knip/fixtures/plugins/angular3/src/main.server-for-non-prod.ts

Whitespace-only changes.

packages/knip/fixtures/plugins/angular3/src/main.server.ts

Whitespace-only changes.

packages/knip/fixtures/plugins/angular3/src/main.ts

Whitespace-only changes.

packages/knip/fixtures/plugins/angular3/src/script-for-non-prod.js

Whitespace-only changes.

packages/knip/fixtures/plugins/angular3/src/script.js

Whitespace-only changes.

packages/knip/fixtures/plugins/angular3/src/server.ts

Whitespace-only changes.

packages/knip/src/plugins/angular/index.ts

+70-41
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import { type Input, toConfig, toDependency, toEntry, toProductionEntry } from '
33
import { join } from '../../util/path.js';
44
import { hasDependency } from '../../util/plugin.js';
55
import * as karma from '../karma/helpers.js';
6-
import type { AngularCLIWorkspaceConfiguration, KarmaTarget, WebpackBrowserSchemaForBuildFacade } from './types.js';
6+
import type {
7+
AngularCLIWorkspaceConfiguration,
8+
KarmaTarget,
9+
Project,
10+
WebpackBrowserSchemaForBuildFacade,
11+
} from './types.js';
712

813
// https://angular.io/guide/workspace-config
914

@@ -24,48 +29,40 @@ const resolveConfig: ResolveConfig<AngularCLIWorkspaceConfiguration> = async (co
2429

2530
for (const project of Object.values(config.projects)) {
2631
if (!project.architect) return [];
27-
for (const target of Object.values(project.architect)) {
32+
for (const [targetName, target] of Object.entries(project.architect)) {
2833
const { options: opts, configurations: configs } = target;
2934
const [packageName] = typeof target.builder === 'string' ? target.builder.split(':') : [];
3035
if (typeof packageName === 'string') inputs.add(toDependency(packageName));
3136
if (opts) {
32-
if ('main' in opts && typeof opts.main === 'string') {
33-
inputs.add(toProductionEntry(join(cwd, opts.main)));
34-
}
35-
if ('browser' in opts && typeof opts.browser === 'string') {
36-
inputs.add(toProductionEntry(join(cwd, opts.browser)));
37-
}
38-
if ('ssr' in opts && opts.ssr && typeof opts.ssr === 'object') {
39-
if ('entry' in opts.ssr && typeof opts.ssr.entry === 'string') {
40-
inputs.add(toProductionEntry(join(cwd, opts.ssr.entry)));
41-
}
42-
}
4337
if ('tsConfig' in opts && typeof opts.tsConfig === 'string') {
4438
inputs.add(toConfig('typescript', opts.tsConfig, configFilePath));
4539
}
46-
if ('server' in opts && opts.server && typeof opts.server === 'string') {
47-
inputs.add(toProductionEntry(join(cwd, opts.server)));
48-
}
49-
if ('fileReplacements' in opts && opts.fileReplacements && Array.isArray(opts.fileReplacements)) {
50-
for (const fileReplacedBy of filesReplacedBy(opts.fileReplacements)) {
51-
inputs.add(toEntry(fileReplacedBy));
52-
}
53-
}
54-
if ('scripts' in opts && opts.scripts && Array.isArray(opts.scripts)) {
55-
for (const scriptStringOrObject of opts.scripts as AngularScriptsBuildOption) {
56-
const script = typeof scriptStringOrObject === 'string' ? scriptStringOrObject : scriptStringOrObject.input;
57-
inputs.add(toProductionEntry(script));
40+
}
41+
const defaultEntriesByOption: EntriesByOption = opts ? entriesByOption(opts) : new Map();
42+
const entriesByOptionByConfig: Map<string, EntriesByOption> = new Map(
43+
configs ? Object.entries(configs).map(([name, opts]) => [name, entriesByOption(opts)]) : []
44+
);
45+
const productionEntriesByOption: EntriesByOption =
46+
entriesByOptionByConfig.get(PRODUCTION_CONFIG_NAME) ?? new Map();
47+
const normalizePath = (path: string) => join(cwd, path);
48+
for (const [configName, entriesByOption] of entriesByOptionByConfig.entries()) {
49+
for (const entries of entriesByOption.values()) {
50+
for (const entry of entries) {
51+
inputs.add(
52+
targetName === BUILD_TARGET_NAME && configName === PRODUCTION_CONFIG_NAME
53+
? toProductionEntry(normalizePath(entry))
54+
: toEntry(normalizePath(entry))
55+
);
5856
}
5957
}
6058
}
61-
if (configs) {
62-
for (const [configName, config] of Object.entries(configs)) {
63-
const isProductionConfig = configName === 'production';
64-
if ('fileReplacements' in config && config.fileReplacements && Array.isArray(config.fileReplacements)) {
65-
for (const fileReplacedBy of filesReplacedBy(config.fileReplacements)) {
66-
inputs.add(isProductionConfig ? toProductionEntry(fileReplacedBy) : toEntry(fileReplacedBy));
67-
}
68-
}
59+
for (const [option, entries] of defaultEntriesByOption.entries()) {
60+
for (const entry of entries) {
61+
inputs.add(
62+
targetName === BUILD_TARGET_NAME && !productionEntriesByOption.get(option)?.length
63+
? toProductionEntry(normalizePath(entry))
64+
: toEntry(normalizePath(entry))
65+
);
6966
}
7067
}
7168
if (target.builder === '@angular-devkit/build-angular:karma' && opts) {
@@ -102,16 +99,48 @@ const resolveConfig: ResolveConfig<AngularCLIWorkspaceConfiguration> = async (co
10299
return Array.from(inputs);
103100
};
104101

105-
type AngularScriptsBuildOption = Exclude<WebpackBrowserSchemaForBuildFacade['scripts'], undefined>;
106-
107-
const filesReplacedBy = (
108-
//👇 Using Webpack-based browser schema to support old `replaceWith` file replacements
109-
fileReplacements: Exclude<WebpackBrowserSchemaForBuildFacade['fileReplacements'], undefined>
110-
): readonly string[] =>
111-
fileReplacements.map(fileReplacement =>
112-
'with' in fileReplacement ? fileReplacement.with : fileReplacement.replaceWith
102+
const entriesByOption = (opts: TargetOptions): EntriesByOption =>
103+
new Map(
104+
Object.entries({
105+
main: 'main' in opts && opts.main && typeof opts.main === 'string' ? [opts.main] : [],
106+
scripts:
107+
'scripts' in opts && opts.scripts && Array.isArray(opts.scripts)
108+
? (opts.scripts as ScriptsBuildOption).map(scriptStringOrObject =>
109+
typeof scriptStringOrObject === 'string' ? scriptStringOrObject : scriptStringOrObject.input
110+
)
111+
: [],
112+
fileReplacements:
113+
'fileReplacements' in opts && opts.fileReplacements && Array.isArray(opts.fileReplacements)
114+
? (opts.fileReplacements as FileReplacementsBuildOption).map(fileReplacement =>
115+
'with' in fileReplacement ? fileReplacement.with : fileReplacement.replaceWith
116+
)
117+
: [],
118+
browser: 'browser' in opts && opts.browser && typeof opts.browser === 'string' ? [opts.browser] : [],
119+
server: 'server' in opts && opts.server && typeof opts.server === 'string' ? [opts.server] : [],
120+
ssrEntry:
121+
'ssr' in opts &&
122+
opts.ssr &&
123+
typeof opts.ssr === 'object' &&
124+
'entry' in opts.ssr &&
125+
typeof opts.ssr.entry === 'string'
126+
? [opts.ssr.entry]
127+
: [],
128+
})
113129
);
114130

131+
type TargetOptions = Exclude<Target['options'], undefined>;
132+
type Target = Architect[string];
133+
type Architect = Exclude<Project['architect'], undefined>;
134+
135+
type EntriesByOption = Map<string, readonly string[]>;
136+
137+
//👇 Using Webpack-based browser schema to support old `replaceWith` file replacements
138+
type FileReplacementsBuildOption = Exclude<WebpackBrowserSchemaForBuildFacade['fileReplacements'], undefined>;
139+
type ScriptsBuildOption = Exclude<WebpackBrowserSchemaForBuildFacade['scripts'], undefined>;
140+
141+
const PRODUCTION_CONFIG_NAME = 'production';
142+
const BUILD_TARGET_NAME = 'build';
143+
115144
export default {
116145
title,
117146
enablers,

packages/knip/src/plugins/angular/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ type FileVersion = number;
1313
* This interface was referenced by `undefined`'s JSON-Schema definition
1414
* via the `patternProperty` "^(?:@[a-zA-Z0-9._-]+/)?[a-zA-Z0-9._-]+$".
1515
*/
16-
type Project = Project1 & {
16+
export type Project = Project1 & {
1717
cli?: {
1818
[k: string]: unknown;
1919
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { test } from 'bun:test';
2+
import assert from 'node:assert/strict';
3+
import { main } from '../../src/index.js';
4+
import { resolve } from '../../src/util/path.js';
5+
import baseArguments from '../helpers/baseArguments.js';
6+
import baseCounters from '../helpers/baseCounters.js';
7+
8+
const cwd = resolve('fixtures/plugins/angular3');
9+
10+
test('Find dependencies with the Angular plugin (production vs non-production)', async () => {
11+
const { issues: nonProdIssues, counters: nonProdCounters } = await main({
12+
...baseArguments,
13+
cwd,
14+
});
15+
16+
assert(nonProdIssues.devDependencies['package.json']['@angular/cli']);
17+
18+
assert.deepEqual(nonProdCounters, {
19+
...baseCounters,
20+
devDependencies: 1,
21+
processed: 8,
22+
total: 8,
23+
});
24+
25+
const { counters: prodCounters } = await main({
26+
...baseArguments,
27+
isProduction: true,
28+
cwd,
29+
});
30+
31+
assert.deepEqual(prodCounters, {
32+
...baseCounters,
33+
processed: 4,
34+
total: 4,
35+
});
36+
});

0 commit comments

Comments
 (0)