create-houdini 1.2.9

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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +8 -0
  3. package/bin.js +355 -0
  4. package/package.json +36 -0
  5. package/templates/react/.meta.json +3 -0
  6. package/templates/react/houdini.config.js +13 -0
  7. package/templates/react/package.json +22 -0
  8. package/templates/react/src/+client.jsx +6 -0
  9. package/templates/react/src/+index.jsx +47 -0
  10. package/templates/react/src/routes/+page.jsx +14 -0
  11. package/templates/react/vite.config.ts +8 -0
  12. package/templates/react-typescript/.meta.json +3 -0
  13. package/templates/react-typescript/houdini.config.js +13 -0
  14. package/templates/react-typescript/package.json +25 -0
  15. package/templates/react-typescript/src/+client.tsx +6 -0
  16. package/templates/react-typescript/src/+index.tsx +47 -0
  17. package/templates/react-typescript/src/routes/+page.tsx +14 -0
  18. package/templates/react-typescript/tsconfig.json +3 -0
  19. package/templates/react-typescript/vite.config.ts +8 -0
  20. package/templates/sveltekit-demo/.env.local +1 -0
  21. package/templates/sveltekit-demo/.eslintignore +13 -0
  22. package/templates/sveltekit-demo/.graphqlrc.yaml +9 -0
  23. package/templates/sveltekit-demo/.meta.json +5 -0
  24. package/templates/sveltekit-demo/.prettierignore +15 -0
  25. package/templates/sveltekit-demo/.prettierrc +9 -0
  26. package/templates/sveltekit-demo/README.md +14 -0
  27. package/templates/sveltekit-demo/houdini.config.js +13 -0
  28. package/templates/sveltekit-demo/package.json +38 -0
  29. package/templates/sveltekit-demo/playwright.config.ts +12 -0
  30. package/templates/sveltekit-demo/schema.graphql +23 -0
  31. package/templates/sveltekit-demo/src/app.d.ts +12 -0
  32. package/templates/sveltekit-demo/src/app.html +12 -0
  33. package/templates/sveltekit-demo/src/client.ts +16 -0
  34. package/templates/sveltekit-demo/src/index.test.ts +7 -0
  35. package/templates/sveltekit-demo/src/lib/index.ts +1 -0
  36. package/templates/sveltekit-demo/src/routes/+layout.gql +3 -0
  37. package/templates/sveltekit-demo/src/routes/+layout.svelte +23 -0
  38. package/templates/sveltekit-demo/src/routes/+page.svelte +26 -0
  39. package/templates/sveltekit-demo/src/routes/links/+page.gql +8 -0
  40. package/templates/sveltekit-demo/src/routes/links/+page.svelte +17 -0
  41. package/templates/sveltekit-demo/src/routes/links/Link.svelte +39 -0
  42. package/templates/sveltekit-demo/src/routes/sponsors/+page.gql +6 -0
  43. package/templates/sveltekit-demo/src/routes/sponsors/+page.svelte +47 -0
  44. package/templates/sveltekit-demo/src/routes/sponsors/Sponsors.svelte +34 -0
  45. package/templates/sveltekit-demo/static/favicon.png +0 -0
  46. package/templates/sveltekit-demo/svelte.config.js +18 -0
  47. package/templates/sveltekit-demo/tests/test.ts +6 -0
  48. package/templates/sveltekit-demo/tsconfig.json +14 -0
  49. package/templates/sveltekit-demo/vite.config.ts +7 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Alec Aivazis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,8 @@
