Skip to content

Commit fefb984

Browse files
committed
feat(commands): add ns live
Runs `npx @nativescript/cloudflare-cli live` with the rest of the command line: it creates a project on preview.nativescript.org (or reopens this folder's), opens its browser IDE and runs it on a phone through NativeScript Preview, or on this Mac with --mac / --electron. It needs no project, so the live CLI runs through npx rather than being installed into one, and its non-zero exit fails the command.
1 parent 226fd44 commit fefb984

4 files changed

Lines changed: 147 additions & 0 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<% if (isJekyll) { %>---
2+
title: ns live
3+
position: 2
4+
---<% } %>
5+
6+
# ns live
7+
8+
### Description
9+
10+
Creates a project on preview.nativescript.org, opens its browser IDE and runs it live: on your phone through NativeScript Preview, or on this Mac. Edits from the IDE or its AI agent hot-reload the app. Run the command again in the same folder to reopen the project.
11+
12+
The project's keys are saved to `.ns-edge.json` in the current folder. Without a create token the project is free: a few hosted agent turns a day, no backend, deleted after 30 days unused.
13+
14+
### Commands
15+
16+
Usage | Synopsis
17+
---|---
18+
Creates a phone project and prints a QR code for NativeScript Preview | `ns live`
19+
Creates a Mac project and runs it on this Mac, streamed into the IDE | `ns live --mac`
20+
Creates an Electron project with native views and runs it on this Mac | `ns live --electron`
21+
22+
### Options
23+
24+
* `--new` - Creates another project instead of reopening this folder's.
25+
* `--no-open` - Doesn't open the IDE in the browser.
26+
* `--allow-eval` - With `--mac` or `--electron`, lets the project's agent run code on this Mac.
27+
* `--token <token>` - A create token for a full project (also read from `NS_EDGE_CREATE_TOKEN`).
28+
29+
<% if(isHtml) { %>
30+
31+
### Command Limitations
32+
33+
* `--mac` and `--electron` need an Apple Silicon Mac with macOS 14.4 or later.
34+
* `ns live` runs `npx @nativescript/cloudflare-cli live`, which you can also run without the NativeScript CLI installed.
35+
36+
<% } %>

‎lib/bootstrap.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,10 @@ registerBuiltInCommand<typeof import("./commands/preview").PreviewCommand>(
262262
"preview",
263263
() => require("./commands/preview").PreviewCommand,
264264
);
265+
registerBuiltInCommand<typeof import("./commands/live").LiveCommand>(
266+
"live",
267+
() => require("./commands/live").LiveCommand,
268+
);
265269

266270
registerBuiltInCommand<typeof import("./commands/debug").iosDebugCommand>(
267271
"debug|ios",

‎lib/commands/live.ts‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { IChildProcess } from "../common/declarations";
2+
import { Command } from "../common/define-command";
3+
import { inject } from "../common/di";
4+
5+
const LIVE_CLI_PACKAGE = "@nativescript/cloudflare-cli";
6+
7+
export class LiveCommand extends Command({
8+
name: "live",
9+
description:
10+
"Creates a live project on preview.nativescript.org, opens its browser IDE and runs it on your phone or this Mac.",
11+
// The flags belong to the live CLI (--mac, --electron, --new, ...), which
12+
// gets the raw command line.
13+
params: "any",
14+
allowUnknownOptions: true,
15+
}) {
16+
private $childProcess = inject<IChildProcess>("childProcess");
17+
18+
public async run(): Promise<void> {
19+
const commandArgs = process.argv.slice(process.argv.indexOf("live") + 1);
20+
// Needs no project, so it runs the live CLI through npx instead of installing it into one.
21+
const child = this.$childProcess.spawn(
22+
"npx",
23+
["--yes", `${LIVE_CLI_PACKAGE}@latest`, "live", ...commandArgs],
24+
// npx is a .cmd script on Windows, which Node only spawns through a shell.
25+
{ stdio: "inherit", shell: process.platform === "win32" },
26+
);
27+
const code = await new Promise<number>((resolve, reject) => {
28+
child.on("error", reject);
29+
child.on("close", (exitCode: number | null) => resolve(exitCode ?? 1));
30+
});
31+
if (code !== 0) {
32+
this.context.fail(`ns live exited with code ${code}.`, { help: false });
33+
}
34+
}
35+
}

‎test/commands/live.ts‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { EventEmitter } from "events";
2+
import { assert } from "chai";
3+
import { LiveCommand } from "../../lib/commands/live";
4+
import { Yok } from "../../lib/common/yok";
5+
import { registerCommand } from "../../lib/common/services/command-definition-adapter";
6+
import { runInInjectionContext } from "../../lib/common/di";
7+
8+
function createTestInjector(exitCode: number) {
9+
const spawned: { command: string; args: string[] }[] = [];
10+
const testInjector = new Yok();
11+
testInjector.register("childProcess", {
12+
spawn: (command: string, args: string[]) => {
13+
spawned.push({ command, args });
14+
const child = new EventEmitter();
15+
setImmediate(() => child.emit("close", exitCode));
16+
return child;
17+
},
18+
});
19+
testInjector.register("errors", {
20+
fail: (message: string) => {
21+
throw new Error(message);
22+
},
23+
failWithHelp: (message: string) => {
24+
throw new Error(message);
25+
},
26+
});
27+
testInjector.register("options", {});
28+
runInInjectionContext(testInjector, () => registerCommand(LiveCommand));
29+
return { testInjector, spawned };
30+
}
31+
32+
describe("live command", () => {
33+
const originalArgv = process.argv;
34+
35+
afterEach(() => {
36+
process.argv = originalArgv;
37+
});
38+
39+
it("runs the live CLI through npx with the flags after `live`", async () => {
40+
process.argv = ["node", "ns", "live", "--mac", "--new"];
41+
const { testInjector, spawned } = createTestInjector(0);
42+
43+
await testInjector.resolveCommand("live").execute(["--mac", "--new"]);
44+
45+
assert.deepEqual(spawned, [
46+
{
47+
command: "npx",
48+
args: [
49+
"--yes",
50+
"@nativescript/cloudflare-cli@latest",
51+
"live",
52+
"--mac",
53+
"--new",
54+
],
55+
},
56+
]);
57+
});
58+
59+
it("fails when the live CLI exits with an error", async () => {
60+
process.argv = ["node", "ns", "live", "--ios"];
61+
const { testInjector } = createTestInjector(1);
62+
63+
let error: Error | undefined;
64+
try {
65+
await testInjector.resolveCommand("live").execute(["--ios"]);
66+
} catch (e) {
67+
error = e;
68+
}
69+
70+
assert.equal(error?.message, "ns live exited with code 1.");
71+
});
72+
});

0 commit comments

Comments
 (0)