From b1e2c8fd5cb5dfa46bc440a12eafaf56cd844b1c Mon Sep 17 00:00:00 2001 From: Philipp Tanlak Date: Mon, 24 Nov 2025 20:54:57 +0100 Subject: Docs --- .../tailwindcss/src/oxide/cli/build/plugin.ts | 442 +++++++++++++++++++++ 1 file changed, 442 insertions(+) create mode 100644 node_modules/tailwindcss/src/oxide/cli/build/plugin.ts (limited to 'node_modules/tailwindcss/src/oxide/cli/build/plugin.ts') diff --git a/node_modules/tailwindcss/src/oxide/cli/build/plugin.ts b/node_modules/tailwindcss/src/oxide/cli/build/plugin.ts new file mode 100644 index 0000000..b878b91 --- /dev/null +++ b/node_modules/tailwindcss/src/oxide/cli/build/plugin.ts @@ -0,0 +1,442 @@ +import path from 'path' +import fs from 'fs' +import postcssrc from 'postcss-load-config' +import { lilconfig } from 'lilconfig' +import loadPlugins from 'postcss-load-config/src/plugins' // Little bit scary, looking at private/internal API +import loadOptions from 'postcss-load-config/src/options' // Little bit scary, looking at private/internal API + +import tailwind from '../../../processTailwindFeatures' +import { loadPostcss, loadPostcssImport, lightningcss } from './deps' +import { formatNodes, drainStdin, outputFile } from './utils' +import { env } from '../../../lib/sharedState' +import resolveConfig from '../../../../resolveConfig' +import { parseCandidateFiles } from '../../../lib/content' +import { createWatcher } from './watching' +import fastGlob from 'fast-glob' +import { findAtConfigPath } from '../../../lib/findAtConfigPath' +import log from '../../../util/log' +import { loadConfig } from '../../../lib/load-config' +import getModuleDependencies from '../../../lib/getModuleDependencies' +import type { Config } from '../../../../types' + +/** + * + * @param {string} [customPostCssPath ] + * @returns + */ +async function loadPostCssPlugins(customPostCssPath) { + let config = customPostCssPath + ? await (async () => { + let file = path.resolve(customPostCssPath) + + // Implementation, see: https://unpkg.com/browse/postcss-load-config@3.1.0/src/index.js + // @ts-ignore + let { config = {} } = await lilconfig('postcss').load(file) + if (typeof config === 'function') { + config = config() + } else { + config = Object.assign({}, config) + } + + if (!config.plugins) { + config.plugins = [] + } + + return { + file, + plugins: loadPlugins(config, file), + options: loadOptions(config, file), + } + })() + : await postcssrc() + + let configPlugins = config.plugins + + let configPluginTailwindIdx = configPlugins.findIndex((plugin) => { + if (typeof plugin === 'function' && plugin.name === 'tailwindcss') { + return true + } + + if (typeof plugin === 'object' && plugin !== null && plugin.postcssPlugin === 'tailwindcss') { + return true + } + + return false + }) + + let beforePlugins = + configPluginTailwindIdx === -1 ? [] : configPlugins.slice(0, configPluginTailwindIdx) + let afterPlugins = + configPluginTailwindIdx === -1 + ? configPlugins + : configPlugins.slice(configPluginTailwindIdx + 1) + + return [beforePlugins, afterPlugins, config.options] +} + +function loadBuiltinPostcssPlugins() { + let postcss = loadPostcss() + let IMPORT_COMMENT = '__TAILWIND_RESTORE_IMPORT__: ' + return [ + [ + (root) => { + root.walkAtRules('import', (rule) => { + if (rule.params.slice(1).startsWith('tailwindcss/')) { + rule.after(postcss.comment({ text: IMPORT_COMMENT + rule.params })) + rule.remove() + } + }) + }, + loadPostcssImport(), + (root) => { + root.walkComments((rule) => { + if (rule.text.startsWith(IMPORT_COMMENT)) { + rule.after( + postcss.atRule({ + name: 'import', + params: rule.text.replace(IMPORT_COMMENT, ''), + }) + ) + rule.remove() + } + }) + }, + ], + [], + {}, + ] +} + +let state = { + /** @type {any} */ + context: null, + + /** @type {ReturnType | null} */ + watcher: null, + + /** @type {{content: string, extension: string}[]} */ + changedContent: [], + + /** @type {{config: Config, dependencies: Set, dispose: Function } | null} */ + configBag: null, + + contextDependencies: new Set(), + + /** @type {import('../../lib/content.js').ContentPath[]} */ + contentPaths: [], + + refreshContentPaths() { + this.contentPaths = parseCandidateFiles(this.context, this.context?.tailwindConfig) + }, + + get config() { + return this.context.tailwindConfig + }, + + get contentPatterns() { + return { + all: this.contentPaths.map((contentPath) => contentPath.pattern), + dynamic: this.contentPaths + .filter((contentPath) => contentPath.glob !== undefined) + .map((contentPath) => contentPath.pattern), + } + }, + + loadConfig(configPath, content) { + if (this.watcher && configPath) { + this.refreshConfigDependencies() + } + + let config = loadConfig(configPath) + let dependencies = getModuleDependencies(configPath) + this.configBag = { + config, + dependencies, + dispose() { + for (let file of dependencies) { + delete require.cache[require.resolve(file)] + } + }, + } + + // @ts-ignore + this.configBag.config = resolveConfig(this.configBag.config, { content: { files: [] } }) + + // Override content files if `--content` has been passed explicitly + if (content?.length > 0) { + this.configBag.config.content.files = content + } + + return this.configBag.config + }, + + refreshConfigDependencies(configPath) { + env.DEBUG && console.time('Module dependencies') + this.configBag?.dispose() + env.DEBUG && console.timeEnd('Module dependencies') + }, + + readContentPaths() { + let content = [] + + // Resolve globs from the content config + // TODO: When we make the postcss plugin async-capable this can become async + let files = fastGlob.sync(this.contentPatterns.all) + + for (let file of files) { + if (__OXIDE__) { + content.push({ + file, + extension: path.extname(file).slice(1), + }) + } else { + content.push({ + content: fs.readFileSync(path.resolve(file), 'utf8'), + extension: path.extname(file).slice(1), + }) + } + } + + // Resolve raw content in the tailwind config + let rawContent = this.config.content.files.filter((file) => { + return file !== null && typeof file === 'object' + }) + + for (let { raw: htmlContent, extension = 'html' } of rawContent) { + content.push({ content: htmlContent, extension }) + } + + return content + }, + + getContext({ createContext, cliConfigPath, root, result, content }) { + if (this.context) { + this.context.changedContent = this.changedContent.splice(0) + + return this.context + } + + env.DEBUG && console.time('Searching for config') + let configPath = findAtConfigPath(root, result) ?? cliConfigPath + env.DEBUG && console.timeEnd('Searching for config') + + env.DEBUG && console.time('Loading config') + let config = this.loadConfig(configPath, content) + env.DEBUG && console.timeEnd('Loading config') + + env.DEBUG && console.time('Creating context') + this.context = createContext(config, []) + Object.assign(this.context, { + userConfigPath: configPath, + }) + env.DEBUG && console.timeEnd('Creating context') + + env.DEBUG && console.time('Resolving content paths') + this.refreshContentPaths() + env.DEBUG && console.timeEnd('Resolving content paths') + + if (this.watcher) { + env.DEBUG && console.time('Watch new files') + this.watcher.refreshWatchedFiles() + env.DEBUG && console.timeEnd('Watch new files') + } + + for (let file of this.readContentPaths()) { + this.context.changedContent.push(file) + } + + return this.context + }, +} + +export async function createProcessor(args, cliConfigPath) { + let postcss = loadPostcss() + + let input = args['--input'] + let output = args['--output'] + let includePostCss = args['--postcss'] + let customPostCssPath = typeof args['--postcss'] === 'string' ? args['--postcss'] : undefined + + let [beforePlugins, afterPlugins, postcssOptions] = includePostCss + ? await loadPostCssPlugins(customPostCssPath) + : loadBuiltinPostcssPlugins() + + if (args['--purge']) { + log.warn('purge-flag-deprecated', [ + 'The `--purge` flag has been deprecated.', + 'Please use `--content` instead.', + ]) + + if (!args['--content']) { + args['--content'] = args['--purge'] + } + } + + let content = args['--content']?.split(/(? { + return { + postcssPlugin: 'tailwindcss', + async Once(root, { result }) { + env.DEBUG && console.time('Compiling CSS') + await tailwind(({ createContext }) => { + console.error() + console.error('Rebuilding...') + + return () => { + return state.getContext({ + createContext, + cliConfigPath, + root, + result, + content, + }) + } + })(root, result) + env.DEBUG && console.timeEnd('Compiling CSS') + }, + } + } + + tailwindPlugin.postcss = true + + let plugins = [ + ...beforePlugins, + tailwindPlugin, + !args['--minify'] && formatNodes, + ...afterPlugins, + ].filter(Boolean) + + /** @type {import('postcss').Processor} */ + // @ts-ignore + let processor = postcss(plugins) + + async function readInput() { + // Piping in data, let's drain the stdin + if (input === '-') { + return drainStdin() + } + + // Input file has been provided + if (input) { + return fs.promises.readFile(path.resolve(input), 'utf8') + } + + // No input file provided, fallback to default atrules + return '@tailwind base; @tailwind components; @tailwind utilities' + } + + async function build() { + let start = process.hrtime.bigint() + + return readInput() + .then((css) => processor.process(css, { ...postcssOptions, from: input, to: output })) + .then((result) => lightningcss(!!args['--minify'], result)) + .then((result) => { + if (!state.watcher) { + return result + } + + env.DEBUG && console.time('Recording PostCSS dependencies') + for (let message of result.messages) { + if (message.type === 'dependency') { + state.contextDependencies.add(message.file) + } + } + env.DEBUG && console.timeEnd('Recording PostCSS dependencies') + + // TODO: This needs to be in a different spot + env.DEBUG && console.time('Watch new files') + state.watcher.refreshWatchedFiles() + env.DEBUG && console.timeEnd('Watch new files') + + return result + }) + .then((result) => { + if (!output) { + process.stdout.write(result.css) + return + } + + return Promise.all([ + outputFile(result.opts.to, result.css), + result.map && outputFile(result.opts.to + '.map', result.map.toString()), + ]) + }) + .then(() => { + let end = process.hrtime.bigint() + console.error() + console.error('Done in', (end - start) / BigInt(1e6) + 'ms.') + }) + .then( + () => {}, + (err) => { + // TODO: If an initial build fails we can't easily pick up any PostCSS dependencies + // that were collected before the error occurred + // The result is not stored on the error so we have to store it externally + // and pull the messages off of it here somehow + + // This results in a less than ideal DX because the watcher will not pick up + // changes to imported CSS if one of them caused an error during the initial build + // If you fix it and then save the main CSS file so there's no error + // The watcher will start watching the imported CSS files and will be + // resilient to future errors. + + if (state.watcher) { + console.error(err) + } else { + return Promise.reject(err) + } + } + ) + } + + /** + * @param {{file: string, content(): Promise, extension: string}[]} changes + */ + async function parseChanges(changes) { + return Promise.all( + changes.map(async (change) => ({ + content: await change.content(), + extension: change.extension, + })) + ) + } + + if (input !== undefined && input !== '-') { + state.contextDependencies.add(path.resolve(input)) + } + + return { + build, + watch: async () => { + state.watcher = createWatcher(args, { + state, + + /** + * @param {{file: string, content(): Promise, extension: string}[]} changes + */ + async rebuild(changes) { + let needsNewContext = changes.some((change) => { + return ( + state.configBag?.dependencies.has(change.file) || + state.contextDependencies.has(change.file) + ) + }) + + if (needsNewContext) { + state.context = null + } else { + for (let change of await parseChanges(changes)) { + state.changedContent.push(change) + } + } + + return build() + }, + }) + + await build() + }, + } +} -- cgit v1.2.3