Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions docs/man_pages/project/testing/live.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<% if (isJekyll) { %>---
title: ns live
position: 2
---<% } %>

# ns live

### Description

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.

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.

### Commands

Usage | Synopsis
---|---
Creates a phone project and prints a QR code for NativeScript Preview | `ns live`
Creates a Mac project and runs it on this Mac, streamed into the IDE | `ns live --mac`
Creates an Electron project with native views and runs it on this Mac | `ns live --electron`

### Options

* `--new` - Creates another project instead of reopening this folder's.
* `--no-open` - Doesn't open the IDE in the browser.
* `--allow-eval` - With `--mac` or `--electron`, lets the project's agent run code on this Mac.
* `--token <token>` - A create token for a full project (also read from `NS_EDGE_CREATE_TOKEN`).

<% if(isHtml) { %>

### Command Limitations

* `--mac` and `--electron` need an Apple Silicon Mac with macOS 14.4 or later.
* `ns live` runs `npx @nativescript/cloudflare-cli live`, which you can also run without the NativeScript CLI installed.

<% } %>
4 changes: 4 additions & 0 deletions lib/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,10 @@ registerBuiltInCommand<typeof import("./commands/preview").PreviewCommand>(
"preview",
() => require("./commands/preview").PreviewCommand,
);
registerBuiltInCommand<typeof import("./commands/live").LiveCommand>(
"live",
() => require("./commands/live").LiveCommand,
);

registerBuiltInCommand<typeof import("./commands/debug").iosDebugCommand>(
"debug|ios",
Expand Down
35 changes: 35 additions & 0 deletions lib/commands/live.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { IChildProcess } from "../common/declarations";
import { Command } from "../common/define-command";
import { inject } from "../common/di";

const LIVE_CLI_PACKAGE = "@nativescript/cloudflare-cli";

export class LiveCommand extends Command({
name: "live",
description:
"Creates a live project on preview.nativescript.org, opens its browser IDE and runs it on your phone or this Mac.",
// The flags belong to the live CLI (--mac, --electron, --new, ...), which
// gets the raw command line.
params: "any",
allowUnknownOptions: true,
}) {
private $childProcess = inject<IChildProcess>("childProcess");

public async run(): Promise<void> {
const commandArgs = process.argv.slice(process.argv.indexOf("live") + 1);
// Needs no project, so it runs the live CLI through npx instead of installing it into one.
const child = this.$childProcess.spawn(
"npx",
["--yes", `${LIVE_CLI_PACKAGE}@latest`, "live", ...commandArgs],
// npx is a .cmd script on Windows, which Node only spawns through a shell.
{ stdio: "inherit", shell: process.platform === "win32" },
);
const code = await new Promise<number>((resolve, reject) => {
child.on("error", reject);
child.on("close", (exitCode: number | null) => resolve(exitCode ?? 1));
});
if (code !== 0) {
this.context.fail(`ns live exited with code ${code}.`, { help: false });
}
}
}
72 changes: 72 additions & 0 deletions test/commands/live.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { EventEmitter } from "events";
import { assert } from "chai";
import { LiveCommand } from "../../lib/commands/live";
import { Yok } from "../../lib/common/yok";
import { registerCommand } from "../../lib/common/services/command-definition-adapter";
import { runInInjectionContext } from "../../lib/common/di";

function createTestInjector(exitCode: number) {
const spawned: { command: string; args: string[] }[] = [];
const testInjector = new Yok();
testInjector.register("childProcess", {
spawn: (command: string, args: string[]) => {
spawned.push({ command, args });
const child = new EventEmitter();
setImmediate(() => child.emit("close", exitCode));
return child;
},
});
testInjector.register("errors", {
fail: (message: string) => {
throw new Error(message);
},
failWithHelp: (message: string) => {
throw new Error(message);
},
});
testInjector.register("options", {});
runInInjectionContext(testInjector, () => registerCommand(LiveCommand));
return { testInjector, spawned };
}

describe("live command", () => {
const originalArgv = process.argv;

afterEach(() => {
process.argv = originalArgv;
});

it("runs the live CLI through npx with the flags after `live`", async () => {
process.argv = ["node", "ns", "live", "--mac", "--new"];
const { testInjector, spawned } = createTestInjector(0);

await testInjector.resolveCommand("live").execute(["--mac", "--new"]);

assert.deepEqual(spawned, [
{
command: "npx",
args: [
"--yes",
"@nativescript/cloudflare-cli@latest",
"live",
"--mac",
"--new",
],
},
]);
});

it("fails when the live CLI exits with an error", async () => {
process.argv = ["node", "ns", "live", "--ios"];
const { testInjector } = createTestInjector(1);

let error: Error | undefined;
try {
await testInjector.resolveCommand("live").execute(["--ios"]);
} catch (e) {
error = e;
}

assert.equal(error?.message, "ns live exited with code 1.");
});
});
Loading