Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support @nuxthub/core #934

Open
2 of 4 tasks
hannoeru opened this issue Sep 5, 2024 · 2 comments
Open
2 of 4 tasks

Support @nuxthub/core #934

hannoeru opened this issue Sep 5, 2024 · 2 comments
Labels
enhancement New feature or request pending triage

Comments

@hannoeru
Copy link

hannoeru commented Sep 5, 2024

Describe the feature

NuxtHub enforce build to cloudflare but test-utils only refer to node build ___location, so if I use setup() with build: true it will resolve to error, for the workaround now is using host that point to the dev server that can handle NuxtHub related features.

Additional information

  • Would you be willing to help implement this feature?
  • Could this feature be implemented as a module?

Final checks

Copy link

dosubot bot commented Sep 5, 2024

Hey @hannoeru! 👋 I'm here to help you with bug fixes, questions, and becoming a contributor. Let's squash those bugs together!

Yes, the feature to enforce NuxtHub build to Cloudflare while test-utils refer to the node build ___location can be implemented as a module. You can use the defineNuxtModule function from the @nuxt/kit package to create a custom module that configures the build process and test-utils settings accordingly.

Here is an example of how you might implement such a module:

import { defineNuxtModule, addVitePlugin, createResolver, resolvePath } from '@nuxt/kit'
import { mergeConfig } from 'vite'
import { defu } from 'defu'
import { getVitestConfigFromNuxt } from './config'
import { setupImportMocking } from './module/mock'
import { NuxtRootStubPlugin } from './module/plugins/entry'

export interface CustomModuleOptions {
  cloudflareBuild?: boolean
  vitestConfig?: VitestConfig
}

export default defineNuxtModule<CustomModuleOptions>({
  meta: {
    name: 'custom-module',
    configKey: 'customModule',
  },
  defaults: {
    cloudflareBuild: false,
  },
  async setup(options, nuxt) {
    if (options.cloudflareBuild) {
      // Enforce NuxtHub build to Cloudflare
      nuxt.options.target = 'static'
      nuxt.options.generate = {
        fallback: true,
        dir: 'dist',
      }
    }

    if (nuxt.options.test || nuxt.options.dev) {
      setupImportMocking()
    }

    const resolver = createResolver(import.meta.url)
    addVitePlugin(NuxtRootStubPlugin.vite({
      entry: await resolvePath('#app/entry', { alias: nuxt.options.alias }),
      rootStubPath: await resolvePath(resolver.resolve('./runtime/nuxt-root')),
    }))

    if (!nuxt.options.test && !nuxt.options.dev) {
      nuxt.options.vite.define ||= {}
      nuxt.options.vite.define['import.meta.vitest'] = 'undefined'
    }

    nuxt.hook('prepare:types', ({ references }) => {
      references.push({ types: 'vitest/import-meta' })
    })

    if (!nuxt.options.dev) return

    if (process.env.TEST || process.env.VITE_TEST) return

    const rawViteConfigPromise = new Promise<ViteConfig>((resolve) => {
      nuxt.hook('app:resolve', () => {
        nuxt.hook('vite:configResolved', (config, { isClient }) => {
          if (isClient) resolve(config)
        })
      })
    })

    async function start() {
      const rawViteConfig = mergeConfig({}, await rawViteConfigPromise)
      const viteConfig = await getVitestConfigFromNuxt({ nuxt, viteConfig: defu({ test: options.vitestConfig }, rawViteConfig) })

      viteConfig.plugins = (viteConfig.plugins || []).filter((p) => {
        return !p || !('name' in p) || !vitePluginBlocklist.includes(p.name)
      })

      process.env.__NUXT_VITEST_RESOLVED__ = 'true'
      const { startVitest } = (await import(pathToFileURL(await resolvePath('vitest/node')).href)) as typeof import('vitest/node')

      const customReporter: Reporter = {
        onInit(_ctx) {
          ctx = _ctx
        },
        onTaskUpdate() {
          testFiles = ctx.state.getFiles()
          updateTabs()
        },
        onFinished() {
          testFiles = ctx.state.getFiles()
          updateTabs()
        },
      }

      const watchMode = !process.env.NUXT_VITEST_DEV_TEST && !isCI

      const PORT = await getPort({ port: 15555 })
      const PROTOCOL = nuxt.options.devServer.https ? 'https' : 'http'
      const URL = `${PROTOCOL}://localhost:${PORT}/__vitest__/`

      const overrides: VitestConfig = watchMode
        ? {
            passWithNoTests: true,
            reporters: options.logToConsole
              ? [
                  ...toArray(options.vitestConfig?.reporters ?? ['default']),
                  customReporter,
                ]
              : [customReporter],
            watch: true,
            ui: true,
            open: false,
            api: {
              port: PORT,
            },
          }
        : { watch: false }

      const promise = startVitest('test', [], defu(overrides, viteConfig.test), viteConfig)
      promise.catch(() => process.exit(1))

      if (watchMode) {
        logger.info(`Vitest UI starting on ${URL}`)
        nuxt.hook('close', () => promise.then(v => v?.close()))
        await new Promise(resolve => setTimeout(resolve, 1000))
      } else {
        promise.then(v => nuxt.close().then(() => v?.close()).then(() => process.exit()))
      }

      loaded = true
    }

    nuxt.hook('devtools:customTabs', (tabs) => {
      const failedCount = testFiles?.filter(f => f.result?.state === 'fail').length ?? 0
      const passedCount = testFiles?.filter(f => f.result?.state === 'pass').length ?? 0
      const totalCount = testFiles?.length ?? 0

      tabs.push({
        title: 'Vitest',
        name: 'vitest',
        icon: 'logos-vitest',
        view: loaded
          ? {
              type: 'iframe',
              src: URL,
            }
          : {
              type: 'launch',
              description: 'Start tests along with Nuxt',
              actions: [
                {
                  label: promise ? 'Starting...' : 'Start Vitest',
                  pending: !!promise,
                  handle: () => {
                    promise = promise || start()
                    return promise
                  },
                },
              ],
            },
        extraTabVNode: totalCount
          ? h('div', { style: { color: failedCount ? 'orange' : 'green' } }, [
            h('span', {}, passedCount),
            h('span', { style: { opacity: '0.5', fontSize: '0.9em' } }, '/'),
            h('span', { style: { opacity: '0.8', fontSize: '0.9em' } }, totalCount),
          ])
          : undefined,
      })
    })

    if (options.startOnBoot) {
      promise = promise || start()
      promise.then(updateTabs)
    }
  },
})

function toArray<T>(value: T | T[]): T[] {
  return Array.isArray(value) ? value : [value]
}

This module sets up the build process for Cloudflare if the cloudflareBuild option is enabled and configures the test-utils to refer to the node build ___location. The getVitestConfigFromNuxt function from the test-utils package is used to obtain the Vitest configuration tailored for Nuxt, ensuring that the test environment is correctly set up [1][2].

To continue talking to Dosu, mention @dosu.

@dosubot dosubot bot added the enhancement New feature or request label Sep 5, 2024
@hannoeru
Copy link
Author

hannoeru commented Sep 5, 2024

@atinux Can you help me to point out more details about this issue? Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request pending triage
Projects
None yet
Development

No branches or pull requests

1 participant