cypress-io / cypress

Fast, easy and reliable testing for anything that runs in a browser.
https://cypress.io
MIT License
46.79k stars 3.17k forks source link

esbuild-dev-server #21034

Open vospascal opened 2 years ago

vospascal commented 2 years ago

What would you like?

i would love if we could have a esbuild-dev-server

Why is this needed?

wy not its fast

Other

No response

thednp commented 2 years ago

...with code-coverage support would be fantastic :)

fochlac commented 1 year ago

I've set up a basic esbuild dev-server, feel free to check it out/use it/adapt it if you want: https://github.com/fochlac/cypress-devserver-esbuild Could also be the basis for a PR towards this repo

lmiller1990 commented 1 year ago

Nice job figuring out how to implement this @fochlac! We really should document the public API and events in more detail.

@vospascal Vite (and our Vite Dev Server) uses esbuild - that gets you most of the way there. Could you just use that?

What would you expect in an esbuild dev server you don't already get from Vite?

Just to clarify, you want this for Component Testing (which uses a dev server) as opposed to E2E (uses preprocessor architecture, for which many cypress-esbuild-preprocessors already exist).

fochlac commented 1 year ago

While using vite might be a solution for new projects, but for existing projects that use only esbuild with a complex setup it might be helpful to have pure esbuild-solution available, so a single configuration can be used for both (we have a very large project with a few custom plugins).

I'm just trying to write down my learnings from this and adapt the dev-server documentation as more detailed docs would have been really helpful when approaching this, see https://github.com/cypress-io/cypress-documentation/pull/5211.

lmiller1990 commented 1 year ago

@fochlac thanks for the docs PR - I will give it a review.

I think the first step for esbuild in Cypress is to have a good third party dev server - third parties can move faster and try things when compared to our core offer, which has to go through a more thorough process. If it gains enough traction, we can definitely assess adding this as part of core in the future.

fochlac commented 1 year ago

I agree, and I will maintain the esbuild-dev-server for now. I guess the most important step is actually having a decent documentation, since with the right knowledge it's actually fairly easy to build a custom dev-server yourself. Especially since you do not need to consider varied use-cases (i.e. esm-bundles with seperate css file vs css in js solutions like styled elements), which make it much harder to develop a generic solution.

Perhaps it might be more helpful to further simplify the API for creating custom dev-servers in the long term. The most complex part is the communication between the index.html and the file-server. Something along the lines of would be much easier to use:

const { defineConfig, createCustomDevServer } = require("cypress");
const { readFile } = require("fs/promises");
const { startBuildToolInWatchMode, customJsMapper, customCssMapper, getSupportFilePath, getOutputFolder } = require('./my-stuff')

module.exports = defineConfig({
    component: {
        devServer: createCustomDevServer(async ({ onBuildComplete, specs, supportFile, serveStatic }) => {
            let stop = startBuildToolInWatchMode(specs, supportFile, onBuildComplete)
            serveStatic(getOutputFolder())
            return {
                loadTest: async (relativeTestPath, { injectHtml, loadBundle, loadSupportFile }) => {
                    const testPath = customJsMapper(relativeTestPath)
                    const cssPath = customCssMapper(relativeTestPath)

                    const testBundle = readFile(testPath, {encoding: 'utf8'})
                    const testCss = readFile(cssPath, {encoding: 'utf8'})

                    loadSupportFile(await readFile(getSupportFilePath(), {encoding: 'utf8'}))
                    loadBundle(await testBundle)
                    injectHtml(`<style>${await testCss}</style>`, 'head')
                },
                onSpecChange: (newSpecs) => {
                    stop()
                    stop = startBuildToolInWatchMode(newSpecs, onBuildComplete)
                },
                devServerPort: 0
            }
        })
    }
})

Would make a custom implementation much more straightforward. A fully implemented esbuild dev server would look like this:

const { defineConfig, createCustomDevServer } = require("cypress");
const { context } = require("esbuild");
const { readFile } = require("fs/promises");

