Fixes #19389 We inlined env vars in the Standalone CLI because we use some custom patches + env vars to ensure that only the appropriate `glibc` / `musl` binaries are included for Lightning CSS and Parcel Watcher for Linux builds. The build happens to run on a macOS Github CI machine though so `NODE_PATH` was getting inlined as the string: ``` /Users/runner/work/tailwindcss/tailwindcss/node_modules/.pnpm/bun@1.3.0/node_modules/bun/bin/node_modules ``` I don't think there's a reason for `NODE_PATH` to work on the Standalone CLI (and it didn't work because of the above bug *anyway*) so I've done a few things here: 1. The build setup now uses `Bun.build(…)` which now supports compiling binaries. This speeds up the build process a bit. 2. We're no longer inlining all env vars. We selectively inline only a few using `define`. 3. I've explicitly disabled the extra `NODE_PATH` support in `@tailwindcss/node` when building with the Standalone CLI. 4. The `__tw_readFile` hack is now gone. Async FS APIs were not originally able to read embedded files but that changed in Bun v1.2.3 making the hack unnecessary. 5. A few more env vars are now inlined + a plugin to simplify the Oxide loading code when bundled. 6. A plugin + env vars prevents bundling WASI build as it's not necessary for the Standalone CLI. I want to find a way to get rid of `__tw_resolve` and `__tw_load` but don't want to change too much in this PR so I haven't looked into it yet.
278 lines
7.3 KiB
TypeScript
278 lines
7.3 KiB
TypeScript
import EnhancedResolve from 'enhanced-resolve'
|
|
import { createJiti, type Jiti } from 'jiti'
|
|
import fs from 'node:fs'
|
|
import fsPromises from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
import { pathToFileURL } from 'node:url'
|
|
import {
|
|
__unstable__loadDesignSystem as ___unstable__loadDesignSystem,
|
|
compile as _compile,
|
|
compileAst as _compileAst,
|
|
Features,
|
|
Polyfills,
|
|
} from 'tailwindcss'
|
|
import type { AstNode } from '../../tailwindcss/src/ast'
|
|
import { getModuleDependencies } from './get-module-dependencies'
|
|
import { rewriteUrls } from './urls'
|
|
|
|
export { Features, Polyfills }
|
|
|
|
export type Resolver = (id: string, base: string) => Promise<string | false | undefined>
|
|
|
|
export interface CompileOptions {
|
|
base: string
|
|
from?: string
|
|
onDependency: (path: string) => void
|
|
shouldRewriteUrls?: boolean
|
|
polyfills?: Polyfills
|
|
|
|
customCssResolver?: Resolver
|
|
customJsResolver?: Resolver
|
|
}
|
|
|
|
function createCompileOptions({
|
|
base,
|
|
from,
|
|
polyfills,
|
|
onDependency,
|
|
shouldRewriteUrls,
|
|
|
|
customCssResolver,
|
|
customJsResolver,
|
|
}: CompileOptions) {
|
|
return {
|
|
base,
|
|
polyfills,
|
|
from,
|
|
async loadModule(id: string, base: string) {
|
|
return loadModule(id, base, onDependency, customJsResolver)
|
|
},
|
|
async loadStylesheet(id: string, sheetBase: string) {
|
|
let sheet = await loadStylesheet(id, sheetBase, onDependency, customCssResolver)
|
|
|
|
if (shouldRewriteUrls) {
|
|
sheet.content = await rewriteUrls({
|
|
css: sheet.content,
|
|
root: base,
|
|
base: sheet.base,
|
|
})
|
|
}
|
|
|
|
return sheet
|
|
},
|
|
}
|
|
}
|
|
|
|
async function ensureSourceDetectionRootExists(compiler: {
|
|
root: Awaited<ReturnType<typeof compile>>['root']
|
|
}) {
|
|
// Verify if the `source(…)` path exists (until the glob pattern starts)
|
|
if (compiler.root && compiler.root !== 'none') {
|
|
let globSymbols = /[*{]/
|
|
let basePath = []
|
|
for (let segment of compiler.root.pattern.split('/')) {
|
|
if (globSymbols.test(segment)) {
|
|
break
|
|
}
|
|
|
|
basePath.push(segment)
|
|
}
|
|
|
|
let exists = await fsPromises
|
|
.stat(path.resolve(compiler.root.base, basePath.join('/')))
|
|
.then((stat) => stat.isDirectory())
|
|
.catch(() => false)
|
|
|
|
if (!exists) {
|
|
throw new Error(
|
|
`The \`source(${compiler.root.pattern})\` does not exist or is not a directory.`,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function compileAst(ast: AstNode[], options: CompileOptions) {
|
|
let compiler = await _compileAst(ast, createCompileOptions(options))
|
|
await ensureSourceDetectionRootExists(compiler)
|
|
return compiler
|
|
}
|
|
|
|
export async function compile(css: string, options: CompileOptions) {
|
|
let compiler = await _compile(css, createCompileOptions(options))
|
|
await ensureSourceDetectionRootExists(compiler)
|
|
return compiler
|
|
}
|
|
|
|
export async function __unstable__loadDesignSystem(css: string, { base }: { base: string }) {
|
|
return ___unstable__loadDesignSystem(css, {
|
|
base,
|
|
async loadModule(id, base) {
|
|
return loadModule(id, base, () => {})
|
|
},
|
|
async loadStylesheet(id, base) {
|
|
return loadStylesheet(id, base, () => {})
|
|
},
|
|
})
|
|
}
|
|
|
|
export async function loadModule(
|
|
id: string,
|
|
base: string,
|
|
onDependency: (path: string) => void,
|
|
customJsResolver?: Resolver,
|
|
) {
|
|
if (id[0] !== '.') {
|
|
let resolvedPath = await resolveJsId(id, base, customJsResolver)
|
|
if (!resolvedPath) {
|
|
throw new Error(`Could not resolve '${id}' from '${base}'`)
|
|
}
|
|
|
|
let module = await importModule(pathToFileURL(resolvedPath).href)
|
|
return {
|
|
path: resolvedPath,
|
|
base: path.dirname(resolvedPath),
|
|
module: module.default ?? module,
|
|
}
|
|
}
|
|
|
|
let resolvedPath = await resolveJsId(id, base, customJsResolver)
|
|
if (!resolvedPath) {
|
|
throw new Error(`Could not resolve '${id}' from '${base}'`)
|
|
}
|
|
|
|
let [module, moduleDependencies] = await Promise.all([
|
|
importModule(pathToFileURL(resolvedPath).href + '?id=' + Date.now()),
|
|
getModuleDependencies(resolvedPath),
|
|
])
|
|
|
|
for (let file of moduleDependencies) {
|
|
onDependency(file)
|
|
}
|
|
return {
|
|
path: resolvedPath,
|
|
base: path.dirname(resolvedPath),
|
|
module: module.default ?? module,
|
|
}
|
|
}
|
|
|
|
async function loadStylesheet(
|
|
id: string,
|
|
base: string,
|
|
onDependency: (path: string) => void,
|
|
cssResolver?: Resolver,
|
|
) {
|
|
let resolvedPath = await resolveCssId(id, base, cssResolver)
|
|
if (!resolvedPath) throw new Error(`Could not resolve '${id}' from '${base}'`)
|
|
|
|
onDependency(resolvedPath)
|
|
|
|
let file = await fsPromises.readFile(resolvedPath, 'utf-8')
|
|
return {
|
|
path: resolvedPath,
|
|
base: path.dirname(resolvedPath),
|
|
content: file,
|
|
}
|
|
}
|
|
|
|
// Attempts to import the module using the native `import()` function. If this
|
|
// fails, it sets up `jiti` and attempts to import this way so that `.ts` files
|
|
// can be resolved properly.
|
|
let jiti: null | Jiti = null
|
|
async function importModule(path: string): Promise<any> {
|
|
if (typeof globalThis.__tw_load === 'function') {
|
|
let module = await globalThis.__tw_load(path)
|
|
if (module) {
|
|
return module
|
|
}
|
|
}
|
|
|
|
try {
|
|
return await import(path)
|
|
} catch (error) {
|
|
jiti ??= createJiti(import.meta.url, { moduleCache: false, fsCache: false })
|
|
return await jiti.import(path)
|
|
}
|
|
}
|
|
|
|
const modules = ['node_modules', ...(process.env.NODE_PATH ? [process.env.NODE_PATH] : [])]
|
|
|
|
const cssResolver = EnhancedResolve.ResolverFactory.createResolver({
|
|
fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000),
|
|
useSyncFileSystemCalls: true,
|
|
extensions: ['.css'],
|
|
mainFields: ['style'],
|
|
conditionNames: ['style'],
|
|
modules,
|
|
})
|
|
async function resolveCssId(
|
|
id: string,
|
|
base: string,
|
|
customCssResolver?: Resolver,
|
|
): Promise<string | false | undefined> {
|
|
if (typeof globalThis.__tw_resolve === 'function') {
|
|
let resolved = globalThis.__tw_resolve(id, base)
|
|
if (resolved) {
|
|
return Promise.resolve(resolved)
|
|
}
|
|
}
|
|
|
|
if (customCssResolver) {
|
|
let customResolution = await customCssResolver(id, base)
|
|
if (customResolution) {
|
|
return customResolution
|
|
}
|
|
}
|
|
|
|
return runResolver(cssResolver, id, base)
|
|
}
|
|
|
|
const esmResolver = EnhancedResolve.ResolverFactory.createResolver({
|
|
fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000),
|
|
useSyncFileSystemCalls: true,
|
|
extensions: ['.js', '.json', '.node', '.ts'],
|
|
conditionNames: ['node', 'import'],
|
|
modules,
|
|
})
|
|
|
|
const cjsResolver = EnhancedResolve.ResolverFactory.createResolver({
|
|
fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000),
|
|
useSyncFileSystemCalls: true,
|
|
extensions: ['.js', '.json', '.node', '.ts'],
|
|
conditionNames: ['node', 'require'],
|
|
modules,
|
|
})
|
|
|
|
async function resolveJsId(
|
|
id: string,
|
|
base: string,
|
|
customJsResolver?: Resolver,
|
|
): Promise<string | false | undefined> {
|
|
if (typeof globalThis.__tw_resolve === 'function') {
|
|
let resolved = globalThis.__tw_resolve(id, base)
|
|
if (resolved) {
|
|
return Promise.resolve(resolved)
|
|
}
|
|
}
|
|
|
|
if (customJsResolver) {
|
|
let customResolution = await customJsResolver(id, base)
|
|
if (customResolution) {
|
|
return customResolution
|
|
}
|
|
}
|
|
|
|
return runResolver(esmResolver, id, base).catch(() => runResolver(cjsResolver, id, base))
|
|
}
|
|
|
|
function runResolver(
|
|
resolver: EnhancedResolve.Resolver,
|
|
id: string,
|
|
base: string,
|
|
): Promise<string | false | undefined> {
|
|
return new Promise((resolve, reject) =>
|
|
resolver.resolve({}, base, id, {}, (err, result) => {
|
|
if (err) return reject(err)
|
|
resolve(result)
|
|
}),
|
|
)
|
|
}
|