tailwindcss/integrations/io.js

192 lines
5.5 KiB
JavaScript
Raw 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)
[Oxide] Automatic content detection (#11173) * resolve all _existing_ content paths * pin `@napi-rs/cli` * WIP: Log all resolved content files/globs * only filter out raw changed content in non-auto mode * skip parseCandidateFiles cache in `auto` mode * improve algorithm of detecting content paths 1. Files in the root should be listed statically instead of using globs. 2. Files and folders in special known direct child folders should be listed statically instead of using globs (e.g.: `public`). This is because these special folders are often used to store generated AND source files at the same time. Using globs could trigger infinite loops because we are watching and acting upon dist files. 3. All file extensions found in the project, should be used in the globs in addition to a known set of extensions. 4. Direct folders seen from the root, can use the glob syntax `<root>/src/**/*.{...known-extensions}` * inline wanted-extensions Not 100% convinced yet, but seems cleaner so far. * ensure writing an file also makes the parent folder(s) * add integration tests for the auto content feature * add pnpm and bun lock files * Revert "inline wanted-extensions" This reverts commit 879c1248524e84216125f4a24e0160b40736333a. * sort binary-extensions and add lockb * sort + add `lock` to ignored extensions * drop `yarn.lock`, because lock extensions are already covered * group template extensions This will make it a bit easier to organize in the future. * drop empty lines and commented lines from template-extensions * skip the config path when resolving template files The config file will automatically trigger a rebuild when this file is changed. However, this should not be part of the template files because that could cause additional css that's not being used. * make `auto content` the default in the oxide engine - In the oxide engine, the default `content: []` will be dropped from the default configuration (config.simple.js, config.full.js). - If you have `content: []` or `content: { files: [] }` then the auto content feature won't be active. However if those arrays are empty a warning will still be shown. Adding files/globs or dropping the `content` section completely will enable auto content. * only test the auto content integration test in the oxide engine * set `content.files` to `auto` instead of using `auto: boolean` This way we don't run into the issue where the `config.content.files` is set and the `config.content.auto` is set to true. * drop log * ensure we validate the config in the CLI * show experimental warning for automatic content detection * use cached version of the getCandidateFiles instead of bypassing it * use `is_empty()` shorthand Thanks, Clippy! * add test to ensure nested ignored folders are not scanned * add `tempfile` for tests * add auto content tests in Rust * refactor auto content detection This will also make sure that if we have (deeply) nested ignored folders, then we won't use deeply nested globs (**/*.{js,html}) for the parent(s) of the nested ignored folders but instead use a shallow glob for each directory (*/*.{js,html}). Then each sibling directory of the parent can use deeply nested globs again except for the direct parent. * use consistent comments * ensure ignored static listed files are not present * improve performance by ~30x On a big test project this goes from ~6s to ~200ms * improve performance by ~5x We started with a ~6s duration Then in the previous commit, we improved it by ~30x and it went down to ~200ms Now with this change, it takes about ~40ms. That's another ~5x improvement. Or in total a ~150x improvement. * ensure nested folders in `public/` are also explicitly listed * add shortcut for normalizing files This is only called once so won't do anything to the main performance of Tailwind CSS. But always nice to make small performance improvements! * run Rust tests in CI * fix lint warnings * update changelog * Update CHANGELOG.md --------- Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2023-05-12 10:13:29 -04:00
// Ensure the parent folder of the file exists
if (
!(await fs
.stat(filePath)
.then(() => true)
.catch(() => false))
) {
await fs.mkdir(path.dirname(filePath), { recursive: true })
}
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
}
}
}
return fs.writeFile(path.resolve(absoluteInputFolder, file), 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)
})
},
}
}