Skip to content

fix: monkey-patch Vite HMR issue & make it reproducible (#464) #496

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

Merged
merged 3 commits into from
Jul 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,27 @@
"dependencies": {
"@nuxt/kit": "^3.0.0-rc.4",
"@nuxt/postcss8": "^1.1.3",
"@types/tailwindcss": "^3.0.10",
"@types/tailwindcss": "^3.0.11",
"autoprefixer": "^10.4.7",
"chalk": "^5.0.1",
"clear-module": "^4.1.2",
"consola": "^2.15.3",
"defu": "^6.0.0",
"postcss": "^8.4.14",
"postcss-custom-properties": "^12.1.8",
"postcss-nesting": "^10.1.8",
"postcss-nesting": "^10.1.10",
"tailwind-config-viewer": "^1.7.1",
"tailwindcss": "^3.1.4",
"ufo": "^0.8.4"
"tailwindcss": "^3.1.5",
"ufo": "^0.8.5"
},
"devDependencies": {
"@fontsource/inter": "^4.5.11",
"@nuxt/module-builder": "latest",
"@nuxt/test-utils": "latest",
"@nuxtjs/eslint-config-typescript": "latest",
"codecov": "latest",
"eslint": "latest",
"nuxt": "^3.0.0-rc.4",
"nuxt": "npm:nuxt3@latest",
"standard-version": "latest"
}
}
14 changes: 0 additions & 14 deletions playground/app.vue

This file was deleted.

10 changes: 9 additions & 1 deletion playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,13 @@ export default defineNuxtConfig({
],
tailwindcss: {
exposeConfig: true
}
},
css: [
// Including Inter CSS is the first component to reproduce HMR issue. It also causes playground to look better,
// since Inter is a native font for Tailwind UI
'@fontsource/inter/400.css',
'@fontsource/inter/500.css',
'@fontsource/inter/600.css',
'@fontsource/inter/700.css'
]
})
25 changes: 25 additions & 0 deletions playground/pages/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<template>
<div>
<div>
<CallToAction />
</div>
<div class="max-w-screen-lg p-4 mx-auto space-y-4">
<div>
<span class="pr-1 font-medium">This is a HMR test, try changing the color:</span>
<span class="text-indigo-500">meow!</span>
</div>
<div>
<span class="text-sm font-semibold text-gray-700">Resolved tailwind config:</span>
<textarea
class="p-4 block text-sm border border-gray-100 rounded shadow w-full h-[32rem] font-mono outline-none"
readonly
:value="JSON.stringify(tailwindConfig, null, 2)"
/>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import tailwindConfig from '#tailwind-config'
</script>
9 changes: 9 additions & 0 deletions playground/tailwind.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default {
theme: {
extend: {
fontFamily: {
sans: 'Inter, ui-sans-serif, system-ui, -apple-system, Arial, Roboto, sans-serif'
}
}
}
}
6 changes: 0 additions & 6 deletions playground/tailwind.config.ts

This file was deleted.

40 changes: 40 additions & 0 deletions src/hmr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { isAbsolute, resolve } from 'path'
import { HmrContext, Plugin } from 'vite'
import minimatch from 'minimatch'

export default function (tailwindConfig, rootDir: string, cssPath: string): Plugin {
const resolvedContent: string[] = tailwindConfig.content.map(f => !isAbsolute(f) ? resolve(rootDir, f) : f)

return {
name: 'nuxt:tailwindcss',
handleHotUpdate (ctx: HmrContext): void {
if (resolvedContent.findIndex(c => minimatch(ctx.file, c)) === -1) {
return
}

const extraModules = ctx.server.moduleGraph.getModulesByFile(cssPath)
const timestamp = +Date.now()

for (const mod of extraModules) {
// This will invalidate Vite cache (e.g. next page reload will be fine), but won't help with HMR on its own
ctx.server.moduleGraph.invalidateModule(mod, undefined, timestamp)
}

// Surely, this is not the best way to fix that, especially given direct `send` call bypassing all Vite logic.
// But just returning extra modules does not cause Vite to send the update, and I didn't find a way to trigger
// that update manually other than sending it directly

ctx.server.ws.send({
type: 'update',
updates: Array.from(extraModules).map((mod) => {
return {
type: mod.type === 'js' ? 'js-update' : 'css-update',
path: mod.url,
acceptedPath: mod.url,
timestamp
}
})
})
}
}
};
24 changes: 19 additions & 5 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import {
requireModule,
isNuxt2,
createResolver,
resolvePath
resolvePath,
addVitePlugin
} from '@nuxt/kit'
import { name, version } from '../package.json'
import vitePlugin from './hmr'
import defaultTailwindConfig from './tailwind.config'

const logger = consola.withScope('nuxt:tailwindcss')
Expand All @@ -34,25 +36,32 @@ export default defineNuxtModule({
config: defaultTailwindConfig(nuxt.options),
viewer: true,
exposeConfig: false,
injectPosition: 0
injectPosition: 0,
disableHmrHotfix: false
}),
async setup (moduleOptions, nuxt) {
const configPath = await resolvePath(moduleOptions.configPath)
const cssPath = await resolvePath(moduleOptions.cssPath, { extensions: ['.css', '.sass', '.scss', '.less', '.styl'] })
const injectPosition = ~~Math.min(moduleOptions.injectPosition, (nuxt.options.css || []).length + 1)

// Include CSS file in project css
let resolvedCss: string
if (typeof cssPath === 'string') {
if (existsSync(cssPath)) {
logger.info(`Using Tailwind CSS from ~/${relative(nuxt.options.srcDir, cssPath)}`)
nuxt.options.css.splice(injectPosition, 0, cssPath)
resolvedCss = cssPath
} else {
logger.info('Using default Tailwind CSS file from runtime/tailwind.css')
const resolver = createResolver(import.meta.url)
nuxt.options.css.splice(injectPosition, 0, resolver.resolve('runtime/tailwind.css'))
resolvedCss = createResolver(import.meta.url).resolve('runtime/tailwind.css')
}
}

// Inject only if this file isn't listed already by user (e.g. user may put custom path both here and in css):
const resolvedNuxtCss = await Promise.all(nuxt.options.css.map(p => resolvePath(p)))
if (!resolvedNuxtCss.includes(resolvedCss)) {
nuxt.options.css.splice(injectPosition, 0, resolvedCss)
}

// Extend the Tailwind config
let tailwindConfig: any = {}
if (existsSync(configPath)) {
Expand Down Expand Up @@ -111,6 +120,11 @@ export default defineNuxtModule({
await installModule('@nuxt/postcss8')
}

if (nuxt.options.dev && !moduleOptions.disableHmrHotfix) {
// Insert Vite plugin to work around HMR issue
addVitePlugin(vitePlugin(tailwindConfig, nuxt.options.rootDir, resolvedCss))
}

// Add _tailwind config viewer endpoint
if (nuxt.options.dev && moduleOptions.viewer) {
const route = '/_tailwind'
Expand Down
Loading