@praxisjs/ssg 0.1.2 → 0.1.4

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +39 -15
  4. package/dist/index.js.map +1 -1
  5. package/dist/loader-hook.d.ts.map +1 -1
  6. package/dist/loader-hook.js +3 -13
  7. package/dist/loader-hook.js.map +1 -1
  8. package/dist/prerender.d.ts +10 -7
  9. package/dist/prerender.d.ts.map +1 -1
  10. package/dist/prerender.js +80 -63
  11. package/dist/prerender.js.map +1 -1
  12. package/package.json +16 -8
  13. package/src/index.ts +42 -16
  14. package/src/loader-hook.ts +3 -13
  15. package/src/prerender.ts +95 -80
  16. package/dist/__tests__/loader-hook.test.d.ts +0 -2
  17. package/dist/__tests__/loader-hook.test.d.ts.map +0 -1
  18. package/dist/__tests__/loader-hook.test.js +0 -66
  19. package/dist/__tests__/loader-hook.test.js.map +0 -1
  20. package/dist/__tests__/plugin.test.d.ts +0 -2
  21. package/dist/__tests__/plugin.test.d.ts.map +0 -1
  22. package/dist/__tests__/plugin.test.js +0 -128
  23. package/dist/__tests__/plugin.test.js.map +0 -1
  24. package/dist/__tests__/prerender.test.d.ts +0 -2
  25. package/dist/__tests__/prerender.test.d.ts.map +0 -1
  26. package/dist/__tests__/prerender.test.js +0 -211
  27. package/dist/__tests__/prerender.test.js.map +0 -1
  28. package/dist/__tests__/register-fallback.test.d.ts +0 -2
  29. package/dist/__tests__/register-fallback.test.d.ts.map +0 -1
  30. package/dist/__tests__/register-fallback.test.js +0 -20
  31. package/dist/__tests__/register-fallback.test.js.map +0 -1
  32. package/dist/__tests__/resolve-sync.test.d.ts +0 -2
  33. package/dist/__tests__/resolve-sync.test.d.ts.map +0 -1
  34. package/dist/__tests__/resolve-sync.test.js +0 -67
  35. package/dist/__tests__/resolve-sync.test.js.map +0 -1
  36. package/dist/__tests__/routes.test.d.ts +0 -2
  37. package/dist/__tests__/routes.test.d.ts.map +0 -1
  38. package/dist/__tests__/routes.test.js +0 -154
  39. package/dist/__tests__/routes.test.js.map +0 -1
  40. package/src/__tests__/loader-hook.test.ts +0 -76
  41. package/src/__tests__/plugin.test.ts +0 -161
  42. package/src/__tests__/prerender.test.ts +0 -181
  43. package/src/__tests__/register-fallback.test.ts +0 -23
  44. package/src/__tests__/resolve-sync.test.ts +0 -91
  45. package/src/__tests__/routes.test.ts +0 -175
  46. package/tsconfig.json +0 -11
package/src/prerender.ts CHANGED
@@ -13,13 +13,13 @@ import type { ComponentConstructor } from "@praxisjs/shared/internal";
13
13
 
14
14
  import { normalizeRoutes, pathToOutputFile, resolvePaths, type GetStaticPaths, type RouteEntry } from "./routes";
15
15
 
16
- // Re-exported here (this whole file, plus these, only resolves once the
17
- // loader hook is registered — see register.ts) rather than from the main
18
- // package entry, so importing `ssgPlugin` alone never needs router/core/head
19
- // resolved statically. See index.ts.
16
+ // Re-exported here rather than from the main entry so importing `ssgPlugin` alone never needs
17
+ // router/core/head resolved statically: this module only loads once the loader hook in index.ts is
18
+ // registered.
20
19
  export { flattenRoutes, isStaticPath, normalizeRoutes, pathToOutputFile, resolvePaths, type FlattenedRoute, type ResolvePathsConfig } from "./routes";
21
20
 
22
21
  const SSG_MARKER = "data-praxis-ssg";
22
+ const ORIGIN = "http://localhost";
23
23
 
