mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
* 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>
181 lines
5.3 KiB
JavaScript
181 lines
5.3 KiB
JavaScript
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)
|
|
|
|
if (!fileCache[filePath]) {
|
|
fileCache[filePath] = await fs.readFile(filePath, 'utf8').catch(() => null)
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
})
|
|
},
|
|
}
|
|
}
|