blitz-js / blitz Goto Github PK
View Code? Open in Web Editor NEW⚡️ The Missing Fullstack Toolkit for Next.js
Home Page: https://Blitzjs.com
License: MIT License
⚡️ The Missing Fullstack Toolkit for Next.js
Home Page: https://Blitzjs.com
License: MIT License
Most new lines are removed from template files for plain JS apps
Example for db/index.js
:
import { PrismaClient } from "@prisma/client"
export * from "@prisma/client"
const prisma = new PrismaClient()
export default prisma
blitz new myapp --js
myapp/db/index.js
and see that it doesn't match the template at packages/generator/templates/app/db/index.ts
This also happens for blitz generate
latest canary branch
Make a blitz recipe for Sentry!
This repo shows how to integrate Sentry: https://github.com/flybayer/blitz-sentry
So need to convert that into a recipe.
This issue is for designing how error handling works for queries & mutation execution. Related #85
Please provide any feedback you have or if you have a suggestion for how this can be better!
Serialize error objects for sending over the network. On the client, the error will be deserialized. The error will be returned from useQuery
, but it will be re-thrown from mutations.
All error object properties will be preserved except for the stack trace (I don't think a server stack trace is helpful on the client)
No special API. Simply write normal Node.js code as you've always done.
success
and error
properties.// /some/path/createProduct.ts
import db from 'db'
class ValidationError extends Error {
constructor(fieldErrors) {
this.fieldErrors = fieldErrors
}
}
export default async function createProduct(args, ctx) {
if (!args.name) {
throw ValidationError({name: "Name must be required})
}
// ... other stuff
return product
}
// inside page.tsx
export default function(props: {query: {id: number}}) {
const [product, {error}] = useQuery(getProduct, {where: {id: props.query.id}})
if (error) {
if (error.name === 'AuthorizationError') {
return <div>You are not authorized to view this</div>
} else {
return <div>Error: {error.message}</div>
}
}
return <div>{product.name}</div>
}
// inside page.tsx
try {
await createProduct(args)
} catch (error) {
if (error.name === 'AuthorizationError') {
alert('You are not authorized')
} else if (error.name === 'ValidationError') {
setErrors(error.fieldErrors)
}
}
Currently blitz new generates an app with the create-react-app ESLint config.
The community is divided enough over their preferred eslint config that I think we should support a few other ones and then prompt the user to choose during blitz new generation.
Other configs to support:
If you want something besides CRA eslint or Standard.js, comment here. We can't support every variation, but we can support the top ones.
And we'll definitely appreciate help implementing this!
What do you want and why do you want it?.
Currently most of the dependency versions in the blitz new
template are defined as latest
. This is not ideal.
It's much better for versions to be explicitly defined to prevent unintended dependency version changes. Unknown dependency version changes are a common source of bugs.
Currently the template is using latest
to ensure the new app always has the latest version.
Possible implementation(s)
latest
and then during blitz new generation automatically update that to the explicit versionTo make Blitz the first choice framework for makers, it would be great to have Stripe integration so we can quickly go from a blank slate to selling products on the site.
This can likely be achieved as a Blitz installer. Stripe integration I believe just requires a serverless endpoint and configuration. Might need to explore this more before suggesting how exactly it could work.
What is the problem?
There's a long pause after the files are created, but before "Installing dependencies" is printed. This is because we are fetching the latest dependency versions.
Instead of an awkward pause, we should show some status update. Move the "installing dependencies" log above this, then add a spinner or something.
Steps to Reproduce:
[email protected]
blitz new
Versions:
When building a full stack application, it's common for it to grow into something much more. For example, a companion mobile application is often part of a website and backend ecosystem. It would great if Blitz expands to accommodate this use case more and generate easy-to-user documentation or client which can be used to build external clients that utilise the generated Blitz API.
axios
or pure fetch
API?I have something like swagger.io in mind, however I'm sure it could be far greater!
Example:
https://petstore.swagger.io/?_ga=2.158440947.899930001.1594640979-1996492643.1594640979
It would be helpful to be able to programmatically add image and video upload, manipulation, optimization, and delivery capabilities to a Blitz app via Cloudinary's REST APIs.
This could be a recipe installer.
Remove the jobs
folder from the new app template. We are not using this currently. And it's possible it will have special behavior in the future, so we need to remove it for now.
This is a simple change: remove this folder: https://github.com/blitz-js/blitz/blob/canary/packages/generator/templates/app/jobs
What is the problem?
TypeError: Cannot read property 'findMany' of undefined
after schema changed and CRUD
generated
Steps to Reproduce:
blitz
started, add a new model on the schema (i.e. Comment
) .blitz db migrate
blitz generate all comment
TypeError: Cannot read property 'findMany' of undefined
error on the browser> Running getComments(null) ✕ getComments failed: TypeError: Cannot read property 'findMany' of undefined
on the consoleVersions:
debug: local
debug: pkgPath: /Users/osirvent/dev/blitzjs/tests/test2/node_modules/@blitzjs/cli
macOS Catalina darwin-x64 node-v12.16.2
blitz: 0.6.2 (global)
blitz: 0.6.2 (local)
Supporting Documentation
`×TypeError: Cannot read property 'findMany' of undefined
getComments
./app/_rpc/comments/queries/getComments.ts:4
1 | import db, {FindManyCommentArgs} from 'db'
2 |
3 | export default async function getComments(args: FindManyCommentArgs) {
4 | const comments = await db.comment.findMany(args)
5 |
6 | return comments
7 | }
at /Users/osirvent/dev/blitzjs/tests/test2/node_modules/
blitzjs/server/dist/server.cjs.development.js:1610:34
at _catch (/Users/osirvent/dev/blitzjs/tests/test2/node_modules/
blitzjs/server/dist/server.cjs.development.js:1566:18
at /Users/osirvent/dev/blitzjs/tests/test2/node_modules/
blitzjs/server/dist/server.cjs.development.js:1609:32
apiResolver
/Users/osirvent/dev/blitzjs/tests/test2/node_modules/next/dist/next-server/server/api-utils.js:46:15
processTicksAndRejections
internal/process/task_queues.js:97:5
async DevServer.handleApiRequest
/Users/osirvent/dev/blitzjs/tests/test2/node_modules/next/dist/next-server/server/next-server.js:464:9
async Object.fn
/Users/osirvent/dev/blitzjs/tests/test2/node_modules/next/dist/next-server/server/next-server.js:386:37
async Router.execute
/Users/osirvent/dev/blitzjs/tests/test2/node_modules/next/dist/next-server/server/router.js:134:32
async DevServer.run
/Users/osirvent/dev/blitzjs/tests/test2/node_modules/next/dist/next-server/server/next-server.js:506:29
async DevServer.handleRequest
/Users/osirvent/dev/blitzjs/tests/test2/node_modules/next/dist/next-server/server/next-server.js:150:20
This screen is visible only in development. It will not appear if the app crashes in production.
Open your browser’s developer console to further inspect this error.`
See #225 (comment)
Extracted from blitz-js/legacy-framework#764. Writing this up as a new issue because blitz-js/legacy-framework#764 was closed.
A way to choose which database engine to use (Postgres or SQLite) on app creation.
@flybayer wrote:
I think we should add a prompt to blitz new that asks if you want sqlite or postgres (same as we do now for form library).
@flybayer also wrote:
We are going to remove the
provider = ["sqlite", "postgres"]
thing because it's causing too many problems.
(I couldn’t find an issue covering this point.)
Currently you need to manually call refetch()
or mutate()
on a useQuery
result after doing a mutation.
We should try to come up with a way to automatically invalidate queries so that most users don't have to think about it all.
I haven't spent a lot of time thinking about this, but here's a couple options.
We could automatically invalidate queries at the same folder level of the mutation.
So with this file structure,
app/products/queries/getProduct.ts
app/products/queries/getProducts.ts
app/products/mutations/createProduct.ts
app/products/mutations/updateProduct.ts
app/products/mutations/deleteProduct.ts
Calling createProduct
, updateProduct
, or deleteProduct
would invalidate the cache for getProduct
and getProducts
.
But the problem with this is that app/queries/getDisplayProducts.ts
would not be invalidated even thought it is also affected. But maybe this is still worth it if it solves the 80% use case.
Maybe it's possible for us to compute the entire data graph at build time using a combination of the Prisma DMMF and Typescript? If possible, this would be amazing because we could accurately invalidate all queries anywhere in the app.
I think this would only work with Prisma, so we should still have some other fallback method for Blitz apps without Prisma.
Something else that's a possibility is busting the cache on route change, but that still requires you to think about it.
I'm super keen to hear anyone's ideas on this!
Currently the transform executor API theoretically could support the user selecting multiple files to transform, or using a detector function to dynamically select all files that pass a custom test.
There's two options here. First, we could continue to use the selectTargetFiles
field which already exists on the API, and just require the author to do whatever they'd like to detect the total list of files to modify.
Alternatively, we could change that prop to something like detectFile
which takes in a file's AST and the recast
API (similar to the transform API) and returns a boolean, telling us whether or not to execute the transform on the file. In that case, we'd have to target the entire codebase recursively with the transform which makes it a bit slower, but has the benefit that it will prevent authors from having to write too much boilerplate.
There is of course the third option where we support both 😬. That's just a stretch goal though
What is the problem?
blitz h
should display the help information instead of this:
~/c/blitz> blitz h
You are using alpha software - if you have any problems, please open an issue here:
https://github.com/blitz-js/blitz/issues/new/choose
Also, this CLI may be slow as it's not yet optimized for speed
› Warning: h is not a blitz command.
Did you mean b? [y/n]:
› Error: Run blitz help for a list of available commands.
Running prisma studio as an npm script is common for folks, so we should add it by default.
Add the below code here: https://github.com/blitz-js/blitz/blob/canary/packages/generator/templates/app/package.json#L7
"studio": "blitz db studio"
I think sometimes it's useful having code that's not automatically exposed as an endpoint, but it's sitting there in a query or a mutation file. For example, when doing trunk based development you want the code in place, but not yet exposed to the world.
A common way to do this are feature flags, but those carry their own issues, another common approach I take is to code the feature, but hide the endpoint. In rails this is easy to do because a controller needs to be connected on the routes file.
I think this should be as simple as returning 404 if there's no default export on a query or a mutation. Currently it tries to apply a middleware to an undefined object and it fails.
N/A
We have a pretty rough situation with our blitz dev environment when running yarn dev
.
When running yarn build
, all packages are built in the proper order and everything works great. Dependent packages are automatically built before parent packages.
However, this does not work right with yarn dev
. Everything is built in a random order resulting in errors like this:
@blitzjs/cli: src/commands/install.ts(3,25): error TS7016: Could not find a declaration file for module '@blitzjs/installer'. '/Users/b/c/blitz/packages/installer/dist/index.js' implicitly has an 'any' type.
@blitzjs/cli: Try `npm install @types/blitzjs__installer` if it exists or add a new declaration (.d.ts) file containing `declare module '@blitzjs/installer';`
@blitzjs/cli: 8:58:46 PM - Found 1 error. Watching for file changes.
As a workaround thus far, we have package.json scripts like this to ensure packages are built in the proper order. But we're now adding more and more packages and it's getting out of hand.
"predev": "wait-on ../core/dist/packages/core/src/index.d.ts && wait-on ../server/dist/packages/server/src/index.d.ts && wait-on ../generator/dist/packages/generator/src/index.d.ts && wait-on ../cli/lib/src/index.js",
predev
package.json scriptsyarn reset
yarn dev
wait-on
stuff. I don't know if this is a lerna thing or a tsdx thing. I think maybe lerna? If so, maybe it's as simple as adding a certain configuration or cli flag. I don't know.wait-on
stuff. The node script can be called via a package.json script, and it would automatically detect the other blitz package dependencies and wait for those to be built.useSession() should also return data during SSR.
Fetch the session data from the incoming request, make it available to useSession.
First discussed during the React Summit discussion.
You get the following error:
~/c/b/e/vanilla-next> yarn start
yarn run v1.19.1
$ blitz start
[ wait ] starting the development server ...
[ info ] waiting on http://localhost:3000 ...
ReferenceError: _launchEditorFn is not defined
at Object.<anonymous> (/Users/b/c/blitz/node_modules/next/dist/compiled/launch-editor/index.js:2:77)
at Module._compile (internal/modules/cjs/loader.js:1158:30)
at Module._compile (/Users/b/c/blitz/node_modules/pirates/lib/index.js:99:24)
at Module._extensions..js (internal/modules/cjs/loader.js:1178:10)
at Object.newLoader [as .js] (/Users/b/c/blitz/node_modules/pirates/lib/index.js:104:7)
at Module.load (internal/modules/cjs/loader.js:1002:32)
at Function.Module._load (internal/modules/cjs/loader.js:901:14)
at Module.require (internal/modules/cjs/loader.js:1044:19)
at require (internal/modules/cjs/helpers.js:77:18)
at Object.<anonymous> (/Users/b/c/blitz/node_modules/next/dist/server/lib/error-overlay-middleware.js:1:166)
i18n is immensely needed in any web framework.
The Nextjs community are yet to agree on the best possible way to implement i18n while keeping SSR / SSG / Serverless working as expected.
Can this be solved by Blitz?
Here's the full session management RFC: #475
@rishabhpoddar has finished the pseudo code for this part of the Essential method: blitz-js/drafts#10
Here's the rendered version of his pseudo code.
We need to add a mutate
function that's returned from useQuery
. This allows you to manually update the query cache without refetching from the network.
mutate
must be fully typed for Typescript usage. The input type is the same as the query function return type.mutate
must automatically trigger a refetch for the initial query to ensure it has the correct dataexport default function (props}) {
const [product, {mutate}] = useQuery(getProduct, {where: {id: props.id}})
return (
<Formik
initialValues={product}
onSubmit={async (values) => {
try {
const product = await updateProduct(values)
mutate(product)
} catch (error) {
alert("Error saving product")
}
}}>
{/* ... */}
</Formik>
)
}
Currently blitz db migrate
always runs prisma migrate save
and prisma migrate up
.
migrate up
will create new migration files if needed and migrate up
will apply existing migration files.
For safety, migrate save
should only run if NODE_ENV !== 'production'
. But migrate up
should always run.
Here's the line of code where this happens: https://github.com/blitz-js/blitz/blob/canary/packages/cli/src/commands/db.ts#L43
What is the problem?
Blitz server isn't swapping out the file paths properly in the Next.js output. See the wrong output in the log below.
Steps to Reproduce:
examples/store/node_modules/@prisma/client
blitz build
in the store exampleVersions:
Supporting Documentation
~/c/b/e/store> blitz b
✔ Blitz is ready
Warning: You have enabled experimental feature(s).
Experimental features are not covered by semver, and may cause unexpected or broken application behavior. Use them at your own risk.
Creating an optimized production build
Failed to compile.
/Users/b/c/blitz/examples/store/.blitz/caches/build/app/_rpc/products/mutations/createProduct.ts
ERROR in /Users/b/c/blitz/examples/store/.blitz/caches/build/app/_rpc/products/mutations/createProduct.ts(1,13):
1:13 Module '"../../../../../../../../../../../../../Users/b/c/blitz/examples/store/.blitz/caches/build/db"' has no exported member 'ProductCreateArgs'. Did you mean to use 'import ProductCreateArgs from "../../../../../../../../../../../../../Users/b/c/blitz/examples/store/.blitz/caches/build/db"' instead?
> 1 | import db, {ProductCreateArgs} from 'db'
| ^
2 |
3 | export default async function createProduct(args: ProductCreateArgs) {
4 | const product = await db.product.create(args)
> Build error occurred
Error: > Build failed because of webpack errors
at build (/Users/b/c/blitz/node_modules/next/dist/build/index.js:13:900)
The new app template's index.tsx
has this:
const modelSnippet = `model Project {
id Int @default(autoincrement()) @id
name String
}`
const migrateSnippet = `$ blitz db migrate
$ blitz generate all project`
And it's very confusing for new folks because it looks like it may be part of blitz code. But in reality it's just a string for display on the webpage.
So we need to just inline that string into the JSX where it's used, instead of having them as separate variables.
Here's the lines that need moved: https://github.com/blitz-js/blitz/blob/canary/packages/generator/templates/app/app/pages/index.tsx#L3-L9
What do you want and why do you want it?.
Inside packages/cli/src/generators/app.tsx
we have the following:
await replaceDependencies(pkg, this.destinationPath(), pkgDependencies, 'dependencies')
await replaceDependencies(pkg, this.destinationPath(), pkgDevDependencies, 'devDependencies')
But that needs to be changed to use Promise.all()
This makes blitz new unnecessarily slow
Update this file to include AppProps
like this:
import { AppProps } from 'blitz'
export default function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
Open source veterans, please leave this issue for someone new to Blitz and/or open-source :)
Commonly, API calls will be mocked out in tests in order to decouple and speed them up.
Since one of Blitz core concepts is to hide network calls and replace them by (opaque) RPC, the natural counterpart would be to just mock out the generated network fetching logic instead.
The interface could look like this:
import ProjectsList from "./ProjectsList";
import getProjects from "./queries/getProjects";
import { mockQuery } from "blitz/test";
const getProjectsMock = mockQuery(
getProjects,
// mock implementation
async () => {
return [{ id: 1, name: "Blitz" }];
}
);
describe("ProjectsList", () => {
it("calls `getProjects`", () => {
const comp = render(() => <ProjectsList />);
expect(getProjects).toHaveBeenCalled();
})
})
Since Blitz already replaces queries with generated code, we could just generate a "sleeping mock" during testing.
By "Sleeping", i mean: mocking would be disabled by default, but could be enabled using mockQuery
.
This would circumvent all Test-Runner mocking mechanisms and thus doesn't run into the problem of mocks needing to be hoisted (see Jest hoisting jest.mock).
Upgrade Next to 9.4.4 in all the package.json's in our repo.
NOTE: this is super easy change, so veterans, please leave it for a new Blitz contributor or someone new to contributing to open source :)
Applications commonly require status endpoints for monitoring purposes.
It'd be great if Blitz supported that out of the box, preferably with included health checks of all necessary resources.
GET /api/health
200 OK
{ "isHealthy": true, "resources": { "redis": true, "db": true } }
Registering a new resource to be health-checked could work like this:
import { registerHealthCheck } from "blitz/health";
registerHealthCheck(
"redis",
async () => {
const response = await redis.ping();
return response === "pong";
}
);
We could readily bundle healthchecks with officially supported resources, e.g. the Prisma Datasource, Job Queues or connected SMTP endpoints.
Add the /api/health
API route, which will check against all registered resources when called.
The specific endpoint could be altered in blitz.config.js
.
I prefer to develop reusable components in isolation using Storybook so support in Blitz would be very useful.
According to https://2019.stateofjs.com/testing/ Storybook is significantly more popular than alternatives such as Styleguidist.
Some teams might also benefit from publishing their Storybook as a helpful guide for other developers.
Given that Storybook is a popular choice and that it doesn't impose any restrictions on the user, I think built-in support could be best based on the "Convention over Configuration" argument.
You can use vanilla JS right now in a Blitz app by creating new files in JS or converting the existing files to JS.
However, currently blitz new generates all Typescript files. We need to add a prompt during blitz new for selecting Typescript or plain Javascript.
The main unknown on implementing this is how to minimize code duplication for template files. Can we write all templates in TS and then have a tool that strips all the TS stuff?
Note: we also need this same JS support for all the blitz generate
commands.
This issue is for defining the RPC specification used for Blitz queries and mutations. Each query and mutation can be used by third parties, so the specification must be clear and documented publicly.
This initial specification is my first pass. I haven't thought real deeply about this, so please help me refine it!
This specification is used for both queries and mutations. From an HTTP perspective, there is no difference between a query and mutation because they are both function calls.
HEAD
This will be used to warm the lambda. For example, when a component renders on the client that uses a mutation, it will issue a HEAD
request to the mutation endpoint immediately on page load so that there's a much higher chance the actual mutation request will hit a warm lambda.
200
POST
Used to actually execute the RPC
params
(required) - This will be provided as the first argument to the query or mutationversion
(optional) - String containing the version of the built RPC clientresult
- the exact result returned from the query/mutation function or null
if there was an errorerror
- null
if success, otherwise an object.Example:
// Valid request
{
params: {id: 1},
}
// Success response
{
result: {id: 1, name: "Brandon"},
error: null
}
HTTP Response Code | Response result value |
Response error value |
|
---|---|---|---|
HEAD |
200 | - | - |
Valid request | 200 | - | - |
Invalid request, not JSON | 400 | null |
{message: "Request body is not valid JSON"} |
Invalid request, missing params |
400 | null |
{message: "Request body is missing the 'params' key"} |
Execution success | 200 | Exact value returned from query/mutation function | null |
Execution error | 200 | null |
TBD |
All other HTTP verbs, like GET |
404 | - | - |
Any time you have client-server communication, there is a very real risk of the client & server code becoming out of sync and causing an error. For example, let's say you have a mutation function whose input is a single object. You decide to change the mutation input to be a single array. You make the change and deploy it. Your server code is now updated, but all your users still have the old client code running in the browser. So it's likely you now have client code sending an object to your mutation which is going to error because it's expecting an array.
Now one obvious solution is to only make changes that are backwards compatible, but sometimes you can't do this.
The other solution is to track the version of your client code and your server code. If there is a version mismatch, you can display a friendly message to the user, telling them they need to reload the webpage to continue.
Blitz is well positioned to provide this, so let's do.
versionMismatch
to the error object if execution fails. Then the Blitz dev can check that field in their error handler if they want.In application development to upload a file is a really common thing to do. (Say for a user avatar upload uploading a video etc). Often especially in a serverless context, we then need to stream it on to a third party asset storage solution such as S3 or cloudinary. Some services provide unsigned options but this can lead to security issues. In any case there is a fair bit management required to successfully accept a file in the browser send the file to the server or serverless function parse it to a stream then send it elsewhere. Currently to accomplish this in Blitz we need to create a custom api route and because fetch has no progress event use XmlHTTPRequest or a lib that is based on it to manage the upload process.
On the clientside it might be normal to use something like React Dropzone to get the file ready in the browser or you might use a native file input and assemble it for form submission using the FormData API. In anycase you would manipulate file blobs using the browsers File API.
On the server side You need to use a library such as formidable
or busboy
to handle parsing the file to a stream and then piping that stream to S3 or Cloudinary or to disk. Really what we need to be able to do is write a mutation that can accept file objects in the browser and receive them as streams on the server.
The browser File API is a Blob which has a stream method so we might want to have a similar API on the server and provide any files passed as a mutation's argument properties to be re assembled as File-like objects on the Node end.
import uploadFile from 'app/projects/mutations/uploadFile'
const {getRootProps, getInputProps} = useDropzone({onDrop: (acceptedFiles) => {
// an instant upload in this case
uploadFile({file:acceptedFiles[0]}, {
onUploadProgress(event) {
const uploadedBytes = event.loaded / event.total;
console.log(`Uploaded ${uploadedBytes} bytes`);
}
})
}})
export default async function uploadFile({ file }: UploadFileInput) {
const [uploadStream, donePromise] = getCloudinaryUploadStreamWithDonePromise(file.name)
file.stream().pipe(uploadStream)
await donePromise
return 'ok'
}
To do this we should probably use busboy
on the server as that does not do any intermediate file storage. See article
I'm sure there are many ways we could do this, but here's what I'm thinking:
Essentially, every usage of a query or mutation inside a React component anywhere in the project should be replaced with a fetch call.
App code:
import getProduct from 'app/product/queries/getProduct'
const product = await getProduct({where: {id: 1}})
Built code:
import {rpc} from 'blitz'
const product = await rpc('/api/product/queries/getProduct')({where: {id: 1}})
But query/mutation usage in server side code should not use RPC. It should still use the query/mutation directly
App code:
import {useQuery} from 'blitz'
import getProduct from 'app/product/queries/getProduct'
export const getStaticProps = async context => {
const product = await getProduct({where: {id: context.params?.id}})
return {props: {product}}
}
export default function(props) {
const [product2] = useQuery(getProduct, {where: {id: props.id})
return (
<div>
<h1>{props.product.name}</h1>
</div>
)
}
Built code:
import {useQuery, rpc} from 'blitz'
import getProduct from 'app/product/queries/getProduct'
export const getStaticProps = async context => {
const product = await getProduct({where: {id: context.params?.id}})
return {props: {product}}
}
export default function(props) {
const [product2] = useQuery(rpc('/api/product/queries/getProduct'), {where: {id: props.id})
return (
<div>
<h1>{props.product.name}</h1>
</div>
)
}
I think this is a pretty straightforward babel transformation, right? The only part I'm not sure about is how we can know the file path to the query/mutation from inside the babel transformation. Maybe we first need to compile a manifest file of all queries/mutations that the babel transform can read from.
Upgrade react-query to the latest version.
It's specified in packages/core/package.json
Veterans, please leave for someone new to Blitz/open-source
Currently the AddDependencyExecutor
just blindly installs the version specified in the executor's metadata, which isn't exactly what we want - we want a pinned version.
We should use the same dependency resolution logic we use in the blitz new
command to resolve the latest valid version for each dependency based on the version string passed in (that may require pulling that logic out into a utility package for sharing, or just copy/pasting it.
As I'm working on coding up the new architecture, I'm discovering a bunch of issues with our server package. So I'm listing them here instead of creating a bunch of independant issues.
.blitz/caches/dev/.blitz/caches/dev
UnhandledPromiseRejectionWarning
errorsroutes
dir instead of pages
dirblitz build
and blitz start -p
both failapp/pages
doesn't get copied to the built pages directoryRecently I saw this presentation video of Phoenix LiveView (at 1:40)and found interesting that the application was able to detect that a migration was not run and asked to do it right from the error page
We need to add an integration test for the recipe installer to prevent bugs like blitz-js/legacy-framework#966
Our current generators are all untested. Since these are such core logic, that's pretty scary. We should write some tests that run the generators and verify that the output file structure and file contents match what we're expecting.
For file contents we should be able to use jest snapshots, for file structure, simple string matching should suffice.
We need to define exactly how queries and mutations are used in Blitz app code.
getStaticProps
) should directly use the query/mutation. It should not make an RPC call to another lambda// routes/product/[id]/edit.tsx
import {useQuery, Router} from 'blitz'
import getProduct from '/app/products/queries/getProduct'
import updateProduct from '/app/products/mutations/updateProduct'
import {Formik} from 'formik'
export default function(props) {
const [product] = useQuery(getProduct, {where: {id: props.query.id}})
return (
<div>
<h1>{product.name}</h1>
<Formik
initialValues={product}
onSubmit={async values => {
try {
const {id} = await updateProduct(values)
Router.push('/products/[id]', `/products/${id}`)
} catch (error) {
alert('Error saving product')
}
}}>
{({handleSubmit}) => <form onSubmit={handleSubmit}></form>}
</Formik>
</div>
)
}
Blitz provides a useQuery
hook. The first argument is a query function. The second argument is the input to the query function.
react-query
under the hood for implementing this hook.import getProduct from '/app/products/queries/getProduct'
export default function(props: {query: {id: number}}) {
const [product] = useQuery(getProduct, {where: {id: props.query.id}})
return <div>{product.name}</div>
}
In server-side code, a query function can be called directly without useQuery
import getProduct from '/app/products/queries/getProduct'
export const getStaticProps = async context => {
const product = await getProduct({where: {id: context.params?.id}})
return {props: {product}}
}
export default function({product}) {
return <div>{product.name}</div>
}
["/api/products/queries/getProduct", {where: {id: props.query.id}]
Blitz apps have concurrent mode enabled by default, so suspense for data fetching is also our default.
Our current plan is to not support SSR for useQuery
. We're hoping the official new concurrent mode SSR renderer is out within a few months.
useQuery
The second query will not execute until the second function argument can be called without throwing.
const [user] = useQuery(getUser, {where: {id: props.query.id}})
const [products] = useQuery(getProducts, () => ({where: {userId: user.id}}))
Both of the following will cache the getProduct
query.
const product = await getProduct({where: {id: props.id}})
<button onMouseEnter={() => getProduct({where: {id: productId}})}>
View Product
</button>
Use the usePaginatedQuery
hook
import {Suspense, useState} from 'react'
import {Link, BlitzPage, usePaginatedQuery} from 'blitz'
import getProducts from 'app/products/queries/getProducts'
const ITEMS_PER_PAGE = 3
const Products = () => {
const [page, setPage] = useState(0)
const [products] = usePaginatedQuery(getProducts, {
skip: ITEMS_PER_PAGE * page,
first: ITEMS_PER_PAGE,
})
return (
<div>
{products.map((product) => (
<p key={product.id}>
<Link href="/products/[handle]" as={`/products/${product.handle}`}>
<a>{product.name}</a>
</Link>
</p>
))}
<button disabled={page === 0} onClick={() => setPage(page - 1)}>
Previous
</button>
<button disabled={products.length !== ITEMS_PER_PAGE} onClick={() => setPage(page + 1)}>
Next
</button>
</div>
)
}
const Page: BlitzPage = function () {
return (
<div>
<h1>Products - Paginated</h1>
<Suspense fallback={<div>Loading...</div>}>
<Products />
</Suspense>
</div>
)
}
export default Page
import { useInfiniteQuery } from 'blitz'
import getProducts from '/app/products/queries/getProducts'
function Products(props) {
const [
groupedProducts,
{
isFetching,
isFetchingMore,
fetchMore,
canFetchMore,
}
] = useInfiniteQuery(
getProducts,
(page = {first: 100, skip: 0}) => ({where: {storeId: props.storeId}, ...page}),
{
getFetchMore: (lastGroup, allGroups) => lastGroup.nextPage,
}
)
return (
<>
{groupedProducts.map((group, i) => (
<React.Fragment key={i}>
{group.products.map(product => (
<p key={product.id}>{product.name}</p>
))}
</React.Fragment>
))}
<div>
<button
onClick={() => fetchMore()}
disabled={!canFetchMore || isFetchingMore}
>
{isFetchingMore
? 'Loading more...'
: canFetchMore
? 'Load More'
: 'Nothing more to load'}
</button>
</div>
<div>{isFetching && !isFetchingMore ? 'Fetching...' : null}</div>
</>
)
}
Queries can be used for SSR with the ssrQuery
function. ssrQuery
will run the appropriate middleware.
import {ssrQuery} from 'blitz'
import getProduct from '/app/products/queries/getProduct'
export const getServerSideProps = async ({params, req, res}) => {
const product = await ssrQuery(getProduct, {where: {id: params.id}}, {req, res}))
return {props: {product}}
}
export default function({product}) {
return <div>{product.name}</div>
}
If a user leaves your application and returns to stale data, you usually want to trigger an update in the background to update any stale queries. Blitz useQuery will do this automatically for you, but there will be an option to disable it.
I think optimistic updates should very rarely be used. The UX for end-users is very tricky to get right with optimistic updates.
Therefore, I think optimistic updates should not have first-class support in Blitz. If users need this, they can use directly use react-query or some other method.
We accept any react-query config item as the third argument to useQuery
.
These include refetchInterval
, initialData
, retry
, retryDelay
, cacheTime
, etc.
Mutations are called directly. There is no useMutation
hook like react-query has.
import updateProduct from '/app/products/mutations/updateProduct'
try {
const product = await updateProduct({name: 'something'})
} catch (error) {
alert('Error saving product')
}
refetch
mutate
Router.push
and Router.replace
accepts a refetchQueries
option, that when true, will cause all queries on the destination page to be invalidated.export default function(props: {query: {id: number}}) {
const [product, {mutate}] = useQuery(getProduct, {where: {id: props.query.id}})
return (
<Formik
initialValues={product}
onSubmit={async values => {
try {
const {id} = await updateProduct(values)
Router.push('/products/[id]', `/products/${id}`, {refetchQueries: true})
} catch (error) {
alert('Error saving product')
}
}}>
{/* ... */}
</Formik>
)
}
refetch
useQuery
returns a refetch
function you can use to trigger a query reloadexport default function(props: {query: {id: number}}) {
const [product, {refetch}] = useQuery(getProduct, {where: {id: props.query.id}})
return (
<Formik
initialValues={product}
onSubmit={async values => {
try {
const product = await updateProduct(values)
refetch()
} catch (error) {
alert('Error saving product')
}
}}>
{/* ... */}
</Formik>
)
}
mutate
useQuery
returns a mutate
function you can use to manually update the cachemutate
will automatically trigger a refetch for the initial query to ensure it has the correct datamutate
will be fully typed for Typescript usageexport default function(props: {query: {id: number}}) {
const [product, {mutate}] = useQuery(getProduct, {where: {id: props.query.id}})
return (
<Formik
initialValues={product}
onSubmit={async values => {
try {
const product = await updateProduct(values)
mutate(product)
} catch (error) {
alert('Error saving product')
}
}}>
{/* ... */}
</Formik>
)
}
There's a several second delay after starting blitz console
where it is unresponsive.
The loading spinner appears saying "loading code" which is fine and correct, but after that finishes and the prompt appears, it's frozen.
Maybe there's something being loaded after the spinner is marked as success?
blitz console
0.12.0
Currently there's a awkward pause during blitz new
after dependencies are installed. It makes the user thing something is broken.
However, it's just prettier running. So we need to add a spinner so the user knows what is happening.
Right here is the code where prettier runs
You need to add something like this:
const spinner = log.spinner(log.withBrand('Formatting your code')).start()
// on success
spinner.succeed()
// on error
spinner.fail(chalk.yellow.bold("We had an error running Prettier, but don't worry your app will still run fine :)"))
Veterans, please leave this for someone new to Blitz or open-source :)
Note: I'm not 100% sure about adding this feature, but I wanted to open an issue for it so we can have a discussion about it.
I think the mutate()
function (#582) might be better for most cases so that the cache is instantly updated with latest data instead of being totally cleared.
It's a very common pattern to have an edit form on /[id]/new
or /[id]/edit
. And after a successful submission, the user is redirected to /[id]
or /index
.
This feature enables automatic cache invalidation in that case so the user doesn't have to manually call refetch()
or mutate()
.
Router.push
and Router.replace
accepts a invalidateQueries
option, that when true, will cause all queries on the destination page to be invalidated so that stale data will not display.useRouter()
and withRouter()
import {Router} from 'blitz'
export default function (props: {query: {id: number}}) {
const [product] = useQuery(getProduct, {where: {id: props.query.id}})
return (
<Formik
initialValues={product}
onSubmit={async (values) => {
try {
const {id} = await updateProduct(values)
Router.push("/products/[id]", `/products/${id}`, {invalidateQueries: true})
} catch (error) {
alert("Error saving product")
}
}}>
{/* ... */}
</Formik>
)
}
We need to add custom wrappers around Router
, useRouter
and withRouter
that Next provides.
What do you want and why do you want it?.
Currently, modifying existing files in the CLI always triggers the conflict checker. This means that generation is restricted to new files, as any line additions to existing files will require manual resolution by the user. We should have a way to temporarily disable the conflict checker for generation commands that will be manually appending to files
Possible implementation(s)
I see two avenues.
UnsafeGenerator
abstract class, which will be extended by Generator
. Generators that are yeoman generators will continue to inherit from Generator
, and generators that need to manually write output to existing files can use UnsafeGenerator
ConflictChecker
to disable its checks temporarily. Generator
can expose disableConflictChecker
and enableConfictChecker
methods which subclasses can call inline to disable the checker. This has the added benefit of a more explicit API, but it means the barrier to disabling the conflict checker is much lower, which I'm not sure we want.The below should print the blitz help information, saying that this command is not supported.
~/c/b/e/store> yarn blitz hacker
yarn run v1.19.1
$ /Users/b/c/blitz/node_modules/.bin/blitz hacker
DEBUG: monorepo
Path: /Users/b/c/blitz/node_modules/@blitzjs/cli
(node:87628) TypeError Plugin: @blitzjs/cli: Cannot read property 'dim' of undefined
module: @oclif/[email protected]
task: runHook command_not_found
plugin: @blitzjs/cli
root: /Users/b/c/blitz/packages/cli
See more details with DEBUG=*
› Error: command hacker not found
error Command failed with exit code 2.
Services like render have different memory limits for build vs run step. So we need build the code in the build step and then only run the code in the run step. But currently blitz start --production
rebuilds the code even if the code is already built.
We could add a new command or command argument.
Or
We can change blitz start --production
to use the built assets if they exist. We would add some type of checksum that makes it easy to verify that the code has not changed since the code was built. If the code has changed, then we run a new build.
I really think we should do the later.
cc @ryardley thoughts on this?
Starting blitz console
has a long hangup time after "Loading your Code" is finished and before you can start typing.
It gets much worse as you add more queries and mutations.
blitz new myapp
cd myapp
blitz g resource project
blitz g resource task
blitz g resource comment
blitz db migrate
blitz console
>
prompt appears. Notice that you can't type for a bit of time.The main problem is loadBlitz
, which for some reason is super slow on first start. Running .reload
after initial start (which re-runs loadBlitz) is quite fast.
You can add a log statement here and see how slow it is for each module.
We are already using a lazy require loader which apparently isn't working here.
Probably we need to add a proxy object for each dependency, that only loads the module when it's accessed. But maybe there's a better way.
See #1098 (comment)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.