@ng-native/nx 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.
- package/README.md +8 -3
- package/application/index.cjs +52 -14
- package/component/index.cjs +66 -0
- package/component/schema.json +38 -0
- package/component/sources.cjs +47 -0
- package/files/AGENTS.md +3 -0
- package/files/src/app/app.ts +22 -4
- package/files/src/main.ts +4 -0
- package/generators.json +22 -0
- package/init/index.cjs +91 -1
- package/library/index.cjs +239 -0
- package/library/schema.json +36 -0
- package/migrations/run.cjs +5 -0
- package/migrations.json +24 -0
- package/native-app.cjs +19 -171
- package/package.json +27 -4
- package/sync/index.cjs +233 -0
- package/sync/schema.json +8 -0
- package/tailwind/index.cjs +573 -0
- package/tailwind/schema.json +37 -0
package/sync/index.cjs
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@ng-native/nx:sync-native-modules`: lists in each Angular Native app's `package.json` the native
|
|
3
|
+
* modules the workspace libraries it uses import, and adds their config plugins to its `app.json`.
|
|
4
|
+
*
|
|
5
|
+
* Expo links only the native modules an app's own `package.json` names. Expo Go contains every
|
|
6
|
+
* module in the SDK, so an app whose library imports one the app does not list works there and
|
|
7
|
+
* fails in a development or release build. A library cannot declare what it needs of an app, but
|
|
8
|
+
* Nx's project graph records what each one imports, so this reads it from there.
|
|
9
|
+
*
|
|
10
|
+
* It is a sync generator: the app generator registers it on the app's `start`, `export` and
|
|
11
|
+
* `prebuild` targets, so Nx runs it before them, and `nx sync:check` fails a CI run it would
|
|
12
|
+
* change. It only adds. A module the app lists stays listed when no library imports it any more,
|
|
13
|
+
* since the app may use it itself.
|
|
14
|
+
*
|
|
15
|
+
* A native module is a package with an Expo module config or a native project of its own, as
|
|
16
|
+
* Expo's autolinking finds them. It counts when a library imports it, and when a package a
|
|
17
|
+
* library imports peers on it without marking the peer optional, as `@ng-native/icons` does on
|
|
18
|
+
* `react-native-svg`.
|
|
19
|
+
*/
|
|
20
|
+
const { existsSync, readdirSync, readFileSync } = require('node:fs');
|
|
21
|
+
const nodePath = require('node:path');
|
|
22
|
+
const {
|
|
23
|
+
createProjectGraphAsync,
|
|
24
|
+
formatFiles,
|
|
25
|
+
getProjects,
|
|
26
|
+
joinPathFragments,
|
|
27
|
+
readJson,
|
|
28
|
+
readProjectConfiguration,
|
|
29
|
+
updateJson,
|
|
30
|
+
updateProjectConfiguration,
|
|
31
|
+
} = require('@nx/devkit');
|
|
32
|
+
|
|
33
|
+
const SYNC_GENERATOR = '@ng-native/nx:sync-native-modules';
|
|
34
|
+
|
|
35
|
+
/** Whether the package in `dir` has native code Expo's autolinking links. */
|
|
36
|
+
function isNativeModule(dir) {
|
|
37
|
+
if (existsSync(nodePath.join(dir, 'expo-module.config.json'))) return true;
|
|
38
|
+
if (readdirSync(dir).some((file) => file.endsWith('.podspec'))) return true;
|
|
39
|
+
return ['build.gradle', 'build.gradle.kts'].some((file) =>
|
|
40
|
+
existsSync(nodePath.join(dir, 'android', file)),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The directory `pkg` is installed in, as Node resolves it from `from`. */
|
|
45
|
+
function packageDir(pkg, from) {
|
|
46
|
+
try {
|
|
47
|
+
return nodePath.dirname(require.resolve(`${pkg}/package.json`, { paths: [from] }));
|
|
48
|
+
} catch {
|
|
49
|
+
// A package whose exports leave out its package.json: up from its entry point.
|
|
50
|
+
}
|
|
51
|
+
let dir;
|
|
52
|
+
try {
|
|
53
|
+
dir = nodePath.dirname(require.resolve(pkg, { paths: [from] }));
|
|
54
|
+
} catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
for (; dir !== nodePath.dirname(dir); dir = nodePath.dirname(dir)) {
|
|
58
|
+
const manifest = nodePath.join(dir, 'package.json');
|
|
59
|
+
if (existsSync(manifest) && JSON.parse(readFileSync(manifest, 'utf8')).name === pkg) return dir;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The workspace beyond the tree: its project graph and its installed packages. Replaced in tests. */
|
|
65
|
+
const workspace = {
|
|
66
|
+
graph: () => createProjectGraphAsync(),
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* What `pkg`, resolved from the directory `from`, is to an app, or undefined when it is not
|
|
70
|
+
* installed.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} pkg
|
|
73
|
+
* @param {string} from
|
|
74
|
+
* @returns {{ version: string, native: boolean, plugin: boolean, peers: string[], dir: string } | undefined}
|
|
75
|
+
*/
|
|
76
|
+
installed(pkg, from) {
|
|
77
|
+
const dir = packageDir(pkg, from);
|
|
78
|
+
if (!dir) return undefined;
|
|
79
|
+
const manifest = JSON.parse(readFileSync(nodePath.join(dir, 'package.json'), 'utf8'));
|
|
80
|
+
const meta = manifest.peerDependenciesMeta ?? {};
|
|
81
|
+
return {
|
|
82
|
+
version: manifest.version,
|
|
83
|
+
native: isNativeModule(dir),
|
|
84
|
+
plugin: existsSync(nodePath.join(dir, 'app.plugin.js')),
|
|
85
|
+
peers: Object.keys(manifest.peerDependencies ?? {}).filter((peer) => !meta[peer]?.optional),
|
|
86
|
+
dir,
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/** The config plugins Expo applies whether an app lists them or not, as `expo install` skips. */
|
|
91
|
+
autoPlugins(from) {
|
|
92
|
+
try {
|
|
93
|
+
const expo = packageDir('expo', from);
|
|
94
|
+
const config = require(require.resolve('@expo/prebuild-config', { paths: [expo ?? from] }));
|
|
95
|
+
return config.getAutoPlugins();
|
|
96
|
+
} catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** The projects the app `name` depends on, directly or through another, in the graph. */
|
|
103
|
+
function librariesOf(graph, name) {
|
|
104
|
+
const found = new Set();
|
|
105
|
+
const visit = (project) => {
|
|
106
|
+
for (const { target } of graph.dependencies[project] ?? []) {
|
|
107
|
+
if (!graph.nodes[target] || found.has(target) || target === name) continue;
|
|
108
|
+
found.add(target);
|
|
109
|
+
visit(target);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
visit(name);
|
|
113
|
+
return [...found];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The packages a project imports, by name. */
|
|
117
|
+
function importsOf(graph, project) {
|
|
118
|
+
return (graph.dependencies[project] ?? [])
|
|
119
|
+
.map(({ target }) => graph.externalNodes?.[target]?.data?.packageName)
|
|
120
|
+
.filter(Boolean);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Whether the project is an Angular Native app: an Expo app that runs on `@ng-native/platform`. */
|
|
124
|
+
function isNativeApp(tree, root) {
|
|
125
|
+
const manifest = joinPathFragments(root, 'package.json');
|
|
126
|
+
if (!tree.exists(joinPathFragments(root, 'app.json')) || !tree.exists(manifest)) return false;
|
|
127
|
+
const { dependencies, devDependencies } = readJson(tree, manifest);
|
|
128
|
+
return Boolean({ ...dependencies, ...devDependencies }['@ng-native/platform']);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function listed(manifest) {
|
|
132
|
+
return { ...manifest.devDependencies, ...manifest.dependencies };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The native modules the app `name` needs for its libraries, each with the library that brings it
|
|
137
|
+
* in, the range to list and whether it has a config plugin.
|
|
138
|
+
*/
|
|
139
|
+
function needed(tree, graph, name) {
|
|
140
|
+
const root = listed(readJson(tree, 'package.json'));
|
|
141
|
+
const modules = new Map();
|
|
142
|
+
for (const library of librariesOf(graph, name)) {
|
|
143
|
+
const libraryRoot = graph.nodes[library].data.root;
|
|
144
|
+
const from = nodePath.join(tree.root, libraryRoot);
|
|
145
|
+
const manifest = joinPathFragments(libraryRoot, 'package.json');
|
|
146
|
+
const own = tree.exists(manifest) ? listed(readJson(tree, manifest)) : {};
|
|
147
|
+
const add = (pkg, info, via) => {
|
|
148
|
+
if (!info?.native || modules.has(pkg)) return;
|
|
149
|
+
const range = root[pkg] ?? own[pkg] ?? info.version;
|
|
150
|
+
modules.set(pkg, { library: libraryRoot, via, range, plugin: info.plugin });
|
|
151
|
+
};
|
|
152
|
+
for (const pkg of importsOf(graph, library)) {
|
|
153
|
+
const info = workspace.installed(pkg, from);
|
|
154
|
+
add(pkg, info);
|
|
155
|
+
for (const peer of info?.peers ?? []) add(peer, workspace.installed(peer, info.dir), pkg);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return modules;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The names in an `app.json`'s `plugins`, each a name or a `[name, options]` pair. */
|
|
162
|
+
function pluginNames(plugins) {
|
|
163
|
+
return (plugins ?? []).map((plugin) => (Array.isArray(plugin) ? plugin[0] : plugin));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @param {import('@nx/devkit').Tree} tree
|
|
168
|
+
* @returns {Promise<{ outOfSyncMessage?: string, outOfSyncDetails?: string[] }>}
|
|
169
|
+
*/
|
|
170
|
+
async function syncNativeModules(tree) {
|
|
171
|
+
const graph = await workspace.graph();
|
|
172
|
+
const details = [];
|
|
173
|
+
for (const [name, project] of getProjects(tree)) {
|
|
174
|
+
if (!isNativeApp(tree, project.root) || !graph.nodes[name]) continue;
|
|
175
|
+
const manifestPath = joinPathFragments(project.root, 'package.json');
|
|
176
|
+
const has = listed(readJson(tree, manifestPath));
|
|
177
|
+
const all = [...needed(tree, graph, name)];
|
|
178
|
+
const modules = all.filter(([pkg]) => !(pkg in has));
|
|
179
|
+
if (modules.length > 0) {
|
|
180
|
+
updateJson(tree, manifestPath, (manifest) => {
|
|
181
|
+
manifest.dependencies ??= {};
|
|
182
|
+
for (const [pkg, { range }] of modules) manifest.dependencies[pkg] = range;
|
|
183
|
+
return manifest;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
for (const [pkg, { library, via }] of modules) {
|
|
187
|
+
const reason = via ? `${library} imports ${via}, which needs it` : `${library} imports it`;
|
|
188
|
+
details.push(`${project.root}/package.json needs ${pkg}: ${reason}.`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const appJson = joinPathFragments(project.root, 'app.json');
|
|
192
|
+
const auto = new Set(workspace.autoPlugins(nodePath.join(tree.root, project.root)));
|
|
193
|
+
const plugins = all.filter(([pkg, { plugin }]) => plugin && !auto.has(pkg)).map(([pkg]) => pkg);
|
|
194
|
+
const present = pluginNames(readJson(tree, appJson).expo?.plugins);
|
|
195
|
+
const missing = plugins.filter((pkg) => !present.includes(pkg));
|
|
196
|
+
if (missing.length > 0) {
|
|
197
|
+
updateJson(tree, appJson, (config) => {
|
|
198
|
+
config.expo ??= {};
|
|
199
|
+
config.expo.plugins = [...(config.expo.plugins ?? []), ...missing];
|
|
200
|
+
return config;
|
|
201
|
+
});
|
|
202
|
+
for (const pkg of missing) details.push(`${appJson} needs the config plugin of ${pkg}.`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (details.length === 0) return {};
|
|
206
|
+
// As the app generator wrote them, rather than as JSON.stringify does.
|
|
207
|
+
await formatFiles(tree);
|
|
208
|
+
return {
|
|
209
|
+
outOfSyncMessage:
|
|
210
|
+
'An app does not list a native module its libraries import, so a development build would not link it.',
|
|
211
|
+
outOfSyncDetails: details,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Registers the sync generator on the app's `start`, `export` and `prebuild` targets. `export` and
|
|
217
|
+
* `prebuild` are `@nx/expo`'s, inferred, and take the option from `project.json` beside the rest.
|
|
218
|
+
*
|
|
219
|
+
* @param {import('@nx/devkit').Tree} tree
|
|
220
|
+
* @param {string} projectName
|
|
221
|
+
*/
|
|
222
|
+
function registerSyncGenerator(tree, projectName) {
|
|
223
|
+
const project = readProjectConfiguration(tree, projectName);
|
|
224
|
+
const targets = { ...project.targets };
|
|
225
|
+
for (const target of ['start', 'export', 'prebuild']) {
|
|
226
|
+
const syncGenerators = targets[target]?.syncGenerators ?? [];
|
|
227
|
+
if (syncGenerators.includes(SYNC_GENERATOR)) continue;
|
|
228
|
+
targets[target] = { ...targets[target], syncGenerators: [...syncGenerators, SYNC_GENERATOR] };
|
|
229
|
+
}
|
|
230
|
+
updateProjectConfiguration(tree, projectName, { ...project, targets });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
module.exports = { syncNativeModules, registerSyncGenerator, workspace, SYNC_GENERATOR };
|
package/sync/schema.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/schema",
|
|
3
|
+
"$id": "NgNativeNxSyncNativeModules",
|
|
4
|
+
"title": "Sync an app's native modules",
|
|
5
|
+
"description": "Lists the native modules an app's libraries import in the app's package.json, with their config plugins in app.json. A sync generator.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {}
|
|
8
|
+
}
|