24
24
  export interface PrerenderConfig {
25
25
  /** Root component class — same one passed to `@Router([...])`/mounted by `main.tsx`. */
@@ -44,58 +44,111 @@ export interface PrerenderedPage {
44
44
  html: string;
45
45
  }
46
46
 
47
+ interface RenderOptions {
48
+ root: ComponentConstructor;
49
+ routes: RouteDefinition[];
50
+ routerOptions: RouterOptions | undefined;
51
+ containerSelector: string;
52
+ hydrate: boolean;
53
+ }
54
+
55
+ /**
56
+ * Renders every route into a full HTML page. One JSDOM window serves the whole run (parsing a new
57
+ * one per route cost more than most renders) and is reset before each route: a fresh copy of the
58
+ * template's document, the route's URL, empty storage and none of the window globals the previous
59
+ * route added. The window is closed at the end, which also stops any timer a page left running.
60
+ */
47
61
  export async function prerender(config: PrerenderConfig): Promise<PrerenderedPage[]> {
48
- const { getStaticPaths } = config;
49
62
  const routes = normalizeRoutes(config.routes);
50
- const paths = await resolvePaths({ routes, getStaticPaths });
63
+ const paths = await resolvePaths({ routes, getStaticPaths: config.getStaticPaths });
64
+ const options: RenderOptions = {
65
+ root: config.root,
66
+ routes,
67
+ routerOptions: config.routerOptions,
68
+ containerSelector: config.containerSelector ?? "#app",
69
+ hydrate: config.hydrate ?? true,
70
+ };
51
71
 
52
72
  const wasServerPass = isServerRenderPass();
53
73
  setServerRenderPass(true);
54
74
  try {
55
- const pages: PrerenderedPage[] = [];
56
- for (const path of paths) {
57
- pages.push(await renderPage(path, { ...config, routes }));
58
- }
59
- return pages;
75
+ return await withDomGlobals(config.template, `${ORIGIN}/`, async (dom) => {
76
+ const { window } = dom;
77
+ const pristine = window.document.documentElement.cloneNode(true);
78
+ const baseline = new Set(Object.getOwnPropertyNames(window));
79
+
80
+ const pages: PrerenderedPage[] = [];
81
+ for (const path of paths) {
82
+ resetWindow(dom, pristine, baseline, path);
83
+ pages.push(await renderPage(dom, path, options));
84
+ }
85
+ return pages;
86
+ });
60
87
  } finally {
61
88
  setServerRenderPass(wasServerPass);
62
89
  }
63
90
  }
64
91
 
65
- const PATCHED_GLOBALS = ["window", "document", "navigator", "history", "location", "Node", "Comment", "Text", "Element", "HTMLElement"] as const;
92
+ function resetWindow(dom: JSDOM, pristine: Node, baseline: Set<string>, path: string): void {
93
+ const { window } = dom;
94
+ const { document } = window;
95
+ document.replaceChild(pristine.cloneNode(true), document.documentElement);
96
+ window.history.replaceState(null, "", `${ORIGIN}${path}`);
97
+ window.localStorage.clear();
98
+ window.sessionStorage.clear();
99
+ const globals = window as unknown as Record<string, unknown>;
100
+ for (const key of Object.getOwnPropertyNames(window)) {
101
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- removing globals the previous route's code added
102
+ if (!baseline.has(key)) delete globals[key];
103
+ }
104
+ }
105
+
106
+ async function renderPage(dom: JSDOM, path: string, options: RenderOptions): Promise<PrerenderedPage> {
107
+ const { root, routes, routerOptions, containerSelector, hydrate } = options;
108
+ resetHeadState();
109
+
110
+ const container = dom.window.document.querySelector<HTMLElement>(containerSelector);
111
+ if (!container) {
112
+ throw new Error(`[@praxisjs/ssg] No element matching "${containerSelector}" found in the HTML template.`);
113
+ }
114
+
115
+ createRouter(routes, routerOptions);
116
+
117
+ const dispose = render(() => mountComponent(root, {}, getCurrentScope()), container);
118
+ try {
119
+ await flushPendingResources();
120
+ await Promise.resolve();
121
+
122
+ // The server always mounts fresh; the marker only tells the client to hydrate instead of remounting.
123
+ if (hydrate) container.setAttribute(SSG_MARKER, "1");
124
+
125
+ return { path, file: pathToOutputFile(path), html: dom.serialize() };
126
+ } finally {
127
+ dispose();
128
+ }
129
+ }
66
130
 
67
- type GlobalSnapshot = Partial<Record<(typeof PATCHED_GLOBALS)[number], PropertyDescriptor>>;
131
+ const PATCHED_GLOBALS = [
132
+ "window", "document", "navigator", "history", "location", "localStorage", "sessionStorage",
133
+ "Node", "Comment", "Text", "Element", "HTMLElement",
134
+ ] as const;
68
135
 
69
- // Modern Node already defines some of these itself (globalThis.navigator is a
70
- // getter-only built-in since Node 21) — plain assignment throws on those, so
71
- // every property is installed *and* restored via defineProperty, capturing
72
- // the original descriptor (not just its value) to put back exactly what was
73
- // there, getter and all, rather than flattening it into a plain value.
136
+ type PatchedGlobal = (typeof PATCHED_GLOBALS)[number];
137
+ type GlobalSnapshot = Partial<Record<PatchedGlobal, PropertyDescriptor>>;
138
+
139
+ // Modern Node defines some of these itself (globalThis.navigator is a getter-only built-in since
140
+ // Node 21), where plain assignment throws. Every property is installed and restored through
141
+ // defineProperty, keeping the original descriptor so a getter comes back as a getter.
74
142
  function installDomGlobals(dom: JSDOM): GlobalSnapshot {
75
- const g = globalThis as unknown as Record<string, unknown>;
76
143
  const snapshot: GlobalSnapshot = {};
144
+ const { window } = dom;
145
+ const source = window as unknown as Record<PatchedGlobal, unknown>;
77
146
  for (const key of PATCHED_GLOBALS) {
78
147
  const descriptor = Object.getOwnPropertyDescriptor(globalThis, key);
79
148
  if (descriptor) snapshot[key] = descriptor;
149
+ const value = key === "window" ? window : source[key];
150
+ Object.defineProperty(globalThis, key, { value, writable: true, configurable: true, enumerable: true });
80
151
  }
81
-
82
- const { window } = dom;
83
- const values: Record<(typeof PATCHED_GLOBALS)[number], unknown> = {
84
- window,
85
- document: window.document,
86
- navigator: window.navigator,
87
- history: window.history,
88
- location: window.location,
89
- Node: window.Node,
90
- Comment: window.Comment,
91
- Text: window.Text,
92
- Element: window.Element,
93
- HTMLElement: window.HTMLElement,
94
- };
95
- for (const key of PATCHED_GLOBALS) {
96
- Object.defineProperty(g, key, { value: values[key], writable: true, configurable: true, enumerable: true });
97
- }
98
-
99
152
  return snapshot;
100
153
  }
101
154
 
@@ -113,13 +166,10 @@ function restoreGlobals(snapshot: GlobalSnapshot): void {
113
166
  }
114
167
 
115
168
  /**
116
- * Runs `fn` with a fresh JSDOM's `window`/`document`/etc installed as globals
117
- * for its duration, then restores whatever was there before. Exported so
118
- * `index.ts`'s `loadEntry()` can use it too: `ssrLoadModule()`-ing the app's
119
- * root module runs `@Router([...])`'s class decorator, which calls
120
- * `createRouter()` immediately (at module-evaluation time) — that needs a
121
- * `window` in place just as much as the real per-route render below does,
122
- * even though it's not rendering anything itself yet.
169
+ * Runs `fn` with a fresh JSDOM's `window`/`document`/etc installed as globals, then restores what
170
+ * was there before and closes the window. Also used by index.ts's `loadEntry()`: loading the root
171
+ * module runs `@Router([...])`, which calls `createRouter()` at module-evaluation time and so needs
172
+ * a `window` even though nothing is rendered yet.
123
173
  */
124
174
  export async function withDomGlobals<T>(html: string, url: string, fn: (dom: JSDOM) => Promise<T>): Promise<T> {
125
175
  const dom = new JSDOM(html, { url });
@@ -128,41 +178,6 @@ export async function withDomGlobals<T>(html: string, url: string, fn: (dom: JSD
128
178
  return await fn(dom);
129
179
  } finally {
130
180
  restoreGlobals(snapshot);
181
+ dom.window.close();
131
182
  }
132
183
  }
133
-
134
- async function renderPage(
135
- path: string,
136
- config: Omit<PrerenderConfig, "routes"> & { routes: RouteDefinition[] },
137
- ): Promise<PrerenderedPage> {
138
- const { root, routes, routerOptions, template, hydrate = true } = config;
139
- const containerSelector = config.containerSelector ?? "#app";
140
-
141
- return withDomGlobals(template, `http://localhost${path}`, async (dom) => {
142
- resetHeadState();
143
-
144
- const container = dom.window.document.querySelector(containerSelector);
145
- if (!container) {
146
- throw new Error(`[@praxisjs/ssg] No element matching "${containerSelector}" found in the HTML template.`);
147
- }
148
-
149
- createRouter(routes, routerOptions);
150
-
151
- const dispose = render(
152
- () => mountComponent(root, {}, getCurrentScope()),
153
- container as HTMLElement,
154
- );
155
-
156
- await flushPendingResources();
157
- await Promise.resolve();
158
-
159
- // Marked *after* rendering — the server always mounts fresh (create-mode);
160
- // the marker only tells the client to hydrate instead of remounting.
161
- if (hydrate) container.setAttribute(SSG_MARKER, "1");
162
-
163
- const html = dom.serialize();
164
- dispose();
165
-
166
- return { path, file: pathToOutputFile(path), html };
167
- });
168
- }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=loader-hook.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"loader-hook.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/loader-hook.test.ts"],"names":[],"mappings":""}
@@ -1,66 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
- import { resolve } from "../loader-hook";
3
- const CONTEXT = {};
4
- function notFound() {
5
- const err = new Error("not found");
6
- err.code = "ERR_MODULE_NOT_FOUND";
7
- throw err;
8
- }
9
- function ok(url) {
10
- return { url, shortCircuit: true };
11
- }
12
- describe("loader-hook resolve()", () => {
13
- it("returns nextResolve's result directly when it succeeds", async () => {
14
- const nextResolve = vi.fn(async () => ok("file:///real.js"));
15
- const result = await resolve("./real", CONTEXT, nextResolve);
16
- expect(result).toEqual(ok("file:///real.js"));
17
- expect(nextResolve).toHaveBeenCalledOnce();
18
- });
19
- it("rethrows without retrying for a bare (non-relative) specifier", async () => {
20
- const nextResolve = vi.fn(async () => notFound());
21
- await expect(resolve("some-package", CONTEXT, nextResolve)).rejects.toThrow("not found");
22
- expect(nextResolve).toHaveBeenCalledOnce();
23
- });
24
- it("rethrows without retrying when the error has no code", async () => {
25
- const nextResolve = vi.fn(async () => {
26
- throw new Error("weird failure");
27
- });
28
- await expect(resolve("./router", CONTEXT, nextResolve)).rejects.toThrow("weird failure");
29
- expect(nextResolve).toHaveBeenCalledOnce();
30
- });
31
- it("rethrows without retrying for a non-retryable error code", async () => {
32
- const nextResolve = vi.fn(async () => {
33
- const err = new Error("permission denied");
34
- err.code = "EACCES";
35
- throw err;
36
- });
37
- await expect(resolve("./router", CONTEXT, nextResolve)).rejects.toThrow("permission denied");
38
- expect(nextResolve).toHaveBeenCalledOnce();
39
- });
40
- it("retries with a .js suffix and returns it when that succeeds", async () => {
41
- const nextResolve = vi.fn(async (specifier) => {
42
- if (specifier === "./router.js")
43
- return ok("file:///router.js");
44
- return notFound();
45
- });
46
- const result = await resolve("./router", CONTEXT, nextResolve);
47
- expect(result).toEqual(ok("file:///router.js"));
48
- expect(nextResolve).toHaveBeenCalledTimes(2);
49
- });
50
- it("falls back to /index.js when the .js candidate also fails", async () => {
51
- const nextResolve = vi.fn(async (specifier) => {
52
- if (specifier === "./router/index.js")
53
- return ok("file:///router/index.js");
54
- return notFound();
55
- });
56
- const result = await resolve("./router", CONTEXT, nextResolve);
57
- expect(result).toEqual(ok("file:///router/index.js"));
58
- expect(nextResolve).toHaveBeenCalledTimes(3);
59
- });
60
- it("rethrows the original error when both retry candidates fail", async () => {
61
- const nextResolve = vi.fn(async () => notFound());
62
- await expect(resolve("../router", CONTEXT, nextResolve)).rejects.toThrow("not found");
63
- expect(nextResolve).toHaveBeenCalledTimes(3);
64
- });
65
- });
66
- //# sourceMappingURL=loader-hook.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"loader-hook.test.js","sourceRoot":"","sources":["../../src/__tests__/loader-hook.test.ts"],"names":[],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=plugin.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"plugin.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/plugin.test.ts"],"names":[],"mappings":""}
@@ -1,128 +0,0 @@
1
- import { mkdtemp, readFile, rm, writeFile, mkdir } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { describe, it, expect, vi, afterEach } from "vitest";
5
- import { prerender } from "../prerender";
6
- import { ssgPlugin } from "../index";
7
- vi.mock("../prerender", () => ({
8
- prerender: vi.fn(),
9
- // Pass-through stand-in — the real DOM-global install/restore mechanics
10
- // are covered in prerender.test.ts; here we only need loadEntry() to call
11
- // through to whatever function it wraps.
12
- withDomGlobals: vi.fn((_html, _url, fn) => fn()),
13
- }));
14
- const ssrLoadModule = vi.fn();
15
- const close = vi.fn();
16
- const createServer = vi.fn(async (_options) => ({ ssrLoadModule, close }));
17
- vi.mock("vite", () => ({
18
- createServer: (options) => createServer(options),
19
- }));
20
- // registerLoaderHook()'s fallback path (module.register with a file URL)
21
- // assumes a built dist/ layout — loader-hook.js physically next to index.js
22
- // — which doesn't exist under vitest's on-the-fly source transform. The
23
- // mechanism itself is covered separately by running the actual built dist/
24
- // output through Node directly (see the plan's manual verification step);
25
- // here we only need to confirm ssgPlugin() triggers registration.
26
- const { registerHooks, register } = vi.hoisted(() => ({
27
- registerHooks: vi.fn(),
28
- register: vi.fn(),
29
- }));
30
- vi.mock("node:module", async (importOriginal) => {
31
- const actual = await importOriginal();
32
- return { ...actual, registerHooks, register };
33
- });
34
- const RootComponent = class {
35
- };
36
- const routes = [{ path: "/", component: RootComponent }];
37
- function fakeResolvedConfig(root, outDir = "dist") {
38
- return {
39
- root,
40
- build: { outDir },
41
- configFile: `${root}/vite.config.ts`,
42
- };
43
- }
44
- describe("ssgPlugin()", () => {
45
- let dir;
46
- afterEach(async () => {
47
- vi.clearAllMocks();
48
- if (dir)
49
- await rm(dir, { recursive: true, force: true });
50
- });
51
- it("loads the root component and routes from a single SSR module load, prerenders, and writes files", async () => {
52
- dir = await mkdtemp(join(tmpdir(), "praxisjs-ssg-"));
53
- await mkdir(join(dir, "dist"), { recursive: true });
54
- await writeFile(join(dir, "dist", "index.html"), "<html><body><div id=\"app\"></div></body></html>");
55
- ssrLoadModule.mockResolvedValue({ default: RootComponent, routes });
56
- vi.mocked(prerender).mockResolvedValue([
57
- { path: "/", file: "index.html", html: "<html>home</html>" },
58
- { path: "/about", file: "about/index.html", html: "<html>about</html>" },
59
- ]);
60
- const plugin = ssgPlugin({ root: "./src/app.tsx" });
61
- // ssgPlugin() must register the extensionless-import retry hook
62
- // synchronously, before closeBundle ever dynamically imports
63
- // ./prerender (which is what actually needs it) — see index.ts.
64
- expect(registerHooks.mock.calls.length + register.mock.calls.length).toBeGreaterThan(0);
65
- const configResolved = plugin.configResolved;
66
- await configResolved(fakeResolvedConfig(dir));
67
- const closeBundle = plugin.closeBundle;
68
- await closeBundle();
69
- expect(createServer).toHaveBeenCalledWith(expect.objectContaining({
70
- root: dir,
71
- configFile: `${dir}/vite.config.ts`,
72
- server: { middlewareMode: true, watch: null },
73
- appType: "custom",
74
- ssr: { external: true },
75
- optimizeDeps: { noDiscovery: true, include: [] },
76
- }));
77
- // A single ssrLoadModule call reads both the default export and the
78
- // named `routes` export — vite.config.ts never imports app source itself.
79
- expect(ssrLoadModule).toHaveBeenCalledOnce();
80
- expect(ssrLoadModule).toHaveBeenCalledWith("./src/app.tsx");
81
- expect(close).toHaveBeenCalledOnce();
82
- expect(prerender).toHaveBeenCalledWith(expect.objectContaining({
83
- root: RootComponent,
84
- routes,
85
- template: "<html><body><div id=\"app\"></div></body></html>",
86
- }));
87
- expect(await readFile(join(dir, "dist", "index.html"), "utf-8")).toBe("<html>home</html>");
88
- expect(await readFile(join(dir, "dist", "about", "index.html"), "utf-8")).toBe("<html>about</html>");
89
- });
90
- it("throws a clear error when the root module has no default export", async () => {
91
- dir = await mkdtemp(join(tmpdir(), "praxisjs-ssg-"));
92
- await mkdir(join(dir, "dist"), { recursive: true });
93
- await writeFile(join(dir, "dist", "index.html"), "<html><body><div id=\"app\"></div></body></html>");
94
- ssrLoadModule.mockResolvedValue({ routes });
95
- const plugin = ssgPlugin({ root: "./src/app.tsx" });
96
- const configResolved = plugin.configResolved;
97
- await configResolved(fakeResolvedConfig(dir));
98
- const closeBundle = plugin.closeBundle;
99
- await expect(closeBundle()).rejects.toThrow(/no default export/);
100
- expect(close).toHaveBeenCalledOnce(); // server is closed even on failure
101
- });
102
- it("throws a clear error when the root module has no named routes export", async () => {
103
- dir = await mkdtemp(join(tmpdir(), "praxisjs-ssg-"));
104
- await mkdir(join(dir, "dist"), { recursive: true });
105
- await writeFile(join(dir, "dist", "index.html"), "<html><body><div id=\"app\"></div></body></html>");
106
- ssrLoadModule.mockResolvedValue({ default: RootComponent });
107
- const plugin = ssgPlugin({ root: "./src/app.tsx" });
108
- const configResolved = plugin.configResolved;
109
- await configResolved(fakeResolvedConfig(dir));
110
- const closeBundle = plugin.closeBundle;
111
- await expect(closeBundle()).rejects.toThrow(/no named "routes" export/);
112
- expect(close).toHaveBeenCalledOnce();
113
- });
114
- it("passes hydrate: false through to prerender()", async () => {
115
- dir = await mkdtemp(join(tmpdir(), "praxisjs-ssg-"));
116
- await mkdir(join(dir, "dist"), { recursive: true });
117
- await writeFile(join(dir, "dist", "index.html"), "<html><body><div id=\"app\"></div></body></html>");
118
- ssrLoadModule.mockResolvedValue({ default: RootComponent, routes });
119
- vi.mocked(prerender).mockResolvedValue([{ path: "/", file: "index.html", html: "<html></html>" }]);
120
- const plugin = ssgPlugin({ root: "./src/app.tsx", hydrate: false });
121
- const configResolved = plugin.configResolved;
122
- await configResolved(fakeResolvedConfig(dir));
123
- const closeBundle = plugin.closeBundle;
124
- await closeBundle();
125
- expect(prerender).toHaveBeenCalledWith(expect.objectContaining({ hydrate: false }));
126
- });
127
- });
128
- //# sourceMappingURL=plugin.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"plugin.test.js","sourceRoot":"","sources":["../../src/__tests__/plugin.test.ts"],"names":[],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=prerender.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"prerender.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/prerender.test.ts"],"names":[],"mappings":""}