@firsthandjs/data 0.5.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,121 @@
1
+ /**
2
+ * The store: what is loaded, and when it has to be loaded again.
3
+ *
4
+ * It holds resources, it matches invalidation patterns against their tags, and
5
+ * it knows nothing else. No URL, no method, no body, no client — a loader is
6
+ * an arbitrary function returning a promise, and it may be a `fetch`, an
7
+ * Apollo call, a worker or an algorithm that never leaves the browser
8
+ * (ADR-0022).
9
+ *
10
+ * What it deliberately is not is a cache. A cache is defined by a second
11
+ * lookup for the same thing finding the first one's result, and that requires
12
+ * identity — a key, a name, something two callers agree on. Every form of that
13
+ * is a thing to forget or to collide on, and deriving it implicitly from tags
14
+ * is worse: it silently serves one resource's data to another, which is the
15
+ * measured bug this design exists to remove. A resource belongs to its call
16
+ * site. Deduplication, response caching and normalisation belong to the
17
+ * transport, where the knowledge of what is *the same thing* actually lives.
18
+ */
19
+ import { type ReadonlyCell, type Signal } from '@firsthandjs/core';
20
+ import { type Tag } from './tags.js';
21
+ export type Status = 'idle' | 'loading' | 'success' | 'error';
22
+ /** What a loader is given. */
23
+ export interface LoadContext {
24
+ /** Aborted when this run is superseded, or the resource goes away. */
25
+ readonly signal: AbortSignal;
26
+ /**
27
+ * Declares what this resource is about. **Replaces**: call it before the
28
+ * first `await` when you know, again after it when only the server does, and
29
+ * the last call of a run wins. Name both if you want both.
30
+ */
31
+ readonly tags: (...tags: Tag[]) => void;
32
+ /**
33
+ * True when this run was caused by an invalidation or by `reload()`.
34
+ *
35
+ * It is the one place the layers touch. A transport cache would otherwise
36
+ * hand back the answer that was just invalidated, and the invalidation would
37
+ * be silently pointless — so pass it on: `cache: force ? 'reload' : 'default'`
38
+ * for `fetch`, `fetchPolicy: force ? 'network-only' : 'cache-first'` for
39
+ * Apollo, `requestPolicy` for urql.
40
+ */
41
+ readonly force: boolean;
42
+ }
43
+ /** What an action is given. */
44
+ export interface ActionContext {
45
+ readonly signal: AbortSignal;
46
+ /**
47
+ * Declares what this action changed, so resources carrying a matching tag
48
+ * reload. Replaces, like `tags`, and may be called after the answer — which
49
+ * is the case where only the server knows what was touched.
50
+ */
51
+ readonly invalidates: (...tags: Tag[]) => void;
52
+ }
53
+ export interface Resource<T> {
54
+ readonly data: ReadonlyCell<T | undefined>;
55
+ readonly error: ReadonlyCell<unknown>;
56
+ readonly status: ReadonlyCell<Status>;
57
+ /** True while a run is in flight, including one behind a visible value. */
58
+ readonly loading: ReadonlyCell<boolean>;
59
+ /** Runs again, with `force`. Never rejects. */
60
+ reload(): Promise<T | undefined>;
61
+ /** Stops this resource and aborts anything in flight. */
62
+ dispose(): void;
63
+ }
64
+ /** Somewhere to keep results between visits. See {@link DataOptions}. */
65
+ export interface Storage {
66
+ /**
67
+ * What was kept under this name, if anything. May return a promise; a value
68
+ * that arrives after the loader has answered is dropped.
69
+ */
70
+ read?(name: string): unknown;
71
+ /** Called after each successful run of a named resource. */
72
+ write?(name: string, data: unknown): void;
73
+ /** Called by `store.clear()`, which is what a sign-out calls. */
74
+ clear?(): void;
75
+ }
76
+ export interface DataOptions {
77
+ /** Where named resources are kept between visits. */
78
+ readonly storage?: Storage;
79
+ }
80
+ interface Held<T = unknown> {
81
+ data: Signal<T | undefined>;
82
+ error: Signal<unknown>;
83
+ status: Signal<Status>;
84
+ loading: Signal<boolean>;
85
+ /** What this resource is currently about. Replaced by every run. */
86
+ tags: Tag[];
87
+ controller: AbortController | null;
88
+ /** Patterns seen while the current run is in flight, for the late-tag race. */
89
+ pending: Tag[];
90
+ /** Set when an invalidation matched a run that had not finished. */
91
+ superseded: boolean;
92
+ disposed: boolean;
93
+ name: string | undefined;
94
+ run: (force: boolean) => Promise<T | undefined>;
95
+ }
96
+ export interface DataStore {
97
+ /** Everything carrying a matching tag runs again, with `force`. */
98
+ invalidate(...patterns: Tag[]): Promise<void>;
99
+ /** Forgets every resource and empties the storage. */
100
+ clear(): void;
101
+ /** How many resources are alive. For tests and devtools. */
102
+ readonly size: number;
103
+ /** Internal: a resource registers itself here. */
104
+ hold(held: Held): () => void;
105
+ /** Internal: the storage this store was given. */
106
+ readonly storage: Storage | undefined;
107
+ }
108
+ export declare function createData(options?: DataOptions): DataStore;
109
+ /**
110
+ * Runs one resource. Shared by `useResource` and the observable bridges, so
111
+ * that a resource behaves the same however its values arrive.
112
+ */
113
+ export declare function createHeld<T>(store: DataStore, name: string | undefined): Held<T>;
114
+ /** Settles a successful run. Exported for the bridges, which have no loader. */
115
+ export declare function succeed<T>(entry: Held<T>, value: T): void;
116
+ /** Settles a failed run. */
117
+ export declare function fail(entry: Held, error: unknown): void;
118
+ /** The public face of a held resource. */
119
+ export declare function expose<T>(entry: Held<T>, release: () => void): Resource<T>;
120
+ export type { Held };
121
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
package/dist/tags.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Tags.
3
+ *
4
+ * A cache key answers "is this the same request?". A tag answers "what is this
5
+ * request *about*?", which is the question invalidation actually asks. React
6
+ * Query conflates the two: the key is both the identity and the invalidation
7
+ * handle, so invalidating everything about user 7 means knowing every key
8
+ * prefix anyone wrote for user 7.
9
+ *
10
+ * Here a query carries as many tags as it likes — `user(id: 7)`, `users`,
11
+ * `permissions(org: 3)` — and a mutation invalidates tags, not queries. A tag
12
+ * with fewer variables matches more: `user` invalidates every user, `user(id:
13
+ * 7)` invalidates exactly that one.
14
+ */
15
+ /** Variables a tag may carry. Values are compared with `Object.is`. */
16
+ export type TagVars = Readonly<Record<string, string | number | boolean | null>>;
17
+ /**
18
+ * The arguments an operation was called with.
19
+ *
20
+ * Deliberately `unknown` rather than something serialisable: what a loader
21
+ * does with them is the transport's business, and narrowing the type here
22
+ * would constrain that for no gain — nothing in this package uses them as an
23
+ * identity.
24
+ */
25
+ export type Variables = Readonly<Record<string, unknown>>;
26
+ export interface Tag {
27
+ readonly name: string;
28
+ readonly vars: TagVars;
29
+ }
30
+ /** Builds a tag. `tag('user', { id })` is the usual shape. */
31
+ export declare function tag(name: string, vars?: TagVars): Tag;
32
+ /**
33
+ * Whether `pattern` covers `candidate`.
34
+ *
35
+ * Same name, and every variable the pattern names has the same value on the
36
+ * candidate. Variables the pattern leaves out are wildcards — which is what
37
+ * makes `tag('user')` mean "every user".
38
+ */
39
+ export declare function tagMatches(pattern: Tag, candidate: Tag): boolean;
40
+ /** Whether any of `patterns` covers any of `tags`. */
41
+ export declare function anyTagMatches(patterns: readonly Tag[], tags: readonly Tag[]): boolean;
42
+ //# sourceMappingURL=tags.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tags.d.ts","sourceRoot":"","sources":["../src/tags.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,uEAAuE;AACvE,MAAM,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC;AAEjF;;;;;;;GAOG;AACH,MAAM,MAAM,SAAS,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAE1D,MAAM,WAAW,GAAG;IAClB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAID,8DAA8D;AAC9D,wBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,OAAiB,GAAG,GAAG,CAE9D;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,GAAG,OAAO,CAUhE;AAED,sDAAsD;AACtD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,SAAS,GAAG,EAAE,EAAE,IAAI,EAAE,SAAS,GAAG,EAAE,GAAG,OAAO,CASrF"}
package/dist/vite.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ interface TransformResult {
2
+ code: string;
3
+ map: null;
4
+ }
5
+ export interface GraphQLPlugin {
6
+ name: string;
7
+ enforce: 'pre';
8
+ transform(this: unknown, code: string, id: string): TransformResult | null;
9
+ }
10
+ /**
11
+ * Inlines `#import "./fragments.gql"`, the convention every GraphQL loader
12
+ * uses.
13
+ *
14
+ * A fragment lives in one file and is used by several operations, and the
15
+ * server has to be sent the fragment along with the operation that spreads it
16
+ * — so the import is resolved here rather than at runtime, where the file
17
+ * system is not available and the cost would be per page load.
18
+ *
19
+ * Each file is included once however many times it is imported, and a cycle
20
+ * terminates for the same reason.
21
+ */
22
+ export declare function inlineImports(source: string, file: string, seen?: Set<string>, read?: (path: string) => string): string;
23
+ export declare function graphql(): GraphQLPlugin;
24
+ export {};
25
+ //# sourceMappingURL=vite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAgCA,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,IAAI,CAAC;CACX;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,KAAK,CAAC;IACf,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAAC;CAC5E;AAKD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,GAAG,CAAC,MAAM,CAAmB,EACnC,IAAI,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAA6C,GACpE,MAAM,CAWR;AAED,wBAAgB,OAAO,IAAI,aAAa,CAgBvC"}
@@ -0,0 +1,223 @@
1
+ // packages/data/src/vite.ts
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+
5
+ // packages/data/src/document.ts
6
+ var DIRECTIVE = /^@(tag|invalidates)\b/;
7
+ var OPERATION = /\b(query|mutation|subscription)\b[^\S\n]*([A-Za-z_]\w*)?/;
8
+ var NAME = /^[_A-Za-z][_0-9A-Za-z]*/;
9
+ var FirsthandDirectiveError = class extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = "FirsthandDirectiveError";
13
+ }
14
+ };
15
+ function endOfString(source, start) {
16
+ if (source.startsWith('"""', start)) {
17
+ const close = source.indexOf('"""', start + 3);
18
+ return close === -1 ? source.length : close + 3;
19
+ }
20
+ let at = start + 1;
21
+ while (at < source.length && source[at] !== '"') {
22
+ at += source[at] === "\\" ? 2 : 1;
23
+ }
24
+ return at + 1;
25
+ }
26
+ function endOfArguments(source, start) {
27
+ let depth = 0;
28
+ let at = start;
29
+ while (at < source.length) {
30
+ const character = source[at];
31
+ if (character === '"') {
32
+ at = endOfString(source, at);
33
+ continue;
34
+ }
35
+ if (character === "(" || character === "[" || character === "{") {
36
+ depth++;
37
+ } else if (character === ")" || character === "]" || character === "}") {
38
+ depth--;
39
+ if (depth === 0) {
40
+ return at + 1;
41
+ }
42
+ }
43
+ at++;
44
+ }
45
+ throw new FirsthandDirectiveError(`unclosed arguments in ${source.slice(start, start + 40)}`);
46
+ }
47
+ function unquote(quoted, directive) {
48
+ try {
49
+ return JSON.parse(quoted);
50
+ } catch {
51
+ throw new FirsthandDirectiveError(`@${directive}: ${quoted} is not a valid string`);
52
+ }
53
+ }
54
+ function parseArguments(raw, directive) {
55
+ const vars = {};
56
+ let at = 0;
57
+ const skipIgnored = () => {
58
+ while (at < raw.length && /[\s,]/.test(raw[at])) {
59
+ at++;
60
+ }
61
+ };
62
+ skipIgnored();
63
+ while (at < raw.length) {
64
+ const name = NAME.exec(raw.slice(at));
65
+ if (name === null) {
66
+ throw new FirsthandDirectiveError(
67
+ `@${directive}: expected an argument name at "${raw.slice(at)}"`
68
+ );
69
+ }
70
+ at += name[0].length;
71
+ skipIgnored();
72
+ if (raw[at] !== ":") {
73
+ throw new FirsthandDirectiveError(`@${directive}: argument ${name[0]} has no value`);
74
+ }
75
+ at++;
76
+ skipIgnored();
77
+ const read = readValue(raw, directive, at);
78
+ vars[name[0]] = read.value;
79
+ at = read.next;
80
+ skipIgnored();
81
+ }
82
+ return vars;
83
+ }
84
+ function readValue(raw, directive, at) {
85
+ const character = raw[at];
86
+ if (character === "$") {
87
+ const name = NAME.exec(raw.slice(at + 1));
88
+ if (name === null) {
89
+ throw new FirsthandDirectiveError(`@${directive}: expected a variable name after $`);
90
+ }
91
+ return { value: { variable: name[0] }, next: at + 1 + name[0].length };
92
+ }
93
+ if (character === '"') {
94
+ const end = endOfString(raw, at);
95
+ const quoted = raw.slice(at, end);
96
+ const literal = quoted.startsWith('"""') ? quoted.slice(3, -3).trim() : unquote(quoted, directive);
97
+ return { value: { literal }, next: end };
98
+ }
99
+ if (character === "[" || character === "{") {
100
+ throw new FirsthandDirectiveError(
101
+ `@${directive}: a tag variable must be a scalar, not a list or object`
102
+ );
103
+ }
104
+ const word = /^[^\s,)]+/.exec(raw.slice(at));
105
+ if (word === null) {
106
+ throw new FirsthandDirectiveError(`@${directive}: expected a value`);
107
+ }
108
+ const text = word[0];
109
+ const next = at + text.length;
110
+ if (text === "true" || text === "false") {
111
+ return { value: { literal: text === "true" }, next };
112
+ }
113
+ if (text === "null") {
114
+ return { value: { literal: null }, next };
115
+ }
116
+ const asNumber = Number(text);
117
+ return { value: { literal: Number.isNaN(asNumber) ? text : asNumber }, next };
118
+ }
119
+ function scan(source) {
120
+ const tags = [];
121
+ const invalidates = [];
122
+ let stripped = "";
123
+ let at = 0;
124
+ let kept = 0;
125
+ while (at < source.length) {
126
+ const character = source[at];
127
+ if (character === '"') {
128
+ at = endOfString(source, at);
129
+ continue;
130
+ }
131
+ if (character === "#") {
132
+ const newline = source.indexOf("\n", at);
133
+ at = newline === -1 ? source.length : newline;
134
+ continue;
135
+ }
136
+ if (character !== "@") {
137
+ at++;
138
+ continue;
139
+ }
140
+ const directive = DIRECTIVE.exec(source.slice(at));
141
+ if (directive === null) {
142
+ at++;
143
+ continue;
144
+ }
145
+ let end = at + directive[0].length;
146
+ let args = "";
147
+ let probe = end;
148
+ while (probe < source.length && /\s/.test(source[probe])) {
149
+ probe++;
150
+ }
151
+ if (source[probe] === "(") {
152
+ const close = endOfArguments(source, probe);
153
+ args = source.slice(probe + 1, close - 1);
154
+ end = close;
155
+ }
156
+ const vars = parseArguments(args, directive[1]);
157
+ const named = vars["name"];
158
+ if (named === void 0 || !("literal" in named) || typeof named.literal !== "string") {
159
+ throw new FirsthandDirectiveError(
160
+ `@${directive[1]} needs a literal name, as in @${directive[1]}(name: "user", id: $id)`
161
+ );
162
+ }
163
+ delete vars["name"];
164
+ (directive[1] === "tag" ? tags : invalidates).push({ name: named.literal, vars });
165
+ let from = at;
166
+ while (from > kept && /\s/.test(source[from - 1])) {
167
+ from--;
168
+ }
169
+ stripped += source.slice(kept, from);
170
+ kept = end;
171
+ at = end;
172
+ }
173
+ return { tags, invalidates, stripped: stripped + source.slice(kept) };
174
+ }
175
+ function parseGraphQL(source) {
176
+ const { tags, invalidates, stripped } = scan(source);
177
+ const operation = OPERATION.exec(stripped.replace(/#[^\n]*/g, ""));
178
+ return {
179
+ source: stripped,
180
+ operation: operation?.[2] ?? "",
181
+ kind: operation?.[1] ?? "query",
182
+ tags,
183
+ invalidates
184
+ };
185
+ }
186
+
187
+ // packages/data/src/vite.ts
188
+ var IMPORT = /^#\s*import\s+(['"])(.+?)\1/gm;
189
+ var DOCUMENT = /\.(graphql|gql)(\?.*)?$/;
190
+ function inlineImports(source, file, seen = /* @__PURE__ */ new Set([file]), read = (path) => readFileSync(path, "utf8")) {
191
+ const imported = [];
192
+ const body = source.replace(IMPORT, (_whole, _quote, specifier) => {
193
+ const path = resolve(dirname(file), specifier);
194
+ if (!seen.has(path)) {
195
+ seen.add(path);
196
+ imported.push(inlineImports(read(path), path, seen, read));
197
+ }
198
+ return "";
199
+ });
200
+ return imported.length === 0 ? body : `${imported.join("\n")}
201
+ ${body}`;
202
+ }
203
+ function graphql() {
204
+ return {
205
+ name: "firsthand-graphql",
206
+ enforce: "pre",
207
+ transform(code, id) {
208
+ const file = id.split("?")[0];
209
+ if (!DOCUMENT.test(id)) {
210
+ return null;
211
+ }
212
+ const document = parseGraphQL(inlineImports(code, file));
213
+ return {
214
+ code: `export default ${JSON.stringify(document)};`,
215
+ map: null
216
+ };
217
+ }
218
+ };
219
+ }
220
+ export {
221
+ graphql,
222
+ inlineImports
223
+ };
package/dist/vite.js ADDED
@@ -0,0 +1,223 @@
1
+ // packages/data/src/vite.ts
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+
5
+ // packages/data/src/document.ts
6
+ var DIRECTIVE = /^@(tag|invalidates)\b/;
7
+ var OPERATION = /\b(query|mutation|subscription)\b[^\S\n]*([A-Za-z_]\w*)?/;
8
+ var NAME = /^[_A-Za-z][_0-9A-Za-z]*/;
9
+ var FirsthandDirectiveError = class extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = "FirsthandDirectiveError";
13
+ }
14
+ };
15
+ function endOfString(source, start) {
16
+ if (source.startsWith('"""', start)) {
17
+ const close = source.indexOf('"""', start + 3);
18
+ return close === -1 ? source.length : close + 3;
19
+ }
20
+ let at = start + 1;
21
+ while (at < source.length && source[at] !== '"') {
22
+ at += source[at] === "\\" ? 2 : 1;
23
+ }
24
+ return at + 1;
25
+ }
26
+ function endOfArguments(source, start) {
27
+ let depth = 0;
28
+ let at = start;
29
+ while (at < source.length) {
30
+ const character = source[at];
31
+ if (character === '"') {
32
+ at = endOfString(source, at);
33
+ continue;
34
+ }
35
+ if (character === "(" || character === "[" || character === "{") {
36
+ depth++;
37
+ } else if (character === ")" || character === "]" || character === "}") {
38
+ depth--;
39
+ if (depth === 0) {
40
+ return at + 1;
41
+ }
42
+ }
43
+ at++;
44
+ }
45
+ throw new FirsthandDirectiveError(`unclosed arguments in ${source.slice(start, start + 40)}`);
46
+ }
47
+ function unquote(quoted, directive) {
48
+ try {
49
+ return JSON.parse(quoted);
50
+ } catch {
51
+ throw new FirsthandDirectiveError(`@${directive}: ${quoted} is not a valid string`);
52
+ }
53
+ }
54
+ function parseArguments(raw, directive) {
55
+ const vars = {};
56
+ let at = 0;
57
+ const skipIgnored = () => {
58
+ while (at < raw.length && /[\s,]/.test(raw[at])) {
59
+ at++;
60
+ }
61
+ };
62
+ skipIgnored();
63
+ while (at < raw.length) {
64
+ const name = NAME.exec(raw.slice(at));
65
+ if (name === null) {
66
+ throw new FirsthandDirectiveError(
67
+ `@${directive}: expected an argument name at "${raw.slice(at)}"`
68
+ );
69
+ }
70
+ at += name[0].length;
71
+ skipIgnored();
72
+ if (raw[at] !== ":") {
73
+ throw new FirsthandDirectiveError(`@${directive}: argument ${name[0]} has no value`);
74
+ }
75
+ at++;
76
+ skipIgnored();
77
+ const read = readValue(raw, directive, at);
78
+ vars[name[0]] = read.value;
79
+ at = read.next;
80
+ skipIgnored();
81
+ }
82
+ return vars;
83
+ }
84
+ function readValue(raw, directive, at) {
85
+ const character = raw[at];
86
+ if (character === "$") {
87
+ const name = NAME.exec(raw.slice(at + 1));
88
+ if (name === null) {
89
+ throw new FirsthandDirectiveError(`@${directive}: expected a variable name after $`);
90
+ }
91
+ return { value: { variable: name[0] }, next: at + 1 + name[0].length };
92
+ }
93
+ if (character === '"') {
94
+ const end = endOfString(raw, at);
95
+ const quoted = raw.slice(at, end);
96
+ const literal = quoted.startsWith('"""') ? quoted.slice(3, -3).trim() : unquote(quoted, directive);
97
+ return { value: { literal }, next: end };
98
+ }
99
+ if (character === "[" || character === "{") {
100
+ throw new FirsthandDirectiveError(
101
+ `@${directive}: a tag variable must be a scalar, not a list or object`
102
+ );
103
+ }
104
+ const word = /^[^\s,)]+/.exec(raw.slice(at));
105
+ if (word === null) {
106
+ throw new FirsthandDirectiveError(`@${directive}: expected a value`);
107
+ }
108
+ const text = word[0];
109
+ const next = at + text.length;
110
+ if (text === "true" || text === "false") {
111
+ return { value: { literal: text === "true" }, next };
112
+ }
113
+ if (text === "null") {
114
+ return { value: { literal: null }, next };
115
+ }
116
+ const asNumber = Number(text);
117
+ return { value: { literal: Number.isNaN(asNumber) ? text : asNumber }, next };
118
+ }
119
+ function scan(source) {
120
+ const tags = [];
121
+ const invalidates = [];
122
+ let stripped = "";
123
+ let at = 0;
124
+ let kept = 0;
125
+ while (at < source.length) {
126
+ const character = source[at];
127
+ if (character === '"') {
128
+ at = endOfString(source, at);
129
+ continue;
130
+ }
131
+ if (character === "#") {
132
+ const newline = source.indexOf("\n", at);
133
+ at = newline === -1 ? source.length : newline;
134
+ continue;
135
+ }
136
+ if (character !== "@") {
137
+ at++;
138
+ continue;
139
+ }
140
+ const directive = DIRECTIVE.exec(source.slice(at));
141
+ if (directive === null) {
142
+ at++;
143
+ continue;
144
+ }
145
+ let end = at + directive[0].length;
146
+ let args = "";
147
+ let probe = end;
148
+ while (probe < source.length && /\s/.test(source[probe])) {
149
+ probe++;
150
+ }
151
+ if (source[probe] === "(") {
152
+ const close = endOfArguments(source, probe);
153
+ args = source.slice(probe + 1, close - 1);
154
+ end = close;
155
+ }
156
+ const vars = parseArguments(args, directive[1]);
157
+ const named = vars["name"];
158
+ if (named === void 0 || !("literal" in named) || typeof named.literal !== "string") {
159
+ throw new FirsthandDirectiveError(
160
+ `@${directive[1]} needs a literal name, as in @${directive[1]}(name: "user", id: $id)`
161
+ );
162
+ }
163
+ delete vars["name"];
164
+ (directive[1] === "tag" ? tags : invalidates).push({ name: named.literal, vars });
165
+ let from = at;
166
+ while (from > kept && /\s/.test(source[from - 1])) {
167
+ from--;
168
+ }
169
+ stripped += source.slice(kept, from);
170
+ kept = end;
171
+ at = end;
172
+ }
173
+ return { tags, invalidates, stripped: stripped + source.slice(kept) };
174
+ }
175
+ function parseGraphQL(source) {
176
+ const { tags, invalidates, stripped } = scan(source);
177
+ const operation = OPERATION.exec(stripped.replace(/#[^\n]*/g, ""));
178
+ return {
179
+ source: stripped,
180
+ operation: operation?.[2] ?? "",
181
+ kind: operation?.[1] ?? "query",
182
+ tags,
183
+ invalidates
184
+ };
185
+ }
186
+
187
+ // packages/data/src/vite.ts
188
+ var IMPORT = /^#\s*import\s+(['"])(.+?)\1/gm;
189
+ var DOCUMENT = /\.(graphql|gql)(\?.*)?$/;
190
+ function inlineImports(source, file, seen = /* @__PURE__ */ new Set([file]), read = (path) => readFileSync(path, "utf8")) {
191
+ const imported = [];
192
+ const body = source.replace(IMPORT, (_whole, _quote, specifier) => {
193
+ const path = resolve(dirname(file), specifier);
194
+ if (!seen.has(path)) {
195
+ seen.add(path);
196
+ imported.push(inlineImports(read(path), path, seen, read));
197
+ }
198
+ return "";
199
+ });
200
+ return imported.length === 0 ? body : `${imported.join("\n")}
201
+ ${body}`;
202
+ }
203
+ function graphql() {
204
+ return {
205
+ name: "firsthand-graphql",
206
+ enforce: "pre",
207
+ transform(code, id) {
208
+ const file = id.split("?")[0];
209
+ if (!DOCUMENT.test(id)) {
210
+ return null;
211
+ }
212
+ const document = parseGraphQL(inlineImports(code, file));
213
+ return {
214
+ code: `export default ${JSON.stringify(document)};`,
215
+ map: null
216
+ };
217
+ }
218
+ };
219
+ }
220
+ export {
221
+ graphql,
222
+ inlineImports
223
+ };