Skip to content

Commit

Permalink
Component query variable types (#610)
Browse files Browse the repository at this point in the history
* fix sigint in init

* add component gen

* failing tests

* group files

* generate type files for components

* remove test

* changeset

* let users overwrite prop type

* update typescript guide
  • Loading branch information
AlecAivazis authored Oct 19, 2022
1 parent c1363fe commit 3168f7d
Show file tree
Hide file tree
Showing 14 changed files with 11,119 additions and 7,895 deletions.
5 changes: 5 additions & 0 deletions .changeset/eighty-vans-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'houdini-svelte': patch
---

Generate variable function definitions for non-route queries
2 changes: 1 addition & 1 deletion e2e/sveltekit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"houdini-svelte": "workspace:^",
"prettier": "^2.5.1",
"prettier-plugin-svelte": "^2.5.0",
"svelte": "^3.47.0",
"svelte": "3.52.0",
"svelte-check": "^2.2.6",
"svelte-preprocess": "^4.10.1",
"tslib": "^2.3.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/houdini-svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"scripts": "workspace:^",
"minimatch": "^5.1.0",
"recast": "^0.21.5",
"svelte": "^3.50.1"
"svelte": "^3.52.0"
},
"devDependencies": {
"@types/minimatch": "^5.1.2",
Expand Down
155 changes: 155 additions & 0 deletions packages/houdini-svelte/src/plugin/codegen/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { ArtifactKind, Config, fs, GenerateHookInput } from 'houdini'
import path from 'path'
import * as recast from 'recast'

import { parseSvelte } from '../../extract'
import { Framework } from '../../kit'

type ExportNamedDeclaration = recast.types.namedTypes.ExportNamedDeclaration
type VariableDeclaration = recast.types.namedTypes.VariableDeclaration

export default async function componentTypesGenerator(
framework: Framework,
{ config, documents }: GenerateHookInput
) {
// if we treat the documents as the source of truth for files that match
// 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

// there could be many queries in a given component so we can't just think about filepaths
const queries: Record<string, { name: string; query: string }[]> = {}
for (const document of documents) {
if (document.kind !== ArtifactKind.Query) {
continue
}

queries[document.filename] = (queries[document.filename] ?? []).concat({
name: document.name,
query: document.originalString,
})
}
let matches = Object.keys(queries).filter((filepath) => filepath.endsWith('.svelte'))

// if we are in kit, don't consider the source directory
if (framework === 'kit') {
matches = matches.filter((match) => !match.startsWith(config.routesDir))
}

// group the files by directory
const files: ProjectDirs = {
dirs: {},
files: [],
}

// put every file we found in the right place
for (let file of matches) {
// only worry about things relative to the project root
file = path.relative(config.projectRoot, file)

// walk down the path
let target = files
const parts = file.split('/')
for (const [i, part] of parts.entries()) {
// if we are at the end of the path, we are looking at a file
if (i === parts.length - 1) {
target.files.push(part)
continue
}

// we are on a file
if (!target.dirs[part]) {
target.dirs[part] = {
dirs: {},
files: [],
}
}

// there is guaranteed to be an entry for this particular filepath part
// focus on it and move onto the next one
target = target.dirs[part]
}
}

// now that we've grouped together all of the files together, we can just walk down the
// structure and generate the necessary types at the right place.
await walk_project(config, files, queries, config.projectRoot)
}

async function walk_project(
config: Config,
dirs: ProjectDirs,
queries: Record<string, { name: string; query: string }[]>,
root: string
) {
// process every child directory
await Promise.all(
Object.entries(dirs.dirs).map(async ([path_part, child]) => {
// keep going with the new root
return walk_project(config, child, queries, path.join(root, path_part))
})
)

// if we don't have any files at this spot we're done
if (dirs.files.length === 0) {
return
}

// every query in this directory needs an entry in the file
let typeFile = "import type { ComponentProps } from 'svelte'"
for (const file of dirs.files) {
const no_ext = path.parse(file).name
const prop_type = no_ext + 'Props'

// figure out the full file path
const filepath = path.join(root, file)

// we need to figure out the props for this component
const contents = await fs.readFile(filepath)
// make typescript happy
if (!contents) {
continue
}

// define the prop types for the component
typeFile =
`
import ${no_ext} from './${file}'
` +
typeFile +
`
type ${prop_type} = ComponentProps<${no_ext}>
`

// a file can contain multiple queries
for (const query of queries[filepath]) {
// we can't generate actual type defs for props so let's just export a
// generic typedefinition
typeFile =
`
import type { ${query.name}$input } from '${path
.relative(filepath, path.join(config.artifactDirectory, query.name))
.replace('/$houdini', '')}'
` +
typeFile +
`
export type ${config.variableFunctionName(
query.name
)} = <_Props = ${prop_type}>(args: { props: _Props }) => FragmentQueryVars$input
`
}
}

// we need to write this file in the correct location in the type root dir
const relative = path.join(config.typeRootDir, path.relative(config.projectRoot, root))

// write the file
await fs.mkdirp(relative)
await fs.writeFile(path.join(relative, '$houdini.d.ts'), typeFile)
}

type ProjectDirs = {
dirs: Record<string, ProjectDirs>
files: string[]
}
10 changes: 8 additions & 2 deletions packages/houdini-svelte/src/plugin/codegen/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { GenerateHookInput, fs, Config } from 'houdini'

import { stores_directory, type_route_dir } from '../kit'
import adapter from './adapter'
import kit from './kit'
import components from './components'
import kit from './routes'
import stores from './stores'

export default async function (input: PluginGenerateInput) {
Expand All @@ -13,7 +14,12 @@ export default async function (input: PluginGenerateInput) {
])

// generate the files
await Promise.all([adapter(input), kit(input.framework, input), stores(input)])
await Promise.all([
adapter(input),
kit(input.framework, input),
stores(input),
components(input.framework, input),
])
}

