@nativedesktop/cli 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/package.json +2 -2
- package/src/package/modules.test.ts +22 -0
- package/src/package/modules.ts +42 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nativedesktop/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "The nd CLI: dev-runs, builds, and packages NativeDesktop apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,6 +25,6 @@
|
|
|
25
25
|
"./config": "./src/config.ts"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@nativedesktop/host": "0.
|
|
28
|
+
"@nativedesktop/host": "0.2.0"
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -142,4 +142,26 @@ describe("assertResolvableEntries", () => {
|
|
|
142
142
|
writeFileSync(join(carrier, "package.json"), JSON.stringify({ name: "host-binary", files: ["bin"] }));
|
|
143
143
|
expect(() => assertResolvableEntries(root, ["host-binary"])).not.toThrow();
|
|
144
144
|
});
|
|
145
|
+
|
|
146
|
+
test("tolerates subpath-only exports (@nativedesktop/native's shape)", () => {
|
|
147
|
+
const root = mkdtempSync(join(tmpdir(), "nd-modules-"));
|
|
148
|
+
const headers = join(root, "node_modules", "header-carrier");
|
|
149
|
+
mkdirSync(headers, { recursive: true });
|
|
150
|
+
writeFileSync(
|
|
151
|
+
join(headers, "package.json"),
|
|
152
|
+
JSON.stringify({ name: "header-carrier", exports: { "./include/nd.h": "./include/nd.h" } }),
|
|
153
|
+
);
|
|
154
|
+
expect(() => assertResolvableEntries(root, ["header-carrier"])).not.toThrow();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("still throws when exports declares a root entry that is missing", () => {
|
|
158
|
+
const root = mkdtempSync(join(tmpdir(), "nd-modules-"));
|
|
159
|
+
const broken = join(root, "node_modules", "needs-build-exports");
|
|
160
|
+
mkdirSync(broken, { recursive: true });
|
|
161
|
+
writeFileSync(
|
|
162
|
+
join(broken, "package.json"),
|
|
163
|
+
JSON.stringify({ name: "needs-build-exports", exports: { ".": "./dist/index.js" } }),
|
|
164
|
+
);
|
|
165
|
+
expect(() => assertResolvableEntries(root, ["needs-build-exports"])).toThrow("bun run build");
|
|
166
|
+
});
|
|
145
167
|
});
|
package/src/package/modules.ts
CHANGED
|
@@ -10,9 +10,14 @@ interface QueueEntry {
|
|
|
10
10
|
spec: string;
|
|
11
11
|
/** Directory the spec resolves from (the requiring package's real root). */
|
|
12
12
|
anchor: string;
|
|
13
|
+
/** Second anchor to try, used for peers whose consumer does not supply one. */
|
|
14
|
+
fallbackAnchor?: string;
|
|
13
15
|
/** Dest-relative dir of the requiring package, for version-conflict nesting. */
|
|
14
16
|
requirer?: string;
|
|
15
17
|
optional: boolean;
|
|
18
|
+
/** A peerDependency: the CONSUMER's copy is the correct one, so an existing
|
|
19
|
+
* flat claim wins outright instead of nesting a second copy. */
|
|
20
|
+
peer?: boolean;
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
const skipNodeModules = (src: string) => basename(src) !== "node_modules";
|
|
@@ -63,12 +68,24 @@ export function flattenRuntimeModules({ appDir, extraRoots = [], dest }: Flatten
|
|
|
63
68
|
|
|
64
69
|
while (queue.length) {
|
|
65
70
|
const entry = queue.shift()!;
|
|
71
|
+
// A peer the app already provides is settled: taking the dependency's own
|
|
72
|
+
// copy instead is what puts two reacts in one bundle.
|
|
73
|
+
if (entry.peer && claimed.has(entry.spec)) continue;
|
|
66
74
|
let root: string;
|
|
67
75
|
try {
|
|
68
76
|
root = packageRoot(entry.spec, entry.anchor);
|
|
69
77
|
} catch (err) {
|
|
70
|
-
if (entry.
|
|
71
|
-
|
|
78
|
+
if (entry.fallbackAnchor) {
|
|
79
|
+
try {
|
|
80
|
+
root = packageRoot(entry.spec, entry.fallbackAnchor);
|
|
81
|
+
} catch {
|
|
82
|
+
if (entry.optional) continue;
|
|
83
|
+
throw new Error(`nd: cannot resolve runtime dependency "${entry.spec}" from ${entry.anchor}: ${err}`);
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
if (entry.optional) continue;
|
|
87
|
+
throw new Error(`nd: cannot resolve runtime dependency "${entry.spec}" from ${entry.anchor}: ${err}`);
|
|
88
|
+
}
|
|
72
89
|
}
|
|
73
90
|
|
|
74
91
|
const existing = claimed.get(entry.spec);
|
|
@@ -94,14 +111,35 @@ export function flattenRuntimeModules({ appDir, extraRoots = [], dest }: Flatten
|
|
|
94
111
|
for (const spec of Object.keys(pkg.dependencies ?? {})) {
|
|
95
112
|
queue.push({ spec, anchor: root, requirer: destDir, optional: false });
|
|
96
113
|
}
|
|
97
|
-
for (const spec of
|
|
114
|
+
for (const spec of Object.keys(pkg.optionalDependencies ?? {})) {
|
|
98
115
|
queue.push({ spec, anchor: root, requirer: destDir, optional: true });
|
|
99
116
|
}
|
|
117
|
+
// Peers resolve from the APP, not from the package that declares them. A
|
|
118
|
+
// `file:` linked dependency living outside the app tree resolves its own
|
|
119
|
+
// peer to its own copy, and a second react in the bundle is the classic
|
|
120
|
+
// "Invalid hook call" at launch. The package's root is only the fallback,
|
|
121
|
+
// for a peer the app does not provide at all.
|
|
122
|
+
for (const spec of Object.keys(pkg.peerDependencies ?? {})) {
|
|
123
|
+
queue.push({ spec, anchor, fallbackAnchor: root, requirer: destDir, optional: true, peer: true });
|
|
124
|
+
}
|
|
100
125
|
}
|
|
101
126
|
|
|
102
127
|
return [...claimed.keys()];
|
|
103
128
|
}
|
|
104
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Whether `exports` offers the package's ROOT specifier. A string is one; an
|
|
132
|
+
* object is one only through a `"."` key or the conditions shorthand (keys that
|
|
133
|
+
* are not subpaths). A subpath-only map (`{"./include/nd.h": …}`, which is what
|
|
134
|
+
* `@nativedesktop/native` publishes) declares no root entry, so failing to
|
|
135
|
+
* resolve the bare name is correct rather than a missing build.
|
|
136
|
+
*/
|
|
137
|
+
function declaresRootEntry(exports: unknown): boolean {
|
|
138
|
+
if (typeof exports === "string") return true;
|
|
139
|
+
if (exports === null || typeof exports !== "object") return false;
|
|
140
|
+
return Object.keys(exports).some((key) => key === "." || !key.startsWith("."));
|
|
141
|
+
}
|
|
142
|
+
|
|
105
143
|
/**
|
|
106
144
|
* Asserts each flat package's main/exports entry resolves inside the bundle.
|
|
107
145
|
* Catches a copied-but-unbuilt package (e.g. @nativedesktop/react without its
|
|
@@ -119,7 +157,7 @@ export function assertResolvableEntries(appRoot: string, names: string[]): void
|
|
|
119
157
|
module?: string;
|
|
120
158
|
exports?: unknown;
|
|
121
159
|
};
|
|
122
|
-
if (pkg.main === undefined && pkg.module === undefined && pkg.exports
|
|
160
|
+
if (pkg.main === undefined && pkg.module === undefined && !declaresRootEntry(pkg.exports)) continue;
|
|
123
161
|
throw new Error(
|
|
124
162
|
`nd: bundled package "${name}" has no resolvable entry (its build output is missing). ` +
|
|
125
163
|
`Run \`bun run build\` in ${name} and package again.`,
|