tailwindcss/integrations/io.js

182 lines
5.3 KiB
JavaScript
Raw Permalink Normal View History

let { rm, existsSync } = require('fs')
let path = require('path')
let fs = require('fs/promises')
let chokidar = require('chokidar')
let resolveToolRoot = require('./resolve-tool-root')
function getWatcherOptions() {
return {
usePolling: true,
interval: 200,
awaitWriteFinish: {
stabilityThreshold: 1500,
pollInterval: 50,
},
}
}
module.exports = function ({
/** Output directory, relative to the tool. */
output = 'dist',
/** Input directory, relative to the tool. */
input = 'src',
/** Whether or not you want to cleanup the output directory. */
cleanup = true,
} = {}) {
let toolRoot = resolveToolRoot()
let fileCache = {}
let absoluteOutputFolder = path.resolve(toolRoot, output)
let absoluteInputFolder = path.resolve(toolRoot, input)
if (cleanup) {
beforeAll((done) => rm(absoluteOutputFolder, { recursive: true, force: true }, done))
afterEach((done) => rm(absoluteOutputFolder, { recursive: true, force: true }, done))
}
// Restore all written files
afterEach(async () => {
await Promise.all(
Object.entries(fileCache).map(async ([file, content]) => {
try {
if (content === null) {
return await fs.unlink(file)
} else {
return await fs.writeFile(file, content, 'utf8')
}
} catch {}
})
)
})
async function readdir(start, parent = []) {
let files = await fs.readdir(start, { withFileTypes: true })
let resolvedFiles = await Promise.all(
files.map((file) => {
if (file.isDirectory()) {
return readdir(path.resolve(start, file.name), [...parent, file.name])
}
return parent.concat(file.name).join(path.sep)
})
)
return resolvedFiles.flat(Infinity)
}
async function resolveFile(fileOrRegex, directory) {
if (fileOrRegex instanceof RegExp) {
let files = await readdir(directory)
if (files.length === 0) {
throw new Error(`No files exists in "${directory}"`)
}
let filtered = files.filter((file) => fileOrRegex.test(file))
if (filtered.length === 0) {
throw new Error(`Not a single file matched: ${fileOrRegex}`)
} else if (filtered.length > 1) {
throw new Error(`Multiple files matched: ${fileOrRegex}`)
}
return filtered[0]
}
return fileOrRegex
}
return {
cleanupFile(file) {
let filePath = path.resolve(toolRoot, file)
fileCache[filePath] = null
},
async fileExists(file) {
let filePath = path.resolve(toolRoot, file)
return existsSync(filePath)
},
async removeFile(file) {
let filePath = path.resolve(toolRoot, file)
Enable ESM and TS based config files (#10785) * add `jiti` and `detective-typescript` dependencies * use `jiti` and `detective-typescript` Instead of `detective`, this way we will be able to support `tailwind.config.ts` files and `ESM` files. * use `@swc/core` instead of the built-in `babel` form `jiti` * update changelog * add `jiti` and `detective-typescript` dependencies to `stable` * use `sucrase` to transform the configs * add `sucrase` dependency to `stable` engine * make loading the config easier * use abstracted loading config utils * WIP: make `load` related files public API * use new config loader in PostCSS plugin * add list of default config files to look for * cleanup unused arguments * find default config path when using CLI * improve `init` command * make eslint happy * keep all files in `stubs` folder * add `tailwind.config.js` stub file * Initialize PostCSS config using the same format as Tailwind config * Rename config content stubs to config.*.js * Improve option descriptions for init options * Remove unused code, remove `constants` file * Fix TS warning * apply CLI changes to the Oxide version * update `--help` output in CLI tests * WIP: make tests work on CI TODO: Test all combinations of `--full`, `--ts`, `--postcss`, and `--esm`. * wip * remove unused `fs` * Fix init tests Did you know you could pass an empty args to a command? No? Me neither. ¯\_(ツ)_/¯ * bump `napi-derive` * list extensions we are interested in * no-op the `removeFile` if file doesn't exist * ensure all `init` flags work * ensure we cleanup the new files * test ESM/CJS generation based on package.json * remove unnecessary test We are not displaying output in the `--help` anymore based on whether `type: module` is present or not. Therefore this test is unneeded. * only look for `TypeScript` files when the entryFile is `TypeScript` as well * refactor `load` to be `loadConfig` This will allow you to use: ```js import loadConfig from 'tailwindcss/loadConfig' let config = loadConfig("/Users/xyz/projects/my-app/tailwind.config.ts") ``` The `loadConfig` function will return the configuration object based on the given absolute path of a tailwind configuration file. The given path can be a CJS, an ESM or a TS file. * use the `config.full.js` stub instead of the `defaultConfig.stub.js` file The root `defaultConfig` is still there for backwards compatibilty reasons. But the `module.exports = requrie('./config.full.js')` was causing some problems when actually using tailwindcss. So dropped it instead. * apply `load` -> `loadConfig` changes to `Oxide` engine CLI * ensure we write the config file in the Oxide engine * improve type in Oxide engine CLI * catch errors instead of checking if the file exists A little smaller but just for tests so doesn't matter too much here 👍 * ensure we publish the correct stub files --------- Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com> Co-authored-by: Jordan Pittman <jordan@cryptica.me> Co-authored-by: Nate Moore <nate@natemoo.re> Co-authored-by: Enzo Innocenzi <enzo@innocenzi.dev>
2023-03-15 22:04:18 +01:00
if (!fileCache[filePath]) {
Enable ESM and TS based config files (#10785) * add `jiti` and `detective-typescript` dependencies * use `jiti` and `detective-typescript` Instead of `detective`, this way we will be able to support `tailwind.config.ts` files and `ESM` files. * use `@swc/core` instead of the built-in `babel` form `jiti` * update changelog * add `jiti` and `detective-typescript` dependencies to `stable` * use `sucrase` to transform the configs * add `sucrase` dependency to `stable` engine * make loading the config easier * use abstracted loading config utils * WIP: make `load` related files public API * use new config loader in PostCSS plugin * add list of default config files to look for * cleanup unused arguments * find default config path when using CLI * improve `init` command * make eslint happy * keep all files in `stubs` folder * add `tailwind.config.js` stub file * Initialize PostCSS config using the same format as Tailwind config * Rename config content stubs to config.*.js * Improve option descriptions for init options * Remove unused code, remove `constants` file * Fix TS warning * apply CLI changes to the Oxide version * update `--help` output in CLI tests * WIP: make tests work on CI TODO: Test all combinations of `--full`, `--ts`, `--postcss`, and `--esm`. * wip * remove unused `fs` * Fix init tests Did you know you could pass an empty args to a command? No? Me neither. ¯\_(ツ)_/¯ * bump `napi-derive` * list extensions we are interested in * no-op the `removeFile` if file doesn't exist * ensure all `init` flags work * ensure we cleanup the new files * test ESM/CJS generation based on package.json * remove unnecessary test We are not displaying output in the `--help` anymore based on whether `type: module` is present or not. Therefore this test is unneeded. * only look for `TypeScript` files when the entryFile is `TypeScript` as well * refactor `load` to be `loadConfig` This will allow you to use: ```js import loadConfig from 'tailwindcss/loadConfig' let config = loadConfig("/Users/xyz/projects/my-app/tailwind.config.ts") ``` The `loadConfig` function will return the configuration object based on the given absolute path of a tailwind configuration file. The given path can be a CJS, an ESM or a TS file. * use the `config.full.js` stub instead of the `defaultConfig.stub.js` file The root `defaultConfig` is still there for backwards compatibilty reasons. But the `module.exports = requrie('./config.full.js')` was causing some problems when actually using tailwindcss. So dropped it instead. * apply `load` -> `loadConfig` changes to `Oxide` engine CLI * ensure we write the config file in the Oxide engine * improve type in Oxide engine CLI * catch errors instead of checking if the file exists A little smaller but just for tests so doesn't matter too much here 👍 * ensure we publish the correct stub files --------- Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com> Co-authored-by: Jordan Pittman <jordan@cryptica.me> Co-authored-by: Nate Moore <nate@natemoo.re> Co-authored-by: Enzo Innocenzi <enzo@innocenzi.dev>
2023-03-15 22:04:18 +01:00
fileCache[filePath] = await fs.readFile(filePath, 'utf8').catch(() => null)
}
Enable ESM and TS based config files (#10785) * add `jiti` and `detective-typescript` dependencies * use `jiti` and `detective-typescript` Instead of `detective`, this way we will be able to support `tailwind.config.ts` files and `ESM` files. * use `@swc/core` instead of the built-in `babel` form `jiti` * update changelog * add `jiti` and `detective-typescript` dependencies to `stable` * use `sucrase` to transform the configs * add `sucrase` dependency to `stable` engine * make loading the config easier * use abstracted loading config utils * WIP: make `load` related files public API * use new config loader in PostCSS plugin * add list of default config files to look for * cleanup unused arguments * find default config path when using CLI * improve `init` command * make eslint happy * keep all files in `stubs` folder * add `tailwind.config.js` stub file * Initialize PostCSS config using the same format as Tailwind config * Rename config content stubs to config.*.js * Improve option descriptions for init options * Remove unused code, remove `constants` file * Fix TS warning * apply CLI changes to the Oxide version * update `--help` output in CLI tests * WIP: make tests work on CI TODO: Test all combinations of `--full`, `--ts`, `--postcss`, and `--esm`. * wip * remove unused `fs` * Fix init tests Did you know you could pass an empty args to a command? No? Me neither. ¯\_(ツ)_/¯ * bump `napi-derive` * list extensions we are interested in * no-op the `removeFile` if file doesn't exist * ensure all `init` flags work * ensure we cleanup the new files * test ESM/CJS generation based on package.json * remove unnecessary test We are not displaying output in the `--help` anymore based on whether `type: module` is present or not. Therefore this test is unneeded. * only look for `TypeScript` files when the entryFile is `TypeScript` as well * refactor `load` to be `loadConfig` This will allow you to use: ```js import loadConfig from 'tailwindcss/loadConfig' let config = loadConfig("/Users/xyz/projects/my-app/tailwind.config.ts") ``` The `loadConfig` function will return the configuration object based on the given absolute path of a tailwind configuration file. The given path can be a CJS, an ESM or a TS file. * use the `config.full.js` stub instead of the `defaultConfig.stub.js` file The root `defaultConfig` is still there for backwards compatibilty reasons. But the `module.exports = requrie('./config.full.js')` was causing some problems when actually using tailwindcss. So dropped it instead. * apply `load` -> `loadConfig` changes to `Oxide` engine CLI * ensure we write the config file in the Oxide engine * improve type in Oxide engine CLI * catch errors instead of checking if the file exists A little smaller but just for tests so doesn't matter too much here 👍 * ensure we publish the correct stub files --------- Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com> Co-authored-by: Jordan Pittman <jordan@cryptica.me> Co-authored-by: Nate Moore <nate@natemoo.re> Co-authored-by: Enzo Innocenzi <enzo@innocenzi.dev>
2023-03-15 22:04:18 +01:00
await fs.unlink(filePath).catch(() => null)
},
async readOutputFile(file) {
file = await resolveFile(file, absoluteOutputFolder)
return fs.readFile(path.resolve(absoluteOutputFolder, file), 'utf8')
},
async readInputFile(file) {
file = await resolveFile(file, absoluteInputFolder)
return fs.readFile(path.resolve(absoluteInputFolder, file), 'utf8')
},
async appendToInputFile(file, contents) {
let filePath = path.resolve(absoluteInputFolder, file)
if (!fileCache[filePath]) {
fileCache[filePath] = await fs.readFile(filePath, 'utf8')
}
return fs.appendFile(filePath, contents, 'utf8')
},
async writeInputFile(file, contents) {
let filePath = path.resolve(absoluteInputFolder, file)
if (!fileCache[filePath]) {
try {
fileCache[filePath] = await fs.readFile(filePath, 'utf8')
} catch (err) {
if (err.code === 'ENOENT') {
fileCache[filePath] = null // Sentinel value to `delete` the file afterwards. This also means that we are writing to a `new` file inside the test.
} else {
throw err
}
}
}
Warn when broad glob patterns are used in the content configuration (#14140) When you use a glob pattern in your `content` configuration that is too broad, it could be that you are accidentally including files that you didn't intend to include. E.g.: all of `node_modules` This has been documented in the [Tailwind CSS documentation](https://tailwindcss.com/docs/content-configuration#pattern-recommendations), but it's still something that a lot of people run into. This PR will try to detect those patterns and show a big warning to let you know if you may have done something wrong. We will show a warning if all of these conditions are true: 1. We detect `**` in the glob pattern 2. _and_ you didn't explicitly use `node_modules` in the glob pattern 3. _and_ we found files that include `node_modules` in the file path 4. _and_ no other globs exist that explicitly match the found file With these rules in place, the DX has nice trade-offs: 1. Very simple projects (that don't even have a `node_modules` folder), can simply use `./**/*` because while resolving actual files we won't see files from `node_modules` and thus won't warn. 2. If you use `./src/**` and you do have a `node_modules`, then we also won't complain (unless you have a `node_modules` folder in the `./src` folder). 3. If you work with a 3rd party library that you want to make changes to. Using an explicit match like `./node_modules/my-package/**/*` is allowed because `node_modules` is explicitly mentioned. Note: this only shows a warning, it does not stop the process entirely. The warning will be show when the very first file in the `node_modules` is detected. <!-- 👋 Hey, thanks for your interest in contributing to Tailwind! **Please ask first before starting work on any significant new features.** It's never a fun experience to have your pull request declined after investing a lot of time and effort into a new feature. To avoid this from happening, we request that contributors create an issue to first discuss any significant new features. This includes things like adding new utilities, creating new at-rules, or adding new component examples to the documentation. https://github.com/tailwindcss/tailwindcss/blob/master/.github/CONTRIBUTING.md --> --------- Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
2024-08-07 16:55:20 +02:00
await fs.mkdir(path.dirname(filePath), { recursive: true })
return fs.writeFile(filePath, contents, 'utf8')
},
async waitForOutputFileCreation(file) {
if (file instanceof RegExp) {
let r = file
let watcher = chokidar.watch(absoluteOutputFolder, getWatcherOptions())
return new Promise((resolve) => {
watcher.on('add', (file) => {
if (r.test(file)) {
watcher.close().then(() => resolve())
}
})
})
} else {
let filePath = path.resolve(absoluteOutputFolder, file)
return new Promise((resolve) => {
let watcher = chokidar.watch(absoluteOutputFolder, getWatcherOptions())
watcher.on('add', (addedFile) => {
if (addedFile !== filePath) return
return watcher.close().finally(resolve)
})
})
}
},
async waitForOutputFileChange(file, cb = () => {}) {
file = await resolveFile(file, absoluteOutputFolder)
let filePath = path.resolve(absoluteOutputFolder, file)
return new Promise((resolve) => {
let watcher = chokidar.watch(absoluteOutputFolder, getWatcherOptions())
watcher
.on('change', (changedFile) => {
if (changedFile !== filePath) return
return watcher.close().finally(resolve)
})
.on('ready', cb)
})
},
}
}