@ng-native/schematics 0.1.3 → 0.3.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.
@@ -96,13 +96,14 @@ function commands(name) {
96
96
  ].join('\n');
97
97
  }
98
98
 
99
- function writeFiles(tree, root, name) {
99
+ function writeFiles(tree, root, name, bundleIdentifier) {
100
100
  for (const file of native.SOURCE_FILES) {
101
101
  tree.create(path.join(root, file), native.sourceFile(file));
102
102
  }
103
- tree.create(path.join(root, 'app.json'), native.appJson(name));
103
+ tree.create(path.join(root, 'app.json'), native.appJson(name, bundleIdentifier));
104
104
  tree.create(path.join(root, 'AGENTS.md'), native.agentsFile(commands(name)));
105
105
  tree.create(path.join(root, 'CLAUDE.md'), '@AGENTS.md\n');
106
+ tree.create(path.join(root, '.gitignore'), native.GITIGNORE);
106
107
  const manifest = native.projectManifest(name, readJson(tree, 'package.json'));
107
108
  tree.create(path.join(root, 'package.json'), JSON.stringify(manifest, null, 2) + '\n');
108
109
  }
@@ -130,7 +131,7 @@ function ignoreExpo(tree) {
130
131
  }
131
132
 
132
133
  /**
133
- * @param {{ name: string, directory?: string, prefix?: string, skipInstall?: boolean }} options
134
+ * @param {{ name: string, directory?: string, prefix?: string, bundleIdentifier?: string, skipInstall?: boolean }} options
134
135
  */
135
136
  function application(options) {
136
137
  return (tree, context) => {
@@ -146,9 +147,13 @@ function application(options) {
146
147
  throw new SchematicsException(`angular.json already has a project called "${name}".`);
147
148
  }
148
149
  const root = options.directory ?? path.join(workspace.newProjectRoot ?? 'projects', name);
150
+ const scope = readJson(tree, 'package.json').name?.match(/^@[^/]+/)?.[0];
151
+ const bundleIdentifier = options.bundleIdentifier ?? native.bundleIdentifier(name, scope);
152
+ const problem = native.bundleIdentifierProblem(bundleIdentifier);
153
+ if (problem) throw new SchematicsException(problem);
149
154
 
150
155
  addDependencies(tree, context);
151
- writeFiles(tree, root, name);
156
+ writeFiles(tree, root, name, bundleIdentifier);
152
157
  workspace.projects = { ...workspace.projects, [name]: project(root, name, options.prefix) };
153
158
  writeJson(tree, 'angular.json', workspace);
154
159
  ignoreExpo(tree);
@@ -21,6 +21,10 @@
21
21
  "default": "app",
22
22
  "pattern": "^[a-z][a-z0-9-]*$"
23
23
  },
24
+ "bundleIdentifier": {
25
+ "type": "string",
26
+ "description": "The iOS bundle identifier and Android package: com.acme.mobile. Defaults to com.<workspace scope>.<name>."
27
+ },
24
28
  "skipInstall": {
25
29
  "type": "boolean",
26
30
  "description": "Write the files and package.json without installing.",
@@ -10,36 +10,7 @@
10
10
  const path = require('node:path').posix;
11
11
  const { SchematicsException } = require('@angular-devkit/schematics');
12
12
  const { strings } = require('@angular-devkit/core');
13
-
14
- function componentSource(className, selector, label) {
15
- return `import { Component } from '@angular/core';
16
- import { Text, View } from '@ng-native/components';
17
-
18
- @Component({
19
- selector: '${selector}',
20
- imports: [Text, View],
21
- template: \`
22
- <view>
23
- <text>${label}</text>
24
- </view>
25
- \`,
26
- })
27
- export class ${className} {}
28
- `;
29
- }
30
-
31
- function testSource(className, file, label) {
32
- return `import { render, screen } from '@ng-native/testing';
33
- import { expect, test } from 'vitest';
34
- import { ${className} } from './${file}.ts';
35
-
36
- test('renders', async () => {
37
- await render(${className});
38
-
39
- expect(screen.getByText('${label}')).toBeTruthy();
40
- });
41
- `;
42
- }
13
+ const { componentSource, testSource } = require('./sources.cjs');
43
14
 
44
15
  /**
45
16
  * The directory the component goes in: the one `ng generate` was run from if that is inside the
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The files a generated component is made of: a component built from `<view>` and `<text>`, and a
3
+ * test that renders it on the fake Fabric. `@ng-native/schematics` and `@ng-native/nx` each ship
4
+ * this file, and a test in `@ng-native/nx` keeps the two identical.
5
+ */
6
+
7
+ /**
8
+ * @param {string} className
9
+ * @param {string} selector
10
+ * @param {string} label the text it shows, which its test finds
11
+ */
12
+ function componentSource(className, selector, label) {
13
+ return `import { Component } from '@angular/core';
14
+ import { Text, View } from '@ng-native/components';
15
+
16
+ @Component({
17
+ selector: '${selector}',
18
+ imports: [Text, View],
19
+ template: \`
20
+ <view>
21
+ <text>${label}</text>
22
+ </view>
23
+ \`,
24
+ })
25
+ export class ${className} {}
26
+ `;
27
+ }
28
+
29
+ /**
30
+ * @param {string} className
31
+ * @param {string} file the component's file name, without `.ts`
32
+ * @param {string} label
33
+ */
34
+ function testSource(className, file, label) {
35
+ return `import { render, screen } from '@ng-native/testing';
36
+ import { expect, test } from 'vitest';
37
+ import { ${className} } from './${file}.ts';
38
+
39
+ test('renders', async () => {
40
+ await render(${className});
41
+
42
+ expect(screen.getByText('${label}')).toBeTruthy();
43
+ });
44
+ `;
45
+ }
46
+
47
+ module.exports = { componentSource, testSource };
package/files/AGENTS.md CHANGED
@@ -33,6 +33,9 @@ change; both are fast.
33
33
  without its import renders as a plain view.
34
34
  - **There is no DOM.** No `document`, no `window`, no `<div>`, `<span>`, `<button>` or `<input>`,
35
35
  no `@angular/platform-browser`, and no `@angular/animations`. Use the native elements above.
36
+ - **A component's host is a flex item.** `<app-case-list />` commits as a view of its own, with no
37
+ `flex`, so a `flex: 1` scroll view inside it gets a height of zero and shows nothing. A component
38
+ that fills the space it is given sets `host: { style: 'flex: 1' }` (or `:host { flex: 1; }`).
36
39
  - **Text only renders inside `<text>`.** `<view>Hello</view>` compiles and shows nothing.
37
40
  - **Events are native:** `(press)` on `<pressable>`, not `(click)`; `[(value)]` or `(changeText)` on
38
41
  `<text-input>`; `(scroll)`, `(layout)`. A pressable text is `<text pressable (press)="...">`.
@@ -1,5 +1,6 @@
1
- import { Component, signal } from '@angular/core';
1
+ import { Component, inject, signal } from '@angular/core';
2
2
  import { Pressable, SafeAreaProvider, SafeAreaView, Text, View } from '@ng-native/components';
3
+ import { StatusBar } from '@ng-native/device';
3
4
 
4
5
  /**
5
6
  * Element names are lowercase, and that is load-bearing rather than style: an uppercase name is
@@ -28,7 +29,7 @@ import { Pressable, SafeAreaProvider, SafeAreaView, Text, View } from '@ng-nativ
28
29
  }
29
30
  .screen {
30
31
  flex: 1;
31
- background-color: #101014;
32
+ background-color: #f5f5f7;
32
33
  }
33
34
  .body {
34
35
  flex: 1;
@@ -37,12 +38,12 @@ import { Pressable, SafeAreaProvider, SafeAreaView, Text, View } from '@ng-nativ
37
38
  padding: 24px;
38
39
  }
39
40
  .title {
40
- color: #ffffff;
41
+ color: #101014;
41
42
  font-size: 28px;
42
43
  font-weight: 700;
43
44
  }
44
45
  .hint {
45
- color: #8b8b96;
46
+ color: #5f5f6b;
46
47
  font-size: 15px;
47
48
  }
48
49
  .button {
@@ -57,8 +58,25 @@ import { Pressable, SafeAreaProvider, SafeAreaView, Text, View } from '@ng-nativ
57
58
  font-size: 16px;
58
59
  font-weight: 600;
59
60
  }
61
+ @media (prefers-color-scheme: dark) {
62
+ .screen {
63
+ background-color: #101014;
64
+ }
65
+ .title {
66
+ color: #ffffff;
67
+ }
68
+ .hint {
69
+ color: #8b8b96;
70
+ }
71
+ }
60
72
  `,
61
73
  })
62
74
  export class App {
63
75
  protected readonly count = signal(0);
76
+
77
+ constructor() {
78
+ // Dark status bar icons in light mode and light ones in dark mode. Unclaimed, Android keeps
79
+ // light icons whatever the scheme.
80
+ inject(StatusBar).set({ style: 'auto' });
81
+ }
64
82
  }
package/files/src/main.ts CHANGED
@@ -1,3 +1,7 @@
1
+ // Expo's runtime: its fetch, whose response streams a body, and URL, TextDecoderStream and
2
+ // structuredClone. Metro runs it before this file only when something imports it, and nothing
3
+ // else does in a release build, which would then get React Native's fetch, with no body.
4
+ import 'expo';
1
5
  import { AppRegistry, Image, Platform, processColor } from 'react-native';
2
6
  import { mount } from '@ng-native/platform';
3
7
  import { currentConditions, deviceTokens, watchConditions } from '@ng-native/device';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The migrations `ng update` 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').forAngular();
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "../../node_modules/@angular-devkit/schematics/collection-schema.json",
3
+ "schematics": {
4
+ "sync-app-versions": {
5
+ "version": "0.3.0",
6
+ "description": "Move the @ng-native packages each project's own package.json lists to the new version.",
7
+ "factory": "./migrations/run.cjs#sync-app-versions"
8
+ },
9
+ "split-store-and-player": {
10
+ "version": "0.3.0",
11
+ "description": "Import Storage, SecureStorage, audioPlayer and videoPlayer from the entry points that now hold them.",
12
+ "factory": "./migrations/run.cjs#split-store-and-player"
13
+ },
14
+ "tracking-available-getter": {
15
+ "version": "0.3.0",
16
+ "description": "Read Tracking's available as a property rather than calling it.",
17
+ "factory": "./migrations/run.cjs#tracking-available-getter"
18
+ },
19
+ "device-react-native-import": {
20
+ "version": "0.3.0",
21
+ "description": "Note each import of reactNative or ReactNative from @ng-native/device, which no longer exports them.",
22
+ "factory": "./migrations/run.cjs#device-react-native-import"
23
+ }
24
+ }
25
+ }
package/native-app.cjs CHANGED
@@ -2,52 +2,16 @@
2
2
  * What an Angular Native app is made of, whichever workspace it is generated into.
3
3
  *
4
4
  * The source of truth is `template/`, the starter `create-expo-app` uses and the setup this project
5
- * verifies before every release. `files/` holds verbatim copies of the template's source files and
6
- * the versions below are the template's own, and `native-app.test.ts` fails the moment either
7
- * drifts from it. A generated app and a templated app are the same app.
5
+ * verifies before every release. `files/` holds verbatim copies of the template's source files,
6
+ * the versions are the template's own, and `native-app.test.ts` fails the moment either drifts
7
+ * from it. A generated app and a templated app are the same app.
8
8
  *
9
- * The `@ng-native/*` packages are pinned to this package's own version, because every one of them
10
- * is released in lockstep with it.
9
+ * What does not depend on the Angular CLI, the dependencies, `app.json` and bundle identifier
10
+ * among them, is `@ng-native/migrate/native-app.cjs`, which `@ng-native/nx` builds its apps from too.
11
11
  */
12
12
  const { readFileSync } = require('node:fs');
13
13
  const path = require('node:path');
14
- const semver = require('semver');
15
-
16
- const { version } = require('./package.json');
17
-
18
- const FRAMEWORK = [
19
- '@ng-native/components',
20
- '@ng-native/device',
21
- '@ng-native/fabric',
22
- '@ng-native/metro',
23
- '@ng-native/platform',
24
- ];
25
-
26
- /** @type {Record<string, string>} */
27
- const dependencies = {
28
- ...Object.fromEntries(FRAMEWORK.map((name) => [name, version])),
29
- '@angular/common': '^22.0.0',
30
- '@angular/core': '^22.0.0',
31
- expo: '~57.0.26',
32
- 'expo-status-bar': '~57.0.1',
33
- 'expo-system-ui': '~57.0.4',
34
- react: '19.2.3',
35
- 'react-native': '0.86.3',
36
- 'react-native-safe-area-context': '~5.7.0',
37
- };
38
-
39
- /** @type {Record<string, string>} */
40
- const devDependencies = {
41
- // An Angular workspace already has Babel 8 through @angular/compiler-cli, and React Native's
42
- // Babel peer would otherwise take the newest copy there is.
43
- // The typecheck script compiles the templates too, which only the Angular compiler can.
44
- '@angular/compiler-cli': '^22.0.0',
45
- '@babel/core': '^7.29.7',
46
- '@ng-native/testing': version,
47
- '@types/react': '~19.2.2',
48
- typescript: '~6.0.3',
49
- vitest: '^5.0.0',
50
- };
14
+ const shared = require('@ng-native/migrate/native-app.cjs');
51
15
 
52
16
  /**
53
17
  * The template's files, copied verbatim into the new app. Its `metro.config.js` and `tsconfig.json`
@@ -82,94 +46,21 @@ function agentsFile(commands) {
82
46
  }
83
47
 
84
48
  /**
85
- * The template's `app.json`, named for this app.
86
- *
87
- * Without the template's icons: they are the template's branding, not this app's, and Expo draws
88
- * its own default until `icon` is set.
89
- *
90
- * @param {string} name the project name, which is also the Expo slug
91
- */
92
- function appJson(name) {
93
- const expo = {
94
- name,
95
- slug: name,
96
- version: '1.0.0',
97
- platforms: ['ios', 'android'],
98
- orientation: 'portrait',
99
- userInterfaceStyle: 'dark',
100
- ios: { supportsTablet: true },
101
- android: { predictiveBackGestureEnabled: false },
102
- scheme: name.replace(/[^a-z0-9]/gi, '').toLowerCase(),
103
- // The template's: Expo otherwise guesses a router root from src/app and says so on every start.
104
- extra: { router: { root: 'src/app' } },
105
- plugins: ['@ng-native/metro'],
106
- };
107
- return JSON.stringify({ expo }, null, 2) + '\n';
108
- }
109
-
110
- /**
111
- * The project's own `package.json`, which only Expo reads: `main` is how it finds the entry file.
112
- *
113
- * It lists every dependency the app has too, at the ranges the workspace root installs, though
114
- * nothing installs from it. Expo's autolinking links the native modules named here and no others,
115
- * so one left out, such as `react-native-safe-area-context`, is missing from the native build. And
116
- * `expo prebuild`, which `expo run:ios` starts with, adds `expo`, `react` or `react-native` if it
117
- * does not find them and then offers to install them into the project's directory, which would put
118
- * a second copy of React Native beside the root's.
49
+ * The project's own `package.json`, which only Expo reads: `main` is how it finds the entry file,
50
+ * and `prebuildPins` says why it lists every dependency the app has.
119
51
  *
120
52
  * @param {string} name
121
53
  * @param {{ dependencies?: Record<string, string>, devDependencies?: Record<string, string> }} root
122
54
  * the workspace's root `package.json`, once this app's dependencies are in it
123
55
  */
124
56
  function projectManifest(name, root) {
125
- const installed = { ...root.devDependencies, ...root.dependencies };
126
- const pinned = Object.keys(dependencies).map((pkg) => [pkg, installed[pkg] ?? dependencies[pkg]]);
127
- return { name, private: true, main: 'src/main.ts', dependencies: Object.fromEntries(pinned) };
128
- }
129
-
130
- /**
131
- * Where what a workspace already has can be older than what a new app is given: `@ng-native/testing`
132
- * installs Vitest 5 but its plugin runs on 4, which `ng new` 22.1 and the Nx preset still start on.
133
- * The range here is that package's `vitest` peer, and a test holds the two together.
134
- */
135
- const accepted = { vitest: '^4.0.8 || ^5.0.0' };
136
-
137
- /**
138
- * Why an existing dependency would stop this app installing or running, as a sentence each.
139
- *
140
- * Only ranges already in the manifest are judged, and only a range that cannot overlap the one
141
- * wanted is a conflict: `~6.0.2` and `~6.0.3` resolve to the same TypeScript. A missing package is
142
- * added at the version above. Nothing is rewritten, because moving a workspace's Angular or Vitest is an upgrade of every
143
- * project in it, and that belongs to `ng update` or `nx migrate`, not to a generator.
144
- *
145
- * @param {{ dependencies?: Record<string, string>, devDependencies?: Record<string, string> }} manifest
146
- * @returns {string[]}
147
- */
148
- function conflicts(manifest) {
149
- const existing = { ...manifest.dependencies, ...manifest.devDependencies };
150
- const problems = [];
151
- for (const [name, installed] of Object.entries({ ...dependencies, ...devDependencies })) {
152
- const range = existing[name];
153
- const wanted = accepted[name] ?? installed;
154
- if (!range || name.startsWith('@ng-native/')) continue;
155
- if (semver.validRange(range) && !semver.intersects(range, wanted)) {
156
- problems.push(
157
- `${name} is ${range} here, and Angular Native needs ${wanted}. ` +
158
- `npm will refuse the install until it is moved.`,
159
- );
160
- }
161
- }
162
- return problems;
57
+ return { name, private: true, main: 'src/main.ts', dependencies: shared.prebuildPins(root) };
163
58
  }
164
59
 
165
60
  module.exports = {
166
- dependencies,
167
- devDependencies,
61
+ ...shared,
168
62
  SOURCE_FILES,
169
63
  sourceFile,
170
64
  agentsFile,
171
- appJson,
172
65
  projectManifest,
173
- conflicts,
174
- accepted,
175
66
  };
package/ng-add/index.cjs CHANGED
@@ -8,7 +8,7 @@
8
8
  */
9
9
  const { application } = require('../application/index.cjs');
10
10
 
11
- /** @param {{ name?: string, directory?: string, skipInstall?: boolean }} options */
11
+ /** @param {{ name?: string, directory?: string, bundleIdentifier?: string, skipInstall?: boolean }} options */
12
12
  function ngAdd(options) {
13
13
  return application({ ...options, name: options.name ?? 'native', prefix: 'app' });
14
14
  }
@@ -14,6 +14,10 @@
14
14
  "type": "string",
15
15
  "description": "Where the project goes. Defaults to newProjectRoot/name, usually projects/native."
16
16
  },
17
+ "bundleIdentifier": {
18
+ "type": "string",
19
+ "description": "The iOS bundle identifier and Android package: com.acme.mobile. Defaults to com.<workspace scope>.<name>."
20
+ },
17
21
  "skipInstall": {
18
22
  "type": "boolean",
19
23
  "description": "Write the files and package.json without installing.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ng-native/schematics",
3
- "version": "0.1.3",
3
+ "version": "0.3.0",
4
4
  "description": "ng add and ng generate for Angular Native: an Expo app in an Angular CLI workspace, with builders that run it.",
5
5
  "keywords": [
6
6
  "angular",
@@ -21,10 +21,31 @@
21
21
  "ng-add": {
22
22
  "save": "devDependencies"
23
23
  },
24
+ "ng-update": {
25
+ "migrations": "./migrations.json",
26
+ "packageGroupName": "@ng-native/schematics",
27
+ "packageGroup": [
28
+ "@ng-native/components",
29
+ "@ng-native/device",
30
+ "@ng-native/expo",
31
+ "@ng-native/fabric",
32
+ "@ng-native/icons",
33
+ "@ng-native/metro",
34
+ "@ng-native/migrate",
35
+ "@ng-native/nx",
36
+ "@ng-native/platform",
37
+ "@ng-native/router",
38
+ "@ng-native/schematics",
39
+ "@ng-native/tailwind",
40
+ "@ng-native/testing",
41
+ "@ng-native/web"
42
+ ]
43
+ },
24
44
  "dependencies": {
25
45
  "@angular-devkit/architect": ">=0.2200.0 <0.2300.0",
26
46
  "@angular-devkit/core": "^22.0.0",
27
47
  "@angular-devkit/schematics": "^22.0.0",
48
+ "@ng-native/migrate": "0.3.0",
28
49
  "semver": "^7.7.0"
29
50
  },
30
51
  "devDependencies": {
@@ -32,6 +53,7 @@
32
53
  "@types/semver": "^7.7.0"
33
54
  },
34
55
  "nx": {
56
+ "projectType": "library",
35
57
  "tags": [
36
58
  "scope:schematics",
37
59
  "layer:build"
@@ -51,6 +73,7 @@
51
73
  "*.cjs",
52
74
  "collection.json",
53
75
  "builders.json",
76
+ "migrations.json",
54
77
  "*/*.cjs",
55
78
  "*/*.json",
56
79
  "files"
@@ -60,6 +83,6 @@
60
83
  },
61
84
  "scripts": {
62
85
  "typecheck": "tsc -p tsconfig.json --noEmit",
63
- "test": "node --test \"*.test.ts\" \"{application,component,builders}/*.test.ts\""
86
+ "test": "node --test \"*.test.ts\" \"{application,component,builders,migrations}/*.test.ts\""
64
87
  }
65
88
  }