@nativescript/cloudflare-cli 0.1.0-beta.0 → 0.1.0-beta.10
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 +12 -1
- package/dist/bin.js +230 -6
- package/dist/sync.d.ts +8 -1
- package/dist/sync.js +12 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
# @nativescript/cloudflare-cli
|
|
2
2
|
|
|
3
|
+
```sh
|
|
4
|
+
npx @nativescript/cloudflare-cli live # a phone app: opens the IDE and prints a QR code for NativeScript Preview
|
|
5
|
+
npx @nativescript/cloudflare-cli live --react # the same with a framework: --ng (Angular), --react, --vue, --svelte or --solid
|
|
6
|
+
npx @nativescript/cloudflare-cli live --mac # a Mac app: opens the IDE and runs the app on this Mac, streamed into the IDE
|
|
7
|
+
npx @nativescript/cloudflare-cli live --electron # an Electron app with native views, run the same way
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
`live` creates a project on preview.nativescript.org (`--server` or `NS_EDGE_SERVER` for another edge), saves it to `.ns-edge.json` and opens its IDE. The framework flags are `ns create`'s (`--ng`/`--angular`, `--react`, `--vue`, `--svelte`, `--solid`, `--ts`); without one a phone project is plain TypeScript with XML views. Run it again in the same folder to reopen that project (no flag needed), or add `--new` for another; a flag for a different kind of project than the folder's asks for `--new`. `--from <link>` starts from a copy of a shared project instead, such as a sample (its link, or a bare id on the same server). While `live` runs a Mac app it restarts the app if it crashes, unless it crashes three times in a minute. Without a create token the project is free: a few hosted agent turns a day, no backend, deleted after 30 days unused. `--no-open` skips the browser; with `--mac` or `--electron`, `--allow-eval` lets the agent run code on this Mac.
|
|
11
|
+
|
|
12
|
+
## Mirror a local project
|
|
13
|
+
|
|
3
14
|
Mirror a NativeScript project into an ns-edge project from a terminal or a Vite dev server.
|
|
4
15
|
|
|
5
16
|
```sh
|
|
6
17
|
npm i -D @nativescript/cloudflare-cli@beta
|
|
7
|
-
npx ns-edge create --server https://preview.nativescript.org --token <create token>
|
|
18
|
+
npx ns-edge create --server https://preview.nativescript.org --token <create token> # --template vue for nativescript-vue
|
|
8
19
|
npx ns-edge watch # push, then every save
|
|
9
20
|
npx ns-edge qr # print the pairing QR code again
|
|
10
21
|
```
|
package/dist/bin.js
CHANGED
|
@@ -1,24 +1,100 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { existsSync, watch } from 'node:fs';
|
|
3
4
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
4
|
-
import { join, sep } from 'node:path';
|
|
5
|
+
import { dirname, join, sep } from 'node:path';
|
|
6
|
+
import { createInterface } from 'node:readline/promises';
|
|
5
7
|
import QRCode from 'qrcode';
|
|
6
8
|
import { applyChanges, collect, createProject, projectUrl, report, SYNCED } from './sync.js';
|
|
7
9
|
const CONFIG = '.ns-edge.json';
|
|
8
10
|
const DEBOUNCE_MS = 120;
|
|
11
|
+
const LIVE_SERVER = process.env.NS_EDGE_SERVER ?? 'https://preview.nativescript.org';
|
|
12
|
+
/** The hosts `live --desktop` (or `--mac`, `--windows`), `--electron` and `--expo` run; bump them with each host release this CLI is tested against. */
|
|
13
|
+
const DESKTOP_HOSTS = {
|
|
14
|
+
macos: { spec: '@nativescript/cloudflare-desktop@0.1.0-alpha.6', bin: 'ns-edge-desktop' },
|
|
15
|
+
electron: { spec: '@nativescript/cloudflare-electron@0.1.0-alpha.3', bin: 'ns-edge-electron' },
|
|
16
|
+
expo: { spec: '@nativescript/cloudflare-expo-desktop@0.1.0-alpha.1', bin: 'ns-edge-expo-desktop' },
|
|
17
|
+
};
|
|
18
|
+
const DESKTOP_TEMPLATES = new Set(['macos', 'electron', 'expo']);
|
|
19
|
+
/** The macos template is the desktop one: it runs on a Mac with AppKit and on Windows with WinUI, so all three flags pick it. */
|
|
20
|
+
const DESKTOP_FLAGS = { macos: ['desktop', 'mac', 'windows'], electron: ['electron'], expo: ['expo'] };
|
|
21
|
+
/** The NativeScript CLI's `ns create` framework flags, so `live` and `create` take the same ones. */
|
|
22
|
+
const FRAMEWORK_FLAGS = {
|
|
23
|
+
angular: ['ng', 'angular'],
|
|
24
|
+
react: ['react', 'reactjs'],
|
|
25
|
+
vue: ['vue', 'vuejs'],
|
|
26
|
+
svelte: ['svelte'],
|
|
27
|
+
solid: ['solid', 'solidjs'],
|
|
28
|
+
core: ['ts', 'tsc', 'typescript'],
|
|
29
|
+
};
|
|
30
|
+
const TEMPLATE_KINDS = {
|
|
31
|
+
core: 'a phone',
|
|
32
|
+
angular: 'an Angular phone',
|
|
33
|
+
react: 'a React phone',
|
|
34
|
+
vue: 'a Vue phone',
|
|
35
|
+
svelte: 'a Svelte phone',
|
|
36
|
+
solid: 'a Solid phone',
|
|
37
|
+
macos: 'a desktop',
|
|
38
|
+
electron: 'an Electron',
|
|
39
|
+
expo: 'an Expo Desktop',
|
|
40
|
+
};
|
|
41
|
+
const MAX_CRASHES = 3;
|
|
42
|
+
const CRASH_WINDOW_MS = 60_000;
|
|
43
|
+
const RESTART_DELAY_MS = 1000;
|
|
9
44
|
const [cmd = 'help', ...args] = process.argv.slice(2);
|
|
45
|
+
/**
|
|
46
|
+
* Editors and tools write temp, swap and backup files next to the real one
|
|
47
|
+
* (sed -i's `.!123!file`, vim's `.file.swp`, `file~`). Skipped like collect()
|
|
48
|
+
* skips dotfiles: one such path in a batch makes the edge reject the whole batch.
|
|
49
|
+
*/
|
|
50
|
+
const isScratch = (rel) => rel.split('/').some((part) => part.startsWith('.')) || rel.endsWith('~');
|
|
10
51
|
const flag = (name) => {
|
|
11
52
|
const i = args.indexOf(`--${name}`);
|
|
12
53
|
return i >= 0 ? args[i + 1] : undefined;
|
|
13
54
|
};
|
|
55
|
+
const has = (name) => args.includes(`--${name}`);
|
|
56
|
+
/** The template --template, a framework flag or a desktop flag asks for, if any; asking for two is an error. */
|
|
57
|
+
function requestedTemplate() {
|
|
58
|
+
const asked = new Set();
|
|
59
|
+
const named = flag('template');
|
|
60
|
+
if (named)
|
|
61
|
+
asked.add(named);
|
|
62
|
+
for (const [template, names] of Object.entries(FRAMEWORK_FLAGS))
|
|
63
|
+
if (names.some((name) => has(name)))
|
|
64
|
+
asked.add(template);
|
|
65
|
+
for (const [template, names] of Object.entries(DESKTOP_FLAGS))
|
|
66
|
+
if (names.some((name) => has(name)))
|
|
67
|
+
asked.add(template);
|
|
68
|
+
if (asked.size > 1)
|
|
69
|
+
throw new Error(`Pick one kind of project: this asked for ${[...asked].join(' and ')}.`);
|
|
70
|
+
return [...asked][0];
|
|
71
|
+
}
|
|
14
72
|
const root = process.cwd();
|
|
15
73
|
async function loadRef() {
|
|
74
|
+
let ref;
|
|
16
75
|
try {
|
|
17
|
-
|
|
76
|
+
ref = JSON.parse(await readFile(join(root, CONFIG), 'utf8'));
|
|
18
77
|
}
|
|
19
78
|
catch {
|
|
20
79
|
throw new Error(`No ${CONFIG} here. Run \`ns-edge create --server <url>\` first.`);
|
|
21
80
|
}
|
|
81
|
+
// Copies meant to be shared (the IDE's StackBlitz export) leave the write key out of the file,
|
|
82
|
+
// so it is asked for here and kept in memory only.
|
|
83
|
+
ref.writeKey ||= process.env.NS_EDGE_WRITE_KEY || (await ask("Paste this project's write key (the IDE copied it to your clipboard): "));
|
|
84
|
+
if (!ref.writeKey)
|
|
85
|
+
throw new Error(`No write key: add it to ${CONFIG} or set NS_EDGE_WRITE_KEY.`);
|
|
86
|
+
return ref;
|
|
87
|
+
}
|
|
88
|
+
async function ask(question) {
|
|
89
|
+
if (!process.stdin.isTTY)
|
|
90
|
+
return '';
|
|
91
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
92
|
+
try {
|
|
93
|
+
return (await rl.question(question)).trim();
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
rl.close();
|
|
97
|
+
}
|
|
22
98
|
}
|
|
23
99
|
async function printPairing(ref) {
|
|
24
100
|
const url = projectUrl(ref);
|
|
@@ -27,10 +103,151 @@ async function printPairing(ref) {
|
|
|
27
103
|
console.log(` device ${device}`);
|
|
28
104
|
console.log(` ide ${url}/#key=${ref.writeKey}`);
|
|
29
105
|
}
|
|
106
|
+
/** This folder's project, if it has one the server still knows. */
|
|
107
|
+
/**
|
|
108
|
+
* The project `--from` names: a link to it (its IDE, read-only or device link)
|
|
109
|
+
* or a bare id on this CLI's server, with what it was made as.
|
|
110
|
+
*/
|
|
111
|
+
async function sourceProject(from) {
|
|
112
|
+
let server = flag('server') ?? LIVE_SERVER;
|
|
113
|
+
let id = from;
|
|
114
|
+
if (!/^[0-9a-f]{32}$/.test(from)) {
|
|
115
|
+
const match = URL.canParse(from) ? new URL(from).pathname.match(/^\/p\/([0-9a-f]{32})/) : null;
|
|
116
|
+
if (!match)
|
|
117
|
+
throw new Error(`--from takes a project link (https://…/p/<id>) or id, not '${from}'.`);
|
|
118
|
+
server = new URL(from).origin;
|
|
119
|
+
id = match[1];
|
|
120
|
+
}
|
|
121
|
+
const res = await fetch(`${server}/p/${id}/details`).catch(() => null);
|
|
122
|
+
if (!res?.ok)
|
|
123
|
+
throw new Error(`There's no project ${id} on ${server} to start from.`);
|
|
124
|
+
const { template, title } = (await res.json());
|
|
125
|
+
return { server, id, template, title };
|
|
126
|
+
}
|
|
127
|
+
async function savedProject() {
|
|
128
|
+
if (!existsSync(join(root, CONFIG)))
|
|
129
|
+
return null;
|
|
130
|
+
const ref = JSON.parse(await readFile(join(root, CONFIG), 'utf8'));
|
|
131
|
+
if (!ref.writeKey)
|
|
132
|
+
return null;
|
|
133
|
+
const res = await fetch(`${projectUrl(ref)}/files`).catch(() => null);
|
|
134
|
+
return res?.ok ? ref : null;
|
|
135
|
+
}
|
|
136
|
+
function openInBrowser(url) {
|
|
137
|
+
const [command, ...rest] = process.platform === 'darwin' ? ['open', url] : process.platform === 'win32' ? ['cmd', '/c', 'start', '""', url.replace(/&/g, '^&')] : ['xdg-open', url];
|
|
138
|
+
spawn(command, rest, { stdio: 'ignore', detached: true }).on('error', () => { }).unref();
|
|
139
|
+
}
|
|
140
|
+
const COMPUTER = process.platform === 'win32' ? 'PC' : 'Mac';
|
|
141
|
+
/**
|
|
142
|
+
* How to run npx. On Windows it's npx.cmd, which Node won't spawn without a
|
|
143
|
+
* shell (and a shell would re-parse the arguments), so npm's own npx-cli.js
|
|
144
|
+
* runs with this Node instead when it's where Node's installer puts it.
|
|
145
|
+
*/
|
|
146
|
+
function npx() {
|
|
147
|
+
if (process.platform !== 'win32')
|
|
148
|
+
return ['npx'];
|
|
149
|
+
const cli = join(dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npx-cli.js');
|
|
150
|
+
return existsSync(cli) ? [process.execPath, cli] : ['npx.cmd'];
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Runs the desktop host for a desktop project until it quits or Ctrl+C. A host
|
|
154
|
+
* that crashes is started again, since nothing in the browser can reach this
|
|
155
|
+
* computer to do it, unless it keeps crashing. The host says what joining allows.
|
|
156
|
+
*/
|
|
157
|
+
async function runDesktopHost(ref) {
|
|
158
|
+
const mac = process.platform === 'darwin' && process.arch === 'arm64';
|
|
159
|
+
const windows = process.platform === 'win32' && (process.arch === 'x64' || process.arch === 'arm64');
|
|
160
|
+
if (ref.template === 'expo' && !mac && !(windows && process.arch === 'x64')) {
|
|
161
|
+
throw new Error('Expo Desktop projects run on an Apple Silicon Mac (macOS 14.4 or later) or a Windows x64 PC. Run the same command on one.');
|
|
162
|
+
}
|
|
163
|
+
if (!mac && !windows) {
|
|
164
|
+
throw new Error('Desktop projects run on an Apple Silicon Mac (macOS 14.4 or later) or a Windows 10 (1903 or later) or Windows 11 PC. Run the same command there.');
|
|
165
|
+
}
|
|
166
|
+
const device = `${projectUrl(ref)}#${ref.deviceKey}`;
|
|
167
|
+
const extra = has('allow-eval') ? ['--allow-eval'] : [];
|
|
168
|
+
const host = DESKTOP_HOSTS[ref.template ?? 'macos'] ?? DESKTOP_HOSTS.macos;
|
|
169
|
+
// NS_EDGE_HOST runs another spec of the host (a version, or file:<packed .tgz>) to test a release before publishing it.
|
|
170
|
+
const spec = process.env.NS_EDGE_HOST ?? host.spec;
|
|
171
|
+
let child;
|
|
172
|
+
let stopping = false;
|
|
173
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
174
|
+
process.on(signal, () => {
|
|
175
|
+
stopping = true;
|
|
176
|
+
child?.kill(signal);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
const crashes = [];
|
|
180
|
+
for (;;) {
|
|
181
|
+
const [command, ...prefix] = npx();
|
|
182
|
+
child = spawn(command, [...prefix, '-y', '--package', spec, '--', host.bin, device, '--yes', ...extra], { stdio: 'inherit' });
|
|
183
|
+
const exit = await new Promise((resolve, reject) => {
|
|
184
|
+
child.on('error', reject);
|
|
185
|
+
child.on('exit', (code, signal) => resolve({ code, signal }));
|
|
186
|
+
});
|
|
187
|
+
if (stopping || exit.code === 0)
|
|
188
|
+
return;
|
|
189
|
+
const now = Date.now();
|
|
190
|
+
crashes.push(now);
|
|
191
|
+
while (crashes[0] < now - CRASH_WINDOW_MS)
|
|
192
|
+
crashes.shift();
|
|
193
|
+
const why = exit.signal ?? `code ${exit.code}`;
|
|
194
|
+
if (crashes.length > MAX_CRASHES) {
|
|
195
|
+
throw new Error(`The desktop host crashed ${crashes.length} times in a minute (last: ${why}), so it wasn't restarted. Fix the error above, then run this command again.`);
|
|
196
|
+
}
|
|
197
|
+
console.log(`\nThe desktop host stopped (${why}). Starting it again…\n`);
|
|
198
|
+
await new Promise((r) => setTimeout(r, RESTART_DELAY_MS));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
30
201
|
const commands = {
|
|
202
|
+
async live() {
|
|
203
|
+
for (const later of ['ios', 'android', 'all']) {
|
|
204
|
+
if (has(later))
|
|
205
|
+
throw new Error(`--${later} isn't available yet. Run \`live\` for a phone app, or \`live --desktop\`, \`--electron\` or \`--expo\` for a desktop app.`);
|
|
206
|
+
}
|
|
207
|
+
const fromArg = flag('from');
|
|
208
|
+
const source = fromArg ? await sourceProject(fromArg) : null;
|
|
209
|
+
let ref = has('new') || source ? null : await savedProject();
|
|
210
|
+
const requested = requestedTemplate();
|
|
211
|
+
if (source && requested && requested !== source.template) {
|
|
212
|
+
throw new Error(`--from names ${TEMPLATE_KINDS[source.template] ?? `a ${source.template}`} project, so leave out the flag asking for ${TEMPLATE_KINDS[requested] ?? `a ${requested}`} one.`);
|
|
213
|
+
}
|
|
214
|
+
// Without a template flag, this folder's project runs as whatever it was made as.
|
|
215
|
+
const template = source?.template ?? requested ?? ref?.template ?? 'core';
|
|
216
|
+
const desktop = DESKTOP_TEMPLATES.has(template);
|
|
217
|
+
if (ref && requested && (ref.template ?? 'core') !== requested) {
|
|
218
|
+
const current = TEMPLATE_KINDS[ref.template ?? 'core'] ?? `a ${ref.template}`;
|
|
219
|
+
throw new Error(`This folder's project (${CONFIG}) is ${current} project. Add --new to start ${TEMPLATE_KINDS[requested] ?? `a ${requested}`} one, or run this in another folder.`);
|
|
220
|
+
}
|
|
221
|
+
if (ref) {
|
|
222
|
+
console.log(`Opening this folder's project ${ref.id} (${CONFIG})`);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
ref = await createProject(source?.server ?? flag('server') ?? LIVE_SERVER, flag('token') ?? process.env.NS_EDGE_CREATE_TOKEN, template, source?.id);
|
|
226
|
+
await writeFile(join(root, CONFIG), JSON.stringify(ref, null, 2) + '\n');
|
|
227
|
+
const made = source ? `your copy of ${source.title ? `"${source.title}"` : `project ${source.id}`}` : `${TEMPLATE_KINDS[template] ?? `a ${template}`} project`;
|
|
228
|
+
console.log(`Created ${made}, saved to ${CONFIG} (it holds the project's keys: keep it private and out of git).`);
|
|
229
|
+
if (ref.free) {
|
|
230
|
+
console.log(`It's a free project: ${ref.free.agentTurnsPerDay} hosted agent turn${ref.free.agentTurnsPerDay === 1 ? '' : 's'} a day, deleted after ${ref.free.idleDays} days unused. ` +
|
|
231
|
+
'For an unlimited agent, add your own API key in the agent panel or connect Claude Code from Connect agent.');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const ide = `${projectUrl(ref)}/#key=${ref.writeKey}`;
|
|
235
|
+
console.log(`\n IDE ${ide}\n`);
|
|
236
|
+
if (!has('no-open'))
|
|
237
|
+
openInBrowser(ide);
|
|
238
|
+
if (desktop) {
|
|
239
|
+
const first = template === 'electron' ? ' (the first run downloads Electron)' : template === 'expo' ? ' (the first run downloads the Expo Desktop shell)' : '';
|
|
240
|
+
console.log(`Starting the app on this ${COMPUTER}${first}. Its window streams to the IDE's Device pane. Press Ctrl+C to stop.\n`);
|
|
241
|
+
return runDesktopHost(ref);
|
|
242
|
+
}
|
|
243
|
+
const device = `${projectUrl(ref)}#${ref.deviceKey}`;
|
|
244
|
+
console.log(await QRCode.toString(`${ref.pairScheme ?? 'nsedge'}://pair?url=${encodeURIComponent(device)}`, { type: 'terminal', small: true }));
|
|
245
|
+
console.log(`Scan with your phone's camera to open it in ${ref.pairScheme === 'nspreview' ? 'NativeScript Preview' : 'the edge shell'}, then edit in the IDE.`);
|
|
246
|
+
console.log('Run this command again in this folder to reopen the project.');
|
|
247
|
+
},
|
|
31
248
|
async create() {
|
|
32
249
|
const server = flag('server') ?? 'http://localhost:5173';
|
|
33
|
-
const ref = await createProject(server, flag('token') ?? process.env.NS_EDGE_CREATE_TOKEN);
|
|
250
|
+
const ref = await createProject(server, flag('token') ?? process.env.NS_EDGE_CREATE_TOKEN, requestedTemplate());
|
|
34
251
|
await writeFile(join(root, CONFIG), JSON.stringify(ref, null, 2) + '\n');
|
|
35
252
|
console.log(`Created project ${ref.id} (saved to ${CONFIG}; it holds the project keys, keep it out of git)`);
|
|
36
253
|
await printPairing(ref);
|
|
@@ -74,7 +291,7 @@ const commands = {
|
|
|
74
291
|
if (!file)
|
|
75
292
|
return;
|
|
76
293
|
const rel = file.split(sep).join('/');
|
|
77
|
-
if (SYNCED.test(rel))
|
|
294
|
+
if (SYNCED.test(rel) && !isScratch(rel))
|
|
78
295
|
changed(rel);
|
|
79
296
|
});
|
|
80
297
|
}
|
|
@@ -100,7 +317,14 @@ const commands = {
|
|
|
100
317
|
async help() {
|
|
101
318
|
console.log(`ns-edge <command>
|
|
102
319
|
|
|
103
|
-
|
|
320
|
+
live [--ng|--react|--vue|--svelte|--solid|--desktop|--electron|--expo] [--new] [--from <link>] [--no-open] [--allow-eval]
|
|
321
|
+
create a project (or reopen this folder's), open its IDE, and print a QR code
|
|
322
|
+
for your phone; --ng, --react, --vue, --svelte or --solid picks the framework
|
|
323
|
+
(plain TypeScript with XML views otherwise); --desktop (or --mac, --windows)
|
|
324
|
+
or --electron runs it on this Mac or Windows PC instead, with that OS's own
|
|
325
|
+
controls; --expo runs an Expo Desktop app on this Mac or Windows x64 PC;
|
|
326
|
+
--from starts from a copy of a shared project (a sample's link or id)
|
|
327
|
+
create [--server <url>] [--token <t>] [--ng|--react|--vue|--svelte|--solid|--template <name>]
|
|
104
328
|
create an edge project and save .ns-edge.json
|
|
105
329
|
push upload app/ and server/ in one transaction
|
|
106
330
|
watch push, then stream every save to the edge
|
package/dist/sync.d.ts
CHANGED
|
@@ -5,6 +5,12 @@ export interface ProjectRef {
|
|
|
5
5
|
deviceKey: string;
|
|
6
6
|
/** URL scheme of the app the server pairs with, from its create response. */
|
|
7
7
|
pairScheme?: string;
|
|
8
|
+
template?: string;
|
|
9
|
+
/** Set on free projects: their daily agent allowance and how long they last unused. */
|
|
10
|
+
free?: {
|
|
11
|
+
agentTurnsPerDay: number;
|
|
12
|
+
idleDays: number;
|
|
13
|
+
};
|
|
8
14
|
}
|
|
9
15
|
export interface ApplyResult {
|
|
10
16
|
changed: string[];
|
|
@@ -17,7 +23,8 @@ export interface ApplyResult {
|
|
|
17
23
|
/** Project-relative paths that are mirrored to the edge. */
|
|
18
24
|
export declare const SYNCED: RegExp;
|
|
19
25
|
export declare function projectUrl(ref: ProjectRef): string;
|
|
20
|
-
|
|
26
|
+
/** With `from`, the project starts as a copy of that one, which must be a `template` project. */
|
|
27
|
+
export declare function createProject(server: string, token?: string, template?: string, from?: string): Promise<ProjectRef>;
|
|
21
28
|
/** Sends a batch as one edge transaction, so devices see one consistent update. */
|
|
22
29
|
export declare function applyChanges(ref: ProjectRef, changes: {
|
|
23
30
|
path: string;
|
package/dist/sync.js
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2
2
|
import { join, relative, sep } from 'node:path';
|
|
3
3
|
/** Project-relative paths that are mirrored to the edge. */
|
|
4
|
-
export const SYNCED = /^(app|server)\/.+\.(ts|mts|js|mjs|xml|css|json)$/;
|
|
4
|
+
export const SYNCED = /^(app|server)\/.+\.(ts|tsx|mts|js|jsx|mjs|vue|svelte|xml|html|css|json)$/;
|
|
5
5
|
export function projectUrl(ref) {
|
|
6
6
|
return `${ref.server.replace(/\/$/, '')}/p/${ref.id}`;
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/** With `from`, the project starts as a copy of that one, which must be a `template` project. */
|
|
9
|
+
export async function createProject(server, token, template, from) {
|
|
10
|
+
const query = from ? `?from=${encodeURIComponent(from)}` : template ? `?template=${encodeURIComponent(template)}` : '';
|
|
11
|
+
const res = await fetch(`${server.replace(/\/$/, '')}/api/projects${query}`, {
|
|
10
12
|
method: 'POST',
|
|
11
13
|
headers: token ? { authorization: `Bearer ${token}` } : {},
|
|
12
14
|
});
|
|
15
|
+
if (res.status === 401)
|
|
16
|
+
throw new Error(`${server} creates projects only with a create token: pass --token or set NS_EDGE_CREATE_TOKEN.`);
|
|
17
|
+
if (res.status === 404 && from)
|
|
18
|
+
throw new Error(`There's no project ${from} on ${server} to start from.`);
|
|
13
19
|
if (!res.ok)
|
|
14
|
-
throw new Error(`create
|
|
15
|
-
const { id, writeKey, deviceKey, pair } = (await res.json());
|
|
16
|
-
return { server, id, writeKey, deviceKey, pairScheme: pair?.split('://')[0] };
|
|
20
|
+
throw new Error(`Couldn't create a project (${res.status}): ${await res.text()}`);
|
|
21
|
+
const { id, writeKey, deviceKey, pair, free } = (await res.json());
|
|
22
|
+
return { server, id, writeKey, deviceKey, pairScheme: pair?.split('://')[0], template: template ?? 'core', ...(free && { free }) };
|
|
17
23
|
}
|
|
18
24
|
/** Sends a batch as one edge transaction, so devices see one consistent update. */
|
|
19
25
|
export async function applyChanges(ref, changes) {
|
package/package.json
CHANGED