@nativescript/cloudflare-cli 0.0.0-stage → 0.1.0-beta.1

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/README.md CHANGED
@@ -1,3 +1,14 @@
1
- # Temporary Holding Version
1
+ # @nativescript/cloudflare-cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Mirror a NativeScript project into an ns-edge project from a terminal or a Vite dev server.
4
+
5
+ ```sh
6
+ npm i -D @nativescript/cloudflare-cli@beta
7
+ npx ns-edge create --server https://preview.nativescript.org --token <create token>
8
+ npx ns-edge watch # push, then every save
9
+ npx ns-edge qr # print the pairing QR code again
10
+ ```
11
+
12
+ Also: `ns-edge push` uploads `app/` and `server/` once, `ns-edge pull` downloads the project's files.
13
+
14
+ `ns-edge create` writes `.ns-edge.json` (project id and keys; keep it out of git). In a Vite config, `@nativescript/cloudflare-cli/vite` mirrors `app/` and `server/` on every save.
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+ import { watch } from 'node:fs';
3
+ import { readFile, writeFile } from 'node:fs/promises';
4
+ import { join, sep } from 'node:path';
5
+ import { createInterface } from 'node:readline/promises';
6
+ import QRCode from 'qrcode';
7
+ import { applyChanges, collect, createProject, projectUrl, report, SYNCED } from './sync.js';
8
+ const CONFIG = '.ns-edge.json';
9
+ const DEBOUNCE_MS = 120;
10
+ const [cmd = 'help', ...args] = process.argv.slice(2);
11
+ /**
12
+ * Editors and tools write temp, swap and backup files next to the real one
13
+ * (sed -i's `.!123!file`, vim's `.file.swp`, `file~`). Skipped like collect()
14
+ * skips dotfiles: one such path in a batch makes the edge reject the whole batch.
15
+ */
16
+ const isScratch = (rel) => rel.split('/').some((part) => part.startsWith('.')) || rel.endsWith('~');
17
+ const flag = (name) => {
18
+ const i = args.indexOf(`--${name}`);
19
+ return i >= 0 ? args[i + 1] : undefined;
20
+ };
21
+ const root = process.cwd();
22
+ async function loadRef() {
23
+ let ref;
24
+ try {
25
+ ref = JSON.parse(await readFile(join(root, CONFIG), 'utf8'));
26
+ }
27
+ catch {
28
+ throw new Error(`No ${CONFIG} here. Run \`ns-edge create --server <url>\` first.`);
29
+ }
30
+ // Copies meant to be shared (the IDE's StackBlitz export) leave the write key out of the file,
31
+ // so it is asked for here and kept in memory only.
32
+ ref.writeKey ||= process.env.NS_EDGE_WRITE_KEY || (await ask("Paste this project's write key (the IDE copied it to your clipboard): "));
33
+ if (!ref.writeKey)
34
+ throw new Error(`No write key: add it to ${CONFIG} or set NS_EDGE_WRITE_KEY.`);
35
+ return ref;
36
+ }
37
+ async function ask(question) {
38
+ if (!process.stdin.isTTY)
39
+ return '';
40
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
41
+ try {
42
+ return (await rl.question(question)).trim();
43
+ }
44
+ finally {
45
+ rl.close();
46
+ }
47
+ }
48
+ async function printPairing(ref) {
49
+ const url = projectUrl(ref);
50
+ const device = `${url}#${ref.deviceKey}`;
51
+ console.log(await QRCode.toString(`${ref.pairScheme ?? 'nsedge'}://pair?url=${encodeURIComponent(device)}`, { type: 'terminal', small: true }));
52
+ console.log(` device ${device}`);
53
+ console.log(` ide ${url}/#key=${ref.writeKey}`);
54
+ }
55
+ const commands = {
56
+ async create() {
57
+ const server = flag('server') ?? 'http://localhost:5173';
58
+ const ref = await createProject(server, flag('token') ?? process.env.NS_EDGE_CREATE_TOKEN);
59
+ await writeFile(join(root, CONFIG), JSON.stringify(ref, null, 2) + '\n');
60
+ console.log(`Created project ${ref.id} (saved to ${CONFIG}; it holds the project keys, keep it out of git)`);
61
+ await printPairing(ref);
62
+ },
63
+ async push() {
64
+ const ref = await loadRef();
65
+ const files = await collect(root);
66
+ report(await applyChanges(ref, files));
67
+ },
68
+ async pull() {
69
+ const ref = await loadRef();
70
+ const list = (await (await fetch(`${projectUrl(ref)}/files`)).json());
71
+ const { mkdir } = await import('node:fs/promises');
72
+ const { dirname } = await import('node:path');
73
+ for (const { path } of list) {
74
+ const content = await (await fetch(`${projectUrl(ref)}/files/${path}`)).text();
75
+ await mkdir(dirname(join(root, path)), { recursive: true });
76
+ await writeFile(join(root, path), content);
77
+ console.log(` ↓ ${path}`);
78
+ }
79
+ },
80
+ async watch() {
81
+ const ref = await loadRef();
82
+ await printPairing(ref);
83
+ report(await applyChanges(ref, await collect(root)));
84
+ const pending = new Set();
85
+ let timer;
86
+ const flush = async () => {
87
+ const paths = [...pending];
88
+ pending.clear();
89
+ const changes = await Promise.all(paths.map(async (path) => ({ path, content: await readFile(join(root, path), 'utf8').catch(() => null) })));
90
+ report(await applyChanges(ref, changes));
91
+ };
92
+ const changed = (rel) => {
93
+ pending.add(rel);
94
+ clearTimeout(timer);
95
+ timer = setTimeout(() => void flush().catch((e) => console.error(` ✖ ${e.message}`)), DEBOUNCE_MS);
96
+ };
97
+ try {
98
+ watch(root, { recursive: true }, (_event, file) => {
99
+ if (!file)
100
+ return;
101
+ const rel = file.split(sep).join('/');
102
+ if (SYNCED.test(rel) && !isScratch(rel))
103
+ changed(rel);
104
+ });
105
+ }
106
+ catch {
107
+ // Runtimes without recursive fs.watch (some sandboxes and WebContainers) compare snapshots instead.
108
+ let last = new Map((await collect(root)).map((c) => [c.path, c.content]));
109
+ setInterval(async () => {
110
+ const now = new Map((await collect(root)).map((c) => [c.path, c.content]));
111
+ for (const [path, content] of now)
112
+ if (last.get(path) !== content)
113
+ changed(path);
114
+ for (const path of last.keys())
115
+ if (!now.has(path))
116
+ changed(path);
117
+ last = now;
118
+ }, 1000);
119
+ }
120
+ console.log(' watching app/ and server/ …');
121
+ },
122
+ async qr() {
123
+ await printPairing(await loadRef());
124
+ },
125
+ async help() {
126
+ console.log(`ns-edge <command>
127
+
128
+ create [--server <url>] [--token <t>]
129
+ create an edge project and save .ns-edge.json
130
+ push upload app/ and server/ in one transaction
131
+ watch push, then stream every save to the edge
132
+ pull download the project's files here
133
+ qr print the pairing QR code`);
134
+ },
135
+ };
136
+ const run = commands[cmd] ?? commands.help;
137
+ run().catch((e) => {
138
+ console.error(e.message);
139
+ process.exit(1);
140
+ });
package/dist/sync.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ export interface ProjectRef {
2
+ server: string;
3
+ id: string;
4
+ writeKey: string;
5
+ deviceKey: string;
6
+ /** URL scheme of the app the server pairs with, from its create response. */
7
+ pairScheme?: string;
8
+ }
9
+ export interface ApplyResult {
10
+ changed: string[];
11
+ diagnostics: {
12
+ path: string;
13
+ error: string;
14
+ }[];
15
+ version: number;
16
+ }
17
+ /** Project-relative paths that are mirrored to the edge. */
18
+ export declare const SYNCED: RegExp;
19
+ export declare function projectUrl(ref: ProjectRef): string;
20
+ export declare function createProject(server: string, token?: string): Promise<ProjectRef>;
21
+ /** Sends a batch as one edge transaction, so devices see one consistent update. */
22
+ export declare function applyChanges(ref: ProjectRef, changes: {
23
+ path: string;
24
+ content: string | null;
25
+ }[]): Promise<ApplyResult>;
26
+ export declare function collect(root: string): Promise<{
27
+ path: string;
28
+ content: string;
29
+ }[]>;
30
+ export declare function report(result: ApplyResult, log?: (s: string) => void): void;
package/dist/sync.js ADDED
@@ -0,0 +1,61 @@
1
+ import { readFile, readdir, stat } from 'node:fs/promises';
2
+ import { join, relative, sep } from 'node:path';
3
+ /** Project-relative paths that are mirrored to the edge. */
4
+ export const SYNCED = /^(app|server)\/.+\.(ts|mts|js|mjs|xml|css|json)$/;
5
+ export function projectUrl(ref) {
6
+ return `${ref.server.replace(/\/$/, '')}/p/${ref.id}`;
7
+ }
8
+ export async function createProject(server, token) {
9
+ const res = await fetch(`${server.replace(/\/$/, '')}/api/projects`, {
10
+ method: 'POST',
11
+ headers: token ? { authorization: `Bearer ${token}` } : {},
12
+ });
13
+ if (!res.ok)
14
+ throw new Error(`create failed: ${res.status} ${await res.text()}`);
15
+ const { id, writeKey, deviceKey, pair } = (await res.json());
16
+ return { server, id, writeKey, deviceKey, pairScheme: pair?.split('://')[0] };
17
+ }
18
+ /** Sends a batch as one edge transaction, so devices see one consistent update. */
19
+ export async function applyChanges(ref, changes) {
20
+ const res = await fetch(`${projectUrl(ref)}/files`, {
21
+ method: 'POST',
22
+ headers: { authorization: `Bearer ${ref.writeKey}`, 'content-type': 'application/json' },
23
+ body: JSON.stringify({ changes }),
24
+ });
25
+ if (!res.ok)
26
+ throw new Error(`sync failed: ${res.status} ${await res.text()}`);
27
+ return (await res.json());
28
+ }
29
+ export async function collect(root) {
30
+ const out = [];
31
+ const walk = async (dir) => {
32
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
33
+ if (entry.name === 'node_modules' || entry.name.startsWith('.'))
34
+ continue;
35
+ const full = join(dir, entry.name);
36
+ if (entry.isDirectory())
37
+ await walk(full);
38
+ else {
39
+ const rel = relative(root, full).split(sep).join('/');
40
+ if (SYNCED.test(rel))
41
+ out.push({ path: rel, content: await readFile(full, 'utf8') });
42
+ }
43
+ }
44
+ };
45
+ for (const top of ['app', 'server']) {
46
+ const dir = join(root, top);
47
+ if (await stat(dir).then((s) => s.isDirectory(), () => false))
48
+ await walk(dir);
49
+ }
50
+ return out;
51
+ }
52
+ export function report(result, log = console.log) {
53
+ if (result.diagnostics.length) {
54
+ for (const d of result.diagnostics)
55
+ log(` ✖ ${d.error}`);
56
+ log(' devices kept the previous version');
57
+ }
58
+ else if (result.changed.length) {
59
+ log(` v${result.version} ← ${result.changed.join(', ')}`);
60
+ }
61
+ }
package/dist/vite.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { Plugin } from 'vite';
2
+ import { type ProjectRef } from './sync.js';
3
+ /**
4
+ * Mirrors a project's app/ and server/ folders into an edge project while
5
+ * `vite dev` runs, so every phone paired with that project hot-reloads from
6
+ * the nearest Cloudflare location, not from this machine.
7
+ */
8
+ export declare function nsEdge(ref: ProjectRef): Plugin;
package/dist/vite.js ADDED
@@ -0,0 +1,40 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { relative, sep } from 'node:path';
3
+ import { applyChanges, collect, report, SYNCED } from './sync.js';
4
+ /**
5
+ * Mirrors a project's app/ and server/ folders into an edge project while
6
+ * `vite dev` runs, so every phone paired with that project hot-reloads from
7
+ * the nearest Cloudflare location, not from this machine.
8
+ */
9
+ export function nsEdge(ref) {
10
+ let root = process.cwd();
11
+ return {
12
+ name: 'ns-edge',
13
+ apply: 'serve',
14
+ configResolved(config) {
15
+ root = config.root;
16
+ },
17
+ async configureServer(server) {
18
+ const log = (s) => server.config.logger.info(`[ns-edge] ${s}`, { timestamp: true });
19
+ report(await applyChanges(ref, await collect(root)), log);
20
+ const pending = new Map();
21
+ let timer;
22
+ const queue = (file, deleted) => {
23
+ const rel = relative(root, file).split(sep).join('/');
24
+ if (!SYNCED.test(rel))
25
+ return;
26
+ pending.set(rel, deleted ? null : file);
27
+ clearTimeout(timer);
28
+ timer = setTimeout(async () => {
29
+ const batch = [...pending];
30
+ pending.clear();
31
+ const changes = await Promise.all(batch.map(async ([path, abs]) => ({ path, content: abs ? await readFile(abs, 'utf8') : null })));
32
+ report(await applyChanges(ref, changes), log);
33
+ }, 80);
34
+ };
35
+ server.watcher.on('add', (f) => queue(f, false));
36
+ server.watcher.on('change', (f) => queue(f, false));
37
+ server.watcher.on('unlink', (f) => queue(f, true));
38
+ },
39
+ };
40
+ }
package/package.json CHANGED
@@ -1,6 +1,43 @@
1
1
  {
2
2
  "name": "@nativescript/cloudflare-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0-beta.1",
4
+ "description": "Mirror a NativeScript project into an ns-edge project from a terminal or a Vite dev server",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "bin": {
8
+ "ns-edge": "./dist/bin.js"
9
+ },
10
+ "exports": {
11
+ ".": "./dist/sync.js",
12
+ "./vite": "./dist/vite.js"
13
+ },
14
+ "scripts": {
15
+ "build": "tsc -p .",
16
+ "typecheck": "tsc -p . --noEmit",
17
+ "prepack": "npm run build"
18
+ },
19
+ "dependencies": {
20
+ "qrcode": "^1.5.4"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^24.0.0",
24
+ "@types/qrcode": "^1.5.6",
25
+ "typescript": "^6.0.0",
26
+ "vite": "^8.3.5"
27
+ },
28
+ "peerDependencies": {
29
+ "vite": ">=6"
30
+ },
31
+ "peerDependenciesMeta": {
32
+ "vite": {
33
+ "optional": true
34
+ }
35
+ },
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "tag": "beta"
42
+ }
43
+ }