@ng-native/testing 0.1.3 → 0.2.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.
Files changed (2) hide show
  1. package/package.json +6 -6
  2. package/runner/vitest.mjs +73 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ng-native/testing",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Test Angular Native components in Node with no simulator: a fake Fabric, Testing Library queries and events, and Vitest and node:test integration.",
5
5
  "keywords": [
6
6
  "angular",
@@ -34,9 +34,9 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@ng-native/metro": "0.1.3",
38
- "@ng-native/platform": "0.1.3",
39
- "@ng-native/fabric": "0.1.3"
37
+ "@ng-native/metro": "0.2.0",
38
+ "@ng-native/fabric": "0.2.0",
39
+ "@ng-native/platform": "0.2.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@angular/compiler": "^22.0.0",
@@ -63,8 +63,8 @@
63
63
  "@angular/router": "22.2.0",
64
64
  "vite": "^8.3.1",
65
65
  "vitest": "^5.0.2",
66
- "@ng-native/components": "0.1.3",
67
- "@ng-native/router": "0.1.3"
66
+ "@ng-native/router": "0.2.0",
67
+ "@ng-native/components": "0.2.0"
68
68
  },
69
69
  "nx": {
70
70
  "tags": [
package/runner/vitest.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `ngNative()`: the Vite plugin that lets Vitest run Angular Native code in Node, with no DOM.
3
3
  *
4
- * Four things, each of which Vitest would otherwise get wrong on its own:
4
+ * Five things, each of which Vitest would otherwise get wrong on its own:
5
5
  *
6
6
  * - It compiles. Every decorated `.ts` goes through `@ng-native/metro`'s AOT transform and every
7
7
  * partial-compiled package through the linker (see `compile.mjs`), before Vite strips types.
@@ -13,6 +13,9 @@
13
13
  * - It stubs assets. With no `require`, an app's `require('./logo.png')` would throw; it becomes
14
14
  * `{ testUri }` instead. And it stands in for the gesture and animation entry points, whose
15
15
  * React Native source Node cannot load (`STAND_INS`).
16
+ * - It resolves as Metro does. A workspace library's copy of a package at the app's version
17
+ * resolves to the app's copy (`appCopy`), so a test has one `@ng-native/components`, as the
18
+ * bundle does.
16
19
  *
17
20
  * The environment is Vitest's default, `node`. There is no DOM to emulate: the renderer talks to
18
21
  * a fake Fabric, and jsdom would only give Angular a `document` to misread.
@@ -100,11 +103,74 @@ function hideRequire(code, file) {
100
103
  return `var require = undefined; ${code}`;
101
104
  }
102
105
 
106
+ /** @param {string} dir */
107
+ function manifest(dir) {
108
+ try {
109
+ return JSON.parse(readFileSync(path.join(dir, 'package.json'), 'utf8'));
110
+ } catch {
111
+ return undefined;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * The directory of the `name` package a resolved file is in, or nothing: installed, or a linked
117
+ * workspace package, whose real path has no `node_modules` in it.
118
+ *
119
+ * @param {string} id
120
+ * @param {string} name
121
+ */
122
+ function packageRoot(id, name) {
123
+ const file = id.split('?')[0];
124
+ const marker = `/node_modules/${name}/`;
125
+ const at = file.lastIndexOf(marker);
126
+ if (at !== -1) return file.slice(0, at + marker.length - 1);
127
+ if (!path.isAbsolute(file)) return undefined;
128
+ for (let dir = path.dirname(file); dir !== path.dirname(dir); dir = path.dirname(dir)) {
129
+ // A package's dist can hold a package.json of its own with only a `type` in it.
130
+ const found = manifest(dir)?.name;
131
+ if (found) return found === name ? dir : undefined;
132
+ }
133
+ return undefined;
134
+ }
135
+
136
+ /**
137
+ * The app's copy of a package, when a bare import resolved to another copy at the same version:
138
+ * the rule the Metro preset applies to a bundle, so a test loads what the device does.
139
+ *
140
+ * pnpm installs a package once per set of peers it resolves, so a workspace library that lists
141
+ * `@ng-native/components` gets a copy of its own at the app's version when it was installed after
142
+ * the app. Loaded from the library, that is a second component registry. A copy at another
143
+ * version is the library's own choice, and stays.
144
+ *
145
+ * @param {import('vite').Rollup.PluginContext} context
146
+ * @param {string} source
147
+ * @param {string} importer
148
+ * @param {object} options what Vite passed to `resolveId`
149
+ * @param {string} root the app's directory
150
+ */
151
+ async function appCopy(context, source, importer, options, root) {
152
+ const resolution = await context.resolve(source, importer, { ...options, skipSelf: true });
153
+ const pkg = /^(?![./\0])(@[^/]+\/[^/]+|[^/]+)/.exec(source)?.[1];
154
+ const own = pkg && resolution && !resolution.external && packageRoot(resolution.id, pkg);
155
+ if (!own) return resolution;
156
+ // The app's copy can fail where the library's resolved: another version that does not export
157
+ // the subpath. The library's copy is then its own choice.
158
+ const app = await context
159
+ .resolve(source, path.join(root, 'package.json'), { ...options, skipSelf: true })
160
+ .catch(() => null);
161
+ const appRoot = app && !app.external && packageRoot(app.id, pkg);
162
+ const version = (/** @type {string} */ dir) => manifest(dir)?.version;
163
+ return appRoot && appRoot !== own && version(appRoot) && version(appRoot) === version(own)
164
+ ? app
165
+ : resolution;
166
+ }
167
+
103
168
  /**
104
169
  * @param {{ inline?: (string | RegExp)[] }} [options]
105
170
  * @returns {import('vitest/config').Plugin}
106
171
  */
107
172
  export function ngNative(options = {}) {
173
+ let root = process.cwd();
108
174
  return {
109
175
  name: 'ng-native',
110
176
  enforce: 'pre',
@@ -123,8 +189,12 @@ export function ngNative(options = {}) {
123
189
  server: { deps: { inline: [...INLINE, ...(options.inline ?? [])] } },
124
190
  },
125
191
  }),
126
- resolveId(source) {
127
- return Object.hasOwn(STAND_INS, source) ? STAND_INS[source] : null;
192
+ configResolved(config) {
193
+ root = config.root;
194
+ },
195
+ resolveId(source, importer, resolveOptions) {
196
+ if (Object.hasOwn(STAND_INS, source)) return STAND_INS[source];
197
+ return importer ? appCopy(this, source, importer, resolveOptions, root) : null;
128
198
  },
129
199
  transform(source, id) {
130
200
  const file = id.split('?')[0];