module.exports = defineConfig({
    component: {
        devServer: createCustomDevServer(async ({ onBuildComplete, onBuildStart, specs, supportFilePath, serveStatic }) => {
            const createEsbuildConfig = (specs) => ({
                entryPoints: [
                    ...specs.map(spec => spec.absolute),
                    supportFilePath
                ],
                outdir: './dist',
                outBase: './',
                plugins: [{
                    name: 'watch',
                    setup(build) {
                        build.onStart(onBuildStart)
                        build.onEnd(onBuildComplete)
                    }
                }]
            })
            serveStatic('./dist')
            let ctx = await context(createEsbuildConfig(specs))
            ctx.watch()

            return {
                loadTest: async (relativeTestPath, { loadBundle, loadSupportFile }) => {
                    const testPath = path.resolve('./dist', relativeTestPath)
                    const supportPath = path.resolve('./dist', supportFilePath)
                    const testBundle = readFile(testPath, {encoding: 'utf8'})
                    const supportBundle = readFile(supportPath, {encoding: 'utf8'})

                    loadSupportFile(await supportBundle)
                    loadBundle(await testBundle)
                },
                onSpecChange: async (newSpecs) => {
                    ctx.dispose()
                    ctx = await context(createEsbuildConfig(newSpecs))
                    ctx.watch()
                },
                devServerPort: 0
            }
        })
    }
})
fochlac commented 1 year ago

Here we go: https://github.com/fochlac/cypress-ct-custom-devserver

vospascal commented 1 year ago

Think current vite solution is good enough at least for me only thing I struggle with it combining vitest and vite coverages to one big one other then that is perfect I mean I can merge them that works fine with a custom script but the lines don’t add up correctly one should compliment the other

lmiller1990 commented 1 year ago

I'm not sure on the combining coverage issue - it should be just a matter of getting all the tools to work together, istanbul/nyc is always a pain to wrangle for some reason...

@fochlac your API looks 🔥 - main reason we never had one was when we developed the original one (webpack) we didn't really know what it would look like - now we've got a good idea of how things should look, we could definitely revisit the API, but this seems like a big breaking change, so it wouldn't be something we can take lightly.

For now, I agree the best approach is good documentation. You could potentially implement your API in useland and wire it up to our event based API.

There's also some other edge cases that would need solving if we want esbuild-dev-server to be part of the core offering, namely ESM with cy.stub and cy.spy. There is https://github.com/cypress-io/cypress/issues/22355 that explains this a bit more. We have an experimental plugin for Vite to facilitate ESM stubbing https://github.com/cypress-io/cypress/pull/26536. I'm guessing you will also have the same issue with esbuild-dev-server - ES modules are sealed by default, so sinon cannot modify them (which is how cy.stub and cy.spy work).

fochlac commented 1 year ago

There's also some other edge cases that would need solving if we want esbuild-dev-server to be part of the core offering, namely ESM with cy.stub and cy.spy. There is https://github.com/cypress-io/cypress/issues/22355 that explains this a bit more. We have an experimental plugin for Vite to facilitate ESM stubbing https://github.com/cypress-io/cypress/pull/26536. I'm guessing you will also have the same issue with esbuild-dev-server - ES modules are sealed by default, so sinon cannot modify them (which is how cy.stub and cy.spy work).

Hmm, I guess that can be a problem. It would be quite straightforward to build a esbuild-plugin that works in the same way as your vite-plugin, since we could reuse it mostly 1:1. However, such plugins are usually the choke-point regarding speed for esbuild (propably the same for vite). Once your plugin is ready for production I might have a another look and port it to the esbuild-dev-server. Since I set up the index.html slightly different and you include client code I will propably have to couple that functionality more tightly with the dev-server. Luckily we wont need it in my company, since we don't mock modules and only use cy.spy() to create mock-Functions to be passed into components as prop.

You could potentially implement your API in useland and wire it up to our event based API.

I created a lib that implements that api. It uses the existing api and just sets up a generic (express)-server and takes care of the communication with the ui. No need to reverse engineer the event based api for now. ^^

lmiller1990 commented 1 year ago

That was quick ⚡

I will leave this issue open as a feature request, let's see how much traction it gets. Thanks for the docs and implementation - I may give it a try on my next project 💯