@pixelbyte-software/pixcode 1.40.4 → 1.40.5

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.
@@ -1,4 +1,4 @@
1
- import { mkdtemp, writeFile } from 'node:fs/promises';
1
+ import { chmod, mkdtemp, writeFile } from 'node:fs/promises';
2
2
  import { tmpdir } from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
@@ -87,6 +87,10 @@ assert.ok(
87
87
  liveViewPanel.includes('managedRuntime') && liveViewPanel.includes('liveView.managedRuntimePreparing'),
88
88
  'Live View panel should explain that Pixcode can prepare managed runtimes automatically.',
89
89
  );
90
+ assert.ok(
91
+ liveViewPanel.includes('isPreparingManagedRuntime') && liveViewPanel.includes('liveView.preparingRuntime'),
92
+ 'Live View panel should show a visible in-progress state while Pixcode downloads and installs a managed runtime.',
93
+ );
90
94
  assert.ok(
91
95
  liveViewPanel.includes("runAction('restart')"),
92
96
  'Live View panel should expose a restart action for failed process runners.',
@@ -101,6 +105,14 @@ assert.ok(
101
105
  managedRuntimes.includes('extractZip') && managedRuntimes.includes('extractTarGz'),
102
106
  'Managed runtime installation should handle common Windows zip and macOS/Linux tarball assets.',
103
107
  );
108
+ assert.ok(
109
+ managedRuntimes.includes('preferManaged'),
110
+ 'Managed PHP Live View should be able to skip external runtimes and prefer Pixcode-owned binaries.',
111
+ );
112
+ assert.ok(
113
+ managedRuntimes.includes("id === 'npm'") && managedRuntimes.includes('installNpmRuntime'),
114
+ 'Managed runtimes should include a Pixcode-owned npm runner for JavaScript projects when npm is not on PATH.',
115
+ );
104
116
 
105
117
  const serverIndex = await read('server/index.js');
106
118
  assert.ok(
@@ -152,6 +164,18 @@ const viteTarget = await detectLiveViewTarget(viteProject);
152
164
  assert.equal(viteTarget.available, true, 'Vite projects should be detected.');
153
165
  assert.equal(viteTarget.command?.id, 'npm-dev-vite', 'Vite projects should get a Vite-aware command.');
154
166
 
167
+ const viteMissingNpmTarget = await detectLiveViewTarget(viteProject, {
168
+ env: {
169
+ ...process.env,
170
+ PATH: '',
171
+ Path: '',
172
+ },
173
+ });
174
+ assert.equal(viteMissingNpmTarget.available, true, 'Vite projects should remain runnable through a Pixcode-managed package runner when npm is missing from PATH.');
175
+ assert.equal(viteMissingNpmTarget.command?.id, 'npm-dev-vite', 'Vite projects should keep the original Vite command identity.');
176
+ assert.equal(viteMissingNpmTarget.managedRuntime?.id, 'npm', 'Missing npm should select the Pixcode-managed npm runner.');
177
+ assert.equal(viteMissingNpmTarget.managedRuntime?.status, 'missing', 'Missing npm should report that the managed package runner still needs preparation.');
178
+
155
179
  const djangoTarget = await detectLiveViewTarget(djangoProject);
156
180
  assert.equal(djangoTarget.available, true, 'Django projects should be detected from manage.py.');
157
181
  assert.equal(djangoTarget.command?.id, 'python-django', 'Django projects should get a runserver command.');
@@ -172,6 +196,27 @@ assert.ok(
172
196
  'Missing PHP should use product language instead of exposing PATH setup as the primary message.',
173
197
  );
174
198
 
199
+ const fakeBin = path.join(workspace, 'fake-bin');
200
+ await mkdir(fakeBin, { recursive: true });
201
+ const fakePhp = path.join(fakeBin, process.platform === 'win32' ? 'php.cmd' : 'php');
202
+ await writeFile(fakePhp, process.platform === 'win32' ? '@echo off\r\nexit /b 0\r\n' : '#!/bin/sh\nexit 0\n');
203
+ if (process.platform !== 'win32') {
204
+ await chmod(fakePhp, 0o755);
205
+ }
206
+ const fakePath = process.platform === 'win32'
207
+ ? `${fakeBin};${process.env.PATH || ''}`
208
+ : `${fakeBin}:${process.env.PATH || ''}`;
209
+ const phpSystemRuntimeTarget = await detectLiveViewTarget(phpProject, {
210
+ env: {
211
+ ...process.env,
212
+ PATH: fakePath,
213
+ Path: fakePath,
214
+ },
215
+ });
216
+ assert.equal(phpSystemRuntimeTarget.available, true, 'PHP projects should stay runnable when php exists on PATH.');
217
+ assert.equal(phpSystemRuntimeTarget.command?.id, 'frankenphp-php-server', 'PHP projects should still use the Pixcode-managed runtime even when external php exists.');
218
+ assert.equal(phpSystemRuntimeTarget.managedRuntime?.id, 'frankenphp', 'PHP projects should prefer the Pixcode-owned FrankenPHP runtime instead of external php.');
219
+
175
220
  const staticSession = await startLiveView('static-smoke', staticProject);
176
221
  assert.equal(staticSession.status, 'running', 'Static Live View should start without a child process.');
177
222
  assert.match(staticSession.sharePath, /^\/live\/[a-f0-9]{24}\/$/, 'Live View should expose a random public share path.');
@@ -459,7 +459,7 @@ export function findExecutableOnPath(name, env = process.env) {
459
459
  * more reliable than trusting PATH — when Pixcode runs as a daemon, PATH
460
460
  * is often minimal and doesn't include the user's node install.
461
461
  */
462
- function resolveNpmCommand(env = process.env) {
462
+ export function resolveNpmCommand(env = process.env) {
463
463
  const nodeDir = path.dirname(process.execPath);
464
464
  const isWindows = process.platform === 'win32';
465
465
  const candidates = isWindows
@@ -4,6 +4,7 @@ import { promises as fs } from 'node:fs';
4
4
  import net from 'node:net';
5
5
  import path from 'node:path';
6
6
 
7
+ import { buildCliSpawnEnv } from './install-jobs.js';
7
8
  import { ensureManagedRuntime, getManagedRuntimeStatus } from './managed-runtimes.js';
8
9
 
9
10
  const sessionsByProject = new Map();
@@ -99,7 +100,7 @@ function runtimeMissingReason(command, framework) {
99
100
  return 'Pixcode can prepare a local PHP runtime automatically before starting this project.';
100
101
  }
101
102
  if (command === 'npm' || command === 'pnpm' || command === 'yarn' || command === 'bun') {
102
- return `${base} Pixcode will use its bundled Node runtime when possible; otherwise install the package manager or use a custom command.`;
103
+ return `${base} Pixcode can prepare a local Node package runner automatically before starting this project.`;
103
104
  }
104
105
  if (command === 'python' || command === 'python3') {
105
106
  return `${base} Pixcode does not have a managed Python runtime for this stack yet.`;
@@ -169,12 +170,44 @@ function buildPackageCommand(packageManager, scriptName, id, label, framework, e
169
170
  id,
170
171
  label,
171
172
  framework,
173
+ packageManager,
174
+ scriptName,
175
+ extraArgs,
172
176
  command: packageManager,
173
177
  args,
174
178
  displayCommand: buildDisplayCommand(packageManager, args),
175
179
  };
176
180
  }
177
181
 
182
+ function isPackageManagerCommand(command) {
183
+ return command === 'npm' || command === 'pnpm' || command === 'yarn' || command === 'bun';
184
+ }
185
+
186
+ function buildManagedPackageCommand(command, runtimeStatus) {
187
+ const npmArgs = command.scriptName
188
+ ? packageRunArgs('npm', command.scriptName, command.extraArgs || [])
189
+ : command.args;
190
+ const runtimeExecutable = runtimeStatus?.executablePath || null;
191
+ const commandExecutable = runtimeExecutable
192
+ ? (runtimeStatus?.runner === 'node' || runtimeExecutable.endsWith('.js') ? process.execPath : runtimeExecutable)
193
+ : command.command;
194
+ const args = runtimeExecutable && (runtimeStatus?.runner === 'node' || runtimeExecutable.endsWith('.js'))
195
+ ? [runtimeExecutable, ...npmArgs]
196
+ : npmArgs;
197
+
198
+ return {
199
+ ...command,
200
+ packageManager: 'npm',
201
+ command: commandExecutable,
202
+ args,
203
+ displayCommand: buildDisplayCommand('npm', npmArgs),
204
+ managedRuntime: {
205
+ id: 'npm',
206
+ status: runtimeStatus?.status || 'missing',
207
+ },
208
+ };
209
+ }
210
+
178
211
  function buildManagedPhpCommand(runtimeStatus) {
179
212
  const executable = runtimeStatus?.executablePath || 'frankenphp';
180
213
  return {
@@ -414,24 +447,46 @@ export async function detectLiveViewTarget(projectPath, options = {}) {
414
447
 
415
448
  const processCommand = await detectProcessCommand(projectPath);
416
449
  if (processCommand) {
450
+ if (isPackageManagerCommand(processCommand.command)) {
451
+ const managedRuntime = await getManagedRuntimeStatus('npm', {
452
+ env: options.env || process.env,
453
+ preferManaged: true,
454
+ });
455
+ const command = buildManagedPackageCommand(processCommand, managedRuntime);
456
+ return {
457
+ available: true,
458
+ kind: 'process',
459
+ label: processCommand.label,
460
+ framework: processCommand.framework,
461
+ command,
462
+ managedRuntime,
463
+ reason: managedRuntime.status === 'missing'
464
+ ? 'Pixcode will prepare a local Node package runner automatically before starting this project.'
465
+ : 'Pixcode will run this project with its managed Node package runner.',
466
+ };
467
+ }
468
+
469
+ if (processCommand.framework === 'PHP' || processCommand.command === 'php') {
470
+ const managedRuntime = await getManagedRuntimeStatus('frankenphp', {
471
+ env: options.env || process.env,
472
+ preferManaged: true,
473
+ });
474
+ const command = buildManagedPhpCommand(managedRuntime);
475
+ return {
476
+ available: true,
477
+ kind: 'process',
478
+ label: command.label,
479
+ framework: command.framework,
480
+ command,
481
+ managedRuntime,
482
+ reason: managedRuntime.status === 'missing'
483
+ ? 'Pixcode will prepare a local PHP runtime automatically before starting this project.'
484
+ : 'Pixcode will run this project with its managed PHP runtime.',
485
+ };
486
+ }
487
+
417
488
  const runtimeAvailable = await checkCommandAvailability(processCommand.command, options.env || process.env);
418
489
  if (!runtimeAvailable) {
419
- if (processCommand.framework === 'PHP' || processCommand.command === 'php') {
420
- const managedRuntime = await getManagedRuntimeStatus('frankenphp', { env: options.env || process.env });
421
- const command = buildManagedPhpCommand(managedRuntime);
422
- return {
423
- available: true,
424
- kind: 'process',
425
- label: command.label,
426
- framework: command.framework,
427
- command,
428
- managedRuntime,
429
- reason: managedRuntime.status === 'missing'
430
- ? 'Pixcode will prepare a local PHP runtime automatically before starting this project.'
431
- : 'Pixcode will run this project with its managed PHP runtime.',
432
- };
433
- }
434
-
435
490
  return {
436
491
  available: false,
437
492
  kind: 'process',
@@ -618,9 +673,13 @@ export async function startLiveView(projectName, projectPath, options = {}) {
618
673
  let runtimeStatus = target.managedRuntime || target.command?.managedRuntime || null;
619
674
  let targetCommand = target.command;
620
675
  if (runtimeStatus?.id && runtimeStatus.status !== 'system' && runtimeStatus.status !== 'installed') {
621
- runtimeStatus = await ensureManagedRuntime(runtimeStatus.id);
676
+ runtimeStatus = await ensureManagedRuntime(runtimeStatus.id, {
677
+ preferManaged: runtimeStatus.id === 'frankenphp' || runtimeStatus.id === 'npm',
678
+ });
622
679
  if (runtimeStatus.id === 'frankenphp') {
623
680
  targetCommand = buildManagedPhpCommand(runtimeStatus);
681
+ } else if (runtimeStatus.id === 'npm') {
682
+ targetCommand = buildManagedPackageCommand(targetCommand, runtimeStatus);
624
683
  }
625
684
  }
626
685
 
@@ -649,8 +708,9 @@ export async function startLiveView(projectName, projectPath, options = {}) {
649
708
  };
650
709
 
651
710
  const env = {
652
- ...process.env,
711
+ ...buildCliSpawnEnv(process.env),
653
712
  ...(command.env || {}),
713
+ ...(process.versions.electron ? { ELECTRON_RUN_AS_NODE: '1' } : {}),
654
714
  PORT: String(port),
655
715
  HOST: '127.0.0.1',
656
716
  VITE_HOST: '127.0.0.1',
@@ -3,7 +3,9 @@ import { promises as fs } from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
 
6
- import { buildCliSpawnEnv, findExecutableOnPath } from './install-jobs.js';
6
+ import * as tar from 'tar';
7
+
8
+ import { buildCliSpawnEnv, findExecutableOnPath, resolveNpmCommand } from './install-jobs.js';
7
9
 
8
10
  const DEFAULT_RUNTIMES_HOME = path.join(os.homedir(), '.pixcode', 'runtimes');
9
11
  const MANIFEST_FILE = 'pixcode-runtime.json';
@@ -161,7 +163,11 @@ async function extractZip(archivePath, targetDir, env = process.env) {
161
163
  }
162
164
 
163
165
  async function extractTarGz(archivePath, targetDir, env = process.env) {
164
- await runProcess('tar', ['-xzf', archivePath, '-C', targetDir], { env });
166
+ void env;
167
+ await tar.x({
168
+ file: archivePath,
169
+ cwd: targetDir,
170
+ });
165
171
  }
166
172
 
167
173
  async function findRuntimeExecutable(searchRoot, binaryName) {
@@ -255,9 +261,53 @@ async function installFrankenPhp(env = process.env) {
255
261
  return manifest;
256
262
  }
257
263
 
264
+ async function installNpmRuntime(env = process.env) {
265
+ const registryUrl = env.PIXCODE_NPM_RUNTIME_REGISTRY
266
+ || 'https://registry.npmjs.org/npm/latest';
267
+ const metadata = await fetchJson(registryUrl, env);
268
+ const tarballUrl = metadata?.dist?.tarball;
269
+ if (!tarballUrl) {
270
+ throw new Error('No npm runtime tarball is available from the npm registry.');
271
+ }
272
+
273
+ const baseDir = runtimeDir('npm', env);
274
+ const stagingDir = path.join(baseDir, `.staging-${Date.now()}`);
275
+ const currentDir = path.join(baseDir, 'current');
276
+ await fs.mkdir(stagingDir, { recursive: true });
277
+
278
+ const archivePath = path.join(stagingDir, 'npm-runtime.tgz');
279
+ await downloadFile(tarballUrl, archivePath, env);
280
+ await extractTarGz(archivePath, stagingDir, env);
281
+
282
+ const packageDir = path.join(stagingDir, 'package');
283
+ const executablePath = path.join(packageDir, 'bin', 'npm-cli.js');
284
+ if (!(await fileExists(executablePath))) {
285
+ throw new Error('Downloaded npm runtime did not contain bin/npm-cli.js.');
286
+ }
287
+
288
+ await fs.rm(currentDir, { recursive: true, force: true });
289
+ await fs.cp(packageDir, currentDir, { recursive: true, force: true });
290
+
291
+ const finalExecutable = path.join(currentDir, 'bin', 'npm-cli.js');
292
+ const manifest = {
293
+ id: 'npm',
294
+ label: 'Pixcode Node package runner',
295
+ provider: 'npm',
296
+ version: metadata?.version || 'latest',
297
+ executablePath: finalExecutable,
298
+ runner: 'node',
299
+ sourceUrl: tarballUrl,
300
+ installedAt: new Date().toISOString(),
301
+ };
302
+ await fs.writeFile(manifestPath('npm', env), JSON.stringify(manifest, null, 2));
303
+ await fs.rm(stagingDir, { recursive: true, force: true }).catch(() => undefined);
304
+ return manifest;
305
+ }
306
+
258
307
  export async function getManagedRuntimeStatus(id, options = {}) {
259
308
  const env = options.env || process.env;
260
- if (id !== 'frankenphp') {
309
+ const preferManaged = Boolean(options.preferManaged);
310
+ if (id !== 'frankenphp' && id !== 'npm') {
261
311
  return {
262
312
  id,
263
313
  status: 'unsupported',
@@ -266,18 +316,59 @@ export async function getManagedRuntimeStatus(id, options = {}) {
266
316
  };
267
317
  }
268
318
 
269
- const spawnEnv = buildCliSpawnEnv(env);
270
- const systemExecutable = findExecutableOnPath('frankenphp', spawnEnv);
271
- if (systemExecutable) {
319
+ if (id === 'npm') {
320
+ if (!preferManaged) {
321
+ const spawnEnv = buildCliSpawnEnv(env);
322
+ const npmExecutable = resolveNpmCommand(spawnEnv);
323
+ if (npmExecutable) {
324
+ return {
325
+ id,
326
+ label: 'npm',
327
+ status: 'system',
328
+ installable: true,
329
+ executablePath: npmExecutable,
330
+ runner: npmExecutable.endsWith('.js') ? 'node' : undefined,
331
+ };
332
+ }
333
+ }
334
+
335
+ const manifest = await readManifest(id, env);
336
+ if (manifest) {
337
+ return {
338
+ id,
339
+ label: manifest.label || 'Pixcode Node package runner',
340
+ status: 'installed',
341
+ installable: true,
342
+ executablePath: manifest.executablePath,
343
+ runner: manifest.runner || 'node',
344
+ version: manifest.version,
345
+ };
346
+ }
347
+
272
348
  return {
273
349
  id,
274
- label: 'FrankenPHP',
275
- status: 'system',
350
+ label: 'Pixcode Node package runner',
351
+ provider: 'npm',
352
+ status: 'missing',
276
353
  installable: true,
277
- executablePath: systemExecutable,
354
+ reason: 'Pixcode will prepare a local Node package runner automatically before starting this project.',
278
355
  };
279
356
  }
280
357
 
358
+ if (!preferManaged) {
359
+ const spawnEnv = buildCliSpawnEnv(env);
360
+ const systemExecutable = findExecutableOnPath('frankenphp', spawnEnv);
361
+ if (systemExecutable) {
362
+ return {
363
+ id,
364
+ label: 'FrankenPHP',
365
+ status: 'system',
366
+ installable: true,
367
+ executablePath: systemExecutable,
368
+ };
369
+ }
370
+ }
371
+
281
372
  const manifest = await readManifest(id, env);
282
373
  if (manifest) {
283
374
  return {
@@ -302,7 +393,10 @@ export async function getManagedRuntimeStatus(id, options = {}) {
302
393
 
303
394
  export async function ensureManagedRuntime(id, options = {}) {
304
395
  const env = options.env || process.env;
305
- const status = await getManagedRuntimeStatus(id, { env });
396
+ const status = await getManagedRuntimeStatus(id, {
397
+ env,
398
+ preferManaged: options.preferManaged,
399
+ });
306
400
  if (status.executablePath) return status;
307
401
  if (!status.installable) {
308
402
  throw new Error(status.reason || 'This runtime cannot be prepared automatically.');
@@ -323,6 +417,18 @@ export async function ensureManagedRuntime(id, options = {}) {
323
417
  version: manifest.version,
324
418
  };
325
419
  }
420
+ if (id === 'npm') {
421
+ const manifest = await installNpmRuntime(env);
422
+ return {
423
+ id,
424
+ label: manifest.label,
425
+ status: 'installed',
426
+ installable: true,
427
+ executablePath: manifest.executablePath,
428
+ runner: manifest.runner || 'node',
429
+ version: manifest.version,
430
+ };
431
+ }
326
432
  throw new Error(`Unsupported managed runtime: ${id}`);
327
433
  })().finally(() => {
328
434
  installLocks.delete(lockKey);