@quilted/create 0.2.17 → 0.2.19

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 (40) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +1 -1
  3. package/templates/app-basic/App.tsx +18 -22
  4. package/templates/app-basic/browser.tsx +8 -1
  5. package/templates/app-basic/features/start.ts +2 -2
  6. package/templates/app-basic/foundation/html/HTML.test.tsx +46 -0
  7. package/templates/app-basic/foundation/{http/Headers.tsx → html/HTML.tsx} +50 -10
  8. package/templates/app-basic/foundation/html.ts +1 -1
  9. package/templates/app-basic/package.json +1 -1
  10. package/templates/app-graphql/App.tsx +24 -41
  11. package/templates/app-graphql/browser.tsx +9 -1
  12. package/templates/app-graphql/features/start.ts +2 -2
  13. package/templates/app-graphql/foundation/html/HTML.test.tsx +46 -0
  14. package/templates/app-graphql/foundation/{http/Headers.tsx → html/HTML.tsx} +50 -10
  15. package/templates/app-graphql/foundation/html.ts +1 -1
  16. package/templates/app-graphql/package.json +1 -1
  17. package/templates/app-graphql/server/graphql.ts +1 -1
  18. package/templates/app-graphql/server.tsx +8 -2
  19. package/templates/app-graphql/shared/graphql.ts +11 -0
  20. package/templates/app-graphql/tests/render/render.tsx +6 -1
  21. package/templates/app-trpc/App.tsx +30 -46
  22. package/templates/app-trpc/browser.tsx +10 -1
  23. package/templates/app-trpc/features/start.ts +2 -2
  24. package/templates/app-trpc/foundation/html/HTML.test.tsx +46 -0
  25. package/templates/app-trpc/foundation/{http/Headers.tsx → html/HTML.tsx} +50 -10
  26. package/templates/app-trpc/foundation/html.ts +1 -1
  27. package/templates/app-trpc/server.tsx +8 -2
  28. package/templates/app-trpc/shared/trpc.ts +4 -2
  29. package/templates/app-basic/foundation/html/Head.test.tsx +0 -25
  30. package/templates/app-basic/foundation/html/Head.tsx +0 -34
  31. package/templates/app-basic/foundation/http/Headers.test.tsx +0 -24
  32. package/templates/app-basic/foundation/http.ts +0 -1
  33. package/templates/app-graphql/foundation/html/Head.test.tsx +0 -25
  34. package/templates/app-graphql/foundation/html/Head.tsx +0 -34
  35. package/templates/app-graphql/foundation/http/Headers.test.tsx +0 -24
  36. package/templates/app-graphql/foundation/http.ts +0 -1
  37. package/templates/app-trpc/foundation/html/Head.test.tsx +0 -25
  38. package/templates/app-trpc/foundation/html/Head.tsx +0 -34
  39. package/templates/app-trpc/foundation/http/Headers.test.tsx +0 -24
  40. package/templates/app-trpc/foundation/http.ts +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @quilted/create
2
2
 
