@forgebook/sveltekit 0.2.0 → 0.2.1
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 +9 -2
- package/dist/src/kitRenderer.js +48 -26
- package/dist/src/loadFetch.d.ts +33 -0
- package/dist/src/loadFetch.js +171 -0
- package/package.json +14 -7
package/README.md
CHANGED
|
@@ -129,8 +129,15 @@ function `use:enhance` takes runs as in the app. A POST form without `use:enhanc
|
|
|
129
129
|
| `$app/env/private`, `$env/*/private`, `$app/server`, `$service-worker` | Refused: the story fails with why, and the value never reaches the page |
|
|
130
130
|
| The other SvelteKit's environment | Refused too: SvelteKit 3 has no `$env/*`, so no variable reaches its page by its prefix alone; SvelteKit 2 has no `$app/env/public` |
|
|
131
131
|
|
|
132
|
-
Generated server tests take `kitRenderer` from `@forgebook/sveltekit/renderer`: a page's `load` runs first,
|
|
133
|
-
|
|
132
|
+
Generated server tests take `kitRenderer` from `@forgebook/sveltekit/renderer`: a page's `load` runs first, and a server
|
|
133
|
+
render never reaches the network for the page's own origin. A load's `fetch` of a path, or of an address on the story's
|
|
134
|
+
page's origin (`http://localhost` unless `page.url` names another), is answered by the story's `parameters.msw`
|
|
135
|
+
handlers, the ones the frame uses, run in Node by MSW's `getResponse` (MSW 2.11 or later, installed by the project): no
|
|
136
|
+
request is intercepted, so tests running at once keep their own mocks. A mocked redirect is followed through the
|
|
137
|
+
handlers and `HttpResponse.error()` rejects the fetch, as in the frame; `{ once: true }` handlers answer again in each
|
|
138
|
+
render. When no handler answers, or one answers `passthrough()`, the story fails with the address and what to give
|
|
139
|
+
instead: a handler, or the page's data (`serverData`, `page.data`); it fails even when the load catches the error. An
|
|
140
|
+
address on another origin, `127.0.0.1` beside a `localhost` page among them, is fetched as it is.
|
|
134
141
|
|
|
135
142
|
## Not drawn
|
|
136
143
|
|
package/dist/src/kitRenderer.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { svelteRenderer } from "@forgebook/svelte/renderer";
|
|
17
17
|
import { hrefCallbacksOf } from "./kitParameters.js";
|
|
18
18
|
import { createKitStore, errorOfThrown, setCurrentStore } from "./kitStore.js";
|
|
19
|
+
import { loadFetch, mockHandlersOf } from "./loadFetch.js";
|
|
19
20
|
/** Whether a value is a plain object. */
|
|
20
21
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
22
|
/** An error as the frame takes it: SvelteKit's `error(404, "…")` written with its status. */
|
|
@@ -26,45 +27,66 @@ const toError = (error) => {
|
|
|
26
27
|
return new Error(`${read.status ?? 500}: ${read.message}`);
|
|
27
28
|
};
|
|
28
29
|
/**
|
|
29
|
-
*
|
|
30
|
-
* server, which has no page to read a path from, from the story's page, as SvelteKit's `fetch` does.
|
|
30
|
+
* Runs a story's loads in order, the layouts' first, each `parent()` the data above it.
|
|
31
31
|
*
|
|
32
|
-
* @param
|
|
33
|
-
* @param
|
|
32
|
+
* @param kit - the story's settings.
|
|
33
|
+
* @param store - the story's page, which records what the loads call.
|
|
34
|
+
* @param at - the page they load.
|
|
35
|
+
* @param fetch - the loads' `fetch`.
|
|
36
|
+
* @param afterLoad - runs after each load, before its data is taken: it throws to stop them.
|
|
37
|
+
* @returns the data, every load's merged.
|
|
34
38
|
*/
|
|
35
|
-
const
|
|
39
|
+
const runLoads = async (kit, store, at, fetch, afterLoad) => {
|
|
40
|
+
let merged = kit.page.data;
|
|
41
|
+
for (const [index, load] of kit.loads.entries()) {
|
|
42
|
+
const above = merged;
|
|
43
|
+
const isPage = index === kit.loads.length - 1;
|
|
44
|
+
const result = await load({
|
|
45
|
+
params: at.params,
|
|
46
|
+
url: at.url,
|
|
47
|
+
route: at.route,
|
|
48
|
+
fetch,
|
|
49
|
+
// the page's server data is the page's load's; a layout's server load is given by `page.data`
|
|
50
|
+
data: isPage ? kit.serverData : null,
|
|
51
|
+
parent: async () => above,
|
|
52
|
+
depends: (...dependencies) => store.record("depends", dependencies),
|
|
53
|
+
setHeaders: () => undefined,
|
|
54
|
+
untrack: (read) => read()
|
|
55
|
+
});
|
|
56
|
+
afterLoad();
|
|
57
|
+
merged = { ...merged, ...(isRecord(result) ? result : {}) };
|
|
58
|
+
}
|
|
59
|
+
return merged;
|
|
60
|
+
};
|
|
36
61
|
/**
|
|
37
|
-
* The loader of a story's page data: its loads
|
|
62
|
+
* The loader of a story's page data: its loads, or its server data without them.
|
|
38
63
|
*
|
|
39
64
|
* @param kit - the story's settings.
|
|
40
65
|
* @param store - the story's page, which records what the loads call.
|
|
66
|
+
* @param handlers - the story's mock handlers, which answer the loads' fetches on a server.
|
|
41
67
|
* @returns the loader, or none for a story that gives no load and no server data.
|
|
42
68
|
*/
|
|
43
|
-
const loaderOf = (kit, store) => {
|
|
69
|
+
const loaderOf = (kit, store, handlers) => {
|
|
44
70
|
if (kit.loads.length === 0 && kit.serverData === null)
|
|
45
71
|
return undefined;
|
|
46
72
|
return async (at) => {
|
|
47
|
-
let merged = kit.page.data;
|
|
48
73
|
if (kit.loads.length === 0)
|
|
49
|
-
return { ...
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
parent: async () => above,
|
|
61
|
-
depends: (...dependencies) => store.record("depends", dependencies),
|
|
62
|
-
setHeaders: () => undefined,
|
|
63
|
-
untrack: (read) => read()
|
|
74
|
+
return { ...kit.page.data, ...kit.serverData };
|
|
75
|
+
// a fetch no mock answered on a server fails the page, though its load caught it or threw an error of its own:
|
|
76
|
+
// what the load drew instead is not the page
|
|
77
|
+
let unanswered;
|
|
78
|
+
const fetch = loadFetch(at.url, handlers, (error) => {
|
|
79
|
+
unanswered ??= error;
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
return await runLoads(kit, store, at, fetch, () => {
|
|
83
|
+
if (unanswered !== undefined)
|
|
84
|
+
throw unanswered;
|
|
64
85
|
});
|
|
65
|
-
merged = { ...merged, ...(isRecord(result) ? result : {}) };
|
|
66
86
|
}
|
|
67
|
-
|
|
87
|
+
catch (thrown) {
|
|
88
|
+
throw unanswered ?? thrown;
|
|
89
|
+
}
|
|
68
90
|
};
|
|
69
91
|
};
|
|
70
92
|
/**
|
|
@@ -178,7 +200,7 @@ const storeOf = (drawing) => {
|
|
|
178
200
|
? shared
|
|
179
201
|
: { store: createKitStore(key, drawing.onEvent), mounts: 0 };
|
|
180
202
|
if (entry !== shared) {
|
|
181
|
-
entry.store.setLoader(loaderOf(entry.store.kit, entry.store));
|
|
203
|
+
entry.store.setLoader(loaderOf(entry.store.kit, entry.store, mockHandlersOf(key)));
|
|
182
204
|
SHARED.set(key, entry);
|
|
183
205
|
}
|
|
184
206
|
entry.mounts += 1;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `fetch` a story's `load` gets: in a page, the frame's own, which the catalogue's mocks answer; on a server, the
|
|
3
|
+
* story's mock handlers run in place of the network for an address on the story's page's origin.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* SvelteKit's `fetch` on a server reads a path from the page's address, so a server render (the generated server
|
|
7
|
+
* tests) would ask `http://localhost` for it: whatever answers on that machine, or nothing, would decide what the page
|
|
8
|
+
* draws. A server render never reaches the network for the page's own origin: the story's handlers
|
|
9
|
+
* (`parameters.msw.handlers`, the ones the frame's worker serves) answer it through MSW's `getResponse`, a plain call
|
|
10
|
+
* with no interception, so concurrent renders never see each other's handlers. A request no handler answers, or one
|
|
11
|
+
* a handler sends on to the network (`passthrough()`), fails the render with what to give instead. An address on
|
|
12
|
+
* another origin is fetched as it is. A handler's `HttpResponse.error()` rejects the fetch and its redirect is followed,
|
|
13
|
+
* as in the frame.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
/** A load's `fetch`. */
|
|
18
|
+
export type LoadFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
19
|
+
/**
|
|
20
|
+
* The fetch a load gets.
|
|
21
|
+
*
|
|
22
|
+
* @param pageUrl - the address of the page being loaded, which a path is read from.
|
|
23
|
+
* @param handlers - the story's mock handlers.
|
|
24
|
+
* @param onUnanswered - told of a request on the page's origin that no handler answered on a server, so the render
|
|
25
|
+
* fails even when the load catches the error and draws something else.
|
|
26
|
+
*/
|
|
27
|
+
export declare const loadFetch: (pageUrl: URL, handlers: readonly unknown[], onUnanswered: (error: Error) => void) => LoadFetch;
|
|
28
|
+
/**
|
|
29
|
+
* The handlers a story's parameters declare, flat: a list, or a record of lists as MSW's Storybook addon writes them.
|
|
30
|
+
*
|
|
31
|
+
* @param parameters - the story's parameters.
|
|
32
|
+
*/
|
|
33
|
+
export declare const mockHandlersOf: (parameters: Readonly<Record<string, unknown>>) => readonly unknown[];
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `fetch` a story's `load` gets: in a page, the frame's own, which the catalogue's mocks answer; on a server, the
|
|
3
|
+
* story's mock handlers run in place of the network for an address on the story's page's origin.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* SvelteKit's `fetch` on a server reads a path from the page's address, so a server render (the generated server
|
|
7
|
+
* tests) would ask `http://localhost` for it: whatever answers on that machine, or nothing, would decide what the page
|
|
8
|
+
* draws. A server render never reaches the network for the page's own origin: the story's handlers
|
|
9
|
+
* (`parameters.msw.handlers`, the ones the frame's worker serves) answer it through MSW's `getResponse`, a plain call
|
|
10
|
+
* with no interception, so concurrent renders never see each other's handlers. A request no handler answers, or one
|
|
11
|
+
* a handler sends on to the network (`passthrough()`), fails the render with what to give instead. An address on
|
|
12
|
+
* another origin is fetched as it is. A handler's `HttpResponse.error()` rejects the fetch and its redirect is followed,
|
|
13
|
+
* as in the frame.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
18
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
19
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
20
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return path;
|
|
24
|
+
};
|
|
25
|
+
/** The module MSW's request handling is imported from, by the project that installs it. */
|
|
26
|
+
const MSW_MODULE = "msw";
|
|
27
|
+
/**
|
|
28
|
+
* The fetch a load gets.
|
|
29
|
+
*
|
|
30
|
+
* @param pageUrl - the address of the page being loaded, which a path is read from.
|
|
31
|
+
* @param handlers - the story's mock handlers.
|
|
32
|
+
* @param onUnanswered - told of a request on the page's origin that no handler answered on a server, so the render
|
|
33
|
+
* fails even when the load catches the error and draws something else.
|
|
34
|
+
*/
|
|
35
|
+
export const loadFetch = (pageUrl, handlers, onUnanswered) => {
|
|
36
|
+
// in a page, a path is read from the frame's address, where the catalogue's mocks answer it
|
|
37
|
+
if (typeof location !== "undefined")
|
|
38
|
+
return (input, init) => globalThis.fetch(input, init);
|
|
39
|
+
restoreHandlers(handlers);
|
|
40
|
+
const send = async (request, redirects) => {
|
|
41
|
+
if (new URL(request.url).origin !== pageUrl.origin)
|
|
42
|
+
return globalThis.fetch(request);
|
|
43
|
+
// kept for a redirect that sends the request again, body included, after a handler read it
|
|
44
|
+
const kept = request.clone();
|
|
45
|
+
const answer = await mockAnswer(handlers, request, pageUrl.origin);
|
|
46
|
+
if (answer === undefined) {
|
|
47
|
+
const error = unansweredError(request);
|
|
48
|
+
onUnanswered(error);
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
// MSW's mocked network error: `fetch` rejects, as it does in the frame
|
|
52
|
+
if (answer.type === "error")
|
|
53
|
+
throw new TypeError("Failed to fetch", { cause: answer });
|
|
54
|
+
const next = redirectOf(answer, kept);
|
|
55
|
+
if (next === undefined)
|
|
56
|
+
return answer;
|
|
57
|
+
if (redirects >= MOST_REDIRECTS)
|
|
58
|
+
throw new TypeError(`Too many redirects, the last to ${next.url}`);
|
|
59
|
+
return send(next, redirects + 1);
|
|
60
|
+
};
|
|
61
|
+
return (input, init) => send(new Request(typeof input === "string" ? new URL(input, pageUrl) : input, init), 0);
|
|
62
|
+
};
|
|
63
|
+
/** How many redirects a fetch follows, as browsers and Node's `fetch` do. */
|
|
64
|
+
const MOST_REDIRECTS = 20;
|
|
65
|
+
/**
|
|
66
|
+
* The request a mocked redirect leads to, as `fetch` follows it: the same method and body for a 307 or a 308, a GET
|
|
67
|
+
* for a 303 and for a POST's 301 or 302; none when the answer is no redirect or the request does not follow one.
|
|
68
|
+
*
|
|
69
|
+
* @param answer - a handler's answer.
|
|
70
|
+
* @param request - the request it answered, its body unread.
|
|
71
|
+
* @throws a `TypeError` for a redirect a request with `redirect: "error"` gets, as `fetch` does.
|
|
72
|
+
*/
|
|
73
|
+
const redirectOf = (answer, request) => {
|
|
74
|
+
const location = answer.headers.get("location");
|
|
75
|
+
if (![301, 302, 303, 307, 308].includes(answer.status) || location === null)
|
|
76
|
+
return undefined;
|
|
77
|
+
if (request.redirect === "manual")
|
|
78
|
+
return undefined;
|
|
79
|
+
if (request.redirect === "error")
|
|
80
|
+
throw new TypeError(`Redirected to ${location}, which the request refuses`);
|
|
81
|
+
const target = new URL(location, request.url);
|
|
82
|
+
const isGet = answer.status === 303 || (request.method === "POST" && answer.status !== 307 && answer.status !== 308);
|
|
83
|
+
if (!isGet)
|
|
84
|
+
return new Request(target, request);
|
|
85
|
+
return new Request(target, {
|
|
86
|
+
method: request.method === "HEAD" ? "HEAD" : "GET",
|
|
87
|
+
headers: request.headers
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* The handlers a story's parameters declare, flat: a list, or a record of lists as MSW's Storybook addon writes them.
|
|
92
|
+
*
|
|
93
|
+
* @param parameters - the story's parameters.
|
|
94
|
+
*/
|
|
95
|
+
export const mockHandlersOf = (parameters) => {
|
|
96
|
+
const msw = parameters["msw"];
|
|
97
|
+
const handlers = isRecord(msw) ? msw["handlers"] : undefined;
|
|
98
|
+
if (Array.isArray(handlers))
|
|
99
|
+
return handlers;
|
|
100
|
+
return isRecord(handlers) ? Object.values(handlers).flat() : [];
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Makes a story's `{ once: true }` handlers answer again, as MSW's `restoreHandlers` does: each page load on a server is
|
|
104
|
+
* a page of its own, as each frame is, while the handler objects outlive it in the test process.
|
|
105
|
+
*
|
|
106
|
+
* @param handlers - the story's handlers.
|
|
107
|
+
*/
|
|
108
|
+
const restoreHandlers = (handlers) => {
|
|
109
|
+
for (const handler of handlers)
|
|
110
|
+
if (isRecord(handler) && typeof handler["isUsed"] === "boolean")
|
|
111
|
+
Reflect.set(handler, "isUsed", false);
|
|
112
|
+
};
|
|
113
|
+
/** Whether a value is an object with named fields. */
|
|
114
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
115
|
+
/**
|
|
116
|
+
* What the story's handlers answer: none when they do not, or when the one that does sends the request on to the
|
|
117
|
+
* network, which a server render never reaches.
|
|
118
|
+
*
|
|
119
|
+
* @param handlers - the story's handlers.
|
|
120
|
+
* @param request - the request.
|
|
121
|
+
* @param origin - the page's origin, which a handler's path (`/api/users`) is read from, as the frame reads it.
|
|
122
|
+
*/
|
|
123
|
+
const mockAnswer = async (handlers, request, origin) => {
|
|
124
|
+
if (handlers.length === 0)
|
|
125
|
+
return undefined;
|
|
126
|
+
const { getResponse } = await mswCore();
|
|
127
|
+
const answer = await getResponse([...handlers], request, { baseUrl: origin });
|
|
128
|
+
return answer === undefined || isPassthrough(answer) ? undefined : answer;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Whether an answer is MSW's `passthrough()`: MSW marks it with this status and header, which its own
|
|
132
|
+
* `isPassthroughResponse` reads (exported from MSW 3 only).
|
|
133
|
+
*
|
|
134
|
+
* @param answer - a handler's answer.
|
|
135
|
+
*/
|
|
136
|
+
const isPassthrough = (answer) => answer.status === 302 && answer.headers.get("x-msw-intention") === "passthrough";
|
|
137
|
+
/**
|
|
138
|
+
* Whether a module is MSW's, as much as a server render uses.
|
|
139
|
+
*
|
|
140
|
+
* @param module - the module, as `import()` gave it.
|
|
141
|
+
*/
|
|
142
|
+
const isMswCore = (module) => typeof module === "object" &&
|
|
143
|
+
module !== null &&
|
|
144
|
+
"getResponse" in module &&
|
|
145
|
+
typeof module.getResponse === "function";
|
|
146
|
+
/**
|
|
147
|
+
* Loads MSW, which only a story with handlers needs on a server.
|
|
148
|
+
*
|
|
149
|
+
* @throws when MSW is not installed, or is older than its `getResponse`.
|
|
150
|
+
*/
|
|
151
|
+
const mswCore = async () => {
|
|
152
|
+
let module;
|
|
153
|
+
try {
|
|
154
|
+
// named by a variable, so a frame's build never resolves MSW: the frame fetches through the page
|
|
155
|
+
module = await import(__rewriteRelativeImportExtension(/* @vite-ignore */ MSW_MODULE));
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
throw new Error("Answering a load's fetch with the story's mocks on a server needs MSW: npm install --save-dev msw", { cause: error });
|
|
159
|
+
}
|
|
160
|
+
if (!isMswCore(module))
|
|
161
|
+
throw new Error("The installed MSW has no getResponse: MSW 2.11 or later is needed");
|
|
162
|
+
return module;
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* The error of a request on the page's origin that no handler answered: what was asked, and what to give.
|
|
166
|
+
*
|
|
167
|
+
* @param request - the request.
|
|
168
|
+
*/
|
|
169
|
+
const unansweredError = (request) => new Error(`A load fetched ${request.method} ${request.url} on a server, and no mock of the story answers it: a server render ` +
|
|
170
|
+
"never reaches the network. Answer it with a handler in parameters.msw.handlers, or give the page's data " +
|
|
171
|
+
"with parameters.sveltekit.serverData or parameters.sveltekit.page.data.");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgebook/sveltekit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Forgebook for SvelteKit: a SvelteKit project's components drawn outside the app, with $app/state, $app/navigation, $app/forms, $app/paths, the environment and a page's load, on Vite.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sveltekit",
|
|
@@ -81,28 +81,35 @@
|
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
"dependencies": {
|
|
84
|
-
"@forgebook/story-format": "0.2.
|
|
85
|
-
"@forgebook/svelte": "0.2.
|
|
84
|
+
"@forgebook/story-format": "0.2.1",
|
|
85
|
+
"@forgebook/svelte": "0.2.1"
|
|
86
86
|
},
|
|
87
87
|
"peerDependencies": {
|
|
88
88
|
"svelte": "^5.46.4",
|
|
89
89
|
"vite": "^8.0.0",
|
|
90
|
-
"
|
|
90
|
+
"msw": "^2.11.0 || ^3.0.0",
|
|
91
|
+
"forgebook": "^0.3.0"
|
|
92
|
+
},
|
|
93
|
+
"peerDependenciesMeta": {
|
|
94
|
+
"msw": {
|
|
95
|
+
"optional": true
|
|
96
|
+
}
|
|
91
97
|
},
|
|
92
98
|
"devDependencies": {
|
|
93
99
|
"@sveltejs/kit": "3.1.1",
|
|
94
100
|
"@sveltejs/vite-plugin-svelte": "7.3.1",
|
|
95
101
|
"@types/node": "24.19.2",
|
|
96
102
|
"happy-dom": "21.0.4",
|
|
103
|
+
"msw": "3.0.3",
|
|
97
104
|
"svelte": "5.57.3",
|
|
98
105
|
"svelte-check": "4.7.8",
|
|
99
106
|
"typescript": "npm:typescript@6.0.3",
|
|
100
107
|
"vite": "8.3.5",
|
|
101
108
|
"vitest": "5.0.5",
|
|
102
|
-
"@forgebook/testing": "0.2.0",
|
|
103
109
|
"@forgebook/typescript-config": "0.0.0",
|
|
104
|
-
"forgebook": "0.
|
|
105
|
-
"@forgebook/
|
|
110
|
+
"@forgebook/tools": "0.0.0",
|
|
111
|
+
"@forgebook/testing": "0.2.1",
|
|
112
|
+
"forgebook": "0.3.0"
|
|
106
113
|
},
|
|
107
114
|
"publishConfig": {
|
|
108
115
|
"access": "public"
|