@blotch/angular-lynx-build 0.0.0-stage → 22.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -2
- package/builders/application/index.js +165 -0
- package/builders/application/native-cache.js +56 -0
- package/builders/application/schema.js +2 -0
- package/builders/application/schema.json +229 -0
- package/builders/builders.json +19 -0
- package/builders/dev-server/index.js +82 -0
- package/builders/dev-server/schema.js +2 -0
- package/builders/dev-server/schema.json +28 -0
- package/builders/package.json +3 -0
- package/builders/project-import.js +64 -0
- package/builders/rspeedy.js +162 -0
- package/builders/unit-test/index.js +207 -0
- package/builders/unit-test/lynx-globals.d.mts +3 -0
- package/builders/unit-test/lynx-globals.mjs +18 -0
- package/builders/unit-test/schema.js +2 -0
- package/builders/unit-test/schema.json +311 -0
- package/client.d.ts +8 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +14 -0
- package/package.json +80 -4
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema",
|
|
3
|
+
"$id": "AngularLynxDevServerBuilder",
|
|
4
|
+
"title": "AngularLynx dev-server builder",
|
|
5
|
+
"description": "Runs the rspeedy dev server, which Lynx Explorer and the web preview load the app from.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"buildTarget": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "The build target to serve, as `project:target[:configuration]`, e.g. `my-app:build:development`. Its options (the Angular compiler options, `config`) configure the build, and its `optimization` picks a production or development build. Defaults to the project's build target with its development configuration, as Angular's own dev server does.",
|
|
12
|
+
"pattern": "^[^:\\s]*:[^:\\s]*(:[^\\s]+)?$"
|
|
13
|
+
},
|
|
14
|
+
"port": {
|
|
15
|
+
"type": "number",
|
|
16
|
+
"description": "The port to serve on. Overrides `server.port` in lynx.config.ts."
|
|
17
|
+
},
|
|
18
|
+
"host": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"description": "The host to listen on. Overrides `server.host` in lynx.config.ts."
|
|
21
|
+
},
|
|
22
|
+
"environment": {
|
|
23
|
+
"type": "array",
|
|
24
|
+
"items": { "type": "string" },
|
|
25
|
+
"description": "Only build these rspeedy environments (e.g. lynx). Defaults to the build target's `environment`, or all of them."
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.importFromProject = void 0;
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_module_1 = require("node:module");
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const node_url_1 = require("node:url");
|
|
11
|
+
/**
|
|
12
|
+
* Imports a package installed in the user's project (rspeedy), not one this
|
|
13
|
+
* package depends on. The project pins its own rspeedy version in
|
|
14
|
+
* package.json, and that is the version `ng build` must run, just as
|
|
15
|
+
* `rspeedy build` would.
|
|
16
|
+
*
|
|
17
|
+
* rspeedy is ESM-only, and its `exports` have no `require` condition, so
|
|
18
|
+
* `require.resolve` can't find its entry point. Its package.json is exported,
|
|
19
|
+
* though: resolve that and read the `import` entry from it.
|
|
20
|
+
*/
|
|
21
|
+
const importFromProject = async (packageName, projectRoot, installHint) => {
|
|
22
|
+
const require = (0, node_module_1.createRequire)(node_path_1.default.join(projectRoot, 'package.json'));
|
|
23
|
+
let packageJsonPath;
|
|
24
|
+
try {
|
|
25
|
+
packageJsonPath = require.resolve(`${packageName}/package.json`);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new Error(`${packageName} is not installed. ${installHint}`);
|
|
29
|
+
}
|
|
30
|
+
const packageJson = JSON.parse((0, node_fs_1.readFileSync)(packageJsonPath, 'utf8'));
|
|
31
|
+
const entry = resolveImportEntry(packageJson);
|
|
32
|
+
if (!entry) {
|
|
33
|
+
throw new Error(`${packageName} has no ESM entry point.`);
|
|
34
|
+
}
|
|
35
|
+
return (await import((0, node_url_1.pathToFileURL)(node_path_1.default.join(node_path_1.default.dirname(packageJsonPath), entry)).href));
|
|
36
|
+
};
|
|
37
|
+
exports.importFromProject = importFromProject;
|
|
38
|
+
const resolveImportEntry = (packageJson) => {
|
|
39
|
+
const exports = packageJson.exports;
|
|
40
|
+
const root = exports && typeof exports === 'object' && '.' in exports
|
|
41
|
+
? exports['.']
|
|
42
|
+
: exports;
|
|
43
|
+
return pick(root ?? null) ?? packageJson.module ?? packageJson.main;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Follows `import` (then `default`) conditions down to a file path.
|
|
47
|
+
*/
|
|
48
|
+
const pick = (value) => {
|
|
49
|
+
if (typeof value === 'string') {
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
if (!value) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
for (const condition of ['import', 'node', 'default']) {
|
|
56
|
+
if (condition in value) {
|
|
57
|
+
const found = pick(value[condition]);
|
|
58
|
+
if (found) {
|
|
59
|
+
return found;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createRspeedy = exports.modeFromOptimization = exports.importQRCodePlugin = exports.importPlugin = exports.importRspeedy = void 0;
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
/**
|
|
10
|
+
* Like the rsbuild plugin, rspeedy is this package's own dependency, so the
|
|
11
|
+
* builders always run the rspeedy version they were released and tested with,
|
|
12
|
+
* and an app needs no build tool in its package.json besides this package
|
|
13
|
+
* (as one from `ng new` needs only `@angular/build`).
|
|
14
|
+
*/
|
|
15
|
+
const importRspeedy = () => import('@lynx-js/rspeedy');
|
|
16
|
+
exports.importRspeedy = importRspeedy;
|
|
17
|
+
/**
|
|
18
|
+
* Unlike rspeedy, the plugin is this package's own dependency, so it is
|
|
19
|
+
* resolved from here rather than from the project: the builders always run
|
|
20
|
+
* the plugin version they were released with.
|
|
21
|
+
*/
|
|
22
|
+
const importPlugin = () => import('@blotch/rsbuild-plugin-angular-lynx');
|
|
23
|
+
exports.importPlugin = importPlugin;
|
|
24
|
+
const importQRCodePlugin = () => import('@lynx-js/qrcode-rsbuild-plugin');
|
|
25
|
+
exports.importQRCodePlugin = importQRCodePlugin;
|
|
26
|
+
/** The name `pluginAngularLynx()` registers itself under. */
|
|
27
|
+
const ANGULAR_PLUGIN_NAME = 'lynx:angular';
|
|
28
|
+
/** The name `pluginQRCode()` registers itself under. */
|
|
29
|
+
const QR_CODE_PLUGIN_NAME = 'lynx:rsbuild:qrcode';
|
|
30
|
+
/**
|
|
31
|
+
* The files rspeedy looks for when no `config` option names one, in its order
|
|
32
|
+
* (`resolveConfigPath` in @lynx-js/rspeedy).
|
|
33
|
+
*/
|
|
34
|
+
const DEFAULT_CONFIG_FILES = [
|
|
35
|
+
'lynx.config.ts',
|
|
36
|
+
'lynx.config.js',
|
|
37
|
+
'lynx.config.mts',
|
|
38
|
+
'lynx.config.mjs',
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* Whether an rspeedy `plugins` list holds the plugin named `name`. The list
|
|
42
|
+
* takes Rsbuild's shapes: plugins, falsy entries (from `cond && plugin()`),
|
|
43
|
+
* promises of either, and nested lists.
|
|
44
|
+
*/
|
|
45
|
+
const hasPlugin = async (plugins, name) => {
|
|
46
|
+
const plugin = await plugins;
|
|
47
|
+
if (Array.isArray(plugin)) {
|
|
48
|
+
for (const item of plugin) {
|
|
49
|
+
if (await hasPlugin(item, name)) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return plugin?.name === name;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* The rspeedy mode for a build target's options. Angular has no mode option:
|
|
59
|
+
* its `optimization` decides, and a build that doesn't optimize scripts runs
|
|
60
|
+
* in dev mode (`ngDevMode` on). The same switch picks rspeedy's mode, so
|
|
61
|
+
* `ng build -c development` (`optimization: false`) is a development build
|
|
62
|
+
* and `ng serve -c production` a production one.
|
|
63
|
+
*/
|
|
64
|
+
const modeFromOptimization = (optimization) => {
|
|
65
|
+
// Angular's own reading of the option (normalizeOptimization in
|
|
66
|
+
// @angular/build): on by default, and in object form `scripts` decides.
|
|
67
|
+
const scripts = typeof optimization === 'object' && optimization !== null
|
|
68
|
+
? !!optimization.scripts
|
|
69
|
+
: optimization !== false;
|
|
70
|
+
return scripts ? 'production' : 'development';
|
|
71
|
+
};
|
|
72
|
+
exports.modeFromOptimization = modeFromOptimization;
|
|
73
|
+
/**
|
|
74
|
+
* Loads lynx.config.ts as `rspeedy build` and `rspeedy dev` would. Unlike
|
|
75
|
+
* them, a project without one builds too, from the defaults
|
|
76
|
+
* {@link createRspeedy} fills in: angular.json holds what the config would
|
|
77
|
+
* repeat, as it does for any Angular app. A `config` option must name a file
|
|
78
|
+
* that exists.
|
|
79
|
+
*/
|
|
80
|
+
const loadProjectConfig = async (rspeedy, projectRoot, config) => {
|
|
81
|
+
if (!config &&
|
|
82
|
+
!DEFAULT_CONFIG_FILES.some((file) => (0, node_fs_1.existsSync)(node_path_1.default.join(projectRoot, file)))) {
|
|
83
|
+
return { content: {} };
|
|
84
|
+
}
|
|
85
|
+
return rspeedy.loadConfig({ cwd: projectRoot, configPath: config });
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Creates an rspeedy instance the way `rspeedy build` and `rspeedy dev` do:
|
|
89
|
+
* load lynx.config.ts, then apply the command-line overrides, which here come
|
|
90
|
+
* from the builder options.
|
|
91
|
+
*
|
|
92
|
+
* What lynx.config.ts leaves out comes from angular.json and the defaults an
|
|
93
|
+
* AngularLynx app needs, so most apps don't need the file at all:
|
|
94
|
+
*
|
|
95
|
+
* - `source.entry` is the build target's `browser` option.
|
|
96
|
+
* - `environments` are `lynx` (the native bundle) and `web` (the browser
|
|
97
|
+
* preview).
|
|
98
|
+
* - The mode follows the build target's `optimization` (see
|
|
99
|
+
* {@link modeFromOptimization}).
|
|
100
|
+
*
|
|
101
|
+
* It also adds plugins, each unless lynx.config.ts lists it itself (to pass
|
|
102
|
+
* it options): the AngularLynx plugin, which compiles Angular, and for the
|
|
103
|
+
* dev server the QR code plugin, which prints the link Lynx Explorer opens.
|
|
104
|
+
* And the Angular CLI context plugin hands the AngularLynx plugin the project
|
|
105
|
+
* and its build options as Angular CLI resolved them (`-c` and command-line
|
|
106
|
+
* overrides included), which it would otherwise read from angular.json under
|
|
107
|
+
* the default configuration.
|
|
108
|
+
*/
|
|
109
|
+
const createRspeedy = async ({ rspeedy, plugin, qrCode, projectRoot, angularCli, config, environment, server, }) => {
|
|
110
|
+
const mode = (0, exports.modeFromOptimization)(angularCli.buildOptions['optimization']);
|
|
111
|
+
// As the rspeedy CLI does before anything else, so plugins (Tailwind,
|
|
112
|
+
// PostCSS) that read NODE_ENV agree with the mode. The CLI derives it from
|
|
113
|
+
// the command; here the mode decides, as `optimization` does for Angular.
|
|
114
|
+
if (!process.env['NODE_ENV']) {
|
|
115
|
+
process.env['NODE_ENV'] = mode;
|
|
116
|
+
}
|
|
117
|
+
const { content, configPath } = await loadProjectConfig(rspeedy, projectRoot, config);
|
|
118
|
+
// As `rspeedy build` does: a persistent build cache must be invalidated
|
|
119
|
+
// when lynx.config.ts itself changes.
|
|
120
|
+
const performance = content['performance'];
|
|
121
|
+
if (configPath) {
|
|
122
|
+
if (performance?.buildCache === true) {
|
|
123
|
+
performance.buildCache = { buildDependencies: [configPath] };
|
|
124
|
+
}
|
|
125
|
+
else if (performance?.buildCache) {
|
|
126
|
+
(performance.buildCache.buildDependencies ??= []).push(configPath);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
content.mode = mode;
|
|
130
|
+
content['environments'] ??= { lynx: {}, web: {} };
|
|
131
|
+
const browser = angularCli.buildOptions['browser'];
|
|
132
|
+
const source = (content['source'] ??= {});
|
|
133
|
+
if (source.entry === undefined && typeof browser === 'string') {
|
|
134
|
+
source.entry = node_path_1.default.join(angularCli.workspaceRoot, browser);
|
|
135
|
+
}
|
|
136
|
+
if (server?.port !== undefined || server?.host !== undefined) {
|
|
137
|
+
content['server'] = {
|
|
138
|
+
...content['server'],
|
|
139
|
+
...(server.port !== undefined && { port: server.port }),
|
|
140
|
+
...(server.host !== undefined && { host: server.host }),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const plugins = Array.isArray(content['plugins']) ? content['plugins'] : [];
|
|
144
|
+
content['plugins'] = [
|
|
145
|
+
plugin.pluginAngularCliContext(angularCli),
|
|
146
|
+
...plugins,
|
|
147
|
+
...((await hasPlugin(plugins, ANGULAR_PLUGIN_NAME))
|
|
148
|
+
? []
|
|
149
|
+
: [plugin.pluginAngularLynx()]),
|
|
150
|
+
// `fullscreen` opens the app in Lynx Explorer without its navigation
|
|
151
|
+
// bar, as the rspeedy project template does.
|
|
152
|
+
...(qrCode && !(await hasPlugin(plugins, QR_CODE_PLUGIN_NAME))
|
|
153
|
+
? [qrCode.pluginQRCode({ fullscreen: true })]
|
|
154
|
+
: []),
|
|
155
|
+
];
|
|
156
|
+
return rspeedy.createRspeedy({
|
|
157
|
+
cwd: projectRoot,
|
|
158
|
+
rspeedyConfig: content,
|
|
159
|
+
environment: environment ?? [],
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
exports.createRspeedy = createRspeedy;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runUnitTest = exports.ASSET_LOADERS = exports.ANGULAR_BUILD_OPTIONS = exports.hasTestingLibrary = void 0;
|
|
7
|
+
const architect_1 = require("@angular-devkit/architect");
|
|
8
|
+
const node_module_1 = require("node:module");
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const project_import_1 = require("../project-import");
|
|
11
|
+
/**
|
|
12
|
+
* @angular/build comes from the project, like Angular's own builders: its
|
|
13
|
+
* version must match the project's Angular version, which `ng new` installs
|
|
14
|
+
* it for.
|
|
15
|
+
*/
|
|
16
|
+
const importAngularBuild = (projectRoot) => (0, project_import_1.importFromProject)('@angular/build', projectRoot, 'Run `npm install --save-dev @angular/build` (`ng new` adds it).');
|
|
17
|
+
/**
|
|
18
|
+
* Whether the project has @blotch/angular-lynx-testing-library. Its package.json
|
|
19
|
+
* isn't in its `exports` in every version, and Node then refuses to resolve
|
|
20
|
+
* it, but only when the package is there.
|
|
21
|
+
*/
|
|
22
|
+
const hasTestingLibrary = (projectRoot) => {
|
|
23
|
+
try {
|
|
24
|
+
(0, node_module_1.createRequire)(node_path_1.default.join(projectRoot, 'package.json')).resolve('@blotch/angular-lynx-testing-library/package.json');
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return (error.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED');
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
exports.hasTestingLibrary = hasTestingLibrary;
|
|
32
|
+
const defaultDeps = {
|
|
33
|
+
importAngularBuild,
|
|
34
|
+
hasTestingLibrary: exports.hasTestingLibrary,
|
|
35
|
+
};
|
|
36
|
+
const LYNX_APPLICATION_BUILDER = '@blotch/angular-lynx-build:application';
|
|
37
|
+
const ANGULAR_APPLICATION_BUILDER = '@angular/build:application';
|
|
38
|
+
/**
|
|
39
|
+
* The options of the Lynx application builder that are Angular's application
|
|
40
|
+
* builder's too, and mean the same. The rest (native app settings, rspeedy's
|
|
41
|
+
* `config` and `environment`) are Lynx-only; Angular's schema rejects them.
|
|
42
|
+
*/
|
|
43
|
+
exports.ANGULAR_BUILD_OPTIONS = [
|
|
44
|
+
'browser',
|
|
45
|
+
'tsConfig',
|
|
46
|
+
'polyfills',
|
|
47
|
+
'styles',
|
|
48
|
+
'assets',
|
|
49
|
+
'scripts',
|
|
50
|
+
'inlineStyleLanguage',
|
|
51
|
+
'fileReplacements',
|
|
52
|
+
'aot',
|
|
53
|
+
'optimization',
|
|
54
|
+
'sourceMap',
|
|
55
|
+
'outputHashing',
|
|
56
|
+
'i18nMissingTranslation',
|
|
57
|
+
'budgets',
|
|
58
|
+
'extractLicenses',
|
|
59
|
+
'outputPath',
|
|
60
|
+
'index',
|
|
61
|
+
'watch',
|
|
62
|
+
];
|
|
63
|
+
/**
|
|
64
|
+
* How Angular's test build loads imported assets (`import logo from
|
|
65
|
+
* './logo.png'`): as files, so the import is the file's URL, as rspeedy
|
|
66
|
+
* makes it in the app. Angular's build has no loader for them by default and
|
|
67
|
+
* fails on the import.
|
|
68
|
+
*/
|
|
69
|
+
exports.ASSET_LOADERS = Object.fromEntries([
|
|
70
|
+
// Images
|
|
71
|
+
'png',
|
|
72
|
+
'jpg',
|
|
73
|
+
'jpeg',
|
|
74
|
+
'gif',
|
|
75
|
+
'webp',
|
|
76
|
+
'avif',
|
|
77
|
+
'bmp',
|
|
78
|
+
'ico',
|
|
79
|
+
'svg',
|
|
80
|
+
// Fonts
|
|
81
|
+
'woff',
|
|
82
|
+
'woff2',
|
|
83
|
+
'ttf',
|
|
84
|
+
'otf',
|
|
85
|
+
'eot',
|
|
86
|
+
// Audio and video
|
|
87
|
+
'mp4',
|
|
88
|
+
'webm',
|
|
89
|
+
'mov',
|
|
90
|
+
'mp3',
|
|
91
|
+
'wav',
|
|
92
|
+
'ogg',
|
|
93
|
+
'm4a',
|
|
94
|
+
'aac',
|
|
95
|
+
].map((extension) => [`.${extension}`, 'file']));
|
|
96
|
+
/**
|
|
97
|
+
* The builder context Angular's unit-test builder gets. It reads the
|
|
98
|
+
* compiler options (tsconfig, polyfills, styles, file replacements) from the
|
|
99
|
+
* build target, and only knows Angular's own application builder: for any
|
|
100
|
+
* other it warns that "unexpected behavior or build failures may occur" and
|
|
101
|
+
* validates the options against that builder's schema. A Lynx build target
|
|
102
|
+
* therefore reaches it as an Angular one, with the options both builders
|
|
103
|
+
* share, which Angular's schema validates and completes with its defaults,
|
|
104
|
+
* the asset loaders rspeedy's defaults correspond to, and `lynxPolyfills`
|
|
105
|
+
* after the app's own polyfills.
|
|
106
|
+
*
|
|
107
|
+
* The Lynx setup goes in as polyfills rather than `setupFiles` because
|
|
108
|
+
* Angular compiles setup files with the tests, and TypeScript rejects one
|
|
109
|
+
* outside the tsconfig's folder when the tsconfig sets an `outDir`, as
|
|
110
|
+
* `ng new`'s tsconfig.spec.json does: a package in the workspace's
|
|
111
|
+
* node_modules is outside a project in `projects/`. Polyfills are bundled as
|
|
112
|
+
* modules, and run first.
|
|
113
|
+
*/
|
|
114
|
+
const asAngularBuildTargets = (context, lynxPolyfills) => {
|
|
115
|
+
const isLynxApplication = async (target) => (await context.getBuilderNameForTarget(target)) ===
|
|
116
|
+
LYNX_APPLICATION_BUILDER;
|
|
117
|
+
const overrides = {
|
|
118
|
+
getBuilderNameForTarget: async (target) => (await isLynxApplication(target))
|
|
119
|
+
? ANGULAR_APPLICATION_BUILDER
|
|
120
|
+
: context.getBuilderNameForTarget(target),
|
|
121
|
+
getTargetOptions: async (target) => {
|
|
122
|
+
const options = await context.getTargetOptions(target);
|
|
123
|
+
if (!(await isLynxApplication(target))) {
|
|
124
|
+
return options;
|
|
125
|
+
}
|
|
126
|
+
const polyfills = options['polyfills'];
|
|
127
|
+
return {
|
|
128
|
+
...Object.fromEntries(Object.entries(options).filter(([key]) => exports.ANGULAR_BUILD_OPTIONS.includes(key))),
|
|
129
|
+
loader: exports.ASSET_LOADERS,
|
|
130
|
+
polyfills: [
|
|
131
|
+
...(Array.isArray(polyfills)
|
|
132
|
+
? polyfills
|
|
133
|
+
: typeof polyfills === 'string'
|
|
134
|
+
? [polyfills]
|
|
135
|
+
: []),
|
|
136
|
+
...lynxPolyfills,
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
return new Proxy(context, {
|
|
142
|
+
get: (target, property) => property in overrides
|
|
143
|
+
? overrides[property]
|
|
144
|
+
: Reflect.get(target, property),
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* The options as Angular CLI hands them to Angular's own builders. For a
|
|
149
|
+
* builder outside `@angular/build`, it sets every array option the user left
|
|
150
|
+
* out to `[]` (and nested ones too, e.g. `coverageWatermarks.lines`), which
|
|
151
|
+
* Angular's unit-test builder reads as set: `reporters: []` runs Vitest with
|
|
152
|
+
* no reporter, so `ng test` printed nothing. An empty list never means more
|
|
153
|
+
* than an absent one here, so empty lists are dropped.
|
|
154
|
+
*/
|
|
155
|
+
const withoutEmptyLists = (value) => {
|
|
156
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
return Object.fromEntries(Object.entries(value)
|
|
160
|
+
.filter(([, item]) => !(Array.isArray(item) && item.length === 0))
|
|
161
|
+
.map(([key, item]) => [key, withoutEmptyLists(item)]));
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* `ng test`: Angular's unit-test builder (`@angular/build:unit-test`), so the
|
|
165
|
+
* options, test discovery (`*.spec.ts` and `*.test.ts`), TestBed, watch mode
|
|
166
|
+
* and coverage are the ones Angular developers know, and the tests compile
|
|
167
|
+
* ahead of time like the app. Two modules load first: one defines the
|
|
168
|
+
* compile-time flags the AngularLynx runtime reads, and, when the project has
|
|
169
|
+
* @blotch/angular-lynx-testing-library, its setup prepares the Lynx
|
|
170
|
+
* environment its `render()` needs.
|
|
171
|
+
*
|
|
172
|
+
* Tests render through Angular's DOM renderer under TestBed and through the
|
|
173
|
+
* Lynx renderer under the testing library's `render()`. Either way the test
|
|
174
|
+
* compile checks templates strictly, as the Angular Language Service does:
|
|
175
|
+
* a component that uses Lynx elements imports LYNX_ELEMENTS (as generated
|
|
176
|
+
* ones do). The app build is laxer, because it adds CUSTOM_ELEMENTS_SCHEMA to
|
|
177
|
+
* every component, which Angular's test build has no hook for.
|
|
178
|
+
*/
|
|
179
|
+
const runUnitTest = async function* (options, context, deps = defaultDeps) {
|
|
180
|
+
const projectName = context.target?.project;
|
|
181
|
+
if (!projectName) {
|
|
182
|
+
yield { success: false, error: 'The builder needs a project target.' };
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const metadata = await context.getProjectMetadata(projectName);
|
|
186
|
+
const projectRoot = node_path_1.default.join(context.workspaceRoot, metadata['root'] ?? '');
|
|
187
|
+
let angularBuild;
|
|
188
|
+
try {
|
|
189
|
+
angularBuild = await deps.importAngularBuild(projectRoot);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
yield {
|
|
193
|
+
success: false,
|
|
194
|
+
error: error instanceof Error ? error.message : String(error),
|
|
195
|
+
};
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const lynxPolyfills = [
|
|
199
|
+
'@blotch/angular-lynx-build/unit-test/lynx-globals',
|
|
200
|
+
...(deps.hasTestingLibrary(projectRoot)
|
|
201
|
+
? ['@blotch/angular-lynx-testing-library/setup']
|
|
202
|
+
: []),
|
|
203
|
+
];
|
|
204
|
+
yield* angularBuild.executeUnitTestBuilder(withoutEmptyLists(options), asAngularBuildTargets(context, lynxPolyfills));
|
|
205
|
+
};
|
|
206
|
+
exports.runUnitTest = runUnitTest;
|
|
207
|
+
exports.default = (0, architect_1.createBuilder)((options, context) => (0, exports.runUnitTest)(options, context));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// The first module `ng test` loads, as a polyfill: before Angular's TestBed,
|
|
2
|
+
// the testing library's setup, and any test that imports @blotch/angular-lynx.
|
|
3
|
+
//
|
|
4
|
+
// The runtime reads these compile-time flags, which the rsbuild plugin
|
|
5
|
+
// defines for each thread when it bundles the app. Angular's unit-test build
|
|
6
|
+
// leaves packages unbundled, so a `define` couldn't reach the runtime's code;
|
|
7
|
+
// globals do. Tests run the way the native main thread does: in development,
|
|
8
|
+
// not on the web, without profiling or SSR.
|
|
9
|
+
//
|
|
10
|
+
// Plain JavaScript, so it isn't compiled to CommonJS with the builders:
|
|
11
|
+
// Angular bundles polyfills as ES modules.
|
|
12
|
+
Object.assign(globalThis, {
|
|
13
|
+
__DEV__: true,
|
|
14
|
+
__MAIN_THREAD__: true,
|
|
15
|
+
__WEB__: false,
|
|
16
|
+
__PROFILE__: false,
|
|
17
|
+
__ENABLE_SSR__: false,
|
|
18
|
+
});
|