@ng-native/testing 0.1.2 → 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.
- package/README.md +1 -1
- package/dist/test-utils.js +7 -1
- package/dist/test-utils.js.map +1 -1
- package/package.json +6 -6
- package/runner/vitest.mjs +73 -3
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ have been sent.
|
|
|
62
62
|
[writing a test](https://ng-native.com/packages/testing/writing-a-test) and the
|
|
63
63
|
[API reference](https://ng-native.com/packages/testing/api)
|
|
64
64
|
- [Root README](https://github.com/ng-native/ng-native/blob/main/README.md) and
|
|
65
|
-
[ARCHITECTURE.md](https://github.com/ng-native/ng-native/blob/main/ARCHITECTURE.md)
|
|
65
|
+
[ARCHITECTURE.md](https://github.com/ng-native/ng-native/blob/main/docs/ARCHITECTURE.md)
|
|
66
66
|
|
|
67
67
|
## License
|
|
68
68
|
|
package/dist/test-utils.js
CHANGED
|
@@ -52,10 +52,16 @@ export function createFakeFabric() {
|
|
|
52
52
|
}, source.instanceHandle);
|
|
53
53
|
};
|
|
54
54
|
const node = (reactTag, viewName, props, instanceHandle) => withHandle({ handle: ++handle, reactTag, viewName, props: { ...props }, children: [] }, instanceHandle);
|
|
55
|
+
// Keys sorted at every depth, so a golden does not depend on the order props were set in. An
|
|
56
|
+
// array of keys as the replacer would sort too, but it is an allow-list for nested objects as
|
|
57
|
+
// well, and printed `accessibilityState: { disabled: true }` as `{}`.
|
|
58
|
+
const sorted = (_key, value) => value && typeof value === 'object' && !Array.isArray(value)
|
|
59
|
+
? Object.fromEntries(Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))
|
|
60
|
+
: value;
|
|
55
61
|
const walk = (nodes, depth, withProps) => nodes.flatMap((n) => {
|
|
56
62
|
const text = typeof n.props['text'] === 'string' ? ` "${n.props['text']}"` : '';
|
|
57
63
|
const props = withProps && Object.keys(n.props).length && !text
|
|
58
|
-
? ' ' + JSON.stringify(n.props,
|
|
64
|
+
? ' ' + JSON.stringify(n.props, sorted)
|
|
59
65
|
: '';
|
|
60
66
|
return [
|
|
61
67
|
' '.repeat(depth) + n.viewName + text + props,
|
package/dist/test-utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-utils.js","sourceRoot":"","sources":["../src/test-utils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Fake `global.nativeFabricUIManager`. Records every host call so tests can assert on\n * the committed tree without a simulator. This is what lets the whole seam be tested in Node.\n */\nimport type { FabricNode, FabricNodeSet, FabricUIManager } from '@ng-native/fabric';\n\nexport interface FakeFabricNode {\n handle: number;\n reactTag: number;\n viewName: string;\n props: Record<string, unknown>;\n children: FakeFabricNode[];\n instanceHandle: unknown;\n}\n\nexport interface FakeFabric extends FabricUIManager {\n /** Root child set from the most recent `completeRoot`. */\n readonly committed: FakeFabricNode[];\n readonly calls: {\n createNode: number;\n cloneWithChildren: number;\n cloneWithProps: number;\n cloneWithChildrenAndProps: number;\n completeRoot: number;\n };\n /**\n * Indented view-name tree, for golden assertions. `props: true` includes the flattened\n * prop payload, so a parity test catches prop regressions as well as shape ones.\n */\n render(options?: { props?: boolean }): string;\n find(viewName: string): FakeFabricNode | undefined;\n /** Drive the registered Fabric event handler, as the C++ side would. */\n emit(target: FakeFabricNode, topLevelType: string, nativeEvent?: unknown): void;\n /** Every `setIsJSResponder` the renderer made, in order. */\n readonly responderCalls: { viewName: string; isResponder: boolean; block: boolean }[];\n /**\n * Every `dispatchCommand`, in order. `node` is the node it was sent to, as a non-enumerable\n * property: there to read, and left out of a `deepEqual`.\n */\n readonly commands: {\n viewName: string;\n name: string;\n args: readonly unknown[];\n readonly node?: FakeFabricNode;\n }[];\n /**\n * What `measureInWindow` answers, keyed by `nativeID` and falling back to view name.\n *\n * A test sets the frames it cares about; a node with no entry is not measured at all, which is\n * what the real thing does for a view the platform has not laid out. The two keys are for the\n * two shapes a test comes in: one candidate on screen, where the view name says all there is to\n * say, and several, where which one got measured is the whole question.\n */\n readonly frames: Map<string, { x: number; y: number; width: number; height: number }>;\n reset(): void;\n}\n\n/**\n * Keep the instance handle off the node's enumerable props. It is the engine's own node, which\n * reaches the whole application, and a node a query returns ends up printed in any failed\n * assertion about it: Node's inspector and Vitest's formatter both walk enumerable keys only.\n */\nfunction withHandle(node: Omit<FakeFabricNode, 'instanceHandle'>, instanceHandle: unknown) {\n Object.defineProperty(node, 'instanceHandle', {\n value: instanceHandle,\n enumerable: false,\n writable: true,\n });\n return node as FakeFabricNode;\n}\n\nexport function createFakeFabric(): FakeFabric {\n let handle = 0;\n let committed: FakeFabricNode[] = [];\n let handler: ((h: unknown, t: string, e: unknown) => void) | null = null;\n const responderCalls: { viewName: string; isResponder: boolean; block: boolean }[] = [];\n const commands: { viewName: string; name: string; args: readonly unknown[] }[] = [];\n const frames = new Map<string, { x: number; y: number; width: number; height: number }>();\n const calls = {\n createNode: 0,\n cloneWithChildren: 0,\n cloneWithProps: 0,\n cloneWithChildrenAndProps: 0,\n completeRoot: 0,\n };\n\n /*\n * The parent each view has been appended to, by tag. Native keeps one parent per view for as\n * long as it lives - `ShadowNodeFamily::setParent` asserts it, in a debug build, and a release\n * build goes on with the old parent and measures and lays out against the wrong ancestry - so a\n * view that moves has to be created again, as React does. This fails the test that tries.\n */\n const parents = new Map<number, number>();\n const ROOT = -1;\n const adopt = (parent: number, child: FakeFabricNode): void => {\n const was = parents.get(child.reactTag);\n if (was !== undefined && was !== parent) {\n throw new Error(\n `${child.viewName} ${child.reactTag} was appended to ${parent} after ${was}: native ` +\n 'never re-parents a view, and asserts on it. Create it again instead.',\n );\n }\n parents.set(child.reactTag, parent);\n };\n\n const clone = (n: FabricNode, props?: object, keepChildren = false): FabricNode => {\n const source = n as unknown as FakeFabricNode;\n return withHandle(\n {\n ...source,\n handle: ++handle,\n // Fabric merges raw props onto the source props rather than replacing them.\n props: props ? { ...source.props, ...props } : source.props,\n children: keepChildren ? source.children : [],\n },\n source.instanceHandle,\n ) as unknown as FabricNode;\n };\n\n const node = (\n reactTag: number,\n viewName: string,\n props: object,\n instanceHandle: unknown,\n ): FakeFabricNode =>\n withHandle(\n { handle: ++handle, reactTag, viewName, props: { ...props }, children: [] },\n instanceHandle,\n );\n\n const walk = (nodes: FakeFabricNode[], depth: number, withProps: boolean): string[] =>\n nodes.flatMap((n) => {\n const text = typeof n.props['text'] === 'string' ? ` \"${n.props['text']}\"` : '';\n const props =\n withProps && Object.keys(n.props).length && !text\n ? ' ' + JSON.stringify(n.props, Object.keys(n.props).sort())\n : '';\n return [\n ' '.repeat(depth) + n.viewName + text + props,\n ...walk(n.children, depth + 1, withProps),\n ];\n });\n\n const fake: FakeFabric = {\n createNode(reactTag, viewName, _rootTag, props, instanceHandle) {\n calls.createNode++;\n return node(reactTag, viewName, props, instanceHandle) as unknown as FabricNode;\n },\n cloneNodeWithNewChildren(n) {\n calls.cloneWithChildren++;\n return clone(n);\n },\n cloneNodeWithNewProps(n, newProps) {\n calls.cloneWithProps++;\n return clone(n, newProps, true);\n },\n cloneNodeWithNewChildrenAndProps(n, newProps) {\n calls.cloneWithChildrenAndProps++;\n return clone(n, newProps);\n },\n appendChild(parent, child) {\n const into = parent as unknown as FakeFabricNode;\n adopt(into.reactTag, child as unknown as FakeFabricNode);\n into.children.push(child as unknown as FakeFabricNode);\n return parent;\n },\n createChildSet() {\n return [] as FabricNodeSet;\n },\n appendChildToSet(set, child) {\n adopt(ROOT, child as unknown as FakeFabricNode);\n (set as FakeFabricNode[]).push(child as unknown as FakeFabricNode);\n },\n completeRoot(_rootTag, set) {\n calls.completeRoot++;\n committed = set as FakeFabricNode[];\n },\n registerEventHandler(fn) {\n handler = fn;\n },\n setIsJSResponder(node, isResponder, block) {\n responderCalls.push({\n viewName: (node as unknown as FakeFabricNode).viewName,\n isResponder,\n block,\n });\n },\n measureInWindow(node, onSuccess) {\n // By `nativeID` first, so a test that cares which of two views was measured can say so; by\n // view name otherwise, which is enough when there is only one candidate on screen.\n const fake = node as unknown as FakeFabricNode;\n const frame = frames.get(String(fake.props['nativeID'])) ?? frames.get(fake.viewName);\n if (frame) onSuccess(frame.x, frame.y, frame.width, frame.height);\n },\n dispatchCommand(node, name, args) {\n const command = { viewName: (node as unknown as FakeFabricNode).viewName, name, args };\n // Which node, for a test that has several of one view; off the enumerable keys, so a\n // `deepEqual` against `{ viewName, name, args }` still holds.\n Object.defineProperty(command, 'node', { value: node, enumerable: false });\n commands.push(command as typeof command & { node: FakeFabricNode });\n },\n responderCalls,\n commands,\n frames,\n get committed() {\n return committed;\n },\n calls,\n render(options) {\n return walk(committed, 0, options?.props === true).join('\\n');\n },\n find(viewName) {\n const search = (nodes: FakeFabricNode[]): FakeFabricNode | undefined => {\n for (const n of nodes) {\n if (n.viewName === viewName) return n;\n const hit = search(n.children);\n if (hit) return hit;\n }\n return undefined;\n };\n return search(committed);\n },\n emit(target, type, nativeEvent = {}) {\n if (!handler) throw new Error('no Fabric event handler registered');\n handler(target.instanceHandle, type, nativeEvent);\n },\n reset() {\n calls.createNode = 0;\n calls.cloneWithChildren = 0;\n calls.cloneWithProps = 0;\n calls.cloneWithChildrenAndProps = 0;\n calls.completeRoot = 0;\n },\n };\n\n return fake;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"test-utils.js","sourceRoot":"","sources":["../src/test-utils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Fake `global.nativeFabricUIManager`. Records every host call so tests can assert on\n * the committed tree without a simulator. This is what lets the whole seam be tested in Node.\n */\nimport type { FabricNode, FabricNodeSet, FabricUIManager } from '@ng-native/fabric';\n\nexport interface FakeFabricNode {\n handle: number;\n reactTag: number;\n viewName: string;\n props: Record<string, unknown>;\n children: FakeFabricNode[];\n instanceHandle: unknown;\n}\n\nexport interface FakeFabric extends FabricUIManager {\n /** Root child set from the most recent `completeRoot`. */\n readonly committed: FakeFabricNode[];\n readonly calls: {\n createNode: number;\n cloneWithChildren: number;\n cloneWithProps: number;\n cloneWithChildrenAndProps: number;\n completeRoot: number;\n };\n /**\n * Indented view-name tree, for golden assertions. `props: true` includes the flattened\n * prop payload, so a parity test catches prop regressions as well as shape ones.\n */\n render(options?: { props?: boolean }): string;\n find(viewName: string): FakeFabricNode | undefined;\n /** Drive the registered Fabric event handler, as the C++ side would. */\n emit(target: FakeFabricNode, topLevelType: string, nativeEvent?: unknown): void;\n /** Every `setIsJSResponder` the renderer made, in order. */\n readonly responderCalls: { viewName: string; isResponder: boolean; block: boolean }[];\n /**\n * Every `dispatchCommand`, in order. `node` is the node it was sent to, as a non-enumerable\n * property: there to read, and left out of a `deepEqual`.\n */\n readonly commands: {\n viewName: string;\n name: string;\n args: readonly unknown[];\n readonly node?: FakeFabricNode;\n }[];\n /**\n * What `measureInWindow` answers, keyed by `nativeID` and falling back to view name.\n *\n * A test sets the frames it cares about; a node with no entry is not measured at all, which is\n * what the real thing does for a view the platform has not laid out. The two keys are for the\n * two shapes a test comes in: one candidate on screen, where the view name says all there is to\n * say, and several, where which one got measured is the whole question.\n */\n readonly frames: Map<string, { x: number; y: number; width: number; height: number }>;\n reset(): void;\n}\n\n/**\n * Keep the instance handle off the node's enumerable props. It is the engine's own node, which\n * reaches the whole application, and a node a query returns ends up printed in any failed\n * assertion about it: Node's inspector and Vitest's formatter both walk enumerable keys only.\n */\nfunction withHandle(node: Omit<FakeFabricNode, 'instanceHandle'>, instanceHandle: unknown) {\n Object.defineProperty(node, 'instanceHandle', {\n value: instanceHandle,\n enumerable: false,\n writable: true,\n });\n return node as FakeFabricNode;\n}\n\nexport function createFakeFabric(): FakeFabric {\n let handle = 0;\n let committed: FakeFabricNode[] = [];\n let handler: ((h: unknown, t: string, e: unknown) => void) | null = null;\n const responderCalls: { viewName: string; isResponder: boolean; block: boolean }[] = [];\n const commands: { viewName: string; name: string; args: readonly unknown[] }[] = [];\n const frames = new Map<string, { x: number; y: number; width: number; height: number }>();\n const calls = {\n createNode: 0,\n cloneWithChildren: 0,\n cloneWithProps: 0,\n cloneWithChildrenAndProps: 0,\n completeRoot: 0,\n };\n\n /*\n * The parent each view has been appended to, by tag. Native keeps one parent per view for as\n * long as it lives - `ShadowNodeFamily::setParent` asserts it, in a debug build, and a release\n * build goes on with the old parent and measures and lays out against the wrong ancestry - so a\n * view that moves has to be created again, as React does. This fails the test that tries.\n */\n const parents = new Map<number, number>();\n const ROOT = -1;\n const adopt = (parent: number, child: FakeFabricNode): void => {\n const was = parents.get(child.reactTag);\n if (was !== undefined && was !== parent) {\n throw new Error(\n `${child.viewName} ${child.reactTag} was appended to ${parent} after ${was}: native ` +\n 'never re-parents a view, and asserts on it. Create it again instead.',\n );\n }\n parents.set(child.reactTag, parent);\n };\n\n const clone = (n: FabricNode, props?: object, keepChildren = false): FabricNode => {\n const source = n as unknown as FakeFabricNode;\n return withHandle(\n {\n ...source,\n handle: ++handle,\n // Fabric merges raw props onto the source props rather than replacing them.\n props: props ? { ...source.props, ...props } : source.props,\n children: keepChildren ? source.children : [],\n },\n source.instanceHandle,\n ) as unknown as FabricNode;\n };\n\n const node = (\n reactTag: number,\n viewName: string,\n props: object,\n instanceHandle: unknown,\n ): FakeFabricNode =>\n withHandle(\n { handle: ++handle, reactTag, viewName, props: { ...props }, children: [] },\n instanceHandle,\n );\n\n // Keys sorted at every depth, so a golden does not depend on the order props were set in. An\n // array of keys as the replacer would sort too, but it is an allow-list for nested objects as\n // well, and printed `accessibilityState: { disabled: true }` as `{}`.\n const sorted = (_key: string, value: unknown): unknown =>\n value && typeof value === 'object' && !Array.isArray(value)\n ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))\n : value;\n\n const walk = (nodes: FakeFabricNode[], depth: number, withProps: boolean): string[] =>\n nodes.flatMap((n) => {\n const text = typeof n.props['text'] === 'string' ? ` \"${n.props['text']}\"` : '';\n const props =\n withProps && Object.keys(n.props).length && !text\n ? ' ' + JSON.stringify(n.props, sorted)\n : '';\n return [\n ' '.repeat(depth) + n.viewName + text + props,\n ...walk(n.children, depth + 1, withProps),\n ];\n });\n\n const fake: FakeFabric = {\n createNode(reactTag, viewName, _rootTag, props, instanceHandle) {\n calls.createNode++;\n return node(reactTag, viewName, props, instanceHandle) as unknown as FabricNode;\n },\n cloneNodeWithNewChildren(n) {\n calls.cloneWithChildren++;\n return clone(n);\n },\n cloneNodeWithNewProps(n, newProps) {\n calls.cloneWithProps++;\n return clone(n, newProps, true);\n },\n cloneNodeWithNewChildrenAndProps(n, newProps) {\n calls.cloneWithChildrenAndProps++;\n return clone(n, newProps);\n },\n appendChild(parent, child) {\n const into = parent as unknown as FakeFabricNode;\n adopt(into.reactTag, child as unknown as FakeFabricNode);\n into.children.push(child as unknown as FakeFabricNode);\n return parent;\n },\n createChildSet() {\n return [] as FabricNodeSet;\n },\n appendChildToSet(set, child) {\n adopt(ROOT, child as unknown as FakeFabricNode);\n (set as FakeFabricNode[]).push(child as unknown as FakeFabricNode);\n },\n completeRoot(_rootTag, set) {\n calls.completeRoot++;\n committed = set as FakeFabricNode[];\n },\n registerEventHandler(fn) {\n handler = fn;\n },\n setIsJSResponder(node, isResponder, block) {\n responderCalls.push({\n viewName: (node as unknown as FakeFabricNode).viewName,\n isResponder,\n block,\n });\n },\n measureInWindow(node, onSuccess) {\n // By `nativeID` first, so a test that cares which of two views was measured can say so; by\n // view name otherwise, which is enough when there is only one candidate on screen.\n const fake = node as unknown as FakeFabricNode;\n const frame = frames.get(String(fake.props['nativeID'])) ?? frames.get(fake.viewName);\n if (frame) onSuccess(frame.x, frame.y, frame.width, frame.height);\n },\n dispatchCommand(node, name, args) {\n const command = { viewName: (node as unknown as FakeFabricNode).viewName, name, args };\n // Which node, for a test that has several of one view; off the enumerable keys, so a\n // `deepEqual` against `{ viewName, name, args }` still holds.\n Object.defineProperty(command, 'node', { value: node, enumerable: false });\n commands.push(command as typeof command & { node: FakeFabricNode });\n },\n responderCalls,\n commands,\n frames,\n get committed() {\n return committed;\n },\n calls,\n render(options) {\n return walk(committed, 0, options?.props === true).join('\\n');\n },\n find(viewName) {\n const search = (nodes: FakeFabricNode[]): FakeFabricNode | undefined => {\n for (const n of nodes) {\n if (n.viewName === viewName) return n;\n const hit = search(n.children);\n if (hit) return hit;\n }\n return undefined;\n };\n return search(committed);\n },\n emit(target, type, nativeEvent = {}) {\n if (!handler) throw new Error('no Fabric event handler registered');\n handler(target.instanceHandle, type, nativeEvent);\n },\n reset() {\n calls.createNode = 0;\n calls.cloneWithChildren = 0;\n calls.cloneWithProps = 0;\n calls.cloneWithChildrenAndProps = 0;\n calls.completeRoot = 0;\n },\n };\n\n return fake;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ng-native/testing",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
38
|
-
"@ng-native/fabric": "0.
|
|
39
|
-
"@ng-native/platform": "0.
|
|
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/
|
|
67
|
-
"@ng-native/
|
|
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
|
-
*
|
|
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
|
-
|
|
127
|
-
|
|
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];
|