Skip to content

Commit efcf289

Browse files
committed
Step 2 - enable user-defined plugins similar to custom-esbuild.
1 parent e1b7aa9 commit efcf289

File tree

11 files changed

+213
-3
lines changed

11 files changed

+213
-3
lines changed

apps/host-application/project.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"options": {
1515
"mf": {
1616
"externalList": "build-external-list.json",
17-
"skipList": "build-skip-list.json"
17+
"skipList": "build-skip-list.json",
18+
"esPlugins": ["tools/esbuild-plugin/test-external-plugin.ts"]
1819
},
1920
"outputPath": "dist/apps/host-application",
2021
"index": "apps/host-application/src/index.html",

libs/nx-angular-mf/src/builders/build/schema.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,13 @@
678678
"type": "string"
679679
}
680680
]
681+
},
682+
"esPlugins": {
683+
"description": "List of esbuild plugins that should be applied to the build.",
684+
"type": "array",
685+
"items": {
686+
"type": "string"
687+
}
681688
}
682689
},
683690
"additionalProperties": false

libs/nx-angular-mf/src/builders/helpers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './utils';
22
export * from './prepare-config'
33
export * from './dependencies'
44
export * from './patch-builder-context'
5+
export * from './load-module'
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import * as path from 'node:path';
2+
import * as url from 'node:url';
3+
import type { logging } from '@angular-devkit/core';
4+
5+
const _tsNodeRegister = (() => {
6+
let lastTsConfig: string | undefined;
7+
return (tsConfig: string, logger: logging.LoggerApi) => {
8+
// Check if the function was previously called with the same tsconfig
9+
if (lastTsConfig && lastTsConfig !== tsConfig) {
10+
logger.warn(`Trying to register ts-node again with a different tsconfig - skipping the registration.
11+
tsconfig 1: ${lastTsConfig}
12+
tsconfig 2: ${tsConfig}`);
13+
}
14+
15+
if (lastTsConfig) {
16+
return;
17+
}
18+
19+
lastTsConfig = tsConfig;
20+
21+
loadTsNode().register({
22+
project: tsConfig,
23+
compilerOptions: {
24+
module: 'CommonJS',
25+
types: [
26+
'node', // NOTE: `node` is added because users scripts can also use pure node's packages as webpack or others
27+
],
28+
},
29+
});
30+
31+
const tsConfigPaths = loadTsConfigPaths();
32+
const result = tsConfigPaths.loadConfig(tsConfig);
33+
// The `loadConfig` returns a `ConfigLoaderResult` which must be guarded with
34+
// the `resultType` check.
35+
if (result.resultType === 'success') {
36+
const { absoluteBaseUrl: baseUrl, paths } = result;
37+
38+
if (baseUrl && paths) {
39+
tsConfigPaths.register({ baseUrl, paths });
40+
}
41+
}
42+
};
43+
})();
44+
45+
/**
46+
* check for TS node registration
47+
* @param file: file name or file directory are allowed
48+
* @todo tsNodeRegistration: require ts-node if file extension is TypeScript
49+
*/
50+
function tsNodeRegister(
51+
file = '',
52+
tsConfig: string,
53+
logger: logging.LoggerApi
54+
) {
55+
if (file?.endsWith('.ts')) {
56+
// Register TS compiler lazily
57+
_tsNodeRegister(tsConfig, logger);
58+
}
59+
}
60+
61+
/**
62+
* This uses a dynamic import to load a module which may be ESM.
63+
* CommonJS code can load ESM code via a dynamic import. Unfortunately, TypeScript
64+
* will currently, unconditionally downlevel dynamic import into a require call.
65+
* require calls cannot load ESM code and will result in a runtime error. To workaround
66+
* this, a Function constructor is used to prevent TypeScript from changing the dynamic import.
67+
* Once TypeScript provides support for keeping the dynamic import this workaround can
68+
* be dropped.
69+
*
70+
* @param modulePath The path of the module to load.
71+
* @returns A Promise that resolves to the dynamically imported module.
72+
*/
73+
function loadEsmModule<T>(modulePath: string | URL): Promise<T> {
74+
return new Function('modulePath', `return import(modulePath);`)(
75+
modulePath
76+
) as Promise<T>;
77+
}
78+
79+
/**
80+
* Loads CJS and ESM modules based on extension
81+
*/
82+
export async function loadModule<T>(
83+
modulePath: string,
84+
tsConfig: string,
85+
logger: logging.LoggerApi
86+
): Promise<T> {
87+
tsNodeRegister(modulePath, tsConfig, logger);
88+
89+
switch (path.extname(modulePath)) {
90+
case '.mjs':
91+
// Load the ESM configuration file using the TypeScript dynamic import workaround.
92+
// Once TypeScript provides support for keeping the dynamic import this workaround can be
93+
// changed to a direct dynamic import.
94+
return (
95+
await loadEsmModule<{ default: T }>(url.pathToFileURL(modulePath))
96+
).default;
97+
case '.cjs':
98+
return require(modulePath);
99+
case '.ts':
100+
try {
101+
// If it's a TS file then there are 2 cases for exporing an object.
102+
// The first one is `export blah`, transpiled into `module.exports = { blah} `.
103+
// The second is `export default blah`, transpiled into `{ default: { ... } }`.
104+
return require(modulePath).default || require(modulePath);
105+
} catch (e: any) {
106+
if (e.code === 'ERR_REQUIRE_ESM') {
107+
// Load the ESM configuration file using the TypeScript dynamic import workaround.
108+
// Once TypeScript provides support for keeping the dynamic import this workaround can be
109+
// changed to a direct dynamic import.
110+
return (
111+
await loadEsmModule<{ default: T }>(url.pathToFileURL(modulePath))
112+
).default;
113+
}
114+
throw e;
115+
}
116+
//.js
117+
default:
118+
// The file could be either CommonJS or ESM.
119+
// CommonJS is tried first then ESM if loading fails.
120+
try {
121+
return require(modulePath);
122+
} catch (e: any) {
123+
if (e.code === 'ERR_REQUIRE_ESM') {
124+
// Load the ESM configuration file using the TypeScript dynamic import workaround.
125+
// Once TypeScript provides support for keeping the dynamic import this workaround can be
126+
// changed to a direct dynamic import.
127+
return (
128+
await loadEsmModule<{ default: T }>(url.pathToFileURL(modulePath))
129+
).default;
130+
}
131+
132+
throw e;
133+
}
134+
}
135+
}
136+
137+
/**
138+
* Loads `ts-node` lazily. Moved to a separate function to declare
139+
* a return type, more readable than an inline variant.
140+
*/
141+
function loadTsNode(): typeof import('ts-node') {
142+
return require('ts-node');
143+
}
144+
145+
/**
146+
* Loads `tsconfig-paths` lazily. Moved to a separate function to declare
147+
* a return type, more readable than an inline variant.
148+
*/
149+
function loadTsConfigPaths(): typeof import('tsconfig-paths') {
150+
return require('tsconfig-paths');
151+
}

