nativescript 9.1.0-alpha.12 → 9.1.0-alpha.14

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.
@@ -56,10 +56,15 @@ class BundlerCompilerService extends events_1.EventEmitter {
56
56
  // HMR updates from. No-op unless bundler is vite + hmr + watch.
57
57
  // Fired in parallel with the build watcher; both child processes
58
58
  // inherit the adb-reverse env the run-controller set before
59
- // prepare, so neither one spawns adb on its own. Intentionally not
60
- // awaited — the device only connects to it at app launch, well
61
- // after the first build.
62
- this.startViteDevServer(platformData, projectData, prepareData);
59
+ // prepare, so neither one spawns adb on its own. Not awaited HERE
60
+ // — but the first-build resolution below gates on it, because the
61
+ // app is (re)started as soon as `compileWithWatch` resolves and its
62
+ // very first HTTP module fetch dies on connection-refused if the
63
+ // server hasn't bound yet (a cold `vite serve` config load + vendor
64
+ // prebuild competes with this build watcher for CPU and can take
65
+ // 10-30s). Running both in parallel keeps the happy-path wall time
66
+ // at max(first build, server bind) instead of their sum.
67
+ const viteDevServerStartup = this.startViteDevServer(platformData, projectData, prepareData);
63
68
  try {
64
69
  const childProcess = await this.startBundleProcess(platformData, projectData, prepareData);
65
70
  // Handle Vite differently from webpack
@@ -105,13 +110,18 @@ class BundlerCompilerService extends events_1.EventEmitter {
105
110
  }
106
111
  this.copyViteBundleToNative(distOutput, destDir);
107
112
  }
108
- // Resolve the promise on first build completion
113
+ // Resolve the promise on first build completion — gated on
114
+ // the HMR dev server being reachable (no-op resolve for
115
+ // non-HMR runs). `startViteDevServer` never rejects and its
116
+ // readiness probe is bounded, so this cannot hang the run;
117
+ // on probe timeout we proceed and the device's own retry
118
+ // handles any residual gap.
109
119
  if (isFirstBundlerWatchCompilation) {
110
120
  isFirstBundlerWatchCompilation = false;
111
121
  if (debugLog) {
112
122
  console.log("Vite first build completed, resolving compileWithWatch");
113
123
  }
114
- resolve(childProcess);
124
+ viteDevServerStartup.then(() => resolve(childProcess));
115
125
  return;
116
126
  }
117
127
  // Transform Vite message to match webpack format
@@ -454,7 +464,11 @@ class BundlerCompilerService extends events_1.EventEmitter {
454
464
  this.$logger.info(`Vite dev server ready on port ${port} (HMR for ${key}).`);
455
465
  }
456
466
  else {
457
- this.$logger.trace(`Vite dev server port ${port} not observed open within the readiness probe window; continuing (it may bind shortly).`);
467
+ // Warn (not trace): the first-build resolution in
468
+ // `compileWithWatch` gates on this method, so a probe timeout
469
+ // means the app will be deployed against a server that may not
470
+ // be up yet — the user should see why a boot might stall.
471
+ this.$logger.warn(`Vite dev server port ${port} not observed open within the readiness probe window; continuing (it may bind shortly).`);
458
472
  }
459
473
  }
