Skip to content

Commit

Permalink
Add support for new loadModule and loadStylesheet APIs from v4 (#317
Browse files Browse the repository at this point in the history
)

* Refactor

* Use `enhanced-resolve` to load files

* Add support for new loadModule and loadStylesheet APIs
  • Loading branch information
thecrypticace authored Sep 24, 2024
1 parent 15e8009 commit cdb3b98
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 31 deletions.
23 changes: 23 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"ast-types": "^0.14.2",
"clear-module": "^4.1.2",
"cpy-cli": "^5.0.0",
"enhanced-resolve": "^5.17.1",
"esbuild": "^0.19.8",
"escalade": "^3.1.1",
"import-sort-style-module": "^6.0.0",
Expand Down
92 changes: 75 additions & 17 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// @ts-check
import * as fs from 'fs/promises'
import { createRequire } from 'module'
import * as path from 'path'
import { pathToFileURL } from 'url'
import clearModule from 'clear-module'
Expand All @@ -18,11 +17,9 @@ import loadConfigFallback from 'tailwindcss/loadConfig'
import resolveConfigFallback from 'tailwindcss/resolveConfig'
import type { RequiredConfig } from 'tailwindcss/types/config.js'
import { expiringMap } from './expiring-map.js'
import { resolveFrom, resolveIn } from './resolve'
import { resolveCssFrom, resolveJsFrom } from './resolve'
import type { ContextContainer } from './types'

let localRequire = createRequire(import.meta.url)

let sourceToPathMap = new Map<string, string | null>()
let sourceToEntryMap = new Map<string, string | null>()
let pathToContextMap = expiringMap<string | null, ContextContainer>(10_000)
Expand Down Expand Up @@ -107,7 +104,7 @@ async function loadTailwindConfig(
let tailwindConfig: RequiredConfig = { content: [] }

try {
let pkgFile = resolveIn('tailwindcss/package.json', [baseDir])
let pkgFile = resolveJsFrom(baseDir, 'tailwindcss/package.json')
let pkgDir = path.dirname(pkgFile)

try {
Expand Down Expand Up @@ -151,29 +148,40 @@ async function loadTailwindConfig(
* Create a loader function that can load plugins and config files relative to
* the CSS file that uses them. However, we don't want missing files to prevent
* everything from working so we'll let the error handler decide how to proceed.
*
* @param {object} param0
* @returns
*/
function createLoader<T>({
legacy,
filepath,
onError,
}: {
legacy: boolean
filepath: string
onError: (id: string, error: unknown) => T
onError: (id: string, error: unknown, resourceType: string) => T
}) {
let baseDir = path.dirname(filepath)
let cacheKey = `${+Date.now()}`

return async function loadFile(id: string) {
async function loadFile(id: string, base: string, resourceType: string) {
try {
let resolved = resolveFrom(baseDir, id)
let resolved = resolveJsFrom(base, id)

let url = pathToFileURL(resolved)
url.searchParams.append('t', cacheKey)

return await import(url.href).then((m) => m.default ?? m)
} catch (err) {
return onError(id, err)
return onError(id, err, resourceType)
}
}

if (legacy) {
let baseDir = path.dirname(filepath)
return (id: string) => loadFile(id, baseDir, 'module')
}

return async (id: string, base: string, resourceType: string) => {
return {
base,
module: await loadFile(id, base, resourceType),
}
}
}
Expand All @@ -184,7 +192,8 @@ async function loadV4(
entryPoint: string | null,
) {
// Import Tailwind — if this is v4 it'll have APIs we can use directly
let pkgPath = resolveIn('tailwindcss', [baseDir])
let pkgPath = resolveJsFrom(baseDir, 'tailwindcss')

let tw = await import(pathToFileURL(pkgPath).toString())

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

let importBasePath = path.dirname(entryPoint)

// Resolve imports in the entrypoint to a flat CSS tree
let css = await fs.readFile(entryPoint, 'utf-8')
let resolveImports = postcss([postcssImport()])
let result = await resolveImports.process(css, { from: entryPoint })

// Determine if the v4 API supports resolving `@import`
let supportsImports = false
try {
await tw.__unstable__loadDesignSystem('@import "./empty";', {
loadStylesheet: () => {
supportsImports = true
return {
base: importBasePath,
content: '',
}
},
})
} catch {}

if (!supportsImports) {
let resolveImports = postcss([postcssImport()])
let result = await resolveImports.process(css, { from: entryPoint })
css = result.css
}

// Load the design system and set up a compatible context object that is
// usable by the rest of the plugin
let design = await tw.__unstable__loadDesignSystem(result.css, {
let design = await tw.__unstable__loadDesignSystem(css, {
base: importBasePath,

// v4.0.0-alpha.25+
loadModule: createLoader({
legacy: false,
filepath: entryPoint,
onError: (id, err, resourceType) => {
console.error(`Unable to load ${resourceType}: ${id}`, err)

if (resourceType === 'config') {
return {}
} else if (resourceType === 'plugin') {
return () => {}
}
},
}),

loadStylesheet: async (id: string, base: string) => {
let resolved = resolveCssFrom(base, id)

return {
base: path.dirname(resolved),
content: await fs.readFile(resolved, 'utf-8'),
}
},

// v4.0.0-alpha.24 and below
loadPlugin: createLoader({
legacy: true,
filepath: entryPoint,
onError(id, err) {
console.error(`Unable to load plugin: ${id}`, err)
Expand All @@ -213,6 +270,7 @@ async function loadV4(
}),

loadConfig: createLoader({
legacy: true,
filepath: entryPoint,
onError(id, err) {
console.error(`Unable to load config: ${id}`, err)
Expand Down
49 changes: 35 additions & 14 deletions src/resolve.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,33 @@
import { createRequire as req } from 'node:module'
import resolveFrom from 'resolve-from'
import fs from 'node:fs'
import { fileURLToPath } from 'node:url'
import { CachedInputFileSystem, ResolverFactory } from 'enhanced-resolve'
import { expiringMap } from './expiring-map'

const localRequire = req(import.meta.url)
const fileSystem = new CachedInputFileSystem(fs, 30_000)

const esmResolver = ResolverFactory.createResolver({
fileSystem,
useSyncFileSystemCalls: true,
extensions: ['.mjs', '.js'],
mainFields: ['module'],
conditionNames: ['node', 'import'],
})

const cjsResolver = ResolverFactory.createResolver({
fileSystem,
useSyncFileSystemCalls: true,
extensions: ['.js', '.cjs'],
mainFields: ['main'],
conditionNames: ['node', 'require'],
})

const cssResolver = ResolverFactory.createResolver({
fileSystem,
useSyncFileSystemCalls: true,
extensions: ['.css'],
mainFields: ['style'],
conditionNames: ['style'],
})

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

export function resolveIn(id: string, paths: string[]) {
return localRequire.resolve(id, {
paths,
})
}

export function maybeResolve(name: string) {
let modpath = resolveCache.get(name)

if (modpath === undefined) {
modpath = freshMaybeResolve(name)
modpath = resolveJsFrom(fileURLToPath(import.meta.url), name)
resolveCache.set(name, modpath)
}

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

function freshMaybeResolve(name: string) {
export function resolveJsFrom(base: string, id: string): string {
try {
return localRequire.resolve(name)
return esmResolver.resolveSync({}, base, id) || id
} catch (err) {
return null
return cjsResolver.resolveSync({}, base, id) || id
}
}

export { resolveFrom }
export function resolveCssFrom(base: string, id: string) {
return cssResolver.resolveSync({}, base, id) || id
}

0 comments on commit cdb3b98

Please sign in to comment.