3
+ ## 0.2.19
4
+
5
+ ### Patch Changes
6
+
7
+ - [#716](https://github.com/lemonmade/quilt/pull/716) [`7daafca`](https://github.com/lemonmade/quilt/commit/7daafca900b3d9ea66be179394eadf7998cc94be) Thanks [@lemonmade](https://github.com/lemonmade)! - Refactor browser APIs
8
+
9
+ ## 0.2.18
10
+
11
+ ### Patch Changes
12
+
13
+ - [#714](https://github.com/lemonmade/quilt/pull/714) [`d4bda43`](https://github.com/lemonmade/quilt/commit/d4bda430900d0e4afd5ccecb04abe9ac81245486) Thanks [@lemonmade](https://github.com/lemonmade)! - Update GraphQL dependencies
14
+
15
+ - [#699](https://github.com/lemonmade/quilt/pull/699) [`8335c47`](https://github.com/lemonmade/quilt/commit/8335c47fa1896ad65d5cd218fe068f22627815d9) Thanks [@lemonmade](https://github.com/lemonmade)! - Update async APIs
16
+
3
17
  ## 0.2.17
4
18
 
5
19
  ### Patch Changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@quilted/create",
3
3
  "type": "module",
4
- "version": "0.2.17",
4
+ "version": "0.2.19",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -1,11 +1,9 @@
1
1
  import {type PropsWithChildren} from 'react';
2
2
 
3
- import {HTML} from '@quilted/quilt/html';
4
3
  import {Routing, useRoutes} from '@quilted/quilt/navigate';
5
4
  import {Localization, useLocaleFromEnvironment} from '@quilted/quilt/localize';
6
5
 
7
- import {Head} from './foundation/html.ts';
8
- import {Headers} from './foundation/http.ts';
6
+ import {HTML} from './foundation/html.ts';
9
7
  import {Frame} from './foundation/frame.ts';
10
8
 
11
9
  import {Start} from './features/start.ts';
@@ -15,27 +13,21 @@ import {
15
13
  type AppContext as AppContextType,
16
14
  } from './shared/context.ts';
17
15
 
18
- export interface AppProps extends AppContextType {}
16
+ export interface AppProps {
17
+ context?: AppContextType;
18
+ }
19
19
 
20
20
  // The root component for your application. You will typically render any
21
21
  // app-wide context in this component.
22
- export function App(props: AppProps) {
23
- const locale = useLocaleFromEnvironment() ?? 'en';
24
-
22
+ export function App({context}: AppProps) {
25
23
  return (
26
- <HTML>
27
- <Localization locale={locale}>
28
- <Routing>
29
- <AppContext {...props}>
30
- <Headers />
31
- <Head />
32
- <Frame>
33
- <Routes />
34
- </Frame>
35
- </AppContext>
36
- </Routing>
37
- </Localization>
38
- </HTML>
24
+ <AppContext context={context}>
25
+ <HTML>
26
+ <Frame>
27
+ <Routes />
28
+ </Frame>
29
+ </HTML>
30
+ </AppContext>
39
31
  );
40
32
  }
41
33
 
@@ -50,10 +42,14 @@ function Routes() {
50
42
  }
51
43
 
52
44
  // This component renders any app-wide context.
53
- function AppContext({children, ...context}: PropsWithChildren<AppProps>) {
45
+ function AppContext({children, context}: PropsWithChildren<AppProps>) {
46
+ const locale = useLocaleFromEnvironment() ?? 'en';
47
+
54
48
  return (
55
49
  <AppContextReact.Provider value={context}>
56
- {children}
50
+ <Localization locale={locale}>
51
+ <Routing>{children}</Routing>
52
+ </Localization>
57
53
  </AppContextReact.Provider>
58
54
  );
59
55
  }
@@ -1,8 +1,15 @@
1
1
  import '@quilted/quilt/globals';
2
2
  import {hydrateRoot} from 'react-dom/client';
3
+ import {Browser, BrowserContext} from '@quilted/quilt/browser';
3
4
 
4
5
  import {App} from './App.tsx';
5
6
 
6
7
  const element = document.querySelector('#app')!;
8
+ const browser = new Browser();
7
9
 
8
- hydrateRoot(element, <App />);
10
+ hydrateRoot(
11
+ element,
12
+ <BrowserContext browser={browser}>
13
+ <App />
14
+ </BrowserContext>,
15
+ );
@@ -1,5 +1,5 @@
1
- import {createAsyncComponent} from '@quilted/quilt/async';
1
+ import {AsyncComponent} from '@quilted/quilt/async';
2
2
 
3
- export const Start = createAsyncComponent(
3
+ export const Start = AsyncComponent.from(
4
4
  () => import('./start/Start/Start.tsx'),
5
5
  );
@@ -0,0 +1,46 @@
1
+ import {describe, it, expect} from 'vitest';
2
+ import {
3
+ CacheControl,
4
+ ContentSecurityPolicy,
5
+ SearchRobots,
6
+ Viewport,
7
+ } from '@quilted/quilt/server';
8
+
9
+ import {renderApp} from '~/tests/render.ts';
10
+
11
+ import {HTML} from './HTML.tsx';
12
+
13
+ describe('<HTML />', () => {
14
+ it('includes a responsive viewport tag', async () => {
15
+ const head = await renderApp(<HTML />);
16
+
17
+ expect(head).toContainReactComponent(Viewport, {
18
+ cover: true,
19
+ });
20
+ });
21
+
22
+ it('prevents search robots from indexing the application', async () => {
23
+ const head = await renderApp(<HTML />);
24
+
25
+ expect(head).toContainReactComponent(SearchRobots, {
26
+ index: false,
27
+ follow: false,
28
+ });
29
+ });
30
+
31
+ it('does not cache the response', async () => {
32
+ const headers = await renderApp(<HTML />);
33
+
34
+ expect(headers).toContainReactComponent(CacheControl, {
35
+ cache: false,
36
+ });
37
+ });
38
+
39
+ it('adds a content security policy with a strict default policy', async () => {
40
+ const headers = await renderApp(<HTML />);
41
+
42
+ expect(headers).toContainReactComponent(ContentSecurityPolicy, {
43
+ defaultSources: ["'self'"],
44
+ });
45
+ });
46
+ });
@@ -1,21 +1,34 @@
1
- import Env from '@quilted/quilt/env';
2
- import {useInitialUrl} from '@quilted/quilt/navigate';
1
+ import type {PropsWithChildren} from 'react';
2
+ import {Title, Favicon, useBrowserRequest} from '@quilted/quilt/browser';
3
3
  import {
4
4
  CacheControl,
5
5
  ResponseHeader,
6
6
  ContentSecurityPolicy,
7
7
  PermissionsPolicy,
8
+ SearchRobots,
8
9
  StrictTransportSecurity,
9
- } from '@quilted/quilt/http';
10
+ Viewport,
11
+ } from '@quilted/quilt/server';
10
12
 
11
- // This component sets details on the HTTP response for all HTML server-rendering
12
- // requests. If you need to customize any of these details based on conditions like
13
- // the active route, or some state about the user, you can move these components to
14
- // wherever in your application you can read that state.
13
+ // This component sets details of the HTML page. If you need to customize
14
+ // any of these details based on conditions like the active route, or some
15
+ // state about the user, you can move these components to wherever in your
16
+ // application you can read that state.
15
17
  //
16
- // @see https://github.com/lemonmade/quilt/blob/main/documentation/features/http.md
17
- export function Headers() {
18
- const isHttps = useInitialUrl().protocol === 'https:';
18
+ // @see https://github.com/lemonmade/quilt/blob/main/documentation/features/html.md
19
+ export function HTML({children}: PropsWithChildren) {
20
+ return (
21
+ <>
22
+ <Headers />
23
+ <Head />
24
+ {children}
25
+ </>
26
+ );
27
+ }
28
+
29
+ function Headers() {
30
+ const {url} = useBrowserRequest();
31
+ const isHttps = new URL(url).protocol === 'https:';
19
32
 
20
33
  return (
21
34
  <>
@@ -116,3 +129,30 @@ export function Headers() {
116
129
  </>
117
130
  );
118
131
  }
132
+
133
+ function Head() {
134
+ return (
135
+ <>
136
+ {/* Sets the default `<title>` for this application. */}
137
+ <Title>App</Title>
138
+
139
+ {/*
140
+ * Sets the default favicon used by the application. You can
141
+ * change this to a different emoji, make it `blank`, or pass
142
+ * a URL with the `source` prop.
143
+ */}
144
+ <Favicon emoji="🧶" />
145
+
146
+ {/* Adds a responsive-friendly `viewport` `<meta>` tag. */}
147
+ <Viewport cover />
148
+
149
+ {/*
150
+ * Disables all search indexing for this application. If you are
151
+ * building an unauthenticated app, you probably want to remove
152
+ * this component, or update it to control how your site is indexed
153
+ * by search engines.
154
+ */}
155
+ <SearchRobots index={false} follow={false} />
156
+ </>
157
+ );
158
+ }
@@ -1 +1 @@
1
- export {Head} from './html/Head.tsx';
1
+ export {HTML} from './html/HTML.tsx';
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "dependencies": {},
13
13
  "devDependencies": {
14
- "@quilted/quilt": "^0.6.0",
14
+ "@quilted/quilt": "workspace:^0.6.0",
15
15
  "@types/react": "^18.2.0",
16
16
  "@types/react-dom": "^18.2.0",
17
17
  "jsdom": "^24.0.0",
@@ -1,15 +1,12 @@
1
- import {useMemo, type PropsWithChildren} from 'react';
1
+ import {type PropsWithChildren} from 'react';
2
2
 
3
- import {HTML} from '@quilted/quilt/html';
4
3
  import {Routing, useRoutes} from '@quilted/quilt/navigate';
5
4
  import {Localization, useLocaleFromEnvironment} from '@quilted/quilt/localize';
6
- import {GraphQLContext, type GraphQLRun} from '@quilted/quilt/graphql';
5
+ import {GraphQLContext} from '@quilted/quilt/graphql';
7
6
 
8
7
  import {ReactQueryContext} from '@quilted/react-query';
9
- import {QueryClient} from '@tanstack/react-query';
10
8
 
11
- import {Head} from './foundation/html.ts';
12
- import {Headers} from './foundation/http.ts';
9
+ import {HTML} from './foundation/html.ts';
13
10
  import {Frame} from './foundation/frame.ts';
14
11
 
15
12
  import {Start} from './features/start.ts';
@@ -19,29 +16,21 @@ import {
19
16
  type AppContext as AppContextType,
20
17
  } from './shared/context.ts';
21
18
 
22
- export interface AppProps extends AppContextType {
23
- fetchGraphQL: GraphQLRun;
19
+ export interface AppProps {
20
+ context: AppContextType;
24
21
  }
25
22
 
26
23
  // The root component for your application. You will typically render any
27
24
  // app-wide context in this component.
28
- export function App(props: AppProps) {
29
- const locale = useLocaleFromEnvironment() ?? 'en';
30
-
25
+ export function App({context}: AppProps) {
31
26
  return (
32
- <HTML>
33
- <Localization locale={locale}>
34
- <Routing>
35
- <AppContext {...props}>
36
- <Headers />
37
- <Head />
38
- <Frame>
39
- <Routes />
40
- </Frame>
41
- </AppContext>
42
- </Routing>
43
- </Localization>
44
- </HTML>
27
+ <AppContext context={context}>
28
+ <HTML>
29
+ <Frame>
30
+ <Routes />
31
+ </Frame>
32
+ </HTML>
33
+ </AppContext>
45
34
  );
46
35
  }
47
36
 
@@ -56,24 +45,18 @@ function Routes() {
56
45
  }
57
46
 
58
47
  // This component renders any app-wide context.
59
- function AppContext({
60
- children,
61
- fetchGraphQL,
62
- ...context
63
- }: PropsWithChildren<AppProps>) {
64
- const {queryClient} = useMemo(() => {
65
- return {
66
- queryClient: new QueryClient(),
67
- };
68
- }, []);
48
+ function AppContext({children, context}: PropsWithChildren<AppProps>) {
49
+ const locale = useLocaleFromEnvironment() ?? 'en';
69
50
 
70
51
  return (
71
- <GraphQLContext fetch={fetchGraphQL}>
72
- <ReactQueryContext client={queryClient}>
73
- <AppContextReact.Provider value={context}>
74
- {children}
75
- </AppContextReact.Provider>
76
- </ReactQueryContext>
77
- </GraphQLContext>
52
+ <AppContextReact.Provider value={context}>
53
+ <GraphQLContext fetch={context.fetchGraphQL}>
54
+ <ReactQueryContext client={context.queryClient}>
55
+ <Localization locale={locale}>
56
+ <Routing>{children}</Routing>
57
+ </Localization>
58
+ </ReactQueryContext>
59
+ </GraphQLContext>
60
+ </AppContextReact.Provider>
78
61
  );
79
62
  }
@@ -1,13 +1,21 @@
1
1
  import '@quilted/quilt/globals';
2
2
 
3
3
  import {hydrateRoot} from 'react-dom/client';
4
+ import {QueryClient} from '@tanstack/react-query';
4
5
  import {createGraphQLFetch} from '@quilted/quilt/graphql';
6
+ import {Browser, BrowserContext} from '@quilted/quilt/browser';
5
7
 
6
8
  import {App} from './App.tsx';
7
9
 
8
10
  const element = document.querySelector('#app')!;
11
+ const browser = new Browser();
12
+
13
+ const queryClient = new QueryClient();
14
+ const fetchGraphQL = createGraphQLFetch({url: '/api/graphql'});
9
15
 
10
16
  hydrateRoot(
11
17
  element,
12
- <App fetchGraphQL={createGraphQLFetch({url: '/api/graphql'})} />,
18
+ <BrowserContext browser={browser}>
19
+ <App context={{fetchGraphQL, queryClient}} />
20
+ </BrowserContext>,
13
21
  );
@@ -1,5 +1,5 @@
1
- import {createAsyncComponent} from '@quilted/quilt/async';
1
+ import {AsyncComponent} from '@quilted/quilt/async';
2
2
 
3
- export const Start = createAsyncComponent(
3
+ export const Start = AsyncComponent.from(
4
4
  () => import('./start/Start/Start.tsx'),
5
5
  );
@@ -0,0 +1,46 @@
1
+ import {describe, it, expect} from 'vitest';
2
+ import {
3
+ CacheControl,
4
+ ContentSecurityPolicy,
5
+ SearchRobots,
6
+ Viewport,
7
+ } from '@quilted/quilt/server';
8
+
9
+ import {renderApp} from '~/tests/render.ts';
10
+
11
+ import {HTML} from './HTML.tsx';
12
+
13
+ describe('<HTML />', () => {
14
+ it('includes a responsive viewport tag', async () => {
15
+ const head = await renderApp(<HTML />);
16
+
17
+ expect(head).toContainReactComponent(Viewport, {
18
+ cover: true,
19
+ });
20
+ });
21
+
22
+ it('prevents search robots from indexing the application', async () => {
23
+ const head = await renderApp(<HTML />);
24
+
25
+ expect(head).toContainReactComponent(SearchRobots, {
26
+ index: false,
27
+ follow: false,
28
+ });
29
+ });
30
+
31
+ it('does not cache the response', async () => {
32
+ const headers = await renderApp(<HTML />);
33
+
34
+ expect(headers).toContainReactComponent(CacheControl, {
35
+ cache: false,
36
+ });
37
+ });
38
+
39
+ it('adds a content security policy with a strict default policy', async () => {
40
+ const headers = await renderApp(<HTML />);
41
+
42
+ expect(headers).toContainReactComponent(ContentSecurityPolicy, {
43
+ defaultSources: ["'self'"],
44
+ });
45
+ });
46
+ });
@@ -1,21 +1,34 @@
1
- import Env from '@quilted/quilt/env';
2
- import {useInitialUrl} from '@quilted/quilt/navigate';
1
+ import type {PropsWithChildren} from 'react';
2
+ import {Title, Favicon, useBrowserRequest} from '@quilted/quilt/browser';
3
3
  import {
4
4
  CacheControl,
5
5
  ResponseHeader,
6
6
  ContentSecurityPolicy,
7
7
  PermissionsPolicy,
8
+ SearchRobots,
8
9
  StrictTransportSecurity,
9
- } from '@quilted/quilt/http';
10
+ Viewport,
11
+ } from '@quilted/quilt/server';
10
12
 
11
- // This component sets details on the HTTP response for all HTML server-rendering
12
- // requests. If you need to customize any of these details based on conditions like
13
- // the active route, or some state about the user, you can move these components to
14
- // wherever in your application you can read that state.
13
+ // This component sets details of the HTML page. If you need to customize
14
+ // any of these details based on conditions like the active route, or some
15
+ // state about the user, you can move these components to wherever in your
16
+ // application you can read that state.
15
17
  //
16
- // @see https://github.com/lemonmade/quilt/blob/main/documentation/features/http.md
17
- export function Headers() {
18
- const isHttps = useInitialUrl().protocol === 'https:';
18
+ // @see https://github.com/lemonmade/quilt/blob/main/documentation/features/html.md
19
+ export function HTML({children}: PropsWithChildren) {
20
+ return (
21
+ <>
22
+ <Headers />
23
+ <Head />
24
+ {children}
25
+ </>
26
+ );
27
+ }
28
+
29
+ function Headers() {
30
+ const {url} = useBrowserRequest();
31
+ const isHttps = new URL(url).protocol === 'https:';
19
32
 
20
33
  return (
21
34
  <>
@@ -116,3 +129,30 @@ export function Headers() {
116
129
  </>
117
130
  );
118
131
  }
132
+
133
+ function Head() {
134
+ return (
135
+ <>
136
+ {/* Sets the default `<title>` for this application. */}
137
+ <Title>App</Title>
138
+
139
+ {/*
140
+ * Sets the default favicon used by the application. You can
141
+ * change this to a different emoji, make it `blank`, or pass
142
+ * a URL with the `source` prop.
143
+ */}
144
+ <Favicon emoji="🧶" />
145
+
146
+ {/* Adds a responsive-friendly `viewport` `<meta>` tag. */}
147
+ <Viewport cover />
148
+
149
+ {/*
150
+ * Disables all search indexing for this application. If you are
151
+ * building an unauthenticated app, you probably want to remove
152
+ * this component, or update it to control how your site is indexed
153
+ * by search engines.
154
+ */}
155
+ <SearchRobots index={false} follow={false} />
156
+ </>
157
+ );
158
+ }
@@ -1 +1 @@
1
- export {Head} from './html/Head.tsx';
1
+ export {HTML} from './html/HTML.tsx';
@@ -16,7 +16,7 @@
16
16
  "@tanstack/react-query": "^5.0.0",
17
17
  "@types/react": "^18.2.0",
18
18
  "@types/react-dom": "^18.2.0",
19
- "graphql": "^16.7.0",
19
+ "graphql": "^16.8.0",
20
20
  "jsdom": "^24.0.0",
21
21
  "preact": "^10.20.0",
22
22
  "react": "npm:@quilted/react@^18.2.0",
@@ -30,7 +30,7 @@ const Query = createQueryResolver({
30
30
 
31
31
  const schema = createGraphQLSchema(schemaSource, {Query, Person});
32
32
 
33
- export const performGraphQLOperation: GraphQLRun =
33
+ export const performGraphQLOperation: GraphQLRun<{}> =
34
34
  async function performGraphQLOperation(operation, options) {
35
35
  const result = await graphql({
36
36
  schema,
@@ -21,15 +21,21 @@ router.post('/api/graphql', async (request) => {
21
21
 
22
22
  // For all GET requests, render our React application.
23
23
  router.get(async (request) => {
24
- const [{App}, {performGraphQLOperation}, {renderToResponse}] =
24
+ const [{App}, {performGraphQLOperation}, {renderToResponse}, {QueryClient}] =
25
25
  await Promise.all([
26
26
  import('./App.tsx'),
27
27
  import('./server/graphql.ts'),
28
28
  import('@quilted/quilt/server'),
29
+ import('@tanstack/react-query'),
29
30
  ]);
30
31
 
31
32
  const response = await renderToResponse(
32
- <App fetchGraphQL={performGraphQLOperation} />,
33
+ <App
34
+ context={{
35
+ fetchGraphQL: performGraphQLOperation,
36
+ queryClient: new QueryClient(),
37
+ }}
38
+ />,
33
39
  {
34
40
  request,
35
41
  assets,
@@ -0,0 +1,11 @@
1
+ import type {GraphQLFetch} from '@quilted/quilt/graphql';
2
+ import type {QueryClient} from '@tanstack/react-query';
3
+
4
+ declare module '~/shared/context.ts' {
5
+ interface AppContext {
6
+ queryClient: QueryClient;
7
+ fetchGraphQL: GraphQLFetch<{}>;
8
+ }
9
+ }
10
+
11
+ export {};
@@ -24,7 +24,12 @@ export const renderApp = createRender<
24
24
  // authors on the `root.context` property. Context is used to share data between your
25
25
  // React tree and your test code, and is ideal for mocking out global context providers.
26
26
  context({router = new TestRouter(), graphql = new GraphQLController()}) {
27
- return {router, graphql, queryClient: new QueryClient()};
27
+ return {
28
+ router,
29
+ graphql,
30
+ fetchGraphQL: graphql.fetch,
31
+ queryClient: new QueryClient(),
32
+ };
28
33
  },
29
34
  // Render all of our app-wide context providers around each component under test.
30
35
  render(element, context, {locale = 'en'}) {
@@ -1,15 +1,11 @@
1
- import {useMemo, type PropsWithChildren} from 'react';
1
+ import {type PropsWithChildren} from 'react';
2
2
 
3
- import {HTML} from '@quilted/quilt/html';
4
- import {Routing, useRoutes, useInitialUrl} from '@quilted/quilt/navigate';
3
+ import {Routing, useRoutes} from '@quilted/quilt/navigate';
5
4
  import {Localization, useLocaleFromEnvironment} from '@quilted/quilt/localize';
6
5
 
7
- import {httpBatchLink} from '@trpc/client';
8
- import {QueryClient} from '@tanstack/react-query';
9
6
  import {ReactQueryContext} from '@quilted/react-query';
10
7
 
11
- import {Head} from './foundation/html.ts';
12
- import {Headers} from './foundation/http.ts';
8
+ import {HTML} from './foundation/html.ts';
13
9
  import {Frame} from './foundation/frame.ts';
14
10
 
15
11
  import {Start} from './features/start.ts';
@@ -20,27 +16,21 @@ import {
20
16
  type AppContext as AppContextType,
21
17
  } from './shared/context.ts';
22
18
 
23
- export interface AppProps extends AppContextType {}
19
+ export interface AppProps {
20
+ context: AppContextType;
21
+ }
24
22
 
25
23
  // The root component for your application. You will typically render any
26
24
  // app-wide context in this component.
27
- export function App(props: AppProps) {
28
- const locale = useLocaleFromEnvironment() ?? 'en';
29
-
25
+ export function App({context}: AppProps) {
30
26
  return (
31
- <HTML>
32
- <Localization locale={locale}>
33
- <Routing>
34
- <AppContext {...props}>
35
- <Headers />
36
- <Head />
37
- <Frame>
38
- <Routes />
39
- </Frame>
40
- </AppContext>
41
- </Routing>
42
- </Localization>
43
- </HTML>
27
+ <AppContext context={context}>
28
+ <HTML>
29
+ <Frame>
30
+ <Routes />
31
+ </Frame>
32
+ </HTML>
33
+ </AppContext>
44
34
  );
45
35
  }
46
36
 
@@ -55,29 +45,23 @@ function Routes() {
55
45
  }
56
46
 
57
47
  // This component renders any app-wide context.
58
- function AppContext({children, ...context}: PropsWithChildren<AppProps>) {
59
- const initialUrl = useInitialUrl();
60
-
61
- const {queryClient, trpcClient} = useMemo(() => {
62
- return {
63
- queryClient: new QueryClient(),
64
- trpcClient: trpc.createClient({
65
- links: [
66
- // We need to use an absolute URL so that queries will
67
- // work during server-side rendering
68
- httpBatchLink({url: new URL('/api', initialUrl).href}),
69
- ],
70
- }),
71
- };
72
- }, [initialUrl]);
48
+ function AppContext({children, context}: PropsWithChildren<AppProps>) {
49
+ const locale = useLocaleFromEnvironment() ?? 'en';
73
50
 
74
51
  return (
75
- <trpc.Provider client={trpcClient} queryClient={queryClient}>
76
- <ReactQueryContext client={queryClient}>
77
- <AppContextReact.Provider value={context}>
78
- {children}
79
- </AppContextReact.Provider>
80
- </ReactQueryContext>
81
- </trpc.Provider>
52
+ <AppContextReact.Provider value={context}>
53
+ <Localization locale={locale}>
54
+ <Routing>
55
+ <trpc.Provider
56
+ client={context.trpc}
57
+ queryClient={context.queryClient}
58
+ >
59
+ <ReactQueryContext client={context.queryClient}>
60
+ {children}
61
+ </ReactQueryContext>
62
+ </trpc.Provider>
63
+ </Routing>
64
+ </Localization>
65
+ </AppContextReact.Provider>
82
66
  );
83
67
  }
@@ -1,15 +1,24 @@
1
1
  import '@quilted/quilt/globals';
2
2
  import {hydrateRoot} from 'react-dom/client';
3
3
  import {httpBatchLink} from '@trpc/client';
4
+ import {QueryClient} from '@tanstack/react-query';
5
+ import {Browser, BrowserContext} from '@quilted/quilt/browser';
4
6
 
5
7
  import {trpc} from '~/shared/trpc.ts';
6
8
 
7
9
  import {App} from './App.tsx';
8
10
 
9
11
  const element = document.querySelector('#app')!;
12
+ const browser = new Browser();
10
13
 
14
+ const queryClient = new QueryClient();
11
15
  const trpcClient = trpc.createClient({
12
16
  links: [httpBatchLink({url: new URL('/api', window.location.href).href})],
13
17
  });
14
18
 
15
- hydrateRoot(element, <App trpc={trpcClient} />);
19
+ hydrateRoot(
20
+ element,
21
+ <BrowserContext browser={browser}>
22
+ <App context={{trpc: trpcClient, queryClient}} />
23
+ </BrowserContext>,
24
+ );
@@ -1,5 +1,5 @@
1
- import {createAsyncComponent} from '@quilted/quilt/async';
1
+ import {AsyncComponent} from '@quilted/quilt/async';
2
2
 
3
- export const Start = createAsyncComponent(
3
+ export const Start = AsyncComponent.from(
4
4
  () => import('./start/Start/Start.tsx'),
5
5
  );
@@ -0,0 +1,46 @@
1
+ import {describe, it, expect} from 'vitest';
2
+ import {
3
+ CacheControl,
4
+ ContentSecurityPolicy,
5
+ SearchRobots,
6
+ Viewport,
7
+ } from '@quilted/quilt/server';
8
+
9
+ import {renderApp} from '~/tests/render.ts';
10
+
11
+ import {HTML} from './HTML.tsx';
12
+
13
+ describe('<HTML />', () => {
14
+ it('includes a responsive viewport tag', async () => {
15
+ const head = await renderApp(<HTML />);
16
+
17
+ expect(head).toContainReactComponent(Viewport, {
18
+ cover: true,
19
+ });
20
+ });
21
+
22
+ it('prevents search robots from indexing the application', async () => {
23
+ const head = await renderApp(<HTML />);
24
+
25
+ expect(head).toContainReactComponent(SearchRobots, {
26
+ index: false,
27
+ follow: false,
28
+ });
29
+ });
30
+
31
+ it('does not cache the response', async () => {
32
+ const headers = await renderApp(<HTML />);
33
+
34
+ expect(headers).toContainReactComponent(CacheControl, {
35
+ cache: false,
36
+ });
37
+ });
38
+
39
+ it('adds a content security policy with a strict default policy', async () => {
40
+ const headers = await renderApp(<HTML />);
41
+
42
+ expect(headers).toContainReactComponent(ContentSecurityPolicy, {
43
+ defaultSources: ["'self'"],
44
+ });
45
+ });
46
+ });
@@ -1,21 +1,34 @@
1
- import Env from '@quilted/quilt/env';
2
- import {useInitialUrl} from '@quilted/quilt/navigate';
1
+ import type {PropsWithChildren} from 'react';
2
+ import {Title, Favicon, useBrowserRequest} from '@quilted/quilt/browser';
3
3
  import {
4
4
  CacheControl,
5
5
  ResponseHeader,
6
6
  ContentSecurityPolicy,
7
7
  PermissionsPolicy,
8
+ SearchRobots,
8
9
  StrictTransportSecurity,
9
- } from '@quilted/quilt/http';
10
+ Viewport,
11
+ } from '@quilted/quilt/server';
10
12
 
11
- // This component sets details on the HTTP response for all HTML server-rendering
12
- // requests. If you need to customize any of these details based on conditions like
13
- // the active route, or some state about the user, you can move these components to
14
- // wherever in your application you can read that state.
13
+ // This component sets details of the HTML page. If you need to customize
14
+ // any of these details based on conditions like the active route, or some
15
+ // state about the user, you can move these components to wherever in your
16
+ // application you can read that state.
15
17
  //
16
- // @see https://github.com/lemonmade/quilt/blob/main/documentation/features/http.md
17
- export function Headers() {
18
- const isHttps = useInitialUrl().protocol === 'https:';
18
+ // @see https://github.com/lemonmade/quilt/blob/main/documentation/features/html.md
19
+ export function HTML({children}: PropsWithChildren) {
20
+ return (
21
+ <>
22
+ <Headers />
23
+ <Head />
24
+ {children}
25
+ </>
26
+ );
27
+ }
28
+
29
+ function Headers() {
30
+ const {url} = useBrowserRequest();
31
+ const isHttps = new URL(url).protocol === 'https:';
19
32
 
20
33
  return (
21
34
  <>
@@ -116,3 +129,30 @@ export function Headers() {
116
129
  </>
117
130
  );
118
131
  }
132
+
133
+ function Head() {
134
+ return (
135
+ <>
136
+ {/* Sets the default `<title>` for this application. */}
137
+ <Title>App</Title>
138
+
139
+ {/*
140
+ * Sets the default favicon used by the application. You can
141
+ * change this to a different emoji, make it `blank`, or pass
142
+ * a URL with the `source` prop.
143
+ */}
144
+ <Favicon emoji="🧶" />
145
+
146
+ {/* Adds a responsive-friendly `viewport` `<meta>` tag. */}
147
+ <Viewport cover />
148
+
149
+ {/*
150
+ * Disables all search indexing for this application. If you are
151
+ * building an unauthenticated app, you probably want to remove
152
+ * this component, or update it to control how your site is indexed
153
+ * by search engines.
154
+ */}
155
+ <SearchRobots index={false} follow={false} />
156
+ </>
157
+ );
158
+ }
@@ -1 +1 @@
1
- export {Head} from './html/Head.tsx';
1
+ export {HTML} from './html/HTML.tsx';
@@ -25,13 +25,19 @@ router.any(
25
25
 
26
26
  // For all GET requests, render our React application.
27
27
  router.get(async (request) => {
28
- const [{App}, {renderToResponse}] = await Promise.all([
28
+ const [{App}, {renderToResponse}, {QueryClient}] = await Promise.all([
29
29
  import('./App.tsx'),
30
30
  import('@quilted/quilt/server'),
31
+ import('@tanstack/react-query'),
31
32
  ]);
32
33
 
33
34
  const response = await renderToResponse(
34
- <App trpc={createDirectClient(appRouter)} />,
35
+ <App
36
+ context={{
37
+ trpc: createDirectClient(appRouter),
38
+ queryClient: new QueryClient(),
39
+ }}
40
+ />,
35
41
  {
36
42
  request,
37
43
  assets,
@@ -1,10 +1,11 @@
1
- import {type TRPCClient} from '@trpc/client';
1
+ import type {TRPCClient} from '@trpc/client';
2
2
  import {createTRPCReact, type CreateTRPCReact} from '@trpc/react-query';
3
+ import type {QueryClient} from '@tanstack/react-query';
3
4
 
4
5
  // Get access to our app’s router type signature, which will
5
6
  // provide strong typing on the queries and mutations we can
6
7
  // perform.
7
- import {type AppRouter} from '../trpc.ts';
8
+ import type {AppRouter} from '../trpc.ts';
8
9
 
9
10
  export const trpc: CreateTRPCReact<AppRouter, unknown, null> =
10
11
  createTRPCReact<AppRouter>();
@@ -12,5 +13,6 @@ export const trpc: CreateTRPCReact<AppRouter, unknown, null> =
12
13
  declare module '~/shared/context.ts' {
13
14
  interface AppContext {
14
15
  trpc: TRPCClient<AppRouter>;
16
+ queryClient: QueryClient;
15
17
  }
16
18
  }
@@ -1,25 +0,0 @@
1
- import {describe, it, expect} from 'vitest';
2
- import {Viewport, SearchRobots} from '@quilted/quilt/html';
3
-
4
- import {renderApp} from '~/tests/render.ts';
5
-
6
- import {Head} from './Head.tsx';
7
-
8
- describe('<Head />', () => {
9
- it('includes a responsive viewport tag', async () => {
10
- const head = await renderApp(<Head />);
11
-
12
- expect(head).toContainReactComponent(Viewport, {
13
- cover: true,
14
- });
15
- });
16
-
17
- it('prevents search robots from indexing the application', async () => {
18
- const head = await renderApp(<Head />);
19
-
20
- expect(head).toContainReactComponent(SearchRobots, {
21
- index: false,
22
- follow: false,
23
- });
24
- });
25
- });
@@ -1,34 +0,0 @@
1
- import {Title, Viewport, Favicon, SearchRobots} from '@quilted/quilt/html';
2
-
3
- // This component sets details of the HTML page. If you need to customize
4
- // any of these details based on conditions like the active route, or some
5
- // state about the user, you can move these components to wherever in your
6
- // application you can read that state.
7
- //
8
- // @see https://github.com/lemonmade/quilt/blob/main/documentation/features/html.md
9
- export function Head() {
10
- return (
11
- <>
12
- {/* Sets the default `<title>` for this application. */}
13
- <Title>App</Title>
14
-
15
- {/*
16
- * Sets the default favicon used by the application. You can
17
- * change this to a different emoji, make it `blank`, or pass
18
- * a URL with the `source` prop.
19
- */}
20
- <Favicon emoji="🧶" />
21
-
22
- {/* Adds a responsive-friendly `viewport` `<meta>` tag. */}
23
- <Viewport cover />
24
-
25
- {/*
26
- * Disables all search indexing for this application. If you are
27
- * building an unauthenticated app, you probably want to remove
28
- * this component, or update it to control how your site is indexed
29
- * by search engines.
30
- */}
31
- <SearchRobots index={false} follow={false} />
32
- </>
33
- );
34
- }
@@ -1,24 +0,0 @@
1
- import {describe, it, expect} from 'vitest';
2
- import {CacheControl, ContentSecurityPolicy} from '@quilted/quilt/http';
3
-
4
- import {renderApp} from '~/tests/render.ts';
5
-
6
- import {Headers} from './Headers.tsx';
7
-
8
- describe('<Headers />', () => {
9
- it('does not cache the response', async () => {
10
- const headers = await renderApp(<Headers />);
11
-
12
- expect(headers).toContainReactComponent(CacheControl, {
13
- cache: false,
14
- });
15
- });
16
-
17
- it('adds a content security policy with a strict default policy', async () => {
18
- const headers = await renderApp(<Headers />);
19
-
20
- expect(headers).toContainReactComponent(ContentSecurityPolicy, {
21
- defaultSources: ["'self'"],
22
- });
23
- });
24
- });
@@ -1 +0,0 @@
1
- export {Headers} from './http/Headers.tsx';
@@ -1,25 +0,0 @@
1
- import {describe, it, expect} from 'vitest';
2
- import {Viewport, SearchRobots} from '@quilted/quilt/html';
3
-
4
- import {renderApp} from '~/tests/render.ts';
5
-
6
- import {Head} from './Head.tsx';
7
-
8
- describe('<Head />', () => {
9
- it('includes a responsive viewport tag', async () => {
10
- const head = await renderApp(<Head />);
11
-
12
- expect(head).toContainReactComponent(Viewport, {
13
- cover: true,
14
- });
15
- });
16
-
17
- it('prevents search robots from indexing the application', async () => {
18
- const head = await renderApp(<Head />);
19
-
20
- expect(head).toContainReactComponent(SearchRobots, {
21
- index: false,
22
- follow: false,
23
- });
24
- });
25
- });
@@ -1,34 +0,0 @@
1
- import {Title, Viewport, Favicon, SearchRobots} from '@quilted/quilt/html';
2
-
3
- // This component sets details of the HTML page. If you need to customize
4
- // any of these details based on conditions like the active route, or some
5
- // state about the user, you can move these components to wherever in your
6
- // application you can read that state.
7
- //
8
- // @see https://github.com/lemonmade/quilt/blob/main/documentation/features/html.md
9
- export function Head() {
10
- return (
11
- <>
12
- {/* Sets the default `<title>` for this application. */}
13
- <Title>App</Title>
14
-
15
- {/*
16
- * Sets the default favicon used by the application. You can
17
- * change this to a different emoji, make it `blank`, or pass
18
- * a URL with the `source` prop.
19
- */}
20
- <Favicon emoji="🧶" />
21
-
22
- {/* Adds a responsive-friendly `viewport` `<meta>` tag. */}
23
- <Viewport cover />
24
-
25
- {/*
26
- * Disables all search indexing for this application. If you are
27
- * building an unauthenticated app, you probably want to remove
28
- * this component, or update it to control how your site is indexed
29
- * by search engines.
30
- */}
31
- <SearchRobots index={false} follow={false} />
32
- </>
33
- );
34
- }
@@ -1,24 +0,0 @@
1
- import {describe, it, expect} from 'vitest';
2
- import {CacheControl, ContentSecurityPolicy} from '@quilted/quilt/http';
3
-
4
- import {renderApp} from '~/tests/render.ts';
5
-
6
- import {Headers} from './Headers.tsx';
7
-
8
- describe('<Headers />', () => {
9
- it('does not cache the response', async () => {
10
- const headers = await renderApp(<Headers />);
11
-
12
- expect(headers).toContainReactComponent(CacheControl, {
13
- cache: false,
14
- });
15
- });
16
-
17
- it('adds a content security policy with a strict default policy', async () => {
18
- const headers = await renderApp(<Headers />);
19
-
20
- expect(headers).toContainReactComponent(ContentSecurityPolicy, {
21
- defaultSources: ["'self'"],
22
- });
23
- });
24
- });
@@ -1 +0,0 @@
1
- export {Headers} from './http/Headers.tsx';
@@ -1,25 +0,0 @@
1
- import {describe, it, expect} from 'vitest';
2
- import {Viewport, SearchRobots} from '@quilted/quilt/html';
3
-
4
- import {renderApp} from '~/tests/render.ts';
5
-
6
- import {Head} from './Head.tsx';
7
-
8
- describe('<Head />', () => {
9
- it('includes a responsive viewport tag', async () => {
10
- const head = await renderApp(<Head />);
11
-
12
- expect(head).toContainReactComponent(Viewport, {
13
- cover: true,
14
- });
15
- });
16
-
17
- it('prevents search robots from indexing the application', async () => {
18
- const head = await renderApp(<Head />);
19
-
20
- expect(head).toContainReactComponent(SearchRobots, {
21
- index: false,
22
- follow: false,
23
- });
24
- });
25
- });
@@ -1,34 +0,0 @@
1
- import {Title, Viewport, Favicon, SearchRobots} from '@quilted/quilt/html';
2
-
3
- // This component sets details of the HTML page. If you need to customize
4
- // any of these details based on conditions like the active route, or some
5
- // state about the user, you can move these components to wherever in your
6
- // application you can read that state.
7
- //
8
- // @see https://github.com/lemonmade/quilt/blob/main/documentation/features/html.md
9
- export function Head() {
10
- return (
11
- <>
12
- {/* Sets the default `<title>` for this application. */}
13
- <Title>App</Title>
14
-
15
- {/*
16
- * Sets the default favicon used by the application. You can
17
- * change this to a different emoji, make it `blank`, or pass
18
- * a URL with the `source` prop.
19
- */}
20
- <Favicon emoji="🧶" />
21
-
22
- {/* Adds a responsive-friendly `viewport` `<meta>` tag. */}
23
- <Viewport cover />
24
-
25
- {/*
26
- * Disables all search indexing for this application. If you are
27
- * building an unauthenticated app, you probably want to remove
28
- * this component, or update it to control how your site is indexed
29
- * by search engines.
30
- */}
31
- <SearchRobots index={false} follow={false} />
32
- </>
33
- );
34
- }
@@ -1,24 +0,0 @@
1
- import {describe, it, expect} from 'vitest';
2
- import {CacheControl, ContentSecurityPolicy} from '@quilted/quilt/http';
3
-
4
- import {renderApp} from '~/tests/render.ts';
5
-
6
- import {Headers} from './Headers.tsx';
7
-
8
- describe('<Headers />', () => {
9
- it('does not cache the response', async () => {
10
- const headers = await renderApp(<Headers />);
11
-
12
- expect(headers).toContainReactComponent(CacheControl, {
13
- cache: false,
14
- });
15
- });
16
-
17
- it('adds a content security policy with a strict default policy', async () => {
18
- const headers = await renderApp(<Headers />);
19
-
20
- expect(headers).toContainReactComponent(ContentSecurityPolicy, {
21
- defaultSources: ["'self'"],
22
- });
23
- });
24
- });
@@ -1 +0,0 @@
1
- export {Headers} from './http/Headers.tsx';