@ng-native/nx 0.17.0 → 0.18.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.
@@ -42,9 +42,14 @@ function names(tree, options) {
42
42
  // A workspace package is named for the workspace's scope, as Nx names its own.
43
43
  const projectName = workspaces && scope ? `${scope}/${name}` : name;
44
44
  const bundleIdentifier = options.bundleIdentifier ?? native.bundleIdentifier(name, scope);
45
- return { directory, name, projectName, workspaces, bundleIdentifier };
45
+ const lint = options.linter === 'eslint';
46
+ return { directory, name, projectName, workspaces, bundleIdentifier, lint };
46
47
  }
47
48
 
49
+ /** The app's dev dependencies: with `--linter=eslint`, ESLint and the plugin as well. */
50
+ const devDependencies = (lint) =>
51
+ lint ? { ...native.devDependencies, ...native.lintDevDependencies } : native.devDependencies;
52
+
48
53
  /**
49
54
  * Whether the workspace's `tsconfig.base.json` is where its libraries' path aliases live, or will.
50
55
  * With package-manager workspaces, only when it has a `paths` list, as the `angular-monorepo`
@@ -125,7 +130,7 @@ function metroPort(tree) {
125
130
  return port;
126
131
  }
127
132
 
128
- function targets(directory, port, oneCopy) {
133
+ function targets(directory, port, oneCopy, lint) {
129
134
  const run = (command) => ({ executor: 'nx:run-commands', options: { cwd: directory, command } });
130
135
  const own = port === DEFAULT_PORT ? '' : ` --port ${port}`;
131
136
  // `expo run:ios` and `expo run:android` build the port into the app, so a later app's has to be
@@ -143,6 +148,7 @@ function targets(directory, port, oneCopy) {
143
148
  outputs: ['{projectRoot}/.angular-native'],
144
149
  },
145
150
  test: { ...run('vitest run'), cache: true, inputs: ['default', '^production'] },
151
+ ...(lint && { lint: { ...run('eslint .'), cache: true, inputs: ['default', '^production'] } }),
146
152
  start: { ...run(`expo start${own}`), continuous: true },
147
153
  serve: { ...run(`expo start${own}`), continuous: true },
148
154
  ...runs,
@@ -150,13 +156,14 @@ function targets(directory, port, oneCopy) {
150
156
  }
151
157
 
152
158
  /** The Commands section of the app's `AGENTS.md`, as this workspace runs them. */
