-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* create fuse app * add buildies * transform next config file * Update tsconfig in create-fuse-app * tweak packagejson * consider module exports * add todo * add api * anotehr todo * nested call * safeguard duplicate import * Handle vscode config in create-fuse-app * add build * add todo * finishing touches * tagline * remove todo * tweak colors * tweak messaging * tweak more * add update to docs * add docs --------- Co-authored-by: Bogdan Soare <[email protected]>
- Loading branch information
1 parent
f34c080
commit 1503183
Showing
11 changed files
with
842 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) Stellate | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# Create-Fuse-App | ||
|
||
Get started with your datalayer! | ||
|
||
When running this in an existing `next` project it will automatically generate all the needed files to get | ||
started with [`fuse`](https://fusejs.org/) | ||
|
||
```sh | ||
npx create-fuse-app | ||
## or | ||
npm init fuse-app | ||
## or | ||
yarn create fuse-app | ||
## or | ||
pnpm create fuse-app | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
{ | ||
"name": "create-fuse-app", | ||
"version": "0.1.0", | ||
"description": "The magical GraphQL framework", | ||
"homepage": "https://github.com/StellateHQ/fuse.js", | ||
"bugs": "https://github.com/StellateHQ/fuse.js/issues", | ||
"license": "MIT", | ||
"author": "Stellate engineering <[email protected]>", | ||
"keywords": [], | ||
"bin": "./dist/index.js", | ||
"type": "module", | ||
"files": [ | ||
"dist", | ||
"LICENSE", | ||
"README.md" | ||
], | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/StellateHQ/fuse.js", | ||
"directory": "packages/create-fuse-app" | ||
}, | ||
"scripts": { | ||
"build": "tsup", | ||
"prepublishOnly": "tsup" | ||
}, | ||
"dependencies": { | ||
"@babel/core": "^7.23.5", | ||
"@clack/prompts": "^0.7.0", | ||
"kolorist": "^1.8.0", | ||
"pkg-install": "^1.0.0" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^20.10.3", | ||
"tsup": "^7.2.0", | ||
"type-fest": "^4.8.3", | ||
"typescript": "^5.3.2" | ||
}, | ||
"publishConfig": { | ||
"access": "public", | ||
"provenance": true | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,275 @@ | ||
#!/usr/bin/env node | ||
import { promises as fs, existsSync } from 'node:fs' | ||
import { resolve } from 'node:path' | ||
import * as prompts from '@clack/prompts' | ||
import { install } from 'pkg-install' | ||
import babel from '@babel/core' | ||
import * as kl from 'kolorist' | ||
import { type PackageJson, TsConfigJson } from 'type-fest' | ||
import rewriteNext from './rewrite-next' | ||
|
||
const s = prompts.spinner() | ||
|
||
async function createFuseApp() { | ||
const packageManager = /yarn/.test(process.env.npm_execpath || '') | ||
? 'yarn' | ||
: 'npm' | ||
|
||
prompts.intro(kl.trueColor(219, 254, 1)('Fuse - Your new datalayer')) | ||
|
||
// TODO: we can prompt for the name of the dir in the future | ||
// when we make this work standalone | ||
const targetDir = resolve(process.cwd()) | ||
|
||
const packageJson = await fs.readFile( | ||
resolve(targetDir, 'package.json'), | ||
'utf-8', | ||
) | ||
const { dependencies, devDependencies } = JSON.parse( | ||
packageJson, | ||
) as PackageJson | ||
const allDeps = { ...dependencies, ...devDependencies } | ||
const nextVersion = allDeps['next'] | ||
|
||
if (!nextVersion) { | ||
throw new Error( | ||
'Could not find "next" as a dependency in your package.json. Please install Next.js first.', | ||
) | ||
} | ||
|
||
s.start('Installing fuse...') | ||
await install(['fuse'], { | ||
prefer: packageManager, | ||
cwd: targetDir, | ||
dev: false, | ||
}) | ||
await install(['@0no-co/graphqlsp', '@graphql-typed-document-node/core'], { | ||
prefer: packageManager, | ||
cwd: targetDir, | ||
dev: true, | ||
}) | ||
s.stop(kl.green('Installed fuse!')) | ||
|
||
// Create initial types and API-Route | ||
s.start('Creating API Route...') | ||
const isUsingSrc = existsSync(resolve(targetDir, 'src')) | ||
const shouldUseAppDir = existsSync(resolve(targetDir, 'app')) | ||
const apiRouteSnippet = createSnippet(shouldUseAppDir) | ||
|
||
if (isUsingSrc) { | ||
const dir = shouldUseAppDir | ||
? resolve(targetDir, 'src', 'app', 'api', 'fuse', 'route.ts') | ||
: resolve(targetDir, 'src', 'pages', 'api', 'fuse.ts') | ||
|
||
if (shouldUseAppDir) { | ||
await fs.mkdir(resolve(targetDir, 'src', 'app', 'api')) | ||
await fs.mkdir(resolve(targetDir, 'src', 'app', 'api', 'fuse')) | ||
} else { | ||
await fs.mkdir(resolve(targetDir, 'src', 'pages', 'api')) | ||
} | ||
await fs.writeFile(dir, apiRouteSnippet) | ||
await fs.mkdir(resolve(targetDir, 'src', 'types')) | ||
await fs.writeFile( | ||
resolve(targetDir, 'src', 'types', 'User.ts'), | ||
initialTypeSnippet, | ||
) | ||
} else { | ||
const dir = shouldUseAppDir | ||
? resolve(targetDir, 'app', 'api', 'fuse', 'route.ts') | ||
: resolve(targetDir, 'pages', 'api', 'fuse.ts') | ||
if (shouldUseAppDir) { | ||
await fs.mkdir(resolve(targetDir, 'app', 'api')) | ||
await fs.mkdir(resolve(targetDir, 'app', 'api', 'fuse')) | ||
} else { | ||
await fs.mkdir(resolve(targetDir, 'pages', 'api')) | ||
} | ||
await fs.writeFile(dir, apiRouteSnippet) | ||
await fs.mkdir(resolve(targetDir, 'types')) | ||
await fs.writeFile( | ||
resolve(targetDir, 'types', 'User.ts'), | ||
initialTypeSnippet, | ||
) | ||
} | ||
s.stop(kl.green('Created API Route!')) | ||
|
||
// Add next plugin to config | ||
s.start('Adding Fuse plugin to Next config...') | ||
const hasJsConfig = existsSync(resolve(targetDir, 'next.config.js')) | ||
const hasMjsConfig = existsSync(resolve(targetDir, 'next.config.mjs')) | ||
|
||
if (hasJsConfig) { | ||
try { | ||
const code = await fs.readFile( | ||
resolve(targetDir, 'next.config.js'), | ||
'utf-8', | ||
) | ||
const result = await babel.transformAsync(code, { | ||
plugins: [[rewriteNext, { isMjs: false }]], | ||
}) | ||
if (result.code) { | ||
await fs.writeFile( | ||
resolve(targetDir, 'next.config.js'), | ||
result.code, | ||
'utf-8', | ||
) | ||
} | ||
} catch (e) {} | ||
} else if (hasMjsConfig) { | ||
try { | ||
const code = await fs.readFile( | ||
resolve(targetDir, 'next.config.mjs'), | ||
'utf-8', | ||
) | ||
const result = await babel.transformAsync(code, { | ||
plugins: [[rewriteNext, { isMjs: true }]], | ||
}) | ||
if (result.code) { | ||
await fs.writeFile( | ||
resolve(targetDir, 'next.config.js'), | ||
result.code, | ||
'utf-8', | ||
) | ||
} | ||
} catch (e) {} | ||
} else { | ||
prompts.text({ | ||
message: | ||
'No next config found, you can add the fuse plugin yourself by importing it from "fuse/next/plugin"!', | ||
}) | ||
} | ||
|
||
if (existsSync(resolve(targetDir, '.vscode', 'settings.json'))) { | ||
const vscodeSettingsFile = await fs.readFile( | ||
resolve(targetDir, '.vscode', 'settings.json'), | ||
'utf-8', | ||
) | ||
const vscodeSettings = JSON.parse(vscodeSettingsFile) | ||
|
||
if ( | ||
vscodeSettings['typescript.tsdk'] !== 'node_modules/typescript/lib' || | ||
vscodeSettings['typescript.enablePromptUseWorkspaceTsdk'] !== true | ||
) { | ||
await fs.writeFile( | ||
resolve(targetDir, '.vscode', 'settings.json'), | ||
JSON.stringify(generateVscodeSettings(vscodeSettings), undefined, 2), | ||
'utf-8', | ||
) | ||
} | ||
} else { | ||
await fs.mkdir(resolve(targetDir, '.vscode')) | ||
await fs.writeFile( | ||
resolve(targetDir, '.vscode', 'settings.json'), | ||
JSON.stringify(generateVscodeSettings(), undefined, 2), | ||
'utf-8', | ||
) | ||
} | ||
|
||
const tsConfigFile = await fs.readFile( | ||
resolve(targetDir, 'tsconfig.json'), | ||
'utf-8', | ||
) | ||
const tsConfig = JSON.parse(tsConfigFile) as TsConfigJson | ||
if ( | ||
!tsConfig.compilerOptions?.plugins?.find( | ||
(plugin) => plugin.name === '@0no-co/graphqlsp', | ||
) | ||
) { | ||
const updatedTsConfig = { | ||
...tsConfig, | ||
compilerOptions: { | ||
...tsConfig.compilerOptions, | ||
plugins: [ | ||
...(tsConfig.compilerOptions?.plugins || []), | ||
{ | ||
name: '@0no-co/graphqlsp', | ||
schema: './schema.graphql', | ||
disableTypegen: true, | ||
templateIsCallExpression: true, | ||
template: 'graphql', | ||
}, | ||
], | ||
}, | ||
} | ||
await fs.writeFile( | ||
resolve(targetDir, 'tsconfig.json'), | ||
JSON.stringify(updatedTsConfig, undefined, 2), | ||
'utf-8', | ||
) | ||
} | ||
s.stop(kl.green('Added Fuse plugin to next config!')) | ||
prompts.outro( | ||
kl.trueColor(219, 254, 1)("You're all set to work with your datalayer!"), | ||
) | ||
} | ||
|
||
createFuseApp().catch(console.error) | ||
|
||
const initialTypeSnippet = `import { node } from 'fuse' | ||
type UserSource = { | ||
id: string | ||
name: string | ||
avatar_url: string | ||
} | ||
// "Nodes" are the core abstraction of Fuse.js. Each node represents | ||
// a resource/entity with multiple fields and has to define two things: | ||
// 1. load(): How to fetch from the underlying data source | ||
// 2. fields: What fields should be exposed and added for clients | ||
export const UserNode = node<UserSource>({ | ||
name: 'User', | ||
load: async (ids) => getUsers(ids), | ||
fields: (t) => ({ | ||
name: t.exposeString('name'), | ||
// rename to camel-case | ||
avatarUrl: t.exposeString('avatar_url'), | ||
// Add an additional firstName field | ||
firstName: t.string({ | ||
resolve: (user) => user.name.split(' ')[0], | ||
}), | ||
}), | ||
}) | ||
// Fake function to fetch users. In real applications, this would | ||
// talk to an underlying REST API/gRPC service/third-party API/… | ||
async function getUsers(ids: string[]): Promise<UserSource[]> { | ||
return ids.map((id) => ({ | ||
id, | ||
name: \`Peter #\${id}\`, | ||
avatar_url: \`https://i.pravatar.cc/300?u=\${id}\`, | ||
})) | ||
}` | ||
|
||
const createSnippet = (appDir) => `import { ${ | ||
appDir ? 'createAPIRouteHandler' : 'createPagesRouteHandler' | ||
} } from 'fuse/next' | ||
// NOTE: This makes Fuse.js automatically pick up every type in the /types folder | ||
// Alternatively, you can manually import each type in the /types folder and remove this snippet | ||
// @ts-expect-error | ||
const files = require.context(${ | ||
appDir ? "'../../../types'" : "'../../types'" | ||
}, true, /\.ts$/) | ||
files | ||
.keys() | ||
.filter((path: string) => path.includes('types/')) | ||
.forEach(files) | ||
const handler = ${ | ||
appDir ? 'createAPIRouteHandler' : 'createPagesRouteHandler' | ||
}() | ||
${ | ||
appDir | ||
? `export const GET = handler\nexport const POST = handler` | ||
: `export default handler` | ||
} | ||
` | ||
|
||
function generateVscodeSettings(settings: any = {}) { | ||
return { | ||
...settings, | ||
'typescript.tsdk': 'node_modules/typescript/lib', | ||
'typescript.enablePromptUseWorkspaceTsdk': true, | ||
} | ||
} |
Oops, something went wrong.
1503183
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
spacex-fuse – ./examples/spacex
spacex-fuse-git-main-stellate.vercel.app
spacex-fuse-stellate.vercel.app
spacex-fuse.vercel.app