diff --git a/package-lock.json b/package-lock.json index 3830873..9ee20d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,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", @@ -3172,6 +3173,19 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/enhanced-resolve": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -6738,6 +6752,15 @@ "postcss": "^8.0.0" } }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", diff --git a/package.json b/package.json index 19b0e2b..dc97913 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/config.ts b/src/config.ts index d2195e0..c9aa609 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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' @@ -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() let sourceToEntryMap = new Map() let pathToContextMap = expiringMap(10_000) @@ -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 { @@ -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({ + 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), } } } @@ -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 @@ -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) @@ -213,6 +270,7 @@ async function loadV4( }), loadConfig: createLoader({ + legacy: true, filepath: entryPoint, onError(id, err) { console.error(`Unable to load config: ${id}`, err) diff --git a/src/resolve.ts b/src/resolve.ts index 00f5c08..7ca2929 100644 --- a/src/resolve.ts +++ b/src/resolve.ts @@ -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 @@ -11,17 +36,11 @@ const localRequire = req(import.meta.url) // failed module resolutions making repeated checks very expensive. const resolveCache = expiringMap(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) } @@ -39,12 +58,14 @@ export async function loadIfExists(name: string): Promise { 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 +}