Skip to content

Commit cdb3b98

Browse files
Add support for new loadModule and loadStylesheet APIs from v4 (#317)
* Refactor * Use `enhanced-resolve` to load files * Add support for new loadModule and loadStylesheet APIs
1 parent 15e8009 commit cdb3b98

File tree

4 files changed

+134
-31
lines changed

4 files changed

+134
-31
lines changed

package-lock.json

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"ast-types": "^0.14.2",
4444
"clear-module": "^4.1.2",
4545
"cpy-cli": "^5.0.0",
46+
"enhanced-resolve": "^5.17.1",
4647
"esbuild": "^0.19.8",
4748
"escalade": "^3.1.1",
4849
"import-sort-style-module": "^6.0.0",

src/config.ts

Lines changed: 75 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// @ts-check
22
import * as fs from 'fs/promises'
3-
import { createRequire } from 'module'
43
import * as path from 'path'
54
import { pathToFileURL } from 'url'
65
import clearModule from 'clear-module'
@@ -18,11 +17,9 @@ import loadConfigFallback from 'tailwindcss/loadConfig'
1817
import resolveConfigFallback from 'tailwindcss/resolveConfig'
1918
import type { RequiredConfig } from 'tailwindcss/types/config.js'
2019
import { expiringMap } from './expiring-map.js'
21-
import { resolveFrom, resolveIn } from './resolve'
20+
import { resolveCssFrom, resolveJsFrom } from './resolve'
2221
import type { ContextContainer } from './types'
2322

24-
let localRequire = createRequire(import.meta.url)
25-
2623
let sourceToPathMap = new Map<string, string | null>()
2724
let sourceToEntryMap = new Map<string, string | null>()
2825
let pathToContextMap = expiringMap<string | null, ContextContainer>(10_000)
@@ -107,7 +104,7 @@ async function loadTailwindConfig(
107104
let tailwindConfig: RequiredConfig = { content: [] }
108105

109106
try {
110-
let pkgFile = resolveIn('tailwindcss/package.json', [baseDir])
107+
let pkgFile = resolveJsFrom(baseDir, 'tailwindcss/package.json')
111108
let pkgDir = path.dirname(pkgFile)
112109

113110
try {
@@ -151,29 +148,40 @@ async function loadTailwindConfig(
151148
* Create a loader function that can load plugins and config files relative to
152149
* the CSS file that uses them. However, we don't want missing files to prevent
153150
* everything from working so we'll let the error handler decide how to proceed.
154-
*
155-
* @param {object} param0
156-
* @returns
157151
*/
158152
function createLoader<T>({
153+
legacy,
159154
filepath,
160155
onError,
161156
}: {
157+
legacy: boolean
162158
filepath: string
163-
onError: (id: string, error: unknown) => T
159+
onError: (id: string, error: unknown, resourceType: string) => T
164160
}) {
165-
let baseDir = path.dirname(filepath)
166161
let cacheKey = `${+Date.now()}`
167162

168-
return async function loadFile(id: string) {
163+
async function loadFile(id: string, base: string, resourceType: string) {
169164
try {
170-
let resolved = resolveFrom(baseDir, id)
165+
let resolved = resolveJsFrom(base, id)
166+
171167
let url = pathToFileURL(resolved)
172168
url.searchParams.append('t', cacheKey)
173169

174170
return await import(url.href).then((m) => m.default ?? m)
175171
} catch (err) {
176-
return onError(id, err)
172+
return onError(id, err, resourceType)
173+
}
174+
}
175+
176+
if (legacy) {
177+
let baseDir = path.dirname(filepath)
178+
return (id: string) => loadFile(id, baseDir, 'module')
179+
}
180+
181+
return async (id: string, base: string, resourceType: string) => {
182+
return {
183+
base,
184+
module: await loadFile(id, base, resourceType),
177185
}
178186
}
179187
}
@@ -184,7 +192,8 @@ async function loadV4(
184192
entryPoint: string | null,
185193
) {
186194
// Import Tailwind — if this is v4 it'll have APIs we can use directly
187-
let pkgPath = resolveIn('tailwindcss', [baseDir])
195+
let pkgPath = resolveJsFrom(baseDir, 'tailwindcss')
196+
188197
let tw = await import(pathToFileURL(pkgPath).toString())
189198

190199
// This is not Tailwind v4
@@ -195,15 +204,63 @@ async function loadV4(
195204
// If the user doesn't define an entrypoint then we use the default theme
196205
entryPoint = entryPoint ?? `${pkgDir}/theme.css`
197206

207+
let importBasePath = path.dirname(entryPoint)
208+
198209
// Resolve imports in the entrypoint to a flat CSS tree
199210
let css = await fs.readFile(entryPoint, 'utf-8')
200-
let resolveImports = postcss([postcssImport()])
201-
let result = await resolveImports.process(css, { from: entryPoint })
211+
212+
// Determine if the v4 API supports resolving `@import`
213+
let supportsImports = false
214+
try {
215+
await tw.__unstable__loadDesignSystem('@import "./empty";', {
216+
loadStylesheet: () => {
217+
supportsImports = true
218+
return {
219+
base: importBasePath,
220+
content: '',
221+
}
222+
},
223+
})
224+
} catch {}
225+
226+
if (!supportsImports) {
227+
let resolveImports = postcss([postcssImport()])
228+
let result = await resolveImports.process(css, { from: entryPoint })
229+
css = result.css
230+
}
202231

203232
// Load the design system and set up a compatible context object that is
204233
// usable by the rest of the plugin
205-
let design = await tw.__unstable__loadDesignSystem(result.css, {
234+
let design = await tw.__unstable__loadDesignSystem(css, {
235+
base: importBasePath,
236+
237+
// v4.0.0-alpha.25+
238+
loadModule: createLoader({
239+
legacy: false,
240+
filepath: entryPoint,
241+
onError: (id, err, resourceType) => {
242+
console.error(`Unable to load ${resourceType}: ${id}`, err)
243+
244+
if (resourceType === 'config') {
245+
return {}
246+
} else if (resourceType === 'plugin') {
247+
return () => {}
248+
}
249+
},
250+
}),
251+
252+
loadStylesheet: async (id: string, base: string) => {
253+
let resolved = resolveCssFrom(base, id)
254+
255+
return {
256+
base: path.dirname(resolved),
257+
content: await fs.readFile(resolved, 'utf-8'),
258+
}
259+
},
260+
261+
// v4.0.0-alpha.24 and below
206262
loadPlugin: createLoader({
263+
legacy: true,
207264
filepath: entryPoint,
208265
onError(id, err) {
209266
console.error(`Unable to load plugin: ${id}`, err)
@@ -213,6 +270,7 @@ async function loadV4(
213270
}),
214271

215272
loadConfig: createLoader({
273+
legacy: true,
216274
filepath: entryPoint,
217275
onError(id, err) {
218276
console.error(`Unable to load config: ${id}`, err)

src/resolve.ts

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,33 @@
1-
import { createRequire as req } from 'node:module'
2-
import resolveFrom from 'resolve-from'
1+
import fs from 'node:fs'
2+
import { fileURLToPath } from 'node:url'
3+
import { CachedInputFileSystem, ResolverFactory } from 'enhanced-resolve'
34
import { expiringMap } from './expiring-map'
45

5-
const localRequire = req(import.meta.url)
6+
const fileSystem = new CachedInputFileSystem(fs, 30_000)
7+
8+
const esmResolver = ResolverFactory.createResolver({
9+
fileSystem,
10+
useSyncFileSystemCalls: true,
11+
extensions: ['.mjs', '.js'],
12+
mainFields: ['module'],
13+
conditionNames: ['node', 'import'],
14+
})
15+
16+
const cjsResolver = ResolverFactory.createResolver({
17+
fileSystem,
18+
useSyncFileSystemCalls: true,
19+
extensions: ['.js', '.cjs'],
20+
mainFields: ['main'],
21+
conditionNames: ['node', 'require'],
22+
})
23+
24+
const cssResolver = ResolverFactory.createResolver({
25+
fileSystem,
26+
useSyncFileSystemCalls: true,
27+
extensions: ['.css'],
28+
mainFields: ['style'],
29+
conditionNames: ['style'],
30+
})
631

732
// This is a long-lived cache for resolved modules whether they exist or not
833
// Because we're compatible with a large number of plugins, we need to check
@@ -11,17 +36,11 @@ const localRequire = req(import.meta.url)
1136
// failed module resolutions making repeated checks very expensive.
1237
const resolveCache = expiringMap<string, string | null>(30_000)
1338

14-
export function resolveIn(id: string, paths: string[]) {
15-
return localRequire.resolve(id, {
16-
paths,
17-
})
18-
}
19-
2039
export function maybeResolve(name: string) {
2140
let modpath = resolveCache.get(name)
2241

2342
if (modpath === undefined) {
24-
modpath = freshMaybeResolve(name)
43+
modpath = resolveJsFrom(fileURLToPath(import.meta.url), name)
2544
resolveCache.set(name, modpath)
2645
}
2746

@@ -39,12 +58,14 @@ export async function loadIfExists<T>(name: string): Promise<T | null> {
3958
return null
4059
}
4160

42-
function freshMaybeResolve(name: string) {
61+
export function resolveJsFrom(base: string, id: string): string {
4362
try {
44-
return localRequire.resolve(name)
63+
return esmResolver.resolveSync({}, base, id) || id
4564
} catch (err) {
46-
return null
65+
return cjsResolver.resolveSync({}, base, id) || id
4766
}
4867
}
4968

50-
export { resolveFrom }
69+
export function resolveCssFrom(base: string, id: string) {
70+
return cssResolver.resolveSync({}, base, id) || id
71+
}

0 commit comments

Comments
 (0)