Skip to content

Commit

Permalink
Static analysis (#39)
Browse files Browse the repository at this point in the history
* Initial babel plugin

* Cache + analyzer

* Member analysis

* Filter preloader output based on arguments

* Handle more edge cases

* Import analysis

* Add more tests

* Track function calls

* Add more tests

* fix

* Support var destructuring with pathCtx

* Pattern scanning

* Add more tests

* Track array iterations

* Add tests

* Initial devtools commit

* fix: build

* fix: typescript types
  • Loading branch information
samdenty committed Mar 13, 2020
1 parent 2f6f3f4 commit 93888e9
Show file tree
Hide file tree
Showing 138 changed files with 13,648 additions and 1,292 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -15,3 +15,4 @@ build
.env
.envrc
.changelog
.history
112 changes: 112 additions & 0 deletions SPEC/COMPILER/COMPILER.md
@@ -0,0 +1,112 @@
# gqless compiler

## What is does

It takes your sourcecode, and generates a Graph from it.#

## Options

Due to the single-file nature of babel, cross-file data references will be tricky to implement.

### Static analysis IR

Outputs `[file].data.json` files from your sourcecode. These files will be read by the babel-plugin, to transform the code.

Limitations:

- Requires a separate "extraction" step & files.

See [data/index.data.json](./data/index.data.json)

### Single-file static analysis

Works as a babel-plugin.

Has a static graph of your app.

Limitations:

- All graphql-data references should be local to the file, as imports can't be resolved.

```ts
const useFullName = user => user.firstName + user.lastName

const User = graphql(({ user }) => {
const fullName = useFullName(user)
return <div>{fullName}</div>
})

// =>
const App = graphql(({ user }) => {
const fullName = useFullName(user)
return <div>{fullName}</div>
})

App[Symbol.preload] = props => {
props.user.firstName
props.user.lastName
}
```

### "Dynamic" analysis

Works as a babel-plugin.
Doesn't actually have a static graph of your app. Relies upon the runtime to generate it.

Require all functions to have a `Symbol.preload` method on them:

Limitations:

- Dead-code removal would be infeasible

```tsx
const useFullName = user => user.firstName + user.lastName

const User = graphql(({ user }) => {
const fullName = useFullName(user)
return <div>{fullName}</div>
})

// =>

const useFullName = user => {
user.firstName
user.lastName
}
useFullName[Symbol.preload] = user => {
user.firstName
user.lastName
}

const App = graphql(({ user }) => {
const fullName = useFullName(user)>
return <div>{fullName}</div>
})

App[Symbol.preload] = props => {
useFullName[Symbol.preload]?.(props.user)
}
```

## Method

Find all import references to `query` from `./graphql`.

Iterate over references

- If referenced inside a function

## Preloaders

In order for dead code elimination and lazy-loading to work, a file named `preloaders.js` will be created in the client directory.

It will take `preload(Component, {})` and convert it into `preloadComponent` (inserting an import from preloaders.js).

**preloaders.js:**

```js
export function preloadUserComponent() {}
export function preloadAppComponent() {
preloadUserComponent({})
}
```
15 changes: 15 additions & 0 deletions SPEC/COMPILER/EXAMPLES.md
@@ -0,0 +1,15 @@
```ts
preload(
_0 => {
_0.a.name
_0.b.name
},
// Value of the call arguments, is used to 'refine' output
{ a: data }
)

// =>
const preloadFunc = _0 => {
_0.a?.name
}
```
3 changes: 3 additions & 0 deletions SPEC/COMPILER/METHOD.md
@@ -0,0 +1,3 @@
# Method

Find all references to `preload(Function, {})`, and traverse from there.
47 changes: 47 additions & 0 deletions SPEC/COMPILER/TRACKING.md
@@ -0,0 +1,47 @@
# Tracking

`preload(myFunc, GRAPHQL_DATA)`

In order to `preload` a function, call information is required.

```ts
preload(
_0 => {
_0.a.name
_0.b.name
},
// Value of the call arguments, is used to 'refine' output
{ a: data }
)

// =>
const preloadFunc = _0 => {
const _0a = _0.a
if (_0a) {
_0a.name
}
}
```

```ts
preload(_0 => {
_0.a.name
_0.b.name
}, {})

// =>
const preloadFunc = _0 => {}
```

## Preload arguments

Arguments can be either inline objects, or references:

```js
{ user },
{ user: { followers: [user] }}
```

When inline objects/arrays are used, output will be refined.

When references are used, the entire function's output will be produced.
18 changes: 18 additions & 0 deletions SPEC/COMPILER/data/.gqless-data/AppComponent.json
@@ -0,0 +1,18 @@
{
"AppComponent": {
"variables": {
"limit": "10"
},
"data": [
{
"file": "UserComponent.json",
"export": "UserComponent",
"args": [
{
"user": "query.users({ limit: var.limit }).0"
}
]
}
]
}
}
5 changes: 5 additions & 0 deletions SPEC/COMPILER/data/.gqless-data/DescriptionComponent.json
@@ -0,0 +1,5 @@
{
"DescriptionComponent": {
"data": ["0.user.description"]
}
}
17 changes: 17 additions & 0 deletions SPEC/COMPILER/data/.gqless-data/UserComponent.json
@@ -0,0 +1,17 @@
{
"UserComponent": {
"data": [
"query.me.name",
{
"file": "useFullName.json",
"export": "useFullName",
"args": ["0.user"]
},
{
"file": "DescriptionComponent.json",
"export": "DescriptionComponent",
"args": [{ "user": "0.user" }]
}
]
}
}
1 change: 1 addition & 0 deletions SPEC/COMPILER/data/.gqless-data/index.json
@@ -0,0 +1 @@
["AppComponent.json", "./UserComponent.json"]
5 changes: 5 additions & 0 deletions SPEC/COMPILER/data/.gqless-data/useFullName.json
@@ -0,0 +1,5 @@
{
"useFullName": {
"data": ["0.firstName", "0.lastName"]
}
}
22 changes: 22 additions & 0 deletions SPEC/COMPILER/data/AppComponent.js
@@ -0,0 +1,22 @@
import { query } from './client'
import { UserComponent } from './UserComponent'
import { useVariable } from '@gqless/react'

export function AppComponent() {
const limit = useVariable(window.asd ? 0 : 10)

return (
<div>
{query.users({ limit }).map(user => (
<UserComponent user={user} />
))}
</div>
)
}

export function preloadAppComponent() {
const limit = window.asd ? 0 : 10

const user = query.users({ limit })[0]
preloadUserComponent({ user })
}
15 changes: 15 additions & 0 deletions SPEC/COMPILER/data/DataJSON.ts
@@ -0,0 +1,15 @@
type Import = {
file: string
import: string
args?: any[]
}

type Data = (Import | string)[]

export type DataJSON = Record<
string,
{
data: Data
variables?: Record<string, string | number | boolean>
}
>
6 changes: 6 additions & 0 deletions SPEC/COMPILER/data/DescriptionComponent.js
@@ -0,0 +1,6 @@
const getDescription = user => user.description

export function DescriptionComponent({ user }) {
// Same-file functions are flattened into an export
return getDescription(user)
}
15 changes: 15 additions & 0 deletions SPEC/COMPILER/data/UserComponent.js
@@ -0,0 +1,15 @@
import { query } from './client'
import { DescriptionComponent } from './DescriptionComponent'
import { useFullName } from './useFullName'

export function UserComponent({ user }) {
const fullName = useFullName(user)

return (
<div>
{query.me.name}
{fullName}
<DescriptionComponent user={user} />
</div>
)
}
2 changes: 2 additions & 0 deletions SPEC/COMPILER/data/index.js
@@ -0,0 +1,2 @@
export * from './AppComponent'
export * from './UserComponent'
3 changes: 3 additions & 0 deletions SPEC/COMPILER/data/useFullName.js
@@ -0,0 +1,3 @@
export const useFullName = user => {
user.firstName + user.lastName
}
20 changes: 20 additions & 0 deletions SPEC/COMPILER/react-example.tsx
@@ -0,0 +1,20 @@
import * as React from 'react'
import { query } from './gqless'

const UserComponent = graphql(({ user }) => {
return <div>{user.name}</div>
})
UserComponent.preload = ({ user }) => {
user.name
}

const App = graphql(() => {
return (
<div>
<UserComponent user={query.me} />
</div>
)
})

App.preload()
UserComponent.preload({ user: query.me })

0 comments on commit 93888e9

Please sign in to comment.