@ng-native/nx 0.2.0 → 0.4.0

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,336 @@
1
+ /**
2
+ * `nx g @ng-native/nx:library packages/ui`: a library of native components, tested the way an
3
+ * Angular Native app is.
4
+ *
5
+ * The library itself is the workspace's own kind: `@nx/angular:library` where the workspace has
6
+ * `@nx/angular`, and `@nx/js:library` in the TypeScript preset, which cannot take `@nx/angular`
7
+ * because Angular does not support TypeScript project references. Either is run without its
8
+ * tests, which would be Analog on jsdom or a plain Vitest, neither of which can render a native
9
+ * component. In their place this writes the app's `vitest.config.mts`, a `test` target that runs
10
+ * it once, a `tsconfig.spec.json` for the tests, and a native component with a test for the fake
11
+ * Fabric in place of the one the generator wrote.
12
+ */
13
+ const { readFileSync } = require('node:fs');
14
+ const nodePath = require('node:path');
15
+ const path = nodePath.posix;
16
+ const {
17
+ addDependenciesToPackageJson,
18
+ ensurePackage,
19
+ formatFiles,
20
+ getProjects,
21
+ installPackagesTask,
22
+ joinPathFragments,
23
+ names,
24
+ readNxJson,
25
+ readJson,
26
+ writeJson,
27
+ updateJson,
28
+ updateProjectConfiguration,
29
+ } = require('@nx/devkit');
30
+ const native = require('../native-app.cjs');
31
+ const { workspaceNxVersion } = require('../init/index.cjs');
32
+ const { hasPathAliases } = require('../application/index.cjs');
33
+ const { usesWorkspaces } = require('../application/workspaces.cjs');
34
+ const { asSaved } = require('../save-exact.cjs');
35
+ const { writeComponent } = require('../component/index.cjs');
36
+
37
+ /**
38
+ * The defaults a generator's schema gives its options, from the copy installed at `root`.
39
+ *
40
+ * @param {string} root
41
+ * @param {string} pkg
42
+ * @param {string} generator
43
+ * @returns {Record<string, unknown>}
44
+ */
45
+ function schemaDefaults(root, pkg, generator) {
46
+ let manifest;
47
+ try {
48
+ manifest = require.resolve(`${pkg}/package.json`, { paths: [root] });
49
+ } catch {
50
+ return {};
51
+ }
52
+ const read = (file) =>
53
+ JSON.parse(readFileSync(nodePath.join(nodePath.dirname(manifest), file), 'utf8'));
54
+ const entry = read(read('package.json').generators).generators[generator];
55
+ const { properties = {} } = read(entry.schema);
56
+ return Object.fromEntries(
57
+ Object.entries(properties)
58
+ .filter(([, property]) => 'default' in property)
59
+ .map(([name, property]) => [name, property.default]),
60
+ );
61
+ }
62
+
63
+ /**
64
+ * The options `nx g <pkg>:<generator>` starts from: its schema's defaults, then the workspace's
65
+ * own in `nx.json`. A generator called from another gets neither, and `@nx/angular:library` called
66
+ * bare writes a library with strict type checking off.
67
+ *
68
+ * @param {import('@nx/devkit').Tree} tree
69
+ * @param {string} pkg
70
+ * @param {string} generator
71
+ */
72
+ function generatorDefaults(tree, pkg, generator, root = tree.root) {
73
+ const workspace = readNxJson(tree)?.generators ?? {};
74
+ return {
75
+ ...schemaDefaults(root, pkg, generator),
76
+ ...workspace[pkg]?.[generator],
77
+ ...workspace[`${pkg}:${generator}`],
78
+ };
79
+ }
80
+
81
+ /** The library generators this one builds on, at the workspace's Nx. Replaced in tests. */
82
+ const bases = {
83
+ /** @param {import('@nx/devkit').Tree} tree @param {object} options */
84
+ async angular(tree, options) {
85
+ // Only run where the workspace lists `@nx/angular`, whose root export has no generators.
86
+ const { libraryGenerator } = ensurePackage('@nx/angular/generators', workspaceNxVersion(tree));
87
+ return libraryGenerator(tree, {
88
+ ...generatorDefaults(tree, '@nx/angular', 'library'),
89
+ ...options,
90
+ });
91
+ },
92
+ /** @param {import('@nx/devkit').Tree} tree @param {object} options */
93
+ async js(tree, options) {
94
+ const { libraryGenerator } = ensurePackage('@nx/js', workspaceNxVersion(tree));
95
+ return libraryGenerator(tree, { ...generatorDefaults(tree, '@nx/js', 'library'), ...options });
96
+ },
97
+ };
98
+
99
+ /** What the component and its test import. */
100
+ const imports = {
101
+ dependencies: {
102
+ '@angular/core': native.dependencies['@angular/core'],
103
+ '@ng-native/components': native.dependencies['@ng-native/components'],
104
+ },
105
+ devDependencies: {
106
+ '@ng-native/testing': native.devDependencies['@ng-native/testing'],
107
+ vitest: native.devDependencies.vitest,
108
+ },
109
+ };
110
+
111
+ /** What `nx typecheck` runs for a library in the TypeScript preset: `ngc`, which reads templates. */
112
+ const typecheckDependencies = {
113
+ '@angular/compiler-cli': native.devDependencies['@angular/compiler-cli'],
114
+ };
115
+
116
+ function projectAt(tree, root) {
117
+ for (const [name, project] of getProjects(tree))
118
+ if (project.root === root) return { name, project };
119
+ throw new Error(`No project was generated in ${root}.`);
120
+ }
121
+
122
+ /**
123
+ * The native component and its test, where the generator put its own component: in a folder of
124
+ * its own under `src/lib` from `@nx/angular`, with an external template and stylesheet this
125
+ * leaves out, or as `src/lib/<name>.ts` from `@nx/js`. `src/index.ts` already exports it.
126
+ */
127
+ function replaceComponent(tree, project, file) {
128
+ const lib = joinPathFragments(project.root, 'src', 'lib');
129
+ const folder = tree.exists(joinPathFragments(lib, file, `${file}.ts`))
130
+ ? joinPathFragments(lib, file)
131
+ : lib;
132
+ for (const extension of ['html', 'css', 'spec.ts']) {
133
+ const own = joinPathFragments(folder, `${file}.${extension}`);
134
+ if (tree.exists(own)) tree.delete(own);
135
+ }
136
+ writeComponent(tree, folder, file, project.prefix);
137
+ }
138
+
139
+ /**
140
+ * A `tsconfig.spec.json` like the one each generator writes with its tests, including the tests
141
+ * and the Vitest config. The tests import their component as `./ui.ts`, as the template's do.
142
+ */
143
+ function writeSpecConfig(tree, root, angular) {
144
+ const include = ['vitest.config.mts', 'src/**/*.test.ts'];
145
+ const spec = angular
146
+ ? {
147
+ extends: './tsconfig.json',
148
+ compilerOptions: { noEmit: true, allowImportingTsExtensions: true },
149
+ include,
150
+ }
151
+ : {
152
+ // A project reference of the TypeScript preset's, which emits declarations only.
153
+ extends: readJson(tree, joinPathFragments(root, 'tsconfig.lib.json')).extends,
154
+ compilerOptions: { outDir: './out-tsc/vitest', allowImportingTsExtensions: true },
155
+ include,
156
+ references: [{ path: './tsconfig.lib.json' }],
157
+ };
158
+ tree.write(joinPathFragments(root, 'tsconfig.spec.json'), JSON.stringify(spec, null, 2) + '\n');
159
+ updateJson(tree, joinPathFragments(root, 'tsconfig.json'), (config) => {
160
+ const references = config.references ?? [];
161
+ if (!references.some((reference) => reference.path === './tsconfig.spec.json')) {
162
+ references.push({ path: './tsconfig.spec.json' });
163
+ }
164
+ return { ...config, references };
165
+ });
166
+ updateJson(tree, joinPathFragments(root, 'tsconfig.lib.json'), (config) => {
167
+ const exclude = config.exclude ?? [];
168
+ if (!exclude.includes('src/**/*.test.ts')) exclude.push('src/**/*.test.ts');
169
+ return { ...config, exclude };
170
+ });
171
+ }
172
+
173
+ /**
174
+ * Lets the library's source import as its test, the template and the documentation do, with
175
+ * `.ts`: `allowImportingTsExtensions` in `tsconfig.lib.json`, and `src/index.ts` written that way
176
+ * in place of the `.js` `@nx/js` gives it.
177
+ *
178
+ * TypeScript takes the option only where no JavaScript is emitted, which is the TypeScript
179
+ * preset's own setting (`emitDeclarationOnly`, which `@nx/js` writes into `tsconfig.lib.json`). A
180
+ * workspace that has turned that off keeps `.js`.
181
+ */
182
+ function importWithTsExtensions(tree, root) {
183
+ const config = joinPathFragments(root, 'tsconfig.lib.json');
184
+ if (!emitsDeclarationsOnly(tree, config)) return;
185
+ updateJson(tree, config, (lib) => ({
186
+ ...lib,
187
+ compilerOptions: { ...lib.compilerOptions, allowImportingTsExtensions: true },
188
+ }));
189
+ const index = joinPathFragments(root, 'src', 'index.ts');
190
+ if (!tree.exists(index)) return;
191
+ const source = tree.read(index, 'utf-8');
192
+ tree.write(index, source.replace(/(from\s+['"]\.{1,2}\/[^'"]*)\.js(['"])/g, '$1.ts$2'));
193
+ }
194
+
195
+ /** The config `nx typecheck` gives `ngc` for a library, beside the one it is built with. */
196
+ const TYPECHECK_CONFIG = 'tsconfig.typecheck.json';
197
+
198
+ /**
199
+ * A config `ngc` can check the library's templates with, as an app's `typecheck` does. `@nx/js`
200
+ * infers a `typecheck` that runs `tsc`, which reads no template: a misspelled input passes.
201
+ *
202
+ * `tsconfig.lib.json` itself will not do: `ngc` refuses `emitDeclarationOnly`, and a project that
203
+ * is `composite` has to emit. The Angular options are the generated app's: a native view has no
204
+ * DOM to check a host binding or an event against.
205
+ */
206
+ function writeTypecheckConfig(tree, root) {
207
+ writeJson(tree, joinPathFragments(root, TYPECHECK_CONFIG), {
208
+ extends: './tsconfig.lib.json',
209
+ compilerOptions: { noEmit: true, composite: false, emitDeclarationOnly: false },
210
+ angularCompilerOptions: {
211
+ strictTemplates: true,
212
+ typeCheckHostBindings: false,
213
+ strictDomEventTypes: false,
214
+ },
215
+ });
216
+ }
217
+
218
+ /** `nx typecheck <library>`: `ngc`, in place of the `tsc` `@nx/js` infers. */
219
+ function typecheckTarget(root) {
220
+ return {
221
+ executor: 'nx:run-commands',
222
+ options: { cwd: root, command: `ngc -p ${TYPECHECK_CONFIG}` },
223
+ cache: true,
224
+ inputs: ['default', '^production'],
225
+ };
226
+ }
227
+
228
+ /** Whether a tsconfig emits declarations and no JavaScript, once what it extends is merged in. */
229
+ function emitsDeclarationsOnly(tree, file) {
230
+ const { noEmit, emitDeclarationOnly } = emitOptions(tree, file, new Set());
231
+ return noEmit === true || emitDeclarationOnly === true;
232
+ }
233
+
234
+ /**
235
+ * `noEmit` and `emitDeclarationOnly` as the compiler reads them: each from the nearest config that
236
+ * sets it, and a later entry of an `extends` list over an earlier one.
237
+ */
238
+ function emitOptions(tree, file, seen) {
239
+ if (seen.has(file) || !tree.exists(file)) return {};
240
+ // The configs on the way here, to stop at a cycle. Not every config read: two entries of a list
241
+ // may share an ancestor, and each is read whole.
242
+ const above = new Set(seen).add(file);
243
+ const { compilerOptions = {}, extends: base = [] } = readJson(tree, file);
244
+ // ponytail: a config that comes from a package is not followed, so a library under one keeps
245
+ // `.js`. Resolve the specifier from the config's directory if a workspace needs it.
246
+ const inherited = [base]
247
+ .flat()
248
+ .filter((from) => typeof from === 'string' && from.startsWith('.'))
249
+ .map((from) => joinPathFragments(path.dirname(file), from))
250
+ .map((from) => emitOptions(tree, from.endsWith('.json') ? from : `${from}.json`, above));
251
+ const { noEmit, emitDeclarationOnly } = compilerOptions;
252
+ return Object.assign(
253
+ {},
254
+ ...inherited,
255
+ noEmit === undefined ? {} : { noEmit },
256
+ emitDeclarationOnly === undefined ? {} : { emitDeclarationOnly },
257
+ );
258
+ }
259
+
260
+ /**
261
+ * The packages the component and its test import: in the library's own `package.json` when it is
262
+ * a workspace package, which pnpm links from, and at the root otherwise. A version already there
263
+ * is left alone.
264
+ */
265
+ async function addImports(tree, root, alsoDev = {}) {
266
+ const devDependencies = { ...imports.devDependencies, ...alsoDev };
267
+ const manifest = joinPathFragments(root, 'package.json');
268
+ const rootManifest = readJson(tree, 'package.json');
269
+ if (tree.exists(manifest)) {
270
+ const own = async (wanted) => asSaved(tree, native.reuseRootRanges(wanted, rootManifest));
271
+ return addDependenciesToPackageJson(
272
+ tree,
273
+ await own(imports.dependencies),
274
+ await own(devDependencies),
275
+ manifest,
276
+ true,
277
+ );
278
+ }
279
+ const existing = Object.keys({ ...rootManifest.dependencies, ...rootManifest.devDependencies });
280
+ return addDependenciesToPackageJson(
281
+ tree,
282
+ await asSaved(tree, imports.dependencies, existing),
283
+ await asSaved(tree, devDependencies, existing),
284
+ 'package.json',
285
+ true,
286
+ );
287
+ }
288
+
289
+ /**
290
+ * @param {import('@nx/devkit').Tree} tree
291
+ * @param {{ directory: string, name?: string, tags?: string, skipInstall?: boolean, skipFormat?: boolean }} options
292
+ */
293
+ async function library(tree, options) {
294
+ const directory = path.normalize(options.directory).replace(/\/$/, '');
295
+ const { dependencies, devDependencies } = readJson(tree, 'package.json');
296
+ const angular = Boolean({ ...dependencies, ...devDependencies }['@nx/angular']);
297
+ const shared = { directory, name: options.name, tags: options.tags, unitTestRunner: 'none' };
298
+ if (angular) await bases.angular(tree, { ...shared, skipFormat: true });
299
+ else await bases.js(tree, { ...shared, bundler: 'none', skipFormat: true });
300
+
301
+ const { name, project } = projectAt(tree, directory);
302
+ const file = names(options.name ?? path.basename(directory)).fileName;
303
+ replaceComponent(tree, project, file);
304
+
305
+ const aliases = hasPathAliases(tree, usesWorkspaces(tree));
306
+ tree.write(joinPathFragments(directory, 'vitest.config.mts'), native.vitestConfig(aliases));
307
+ writeSpecConfig(tree, directory, angular);
308
+ if (!angular) importWithTsExtensions(tree, directory);
309
+ if (!angular) writeTypecheckConfig(tree, directory);
310
+ updateProjectConfiguration(tree, name, {
311
+ ...project,
312
+ targets: {
313
+ ...project.targets,
314
+ ...(angular ? {} : { typecheck: typecheckTarget(directory) }),
315
+ test: {
316
+ executor: 'nx:run-commands',
317
+ options: { cwd: directory, command: 'vitest run' },
318
+ cache: true,
319
+ inputs: ['default', '^production'],
320
+ },
321
+ },
322
+ });
323
+
324
+ await addImports(tree, directory, angular ? {} : typecheckDependencies);
325
+ // `nxViteTsPaths()`, which the Vitest config uses to reach the workspace's other libraries.
326
+ if (aliases) {
327
+ const vite = { '@nx/vite': workspaceNxVersion(tree) };
328
+ addDependenciesToPackageJson(tree, {}, vite, 'package.json', true);
329
+ }
330
+
331
+ if (!options.skipFormat) await formatFiles(tree);
332
+ if (options.skipInstall) return () => {};
333
+ return () => installPackagesTask(tree, true);
334
+ }
335
+
336
+ module.exports = { library, bases, generatorDefaults };
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "https://json-schema.org/schema",
3
+ "$id": "NgNativeNxLibrary",
4
+ "title": "Angular Native library",
5
+ "description": "A library of native components, whose tests render on the fake Fabric as an app's do.",
6
+ "type": "object",
7
+ "properties": {
8
+ "directory": {
9
+ "type": "string",
10
+ "description": "Where the library goes, relative to the workspace root: packages/ui.",
11
+ "$default": { "$source": "argv", "index": 0 },
12
+ "x-prompt": "Which directory should the library be created in?"
13
+ },
14
+ "name": {
15
+ "type": "string",
16
+ "description": "The project name. Defaults to the directory's last segment.",
17
+ "pattern": "^[a-z][a-z0-9-]*$"
18
+ },
19
+ "tags": {
20
+ "type": "string",
21
+ "description": "Comma-separated tags for the project."
22
+ },
23
+ "skipInstall": {
24
+ "type": "boolean",
25
+ "description": "Write the files without installing.",
26
+ "default": false
27
+ },
28
+ "skipFormat": {
29
+ "type": "boolean",
30
+ "description": "Leave the generated files unformatted.",
31
+ "default": false,
32
+ "x-priority": "internal"
33
+ }
34
+ },
35
+ "required": ["directory"]
36
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The migrations `nx migrate` runs, from `@ng-native/migrate`, where each one is written once for
3
+ * `nx migrate`, `ng update` and `npx @ng-native/migrate` alike.
4
+ */
5
+ module.exports = require('@ng-native/migrate').forNx();
@@ -0,0 +1,24 @@
1
+ {
2
+ "generators": {
3
+ "sync-app-versions": {
4
+ "version": "0.4.0",
5
+ "description": "Move the @ng-native packages each project's own package.json lists to the new version.",
6
+ "implementation": "./migrations/run.cjs#sync-app-versions"
7
+ },
8
+ "split-store-and-player": {
9
+ "version": "0.3.0",
10
+ "description": "Import Storage, SecureStorage, audioPlayer and videoPlayer from the entry points that now hold them.",
11
+ "implementation": "./migrations/run.cjs#split-store-and-player"
12
+ },
13
+ "tracking-available-getter": {
14
+ "version": "0.3.0",
15
+ "description": "Read Tracking's available as a property rather than calling it.",
16
+ "implementation": "./migrations/run.cjs#tracking-available-getter"
17
+ },
18
+ "device-react-native-import": {
19
+ "version": "0.3.0",
20
+ "description": "Note each import of reactNative or ReactNative from @ng-native/device, which no longer exports them.",
21
+ "implementation": "./migrations/run.cjs#device-react-native-import"
22
+ }
23
+ }
24
+ }