naro-cli 0.29.0 → 0.30.0
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 +44 -1
- package/dist/{chunk-BAP7PHBF.js → chunk-ITTYPT3A.js} +251 -94
- package/dist/deploy.js +1 -1
- package/dist/functions/deploy.d.ts +7 -0
- package/dist/functions/retry.d.ts +16 -1
- package/dist/index.js +206 -29
- package/dist/{stack-PDV3NPWC.js → stack-DFLT7AVX.js} +41 -22
- package/package.json +8 -9
package/dist/deploy.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { bootCheck, bundleFunction } from "@next-deploy/functions-bundler";
|
|
2
2
|
import { type FunctionUploadTarget, type NaroClient } from "@next-deploy/sdk";
|
|
3
|
+
/**
|
|
4
|
+
* A function's own failures while its version is made: said, and the run
|
|
5
|
+
* goes on with the next. FUNCTION_BUSY is not one — the lease is the
|
|
6
|
+
* project's worker's (design 2026-11-08 G5), so the next function would
|
|
7
|
+
* meet it too; it is retried, then stops the run.
|
|
8
|
+
*/
|
|
3
9
|
export declare const PER_FUNCTION_CODES: ReadonlySet<string>;
|
|
4
10
|
export interface DeploySteps {
|
|
5
11
|
bundle: typeof bundleFunction;
|
|
@@ -67,4 +73,5 @@ export declare function assertFunctionsNode(version?: string): void;
|
|
|
67
73
|
*/
|
|
68
74
|
export declare function uploadTimeoutMs(upload: FunctionUploadTarget, now?: number): number;
|
|
69
75
|
export declare const deadlineReached: (name: string, deadlineMs: number) => string;
|
|
76
|
+
export declare const batchDeadlineReached: (count: number, deadlineMs: number) => string;
|
|
70
77
|
export declare function deployFunctions(options: DeployFunctionsOptions): Promise<DeployFunctionsResult>;
|
|
@@ -6,7 +6,22 @@ export declare const FUNCTION_RATE_LIMIT_MAX_SECONDS = 300;
|
|
|
6
6
|
export declare function defaultSleep(ms: number): Promise<void>;
|
|
7
7
|
/** `details.retryAfter` rounded up, at most 300; 60 when it is missing. */
|
|
8
8
|
export declare function retryAfterSeconds(error: NaroError): number;
|
|
9
|
-
|
|
9
|
+
/** What a retry says, for one function or for a batch of them. */
|
|
10
|
+
export interface RetryWords {
|
|
11
|
+
rateLimited(seconds: number, attempt: number, of: number): string;
|
|
12
|
+
busy(attempt: number, of: number): string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Several functions put live in one call (design 2026-11-08 G4): the lease
|
|
16
|
+
* the server waits on is the project's worker, not one function's.
|
|
17
|
+
*/
|
|
18
|
+
export declare const batchRetryWords: (count: number) => RetryWords;
|
|
19
|
+
/**
|
|
20
|
+
* `op` again after FUNCTIONS_RATE_LIMITED and FUNCTION_BUSY, judged by the
|
|
21
|
+
* code alone (the words are the server's to change). `name` is the
|
|
22
|
+
* function's, or the words of a batch.
|
|
23
|
+
*/
|
|
24
|
+
export declare function withFunctionRetries<T>(op: () => Promise<T>, name: string | RetryWords, io: {
|
|
10
25
|
sleep: (ms: number) => Promise<void>;
|
|
11
26
|
log: (line: string) => void;
|
|
12
27
|
/** Tries after FUNCTIONS_RATE_LIMITED: 3 by default; naro-mcp passes 0. */
|
package/dist/index.js
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
appliedMigrations,
|
|
29
29
|
applyMigrationFiles,
|
|
30
30
|
assertFunctionsNode,
|
|
31
|
+
assertProjectLinkWritable,
|
|
31
32
|
buildResetPlan,
|
|
32
33
|
cannotAsk,
|
|
33
34
|
cellText,
|
|
@@ -66,6 +67,7 @@ import {
|
|
|
66
67
|
isPrintable,
|
|
67
68
|
isReservedEnvTarget,
|
|
68
69
|
isStalledBackend,
|
|
70
|
+
isSymbolicLink,
|
|
69
71
|
isTerminalStatus,
|
|
70
72
|
listMigrationFiles,
|
|
71
73
|
loadStoredSession,
|
|
@@ -136,7 +138,7 @@ import {
|
|
|
136
138
|
warningLines,
|
|
137
139
|
withFunctionRetries,
|
|
138
140
|
writeProjectLink
|
|
139
|
-
} from "./chunk-
|
|
141
|
+
} from "./chunk-ITTYPT3A.js";
|
|
140
142
|
import "./chunk-K3NQKI34.js";
|
|
141
143
|
|
|
142
144
|
// src/args.ts
|
|
@@ -4301,6 +4303,7 @@ async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDep
|
|
|
4301
4303
|
const target = deployTarget(flags);
|
|
4302
4304
|
const flaggedAdapter = flaggedUserAdapter(flags);
|
|
4303
4305
|
const link = flags.project ? { projectId: flags.project, teamSlug: void 0 } : readProjectLink(context.cwd);
|
|
4306
|
+
assertProjectLinkWritable(context.cwd, "deploying");
|
|
4304
4307
|
if (target.production && !flags.yes && !flags.json && isInteractive()) {
|
|
4305
4308
|
const confirmed = await context.prompter.confirm("Deploy to production?");
|
|
4306
4309
|
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
@@ -5154,6 +5157,13 @@ function passwordRecoveryRequired() {
|
|
|
5154
5157
|
message: "updateUser({ password }) works in the password recovery flow: open the link resetPasswordForEmail sent, then call it on that page."
|
|
5155
5158
|
};
|
|
5156
5159
|
}
|
|
5160
|
+
function recoveryForAnotherUser(email) {
|
|
5161
|
+
return {
|
|
5162
|
+
code: "RECOVERY_FOR_ANOTHER_USER",
|
|
5163
|
+
message: "This password reset link is for another account than the one signed in on this browser, so it was not used. To reset that account's password, sign out first. If you did not ask for it, ignore it.",
|
|
5164
|
+
details: { email }
|
|
5165
|
+
};
|
|
5166
|
+
}
|
|
5157
5167
|
function invalidProvider() {
|
|
5158
5168
|
return {
|
|
5159
5169
|
code: "VALIDATION_ERROR",
|
|
@@ -5179,6 +5189,12 @@ function websocketUnavailable() {
|
|
|
5179
5189
|
message: "This runtime has no WebSocket. Pass one to createClient({ realtime: { WebSocket } })."
|
|
5180
5190
|
};
|
|
5181
5191
|
}
|
|
5192
|
+
function websocketHeadersUnavailable() {
|
|
5193
|
+
return {
|
|
5194
|
+
code: "WEBSOCKET_HEADERS_UNAVAILABLE",
|
|
5195
|
+
message: "The service key goes in the apikey header of the realtime connection, which this runtime's WebSocket cannot send. Pass one that takes { headers } \u2014 ws (npm:ws) \u2014 as createClient({ realtime: { WebSocket } })."
|
|
5196
|
+
};
|
|
5197
|
+
}
|
|
5182
5198
|
function realtimeTimedOut(ms) {
|
|
5183
5199
|
const seconds = Math.max(1, Math.round(ms / 1e3));
|
|
5184
5200
|
return {
|
|
@@ -5852,8 +5868,11 @@ function isBrowser(scope = globalThis) {
|
|
|
5852
5868
|
const g = scope;
|
|
5853
5869
|
return g.window !== void 0 && g.document !== void 0;
|
|
5854
5870
|
}
|
|
5855
|
-
function isWorkerd(
|
|
5856
|
-
return
|
|
5871
|
+
function isWorkerd(scope = globalThis) {
|
|
5872
|
+
return scope.navigator?.userAgent === "Cloudflare-Workers";
|
|
5873
|
+
}
|
|
5874
|
+
function isDeno(scope = globalThis) {
|
|
5875
|
+
return typeof scope.Deno?.version?.deno === "string";
|
|
5857
5876
|
}
|
|
5858
5877
|
function hasServerSignal(g) {
|
|
5859
5878
|
return isWorkerd(g) || // Vercel's edge runtime and Next.js's edge (middleware, edge routes).
|
|
@@ -6229,12 +6248,8 @@ var AuthClient = class {
|
|
|
6229
6248
|
await this.#flow.verifiers.take(fragment.naro_flow);
|
|
6230
6249
|
return { data: none, error: flowError(type, fragment.naro_error) };
|
|
6231
6250
|
}
|
|
6232
|
-
if (type === "recovery" && fragment.naro_token)
|
|
6233
|
-
|
|
6234
|
-
await this.#http.accessJwt?.clear();
|
|
6235
|
-
await this.#flow.recovery.keep(fragment.naro_token);
|
|
6236
|
-
return { data: none, error: null };
|
|
6237
|
-
}
|
|
6251
|
+
if (type === "recovery" && fragment.naro_token)
|
|
6252
|
+
return this.#recoveryAnswer(none, fragment.naro_token);
|
|
6238
6253
|
if (fragment.naro_code) {
|
|
6239
6254
|
const traded = await this.exchangeCodeForSession(
|
|
6240
6255
|
fragment.naro_code,
|
|
@@ -6244,6 +6259,45 @@ var AuthClient = class {
|
|
|
6244
6259
|
}
|
|
6245
6260
|
return { data: none, error: null };
|
|
6246
6261
|
}
|
|
6262
|
+
/**
|
|
6263
|
+
* A reset link's token, read on the page it opened (security audit
|
|
6264
|
+
* 2026-11-14 F6). The fragment is whoever sent the link's to write — one
|
|
6265
|
+
* who holds a reset token of their own account can send the landing URL
|
|
6266
|
+
* itself — so the worker is asked whose token it is: the answer names the
|
|
6267
|
+
* account (masked, `data.email`, for the page to show) and whether it is
|
|
6268
|
+
* the one signed in here.
|
|
6269
|
+
*
|
|
6270
|
+
* Signed in as another account, the link is not used: the session stays,
|
|
6271
|
+
* the token is not kept, and the answer is RECOVERY_FOR_ANOTHER_USER — a
|
|
6272
|
+
* password typed here cannot land on the sender's account. Otherwise,
|
|
6273
|
+
* like supabase-js's recovery session, the link takes the place of
|
|
6274
|
+
* whoever was signed in on this browser (the reset ends every session of
|
|
6275
|
+
* that account anyway, PR #68 review 6): `updateUser` then acts on the
|
|
6276
|
+
* link's account alone. A token the worker does not know is INVALID_TOKEN
|
|
6277
|
+
* here, and nothing is kept.
|
|
6278
|
+
*/
|
|
6279
|
+
async #recoveryAnswer(none, token) {
|
|
6280
|
+
const result = await call(this.#http, {
|
|
6281
|
+
method: "POST",
|
|
6282
|
+
path: "/auth/recovery-owner",
|
|
6283
|
+
body: { token },
|
|
6284
|
+
withToken: true,
|
|
6285
|
+
// The page's queries wait on this; they must not wait forever.
|
|
6286
|
+
timeoutMs: EXCHANGE_TIMEOUT_MS
|
|
6287
|
+
});
|
|
6288
|
+
if (result.error !== null) return { data: none, error: result.error };
|
|
6289
|
+
const { body } = result.answer;
|
|
6290
|
+
const owner = isRecord(body) && isRecord(body.data) ? body.data : null;
|
|
6291
|
+
if (owner === null)
|
|
6292
|
+
return { data: none, error: invalidResponse(result.answer.status) };
|
|
6293
|
+
const email = typeof owner.email === "string" ? owner.email : null;
|
|
6294
|
+
if (owner.self === false)
|
|
6295
|
+
return { data: none, error: recoveryForAnotherUser(email) };
|
|
6296
|
+
await this.#http.tokens.clear();
|
|
6297
|
+
await this.#http.accessJwt?.clear();
|
|
6298
|
+
await this.#flow.recovery.keep(token);
|
|
6299
|
+
return { data: { ...none, email }, error: null };
|
|
6300
|
+
}
|
|
6247
6301
|
/**
|
|
6248
6302
|
* Emails a password reset link, when the address has an account. The
|
|
6249
6303
|
* answer is the same either way — it does not say whether it had one.
|
|
@@ -6267,11 +6321,12 @@ var AuthClient = class {
|
|
|
6267
6321
|
* "(P1-2 리뷰 반영)".)
|
|
6268
6322
|
*
|
|
6269
6323
|
* Like supabase-js, it acts on the recovery link's account, which took the
|
|
6270
|
-
* place of any signed-in session when the link was read
|
|
6271
|
-
*
|
|
6272
|
-
*
|
|
6273
|
-
*
|
|
6274
|
-
*
|
|
6324
|
+
* place of any signed-in session when the link was read (a link for
|
|
6325
|
+
* another account than the signed-in one is not read, `#recoveryAnswer`)
|
|
6326
|
+
* — so the token is used only while no session is signed in on this
|
|
6327
|
+
* browser, for 15 minutes after the link was opened, and never after a
|
|
6328
|
+
* sign-in, sign-up or sign-out forgot it (PR #68 review 6). Otherwise
|
|
6329
|
+
* PASSWORD_RECOVERY_REQUIRED, with no request.
|
|
6275
6330
|
*/
|
|
6276
6331
|
async updateUser(attributes) {
|
|
6277
6332
|
await this.#pendingUrl();
|
|
@@ -7966,8 +8021,76 @@ function channelNameProblem(name) {
|
|
|
7966
8021
|
function socketUrl(baseUrl, apiKey) {
|
|
7967
8022
|
const url = new URL("/_naro/v1/realtime/websocket", baseUrl);
|
|
7968
8023
|
url.protocol = url.protocol === "http:" ? "ws:" : "wss:";
|
|
7969
|
-
|
|
7970
|
-
}
|
|
8024
|
+
const key = apiKey === null ? "" : `apikey=${encodeURIComponent(apiKey)}&`;
|
|
8025
|
+
return `${url.href}?${key}vsn=${REALTIME_PROTOCOL_VERSION}`;
|
|
8026
|
+
}
|
|
8027
|
+
var UpgradeSocket = class {
|
|
8028
|
+
onopen = null;
|
|
8029
|
+
onmessage = null;
|
|
8030
|
+
onclose = null;
|
|
8031
|
+
onerror = null;
|
|
8032
|
+
#ws = null;
|
|
8033
|
+
/** `close()` was called, or the open failed: no event goes out after. */
|
|
8034
|
+
#closed = null;
|
|
8035
|
+
constructor(fetchLike, url, headers) {
|
|
8036
|
+
let pending;
|
|
8037
|
+
try {
|
|
8038
|
+
pending = fetchLike(url, {
|
|
8039
|
+
headers: { Upgrade: "websocket", ...headers }
|
|
8040
|
+
});
|
|
8041
|
+
} catch {
|
|
8042
|
+
pending = Promise.reject(new Error("fetch threw"));
|
|
8043
|
+
}
|
|
8044
|
+
pending.then(
|
|
8045
|
+
(response) => this.#arrived(response),
|
|
8046
|
+
() => this.#failed()
|
|
8047
|
+
);
|
|
8048
|
+
}
|
|
8049
|
+
#arrived(response) {
|
|
8050
|
+
const ws = response.webSocket ?? null;
|
|
8051
|
+
if (ws === null) {
|
|
8052
|
+
void response.body?.cancel?.().catch(() => void 0);
|
|
8053
|
+
this.#failed();
|
|
8054
|
+
return;
|
|
8055
|
+
}
|
|
8056
|
+
if (this.#closed !== null) {
|
|
8057
|
+
try {
|
|
8058
|
+
ws.accept();
|
|
8059
|
+
ws.close(this.#closed.code, this.#closed.reason);
|
|
8060
|
+
} catch {
|
|
8061
|
+
}
|
|
8062
|
+
return;
|
|
8063
|
+
}
|
|
8064
|
+
this.#ws = ws;
|
|
8065
|
+
ws.addEventListener("message", (event) => {
|
|
8066
|
+
this.onmessage?.({ data: event.data });
|
|
8067
|
+
});
|
|
8068
|
+
ws.addEventListener("close", (event) => {
|
|
8069
|
+
this.onclose?.({ code: event.code, reason: event.reason ?? "" });
|
|
8070
|
+
});
|
|
8071
|
+
ws.addEventListener("error", () => {
|
|
8072
|
+
this.onerror?.();
|
|
8073
|
+
});
|
|
8074
|
+
ws.accept();
|
|
8075
|
+
this.onopen?.();
|
|
8076
|
+
}
|
|
8077
|
+
/** No connection came of it: a close before the open. */
|
|
8078
|
+
#failed() {
|
|
8079
|
+
if (this.#closed !== null) return;
|
|
8080
|
+
this.#closed = { code: void 0, reason: void 0 };
|
|
8081
|
+
this.onclose?.({ code: 1006, reason: "" });
|
|
8082
|
+
}
|
|
8083
|
+
send(data) {
|
|
8084
|
+
if (this.#ws === null)
|
|
8085
|
+
throw new Error("The realtime connection is not open.");
|
|
8086
|
+
this.#ws.send(data);
|
|
8087
|
+
}
|
|
8088
|
+
close(code, reason) {
|
|
8089
|
+
if (this.#closed !== null) return;
|
|
8090
|
+
this.#closed = { code, reason };
|
|
8091
|
+
this.#ws?.close(code, reason);
|
|
8092
|
+
}
|
|
8093
|
+
};
|
|
7971
8094
|
var HEARTBEAT_MS = 25e3;
|
|
7972
8095
|
var TIMEOUT_MS = 1e4;
|
|
7973
8096
|
var PACED_BURST = CONNECTION_FRAME_BURST - 20;
|
|
@@ -8381,19 +8504,19 @@ var RealtimeSocket = class {
|
|
|
8381
8504
|
}, this.#timeoutMs);
|
|
8382
8505
|
}
|
|
8383
8506
|
#open() {
|
|
8384
|
-
const
|
|
8507
|
+
const passed = this.#config.options.WebSocket;
|
|
8508
|
+
const runtimes = globalThis.WebSocket;
|
|
8509
|
+
const Impl = passed ?? runtimes;
|
|
8385
8510
|
if (typeof Impl !== "function")
|
|
8386
8511
|
return this.#stop(websocketUnavailable(), true);
|
|
8387
8512
|
if (this.#config.service && !isKnownServer())
|
|
8388
8513
|
return this.#stop(serviceKeyOffServer(), true);
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
return this.#stop(socketRefused(error), true);
|
|
8396
|
-
}
|
|
8514
|
+
const made = this.#config.service ? this.#serviceSocket(
|
|
8515
|
+
Impl,
|
|
8516
|
+
passed === void 0 || passed === runtimes
|
|
8517
|
+
) : this.#anonSocket(Impl);
|
|
8518
|
+
if ("error" in made) return this.#stop(made.error, true);
|
|
8519
|
+
const ws = made.ws;
|
|
8397
8520
|
this.#generation += 1;
|
|
8398
8521
|
this.#ws = ws;
|
|
8399
8522
|
this.#state = "connecting";
|
|
@@ -8408,6 +8531,48 @@ var RealtimeSocket = class {
|
|
|
8408
8531
|
this.#timeoutMs
|
|
8409
8532
|
);
|
|
8410
8533
|
}
|
|
8534
|
+
/**
|
|
8535
|
+
* The anon key's connection: the one place a key goes into a URL (design
|
|
8536
|
+
* §10 "3-2d") — a browser's WebSocket has no headers to put it in.
|
|
8537
|
+
*/
|
|
8538
|
+
#anonSocket(Impl) {
|
|
8539
|
+
try {
|
|
8540
|
+
return {
|
|
8541
|
+
ws: new Impl(socketUrl(this.#config.baseUrl, this.#config.apiKey))
|
|
8542
|
+
};
|
|
8543
|
+
} catch (error) {
|
|
8544
|
+
return { error: socketRefused(error) };
|
|
8545
|
+
}
|
|
8546
|
+
}
|
|
8547
|
+
/**
|
|
8548
|
+
* The service key's connection (security audit 2026-11-14 F7): the key in
|
|
8549
|
+
* the `apikey` header and never in the URL, where edge and worker logs
|
|
8550
|
+
* would keep it. Node 22+, Bun and `ws` take `{ headers }` on the
|
|
8551
|
+
* constructor; workerd's constructor takes none, so there — unless the
|
|
8552
|
+
* app passed a WebSocket of its own, other than the runtime's — the
|
|
8553
|
+
* connection is a `fetch` upgrade (`UpgradeSocket`); Deno's takes none
|
|
8554
|
+
* either, and has no upgrade fetch, so the service key is refused there
|
|
8555
|
+
* until a WebSocket that takes headers (`ws`) is passed. `runtimes` says
|
|
8556
|
+
* the class is the runtime's own.
|
|
8557
|
+
*/
|
|
8558
|
+
#serviceSocket(Impl, runtimes) {
|
|
8559
|
+
const headers = { apikey: this.#config.apiKey };
|
|
8560
|
+
const url = socketUrl(this.#config.baseUrl, null);
|
|
8561
|
+
if (runtimes && isWorkerd())
|
|
8562
|
+
return {
|
|
8563
|
+
ws: new UpgradeSocket(
|
|
8564
|
+
this.#config.fetch ?? globalThis.fetch,
|
|
8565
|
+
`http${url.slice(2)}`,
|
|
8566
|
+
headers
|
|
8567
|
+
)
|
|
8568
|
+
};
|
|
8569
|
+
if (runtimes && isDeno()) return { error: websocketHeadersUnavailable() };
|
|
8570
|
+
try {
|
|
8571
|
+
return { ws: new Impl(url, { headers }) };
|
|
8572
|
+
} catch (error) {
|
|
8573
|
+
return { error: socketRefused(error) };
|
|
8574
|
+
}
|
|
8575
|
+
}
|
|
8411
8576
|
#opened(ws) {
|
|
8412
8577
|
if (this.#ws !== ws || this.#state !== "connecting") return;
|
|
8413
8578
|
clearTimeout(this.#openTimer);
|
|
@@ -9591,6 +9756,7 @@ var RealtimeClient = class {
|
|
|
9591
9756
|
apiKey: http.apiKey,
|
|
9592
9757
|
service,
|
|
9593
9758
|
options,
|
|
9759
|
+
fetch: http.fetch,
|
|
9594
9760
|
events: {
|
|
9595
9761
|
// Not awaited, and not live yet: onLive says when the hub took it.
|
|
9596
9762
|
opened: () => {
|
|
@@ -12688,6 +12854,12 @@ function parsePullSurface(value) {
|
|
|
12688
12854
|
async function runEnvPull(context) {
|
|
12689
12855
|
const file = context.positionals[0] ?? ".env.local";
|
|
12690
12856
|
const surface = parsePullSurface(context.flags.env);
|
|
12857
|
+
const path = resolve5(context.cwd, file);
|
|
12858
|
+
if (isSymbolicLink(path))
|
|
12859
|
+
throw new NaroError(
|
|
12860
|
+
"VALIDATION_FAILED",
|
|
12861
|
+
`${file} is a symbolic link. Remove it, or pass another file name.`
|
|
12862
|
+
);
|
|
12691
12863
|
const client = await context.client();
|
|
12692
12864
|
const projectId = await context.resolveProjectId();
|
|
12693
12865
|
if (!isReservedEnvTarget(surface))
|
|
@@ -12697,7 +12869,6 @@ async function runEnvPull(context) {
|
|
|
12697
12869
|
const count2 = Object.keys(vars).length;
|
|
12698
12870
|
const text = `# Pulled from Naro (${surface}) \u2014 do not commit secrets.
|
|
12699
12871
|
${serializeDotenv(vars)}`;
|
|
12700
|
-
const path = resolve5(context.cwd, file);
|
|
12701
12872
|
if (existsSync(path) && isInteractive() && !context.flags.yes && !context.flags.json) {
|
|
12702
12873
|
const confirmed = await context.prompter.confirm(`Overwrite ${file}?`);
|
|
12703
12874
|
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
@@ -13098,7 +13269,7 @@ function functionName(context, usage) {
|
|
|
13098
13269
|
}
|
|
13099
13270
|
function statusWord(fn) {
|
|
13100
13271
|
if (fn.status !== "error") return fn.status;
|
|
13101
|
-
return `error
|
|
13272
|
+
return `error: ${printable(fn.error ?? "unknown error")}`;
|
|
13102
13273
|
}
|
|
13103
13274
|
var cpuHours = (ms) => (ms / 36e5).toLocaleString("en-US", { maximumFractionDigits: 1 });
|
|
13104
13275
|
function functionLines(list) {
|
|
@@ -13119,8 +13290,13 @@ function functionLines(list) {
|
|
|
13119
13290
|
return `${fn.name.padEnd(width)} ${version2.padEnd(versionWidth)} ${verify} ${statusWord(fn)} ${fn.url}${stale}`;
|
|
13120
13291
|
});
|
|
13121
13292
|
lines.push(
|
|
13122
|
-
`Up to ${list.limits.functions.toLocaleString("en-US")} functions per
|
|
13293
|
+
`Up to ${list.limits.functions.toLocaleString("en-US")} functions per project.`
|
|
13123
13294
|
);
|
|
13295
|
+
const today = list.today;
|
|
13296
|
+
if (today)
|
|
13297
|
+
lines.push(
|
|
13298
|
+
`Functions today (UTC, the project's): ${today.invocations.toLocaleString("en-US")} calls, ${today.cpuMs.toLocaleString("en-US")} ms of CPU.`
|
|
13299
|
+
);
|
|
13124
13300
|
const usage = list.usage;
|
|
13125
13301
|
if (usage !== null) {
|
|
13126
13302
|
const percent2 = usage.cpuMsLimit > 0 ? Math.floor(usage.cpuMs / usage.cpuMsLimit * 100) : 0;
|
|
@@ -14555,7 +14731,8 @@ Usage:
|
|
|
14555
14731
|
naro functions new <name> Write naro/functions/<name>/index.ts, a handler to start from
|
|
14556
14732
|
naro functions deploy [name\u2026] [--no-verify-jwt] [--import-map <file>] [--skip-check]
|
|
14557
14733
|
Bundle on this machine (Node.js 22 or later), check that it starts, upload;
|
|
14558
|
-
every function in the folder when no name is given
|
|
14734
|
+
every function in the folder when no name is given;
|
|
14735
|
+
several go live together in one upload
|
|
14559
14736
|
naro functions versions <name> Its stored versions, newest first
|
|
14560
14737
|
naro functions rollback <name> [<version>]
|
|
14561
14738
|
Put an earlier version live again (the one before the live one by default)
|
|
@@ -4,9 +4,43 @@ import "./chunk-K3NQKI34.js";
|
|
|
4
4
|
// ../../packages/build/src/stack.ts
|
|
5
5
|
import { readFile, stat } from "fs/promises";
|
|
6
6
|
import { join } from "path";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
var STACKS = [
|
|
8
|
+
{ slug: "nextjs", detectors: { every: [{ matchPackage: "next" }] } },
|
|
9
|
+
{ slug: "gatsby", detectors: { every: [{ matchPackage: "gatsby" }] } },
|
|
10
|
+
{ slug: "astro", detectors: { every: [{ matchPackage: "astro" }] } },
|
|
11
|
+
{
|
|
12
|
+
slug: "docusaurus-2",
|
|
13
|
+
detectors: { some: [{ matchPackage: "@docusaurus/core" }] }
|
|
14
|
+
},
|
|
15
|
+
{ slug: "angular", detectors: { every: [{ matchPackage: "@angular/cli" }] } },
|
|
16
|
+
// Vercel spells this one as a content pattern on package.json — the very
|
|
17
|
+
// pattern matchPackage builds (see detectorMatches).
|
|
18
|
+
{
|
|
19
|
+
slug: "sveltekit-1",
|
|
20
|
+
detectors: { every: [{ matchPackage: "@sveltejs/kit" }] }
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
slug: "create-react-app",
|
|
24
|
+
detectors: {
|
|
25
|
+
some: [
|
|
26
|
+
{ matchPackage: "react-scripts" },
|
|
27
|
+
{ matchPackage: "react-dev-utils" }
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
slug: "nuxtjs",
|
|
33
|
+
detectors: {
|
|
34
|
+
some: [
|
|
35
|
+
{ matchPackage: "nuxt" },
|
|
36
|
+
{ matchPackage: "nuxt3" },
|
|
37
|
+
{ matchPackage: "nuxt-edge" },
|
|
38
|
+
{ matchPackage: "nuxt-nightly" }
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{ slug: "vite", detectors: { every: [{ matchPackage: "vite" }] } }
|
|
43
|
+
];
|
|
10
44
|
var PRESET_STACKS = {
|
|
11
45
|
next: "nextjs",
|
|
12
46
|
"next-export": "nextjs",
|
|
@@ -51,8 +85,7 @@ async function detectorMatches(reader, { path, matchContent, matchPackage }) {
|
|
|
51
85
|
if (!await reader.isFile(file)) return false;
|
|
52
86
|
return new RegExp(pattern, "m").test(await reader.readFile(file));
|
|
53
87
|
}
|
|
54
|
-
async function
|
|
55
|
-
const { every, some } = framework.detectors ?? {};
|
|
88
|
+
async function stackMatches(reader, { detectors: { every, some } }) {
|
|
56
89
|
if (!every && !some) return false;
|
|
57
90
|
if (every) {
|
|
58
91
|
const results = await Promise.all(
|
|
@@ -68,27 +101,13 @@ async function frameworkMatches(reader, framework) {
|
|
|
68
101
|
}
|
|
69
102
|
return true;
|
|
70
103
|
}
|
|
71
|
-
function removeSuperseded(matches, slug) {
|
|
72
|
-
const index = matches.findIndex((framework2) => framework2?.slug === slug);
|
|
73
|
-
const framework = matches[index];
|
|
74
|
-
if (!framework) return;
|
|
75
|
-
for (const next of framework.supersedes ?? [])
|
|
76
|
-
removeSuperseded(matches, next);
|
|
77
|
-
matches.splice(index, 1);
|
|
78
|
-
}
|
|
79
104
|
async function detectStack(root, reader = localReader(root)) {
|
|
80
|
-
const candidates = frameworkList.filter(
|
|
81
|
-
(framework) => !framework.experimental && framework.slug
|
|
82
|
-
);
|
|
83
105
|
const matches = await Promise.all(
|
|
84
|
-
|
|
85
|
-
async (
|
|
106
|
+
STACKS.map(
|
|
107
|
+
async (stack) => await stackMatches(reader, stack) ? stack.slug : null
|
|
86
108
|
)
|
|
87
109
|
);
|
|
88
|
-
|
|
89
|
-
for (const slug2 of match?.supersedes ?? []) removeSuperseded(matches, slug2);
|
|
90
|
-
}
|
|
91
|
-
const slug = matches.find((match) => match !== null)?.slug ?? null;
|
|
110
|
+
const slug = matches.find((match) => match !== null) ?? null;
|
|
92
111
|
if (slug === "vite") return await viteLibrary(reader) ?? "vite";
|
|
93
112
|
return slug && STACK_SLUG_PATTERN.test(slug) ? slug : null;
|
|
94
113
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "naro-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"naro": "./dist/index.js"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"access": "public"
|
|
14
14
|
},
|
|
15
15
|
"engines": {
|
|
16
|
-
"node": ">=
|
|
16
|
+
"node": ">=22"
|
|
17
17
|
},
|
|
18
18
|
"exports": {
|
|
19
19
|
".": {
|
|
@@ -24,26 +24,25 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@cloudflare/unenv-preset": "2.16.2",
|
|
26
26
|
"@supabase/supabase-js": "2.112.3",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"miniflare": "5.20261006.0-alpha",
|
|
27
|
+
"esbuild": "0.28.2",
|
|
28
|
+
"miniflare": "5.20261104.0-beta",
|
|
30
29
|
"unenv": "2.0.0-rc.24",
|
|
31
30
|
"vinext": "1.1.0",
|
|
32
|
-
"wrangler": "4.
|
|
31
|
+
"wrangler": "4.158.0"
|
|
33
32
|
},
|
|
34
33
|
"devDependencies": {
|
|
35
34
|
"tsup": "8.5.0",
|
|
36
35
|
"tsx": "4.23.12",
|
|
37
36
|
"typescript": "7.0.2",
|
|
38
37
|
"vitest": "3.2.7",
|
|
38
|
+
"@next-deploy/build": "0.1.0",
|
|
39
39
|
"naro-js": "0.9.0",
|
|
40
|
+
"@next-deploy/config": "0.1.0",
|
|
40
41
|
"@next-deploy/core": "0.1.0",
|
|
41
42
|
"@next-deploy/data-core": "0.1.0",
|
|
42
43
|
"@next-deploy/functions-runtime": "0.1.0",
|
|
43
44
|
"@next-deploy/sdk": "0.1.0",
|
|
44
|
-
"@next-deploy/
|
|
45
|
-
"@next-deploy/functions-bundler": "0.1.0",
|
|
46
|
-
"@next-deploy/build": "0.1.0"
|
|
45
|
+
"@next-deploy/functions-bundler": "0.1.0"
|
|
47
46
|
},
|
|
48
47
|
"scripts": {
|
|
49
48
|
"build": "tsup && pnpm build:types",
|