153
- function commands(name) {
159
+ function commands(name, lint) {
154
160
  const rows = [
155
161
  [`nx start ${name}`, 'Metro; press i or a for a simulator, or scan the QR code with Expo Go'],
156
162
  [`nx run ${name}:run-ios`, 'the iOS simulator'],
157
163
  [`nx run ${name}:run-android`, 'the Android emulator'],
158
164
  [`nx test ${name}`, 'Vitest, in Node against a fake native layer: no simulator needed'],
159
165
  [`nx typecheck ${name}`, 'TypeScript, without emitting'],
166
+ ...(lint ? [[`nx lint ${name}`, 'ESLint: what compiles but does nothing on a device']] : []),
160
167
  ];
161
168
  const width = Math.max(...rows.map(([command]) => command.length)) + 2;
162
169
  return [
@@ -165,7 +172,10 @@ function commands(name) {
165
172
  '```',
166
173
  '',
167
174
  '`src/main.ts` mounts the root component, `src/app/app.ts`. ' +
168
- `Run \`nx test ${name}\` and \`nx typecheck ${name}\` after a change; both are fast.`,
175
+ (lint
176
+ ? `Run \`nx test ${name}\`, \`nx typecheck ${name}\` and \`nx lint ${name}\` after a change; ` +
177
+ 'all three are fast.'
178
+ : `Run \`nx test ${name}\` and \`nx typecheck ${name}\` after a change; both are fast.`),
169
179
  ].join('\n');
170
180
  }
171
181
 
@@ -186,14 +196,15 @@ function listed(manifest) {
186
196
  return Object.keys({ ...manifest.dependencies, ...manifest.devDependencies });
187
197
  }
188
198
 
189
- async function writeFiles(tree, { directory, projectName, workspaces, bundleIdentifier }) {
199
+ async function writeFiles(tree, { directory, projectName, workspaces, bundleIdentifier, lint }) {
190
200
  const file = (name, content) => tree.write(joinPathFragments(directory, name), content);
191
201
  for (const name of native.SOURCE_FILES) file(name, native.sourceFile(name));
192
202
  file('app.json', native.appJson(projectName, bundleIdentifier));
193
- file('AGENTS.md', native.agentsFile(commands(projectName)));
203
+ file('AGENTS.md', native.agentsFile(commands(projectName, lint)));
194
204
  file('CLAUDE.md', '@AGENTS.md\n');
195
205
  file('metro.config.js', native.METRO_CONFIG);
196
206
  file('.gitignore', native.GITIGNORE);
207
+ if (lint) file('eslint.config.mjs', native.ESLINT_CONFIG);
197
208
 
198
209
  const base = hasPathAliases(tree, workspaces);
199
210
  const workspaceBase = base ? `${offsetFromRoot(directory)}tsconfig.base.json` : undefined;
@@ -210,7 +221,7 @@ async function writeFiles(tree, { directory, projectName, workspaces, bundleIden
210
221
  const root = readJson(tree, 'package.json');
211
222
  Object.assign(manifest, {
212
223
  dependencies: await appDependencies(tree, root, native.dependencies),
213
- devDependencies: await appDependencies(tree, root, native.devDependencies),
224
+ devDependencies: await appDependencies(tree, root, devDependencies(lint)),
214
225
  });
215
226
  } else {
216
227
  manifest.dependencies = native.prebuildPins(root);
@@ -228,11 +239,11 @@ function ignoreExpo(tree) {
228
239
 
229
240
  /**
230
241
  * @param {import('@nx/devkit').Tree} tree
231
- * @param {{ directory: string, name?: string, bundleIdentifier?: string, tags?: string, skipInstall?: boolean, skipFormat?: boolean }} options
242
+ * @param {{ directory: string, name?: string, bundleIdentifier?: string, tags?: string, linter?: 'eslint' | 'none', skipInstall?: boolean, skipFormat?: boolean }} options
232
243
  */
233
244
  async function application(tree, options) {
234
245
  const resolved = names(tree, options);
235
- const { directory, projectName, workspaces } = resolved;
246
+ const { directory, projectName, workspaces, lint } = resolved;
236
247
  const problem = native.bundleIdentifierProblem(resolved.bundleIdentifier);
237
248
  if (problem) throw new Error(problem);
238
249
  if (tree.exists(joinPathFragments(directory, 'package.json'))) {
@@ -244,12 +255,15 @@ async function application(tree, options) {
244
255
  if (workspaces) {
245
256
  includeInWorkspaces(tree, directory);
246
257
  } else {
247
- for (const problem of native.conflicts(readJson(tree, 'package.json'))) logger.warn(problem);
258
+ const linter = options.linter;
259
+ for (const problem of native.conflicts(readJson(tree, 'package.json'), { linter })) {
260
+ logger.warn(problem);
261
+ }
248
262
  const existing = listed(readJson(tree, 'package.json'));
249
263
  addDependenciesToPackageJson(
250
264
  tree,
251
265
  await asSaved(tree, native.dependencies, existing),
252
- await asSaved(tree, native.devDependencies, existing),
266
+ await asSaved(tree, devDependencies(lint), existing),
253
267
  'package.json',
254
268
  true,
255
269
  );
@@ -270,7 +284,7 @@ async function application(tree, options) {
270
284
  .split(',')
271
285
  .map((tag) => tag.trim())
272
286
  .filter(Boolean),
273
- targets: targets(directory, port, linksLibraries(tree, workspaces)),
287
+ targets: targets(directory, port, linksLibraries(tree, workspaces), lint),
274
288
  });
275
289
 
276
290
  ignoreExpo(tree);
@@ -279,7 +293,9 @@ async function application(tree, options) {
279
293
  if (!options.skipFormat) await formatFiles(tree);
280
294
  logger.info(
281
295
  `Created ${directory}. nx start ${projectName} runs Metro; nx run ${projectName}:run-ios, ` +
282
- `nx test ${projectName}, nx typecheck ${projectName}, nx export ${projectName}.`,
296
+ `nx test ${projectName}, nx typecheck ${projectName}, ` +
297
+ (lint ? `nx lint ${projectName}, ` : '') +
298
+ `nx export ${projectName}.`,
283
299
  );
284
300
  if (options.skipInstall) return initTask;
285
301
  return runTasksInSerial(initTask, () => installPackagesTask(tree, true));
@@ -8,7 +8,10 @@
8
8
  "directory": {
9
9
  "type": "string",
10
10
  "description": "Where the app goes, relative to the workspace root: apps/mobile.",
11
- "$default": { "$source": "argv", "index": 0 },
11
+ "$default": {
12
+ "$source": "argv",
13
+ "index": 0
14
+ },
12
15
  "x-prompt": "Which directory should the app be created in?"
13
16
  },
14
17
  "name": {
@@ -24,6 +27,12 @@
24
27
  "type": "string",
25
28
  "description": "Comma-separated tags for the project."
26
29
  },
30
+ "linter": {
31
+ "type": "string",
32
+ "description": "eslint adds an eslint.config.mjs with @ng-native/eslint-plugin's recommended rules, its dependencies and a lint target.",
33
+ "enum": ["eslint", "none"],
34
+ "default": "none"
35
+ },
27
36
  "skipInstall": {
28
37
  "type": "boolean",
29
38
  "description": "Write the files without installing.",
package/files/AGENTS.md CHANGED
@@ -83,3 +83,7 @@ change; both are fast.
83
83
  `getByText` or `getByTestId` (which matches `nativeID` and `testID`), and act with
84
84
  `userEvent.setup().press(...)` or `.type(...)`, all from `@ng-native/testing`. Tests run in Node
85
85
  with no simulator, so write one alongside a change.
86
+
87
+ A service that depends on the platform injects `OS` and `OS_VERSION` from `@ng-native/device`
88
+ rather than importing `Platform` from `react-native`, which a test cannot load. A test provides
89
+ the values it wants: `{ provide: OS, useValue: 'android' }`.
@@ -3,8 +3,8 @@ import { Pressable, SafeAreaProvider, SafeAreaView, Text, View } from '@ng-nativ
3
3
  import { StatusBar } from '@ng-native/device';
4
4
 
5
5
  /**
6
- * Element names are lowercase, and that is load-bearing rather than style: an uppercase name is
7
- * read as an unknown Angular component and commits as a plain view with nothing in it.
6
+ * Element names are lowercase, and that is load-bearing rather than style: an uppercase name such
7
+ * as <View> matches no component's selector, and the Metro transform fails the build on it.
8
8
  */
9
9
  @Component({
10
10
  imports: [Pressable, SafeAreaProvider, SafeAreaView, Text, View],
package/files/src/main.ts CHANGED
@@ -10,7 +10,7 @@ import { App } from './app/app.ts';
10
10
 
11
11
  registerPlatformComponents(Platform.OS);
12
12
 
13
- AppRegistry.registerRunnable('main', ({ rootTag }: { rootTag: number | string }) => {
13
+ AppRegistry.registerRunnable('main', ({ rootTag }) => {
14
14
  const app = mount(Number(rootTag), App, getFabricUIManager(), {
15
15
  // Colours, as the integers the platform wants.
16
16
  processColor,
@@ -26,6 +26,8 @@ AppRegistry.registerRunnable('main', ({ rootTag }: { rootTag: number | string })
26
26
 
27
27
  // Re-resolves the conditions when the device rotates or the theme changes. A rotation dirties
28
28
  // no component and no binding, so without this nothing re-renders and `dark:` stops following
29
- // the system switch.
30
- watchConditions(app.engine);
29
+ // the system switch. It stops with the app, which Android destroys when it recreates the
30
+ // activity, so the app mounted in its place is the only one still watching.
31
+ const stopWatching = watchConditions(app.engine);
32
+ app.applicationRef.onDestroy(stopWatching);
31
33
  });
package/library/index.cjs CHANGED
@@ -200,13 +200,19 @@ const TYPECHECK_CONFIG = 'tsconfig.typecheck.json';
200
200
  * infers a `typecheck` that runs `tsc`, which reads no template: a misspelled input passes.
201
201
  *
202
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.
203
+ * is `composite` has to emit. Without `composite`, a `declarationMap` the preset's base sets is an
204
+ * error (TS5069), since nothing emits declarations. The Angular options are the generated app's: a
205
+ * native view has no DOM to check a host binding or an event against.
205
206
  */
206
207
  function writeTypecheckConfig(tree, root) {
207
208
  writeJson(tree, joinPathFragments(root, TYPECHECK_CONFIG), {
208
209
  extends: './tsconfig.lib.json',
209
- compilerOptions: { noEmit: true, composite: false, emitDeclarationOnly: false },
210
+ compilerOptions: {
211
+ noEmit: true,
212
+ composite: false,
213
+ emitDeclarationOnly: false,
214
+ declarationMap: false,
215
+ },
210
216
  angularCompilerOptions: {
211
217
  strictTemplates: true,
212
218
  typeCheckHostBindings: false,
package/migrations.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "generators": {
3
3
  "sync-app-versions": {
4
- "version": "0.17.0",
4
+ "version": "0.18.0",
5
5
  "description": "Move the @ng-native packages each project's own package.json lists to the new version.",
6
6
  "implementation": "./migrations/run.cjs#sync-app-versions"
7
7
  },
@@ -29,6 +29,21 @@
29
29
  "version": "0.17.0",
30
30
  "description": "Read an engine node's children, text, kind, host and sheet by their new names.",
31
31
  "implementation": "./migrations/run.cjs#engine-node-fields"
32
+ },
33
+ "library-typecheck-declaration-map": {
34
+ "version": "0.17.1",
35
+ "description": "Turn off declarationMap in each Nx library's tsconfig.typecheck.json, which TypeScript refuses beside composite: false.",
36
+ "implementation": "./migrations/run.cjs#library-typecheck-declaration-map"
37
+ },
38
+ "native-typecheck-target": {
39
+ "version": "0.17.1",
40
+ "description": "Give each Angular Native project in angular.json a typecheck target, run by ngc.",
41
+ "implementation": "./migrations/run.cjs#native-typecheck-target"
42
+ },
43
+ "stop-watching-conditions": {
44
+ "version": "0.17.2",
45
+ "description": "Keep what watchConditions returns in each app's entry point and stop it as the app is destroyed.",
46
+ "implementation": "./migrations/run.cjs#stop-watching-conditions"
32
47
  }
33
48
  }
34
49
  }
package/native-app.cjs CHANGED
@@ -33,10 +33,10 @@ const { dependencies, devDependencies, accepted } = shared;
33
33
  */
34
34
  const expoCompanions = {
35
35
  'react-dom': dependencies.react,
36
- '@expo/cli': '^57.0.27',
36
+ '@expo/cli': '^58.1.14',
37
37
  '@babel/runtime': '^7.20.0',
38
38
  '@babel/core': devDependencies['@babel/core'],
39
- '@expo/metro': '~56.0.2',
39
+ '@expo/metro': '~58.0.0',
40
40
  };
41
41
 
42
42
  /**
@@ -98,13 +98,13 @@ function tsconfig(workspaceBase, conditions = []) {
98
98
  const customConditions = ['react-native', ...conditions.filter((c) => c !== 'react-native')];
99
99
  if (!workspaceBase) {
100
100
  if (customConditions.length > 1) compilerOptions.customConditions = customConditions;
101
- return { extends: 'expo/tsconfig.base', compilerOptions, angularCompilerOptions };
101
+ return { extends: 'expo/tsconfig.base.json', compilerOptions, angularCompilerOptions };
102
102
  }
103
103
  // The workspace's base is written for a web build or for emitting declarations, and wins over
104
104
  // Expo's where they overlap. These are Expo's, restored: without `DOM` and `ESNext` the framework
105
105
  // packages' own source, which the app compiles, fails to typecheck.
106
106
  return {
107
- extends: ['expo/tsconfig.base', workspaceBase],
107
+ extends: ['expo/tsconfig.base.json', workspaceBase],
108
108
  compilerOptions: {
109
109
  ...compilerOptions,
110
110
  noEmit: true,
@@ -122,7 +122,7 @@ function tsconfig(workspaceBase, conditions = []) {
122
122
  }
123
123
 
124
124
  /** The `@ng-native` packages with entry points beside their main one, each published in `dist`. */
125
- const WITH_ENTRY_POINTS = ['components', 'expo', 'platform', 'web'];
125
+ const WITH_ENTRY_POINTS = ['components', 'expo', 'platform', 'web', 'web-compat'];
126
126
 
127
127
  /** `nx typecheck` in a workspace whose libraries are the package manager's links. */
128
128
  const TYPECHECK_ONE_COPY = TYPECHECK.replace('tsconfig.json', 'tsconfig.typecheck.json');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ng-native/nx",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Nx generators for Angular Native: nx add, an Expo app project whose targets run it, and native libraries and components.",
5
5
  "keywords": [
6
6
  "angular",
@@ -23,6 +23,7 @@
23
23
  "@ng-native/analog",
24
24
  "@ng-native/components",
25
25
  "@ng-native/device",
26
+ "@ng-native/eslint-plugin",
26
27
  "@ng-native/expo",
27
28
  "@ng-native/fabric",
28
29
  "@ng-native/icons",
@@ -39,13 +40,13 @@
39
40
  ]
40
41
  },
41
42
  "dependencies": {
42
- "@ng-native/migrate": "0.17.0",
43
- "@nx/devkit": "^23.0.0",
43
+ "@ng-native/migrate": "0.18.0",
44
+ "@nx/devkit": "^23.0.0 || ^24.0.0",
44
45
  "minimatch": "^10.0.0",
45
46
  "semver": "^7.7.0"
46
47
  },
47
48
  "peerDependencies": {
48
- "nx": "^23.0.0"
49
+ "nx": "^23.0.0 || ^24.0.0"
49
50
  },
50
51
  "devDependencies": {
51
52
  "@types/semver": "^7.7.0",