diff --git a/.changeset/spicy-masks-care.md b/.changeset/spicy-masks-care.md new file mode 100644 index 0000000000..c30005affe --- /dev/null +++ b/.changeset/spicy-masks-care.md @@ -0,0 +1,8 @@ +--- +'houdini-adapter-static': minor +'houdini-svelte': minor +'houdini-react': minor +'houdini': minor +--- + +Added support for configuring Houdini's output directory diff --git a/.gitignore b/.gitignore index 42a51391ed..907df3f4ec 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ vite.config.*.timestamp-* .vercel .netlify + +mise.toml diff --git a/.npmrc b/.npmrc index 3bfab73a91..25788ed553 100644 --- a/.npmrc +++ b/.npmrc @@ -1,3 +1,3 @@ auto-install-peers=true strict-peer-dependencies=false - +engine-strict=true diff --git a/e2e/kit/.gitignore b/e2e/kit/.gitignore index 351aa7eaa9..51976829df 100644 --- a/e2e/kit/.gitignore +++ b/e2e/kit/.gitignore @@ -7,6 +7,6 @@ node_modules .env.* !.env.example -$houdini +.houdini playwright-report -test-results \ No newline at end of file +test-results diff --git a/e2e/kit/houdini.config.js b/e2e/kit/houdini.config.js index af21538756..6c6011513f 100644 --- a/e2e/kit/houdini.config.js +++ b/e2e/kit/houdini.config.js @@ -6,6 +6,7 @@ const config = { schemaPath: '../_api/*.graphql', defaultPartial: true, acceptImperativeInstability: true, + runtimeDir: '.houdini', // logLevel: 'Full', scalars: { DateTime: { diff --git a/e2e/kit/svelte.config.js b/e2e/kit/svelte.config.js index 02313b2731..92563a00ce 100644 --- a/e2e/kit/svelte.config.js +++ b/e2e/kit/svelte.config.js @@ -12,8 +12,7 @@ const config = { adapter: adapter(), alias: { - $houdini: path.resolve('./$houdini'), - $lib: path.resolve('./src/lib') + $houdini: '.houdini/' } } }; diff --git a/e2e/kit/tsconfig.json b/e2e/kit/tsconfig.json index 8b34ec566a..c447c1b454 100644 --- a/e2e/kit/tsconfig.json +++ b/e2e/kit/tsconfig.json @@ -9,7 +9,7 @@ "skipLibCheck": true, "sourceMap": true, "strict": true, - "rootDirs": [".", "./.svelte-kit/types", "./$houdini/types"], + "rootDirs": [".", "./.svelte-kit/types", "./.houdini/types"], "noImplicitAny": true } } diff --git a/e2e/kit/vite.config.js b/e2e/kit/vite.config.js index 18f2bf89b1..cb705a298b 100644 --- a/e2e/kit/vite.config.js +++ b/e2e/kit/vite.config.js @@ -14,10 +14,10 @@ const config = { libReporter([ { name: 'houdini runtime core', - includes: ['$houdini/runtime'], + includes: ['.houdini/runtime'], excludes: [ - '$houdini/index.js', - '$houdini/plugins/index.js', + '.houdini/index.js', + '.houdini/plugins/index.js', 'houdini.config.js', 'graphql-ws', 'svelte' @@ -25,11 +25,11 @@ const config = { }, { name: 'houdini runtime svelte', - includes: ['$houdini/plugins/houdini-svelte/runtime', 'src/client.ts'], + includes: ['.houdini/plugins/houdini-svelte/runtime', 'src/client.ts'], excludes: [ - '$houdini/runtime', - '$houdini/index.js', - '$houdini/plugins/index.js', + '.houdini/runtime', + '.houdini/index.js', + '.houdini/plugins/index.js', 'graphql-ws', 'vite/preload-helper', 'svelte' @@ -37,7 +37,7 @@ const config = { }, { name: 'houdini full e2e', - includes: ['$houdini', 'src/client.ts', 'houdini.config.js'], + includes: ['.houdini', 'src/client.ts', 'houdini.config.js'], excludes: ['graphql-ws', 'vite/preload-helper', 'svelte'] } ]) diff --git a/package.json b/package.json index d3edd48132..171995cbd6 100755 --- a/package.json +++ b/package.json @@ -58,7 +58,13 @@ "*.js": "prettier -w ", "*.json": "prettier -w " }, - "resolutions": { - "graphql": "15.5.0" + "pnpm": { + "overrides": { + "graphql": "15.5.0" + } + }, + "engines": { + "node": "^20", + "pnpm": "^8" } } diff --git a/packages/adapter-static/src/index.ts b/packages/adapter-static/src/index.ts index d9fe805312..babbffc27e 100644 --- a/packages/adapter-static/src/index.ts +++ b/packages/adapter-static/src/index.ts @@ -7,22 +7,22 @@ import ReactDOM from 'react-dom/server' // in order to prepare the app as a single-page app, we have 2 create 2 additional files: // - an index.js that imports the application and calls React.render. This file needs to be built by vite so it's passed with the includePaths option for an adapter // - an index.html containing the static shell that wraps the application. -const adapter: Adapter = async ({ outDir }) => { +const adapter: Adapter = async ({ outDir, config: { runtimeDir } }) => { // the first thing we need to do is pull out the rendered html file into the root of the outDir await fs.copyFile( - path.join(outDir, 'assets', '$houdini', 'temp', 'spa-shell', 'index.html'), + path.join(outDir, 'assets', runtimeDir, 'temp', 'spa-shell', 'index.html'), path.join(outDir, 'index.html') ) try { - await fs.rmdir(path.join(outDir, 'assets', '$houdini')) + await fs.rmdir(path.join(outDir, 'assets', runtimeDir)) } catch {} } // make sure we include the app entry point in the bundle -adapter.includePaths = { - shell: '$houdini/temp/spa-shell/index.html', -} +adapter.includePaths = ({ config: { runtimeDir } }) => ({ + shell: path.join(runtimeDir, 'temp', 'spa-shell', 'index.html'), +}) // we dont want any server artifacts to be generated adapter.disableServer = true diff --git a/packages/houdini-react/src/plugin/codegen/typeRoot.ts b/packages/houdini-react/src/plugin/codegen/typeRoot.ts index 7e78714ab8..f40b7d20a6 100644 --- a/packages/houdini-react/src/plugin/codegen/typeRoot.ts +++ b/packages/houdini-react/src/plugin/codegen/typeRoot.ts @@ -55,7 +55,7 @@ export async function generate_type_root({ const all_queries = (page?.query_options ?? []).concat(layout?.query_options ?? []) - // compute the path prefix to bring us to the root of the $houdini directory + // compute the path prefix to bring us to the root of the houdini runtimeDir const relative = path.relative(target_dir, config.rootDir) // build up the type definitions diff --git a/packages/houdini-react/src/plugin/vite.tsx b/packages/houdini-react/src/plugin/vite.tsx index 3d041cefa0..ecd15da1de 100644 --- a/packages/houdini-react/src/plugin/vite.tsx +++ b/packages/houdini-react/src/plugin/vite.tsx @@ -1,15 +1,15 @@ import { - PluginHooks, fs, isSecondaryBuild, load_manifest, path, + PluginHooks, type ProjectManifest, - type RouterManifest, routerConventions, + type RouterManifest, } from 'houdini' import React from 'react' -import { build, ConfigEnv, type BuildOptions, type Connect } from 'vite' +import { build, type BuildOptions, ConfigEnv, type Connect } from 'vite' import { manifest, setManifest } from '.' @@ -72,8 +72,15 @@ export default { 'entries/adapter': routerConventions.adapter_config_path(config), } - if (env.command === 'build' && config.adapter && config.adapter?.includePaths) { - Object.assign(conf.build!.rollupOptions!.input, config.adapter?.includePaths) + if (env.command === 'build' && config.adapter && config.adapter.includePaths) { + if (typeof config.adapter?.includePaths === 'function') { + Object.assign( + conf.build!.rollupOptions!.input, + config.adapter.includePaths({ config }) + ) + } else { + Object.assign(conf.build!.rollupOptions!.input, config.adapter.includePaths) + } } // every page in the manifest is a new entry point for vite diff --git a/packages/houdini-svelte/src/plugin/codegen/components/index.ts b/packages/houdini-svelte/src/plugin/codegen/components/index.ts index 39a4580283..25f206eae0 100644 --- a/packages/houdini-svelte/src/plugin/codegen/components/index.ts +++ b/packages/houdini-svelte/src/plugin/codegen/components/index.ts @@ -11,7 +11,7 @@ export default async function componentTypesGenerator( // we can just filter out the ones that don't apply:t // - in kit, exclude the route directory // - group the files by directory - // - generate ./$houdini in the typeroot directory at the correct spot + // - generate runtimeDir in the typeroot directory at the correct spot // there could be many queries in a given component so we can't just think about filepaths const queries: Record = {} diff --git a/packages/houdini-svelte/src/plugin/fsPatch.ts b/packages/houdini-svelte/src/plugin/fsPatch.ts index 1c0c14c37a..5481ff9c0e 100644 --- a/packages/houdini-svelte/src/plugin/fsPatch.ts +++ b/packages/houdini-svelte/src/plugin/fsPatch.ts @@ -280,7 +280,9 @@ function is_root_route(filepath: PathLike): boolean { filepath.endsWith(path.join('src', 'routes')) && // ignore the src/routes that exists in the type roots !filepath.includes('.svelte-kit') && - !filepath.includes('$houdini') + // ! Hey! This second value always needs to correspond to the default value for the runtimeDir + // if you're changing this here, please also update it in `/packages/houdini/src/lib/config.ts` + !filepath.includes(_config.runtimeDir ?? '$houdini') ) } diff --git a/packages/houdini/src/cmd/init.ts b/packages/houdini/src/cmd/init.ts index a596b55437..3cd0388d49 100644 --- a/packages/houdini/src/cmd/init.ts +++ b/packages/houdini/src/cmd/init.ts @@ -318,6 +318,8 @@ async function houdiniConfig( } } + config.runtimeDir = '.houdini' + // if it's different for defaults, write it down if (schemaPath !== './schema.graphql') { config.schemaPath = schemaPath @@ -431,7 +433,7 @@ const config = { kit: { adapter: adapter(), alias: { - $houdini: './$houdini', + $houdini: '.houdini/' } } }; @@ -446,7 +448,7 @@ const config = { kit: { adapter: adapter(), alias: { - $houdini: './$houdini', + $houdini: '.houdini/' } } }; @@ -465,8 +467,8 @@ async function gitIgnore(targetPath: string) { const filepath = path.join(targetPath, '.gitignore') const existing = (await fs.readFile(filepath)) || '' - if (!existing.includes('\n$houdini\n')) { - await fs.writeFile(filepath, existing + '\n$houdini\n') + if (!existing.includes('\n.houdini\n')) { + await fs.writeFile(filepath, existing + '\n.houdini\n') } } @@ -478,11 +480,11 @@ async function graphqlRC(targetPath: string) { default: schema: - ./schema.graphql - - ./$houdini/graphql/schema.graphql + - ./.houdini/graphql/schema.graphql documents: - '**/*.gql' - '**/*.svelte' - - ./$houdini/graphql/documents.gql + - ./.houdini/graphql/documents.gql ` await fs.writeFile(target, content) @@ -507,7 +509,7 @@ export default defineConfig({ resolve: { alias: { - $houdini: path.resolve('$houdini'), + $houdini: '.houdini/', }, }, }) @@ -553,9 +555,9 @@ async function tjsConfig(targetPath: string, frameworkInfo: HoudiniFrameworkInfo // new rootDirs (will overwrite the one in "extends": "./.svelte-kit/tsconfig.json") if (frameworkInfo.framework === 'svelte') { - tjsConfig.compilerOptions.rootDirs = ['.', './$houdini/types'] + tjsConfig.compilerOptions.rootDirs = ['.', './.houdini/types'] } else if (frameworkInfo.framework === 'kit') { - tjsConfig.compilerOptions.rootDirs = ['.', './.svelte-kit/types', './$houdini/types'] + tjsConfig.compilerOptions.rootDirs = ['.', './.svelte-kit/types', './.houdini/types'] } // In kit, no need to add manually the path. Why? Because: @@ -565,8 +567,8 @@ async function tjsConfig(targetPath: string, frameworkInfo: HoudiniFrameworkInfo if (frameworkInfo.framework === 'svelte') { tjsConfig.compilerOptions.paths = { ...tjsConfig.compilerOptions.paths, - $houdini: ['./$houdini'], - '$houdini/*': ['./$houdini/*'], + $houdini: ['./.houdini/'], + '$houdini/*': ['./.houdini/*'], } } diff --git a/packages/houdini/src/lib/config.ts b/packages/houdini/src/lib/config.ts index ec678f73c3..861047d19f 100644 --- a/packages/houdini/src/lib/config.ts +++ b/packages/houdini/src/lib/config.ts @@ -36,8 +36,9 @@ export class Config { localSchema: boolean projectRoot: string schema: graphql.GraphQLSchema + runtimeDir?: string schemaPath?: string - persistedQueriesPath: string = './$houdini/persisted_queries.json' + persistedQueriesPath: string exclude: string[] scalars?: ConfigFile['scalars'] module: 'commonjs' | 'esm' = 'esm' @@ -101,6 +102,9 @@ export class Config { let { schema, schemaPath = './schema.graphql', + + // Hey! If you change this default, please also update it in `/packages/houdini-svelte/src/plugin/fsPatch.ts` + runtimeDir = '$houdini', exclude = [], module = 'esm', scalars, @@ -145,6 +149,7 @@ export class Config { this.projectRoot = path.dirname( projectDir ? path.join(process.cwd(), projectDir) : filepath ) + this.runtimeDir = runtimeDir this.scalars = scalars this.cacheBufferSize = cacheBufferSize this.defaultCachePolicy = defaultCachePolicy @@ -159,13 +164,11 @@ export class Config { this.schemaPollInterval = watchSchema?.interval === undefined ? 2000 : watchSchema.interval this.schemaPollTimeout = watchSchema?.timeout ?? 30000 this.schemaPollHeaders = watchSchema?.headers ?? {} - this.rootDir = path.join(this.projectRoot, '$houdini') + this.rootDir = path.join(this.projectRoot, this.runtimeDir) + this.persistedQueriesPath = + persistedQueriesPath ?? path.join(this.rootDir, 'persisted_queries.json') this.#fragmentVariableMaps = {} - if (persistedQueriesPath) { - this.persistedQueriesPath = persistedQueriesPath - } - // hold onto the key config if (defaultKeys) { this.defaultKeys = defaultKeys @@ -389,7 +392,7 @@ export class Config { return path.join(this.rootDir, 'runtime') } - // Default to => $houdini/graphql + // Default to => /graphql get definitionsDirectory() { return this.definitionsFolder ? path.join(this.projectRoot, this.definitionsFolder) diff --git a/packages/houdini/src/lib/router/types.ts b/packages/houdini/src/lib/router/types.ts index 2133a93b96..c21b96fe43 100644 --- a/packages/houdini/src/lib/router/types.ts +++ b/packages/houdini/src/lib/router/types.ts @@ -10,7 +10,7 @@ export type Adapter = ((args: { manifest: ProjectManifest adapterPath: string }) => void | Promise) & { - includePaths?: Record + includePaths?: Record | ((args: { config: Config }) => Record) disableServer?: boolean pre?: (args: { config: Config diff --git a/packages/houdini/src/runtime/lib/config.ts b/packages/houdini/src/runtime/lib/config.ts index 122c1fbaf3..18be4dba9e 100644 --- a/packages/houdini/src/runtime/lib/config.ts +++ b/packages/houdini/src/runtime/lib/config.ts @@ -195,7 +195,7 @@ export type ConfigFile = { watchSchema?: WatchSchemaConfig /** - * Specifies the the persisted queries path and file. (default: `./$houdini/persisted_queries.json`) + * Specifies the the persisted queries path and file. (default: `/persisted_queries.json`) */ persistedQueriesPath?: string @@ -210,6 +210,12 @@ export type ConfigFile = { */ projectDir?: string + /** + * The relative path from your project directory pointing to your output directory. + * @default `$houdini` + */ + runtimeDir?: string + /** * For now, the cache's imperative API is considered unstable. In order to suppress the warning, * you must enable this flag. diff --git a/site/src/routes/api/config/+page.svx b/site/src/routes/api/config/+page.svx index 8c4899c179..cdcca8974a 100644 --- a/site/src/routes/api/config/+page.svx +++ b/site/src/routes/api/config/+page.svx @@ -35,9 +35,9 @@ By default, your config file can contain the following values: - `exclude` (optional): a pattern (or list of patterns) that filters out files that match the include pattern - `schemaPath` (optional, default: `"./schema.graphql"`): the path to the static representation of your schema, can be a glob pointing to multiple files - `watchSchema` (optional, an object): configure the development server to poll a remote url for changes in the schema. When a change is detected, the dev server will automatically regenerate your runtime. For more information see [Schema Polling](#schema-polling). -- `persistedQueriesPath` (optional, default: `./$houdini/persisted_queries.json`): Configure the path of the persisted queries file. +- `persistedQueriesPath` (optional, default: `/persisted_queries.json`): Configure the path of the persisted queries file. - `module` (optional, default: `"esm"`): One of `"esm"` or `"commonjs"`. Used to tell the artifact generator what kind of modules to create. -- `definitionsPath` (optional, default: `"$houdini/graphql"`): a path that the generator will use to write `schema.graphql` and `documents.gql` files containing all of the internal fragment and directive definitions used in the project. +- `definitionsPath` (optional, default: `"/graphql"`): a path that the generator will use to write `schema.graphql` and `documents.gql` files containing all of the internal fragment and directive definitions used in the project. - `scalars` (optional): An object describing custom scalars for your project (see below). - `cacheBufferSize` (optional, default: `10`): The number of queries that must occur before a value is removed from the cache. For more information, see the [Caching Guide](/guides/caching-data). - `defaultCachePolicy` (optional, default: `"CacheOrNetwork"`): The default cache policy to use for queries. For a list of the policies or other information see the [Caching Guide](/guides/caching-data). @@ -74,6 +74,7 @@ Here is a summary of the possible configuration values: - `client` (optional, default: `"./src/client"`): a relative path (from houdini.config.js) to a file that exports your houdini client as its default. - `defaultRouteBlocking` (optional, default: `false`): Specifies the default blocking behavior for client-side navigation. For more information, please visit [this section of the docs](https://houdinigraphql.com/api/query#loading-states). - `projectDir` (optional, default: `process.cwd()`): an absolute path pointing to your SvelteKit project (useful for monorepos) +- `runtimeDir` (optional, default: `'$houdini`): The name of the directory used to output the generated Houdini runtime, relative to `projectDir`. - `pageQueryFilename` (optional, default: `"+page.gql"`): The name of the file used to define page queries. - `layoutQueryFilename` (optional, default: `"+layout.gql"`): The name of the file used to define layout queries. - `quietQueryErrors` (optional, default: `false`): With this enabled, errors in your query will not be thrown as exceptions. You will have to handle error state in your route components or by hand in your load (or the onError hook) diff --git a/site/src/routes/guides/contributing/+page.svx b/site/src/routes/guides/contributing/+page.svx index 7b4b887e3e..bdd17fc955 100644 --- a/site/src/routes/guides/contributing/+page.svx +++ b/site/src/routes/guides/contributing/+page.svx @@ -25,6 +25,14 @@ should be able to visit `localhost:5173` in a web browser and see the end-to-end route in this application to work against. Don't worry about where it "belongs" - we'll make sure it goes in the right place when your PR is open. +Make sure you're using NodeJS and PNPM versions compatible with the `engines` config in the repo's `package.json`, otherwise you may experience inconsistent behavior between local, CI and deploy environments. Some good options for multiple NodeJS versions are: + +- [nvm](https://github.com/nvm-sh/nvm) +- [nix](https://nixos.org/) +- [asdf](https://asdf-vm.com/) +- [mise-en-place](https://mise.jdx.dev/) +- [pkgx](https://github.com/pkgxdev/pkgx) + ## General Introduction At a high level, houdini is broken up into a few parts. The core houdini project is located at [packages/houdini](https://github.com/HoudiniGraphQL/houdini/tree/main/packages/houdini) which provides the core artifact generation pipeline, cache runtime, vite plugin, and a collection of utilities for building extensions to the system. diff --git a/tsconfig.json b/tsconfig.json index d0de0a6ed0..26b126a941 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,20 @@ { - "exclude": ["node_modules", "packages/*/build/**"], - "include": ["packages/**/*.ts", "packages/**/*.tsx"], + "exclude": [ + "node_modules", + "packages/*/build/**" + ], + "include": [ + "packages/**/*.ts", + "packages/**/*.tsx" + ], "compilerOptions": { "moduleResolution": "node", "module": "es2020", - "lib": ["ES2021", "DOM", "ES2021.String"], + "lib": [ + "ES2021", + "DOM", + "ES2021.String" + ], "target": "ESNext", "isolatedModules": true, "resolveJsonModule": true, @@ -20,8 +30,12 @@ "emitDeclarationOnly": true, "baseUrl": "./packages/houdini", "paths": { - "$houdini": ["../houdini/src"], - "$houdini/*": ["../houdini/src/*"] + "$houdini": [ + "../houdini/src" + ], + "$houdini/*": [ + "../houdini/src/*" + ] }, "jsx": "react-jsx" }