@naru-pub/dev 0.0.0-stage → 1.1.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/LICENSE +661 -0
- package/README.md +169 -2
- package/dist/build.json +4 -0
- package/dist/chunk-CQFKRWOD.mjs +1230 -0
- package/dist/chunk-K7ZVYKB2.mjs +11 -0
- package/dist/cli.mjs +103 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.mjs +7 -0
- package/dist/sdk/1.0.0/naru.d.ts +207 -0
- package/dist/sdk/1.0.0/naru.js +722 -0
- package/dist/sdk/1.1.0/functions.d.ts +206 -0
- package/dist/sdk/1.1.0/naru.d.ts +349 -0
- package/dist/sdk/1.1.0/naru.js +1066 -0
- package/dist/testing.d.ts +1 -0
- package/dist/testing.mjs +7 -0
- package/dist/worker/05f16b7a30ddbe6e522bc3300013f93b30366682-metered.wasm +0 -0
- package/dist/worker/config.capnp +52 -0
- package/dist/worker/seoul.js +15357 -0
- package/package.json +61 -4
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
packagedInfo,
|
|
4
|
+
packagedResources,
|
|
5
|
+
start
|
|
6
|
+
} from "./chunk-CQFKRWOD.mjs";
|
|
7
|
+
|
|
8
|
+
// src/cli.ts
|
|
9
|
+
import { readdirSync } from "node:fs";
|
|
10
|
+
import { join, resolve } from "node:path";
|
|
11
|
+
var HELP = `Usage: naru-dev [site folder] [options]
|
|
12
|
+
|
|
13
|
+
Serves a Naru site locally: its pages, the Data SDK, _naru/ (migrations,
|
|
14
|
+
queries, mutations, routes) on the real edge Worker, one-click owner and
|
|
15
|
+
member sign-in, and uploads.
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--site <name> the site's Naru name (<name>.naru.pub); default localsite
|
|
19
|
+
--port <port> default 4000; 0 picks a free one
|
|
20
|
+
--state <dir> where data is kept; default <site folder>/.naru-dev
|
|
21
|
+
--quiet print only the address
|
|
22
|
+
--version print the version and exit
|
|
23
|
+
--help print this and exit
|
|
24
|
+
`;
|
|
25
|
+
async function main(argv, resources, info) {
|
|
26
|
+
const args = [...argv];
|
|
27
|
+
const flag = (name) => {
|
|
28
|
+
const index = args.indexOf(name);
|
|
29
|
+
return index >= 0 ? (args.splice(index, 1), true) : false;
|
|
30
|
+
};
|
|
31
|
+
const option = (name) => {
|
|
32
|
+
const index = args.indexOf(name);
|
|
33
|
+
if (index < 0) return void 0;
|
|
34
|
+
const value = args[index + 1];
|
|
35
|
+
if (value === void 0 || value.startsWith("--"))
|
|
36
|
+
throw new UsageError(`${name} needs a value.`);
|
|
37
|
+
return args.splice(index, 2)[1];
|
|
38
|
+
};
|
|
39
|
+
const sdk = readdirSync(resources.sdkDir).filter((name) => /^\d+\.\d+\.\d+$/.test(name)).sort();
|
|
40
|
+
const banner = `@naru-pub/dev ${info.version} (naru-pub ${info.commit}), SDK ${sdk.join(", ")}`;
|
|
41
|
+
try {
|
|
42
|
+
if (flag("--help") || flag("-h")) {
|
|
43
|
+
console.log(HELP);
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
if (flag("--version") || flag("-v")) {
|
|
47
|
+
console.log(banner);
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
const quiet = flag("--quiet");
|
|
51
|
+
const port = Number(option("--port") ?? 4e3);
|
|
52
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535)
|
|
53
|
+
throw new UsageError("--port takes a port number.");
|
|
54
|
+
const name = option("--site");
|
|
55
|
+
if (name !== void 0 && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name))
|
|
56
|
+
throw new UsageError(
|
|
57
|
+
"--site takes a Naru login name: lowercase letters, digits, hyphens."
|
|
58
|
+
);
|
|
59
|
+
const stateOption = option("--state");
|
|
60
|
+
const unknown = args.find((arg) => arg.startsWith("-"));
|
|
61
|
+
if (unknown) throw new UsageError(`Unknown option ${unknown}.`);
|
|
62
|
+
if (args.length > 1) throw new UsageError("Give one site folder.");
|
|
63
|
+
const folder = resolve(args[0] ?? ".");
|
|
64
|
+
if (!quiet) console.log(banner);
|
|
65
|
+
const naru = await start(
|
|
66
|
+
{
|
|
67
|
+
site: folder,
|
|
68
|
+
name,
|
|
69
|
+
port,
|
|
70
|
+
state: stateOption ? resolve(stateOption) : join(folder, ".naru-dev"),
|
|
71
|
+
quiet
|
|
72
|
+
},
|
|
73
|
+
resources
|
|
74
|
+
);
|
|
75
|
+
if (quiet) console.log(naru.origin);
|
|
76
|
+
await new Promise((done) => {
|
|
77
|
+
const stop = () => {
|
|
78
|
+
process.off("SIGINT", stop);
|
|
79
|
+
process.off("SIGTERM", stop);
|
|
80
|
+
void naru.stop().then(done);
|
|
81
|
+
};
|
|
82
|
+
process.on("SIGINT", stop);
|
|
83
|
+
process.on("SIGTERM", stop);
|
|
84
|
+
});
|
|
85
|
+
return 0;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error instanceof UsageError) {
|
|
88
|
+
console.error(`naru-dev: ${error.message}
|
|
89
|
+
|
|
90
|
+
${HELP}`);
|
|
91
|
+
return 2;
|
|
92
|
+
}
|
|
93
|
+
console.error(`naru-dev: ${error.message}`);
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
var UsageError = class extends Error {
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// src/bin.ts
|
|
101
|
+
process.exit(
|
|
102
|
+
await main(process.argv.slice(2), packagedResources(), packagedInfo())
|
|
103
|
+
);
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// @naru-pub/dev's public API: shipped as the package's types, and what
|
|
2
|
+
// src/naru.ts implements (it imports these), so the two cannot disagree.
|
|
3
|
+
|
|
4
|
+
export interface StartOptions {
|
|
5
|
+
/** The site's folder: its pages, and `_naru/` if it has one. */
|
|
6
|
+
site: string;
|
|
7
|
+
/**
|
|
8
|
+
* The site's Naru name, as in `<name>.naru.pub`: lowercase letters, digits
|
|
9
|
+
* and hyphens. Default `localsite`.
|
|
10
|
+
*/
|
|
11
|
+
name?: string;
|
|
12
|
+
/** The port to serve on; 0 (the default) picks a free one. */
|
|
13
|
+
port?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Where data, uploads and sign-ins are kept. Default: a new temporary
|
|
16
|
+
* folder, removed by `stop()`. The CLI keeps them in `<site>/.naru-dev/`.
|
|
17
|
+
*/
|
|
18
|
+
state?: string;
|
|
19
|
+
/** Print nothing; `log` still has everything. */
|
|
20
|
+
quiet?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Who signs in: the site's owner, or a test member by name. */
|
|
24
|
+
export type Who = "owner" | (string & {});
|
|
25
|
+
|
|
26
|
+
export interface SignInOptions {
|
|
27
|
+
/** A member's status, kept for later sign-ins too. Default `approved`. */
|
|
28
|
+
status?: "approved" | "pending" | "banned";
|
|
29
|
+
/**
|
|
30
|
+
* The owner's: collections to make private if missing, as Naru's sign-in
|
|
31
|
+
* page does for the collections a page asks for.
|
|
32
|
+
*/
|
|
33
|
+
collections?: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A sign-in, as the SDK keeps it, and a header for your own requests. */
|
|
37
|
+
export interface Credentials {
|
|
38
|
+
accessToken: string;
|
|
39
|
+
/** Milliseconds since the epoch. */
|
|
40
|
+
expiresAt: number;
|
|
41
|
+
/** A member's id; absent for the owner. */
|
|
42
|
+
memberId?: string;
|
|
43
|
+
/** `{ authorization: "Bearer <accessToken>" }`, for fetch. */
|
|
44
|
+
headers: { authorization: string };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* What `signIn` needs of a Playwright `Page` or `BrowserContext` (or any
|
|
49
|
+
* browser driver with the same methods).
|
|
50
|
+
*/
|
|
51
|
+
export interface BrowserTarget {
|
|
52
|
+
addInitScript(script: string): Promise<unknown>;
|
|
53
|
+
evaluate?(expression: string): Promise<unknown>;
|
|
54
|
+
url?(): string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface Naru {
|
|
58
|
+
/** Where the site is served, such as `http://localhost:4000`. */
|
|
59
|
+
readonly origin: string;
|
|
60
|
+
/** The site's Naru name. */
|
|
61
|
+
readonly name: string;
|
|
62
|
+
/** The folder data is kept in. */
|
|
63
|
+
readonly state: string;
|
|
64
|
+
/** Everything printed so far, printed or not (`quiet`). */
|
|
65
|
+
readonly log: string;
|
|
66
|
+
/** `origin` + `path`. */
|
|
67
|
+
url(path?: string): string;
|
|
68
|
+
/**
|
|
69
|
+
* Signs `who` in. Given a page or browser context, its pages on this
|
|
70
|
+
* origin find the session where the SDK keeps it, from their next
|
|
71
|
+
* navigation (and at once, for a page already here). Returns the
|
|
72
|
+
* credentials either way, usable with fetch through `headers`.
|
|
73
|
+
*/
|
|
74
|
+
signIn(
|
|
75
|
+
target: BrowserTarget | null | undefined,
|
|
76
|
+
who: Who,
|
|
77
|
+
options?: SignInOptions,
|
|
78
|
+
): Promise<Credentials>;
|
|
79
|
+
/**
|
|
80
|
+
* Empties the site: its collections, tables and uploads. `_naru/` is
|
|
81
|
+
* published again, its migrations applied to the empty database; members
|
|
82
|
+
* and sign-ins are kept.
|
|
83
|
+
*/
|
|
84
|
+
reset(): Promise<void>;
|
|
85
|
+
/** Stops the servers; removes `state` if `startNaru` made it. */
|
|
86
|
+
stop(): Promise<void>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Serves `options.site` on a local Naru; resolves once it answers. */
|
|
90
|
+
export function startNaru(options: StartOptions): Promise<Naru>;
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Naru Data SDK 1.0.0: browser SDK for data owned by one Naru site.
|
|
3
|
+
* @packageDocumentation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type Json =
|
|
7
|
+
| null
|
|
8
|
+
| boolean
|
|
9
|
+
| number
|
|
10
|
+
| string
|
|
11
|
+
| Json[]
|
|
12
|
+
| { [key: string]: Json };
|
|
13
|
+
|
|
14
|
+
/** @internal */
|
|
15
|
+
declare const revisionBrand: unique symbol;
|
|
16
|
+
/** An opaque concurrency token. Store and return it unchanged. */
|
|
17
|
+
export type Revision = string & { readonly [revisionBrand]: true };
|
|
18
|
+
|
|
19
|
+
/** A read or successful write: user data stays separate from server metadata. */
|
|
20
|
+
export interface Document<T = Json> {
|
|
21
|
+
id: string;
|
|
22
|
+
data: T;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
updatedAt: string;
|
|
25
|
+
revision: Revision;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Page<T = Json> {
|
|
29
|
+
documents: Document<T>[];
|
|
30
|
+
nextCursor: string | null;
|
|
31
|
+
totalCount?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Why an operation failed. The list is closed for v1: a code a later server
|
|
36
|
+
* adds reaches this version as the nearest code here.
|
|
37
|
+
*/
|
|
38
|
+
export type NaruErrorCode =
|
|
39
|
+
| "CONFLICT"
|
|
40
|
+
| "QUOTA_EXCEEDED"
|
|
41
|
+
| "AUTH_REQUIRED"
|
|
42
|
+
| "ACCESS_DENIED"
|
|
43
|
+
| "NOT_FOUND"
|
|
44
|
+
| "RATE_LIMITED"
|
|
45
|
+
| "INVALID_REQUEST"
|
|
46
|
+
| "UNAVAILABLE";
|
|
47
|
+
|
|
48
|
+
/** A Naru operation that could not be completed. Check it with `instanceof`. */
|
|
49
|
+
export declare class NaruError extends Error {
|
|
50
|
+
private constructor();
|
|
51
|
+
readonly code: NaruErrorCode;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Top-level user fields, combined with AND. */
|
|
55
|
+
export type Filter = Record<
|
|
56
|
+
string,
|
|
57
|
+
| string
|
|
58
|
+
| number
|
|
59
|
+
| boolean
|
|
60
|
+
| null
|
|
61
|
+
| {
|
|
62
|
+
gt?: string | number;
|
|
63
|
+
gte?: string | number;
|
|
64
|
+
lt?: string | number;
|
|
65
|
+
lte?: string | number;
|
|
66
|
+
}
|
|
67
|
+
>;
|
|
68
|
+
|
|
69
|
+
/** One or two keys: a user field by name, or document metadata. */
|
|
70
|
+
export type Sort =
|
|
71
|
+
| readonly [
|
|
72
|
+
readonly [
|
|
73
|
+
string | { metadata: "id" | "createdAt" | "updatedAt" },
|
|
74
|
+
"asc" | "desc",
|
|
75
|
+
],
|
|
76
|
+
]
|
|
77
|
+
| readonly [
|
|
78
|
+
readonly [
|
|
79
|
+
string | { metadata: "id" | "createdAt" | "updatedAt" },
|
|
80
|
+
"asc" | "desc",
|
|
81
|
+
],
|
|
82
|
+
readonly [
|
|
83
|
+
string | { metadata: "id" | "createdAt" | "updatedAt" },
|
|
84
|
+
"asc" | "desc",
|
|
85
|
+
],
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
export interface ListOptions {
|
|
89
|
+
filter?: Filter;
|
|
90
|
+
sort?: Sort;
|
|
91
|
+
/** Default 50; maximum 100. */
|
|
92
|
+
size?: number;
|
|
93
|
+
/** Opaque cursor returned by the preceding page. */
|
|
94
|
+
after?: string | null;
|
|
95
|
+
includeTotal?: boolean;
|
|
96
|
+
signal?: AbortSignal;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Write only at this revision, or only if the document does not exist yet. */
|
|
100
|
+
export type WriteCondition = { revision: Revision } | { absent: true };
|
|
101
|
+
|
|
102
|
+
export interface PublicCollection<T = Json> {
|
|
103
|
+
get(id: string, options?: { signal?: AbortSignal }): Promise<Document<T>>;
|
|
104
|
+
list(options?: ListOptions): Promise<Page<T>>;
|
|
105
|
+
/** How many documents match `filter`, or the whole collection. */
|
|
106
|
+
count(options?: { filter?: Filter; signal?: AbortSignal }): Promise<number>;
|
|
107
|
+
/**
|
|
108
|
+
* Every page of a query in turn, following `nextCursor` from `after` (or
|
|
109
|
+
* the start). Not a snapshot: writes in between can shift later pages.
|
|
110
|
+
*/
|
|
111
|
+
pages(options?: ListOptions): AsyncIterable<Page<T>>;
|
|
112
|
+
add(data: T, options?: { signal?: AbortSignal }): Promise<Document<T>>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface AdminCollection<T = Json> extends PublicCollection<T> {
|
|
116
|
+
/** Replaces the whole document or creates it. */
|
|
117
|
+
set(
|
|
118
|
+
id: string,
|
|
119
|
+
data: T,
|
|
120
|
+
options?: { condition?: WriteCondition; signal?: AbortSignal },
|
|
121
|
+
): Promise<Document<T>>;
|
|
122
|
+
/** Deleting a missing document succeeds unless a condition was supplied. */
|
|
123
|
+
delete(
|
|
124
|
+
id: string,
|
|
125
|
+
options?: { condition?: { revision: Revision }; signal?: AbortSignal },
|
|
126
|
+
): Promise<void>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface Admin {
|
|
130
|
+
collection<T = Json>(name: string): AdminCollection<T>;
|
|
131
|
+
/**
|
|
132
|
+
* Commits every write or none. Conditions guard individual documents. Does
|
|
133
|
+
* not retry. Resolves, in the order written, with what each `set` stored and
|
|
134
|
+
* `null` for each `delete`.
|
|
135
|
+
*/
|
|
136
|
+
batch(
|
|
137
|
+
writes: readonly (
|
|
138
|
+
| {
|
|
139
|
+
collection: string;
|
|
140
|
+
set: { id: string; data: unknown; condition?: WriteCondition };
|
|
141
|
+
}
|
|
142
|
+
| {
|
|
143
|
+
collection: string;
|
|
144
|
+
delete: { id: string; condition?: { revision: Revision } };
|
|
145
|
+
}
|
|
146
|
+
)[],
|
|
147
|
+
options?: { signal?: AbortSignal },
|
|
148
|
+
): Promise<
|
|
149
|
+
({
|
|
150
|
+
id: string;
|
|
151
|
+
revision: Revision;
|
|
152
|
+
createdAt: string;
|
|
153
|
+
updatedAt: string;
|
|
154
|
+
} | null)[]
|
|
155
|
+
>;
|
|
156
|
+
media: {
|
|
157
|
+
/**
|
|
158
|
+
* Stores a file publicly. Large photos may be shrunk first. A HEIC photo
|
|
159
|
+
* this browser cannot convert throws a `TypeError` before anything is sent.
|
|
160
|
+
*/
|
|
161
|
+
upload(
|
|
162
|
+
file: File | Blob,
|
|
163
|
+
options?: {
|
|
164
|
+
signal?: AbortSignal;
|
|
165
|
+
/**
|
|
166
|
+
* Called as the upload moves along: `preparing` while a photo is
|
|
167
|
+
* shrunk and the upload authorized, `uploading` with the bytes sent
|
|
168
|
+
* so far of the `total` actually sent (after shrinking), then
|
|
169
|
+
* `finishing` while storage confirms and Naru records the file.
|
|
170
|
+
* `loaded` can reach `total` before storage has answered.
|
|
171
|
+
*/
|
|
172
|
+
onProgress?: (
|
|
173
|
+
progress:
|
|
174
|
+
| { phase: "preparing" }
|
|
175
|
+
| { phase: "uploading"; loaded: number; total: number }
|
|
176
|
+
| { phase: "finishing" },
|
|
177
|
+
) => void;
|
|
178
|
+
},
|
|
179
|
+
): Promise<{
|
|
180
|
+
url: string;
|
|
181
|
+
name: string;
|
|
182
|
+
contentType: string;
|
|
183
|
+
size: number;
|
|
184
|
+
}>;
|
|
185
|
+
};
|
|
186
|
+
/** This handle stops working at once, even if the revoke request fails. */
|
|
187
|
+
signOut(): Promise<void>;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface NaruClient {
|
|
191
|
+
/** Visitor access, even when signed in. */
|
|
192
|
+
collection<T = Json>(name: string): PublicCollection<T>;
|
|
193
|
+
auth: {
|
|
194
|
+
/**
|
|
195
|
+
* The admin client for this page, or null. A sign-in that was denied,
|
|
196
|
+
* went stale or could not be exchanged also resolves null.
|
|
197
|
+
*/
|
|
198
|
+
session(): Promise<Admin | null>;
|
|
199
|
+
/** Redirects to Naru for approval. */
|
|
200
|
+
signIn(options: { collections: readonly string[] }): Promise<void>;
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Creates a client bound to one site. */
|
|
205
|
+
export declare function createNaru(options?: { site?: string }): NaruClient;
|
|
206
|
+
|
|
207
|
+
export {};
|