@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.
@@ -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,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -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,3 @@
1
+ // lynx-globals.mjs only sets globals: `ng test` loads it for its side
2
+ // effects, as a polyfill. Declared so type checkers (and attw) see the module.
3
+ export {};
@@ -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
+ });
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });