diff --git a/.changeset/many-hounds-punch.md b/.changeset/many-hounds-punch.md new file mode 100644 index 000000000..b591d6ae6 --- /dev/null +++ b/.changeset/many-hounds-punch.md @@ -0,0 +1,5 @@ +--- +'houdini': patch +--- + +Plugin runtimes are now generated before documents are collected diff --git a/packages/houdini-react/package.json b/packages/houdini-react/package.json index 542906f7d..cc1918b8c 100644 --- a/packages/houdini-react/package.json +++ b/packages/houdini-react/package.json @@ -74,4 +74,4 @@ }, "main": "./build/plugin-cjs/index.js", "types": "./build/plugin/index.d.ts" -} +} \ No newline at end of file diff --git a/packages/houdini-svelte/package.json b/packages/houdini-svelte/package.json index 8d6f15185..998dba006 100644 --- a/packages/houdini-svelte/package.json +++ b/packages/houdini-svelte/package.json @@ -72,4 +72,4 @@ }, "main": "./build/plugin-cjs/index.js", "types": "./build/plugin/index.d.ts" -} +} \ No newline at end of file diff --git a/packages/houdini/package.json b/packages/houdini/package.json index a57c6b0d1..65785d460 100644 --- a/packages/houdini/package.json +++ b/packages/houdini/package.json @@ -110,4 +110,4 @@ "bin": "./build/cmd-esm/index.js", "main": "./build/lib-cjs/index.js", "types": "./build/lib/index.d.ts" -} +} \ No newline at end of file diff --git a/packages/houdini/src/codegen/generators/runtime/index.ts b/packages/houdini/src/codegen/generators/runtime/index.ts index 83f556515..fa146c312 100644 --- a/packages/houdini/src/codegen/generators/runtime/index.ts +++ b/packages/houdini/src/codegen/generators/runtime/index.ts @@ -7,18 +7,11 @@ import { generatePluginIndex } from './pluginIndex' import { injectConfig } from './runtimeConfig' export default async function runtimeGenerator(config: Config, docs: Document[]) { - const importStatement = - config.module === 'commonjs' - ? importDefaultFrom - : (where: string, as: string) => `import ${as} from '${where}'` - - const exportStatement = - config.module === 'commonjs' ? exportDefault : (as: string) => `export default ${as}` - - const exportStar = - config.module === 'commonjs' - ? exportStarFrom - : (where: string) => `export * from '${where}'` + const { + importStatement, + exportDefaultStatement: exportStatement, + exportStarStatement: exportStar, + } = moduleStatments(config) // copy the appropriate runtime first so we can generate files over it await Promise.all([ @@ -47,83 +40,105 @@ ${exportStatement('config')} content ) => injectPlugins({ config, content, importStatement, exportStatement }), }), - ...config.plugins - .filter((plugin) => plugin.includeRuntime) - .map((plugin) => - generatePluginRuntime({ - config, - docs, - plugin, - importStatement, - exportDefaultStatement: exportStatement, - exportStarStatement: exportStar, - }) - ), + transformPluginRuntimes({ config, docs }), generatePluginIndex({ config, exportStatement: exportStar }), ]) await generateGraphqlReturnTypes(config, docs) } -async function generatePluginRuntime({ - config, - docs, - plugin, - importStatement, - exportDefaultStatement, - exportStarStatement, -}: { - config: Config - docs: Document[] - plugin: Config['plugins'][number] - importStatement: (where: string, as: string) => string - exportDefaultStatement: (val: string) => string - exportStarStatement: (val: string) => string -}) { - if (houdini_mode.is_testing || !plugin.includeRuntime) { - return - } +export async function generatePluginRuntimes({ config }: { config: Config }) { + await Promise.all( + config.plugins + .filter((plugin) => plugin.includeRuntime) + .map(async (plugin) => { + if (houdini_mode.is_testing || !plugin.includeRuntime) { + return + } + + // a plugin has told us to include a runtime then the path is relative to the plugin file + const runtime_path = path.join( + path.dirname(plugin.filepath), + typeof plugin.includeRuntime === 'string' + ? plugin.includeRuntime + : plugin.includeRuntime[config.module] + ) + + try { + await fs.stat(runtime_path) + } catch { + throw new HoudiniError({ + message: 'Cannot find runtime to generate for ' + plugin.name, + description: 'Maybe it was bundled?', + }) + } - // a plugin has told us to include a runtime then the path is relative to the plugin file - const runtime_path = path.join( - path.dirname(plugin.filepath), - typeof plugin.includeRuntime === 'string' - ? plugin.includeRuntime - : plugin.includeRuntime[config.module] + // copy the runtime + const pluginDir = config.pluginRuntimeDirectory(plugin.name) + + await fs.mkdirp(pluginDir) + await fs.recursiveCopy(runtime_path, pluginDir) + }) ) +} - try { - await fs.stat(runtime_path) - } catch { - throw new HoudiniError({ - message: 'Cannot find runtime to generate for ' + plugin.name, - description: 'Maybe it was bundled?', - }) - } +async function transformPluginRuntimes({ config, docs }: { config: Config; docs: Document[] }) { + const { importStatement, exportDefaultStatement, exportStarStatement } = moduleStatments(config) - // copy the runtime - const pluginDir = config.pluginRuntimeDirectory(plugin.name) - let transformMap = plugin.transformRuntime ?? {} - if (transformMap && typeof transformMap === 'function') { - transformMap = transformMap(docs, { config }) - } + await Promise.all( + config.plugins + .filter((plugin) => plugin.includeRuntime) + .map(async (plugin) => { + // the transform map holds a map of files to transform functions + let transformMap = plugin.transformRuntime ?? {} + if (transformMap && typeof transformMap === 'function') { + transformMap = transformMap(docs, { config }) + } + + // the keys of the transform map are the files we have to transform + for (const [target, transform] of Object.entries(transformMap)) { + // the path to the file we're transforming + const targetPath = path.join(config.pluginRuntimeDirectory(plugin.name), target) + + // read the file + const content = await fs.readFile(targetPath) + if (!content) { + return + } - await fs.mkdirp(pluginDir) - await fs.recursiveCopy( - runtime_path, - pluginDir, - Object.fromEntries( - Object.entries(transformMap).map(([key, value]) => [ - path.join(runtime_path, key), - (content) => - value({ + // transform the file + const transformed = transform({ config, content, importStatement, exportDefaultStatement: exportDefaultStatement, exportStarStatement: exportStarStatement, - }), - ]) - ) + }) + + // write the file back out + await fs.writeFile(targetPath, transformed) + } + }) ) } + +function moduleStatments(config: Config) { + const importStatement = + config.module === 'commonjs' + ? importDefaultFrom + : (where: string, as: string) => `import ${as} from '${where}'` + + const exportDefaultStatement = + config.module === 'commonjs' ? exportDefault : (as: string) => `export default ${as}` + + const exportStarStatement = + config.module === 'commonjs' + ? exportStarFrom + : (where: string) => `export * from '${where}'` + + return { + importStatement, + exportDefaultStatement, + exportStarStatement, + } +} diff --git a/packages/houdini/src/codegen/index.ts b/packages/houdini/src/codegen/index.ts index 59d39f0b6..2550f21ec 100755 --- a/packages/houdini/src/codegen/index.ts +++ b/packages/houdini/src/codegen/index.ts @@ -4,11 +4,16 @@ import type { Config, PluginHooks, Document, LogLevels } from '../lib' import { runPipeline as run, LogLevel, find_graphql, parseJS, HoudiniError, fs, path } from '../lib' import { ArtifactKind, type ArtifactKinds } from '../runtime/lib/types' import * as generators from './generators' +import { generatePluginRuntimes } from './generators/runtime' import * as transforms from './transforms' import * as validators from './validators' // the main entry point of the compile script export default async function compile(config: Config) { + // before we collect the documents, we need to generate the plugin runtimes + // so that they can include documents in the user's project + await generatePluginRuntimes({ config }) + // grab the graphql documents const documents = await collectDocuments(config) diff --git a/packages/houdini/src/lib/config.test.ts b/packages/houdini/src/lib/config.test.ts index 2a79ef4d2..4846124c1 100644 --- a/packages/houdini/src/lib/config.test.ts +++ b/packages/houdini/src/lib/config.test.ts @@ -106,3 +106,19 @@ test(`Files that should not be included`, async () => { expect(config.includeFile(path.join(process.cwd(), 'src/routes/test?'))).toBe(false) expect(config.includeFile(path.join(process.cwd(), 'src/rou?tes/page.nop?s?e'))).toBe(false) }) + +test('Config.include includes plugin runtimes', () => { + const config = testConfig() + + config.plugins = [ + { + name: 'test-plugin', + filepath: '', + includeRuntime: 'foo', + }, + ] + + // make sure we are including the plugin runtime + const includePath = path.relative(config.projectRoot, config.pluginDirectory('test-plugin')) + expect(config.include.some((path) => path.includes(includePath))).toBeTruthy() +}) diff --git a/packages/houdini/src/lib/config.ts b/packages/houdini/src/lib/config.ts index a5e73c7ed..4168f9cfa 100644 --- a/packages/houdini/src/lib/config.ts +++ b/packages/houdini/src/lib/config.ts @@ -196,8 +196,25 @@ export class Config { this.plugins.flatMap((plugin) => plugin.extensions ?? []) ) - // any file of a valid extension in src is good enough - return [`src/**/*{${extensions.join(',')}}`] + // by default, any file of a valid extension in src is good enough + const include = [`src/**/*{${extensions.join(',')}}`] + + // if any of the plugins specify included runtimes then their paths might have + // documents + for (const plugin of this.plugins) { + // skip plugins that dont' include runtimes + if (!plugin.includeRuntime) { + continue + } + + // the include path is relative to root of the vite project + const includePath = path.relative(this.projectRoot, this.pluginDirectory(plugin.name)) + + // add the plugin's directory to the include pile + include.push(`${includePath}/**/*{${extensions.join(',')}}`) + } + + return include } pluginConfig(name: string): ConfigType { diff --git a/packages/houdini/src/lib/types.ts b/packages/houdini/src/lib/types.ts index 97f8b3111..f9a2a5f40 100644 --- a/packages/houdini/src/lib/types.ts +++ b/packages/houdini/src/lib/types.ts @@ -137,6 +137,13 @@ export type PluginHooks = { includeRuntime?: string | { esm: string; commonjs: string } + /** + * A relative path from the file exporting your plugin to a runtime that can be + * added to the project before generating artifacts. This is useful for plugins + * that want to add third-party documents to the user's application. + */ + staticRuntime?: string | { esm: string; commonjs: string } + /** * Transform the plugin's runtime while houdini is copying it . * You must have passed a value to includeRuntime for this hook to matter. diff --git a/packages/plugin-svelte-global-stores/package.json b/packages/plugin-svelte-global-stores/package.json index 601b13b5a..1a9271999 100644 --- a/packages/plugin-svelte-global-stores/package.json +++ b/packages/plugin-svelte-global-stores/package.json @@ -58,4 +58,4 @@ }, "main": "./build/plugin-cjs/index.js", "types": "./build/plugin/index.d.ts" -} +} \ No newline at end of file