1
+ # create-houdini
2
+
3
+ A CLI for creating new [Houdini](https://houdinigraphql.com/) projects.
4
+
5
+ ```bash
6
+ npm create houdini@latest hello-houdini
7
+ ```
8
+
package/bin.js ADDED
@@ -0,0 +1,355 @@
1
+ #!/usr/bin/env node
2
+ import * as p from '@clack/prompts'
3
+ import * as graphql from 'graphql'
4
+ import { bold, cyan, gray, green, grey, italic, white } from 'kleur/colors'
5
+ import fs, { readFileSync, writeFileSync } from 'node:fs'
6
+ import path from 'node:path'
7
+ import { exit } from 'node:process'
8
+ import { fileURLToPath } from 'node:url'
9
+
10
+ // the first argument is the name of the project
11
+ let projectDir = process.argv[2]
12
+ let projectName = projectDir
13
+
14
+ // log the version of create-houdini that this was run with by looking at the packge's package.json
15
+ const { version } = JSON.parse(fs.readFileSync(new URL('package.json', import.meta.url), 'utf-8'))
16
+ console.log(`${grey(`create-houdini version ${version}`)}\n`)
17
+
18
+ // prepare options
19
+ const templatesDir = sourcePath(`./templates`)
20
+ const options = fs.readdirSync(templatesDir).map((templateDir) => {
21
+ // in .meta.json you can find:
22
+ /** @type {{label?: string, hint?: string, apiUrl?: string}} */
23
+ let data = {}
24
+ const metaPath = path.join(templatesDir, templateDir, '.meta.json')
25
+ if (fs.existsSync(metaPath)) {
26
+ data = JSON.parse(readFileSync(metaPath, 'utf-8'))
27
+ }
28
+ return { ...data, value: templateDir }
29
+ })
30
+
31
+ p.intro('🎩 Welcome to Houdini!')
32
+
33
+ // if we weren't given a directory, then we should ask
34
+ if (!projectDir) {
35
+ const dir = await p.text({
36
+ message: `Where should we create your project?`,
37
+ placeholder: ' (press Enter to use the current directory)',
38
+ })
39
+
40
+ if (p.isCancel(dir)) {
41
+ process.exit(1)
42
+ }
43
+
44
+ if (dir) {
45
+ projectDir = dir
46
+ projectName = 'hello-houdini'
47
+ } else {
48
+ projectDir = '.'
49
+ }
50
+ }
51
+
52
+ // if we were told to use the current directory then we need
53
+ // a more appropriate name for the project
54
+ if (projectDir === '.') {
55
+ projectName = 'hello-houdini'
56
+ }
57
+ let dirToCreate = true
58
+
59
+ // project location emtpy?
60
+ if (fs.existsSync(projectDir)) {
61
+ if (fs.readdirSync(projectDir).length > 0) {
62
+ const force = await p.confirm({
63
+ message:
64
+ 'Target directory is not empty. Continue anyway? This might overwrite existing files.',
65
+ initialValue: false,
66
+ })
67
+ dirToCreate = false
68
+
69
+ // bail if `force` is `false` or the user cancelled with Ctrl-C
70
+ if (force !== true) {
71
+ process.exit(1)
72
+ }
73
+ }
74
+ }
75
+
76
+ const template = await p.select({
77
+ message: 'Which template do you want to use?',
78
+ initialValue: 'react-typescript',
79
+ options,
80
+ })
81
+ if (p.isCancel(template)) {
82
+ process.exit(1)
83
+ }
84
+ const templateMeta = options.find((option) => option.value === template)
85
+ if (!templateMeta) {
86
+ // this will never happen, but it helps to types later
87
+ exit(1)
88
+ }
89
+ const templateDir = sourcePath(path.join(templatesDir, template))
90
+
91
+ let apiUrl = templateMeta.apiUrl ?? ''
92
+ let pullSchema_content = ''
93
+ if (apiUrl === '') {
94
+ const { apiUrl: apiUrlCli, pullSchema_content: pullSchema_content_cli } = await pullSchemaCli()
95
+ apiUrl = apiUrlCli
96
+ if (pullSchema_content_cli === null) {
97
+ pCancel('We could not pull the schema. Please try again.')
98
+ } else {
99
+ pullSchema_content = pullSchema_content_cli
100
+ }
101
+ } else {
102
+ const pullSchema_content_local = await pullSchema(apiUrl, {})
103
+ if (pullSchema_content_local === null) {
104
+ pCancel('We could not pull the schema. Please report this on Discord.')
105
+ } else {
106
+ pullSchema_content = pullSchema_content_local
107
+ }
108
+ }
109
+
110
+ // create the project directory if necessary
111
+ if (dirToCreate) {
112
+ fs.mkdirSync(projectDir)
113
+ }
114
+ writeFileSync(path.join(projectDir, 'schema.graphql'), pullSchema_content)
115
+
116
+ copy(
117
+ templateDir,
118
+ projectDir,
119
+ {
120
+ API_URL: apiUrl,
121
+ PROJECT_NAME: projectName,
122
+ HOUDINI_VERSION: version,
123
+ },
124
+ ['.meta.json']
125
+ )
126
+
127
+ // If anything goes wrong, we don't want to block the user
128
+ let sponsor_msg = ''
129
+ try {
130
+ const selected = await getSponsors()
131
+ sponsor_msg = `🙏 Special thanks to the ${bold(white(selected))} for supporting Houdini!`
132
+ } catch (error) {}
133
+
134
+ p.outro(`🎉 Everything is ready!
135
+
136
+ 👉 Next Steps
137
+ 0️⃣ Go to your project : ${green(`cd ${projectDir}`)}
138
+ 1️⃣ Install dependencies : ${green(`npm i`)} | ${gray(`pnpm i`)} | ${gray(`yarn`)}
139
+ 2️⃣ Start your application : ${green(`npm run dev`)} | ${gray(`pnpm dev`)} | ${gray(`yarn dev`)}`)
140
+
141
+ console.log(
142
+ gray(
143
+ italic(
144
+ `${bold('❔ More help')} ` +
145
+ `at ${cyan('https://houdinigraphql.com')} ` +
146
+ `(📄 Docs, ⭐ Github, 📣 Discord, ...) ` +
147
+ `${sponsor_msg ? `\n${sponsor_msg}` : ``}\n`
148
+ )
149
+ )
150
+ )
151
+
152
+ // Function to copy files recursively
153
+ function copy(
154
+ /** @type {string} */ sourceDir,
155
+ /** @type {string} */ destDir,
156
+ /** @type {Record<string, string>} */ transformMap,
157
+ /** @type {string[]} */ ignoreList
158
+ ) {
159
+ if (!fs.existsSync(destDir)) {
160
+ fs.mkdirSync(destDir)
161
+ }
162
+
163
+ const files = fs.readdirSync(sourceDir)
164
+ for (const file of files) {
165
+ const sourceFilePath = path.join(sourceDir, file)
166
+ const sourceRelative = path.relative(templateDir, sourceFilePath)
167
+ // skip the ignore list
168
+ if (!ignoreList.includes(sourceRelative)) {
169
+ const destFilePath = path.join(destDir, file)
170
+
171
+ const stats = fs.statSync(sourceFilePath)
172
+
173
+ // files need to be copied and potentially transformed
174
+ if (stats.isFile()) {
175
+ // read the source file
176
+ const source = fs.readFileSync(sourceFilePath)
177
+
178
+ // apply any transformations if necessary
179
+ const transformed = Object.entries(transformMap).reduce(
180
+ (prev, [pattern, value]) => {
181
+ return prev.replaceAll(pattern, value)
182
+ },
183
+ source.toString()
184
+ )
185
+
186
+ // write the result
187
+ fs.writeFileSync(destFilePath, transformed)
188
+ }
189
+ // if we run into a directory then we should keep going
190
+ else if (stats.isDirectory()) {
191
+ copy(sourceFilePath, destFilePath, transformMap, ignoreList)
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ function sourcePath(/** @type {string} */ path) {
198
+ return fileURLToPath(new URL(path, import.meta.url).href)
199
+ }
200
+
201
+ async function pullSchemaCli() {
202
+ let number_of_round = 0
203
+ let url_and_headers = ''
204
+ let apiUrl = ''
205
+ let pullSchema_content = null
206
+ while (pullSchema_content === null && number_of_round < 10) {
207
+ number_of_round++
208
+ const answer = await p.group(
209
+ {
210
+ url_and_headers: async () =>
211
+ p.text({
212
+ defaultValue: 'http://localhost:4000/graphql',
213
+ message: `What's the URL for your api? ${
214
+ number_of_round === 1 ? '' : `(attempt ${number_of_round}/10)`
215
+ }`,
216
+ placeholder: `http://localhost:4000/graphql ${
217
+ number_of_round === 1 ? '' : 'Authorization=Bearer MyToken'
218
+ }`,
219
+ // initialValue: url_and_headers,
220
+ validate: (value) => {
221
+ // If empty, let's assume the placeholder value
222
+ if (value === '') {
223
+ value = 'http://localhost:4000/graphql'
224
+ }
225
+
226
+ if (!value.startsWith('http')) {
227
+ return 'Please enter a valid URL'
228
+ }
229
+ },
230
+ }),
231
+ },
232
+ {
233
+ onCancel: () => pCancel(),
234
+ }
235
+ )
236
+
237
+ url_and_headers = answer.url_and_headers
238
+ const value_splited = url_and_headers.split(' ')
239
+ const apiUrl = value_splited[0]
240
+
241
+ const local_headers =
242
+ value_splited.length > 1
243
+ ? // remove the url and app all the headers
244
+ extractHeadersStr(value_splited.slice(1).join(' '))
245
+ : {}
246
+
247
+ pullSchema_content = await pullSchema(apiUrl, local_headers)
248
+
249
+ if (pullSchema_content === null) {
250
+ const msg = `If you need to pass headers, add them after the URL (eg: '${`${apiUrl} Authorization=Bearer MyToken`}')`
251
+ p.log.error(msg)
252
+ }
253
+ }
254
+
255
+ // if we are here... it means that we have tried x times to pull the schema and it failed
256
+ if (pullSchema_content === null) {
257
+ pCancel("We couldn't pull the schema. Please check your URL/headers and try again.")
258
+ }
259
+
260
+ return { apiUrl, pullSchema_content }
261
+ }
262
+
263
+ async function pullSchema(
264
+ /** @type {string} */ url,
265
+ /** @type {Record<string, string>} */ headers
266
+ ) {
267
+ let content = ''
268
+ const spinnerPullSchema = p.spinner()
269
+ spinnerPullSchema.start('Pulling your schema...')
270
+ let fileData = null
271
+ try {
272
+ // send the request
273
+ const resp = await fetch(url, {
274
+ method: 'POST',
275
+ body: JSON.stringify({
276
+ query: graphql.getIntrospectionQuery(),
277
+ }),
278
+ headers: { 'Content-Type': 'application/json', ...headers },
279
+ })
280
+ content = await resp.text()
281
+
282
+ const jsonSchema = JSON.parse(content).data
283
+ const schema = graphql.buildClientSchema(jsonSchema)
284
+ fileData = graphql.printSchema(graphql.lexicographicSortSchema(schema))
285
+ } catch (/** @type {any} */ e) {
286
+ if (content) {
287
+ console.warn(
288
+ `⚠️ Couldn't pull your schema.
289
+ ${' Reponse:'} ${content}
290
+ ${' Error :'} ${e.message}`
291
+ )
292
+ } else {
293
+ console.warn(`⚠️ Couldn't pull your schema: ${e.message}`)
294
+ }
295
+ }
296
+ spinnerPullSchema.stop(fileData ? 'Schema pulled 🪄' : 'Failed to pull schema!')
297
+ return fileData
298
+ }
299
+
300
+ function extractHeadersStr(/** @type {string} */ str) {
301
+ const regex = /(\w+)=("[^"]*"|[^ ]*)/g
302
+ const /** @type {Record<string, string>} */ obj = {}
303
+
304
+ let match
305
+ while ((match = regex.exec(str ?? '')) !== null) {
306
+ obj[match[1]] = match[2].replaceAll('"', '')
307
+ }
308
+
309
+ return obj
310
+ }
311
+
312
+ function pCancel(cancelText = 'Operation cancelled.') {
313
+ p.cancel(cancelText)
314
+ process.exit(1)
315
+ }
316
+
317
+ async function getSponsors() {
318
+ const res = await fetch(
319
+ 'https://raw.githubusercontent.com/HoudiniGraphql/sponsors/main/generated/sponsors.json'
320
+ )
321
+ const /**@type {any[]} */ jsonData = await res.json()
322
+
323
+ /** @returns {[number, string]} */
324
+ function getTier(/**@type {number}*/ value) {
325
+ if (value >= 1500) {
326
+ return [10, 'Wizard']
327
+ }
328
+ if (value >= 500) {
329
+ return [5, 'Mage']
330
+ }
331
+ if (value >= 25) {
332
+ return [2, "Magician's Apprentice"]
333
+ }
334
+ if (value >= 10) {
335
+ return [1, 'Supportive Muggle']
336
+ }
337
+ // don't display the past sponsors
338
+ return [0, 'Past Sponsors']
339
+ }
340
+
341
+ const list = jsonData.flatMap(
342
+ (/** @type {{sponsor: {name: string}, monthlyDollars: number}} */ c) => {
343
+ const [coef, title] = getTier(c.monthlyDollars)
344
+ const names = []
345
+ for (let i = 0; i < coef; i++) {
346
+ names.push(`${title}, ${c.sponsor.name}`)
347
+ }
348
+ return names
349
+ }
350
+ )
351
+
352
+ const selected_to_display = list[Math.floor(Math.random() * list.length)]
353
+
354
+ return selected_to_display
355
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "create-houdini",
3
+ "version": "1.2.9",
4
+ "description": "A CLI for creating new Houdini projects",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/HoudiniGraphQL/houdini",
8
+ "directory": "packages/create-houdini"
9
+ },
10
+ "license": "MIT",
11
+ "homepage": "https://houdinigraphql.com/",
12
+ "bin": "./bin.js",
13
+ "dependencies": {
14
+ "@clack/prompts": "^0.6.3",
15
+ "kleur": "^4.1.5",
16
+ "graphql": "16.8.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^18.7.23",
20
+ "prettier": "^2.8.3"
21
+ },
22
+ "files": [
23
+ "templates",
24
+ "bin.js"
25
+ ],
26
+ "types": "types/index.d.ts",
27
+ "type": "module",
28
+ "scripts": {
29
+ "dev": "node bin.js",
30
+ "build:": "cd ../../ && ((pnpm run build && cd -) || (cd - && exit 1))",
31
+ "build:dev": "pnpm build: && pnpm dev",
32
+ "check": "tsc",
33
+ "lint": "prettier --check . --config ../../.prettierrc --ignore-path ../../.gitignore --ignore-path .gitignore --plugin prettier-plugin-svelte --plugin-search-dir=.",
34
+ "format": "pnpm lint --write"
35
+ }
36
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "label": "React"
3
+ }
@@ -0,0 +1,13 @@
1
+ /// <references types="houdini-react">
2
+ /// <references types="houdini-router">
3
+ /** @type {import('houdini').ConfigFile} */
4
+ const config = {
5
+ watchSchema: {
6
+ url: 'API_URL',
7
+ },
8
+ plugins: {
9
+ 'houdini-react': {},
10
+ },
11
+ }
12
+
13
+ export default config
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "PROJECT_NAME",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "vite build",
8
+ "dev": "vite",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "houdini": "^HOUDINI_VERSION",
13
+ "houdini-react": "^HOUDINI_VERSION",
14
+ "react": "^18.2.0",
15
+ "react-dom": "^18.2.0",
16
+ "react-streaming": "^0.3.14"
17
+ },
18
+ "devDependencies": {
19
+ "@vitejs/plugin-react": "^3.1.0",
20
+ "vite": "^4.1.0"
21
+ }
22
+ }
@@ -0,0 +1,6 @@
1
+ import { HoudiniClient } from '$houdini'
2
+
3
+ // Export the Houdini client
4
+ export default new HoudiniClient({
5
+ url: 'API_URL',
6
+ })
@@ -0,0 +1,47 @@
1
+ import React from "react";
2
+
3
+ export default function App({ children }) {
4
+ return (
5
+ <html>
6
+ <head>
7
+ <meta charSet="utf-8" />
8
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
9
+ <link
10
+ rel="icon"
11
+ type="image/png"
12
+ href="https://houdinigraphql.com/images/logo.png"
13
+ />
14
+ <link
15
+ rel="stylesheet"
16
+ href="https://cdn.jsdelivr.net/npm/water.css@2/out/dark.css"
17
+ />
18
+ <title>Houdini • React</title>
19
+ </head>
20
+ <body>
21
+ <ErrorBoundary>{children}</ErrorBoundary>
22
+ </body>
23
+ </html>
24
+ );
25
+ }
26
+
27
+ class ErrorBoundary extends React.Component {
28
+ constructor(props) {
29
+ super(props);
30
+ this.state = { hasError: false };
31
+ }
32
+
33
+ static getDerivedStateFromError(error) {
34
+ return { hasError: true };
35
+ }
36
+
37
+ componentDidCatch(error, info) {
38
+ console.error("ErrorBoundary caught an error:", error, info);
39
+ }
40
+
41
+ render() {
42
+ if (this.state.hasError) {
43
+ return <h1>Something went wrong.</h1>;
44
+ }
45
+ return this.props.children;
46
+ }
47
+ }
@@ -0,0 +1,14 @@
1
+ // import type { PageProps } from "./$types";
2
+
3
+ // export default function ({}: PageProps) {
4
+ export default function () {
5
+ return (
6
+ <div>
7
+ <h2>Home</h2>
8
+
9
+ <p>
10
+ Visit <a href="https://houdinigraphql.com/">Houdini Graphql</a> to read the doc.
11
+ </p>
12
+ </div>
13
+ )
14
+ }
@@ -0,0 +1,8 @@
1
+ import react from '@vitejs/plugin-react'
2
+ import houdini from 'houdini/vite'
3
+ import { defineConfig } from 'vite'
4
+
5
+ // https://vitejs.dev/config/
6
+ export default defineConfig({
7
+ plugins: [houdini(), react({ fastRefresh: false })],
8
+ })
@@ -0,0 +1,3 @@
1
+ {
2
+ "label": "React w/ TypeScript"
3
+ }
@@ -0,0 +1,13 @@
1
+ /// <references types="houdini-react">
2
+ /// <references types="houdini-router">
3
+ /** @type {import('houdini').ConfigFile} */
4
+ const config = {
5
+ watchSchema: {
6
+ url: 'API_URL',
7
+ },
8
+ plugins: {
9
+ 'houdini-react': {},
10
+ },
11
+ }
12
+
13
+ export default config
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "PROJECT_NAME",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "vite build",
8
+ "dev": "vite",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "houdini": "^HOUDINI_VERSION",
13
+ "houdini-react": "^HOUDINI_VERSION",
14
+ "react": "^18.2.0",
15
+ "react-dom": "^18.2.0",
16
+ "react-streaming": "^0.3.14"
17
+ },
18
+ "devDependencies": {
19
+ "@types/react": "^18.0.27",
20
+ "@types/react-dom": "^18.0.10",
21
+ "@vitejs/plugin-react": "^3.1.0",
22
+ "typescript": "^4.9.3",
23
+ "vite": "^4.1.0"
24
+ }
25
+ }
@@ -0,0 +1,6 @@
1
+ import { HoudiniClient } from '$houdini'
2
+
3
+ // Export the Houdini client
4
+ export default new HoudiniClient({
5
+ url: 'API_URL',
6
+ })
@@ -0,0 +1,47 @@
1
+ import React from "react";
2
+
3
+ export default function App({ children }) {
4
+ return (
5
+ <html>
6
+ <head>
7
+ <meta charSet="utf-8" />
8
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
9
+ <link
10
+ rel="icon"
11
+ type="image/png"
12
+ href="https://houdinigraphql.com/images/logo.png"
13
+ />
14
+ <link
15
+ rel="stylesheet"
16
+ href="https://cdn.jsdelivr.net/npm/water.css@2/out/dark.css"
17
+ />
18
+ <title>Houdini • React</title>
19
+ </head>
20
+ <body>
21
+ <ErrorBoundary>{children}</ErrorBoundary>
22
+ </body>
23
+ </html>
24
+ );
25
+ }
26
+
27
+ class ErrorBoundary extends React.Component {
28
+ constructor(props) {
29
+ super(props);
30
+ this.state = { hasError: false };
31
+ }
32
+
33
+ static getDerivedStateFromError(error) {
34
+ return { hasError: true };
35
+ }
36
+
37
+ componentDidCatch(error, info) {
38
+ console.error("ErrorBoundary caught an error:", error, info);
39
+ }
40
+
41
+ render() {
42
+ if (this.state.hasError) {
43
+ return <h1>Something went wrong.</h1>;
44
+ }
45
+ return this.props.children;
46
+ }
47
+ }
@@ -0,0 +1,14 @@
1
+ // import type { PageProps } from "./$types";
2
+
3
+ // export default function ({}: PageProps) {
4
+ export default function () {
5
+ return (
6
+ <div>
7
+ <h2>Home</h2>
8
+
9
+ <p>
10
+ Visit <a href="https://houdinigraphql.com/">Houdini Graphql</a> to read the doc.
11
+ </p>
12
+ </div>
13
+ )
14
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "./$houdini/tsconfig.json"
3
+ }
@@ -0,0 +1,8 @@
1
+ import react from '@vitejs/plugin-react'
2
+ import houdini from 'houdini/vite'
3
+ import { defineConfig } from 'vite'
4
+
5
+ // https://vitejs.dev/config/
6
+ export default defineConfig({
7
+ plugins: [houdini(), react({ fastRefresh: false })],
8
+ })
@@ -0,0 +1 @@
1
+ PUBLIC_GRAPHQL_ENDPOINT = 'API_URL'
@@ -0,0 +1,13 @@
1
+ .DS_Store
2
+ node_modules
3
+ /build
4
+ /.svelte-kit
5
+ /package
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+
10
+ # Ignore files for PNPM, NPM and YARN
11
+ pnpm-lock.yaml
12
+ package-lock.json
13
+ yarn.lock
@@ -0,0 +1,9 @@
1
+ projects:
2
+ default:
3
+ schema:
4
+ - ./schema.graphql
5
+ - ./$houdini/graphql/schema.graphql
6
+ documents:
7
+ - '**/*.gql'
8
+ - '**/*.svelte'
9
+ - ./$houdini/graphql/documents.gql
@@ -0,0 +1,5 @@
1
+ {
2
+ "label": "SvelteKit - Demo",
3
+ "hint": "Light demo with a few pages, layout, queries. TypeScript",
4
+ "apiUrl": "https://houdinigraphql.com/graphql"
5
+ }
@@ -0,0 +1,15 @@
1
+ .DS_Store
2
+ node_modules
3
+ /build
4
+ /.svelte-kit
5
+ /package
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+
10
+ # Ignore files for PNPM, NPM and YARN
11
+ pnpm-lock.yaml
12
+ package-lock.json
13
+ yarn.lock
14
+
15
+ $houdini
@@ -0,0 +1,9 @@
1
+ {
2
+ "useTabs": true,
3
+ "singleQuote": true,
4
+ "trailingComma": "none",
5
+ "printWidth": 100,
6
+ "plugins": ["prettier-plugin-svelte"],
7
+ "pluginSearchDirs": ["."],
8
+ "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
9
+ }
@@ -0,0 +1,14 @@
1
+ # create-houdini
2
+
3
+ Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
4
+
5
+ ```bash
6
+ npm run dev
7
+
8
+ # or start the server and open the app in a new browser tab
9
+ npm run dev -- --open
10
+ ```
11
+
12
+ You will lean the rest on the Home page of the demo app.
13
+
14
+ More info on: https://github.com/HoudiniGraphql/houdini
@@ -0,0 +1,13 @@
1
+ /// <references types="houdini-svelte">
2
+
3
+ /** @type {import('houdini').ConfigFile} */
4
+ const config = {
5
+ watchSchema: {
6
+ url: 'env:PUBLIC_GRAPHQL_ENDPOINT'
7
+ },
8
+ plugins: {
9
+ 'houdini-svelte': {}
10
+ }
11
+ };
12
+
13
+ export default config;
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "PROJECT_NAME",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "vite dev",
7
+ "build": "vite build",
8
+ "preview": "vite preview",
9
+ "test": "npm run test:integration && npm run test:unit",
10
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
11
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
12
+ "lint": "prettier --plugin-search-dir . --check . && eslint .",
13
+ "format": "prettier --plugin-search-dir . --write .",
14
+ "test:integration": "playwright test",
15
+ "test:unit": "vitest"
16
+ },
17
+ "devDependencies": {
18
+ "@playwright/test": "^1.28.1",
19
+ "@sveltejs/adapter-auto": "^2.0.0",
20
+ "@sveltejs/kit": "^1.20.4",
21
+ "@typescript-eslint/eslint-plugin": "^5.45.0",
22
+ "@typescript-eslint/parser": "^5.45.0",
23
+ "eslint": "^8.28.0",
24
+ "eslint-config-prettier": "^8.5.0",
25
+ "eslint-plugin-svelte": "^2.30.0",
26
+ "houdini": "^HOUDINI_VERSION",
27
+ "houdini-svelte": "^HOUDINI_VERSION",
28
+ "prettier": "^2.8.0",
29
+ "prettier-plugin-svelte": "^2.10.1",
30
+ "svelte": "^4.0.5",
31
+ "svelte-check": "^3.4.3",
32
+ "tslib": "^2.4.1",
33
+ "typescript": "^5.0.0",
34
+ "vite": "^4.4.2",
35
+ "vitest": "^0.32.2"
36
+ },
37
+ "type": "module"
38
+ }
@@ -0,0 +1,12 @@
1
+ import type { PlaywrightTestConfig } from '@playwright/test';
2
+
3
+ const config: PlaywrightTestConfig = {
4
+ webServer: {
5
+ command: 'npm run build && npm run preview',
6
+ port: 4173
7
+ },
8
+ testDir: 'tests',
9
+ testMatch: /(.+\.)?(test|spec)\.[jt]s/
10
+ };
11
+
12
+ export default config;
@@ -0,0 +1,23 @@
1
+ type Link {
2
+ name: String
3
+ url: String
4
+ }
5
+
6
+ type Mutation {
7
+ hello(name: String!): String!
8
+ }
9
+
10
+ type Query {
11
+ giveMeAnError: String
12
+ links(delai: Int): [Link!]!
13
+ sponsors: [Sponsor!]!
14
+ welcome: String!
15
+ }
16
+
17
+ type Sponsor {
18
+ avatarUrl: String!
19
+ login: String!
20
+ name: String!
21
+ tiersTitle: String!
22
+ websiteUrl: String
23
+ }
@@ -0,0 +1,12 @@
1
+ // See https://kit.svelte.dev/docs/types#app
2
+ // for information about these interfaces
3
+ declare global {
4
+ namespace App {
5
+ // interface Error {}
6
+ // interface Locals {}
7
+ // interface PageData {}
8
+ // interface Platform {}
9
+ }
10
+ }
11
+
12
+ export {};
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="icon" href="%sveltekit.assets%/favicon.png" />
6
+ <meta name="viewport" content="width=device-width" />
7
+ %sveltekit.head%
8
+ </head>
9
+ <body data-sveltekit-preload-data="hover">
10
+ <div style="display: contents">%sveltekit.body%</div>
11
+ </body>
12
+ </html>
@@ -0,0 +1,16 @@
1
+ import { PUBLIC_GRAPHQL_ENDPOINT } from '$env/static/public';
2
+ import { HoudiniClient } from '$houdini';
3
+
4
+ export default new HoudiniClient({
5
+ url: PUBLIC_GRAPHQL_ENDPOINT
6
+
7
+ // uncomment this to configure the network call (for things like authentication)
8
+ // for more information, please visit here: https://www.houdinigraphql.com/guides/authentication
9
+ // fetchParams({ session }) {
10
+ // return {
11
+ // headers: {
12
+ // Authentication: `Bearer ${session.token}`,
13
+ // }
14
+ // }
15
+ // }
16
+ });
@@ -0,0 +1,7 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ describe('sum test', () => {
4
+ it('adds 1 + 2 to equal 3', () => {
5
+ expect(1 + 2).toBe(3);
6
+ });
7
+ });
@@ -0,0 +1 @@
1
+ // place files you want to import through the `$lib` alias in this folder.
@@ -0,0 +1,3 @@
1
+ query Layout {
2
+ welcome
3
+ }
@@ -0,0 +1,23 @@
1
+ <script lang="ts">
2
+ import type { PageData } from './$houdini';
3
+
4
+ export let data: PageData;
5
+
6
+ $: ({ Layout } = data);
7
+ </script>
8
+
9
+ <svelte:head>
10
+ <title>Houdini • demo</title>
11
+ <link rel="icon" type="image/png" href="https://houdinigraphql.com/images/logo.png" />
12
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/dark.css" />
13
+ </svelte:head>
14
+
15
+ <h1>{$Layout.data?.welcome}</h1>
16
+
17
+ <a href="/">Home</a> |
18
+ <a href="/links">Links</a> |
19
+ <a href="/sponsors">Sponsors</a>
20
+
21
+ <hr />
22
+
23
+ <slot />
@@ -0,0 +1,26 @@
1
+ <p>First of all, thank you for using Houdini 🎉!</p>
2
+ <p>
3
+ You will find here a very small app to show you a few featues like
4
+ <span class="code">+page.gql</span>, <span class="code">+layout.gql</span>,
5
+ <span class="code">fragment</span>, <span class="code">data masking</span>,
6
+ <span class="code">@loading</span>
7
+ </p>
8
+ <p>To reset this demo and make it yours:</p>
9
+ <ol>
10
+ <li>Remove all files in <span class="code">routes</span> folder.</li>
11
+ <li>
12
+ Update <span class="code">.env.local</span> so that
13
+ <span class="code">PUBLIC_GRAPHQL_ENDPOINT</span> targets your endpoint.
14
+ </li>
15
+ <li>Restart the app.</li>
16
+ </ol>
17
+
18
+ <p><i>//Enjoy 🥳🥳🥳</i></p>
19
+
20
+ <style>
21
+ .code {
22
+ background-color: black;
23
+ padding: 0.25rem;
24
+ margin: 0.15rem;
25
+ }
26
+ </style>
@@ -0,0 +1,8 @@
1
+ query PageHome {
2
+ links(
3
+ # The delai here is just to simulate a slow response from the server
4
+ delai: 3000
5
+ ) @loading(count: 5) {
6
+ ...LinkInfo
7
+ }
8
+ }
@@ -0,0 +1,17 @@
1
+ <script lang="ts">
2
+ import type { PageData } from './$houdini';
3
+ import Link from './Link.svelte';
4
+
5
+ export let data: PageData;
6
+
7
+ $: ({ PageHome } = data);
8
+ </script>
9
+
10
+ <h2>Links</h2>
11
+ <ul>
12
+ {#each $PageHome.data?.links ?? [] as link}
13
+ <li>
14
+ <Link {link} />
15
+ </li>
16
+ {/each}
17
+ </ul>
@@ -0,0 +1,39 @@
1
+ <script lang="ts">
2
+ import { PendingValue, fragment, graphql } from '$houdini';
3
+ import type { LinkInfo } from '$houdini';
4
+
5
+ export let link: LinkInfo;
6
+ $: data = fragment(
7
+ link,
8
+ graphql(`
9
+ fragment LinkInfo on Link {
10
+ name @loading
11
+ url
12
+ }
13
+ `)
14
+ );
15
+ </script>
16
+
17
+ {#if $data.name === PendingValue}
18
+ <div class="skeleton" />
19
+ {:else}
20
+ <a href={$data.url} target="_blank">{$data.name}</a>
21
+ {/if}
22
+
23
+ <style>
24
+ .skeleton {
25
+ animation: skeleton-loading 0.5s linear infinite alternate;
26
+ width: 10rem;
27
+ height: 1rem;
28
+ border-radius: 0.25rem;
29
+ }
30
+
31
+ @keyframes skeleton-loading {
32
+ 0% {
33
+ background-color: hsl(200, 20%, 80%);
34
+ }
35
+ 100% {
36
+ background-color: hsl(200, 20%, 95%);
37
+ }
38
+ }
39
+ </style>
@@ -0,0 +1,6 @@
1
+ query PageSponsors {
2
+ sponsors {
3
+ ...SponsorInfo
4
+ tiersTitle
5
+ }
6
+ }
@@ -0,0 +1,47 @@
1
+ <script lang="ts">
2
+ import type { SponsorInfo } from '$houdini';
3
+ import type { PageData } from './$houdini';
4
+ import Sponsors from './Sponsors.svelte';
5
+
6
+ export let data: PageData;
7
+
8
+ $: ({ PageSponsors } = data);
9
+
10
+ $: grouped =
11
+ $PageSponsors.data?.sponsors.reduce((acc: Record<string, SponsorInfo[]>, sponsor) => {
12
+ acc[sponsor.tiersTitle] = acc[sponsor.tiersTitle] || [];
13
+ acc[sponsor.tiersTitle].push(sponsor);
14
+ return acc;
15
+ }, {}) ?? {};
16
+ </script>
17
+
18
+ <h2>Sponsors</h2>
19
+ <center>
20
+ {#if $PageSponsors.fetching}
21
+ Loading...
22
+ {:else if $PageSponsors.errors}
23
+ {#each $PageSponsors.errors as error}
24
+ {error.message}
25
+ {/each}
26
+ {:else}
27
+ {#each Object.entries(grouped) as [key, sponsors], i}
28
+ {@const size = 0.8 + (Object.entries(grouped).length - i) / 5}
29
+ <h3 style="font-size: {size}rem;">{key}</h3>
30
+ <div class="list">
31
+ {#each sponsors as sponsor}
32
+ <Sponsors {sponsor} {size} />
33
+ {/each}
34
+ </div>
35
+ {/each}
36
+ {/if}
37
+ </center>
38
+
39
+ <style>
40
+ .list {
41
+ display: flex;
42
+ flex-wrap: wrap;
43
+ justify-content: space-evenly;
44
+ gap: 1rem;
45
+ margin-bottom: 3rem;
46
+ }
47
+ </style>
@@ -0,0 +1,34 @@
1
+ <script lang="ts">
2
+ import { fragment, graphql } from '$houdini';
3
+ import type { SponsorInfo } from '$houdini';
4
+
5
+ export let sponsor: SponsorInfo;
6
+ export let size: number = 1;
7
+ $: data = fragment(
8
+ sponsor,
9
+ graphql(`
10
+ fragment SponsorInfo on Sponsor {
11
+ login
12
+ name
13
+ avatarUrl
14
+ }
15
+ `)
16
+ );
17
+ </script>
18
+
19
+ <a href="https://github.com/{$data.login}" target="_blank" style="font-size: {size}rem;">
20
+ <img src={$data.avatarUrl} alt={$data.name} width={size * 50} />
21
+ <span>{$data.name}</span>
22
+ </a>
23
+
24
+ <style>
25
+ img {
26
+ border-radius: 30%;
27
+ }
28
+
29
+ a {
30
+ display: flex;
31
+ flex-direction: column;
32
+ align-items: center;
33
+ }
34
+ </style>
@@ -0,0 +1,18 @@
1
+ import adapter from '@sveltejs/adapter-auto';
2
+ import { vitePreprocess } from '@sveltejs/kit/vite';
3
+
4
+ /** @type {import('@sveltejs/kit').Config} */
5
+ const config = {
6
+ // Consult https://kit.svelte.dev/docs/integrations#preprocessors
7
+ // for more information about preprocessors
8
+ preprocess: vitePreprocess(),
9
+
10
+ kit: {
11
+ adapter: adapter(),
12
+ alias: {
13
+ $houdini: './$houdini'
14
+ }
15
+ }
16
+ };
17
+
18
+ export default config;
@@ -0,0 +1,6 @@
1
+ import { expect, test } from '@playwright/test';
2
+
3
+ test('index page has expected h1', async ({ page }) => {
4
+ await page.goto('/');
5
+ await expect(page.getByRole('heading', { name: 'Welcome to SvelteKit' })).toBeVisible();
6
+ });
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "./.svelte-kit/tsconfig.json",
3
+ "compilerOptions": {
4
+ "allowJs": true,
5
+ "checkJs": true,
6
+ "esModuleInterop": true,
7
+ "forceConsistentCasingInFileNames": true,
8
+ "resolveJsonModule": true,
9
+ "skipLibCheck": true,
10
+ "sourceMap": true,
11
+ "strict": true,
12
+ "rootDirs": [".", "./.svelte-kit/types", "./$houdini/types"]
13
+ }
14
+ }
@@ -0,0 +1,7 @@
1
+ import { sveltekit } from '@sveltejs/kit/vite';
2
+ import houdini from 'houdini/vite';
3
+ import { defineConfig } from 'vite';
4
+
5
+ export default defineConfig({
6
+ plugins: [houdini(), sveltekit()]
7
+ });