export type PluginGenerateInput = Omit<GenerateHookInput, 'config'> & {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export default async function svelteKitGenerator(
framework: Framework,
{ config }: GenerateHookInput
) {
// if we're not in a sveltekit project, don't do anything
// this generator creates the locally imported type definitions.
// the component type generator will handle
if (framework !== 'kit') {
return
}
Expand Down Expand Up @@ -219,15 +220,15 @@ function append_afterLoad(
return afterLoad
? `
type AfterLoadReturn = ReturnType<typeof import('./+${type.toLowerCase()}').afterLoad>;
type AfterLoadData = {
${internal_append_afterLoad(queries)}
}
type LoadInput = {
${internal_append_afterLoadInput(queries)}
}
export type AfterLoadEvent = {
event: PageLoadEvent
data: AfterLoadData
Expand Down
39 changes: 28 additions & 11 deletions packages/houdini/src/cmd/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export default async function init(
message: 'Is your GraphQL API running?',
name: 'running',
type: 'confirm',
initial: true,
})
).running
}
Expand All @@ -44,12 +45,19 @@ export default async function init(
return
}

let { url } = await prompts({
message: "What's the URL for your api?",
name: 'url',
type: 'text',
initial: 'http://localhost:3000/api/graphql',
})
let { url } = await prompts(
{
message: "What's the URL for your api?",
name: 'url',
type: 'text',
initial: 'http://localhost:4000/api/graphql',
},
{
onCancel() {
process.exit(1)
},
}
)

try {
// verify we can send graphql queries to the server
Expand Down Expand Up @@ -566,6 +574,8 @@ async function detectTools(cwd: string): Promise<DetectedTools> {
const packageJSONFile = await fs.readFile(path.join(cwd, 'package.json'))
if (packageJSONFile) {
var packageJSON = JSON.parse(packageJSONFile)
} else {
throw new Error('not found')
}
} catch {
throw new Error(
Expand Down Expand Up @@ -622,11 +632,18 @@ async function updateFile({
${content}`)

// ask the user if we should continue
const { done } = await prompts({
name: 'done',
type: 'confirm',
message: 'Should we overwrite the file? If not, please update it manually.',
})
const { done } = await prompts(
{
name: 'done',
type: 'confirm',
message: 'Should we overwrite the file? If not, please update it manually.',
},
{
onCancel() {
process.exit(1)
},
}
)

if (!done) {
return
Expand Down
2 changes: 2 additions & 0 deletions packages/houdini/src/codegen/generators/indexFile/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export default async function writeIndexFile(config: Config, docs: CollectedGrap
export_default_as,
export_star_from,
plugin_root: config.pluginDirectory(plugin.name),
typedef: false,
documents: docs,
})
}

Expand Down
2 changes: 2 additions & 0 deletions packages/houdini/src/codegen/generators/typescript/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ export default async function typescriptGenerator(
export_default_as,
export_star_from,
plugin_root: config.pluginDirectory(plugin.name),
typedef: true,
documents: docs,
})

// if the plugin generated a runtime
Expand Down
27 changes: 1 addition & 26 deletions packages/houdini/src/codegen/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,32 +168,7 @@ export async function runPipeline(config: Config, docs: CollectedGraphQLDocument

async function collectDocuments(config: Config): Promise<CollectedGraphQLDocument[]> {
// the first step we have to do is grab a list of every file in the source tree
let sourceFiles = [
...new Set(
(
await Promise.all(
config.include.map((filepath) =>
promisify(glob)(path.join(config.projectRoot, filepath))
)
)
)
.flat()
.filter((filepath) => config.includeFile(filepath))
// don't include the schema path as a source file
.filter((filepath) => {
const prefix = config.schemaPath?.startsWith('./') ? './' : ''

return (
!config.schemaPath ||
!minimatch(
prefix +
path.relative(config.projectRoot, filepath).replaceAll('\\', '/'),
config.schemaPath
)
)
})
),
]
let sourceFiles = await config.sourceFiles()

// the list of documents we found
const documents: DiscoveredDoc[] = []
Expand Down
31 changes: 31 additions & 0 deletions packages/houdini/src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,35 @@ export class Config {
)
}

async sourceFiles() {
return [
...new Set(
(
await Promise.all(
this.include.map((filepath) =>
promisify(glob)(path.join(this.projectRoot, filepath))
)
)
)
.flat()
.filter((filepath) => this.includeFile(filepath))
// don't include the schema path as a source file
.filter((filepath) => {
const prefix = this.schemaPath?.startsWith('./') ? './' : ''

return (
!this.schemaPath ||
!minimatch(
prefix +
path.relative(this.projectRoot, filepath).replaceAll('\\', '/'),
this.schemaPath
)
)
})
),
]
}

/*
Directory structure
Expand Down Expand Up @@ -900,6 +929,8 @@ type ModuleIndexTransform = (arg: {
export_default_as(args: { module: string; as: string }): string
export_star_from(args: { module: string }): string
plugin_root: string
typedef: boolean
documents: CollectedGraphQLDocument[]
}) => string

export type GenerateHook = (args: GenerateHookInput) => Promise<void> | void
Expand Down
Loading

1 comment on commit 3168f7d

@vercel
Copy link

@vercel vercel bot commented on 3168f7d Oct 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.