@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.
@@ -0,0 +1,11 @@
1
+ import {
2
+ packagedResources,
3
+ start
4
+ } from "./chunk-CQFKRWOD.mjs";
5
+
6
+ // src/index.ts
7
+ var startNaru = (options) => start(options, packagedResources());
8
+
9
+ export {
10
+ startNaru
11
+ };
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
+ );
@@ -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,7 @@
1
+ import {
2
+ startNaru
3
+ } from "./chunk-K7ZVYKB2.mjs";
4
+ import "./chunk-CQFKRWOD.mjs";
5
+ export {
6
+ startNaru
7
+ };
@@ -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 {};