libs/nx-angular-mf/src/builders/helpers/prepare-config.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export async function prepareConfig(
1515
): Promise<ConfigMf> {
1616
const skipList: ConfigMf['skipList'] = [];
1717
const externalList: ConfigMf['externalList'] = [];
18+
const esPlugins: ConfigMf['esPlugins'] = [];
1819

1920
if (defaultOptions.skipList) {
2021
skipList.push(...(await checkIsFileOrArray(defaultOptions.skipList)));
@@ -27,14 +28,29 @@ export async function prepareConfig(
2728
const externalMapObject = externalMap(externalList);
2829
const shareObject = getFullShare(externalMapObject, skipList);
2930

31+
if (defaultOptions.esPlugins && Array.isArray(defaultOptions.esPlugins)) {
32+
const tmpEsPlugins = [];
33+
for (const pathToPlugin of defaultOptions.esPlugins) {
34+
const fullPathToPlugin = join(workspaceRootPath, pathToPlugin);
35+
const result = await fsPromise.lstat(fullPathToPlugin);
36+
if (!result.isFile()) {
37+
throw new Error(`Invalid plugin path: ${result}`);
38+
}
39+
40+
tmpEsPlugins.push(pathToPlugin);
41+
}
42+
esPlugins.push(...tmpEsPlugins);
43+
}
44+
3045
return {
3146
skipList: skipList,
3247
externalList: externalList,
3348
shared: shareObject,
3449
sharedMappings: getSharedMappings().filter(
3550
(i) => !skipList.includes(i.key)
3651
),
37-
outPutFileNames: []
52+
outPutFileNames: [],
53+
esPlugins
3854
};
3955
}
4056

libs/nx-angular-mf/src/builders/schema/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ export type SchemaMf = {
22
mf?: {
33
skipList?: string | string[];
44
externalList?: string | string[];
5+
esPlugins?: string[];
56
};
67
};

libs/nx-angular-mf/src/builders/schema/schema.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@
3434
"type": "string"
3535
}
3636
]
37+
},
38+
"esPlugins": {
39+
"description": "List of esbuild plugins that should be applied to the build.",
40+
"type": "array",
41+
"items": {
42+
"type": "string"
43+
}
3744
}
3845
},
3946
"additionalProperties": false

libs/nx-angular-mf/src/builders/serve/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ import {
88
serveWithVite,
99
buildApplicationInternal,
1010
} from '@angular/build/private';
11+
import { Plugin } from 'esbuild';
1112

1213
import { ServeExecutorSchema } from './schema';
1314
import { BuildExecutorSchema } from '../build/schema';
14-
import { deepMergeObject, getMapName, patchBuilderContext, prepareConfig } from '../helpers';
15+
import { deepMergeObject, getMapName, loadModule, patchBuilderContext, prepareConfig } from '../helpers';
1516
import { entryPointForExtendDependencies } from '../es-plugin';
1617

1718

@@ -78,7 +79,13 @@ export async function* runBuilder(
7879
defaultOptions
7980
);
8081

82+
const esPluginPromise = optionsMfe.esPlugins.map((item) =>
83+
loadModule<Plugin>(item, targetOptions.tsConfig, context.logger)
84+
);
85+
const esPlugins = await Promise.all(esPluginPromise);
86+
8187
const resultEsBuild = [
88+
...esPlugins,
8289
entryPointForExtendDependencies(optionsMfe)
8390
]
8491

libs/nx-angular-mf/src/builders/serve/schema.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,13 @@
169169
"type": "string"
170170
}
171171
]
172+
},
173+
"esPlugins": {
174+
"description": "List of esbuild plugins that should be applied to the build.",
175+
"type": "array",
176+
"items": {
177+
"type": "string"
178+
}
172179
}
173180
},
174181
"additionalProperties": false

libs/nx-angular-mf/src/builders/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type ConfigMf = {
44
shared: SharedMap;
55
sharedMappings: { key: string; path: string }[];
66
outPutFileNames: string[];
7+
esPlugins: string[];
78
};
89

910
export type ShareOptions = {

0 commit comments

Comments
 (0)