@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nativedesktop/cli",
3
- "version": "0.1.2",
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.1.2"
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
  });
@@ -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.optional) continue;
71
- throw new Error(`nd: cannot resolve runtime dependency "${entry.spec}" from ${entry.anchor}: ${err}`);
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 [...Object.keys(pkg.optionalDependencies ?? {}), ...Object.keys(pkg.peerDependencies ?? {})]) {
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 === undefined) continue;
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.`,