460
474
  catch (err) {
@@ -5,6 +5,8 @@ const yok_1 = require("../../common/yok");
5
5
  const trapezedev_project_1 = require("@nstudio/trapezedev-project");
6
6
  const color_1 = require("../../color");
7
7
  const path = require("path");
8
+ const os = require("os");
9
+ const fs = require("fs");
8
10
  class SPMService {
9
11
  constructor($logger, $fs, $projectConfigService, $terminalSpinnerService, $xcodebuildCommandService, $xcodebuildArgsService) {
10
12
  this.$logger = $logger;
@@ -28,11 +30,19 @@ class SPMService {
28
30
  // include swift packages from plugin configs
29
31
  // but allow app packages to override plugin packages with the same name
30
32
  const appPackageNames = new Set(appPackages.map((pkg) => pkg.name));
33
+ // multiple plugins may declare the same package (e.g. a shared shim) —
34
+ // only the first declaration is added; a second same-name package would
35
+ // produce duplicate (and possibly conflicting) references in the pbxproj.
36
+ const addedPluginPackageNames = new Set();
31
37
  for (const pluginPkg of pluginPackages) {
32
38
  if (appPackageNames.has(pluginPkg.name)) {
33
39
  this.$logger.trace(`SPM: app package overrides plugin package: ${pluginPkg.name}`);
34
40
  }
41
+ else if (addedPluginPackageNames.has(pluginPkg.name)) {
42
+ this.$logger.trace(`SPM: skipping duplicate plugin package: ${pluginPkg.name}`);
43
+ }
35
44
  else {
45
+ addedPluginPackageNames.add(pluginPkg.name);
36
46
  appPackages.push(pluginPkg);
37
47
  }
38
48
  }
@@ -52,6 +62,12 @@ class SPMService {
52
62
  this.$logger.trace("SPM: no SPM packages to apply.");
53
63
  return;
54
64
  }
65
+ // one compact line naming every package and where it comes from —
66
+ // when resolution is slow or fails, this is the first thing needed
67
+ // to tell WHICH dependency is responsible.
68
+ this.$logger.info(`Swift Packages: ${spmPackages
69
+ .map((pkg) => this.describePackageSource(pkg))
70
+ .join(", ")}`);
55
71
  const project = new trapezedev_project_1.MobileProject(platformData.projectRoot, {
56
72
  ios: {
57
73
  path: ".",
@@ -70,6 +86,11 @@ class SPMService {
70
86
  // resolve the path relative to the project root
71
87
  this.$logger.trace("SPM: resolving path for package: ", pkg.path);
72
88
  pkg.path = path.resolve(projectData.projectDir, pkg.path);
89
+ if (!this.$fs.exists(pkg.path)) {
90
+ // surface this now — otherwise the only symptom is a cryptic
91
+ // xcodebuild resolution failure much later.
92
+ this.$logger.warn(`SPM: local package path for "${pkg.name}" does not exist: ${pkg.path} — Xcode will fail to resolve it.`);
93
+ }
73
94
  }
74
95
  this.$logger.trace(`SPM: adding package ${pkg.name} to project.`, pkg);
75
96
  await project.ios.addSPMPackage(projectData.projectName, pkg);
@@ -126,22 +147,73 @@ class SPMService {
126
147
  const startedAt = Date.now();
127
148
  let activity = "Resolving Swift Package dependencies";
128
149
  let lineBuffer = "";
150
+ // package currently being git-fetched (short name), if any — used to
151
+ // measure its growing clone in the SwiftPM cache so a long silent fetch
152
+ // shows visible progress ("2.31 GB of git history fetched") instead of
153
+ // looking hung.
154
+ let fetchingPackageRef = null;
155
+ let fetchedBytes = 0;
156
+ let largeCloneNoted = false;
157
+ // rolling tail of raw xcodebuild output for the failure report.
158
+ const outputTail = [];
129
159
  const render = () => {
130
160
  const elapsed = Math.round((Date.now() - startedAt) / 1000);
131
- spinner.text = `${activity}… ${color_1.color.dim(`(${this.formatElapsed(elapsed)})`)}`;
161
+ const fetched = fetchedBytes > 0
162
+ ? ` — ${this.formatBytes(fetchedBytes)} of git history fetched`
163
+ : "";
164
+ spinner.text = `${activity}…${fetched} ${color_1.color.dim(`(${this.formatElapsed(elapsed)})`)}`;
132
165
  };
133
166
  // keep the elapsed timer ticking even when xcodebuild is silent (e.g.
134
- // while a binary artifact downloads) so the user can see it's alive.
135
- const ticker = setInterval(render, 1000);
167
+ // while a repository clones or a binary artifact downloads) so the user
168
+ // can see it's alive. Every 5th tick, measure the in-progress clone.
169
+ let tickCount = 0;
170
+ const ticker = setInterval(() => {
171
+ tickCount++;
172
+ if (fetchingPackageRef && tickCount % 5 === 0) {
173
+ fetchedBytes = this.getPackageCloneSizeBytes(fetchingPackageRef);
174
+ if (!largeCloneNoted &&
175
+ fetchedBytes >= SPMService.LARGE_CLONE_NOTE_BYTES) {
176
+ largeCloneNoted = true;
177
+ // persist a one-time explanation above the spinner: this is the
178
+ // point where users otherwise assume the CLI is stuck.
179
+ spinner.stopAndPersist({
180
+ symbol: color_1.color.yellow("ℹ"),
181
+ text: color_1.color.yellow(`the "${fetchingPackageRef}" package is hosted in a repository with a large git history — ` +
182
+ `SwiftPM clones the entire repository on first fetch, which can take a long time.\n` +
183
+ ` cache: ${SPMService.SWIFTPM_REPO_CACHE}`),
184
+ });
185
+ spinner.start();
186
+ }
187
+ }
188
+ render();
189
+ }, 1000);
136
190
  const onProgress = (chunk) => {
137
191
  lineBuffer += chunk.data;
138
192
  const lines = lineBuffer.split("\n");
139
193
  // keep the last (possibly partial) line in the buffer
140
194
  lineBuffer = lines.pop();
141
195
  for (const line of lines) {
196
+ const trimmed = line.trim();
197
+ if (trimmed) {
198
+ this.$logger.trace(`SPM: ${trimmed}`);
199
+ outputTail.push(trimmed);
200
+ if (outputTail.length > SPMService.OUTPUT_TAIL_LINES) {
201
+ outputTail.shift();
202
+ }
203
+ }
142
204
  const described = this.describeSPMActivity(line);
143
205
  if (described) {
144
206
  activity = described;
207
+ // track which package a "Fetching <url>" line refers to so the
208
+ // ticker can measure its clone; any other activity means the
209
+ // fetch finished.
210
+ if (/^Fetching\b/i.test(trimmed)) {
211
+ fetchingPackageRef = this.shortenPackageRef(trimmed);
212
+ }
213
+ else {
214
+ fetchingPackageRef = null;
215
+ fetchedBytes = 0;
216
+ }
145
217
  render();
146
218
  }
147
219
  }
@@ -153,10 +225,20 @@ class SPMService {
153
225
  cwd: projectData.projectDir,
154
226
  onProgress,
155
227
  });
156
- spinner.succeed(color_1.color.green("Swift Package dependencies resolved"));
228
+ const elapsed = Math.round((Date.now() - startedAt) / 1000);
229
+ spinner.succeed(color_1.color.green("Swift Package dependencies resolved") +
230
+ color_1.color.dim(` (${this.formatElapsed(elapsed)})`));
157
231
  }
158
232
  catch (err) {
159
233
  spinner.fail(color_1.color.red("Failed to resolve Swift Package dependencies"));
234
+ // the spinner swallowed the raw log — replay the tail so the actual
235
+ // xcodebuild error is visible without rerunning in verbose mode.
236
+ if (outputTail.length) {
237
+ this.$logger.info(color_1.color.dim("xcodebuild output (last lines):"));
238
+ for (const line of outputTail) {
239
+ this.$logger.info(color_1.color.dim(` ${line}`));
240
+ }
241
+ }
160
242
  throw err;
161
243
  }
162
244
  finally {
@@ -253,6 +335,88 @@ class SPMService {
253
335
  const seconds = totalSeconds % 60;
254
336
  return `${minutes}m ${seconds}s`;
255
337
  }
338
+ /**
339
+ * One-line description of a package and where it resolves from, e.g.
340
+ * "FontManager (1.0.12 · https://github.com/NativeScript/font-manager.git)"
341
+ * or "CanvasNative (local: node_modules/@nativescript/canvas/platforms/ios/NativeScriptV8)".
342
+ */
343
+ describePackageSource(pkg) {
344
+ if ("path" in pkg) {
345
+ return `${pkg.name} (local: ${pkg.path})`;
346
+ }
347
+ return `${pkg.name} (${pkg.version} · ${pkg.repositoryURL})`;
348
+ }
349
+ /**
350
+ * Size on disk of the SwiftPM cache clone(s) for a package ref (the short
351
+ * name produced by shortenPackageRef). Cache entries are named
352
+ * "<repo-name>-<hash>". Returns 0 when nothing is there (yet).
353
+ */
354
+ getPackageCloneSizeBytes(packageRef) {
355
+ let entries;
356
+ try {
357
+ entries = fs.readdirSync(SPMService.SWIFTPM_REPO_CACHE, {
358
+ withFileTypes: true,
359
+ });
360
+ }
361
+ catch (err) {
362
+ return 0;
363
+ }
364
+ let total = 0;
365
+ for (const entry of entries) {
366
+ if (entry.isDirectory() &&
367
+ (entry.name === packageRef ||
368
+ entry.name.startsWith(`${packageRef}-`))) {
369
+ total += this.getDirectorySizeBytes(path.join(SPMService.SWIFTPM_REPO_CACHE, entry.name));
370
+ }
371
+ }
372
+ return total;
373
+ }
374
+ /**
375
+ * Recursive directory size via the raw fs API. Tolerates files vanishing
376
+ * mid-walk (git renames its temp packfiles while cloning) and does not
377
+ * follow symlinks.
378
+ */
379
+ getDirectorySizeBytes(dirPath) {
380
+ let total = 0;
381
+ const pending = [dirPath];
382
+ while (pending.length) {
383
+ const current = pending.pop();
384
+ let entries;
385
+ try {
386
+ entries = fs.readdirSync(current, { withFileTypes: true });
387
+ }
388
+ catch (err) {
389
+ continue;
390
+ }
391
+ for (const entry of entries) {
392
+ const fullPath = path.join(current, entry.name);
393
+ try {
394
+ if (entry.isDirectory()) {
395
+ pending.push(fullPath);
396
+ }
397
+ else if (entry.isFile()) {
398
+ total += fs.statSync(fullPath).size;
399
+ }
400
+ }
401
+ catch (err) {
402
+ // entry disappeared between readdir and stat — ignore
403
+ }
404
+ }
405
+ }
406
+ return total;
407
+ }
408
+ /** Formats a byte count as a short human-readable size ("2.31 GB"). */
409
+ formatBytes(bytes) {
410
+ const GB = 1024 ** 3;
411
+ const MB = 1024 ** 2;
412
+ if (bytes >= GB) {
413
+ return `${(bytes / GB).toFixed(2)} GB`;
414
+ }
415
+ if (bytes >= MB) {
416
+ return `${Math.round(bytes / MB)} MB`;
417
+ }
418
+ return `${Math.round(bytes / 1024)} KB`;
419
+ }
256
420
  /** True when the Xcode project references any Swift packages. */
257
421
  hasSPMReferences(platformData, projectData) {
258
422
  const pbxprojPath = path.join(platformData.projectRoot, `${projectData.projectName}.xcodeproj`, "project.pbxproj");
@@ -278,5 +442,17 @@ class SPMService {
278
442
  }
279
443
  }
280
444
  exports.SPMService = SPMService;
445
+ // SwiftPM keeps one bare clone per source package here (shared by Xcode and
446
+ // xcodebuild). Watching a package's clone grow is the only visibility into
447
+ // an otherwise silent long fetch — SwiftPM clones a repository's ENTIRE git
448
+ // history to read Package.swift, so a package hosted in a multi-GB repo can
449
+ // legitimately "fetch" for tens of minutes with no output.
450
+ SPMService.SWIFTPM_REPO_CACHE = path.join(os.homedir(), "Library", "Caches", "org.swift.swiftpm", "repositories");
451
+ // Once a single package's clone passes this size, surface a one-time note
452
+ // explaining why the fetch is slow (250 MB is already far beyond any
453
+ // reasonably-hosted Swift package).
454
+ SPMService.LARGE_CLONE_NOTE_BYTES = 250 * 1024 * 1024;
455
+ // Lines of raw xcodebuild output kept for the failure report.
456
+ SPMService.OUTPUT_TAIL_LINES = 25;
281
457
  yok_1.injector.register("spmService", SPMService);
282
458
  //# sourceMappingURL=spm-service.js.map
@@ -697,6 +697,23 @@ class IOSProjectService extends projectServiceBaseLib.PlatformProjectServiceBase
697
697
  const config = this.$projectConfigService.readConfig(plugin.fullPath);
698
698
  const packages = _.get(config, `${platformData.platformNameLowerCase}.SPMPackages`, []);
699
699
  if (packages.length) {
700
+ for (const pkg of packages) {
701
+ // a plugin's local package path is naturally authored relative
702
+ // to the plugin itself, but the SPM service resolves relative
703
+ // paths against the app project dir. When the app-relative
704
+ // path doesn't exist (e.g. non-hoisted node_modules layouts),
705
+ // fall back to resolving against the plugin's own directory.
706
+ if ("path" in pkg &&
707
+ pkg.path &&
708
+ !path.isAbsolute(pkg.path) &&
709
+ !this.$fs.exists(path.resolve(projectData.projectDir, pkg.path))) {
710
+ const pluginRelativePath = path.resolve(plugin.fullPath, pkg.path);
711
+ if (this.$fs.exists(pluginRelativePath)) {
712
+ this.$logger.trace(`SPM: resolved plugin-relative package path for ${pkg.name}: ${pluginRelativePath}`);
713
+ pkg.path = pluginRelativePath;
714
+ }
715
+ }
716
+ }
700
717
  pluginSpmPackages.push(...packages);
701
718
  }
702
719
  }
@@ -798,11 +815,18 @@ class IOSProjectService extends projectServiceBaseLib.PlatformProjectServiceBase
798
815
  this.$fs.rename(path.join(fileRootLocation, oldFileName), path.join(fileRootLocation, newFileName));
799
816
  }
800
817
  async prepareNativeSourceCode(groupName, sourceFolderPath, projectData) {
818
+ var _a;
801
819
  const project = this.createPbxProj(projectData);
802
820
  const group = await this.getRootGroup(groupName, sourceFolderPath);
821
+ // pin the sources to the main app target: without an explicit target the
822
+ // underlying xcode lib picks whichever "Sources" build phase it finds
823
+ // first, which can be an extension target (e.g. a widget) once one
824
+ // exists — compiling plugin native code into extensions breaks their
825
+ // builds (and bloats them) since they lack the app's search paths.
803
826
  project.addPbxGroup(group.files, group.name, group.path, null, {
804
827
  isMain: true,
805
828
  filesRelativeToProject: true,
829
+ target: (_a = project.getFirstTarget()) === null || _a === void 0 ? void 0 : _a.uuid,
806
830
  });
807
831
  project.addToHeaderSearchPaths(group.path);
808
832
  const headerFiles = this.$fs.exists(sourceFolderPath)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nativescript",
3
3
  "main": "./lib/nativescript-cli-lib.js",
4
- "version": "9.1.0-alpha.12",
4
+ "version": "9.1.0-alpha.14",
5
5
  "author": "NativeScript <oss@nativescript.org>",
6
6
  "description": "Command-line interface for building NativeScript projects",
7
7
  "bin": {