houdini-react 2.0.0-next.41 → 2.0.0-next.45

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,163 @@
1
+ // @refresh reset
2
+ import { getCurrentConfig } from '$houdini/runtime/config'
3
+ import { Cache } from 'houdini/runtime/cache'
4
+ import { DataSource } from 'houdini/runtime/types'
5
+ import { HoudiniClient } from '$houdini/runtime/client'
6
+ import React from 'react'
7
+
8
+ import { scalarMarshalers, serializeSearch } from './resolve-href.js'
9
+ import { Router as RouterImpl, RouterContextProvider, router_cache } from './routing/index.js'
10
+ import manifest from './manifest.js'
11
+
12
+ type MockValue =
13
+ | Record<string, unknown>
14
+ | ((vars: any) => Record<string, unknown>)
15
+ | AsyncIterable<Record<string, unknown>>
16
+ | ((vars: any) => AsyncIterable<Record<string, unknown>>)
17
+
18
+ // buildMockPath turns a route pattern, its params, and an optional search object into
19
+ // the concrete path a test wants to render. The typed createMock wrapper calls this so
20
+ // that param substitution (and its missing-param error) and search serialization both
21
+ // happen before the path reaches _createMock.
22
+ export function buildMockPath(
23
+ pattern: string,
24
+ params: Record<string, string>,
25
+ search?: Record<string, unknown>
26
+ ): string {
27
+ if (!search) {
28
+ return buildURL(pattern, params)
29
+ }
30
+ // marshal custom-scalar search values the same way <Link> does, so tests exercise
31
+ // the real serialization path
32
+ const m = manifest as any
33
+ const page = m.pages[m.pagesByUrl[pattern]] as
34
+ | { searchParams?: ReadonlyArray<{ name: string; type: string }> }
35
+ | undefined
36
+ const marshalers = scalarMarshalers(page?.searchParams, getCurrentConfig()?.scalars)
37
+ return buildURL(pattern, params) + serializeSearch(search, marshalers)
38
+ }
39
+
40
+ export function _createMock({
41
+ path,
42
+ data: mocks,
43
+ }: {
44
+ path: string
45
+ data: Record<string, MockValue>
46
+ }): React.ComponentType<{}> {
47
+ // the route patterns are anchored to the path, so strip any search string
48
+ // before matching
49
+ const pathname = path.split('?')[0]
50
+
51
+ // Validate required mocks up-front at the call site so the error points
52
+ // directly to the createMock() call rather than surfacing during rendering.
53
+ const pages = manifest.pages as Record<
54
+ string,
55
+ { pattern: RegExp; documents: Record<string, unknown> }
56
+ >
57
+ const page = Object.values(pages).find((p) => p.pattern.test(pathname))
58
+ if (page) {
59
+ const missing = Object.keys(page.documents).filter((name) => !(name in mocks))
60
+ if (missing.length > 0) {
61
+ throw new Error(
62
+ `createMock: missing mock data for ${missing.map((n) => `"${n}"`).join(', ')} on route "${pathname}". Add ${missing.length === 1 ? 'it' : 'them'} to the data object passed to createMock.`
63
+ )
64
+ }
65
+ }
66
+
67
+ const cache = new Cache(getCurrentConfig())
68
+
69
+ const mockPlugin = () => ({
70
+ network(ctx: any, { resolve }: any) {
71
+ const mock = mocks[ctx.artifact.name]
72
+ if (mock === undefined) {
73
+ throw new Error(
74
+ `createMock: "${ctx.artifact.name}" fired but was not in data. Add it to the data object passed to createMock.`
75
+ )
76
+ }
77
+
78
+ if (ctx.artifact.kind === 'HoudiniSubscription') {
79
+ const iterable =
80
+ typeof mock === 'function'
81
+ ? (mock as (v: any) => AsyncIterable<Record<string, unknown>>)(
82
+ ctx.variables ?? {}
83
+ )
84
+ : (mock as AsyncIterable<Record<string, unknown>>)
85
+ const iterator = iterable[Symbol.asyncIterator]()
86
+ ctx.abortController.signal.addEventListener(
87
+ 'abort',
88
+ () => {
89
+ iterator.return?.()
90
+ },
91
+ { once: true }
92
+ )
93
+ ;(async () => {
94
+ for await (const data of { [Symbol.asyncIterator]: () => iterator }) {
95
+ if (ctx.abortController.signal.aborted) break
96
+ resolve(ctx, {
97
+ data,
98
+ errors: null,
99
+ fetching: false,
100
+ variables: null,
101
+ source: DataSource.Network,
102
+ partial: false,
103
+ stale: false,
104
+ })
105
+ }
106
+ })()
107
+ return
108
+ }
109
+
110
+ const data = typeof mock === 'function' ? mock(ctx.variables ?? {}) : mock
111
+ resolve(ctx, {
112
+ data,
113
+ errors: null,
114
+ fetching: false,
115
+ variables: null,
116
+ source: DataSource.Network,
117
+ partial: false,
118
+ stale: false,
119
+ })
120
+ },
121
+ })
122
+
123
+ // Pass the same fresh cache to the client so the cache policy plugin doesn't
124
+ // reach into the global cacheRef singleton and return stale data from previous tests.
125
+ const mockClient = new HoudiniClient({
126
+ url: 'http://localhost/graphql',
127
+ plugins: [mockPlugin as any],
128
+ cache,
129
+ })
130
+
131
+ return function MockApp() {
132
+ const cachesRef = React.useRef<ReturnType<typeof router_cache> | null>(null)
133
+ if (cachesRef.current === null) {
134
+ cachesRef.current = router_cache()
135
+ }
136
+ const caches = cachesRef.current
137
+ return (
138
+ <RouterContextProvider
139
+ client={mockClient}
140
+ cache={cache}
141
+ artifact_cache={caches.artifact_cache}
142
+ component_cache={caches.component_cache}
143
+ data_cache={caches.data_cache}
144
+ ssr_signals={caches.ssr_signals}
145
+ last_variables={caches.last_variables}
146
+ >
147
+ <React.Suspense fallback={null}>
148
+ <RouterImpl manifest={manifest} initialURL={path} assetPrefix="" />
149
+ </React.Suspense>
150
+ </RouterContextProvider>
151
+ )
152
+ }
153
+ }
154
+
155
+ function buildURL(pattern: string, params: Record<string, string>): string {
156
+ return pattern.replace(/\[([^\]]+)\]/g, (_, key) => {
157
+ const value = params[key]
158
+ if (value === undefined) {
159
+ throw new Error(`createMock: missing param "${key}" for pattern "${pattern}"`)
160
+ }
161
+ return String(value)
162
+ })
163
+ }
package/vite/index.js CHANGED
@@ -146,7 +146,7 @@ function index_default(ctx) {
146
146
  hotUpdate() {
147
147
  cfCache = null;
148
148
  },
149
- async transform(code, filepath) {
149
+ async transform(code, filepath, options) {
150
150
  filepath = path.posixify(filepath);
151
151
  if (filepath.startsWith("/src/")) {
152
152
  filepath = path.join(process.cwd(), filepath);
@@ -154,6 +154,7 @@ function index_default(ctx) {
154
154
  if (!ctx.config.includeFile(filepath)) {
155
155
  return;
156
156
  }
157
+ const stripHeaders = !options?.ssr && /(?:^|\/)\+(?:page|layout)\.[jt]sx?$/.test(filepath);
157
158
  if (cfCache === null) {
158
159
  try {
159
160
  cfCache = ctx.db.all(
@@ -170,7 +171,8 @@ function index_default(ctx) {
170
171
  filepath: path.posixify(filepath),
171
172
  watch_file: this.addWatchFile.bind(this)
172
173
  },
173
- cfCache
174
+ cfCache,
175
+ { stripHeaders }
174
176
  );
175
177
  },
176
178
  async closeBundle() {
@@ -284,9 +286,15 @@ mount_static_app(App, manifest)
284
286
  next();
285
287
  return;
286
288
  }
287
- const { default: router_manifest } = await server.ssrLoadModule(
289
+ const manifest_module = await server.ssrLoadModule(
288
290
  path.join(plugin_dir(ctx.config, "houdini-react"), "runtime", "manifest.ts")
289
291
  );
292
+ const router_manifest = manifest_module.default;
293
+ for (const id of Object.keys(manifest_module.route_headers ?? {})) {
294
+ if (router_manifest.pages[id]) {
295
+ router_manifest.pages[id].headers = manifest_module.route_headers[id];
296
+ }
297
+ }
290
298
  const { createServerAdapter } = await server.ssrLoadModule(
291
299
  adapter_config_path(ctx.config)
292
300
  );
@@ -0,0 +1 @@
1
+ export declare function strip_named_export(script: any, name: string): void;
@@ -0,0 +1,35 @@
1
+ function strip_named_export(script, name) {
2
+ const body = script.body;
3
+ for (let i = body.length - 1; i >= 0; i--) {
4
+ const node = body[i];
5
+ if (node.type !== "ExportNamedDeclaration") {
6
+ continue;
7
+ }
8
+ if (node.declaration) {
9
+ const decl = node.declaration;
10
+ if (decl.type === "FunctionDeclaration" && decl.id?.name === name) {
11
+ body.splice(i, 1);
12
+ continue;
13
+ }
14
+ if (decl.type === "VariableDeclaration") {
15
+ decl.declarations = decl.declarations.filter(
16
+ (d) => !(d.id?.type === "Identifier" && d.id.name === name)
17
+ );
18
+ if (decl.declarations.length === 0) {
19
+ body.splice(i, 1);
20
+ }
21
+ continue;
22
+ }
23
+ continue;
24
+ }
25
+ if (node.specifiers?.length) {
26
+ node.specifiers = node.specifiers.filter((s) => s.exported?.name !== name);
27
+ if (node.specifiers.length === 0) {
28
+ body.splice(i, 1);
29
+ }
30
+ }
31
+ }
32
+ }
33
+ export {
34
+ strip_named_export
35
+ };
@@ -5,7 +5,9 @@ export type ComponentFieldRow = {
5
5
  field: string;
6
6
  fragment: string;
7
7
  };
8
- export declare function transform_file(page: TransformPage, cfRows: ComponentFieldRow[]): Promise<{
8
+ export declare function transform_file(page: TransformPage, cfRows: ComponentFieldRow[], opts?: {
9
+ stripHeaders?: boolean;
10
+ }): Promise<{
9
11
  code: string;
10
12
  map?: SourceMapInput;
11
13
  }>;
package/vite/transform.js CHANGED
@@ -10,13 +10,17 @@ import {
10
10
  } from "houdini";
11
11
  import { componentField_unit_path, houdini_root } from "houdini/router/conventions";
12
12
  import * as recast from "recast";
13
+ import { strip_named_export } from "./strip-headers.js";
13
14
  const AST = recast.types.builders;
14
- async function transform_file(page, cfRows) {
15
+ async function transform_file(page, cfRows, opts = {}) {
15
16
  const isJSX = page.filepath.endsWith(".tsx") || page.filepath.endsWith(".jsx");
16
17
  if (!isJSX && !page.filepath.endsWith(".ts") && !page.filepath.endsWith(".js")) {
17
18
  return { code: page.content, map: page.map };
18
19
  }
19
20
  const script = parseJS(page.content, isJSX ? { plugins: ["jsx"] } : {});
21
+ if (opts.stripHeaders) {
22
+ strip_named_export(script, "headers");
23
+ }
20
24
  const cfMap = {};
21
25
  for (const row of cfRows) {
22
26
  if (row.type && row.field && row.fragment) {
@@ -29,9 +33,9 @@ async function transform_file(page, cfRows) {
29
33
  tag({ node, artifact, parsedDocument }) {
30
34
  const { id: artifactRef } = artifact_import({ page, script, artifact });
31
35
  const properties = [AST.objectProperty(AST.stringLiteral("artifact"), artifactRef)];
32
- if (is_paginated(parsedDocument)) {
36
+ if (is_paginated(parsedDocument) || is_refetchable(parsedDocument)) {
33
37
  if (artifact.kind !== ArtifactKind.Query) {
34
- const refetchName = artifact.name + "_Pagination_Query";
38
+ const refetchName = artifact.name + (is_paginated(parsedDocument) ? "_Pagination_Query" : "_Refetch_Query");
35
39
  const { id: refetchRef } = artifact_import({
36
40
  page,
37
41
  script,
@@ -85,6 +89,17 @@ function is_paginated(doc) {
85
89
  });
86
90
  return paginated;
87
91
  }
92
+ function is_refetchable(doc) {
93
+ let refetchable = false;
94
+ graphql.visit(doc, {
95
+ Directive(node) {
96
+ if (node.name.value === "refetchable") {
97
+ refetchable = true;
98
+ }
99
+ }
100
+ });
101
+ return refetchable;
102
+ }
88
103
  export {
89
104
  transform_file
90
105
  };