@ng-native/nx 0.3.0 → 0.5.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.
@@ -59,6 +59,15 @@ function hasPathAliases(tree, workspaces) {
59
59
  * The compiler options `file` ends up with, those it inherits through `extends` included, as
60
60
  * TypeScript merges them: each option from the last config that sets it.
61
61
  */
62
+ /**
63
+ * Whether the app reaches its libraries through the package manager's links alone, where a
64
+ * library can have a copy of its own of a package the app has too. With path aliases the
65
+ * workspace has one `node_modules`, and a `paths` list of the app's would replace the aliases.
66
+ */
67
+ function linksLibraries(tree, workspaces) {
68
+ return workspaces && !hasPathAliases(tree, workspaces);
69
+ }
70
+
62
71
  function baseCompilerOptions(tree, file = 'tsconfig.base.json', ancestors = []) {
63
72
  if (ancestors.includes(file) || !tree.exists(file)) return {};
64
73
  const config = readJson(tree, file);
@@ -116,7 +125,7 @@ function metroPort(tree) {
116
125
  return port;
117
126
  }
118
127
 
119
- function targets(directory, port) {
128
+ function targets(directory, port, oneCopy) {
120
129
  const run = (command) => ({ executor: 'nx:run-commands', options: { cwd: directory, command } });
121
130
  const own = port === DEFAULT_PORT ? '' : ` --port ${port}`;
122
131
  // `expo run:ios` and `expo run:android` build the port into the app, so a later app's has to be
@@ -127,7 +136,7 @@ function targets(directory, port) {
127
136
  };
128
137
  return {
129
138
  typecheck: {
130
- ...run(native.TYPECHECK),
139
+ ...run(oneCopy ? native.TYPECHECK_ONE_COPY : native.TYPECHECK),
131
140
  cache: true,
132
141
  inputs: ['default', '^production'],
133
142
  // The Tailwind sheet the command builds, which a cache hit restores rather than rebuilds.
@@ -190,6 +199,9 @@ async function writeFiles(tree, { directory, projectName, workspaces, bundleIden
190
199
  const workspaceBase = base ? `${offsetFromRoot(directory)}tsconfig.base.json` : undefined;
191
200
  const conditions = baseCompilerOptions(tree).customConditions ?? [];
192
201
  file('tsconfig.json', JSON.stringify(native.tsconfig(workspaceBase, conditions), null, 2) + '\n');
202
+ if (linksLibraries(tree, workspaces)) {
203
+ file('tsconfig.typecheck.json', JSON.stringify(native.typecheckTsconfig(), null, 2) + '\n');
204
+ }
193
205
  file('vitest.config.mts', native.vitestConfig(Boolean(base)));
194
206
 
195
207
  const manifest = { name: projectName, version: '0.0.1', private: true, main: 'src/main.ts' };
@@ -258,7 +270,7 @@ async function application(tree, options) {
258
270
  .split(',')
259
271
  .map((tag) => tag.trim())
260
272
  .filter(Boolean),
261
- targets: targets(directory, port),
273
+ targets: targets(directory, port, linksLibraries(tree, workspaces)),
262
274
  });
263
275
 
264
276
  ignoreExpo(tree);
package/files/AGENTS.md CHANGED
@@ -31,12 +31,17 @@ change; both are fast.
31
31
  imported from `@ng-native/components` into the component's `imports`, like any Angular
32
32
  component: `imports: [View, Text]`. `<View>` compiles to an empty template, and an element used
33
33
  without its import renders as a plain view.
34
- - **There is no DOM.** No `document`, no `window`, no `<div>`, `<span>`, `<button>` or `<input>`,
35
- no `@angular/platform-browser`, and no `@angular/animations`. Use the native elements above.
34
+ - **There is no DOM.** No `document`, no `window`, no `<button>`, `<a>`, `<input>` or `<img>`, no
35
+ `@angular/platform-browser`, and no `@angular/animations`. Use the native elements above. HTML's
36
+ text elements (`p`, `span`, `h1`, `strong`) draw as text and its layout elements (`div`,
37
+ `section`, `ul`) as plain views, with none of a browser's default styles.
36
38
  - **A component's host is a flex item.** `<app-case-list />` commits as a view of its own, with no
37
39
  `flex`, so a `flex: 1` scroll view inside it gets a height of zero and shows nothing. A component
38
40
  that fills the space it is given sets `host: { style: 'flex: 1' }` (or `:host { flex: 1; }`).
39
- - **Text only renders inside `<text>`.** `<view>Hello</view>` compiles and shows nothing.
41
+ - **Text is drawn in a text element.** `<text>` is the one with inputs and outputs (`numberOfLines`,
42
+ `(press)`). `p`, `span` and the other HTML text elements are text while they hold only text and
43
+ other text elements; one that holds a view is a view. Text written straight into a view is given
44
+ a paragraph of its own, one per run, and never flows inline with the elements beside it.
40
45
  - **Events are native:** `(press)` on `<pressable>`, not `(click)`; `[(value)]` or `(changeText)` on
41
46
  `<text-input>`; `(scroll)`, `(layout)`. A pressable text is `<text pressable (press)="...">`.
42
47
  - **Signals, zoneless, AOT.** State is signals and `computed()`; there is no zone.js, so nothing
package/library/index.cjs CHANGED
@@ -23,6 +23,7 @@ const {
23
23
  names,
24
24
  readNxJson,
25
25
  readJson,
26
+ writeJson,
26
27
  updateJson,
27
28
  updateProjectConfiguration,
28
29
  } = require('@nx/devkit');
@@ -107,6 +108,11 @@ const imports = {
107
108
  },
108
109
  };
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
+
110
116
  function projectAt(tree, root) {
111
117
  for (const [name, project] of getProjects(tree))
112
118
  if (project.root === root) return { name, project };
@@ -164,12 +170,100 @@ function writeSpecConfig(tree, root, angular) {
164
170
  });
165
171
  }
166
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
+
167
260
  /**
168
261
  * The packages the component and its test import: in the library's own `package.json` when it is
169
262
  * a workspace package, which pnpm links from, and at the root otherwise. A version already there
170
263
  * is left alone.
171
264
  */
172
- async function addImports(tree, root) {
265
+ async function addImports(tree, root, alsoDev = {}) {
266
+ const devDependencies = { ...imports.devDependencies, ...alsoDev };
173
267
  const manifest = joinPathFragments(root, 'package.json');
174
268
  const rootManifest = readJson(tree, 'package.json');
175
269
  if (tree.exists(manifest)) {
@@ -177,7 +271,7 @@ async function addImports(tree, root) {
177
271
  return addDependenciesToPackageJson(
178
272
  tree,
179
273
  await own(imports.dependencies),
180
- await own(imports.devDependencies),
274
+ await own(devDependencies),
181
275
  manifest,
182
276
  true,
183
277
  );
@@ -186,7 +280,7 @@ async function addImports(tree, root) {
186
280
  return addDependenciesToPackageJson(
187
281
  tree,
188
282
  await asSaved(tree, imports.dependencies, existing),
189
- await asSaved(tree, imports.devDependencies, existing),
283
+ await asSaved(tree, devDependencies, existing),
190
284
  'package.json',
191
285
  true,
192
286
  );
@@ -211,10 +305,13 @@ async function library(tree, options) {
211
305
  const aliases = hasPathAliases(tree, usesWorkspaces(tree));
212
306
  tree.write(joinPathFragments(directory, 'vitest.config.mts'), native.vitestConfig(aliases));
213
307
  writeSpecConfig(tree, directory, angular);
308
+ if (!angular) importWithTsExtensions(tree, directory);
309
+ if (!angular) writeTypecheckConfig(tree, directory);
214
310
  updateProjectConfiguration(tree, name, {
215
311
  ...project,
216
312
  targets: {
217
313
  ...project.targets,
314
+ ...(angular ? {} : { typecheck: typecheckTarget(directory) }),
218
315
  test: {
219
316
  executor: 'nx:run-commands',
220
317
  options: { cwd: directory, command: 'vitest run' },
@@ -224,7 +321,7 @@ async function library(tree, options) {
224
321
  },
225
322
  });
226
323
 
227
- await addImports(tree, directory);
324
+ await addImports(tree, directory, angular ? {} : typecheckDependencies);
228
325
  // `nxViteTsPaths()`, which the Vitest config uses to reach the workspace's other libraries.
229
326
  if (aliases) {
230
327
  const vite = { '@nx/vite': workspaceNxVersion(tree) };
package/migrations.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "generators": {
3
3
  "sync-app-versions": {
4
- "version": "0.3.0",
4
+ "version": "0.5.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
  },
package/native-app.cjs CHANGED
@@ -121,6 +121,31 @@ function tsconfig(workspaceBase, conditions = []) {
121
121
  };
122
122
  }
123
123
 
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'];
126
+
127
+ /** `nx typecheck` in a workspace whose libraries are the package manager's links. */
128
+ const TYPECHECK_ONE_COPY = TYPECHECK.replace('tsconfig.json', 'tsconfig.typecheck.json');
129
+
130
+ /**
131
+ * The tsconfig `nx typecheck` reads where libraries are the package manager's links: the app's,
132
+ * with every `@ng-native` package resolved to the app's own copy.
133
+ *
134
+ * pnpm gives a library a copy of its own of a package whose peers it does not share with the app,
135
+ * the gesture and animation packages an app adds among them. `ngc` then finds a component a
136
+ * library imports in one copy and the app's import of it in the other, and stops at `NG3004`
137
+ * before it checks a template. The Metro preset and the Vitest plugin make the same choice where
138
+ * they resolve. It is a file of its own because Metro reads the `paths` of `tsconfig.json`, and
139
+ * these skip the `exports` of each package, its `browser` condition included.
140
+ */
141
+ function typecheckTsconfig() {
142
+ const paths = { '@ng-native/*': ['./node_modules/@ng-native/*'] };
143
+ for (const name of WITH_ENTRY_POINTS) {
144
+ paths[`@ng-native/${name}/*`] = [`./node_modules/@ng-native/${name}/dist/*`];
145
+ }
146
+ return { extends: './tsconfig.json', compilerOptions: { paths } };
147
+ }
148
+
124
149
  /** @param {boolean} withPaths whether to resolve the workspace's tsconfig path aliases */
125
150
  function vitestConfig(withPaths) {
126
151
  const template = readFileSync(path.join(__dirname, 'files', 'vitest.config.mts'), 'utf8');
@@ -165,7 +190,10 @@ module.exports = {
165
190
  agentsFile,
166
191
  METRO_CONFIG,
167
192
  TYPECHECK,
193
+ TYPECHECK_ONE_COPY,
194
+ WITH_ENTRY_POINTS,
168
195
  tsconfig,
196
+ typecheckTsconfig,
169
197
  vitestConfig,
170
198
  reuseRootRanges,
171
199
  expoCompanions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ng-native/nx",
3
- "version": "0.3.0",
3
+ "version": "0.5.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",
@@ -37,7 +37,7 @@
37
37
  ]
38
38
  },
39
39
  "dependencies": {
40
- "@ng-native/migrate": "0.3.0",
40
+ "@ng-native/migrate": "0.5.0",
41
41
  "@nx/devkit": "^23.0.0",
42
42
  "minimatch": "^10.0.0",
43
43
  "semver": "^7.7.0"
@@ -54,6 +54,9 @@
54
54
  "tags": [
55
55
  "scope:nx",
56
56
  "layer:build"
57
+ ],
58
+ "implicitDependencies": [
59
+ "@ng-native/template"
57
60
  ]
58
61
  },
59
62
  "license": "MIT",