Skip to content

Commit ac1383f

Browse files
devversiondgp1130
authored andcommitted
fix(@angular-devkit/build-angular): properly handle locally-built APF v14 libraries
Locally-built APF v14 libraries should be resolved properly. Webpack currently does not resolve them (in e.g. `dist/`) because the local distribution folders are not marked as module roots, causing Webpack to never hit the `module`/`raw-module` resolution hooks and therefore skipping package exports resolution and breaking secondary entry-points from being resolved properly (when bundling). We fix this by also attempting to resolve path mappings as modules, allowing for Webpacks `resolve-in-package` hooks to be activated. These hooks support the `exports` field and therefore APF v14 secondary entry-points which are not necessarily inside a Webpack resolve `modules:` root (but e.g. in `dist/`) (cherry picked from commit ba93117)
1 parent b0c3ced commit ac1383f

File tree

2 files changed

+143
-20
lines changed

2 files changed

+143
-20
lines changed

packages/ngtools/webpack/src/paths-plugin.ts

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import * as path from 'path';
1010
import { CompilerOptions } from 'typescript';
11+
1112
import type { Configuration } from 'webpack';
1213

1314
// eslint-disable-next-line @typescript-eslint/no-empty-interface
@@ -16,6 +17,9 @@ export interface TypeScriptPathsPluginOptions extends Pick<CompilerOptions, 'pat
1617
// Extract Resolver type from Webpack types since it is not directly exported
1718
type Resolver = Exclude<Exclude<Configuration['resolve'], undefined>['resolver'], undefined>;
1819

20+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
21+
type DoResolveValue = any;
22+
1923
interface PathPattern {
2024
starIndex: number;
2125
prefix: string;
@@ -154,40 +158,75 @@ export class TypeScriptPathsPlugin {
154158
// For example, if the first one resolves, any others are not needed and do not need
155159
// to be created.
156160
const replacements = findReplacements(originalRequest, this.patterns);
161+
const basePath = this.baseUrl ?? '';
162+
163+
const attemptResolveRequest = (request: DoResolveValue): Promise<DoResolveValue | null> => {
164+
return new Promise((resolve, reject) => {
165+
resolver.doResolve(
166+
target,
167+
request,
168+
'',
169+
resolveContext,
170+
(error: Error | null, result: DoResolveValue) => {
171+
if (error) {
172+
reject(error);
173+
} else if (result) {
174+
resolve(result);
175+
} else {
176+
resolve(null);
177+
}
178+
},
179+
);
180+
});
181+
};
157182

158-
const tryResolve = () => {
183+
const tryNextReplacement = () => {
159184
const next = replacements.next();
160185
if (next.done) {
161186
callback();
162187

163188
return;
164189
}
165190

166-
const potentialRequest = {
191+
const targetPath = path.resolve(basePath, next.value);
192+
// If there is no extension. i.e. the target does not refer to an explicit
193+
// file, then this is a candidate for module/package resolution.
194+
const canBeModule = path.extname(targetPath) === '';
195+
196+
// Resolution in the target location, preserving the original request.
197+
// This will work with the `resolve-in-package` resolution hook, supporting
198+
// package exports for e.g. locally-built APF libraries.
199+
const potentialRequestAsPackage = {
167200
...request,
168-
request: path.resolve(this.baseUrl ?? '', next.value),
201+
path: targetPath,
169202
typescriptPathMapped: true,
170203
};
171204

172-
resolver.doResolve(
173-
target,
174-
potentialRequest,
175-
'',
176-
resolveContext,
177-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
178-
(error: Error | null, result: any) => {
179-
if (error) {
180-
callback(error);
181-
} else if (result) {
182-
callback(undefined, result);
183-
} else {
184-
tryResolve();
185-
}
186-
},
187-
);
205+
// Resolution in the original callee location, but with the updated request
206+
// to point to the mapped target location.
207+
const potentialRequestAsFile = {
208+
...request,
209+
request: targetPath,
210+
typescriptPathMapped: true,
211+
};
212+
213+
let resultPromise = attemptResolveRequest(potentialRequestAsFile);
214+
215+
// If the request can be a module, we configure the resolution to try package/module
216+
// resolution if the file resolution did not have a result.
217+
if (canBeModule) {
218+
resultPromise = resultPromise.then(
219+
(result) => result ?? attemptResolveRequest(potentialRequestAsPackage),
220+
);
221+
}
222+
223+
// If we have a result, complete. If not, and no error, try the next replacement.
224+
resultPromise
225+
.then((res) => (res === null ? tryNextReplacement() : callback(undefined, res)))
226+
.catch((error) => callback(error));
188227
};
189228

190-
tryResolve();
229+
tryNextReplacement();
191230
},
192231
);
193232
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { createDir, writeFile } from '../../utils/fs';
10+
import { ng } from '../../utils/process';
11+
import { updateJsonFile } from '../../utils/project';
12+
13+
export default async function () {
14+
await ng('generate', 'library', 'mylib');
15+
await createLibraryEntryPoint('secondary', 'SecondaryModule', 'index.ts');
16+
await createLibraryEntryPoint('another', 'AnotherModule', 'index.ts');
17+
18+
// Scenario #1 where we use wildcard path mappings for secondary entry-points.
19+
await updateJsonFile('tsconfig.json', (json) => {
20+
json.compilerOptions.paths = { 'mylib': ['dist/mylib'], 'mylib/*': ['dist/mylib/*'] };
21+
});
22+
23+
await writeFile(
24+
'src/app/app.module.ts',
25+
`
26+
import {NgModule} from '@angular/core';
27+
import {BrowserModule} from '@angular/platform-browser';
28+
import {SecondaryModule} from 'mylib/secondary';
29+
import {AnotherModule} from 'mylib/another';
30+
31+
import {AppComponent} from './app.component';
32+
33+
@NgModule({
34+
declarations: [
35+
AppComponent
36+
],
37+
imports: [
38+
SecondaryModule,
39+
AnotherModule,
40+
BrowserModule
41+
],
42+
providers: [],
43+
bootstrap: [AppComponent]
44+
})
45+
export class AppModule { }
46+
`,
47+
);
48+
49+
await ng('build', 'mylib');
50+
await ng('build');
51+
52+
// Scenario #2 where we don't use wildcard path mappings.
53+
await updateJsonFile('tsconfig.json', (json) => {
54+
json.compilerOptions.paths = {
55+
'mylib': ['dist/mylib'],
56+
'mylib/secondary': ['dist/mylib/secondary'],
57+
'mylib/another': ['dist/mylib/another'],
58+
};
59+
});
60+
61+
await ng('build');
62+
}
63+
64+
async function createLibraryEntryPoint(name: string, moduleName: string, entryFileName: string) {
65+
await createDir(`projects/mylib/${name}`);
66+
await writeFile(
67+
`projects/mylib/${name}/${entryFileName}`,
68+
`
69+
import {NgModule} from '@angular/core';
70+
71+
@NgModule({})
72+
export class ${moduleName} {}
73+
`,
74+
);
75+
76+
await writeFile(
77+
`projects/mylib/${name}/ng-package.json`,
78+
JSON.stringify({
79+
lib: {
80+
entryFile: entryFileName,
81+
},
82+
}),
83+
);
84+
}

0 commit comments

Comments
 (0)