Compare commits
16 commits
main
...
feature/li
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
215e86f0f3 | ||
|
|
9735d8d82a | ||
|
|
78eea5bee0 | ||
|
|
59fc32e3ad | ||
|
|
20ab1b3c64 | ||
|
|
9ac928b92c | ||
|
|
a15bee463f | ||
|
|
1d84e5b734 | ||
|
|
3871cc54ea | ||
|
|
056d18022a | ||
|
|
383354c478 | ||
|
|
91ddcedb37 | ||
|
|
4bff7654f5 | ||
|
|
04132c33ca | ||
|
|
9dec4f28ef | ||
|
|
eed0e8b92b |
15 changed files with 467 additions and 148 deletions
46
integrations/tailwindcss-cli/package-lock.json
generated
46
integrations/tailwindcss-cli/package-lock.json
generated
|
|
@ -12,13 +12,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../..": {
|
"../..": {
|
||||||
"version": "3.0.24",
|
"version": "3.1.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"arg": "^5.0.1",
|
"arg": "^5.0.2",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"color-name": "^1.1.4",
|
"color-name": "^1.1.4",
|
||||||
"detective": "^5.2.0",
|
"detective": "^5.2.1",
|
||||||
"didyoumean": "^1.2.2",
|
"didyoumean": "^1.2.2",
|
||||||
"dlv": "^1.1.3",
|
"dlv": "^1.1.3",
|
||||||
"fast-glob": "^3.2.11",
|
"fast-glob": "^3.2.11",
|
||||||
|
|
@ -36,26 +36,26 @@
|
||||||
"postcss-selector-parser": "^6.0.10",
|
"postcss-selector-parser": "^6.0.10",
|
||||||
"postcss-value-parser": "^4.2.0",
|
"postcss-value-parser": "^4.2.0",
|
||||||
"quick-lru": "^5.1.1",
|
"quick-lru": "^5.1.1",
|
||||||
"resolve": "^1.22.0"
|
"resolve": "^1.22.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"tailwind": "lib/cli.js",
|
"tailwind": "lib/cli.js",
|
||||||
"tailwindcss": "lib/cli.js"
|
"tailwindcss": "lib/cli.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@parcel/css": "^1.11.2",
|
||||||
"@swc/cli": "^0.1.57",
|
"@swc/cli": "^0.1.57",
|
||||||
"@swc/core": "^1.2.160",
|
"@swc/core": "^1.2.196",
|
||||||
"@swc/jest": "^0.2.21",
|
"@swc/jest": "^0.2.21",
|
||||||
"@swc/register": "^0.1.10",
|
"@swc/register": "^0.1.10",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
"cssnano": "^5.1.9",
|
"esbuild": "^0.14.48",
|
||||||
"esbuild": "^0.14.39",
|
"eslint": "^8.18.0",
|
||||||
"eslint": "^8.15.0",
|
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"jest": "^28.0.3",
|
"jest": "^28.1.2",
|
||||||
"jest-diff": "^28.1.0",
|
"jest-diff": "^28.1.1",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^2.7.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.1.11",
|
"prettier-plugin-tailwindcss": "^0.1.11",
|
||||||
"rimraf": "^3.0.0",
|
"rimraf": "^3.0.0",
|
||||||
"source-map-js": "^1.0.2"
|
"source-map-js": "^1.0.2"
|
||||||
|
|
@ -76,27 +76,27 @@
|
||||||
"tailwindcss": {
|
"tailwindcss": {
|
||||||
"version": "file:../..",
|
"version": "file:../..",
|
||||||
"requires": {
|
"requires": {
|
||||||
|
"@parcel/css": "^1.11.2",
|
||||||
"@swc/cli": "^0.1.57",
|
"@swc/cli": "^0.1.57",
|
||||||
"@swc/core": "^1.2.160",
|
"@swc/core": "^1.2.196",
|
||||||
"@swc/jest": "^0.2.21",
|
"@swc/jest": "^0.2.21",
|
||||||
"@swc/register": "^0.1.10",
|
"@swc/register": "^0.1.10",
|
||||||
"arg": "^5.0.1",
|
"arg": "^5.0.2",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"color-name": "^1.1.4",
|
"color-name": "^1.1.4",
|
||||||
"cssnano": "^5.1.9",
|
"detective": "^5.2.1",
|
||||||
"detective": "^5.2.0",
|
|
||||||
"didyoumean": "^1.2.2",
|
"didyoumean": "^1.2.2",
|
||||||
"dlv": "^1.1.3",
|
"dlv": "^1.1.3",
|
||||||
"esbuild": "^0.14.39",
|
"esbuild": "^0.14.48",
|
||||||
"eslint": "^8.15.0",
|
"eslint": "^8.18.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"fast-glob": "^3.2.11",
|
"fast-glob": "^3.2.11",
|
||||||
"glob-parent": "^6.0.2",
|
"glob-parent": "^6.0.2",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
"jest": "^28.0.3",
|
"jest": "^28.1.2",
|
||||||
"jest-diff": "^28.1.0",
|
"jest-diff": "^28.1.1",
|
||||||
"lilconfig": "^2.0.5",
|
"lilconfig": "^2.0.5",
|
||||||
"normalize-path": "^3.0.0",
|
"normalize-path": "^3.0.0",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
|
|
@ -108,10 +108,10 @@
|
||||||
"postcss-nested": "5.0.6",
|
"postcss-nested": "5.0.6",
|
||||||
"postcss-selector-parser": "^6.0.10",
|
"postcss-selector-parser": "^6.0.10",
|
||||||
"postcss-value-parser": "^4.2.0",
|
"postcss-value-parser": "^4.2.0",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^2.7.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.1.11",
|
"prettier-plugin-tailwindcss": "^0.1.11",
|
||||||
"quick-lru": "^5.1.1",
|
"quick-lru": "^5.1.1",
|
||||||
"resolve": "^1.22.0",
|
"resolve": "^1.22.1",
|
||||||
"rimraf": "^3.0.0",
|
"rimraf": "^3.0.0",
|
||||||
"source-map-js": "^1.0.2"
|
"source-map-js": "^1.0.2"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,24 @@ describe('Build command', () => {
|
||||||
expect(withoutMinify.length).toBeGreaterThan(withMinify.length)
|
expect(withoutMinify.length).toBeGreaterThan(withMinify.length)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('--minify applies Lightning CSS optimizations', async () => {
|
||||||
|
await writeInputFile('index.html', html`<div class="font-bold"></div>`)
|
||||||
|
await writeInputFile(
|
||||||
|
'index.css',
|
||||||
|
css`
|
||||||
|
.text-teal {
|
||||||
|
color: hsl(calc(360deg / 2) 50% 50%);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
await $(`${EXECUTABLE} --input ./src/index.css --output ./dist/main.css --minify`)
|
||||||
|
|
||||||
|
let withMinify = await readOutputFile('main.css')
|
||||||
|
|
||||||
|
expect(withMinify).toEqual(`.text-teal{color:#40bfbf}`)
|
||||||
|
})
|
||||||
|
|
||||||
test('--no-autoprefixer', async () => {
|
test('--no-autoprefixer', async () => {
|
||||||
await writeInputFile('index.html', html`<div class="select-none"></div>`)
|
await writeInputFile('index.html', html`<div class="select-none"></div>`)
|
||||||
|
|
||||||
|
|
@ -459,6 +477,7 @@ describe('Build command', () => {
|
||||||
-m, --minify Minify the output
|
-m, --minify Minify the output
|
||||||
-c, --config Path to a custom config file
|
-c, --config Path to a custom config file
|
||||||
--no-autoprefixer Disable autoprefixer
|
--no-autoprefixer Disable autoprefixer
|
||||||
|
--experimental-contextual-paths Enable experimental contextual path resolution
|
||||||
-h, --help Display usage information
|
-h, --help Display usage information
|
||||||
`)
|
`)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preswcify": "npm run generate && rimraf lib",
|
"preswcify": "npm run generate && rimraf lib",
|
||||||
"swcify": "swc src --out-dir lib --copy-files",
|
"swcify": "swc src --out-dir lib --copy-files",
|
||||||
"postswcify": "esbuild lib/cli-peer-dependencies.js --bundle --platform=node --outfile=peers/index.js",
|
"postswcify": "esbuild lib/cli-peer-dependencies.js --bundle --platform=node --outfile=peers/index.js --define:process.env.CSS_TRANSFORMER_WASM=false",
|
||||||
"rebuild-fixtures": "npm run swcify && node -r @swc/register scripts/rebuildFixtures.js",
|
"rebuild-fixtures": "npm run swcify && node -r @swc/register scripts/rebuildFixtures.js",
|
||||||
"prepublishOnly": "npm install --force && npm run swcify",
|
"prepublishOnly": "npm install --force && npm run swcify",
|
||||||
"style": "eslint .",
|
"style": "eslint .",
|
||||||
|
|
@ -47,13 +47,13 @@
|
||||||
"@swc/jest": "^0.2.22",
|
"@swc/jest": "^0.2.22",
|
||||||
"@swc/register": "^0.1.10",
|
"@swc/register": "^0.1.10",
|
||||||
"autoprefixer": "^10.4.8",
|
"autoprefixer": "^10.4.8",
|
||||||
"cssnano": "^5.1.13",
|
|
||||||
"esbuild": "^0.15.7",
|
"esbuild": "^0.15.7",
|
||||||
"eslint": "^8.23.0",
|
"eslint": "^8.23.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"jest": "^28.1.3",
|
"jest": "^28.1.3",
|
||||||
"jest-diff": "^28.1.3",
|
"jest-diff": "^28.1.3",
|
||||||
|
"lightningcss": "^1.11.2",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.1.13",
|
"prettier-plugin-tailwindcss": "^0.1.13",
|
||||||
"rimraf": "^3.0.0",
|
"rimraf": "^3.0.0",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,6 @@ export function lazyAutoprefixer() {
|
||||||
return require('autoprefixer')
|
return require('autoprefixer')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function lazyCssnano() {
|
export function lazyLightningCss() {
|
||||||
return require('cssnano')
|
return require('lightningcss')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
116
src/cli.js
116
src/cli.js
|
|
@ -1,6 +1,11 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { lazyPostcss, lazyPostcssImport, lazyCssnano, lazyAutoprefixer } from '../peers/index.js'
|
import {
|
||||||
|
lazyPostcss,
|
||||||
|
lazyPostcssImport,
|
||||||
|
lazyLightningCss,
|
||||||
|
lazyAutoprefixer,
|
||||||
|
} from '../peers/index.js'
|
||||||
|
|
||||||
import chokidar from 'chokidar'
|
import chokidar from 'chokidar'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
@ -19,6 +24,8 @@ import packageJson from '../package.json'
|
||||||
import normalizePath from 'normalize-path'
|
import normalizePath from 'normalize-path'
|
||||||
import micromatch from 'micromatch'
|
import micromatch from 'micromatch'
|
||||||
import { validateConfig } from './util/validateConfig.js'
|
import { validateConfig } from './util/validateConfig.js'
|
||||||
|
import { parseCandidateFiles } from './lib/content.js'
|
||||||
|
import resolveConfigPath from './util/resolveConfigPath.js'
|
||||||
|
|
||||||
let env = {
|
let env = {
|
||||||
DEBUG: process.env.DEBUG !== undefined && process.env.DEBUG !== '0',
|
DEBUG: process.env.DEBUG !== undefined && process.env.DEBUG !== '0',
|
||||||
|
|
@ -219,6 +226,10 @@ let commands = {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
description: 'Disable autoprefixer',
|
description: 'Disable autoprefixer',
|
||||||
},
|
},
|
||||||
|
'--experimental-contextual-paths': {
|
||||||
|
type: Boolean,
|
||||||
|
description: 'Enable experimental contextual path resolution',
|
||||||
|
},
|
||||||
'-c': '--config',
|
'-c': '--config',
|
||||||
'-i': '--input',
|
'-i': '--input',
|
||||||
'-o': '--output',
|
'-o': '--output',
|
||||||
|
|
@ -353,6 +364,67 @@ if (args['--help']) {
|
||||||
|
|
||||||
run()
|
run()
|
||||||
|
|
||||||
|
/// ---
|
||||||
|
|
||||||
|
/** @type {import('lightningcss')} */
|
||||||
|
let lightningCss
|
||||||
|
|
||||||
|
/** @returns {import('lightningcss')} */
|
||||||
|
function loadLightningCss() {
|
||||||
|
if (lightningCss) {
|
||||||
|
return lightningCss
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to load a local version first
|
||||||
|
try {
|
||||||
|
return (lightningCss = require('lightningcss'))
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return (lightningCss = lazyLightningCss())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {boolean} shouldMinify
|
||||||
|
* @param {import('postcss').Result} result
|
||||||
|
* @returns {import('postcss').Result}
|
||||||
|
*/
|
||||||
|
async function minifyCss(shouldMinify, result) {
|
||||||
|
if (!shouldMinify) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
let css = loadLightningCss()
|
||||||
|
let transformed
|
||||||
|
|
||||||
|
try {
|
||||||
|
transformed = css.transform({
|
||||||
|
filename: result.opts.from || 'input.css',
|
||||||
|
code: Buffer.from(result.css, 'utf-8'),
|
||||||
|
minify: true,
|
||||||
|
sourceMap: true,
|
||||||
|
sourceMap: !!result.map,
|
||||||
|
inputSourceMap: result.map ? result.map.toString() : undefined,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Unable to minify CSS. Using unminified version instead.')
|
||||||
|
console.error(err)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.assign(result, {
|
||||||
|
css: transformed.code.toString('utf8'),
|
||||||
|
map: result.map
|
||||||
|
? Object.assign(result.map, {
|
||||||
|
toString() {
|
||||||
|
return transformed.map.toString()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: result.map,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
|
@ -429,6 +501,7 @@ async function build() {
|
||||||
process.exit(9)
|
process.exit(9)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Reference the @config path here if exists
|
||||||
let configPath = args['--config']
|
let configPath = args['--config']
|
||||||
? args['--config']
|
? args['--config']
|
||||||
: ((defaultPath) => (fs.existsSync(defaultPath) ? defaultPath : null))(
|
: ((defaultPath) => (fs.existsSync(defaultPath) ? defaultPath : null))(
|
||||||
|
|
@ -551,14 +624,14 @@ async function build() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractFileGlobs(config) {
|
function extractFileGlobs(config) {
|
||||||
return config.content.files
|
let context = {
|
||||||
.filter((file) => {
|
tailwindConfig: config,
|
||||||
// Strings in this case are files / globs. If it is something else,
|
userConfigPath: configPath,
|
||||||
// like an object it's probably a raw content object. But this object
|
}
|
||||||
// is not watchable, so let's remove it.
|
|
||||||
return typeof file === 'string'
|
let contentPaths = parseCandidateFiles(context, config)
|
||||||
})
|
|
||||||
.map((glob) => normalizePath(glob))
|
return contentPaths.map((contentPath) => contentPath.pattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractRawContent(config) {
|
function extractRawContent(config) {
|
||||||
|
|
@ -626,17 +699,6 @@ async function build() {
|
||||||
|
|
||||||
return lazyAutoprefixer()
|
return lazyAutoprefixer()
|
||||||
})(),
|
})(),
|
||||||
args['--minify'] &&
|
|
||||||
(() => {
|
|
||||||
let options = { preset: ['default', { cssDeclarationSorter: false }] }
|
|
||||||
|
|
||||||
// Try to load a local `cssnano` version first
|
|
||||||
try {
|
|
||||||
return require('cssnano')
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
return lazyCssnano()(options)
|
|
||||||
})(),
|
|
||||||
].filter(Boolean)
|
].filter(Boolean)
|
||||||
|
|
||||||
let postcss = loadPostcss()
|
let postcss = loadPostcss()
|
||||||
|
|
@ -647,6 +709,7 @@ async function build() {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => (output ? fs.promises.mkdir(path.dirname(output), { recursive: true }) : null))
|
.then(() => (output ? fs.promises.mkdir(path.dirname(output), { recursive: true }) : null))
|
||||||
.then(() => processor.process(css, { ...postcssOptions, from: input, to: output }))
|
.then(() => processor.process(css, { ...postcssOptions, from: input, to: output }))
|
||||||
|
.then((result) => minifyCss(!!args['--minify'], result))
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (!output) {
|
if (!output) {
|
||||||
return process.stdout.write(result.css)
|
return process.stdout.write(result.css)
|
||||||
|
|
@ -728,17 +791,6 @@ async function build() {
|
||||||
|
|
||||||
return lazyAutoprefixer()
|
return lazyAutoprefixer()
|
||||||
})(),
|
})(),
|
||||||
args['--minify'] &&
|
|
||||||
(() => {
|
|
||||||
let options = { preset: ['default', { cssDeclarationSorter: false }] }
|
|
||||||
|
|
||||||
// Try to load a local `cssnano` version first
|
|
||||||
try {
|
|
||||||
return require('cssnano')
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
return lazyCssnano()(options)
|
|
||||||
})(),
|
|
||||||
].filter(Boolean)
|
].filter(Boolean)
|
||||||
|
|
||||||
async function rebuild(config) {
|
async function rebuild(config) {
|
||||||
|
|
@ -785,6 +837,7 @@ async function build() {
|
||||||
output ? fs.promises.mkdir(path.dirname(output), { recursive: true }) : null
|
output ? fs.promises.mkdir(path.dirname(output), { recursive: true }) : null
|
||||||
)
|
)
|
||||||
.then(() => processor.process(css, { from: input, to: output }))
|
.then(() => processor.process(css, { from: input, to: output }))
|
||||||
|
.then((result) => minifyCss(!!args['--minify'], result))
|
||||||
.then(async (result) => {
|
.then(async (result) => {
|
||||||
for (let message of result.messages) {
|
for (let message of result.messages) {
|
||||||
if (message.type === 'dependency') {
|
if (message.type === 'dependency') {
|
||||||
|
|
@ -845,6 +898,7 @@ async function build() {
|
||||||
* @return {{all: string[], dynamic: string[], static: string[]}}
|
* @return {{all: string[], dynamic: string[], static: string[]}}
|
||||||
**/
|
**/
|
||||||
function refreshContentPatterns(config) {
|
function refreshContentPatterns(config) {
|
||||||
|
// TODO: This can be optimized since we're using `fastGlob.generateTasks` indirectly in extractFileGlobs
|
||||||
let globs = extractFileGlobs(config)
|
let globs = extractFileGlobs(config)
|
||||||
let tasks = fastGlob.generateTasks(globs, { absolute: true })
|
let tasks = fastGlob.generateTasks(globs, { absolute: true })
|
||||||
let dynamicPatterns = tasks.filter((task) => task.dynamic).flatMap((task) => task.patterns)
|
let dynamicPatterns = tasks.filter((task) => task.dynamic).flatMap((task) => task.patterns)
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,13 @@ let featureFlags = {
|
||||||
'hoverOnlyWhenSupported',
|
'hoverOnlyWhenSupported',
|
||||||
'respectDefaultRingColorOpacity',
|
'respectDefaultRingColorOpacity',
|
||||||
'disableColorOpacityUtilitiesByDefault',
|
'disableColorOpacityUtilitiesByDefault',
|
||||||
|
'relativeContentPathsByDefault',
|
||||||
|
],
|
||||||
|
experimental: [
|
||||||
|
'optimizeUniversalDefaults',
|
||||||
|
'matchVariant',
|
||||||
|
// 'variantGrouping',
|
||||||
],
|
],
|
||||||
experimental: ['optimizeUniversalDefaults', 'matchVariant' /* , 'variantGrouping' */],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function flagEnabled(config, flag) {
|
export function flagEnabled(config, flag) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import setupTrackingContext from './lib/setupTrackingContext'
|
import setupTrackingContext from './lib/setupTrackingContext'
|
||||||
import processTailwindFeatures from './processTailwindFeatures'
|
import processTailwindFeatures from './processTailwindFeatures'
|
||||||
import { env } from './lib/sharedState'
|
import { env } from './lib/sharedState'
|
||||||
|
import { findAtConfigPath } from './lib/findAtConfigPath'
|
||||||
|
|
||||||
module.exports = function tailwindcss(configOrPath) {
|
module.exports = function tailwindcss(configOrPath) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -13,6 +14,10 @@ module.exports = function tailwindcss(configOrPath) {
|
||||||
return root
|
return root
|
||||||
},
|
},
|
||||||
function (root, result) {
|
function (root, result) {
|
||||||
|
// Use the path for the `@config` directive if it exists, otherwise use the
|
||||||
|
// path for the file being processed
|
||||||
|
configOrPath = findAtConfigPath(root, result) ?? configOrPath
|
||||||
|
|
||||||
let context = setupTrackingContext(configOrPath)
|
let context = setupTrackingContext(configOrPath)
|
||||||
|
|
||||||
if (root.type === 'document') {
|
if (root.type === 'document') {
|
||||||
|
|
|
||||||
200
src/lib/content.js
Normal file
200
src/lib/content.js
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import isGlob from 'is-glob'
|
||||||
|
import fastGlob from 'fast-glob'
|
||||||
|
import normalizePath from 'normalize-path'
|
||||||
|
import { flagEnabled } from '../featureFlags.js'
|
||||||
|
import { parseGlob } from '../util/parseGlob'
|
||||||
|
import { env } from './sharedState'
|
||||||
|
|
||||||
|
/** @typedef {import('../../types/config.js').RawFile} RawFile */
|
||||||
|
/** @typedef {import('../../types/config.js').FilePath} FilePath */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} ContentPath
|
||||||
|
* @property {string} original
|
||||||
|
* @property {string} base
|
||||||
|
* @property {string | null} glob
|
||||||
|
* @property {boolean} ignore
|
||||||
|
* @property {string} pattern
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn a list of content paths (absolute or not; glob or not) into a list of
|
||||||
|
* absolute file paths that exist on the filesystem
|
||||||
|
*
|
||||||
|
* If there are symlinks in the path then multiple paths will be returned
|
||||||
|
* one for the symlink and one for the actual file
|
||||||
|
*
|
||||||
|
* @param {*} context
|
||||||
|
* @param {import('tailwindcss').Config} tailwindConfig
|
||||||
|
* @returns {ContentPath[]}
|
||||||
|
*/
|
||||||
|
export function parseCandidateFiles(context, tailwindConfig) {
|
||||||
|
let files = tailwindConfig.content.files
|
||||||
|
|
||||||
|
// Normalize the file globs
|
||||||
|
files = files.filter((filePath) => typeof filePath === 'string')
|
||||||
|
files = files.map(normalizePath)
|
||||||
|
|
||||||
|
// Split into included and excluded globs
|
||||||
|
let tasks = fastGlob.generateTasks(files)
|
||||||
|
|
||||||
|
/** @type {ContentPath[]} */
|
||||||
|
let included = []
|
||||||
|
|
||||||
|
/** @type {ContentPath[]} */
|
||||||
|
let excluded = []
|
||||||
|
|
||||||
|
for (const task of tasks) {
|
||||||
|
included.push(...task.positive.map((filePath) => parseFilePath(filePath, false)))
|
||||||
|
excluded.push(...task.negative.map((filePath) => parseFilePath(filePath, true)))
|
||||||
|
}
|
||||||
|
|
||||||
|
let paths = [...included, ...excluded]
|
||||||
|
|
||||||
|
// Resolve paths relative to the config file or cwd
|
||||||
|
paths = resolveRelativePaths(context, paths)
|
||||||
|
|
||||||
|
// Resolve symlinks if possible
|
||||||
|
paths = paths.flatMap(resolvePathSymlinks)
|
||||||
|
|
||||||
|
// Update cached patterns
|
||||||
|
paths = paths.map(resolveGlobPattern)
|
||||||
|
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} filePath
|
||||||
|
* @param {boolean} ignore
|
||||||
|
* @returns {ContentPath}
|
||||||
|
*/
|
||||||
|
function parseFilePath(filePath, ignore) {
|
||||||
|
let contentPath = {
|
||||||
|
original: filePath,
|
||||||
|
base: filePath,
|
||||||
|
ignore,
|
||||||
|
pattern: filePath,
|
||||||
|
glob: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGlob(filePath)) {
|
||||||
|
Object.assign(contentPath, parseGlob(filePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
return contentPath
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {ContentPath} contentPath
|
||||||
|
* @returns {ContentPath}
|
||||||
|
*/
|
||||||
|
function resolveGlobPattern(contentPath) {
|
||||||
|
contentPath.pattern = contentPath.glob
|
||||||
|
? `${contentPath.base}/${contentPath.glob}`
|
||||||
|
: contentPath.base
|
||||||
|
|
||||||
|
contentPath.pattern = contentPath.ignore ? `!${contentPath.pattern}` : contentPath.pattern
|
||||||
|
|
||||||
|
return contentPath
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve each path relative to the config file (when possible) if the experimental flag is enabled
|
||||||
|
* Otherwise, resolve relative to the current working directory
|
||||||
|
*
|
||||||
|
* @param {any} context
|
||||||
|
* @param {ContentPath[]} contentPaths
|
||||||
|
* @returns {ContentPath[]}
|
||||||
|
*/
|
||||||
|
function resolveRelativePaths(context, contentPaths) {
|
||||||
|
let resolveFrom = []
|
||||||
|
|
||||||
|
// Resolve base paths relative to the config file (when possible) if the experimental flag is enabled
|
||||||
|
if (context.userConfigPath && context.tailwindConfig.content.relative) {
|
||||||
|
resolveFrom = [path.dirname(context.userConfigPath)]
|
||||||
|
}
|
||||||
|
|
||||||
|
return contentPaths.map((contentPath) => {
|
||||||
|
contentPath.base = path.resolve(...resolveFrom, contentPath.base)
|
||||||
|
|
||||||
|
return contentPath
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the symlink for the base directory / file in each path
|
||||||
|
* These are added as additional dependencies to watch for changes because
|
||||||
|
* some tools (like webpack) will only watch the actual file or directory
|
||||||
|
* but not the symlink itself even in projects that use monorepos.
|
||||||
|
*
|
||||||
|
* @param {ContentPath} contentPath
|
||||||
|
* @returns {ContentPath[]}
|
||||||
|
*/
|
||||||
|
function resolvePathSymlinks(contentPath) {
|
||||||
|
let paths = [contentPath]
|
||||||
|
|
||||||
|
try {
|
||||||
|
let resolvedPath = fs.realpathSync(contentPath.base)
|
||||||
|
if (resolvedPath !== contentPath.base) {
|
||||||
|
paths.push({
|
||||||
|
...contentPath,
|
||||||
|
base: resolvedPath,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// TODO: log this?
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} context
|
||||||
|
* @param {ContentPath[]} candidateFiles
|
||||||
|
* @param {Map<string, number>} fileModifiedMap
|
||||||
|
* @returns {{ content: string, extension: string }[]}
|
||||||
|
*/
|
||||||
|
export function resolvedChangedContent(context, candidateFiles, fileModifiedMap) {
|
||||||
|
let changedContent = context.tailwindConfig.content.files
|
||||||
|
.filter((item) => typeof item.raw === 'string')
|
||||||
|
.map(({ raw, extension = 'html' }) => ({ content: raw, extension }))
|
||||||
|
|
||||||
|
for (let changedFile of resolveChangedFiles(candidateFiles, fileModifiedMap)) {
|
||||||
|
let content = fs.readFileSync(changedFile, 'utf8')
|
||||||
|
let extension = path.extname(changedFile).slice(1)
|
||||||
|
changedContent.push({ content, extension })
|
||||||
|
}
|
||||||
|
|
||||||
|
return changedContent
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {ContentPath[]} candidateFiles
|
||||||
|
* @param {Map<string, number>} fileModifiedMap
|
||||||
|
* @returns {Set<string>}
|
||||||
|
*/
|
||||||
|
function resolveChangedFiles(candidateFiles, fileModifiedMap) {
|
||||||
|
let paths = candidateFiles.map((contentPath) => contentPath.pattern)
|
||||||
|
|
||||||
|
let changedFiles = new Set()
|
||||||
|
env.DEBUG && console.time('Finding changed files')
|
||||||
|
let files = fastGlob.sync(paths, { absolute: true })
|
||||||
|
for (let file of files) {
|
||||||
|
let prevModified = fileModifiedMap.has(file) ? fileModifiedMap.get(file) : -Infinity
|
||||||
|
let modified = fs.statSync(file).mtimeMs
|
||||||
|
|
||||||
|
if (modified > prevModified) {
|
||||||
|
changedFiles.add(file)
|
||||||
|
fileModifiedMap.set(file, modified)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env.DEBUG && console.timeEnd('Finding changed files')
|
||||||
|
return changedFiles
|
||||||
|
}
|
||||||
33
src/lib/findAtConfigPath.js
Normal file
33
src/lib/findAtConfigPath.js
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the @config at-rule in the given CSS AST and return the relative path to the config file
|
||||||
|
*
|
||||||
|
* @param {import('postcss').Root} root
|
||||||
|
* @param {import('postcss').Result} result
|
||||||
|
*/
|
||||||
|
export function findAtConfigPath(root, result) {
|
||||||
|
let configPath = null
|
||||||
|
let relativeTo = root.source.input.file ?? result.opts.from ?? null
|
||||||
|
|
||||||
|
root.walkAtRules('config', (rule) => {
|
||||||
|
if (relativeTo === undefined) {
|
||||||
|
throw rule.error(
|
||||||
|
'The `@config` at-rule cannot be used without a `from` option being set on the PostCSS config.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configPath) {
|
||||||
|
throw rule.error('Only `@config` at-rule is allowed per file.')
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath = rule.params.replace(/^['"]|['"]$/g, '')
|
||||||
|
rule.remove()
|
||||||
|
|
||||||
|
if (path.isAbsolute(configPath)) {
|
||||||
|
throw rule.error('The `@config` at-rule cannot be used with an absolute path.')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return configPath ? path.resolve(path.dirname(relativeTo), configPath) : null
|
||||||
|
}
|
||||||
|
|
@ -997,6 +997,10 @@ export function getContext(
|
||||||
|
|
||||||
let context = createContext(tailwindConfig, [], root)
|
let context = createContext(tailwindConfig, [], root)
|
||||||
|
|
||||||
|
Object.assign(context, {
|
||||||
|
userConfigPath,
|
||||||
|
})
|
||||||
|
|
||||||
trackModified([...contextDependencies], getFileModifiedMap(context))
|
trackModified([...contextDependencies], getFileModifiedMap(context))
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,14 @@
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
import fastGlob from 'fast-glob'
|
|
||||||
import LRU from 'quick-lru'
|
import LRU from 'quick-lru'
|
||||||
import normalizePath from 'normalize-path'
|
|
||||||
|
|
||||||
import hash from '../util/hashConfig'
|
import hash from '../util/hashConfig'
|
||||||
import getModuleDependencies from '../lib/getModuleDependencies'
|
import getModuleDependencies from '../lib/getModuleDependencies'
|
||||||
|
|
||||||
import resolveConfig from '../public/resolve-config'
|
import resolveConfig from '../public/resolve-config'
|
||||||
|
|
||||||
import resolveConfigPath from '../util/resolveConfigPath'
|
import resolveConfigPath from '../util/resolveConfigPath'
|
||||||
|
|
||||||
import { env } from './sharedState'
|
|
||||||
|
|
||||||
import { getContext, getFileModifiedMap } from './setupContextUtils'
|
import { getContext, getFileModifiedMap } from './setupContextUtils'
|
||||||
import parseDependency from '../util/parseDependency'
|
import parseDependency from '../util/parseDependency'
|
||||||
import { validateConfig } from '../util/validateConfig.js'
|
import { validateConfig } from '../util/validateConfig.js'
|
||||||
|
import { parseCandidateFiles, resolvedChangedContent } from './content.js'
|
||||||
|
|
||||||
let configPathCache = new LRU({ maxSize: 100 })
|
let configPathCache = new LRU({ maxSize: 100 })
|
||||||
|
|
||||||
|
|
@ -27,9 +19,7 @@ function getCandidateFiles(context, tailwindConfig) {
|
||||||
return candidateFilesCache.get(context)
|
return candidateFilesCache.get(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
let candidateFiles = tailwindConfig.content.files
|
let candidateFiles = parseCandidateFiles(context, tailwindConfig)
|
||||||
.filter((item) => typeof item === 'string')
|
|
||||||
.map((contentPath) => normalizePath(contentPath))
|
|
||||||
|
|
||||||
return candidateFilesCache.set(context, candidateFiles).get(context)
|
return candidateFilesCache.set(context, candidateFiles).get(context)
|
||||||
}
|
}
|
||||||
|
|
@ -80,36 +70,6 @@ function getTailwindConfig(configOrPath) {
|
||||||
return [newConfig, null, hash(newConfig), []]
|
return [newConfig, null, hash(newConfig), []]
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvedChangedContent(context, candidateFiles, fileModifiedMap) {
|
|
||||||
let changedContent = context.tailwindConfig.content.files
|
|
||||||
.filter((item) => typeof item.raw === 'string')
|
|
||||||
.map(({ raw, extension = 'html' }) => ({ content: raw, extension }))
|
|
||||||
|
|
||||||
for (let changedFile of resolveChangedFiles(candidateFiles, fileModifiedMap)) {
|
|
||||||
let content = fs.readFileSync(changedFile, 'utf8')
|
|
||||||
let extension = path.extname(changedFile).slice(1)
|
|
||||||
changedContent.push({ content, extension })
|
|
||||||
}
|
|
||||||
return changedContent
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveChangedFiles(candidateFiles, fileModifiedMap) {
|
|
||||||
let changedFiles = new Set()
|
|
||||||
env.DEBUG && console.time('Finding changed files')
|
|
||||||
let files = fastGlob.sync(candidateFiles, { absolute: true })
|
|
||||||
for (let file of files) {
|
|
||||||
let prevModified = fileModifiedMap.has(file) ? fileModifiedMap.get(file) : -Infinity
|
|
||||||
let modified = fs.statSync(file).mtimeMs
|
|
||||||
|
|
||||||
if (modified > prevModified) {
|
|
||||||
changedFiles.add(file)
|
|
||||||
fileModifiedMap.set(file, modified)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
env.DEBUG && console.timeEnd('Finding changed files')
|
|
||||||
return changedFiles
|
|
||||||
}
|
|
||||||
|
|
||||||
// DISABLE_TOUCH = TRUE
|
// DISABLE_TOUCH = TRUE
|
||||||
|
|
||||||
// Retrieve an existing context from cache if possible (since contexts are unique per
|
// Retrieve an existing context from cache if possible (since contexts are unique per
|
||||||
|
|
@ -161,9 +121,8 @@ export default function setupTrackingContext(configOrPath) {
|
||||||
let fileModifiedMap = getFileModifiedMap(context)
|
let fileModifiedMap = getFileModifiedMap(context)
|
||||||
|
|
||||||
// Add template paths as postcss dependencies.
|
// Add template paths as postcss dependencies.
|
||||||
for (let fileOrGlob of candidateFiles) {
|
for (let contentPath of candidateFiles) {
|
||||||
let dependency = parseDependency(fileOrGlob)
|
for (let dependency of parseDependency(contentPath)) {
|
||||||
if (dependency) {
|
|
||||||
registerDependency(dependency)
|
registerDependency(dependency)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,9 +56,11 @@ export function normalizeConfig(config) {
|
||||||
|
|
||||||
// When `config.content` is an object
|
// When `config.content` is an object
|
||||||
if (typeof config.content === 'object' && config.content !== null) {
|
if (typeof config.content === 'object' && config.content !== null) {
|
||||||
// Only `files`, `extract` and `transform` can exist in `config.content`
|
// Only `files`, `relative`, `extract`, and `transform` can exist in `config.content`
|
||||||
if (
|
if (
|
||||||
Object.keys(config.content).some((key) => !['files', 'extract', 'transform'].includes(key))
|
Object.keys(config.content).some(
|
||||||
|
(key) => !['files', 'relative', 'extract', 'transform'].includes(key)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -112,6 +114,14 @@ export function normalizeConfig(config) {
|
||||||
) {
|
) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `config.content.relative` is optional and can be a boolean
|
||||||
|
if (
|
||||||
|
typeof config.content.relative !== 'boolean' &&
|
||||||
|
typeof config.content.relative !== 'undefined'
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
@ -154,6 +164,16 @@ export function normalizeConfig(config) {
|
||||||
|
|
||||||
// Normalize the `content`
|
// Normalize the `content`
|
||||||
config.content = {
|
config.content = {
|
||||||
|
relative: (() => {
|
||||||
|
let { content } = config
|
||||||
|
|
||||||
|
if (content?.relative) {
|
||||||
|
return content.relative
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.future?.relativeContentPathsByDefault ?? false
|
||||||
|
})(),
|
||||||
|
|
||||||
files: (() => {
|
files: (() => {
|
||||||
let { content, purge } = config
|
let { content, purge } = config
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,44 @@
|
||||||
import isGlob from 'is-glob'
|
// @ts-check
|
||||||
import globParent from 'glob-parent'
|
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
// Based on `glob-base`
|
/**
|
||||||
// https://github.com/micromatch/glob-base/blob/master/index.js
|
* @typedef {{type: 'dependency', file: string} | {type: 'dir-dependency', dir: string, glob: string}} Dependency
|
||||||
function parseGlob(pattern) {
|
*/
|
||||||
let glob = pattern
|
|
||||||
let base = globParent(pattern)
|
|
||||||
|
|
||||||
if (base !== '.') {
|
/**
|
||||||
glob = pattern.substr(base.length)
|
*
|
||||||
if (glob.charAt(0) === '/') {
|
* @param {import('../lib/content.js').ContentPath} contentPath
|
||||||
glob = glob.substr(1)
|
* @returns {Dependency[]}
|
||||||
}
|
*/
|
||||||
|
export default function parseDependency(contentPath) {
|
||||||
|
if (contentPath.ignore) {
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (glob.substr(0, 2) === './') {
|
if (!contentPath.glob) {
|
||||||
glob = glob.substr(2)
|
return [
|
||||||
}
|
{
|
||||||
if (glob.charAt(0) === '/') {
|
type: 'dependency',
|
||||||
glob = glob.substr(1)
|
file: contentPath.base,
|
||||||
}
|
},
|
||||||
|
]
|
||||||
return { base, glob }
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function parseDependency(normalizedFileOrGlob) {
|
|
||||||
if (normalizedFileOrGlob.startsWith('!')) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
let message
|
|
||||||
|
|
||||||
if (isGlob(normalizedFileOrGlob)) {
|
|
||||||
let { base, glob } = parseGlob(normalizedFileOrGlob)
|
|
||||||
message = { type: 'dir-dependency', dir: path.resolve(base), glob }
|
|
||||||
} else {
|
|
||||||
message = { type: 'dependency', file: path.resolve(normalizedFileOrGlob) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.ROLLUP_WATCH === 'true') {
|
||||||
// rollup-plugin-postcss does not support dir-dependency messages
|
// rollup-plugin-postcss does not support dir-dependency messages
|
||||||
// but directories can be watched in the same way as files
|
// but directories can be watched in the same way as files
|
||||||
if (message.type === 'dir-dependency' && process.env.ROLLUP_WATCH === 'true') {
|
return [
|
||||||
message = { type: 'dependency', file: message.dir }
|
{
|
||||||
|
type: 'dependency',
|
||||||
|
file: contentPath.base,
|
||||||
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
return message
|
return [
|
||||||
|
{
|
||||||
|
type: 'dir-dependency',
|
||||||
|
dir: contentPath.base,
|
||||||
|
glob: contentPath.glob,
|
||||||
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
24
src/util/parseGlob.js
Normal file
24
src/util/parseGlob.js
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import globParent from 'glob-parent'
|
||||||
|
|
||||||
|
// Based on `glob-base`
|
||||||
|
// https://github.com/micromatch/glob-base/blob/master/index.js
|
||||||
|
export function parseGlob(pattern) {
|
||||||
|
let glob = pattern
|
||||||
|
let base = globParent(pattern)
|
||||||
|
|
||||||
|
if (base !== '.') {
|
||||||
|
glob = pattern.substr(base.length)
|
||||||
|
if (glob.charAt(0) === '/') {
|
||||||
|
glob = glob.substr(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (glob.substr(0, 2) === './') {
|
||||||
|
glob = glob.substr(2)
|
||||||
|
}
|
||||||
|
if (glob.charAt(0) === '/') {
|
||||||
|
glob = glob.substr(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { base, glob }
|
||||||
|
}
|
||||||
1
types/config.d.ts
vendored
1
types/config.d.ts
vendored
|
|
@ -31,6 +31,7 @@ type ContentConfig =
|
||||||
| (FilePath | RawFile)[]
|
| (FilePath | RawFile)[]
|
||||||
| {
|
| {
|
||||||
files: (FilePath | RawFile)[]
|
files: (FilePath | RawFile)[]
|
||||||
|
relative?: boolean
|
||||||
extract?: ExtractorFn | { [extension: string]: ExtractorFn }
|
extract?: ExtractorFn | { [extension: string]: ExtractorFn }
|
||||||
transform?: TransformerFn | { [extension: string]: TransformerFn }
|
transform?: TransformerFn | { [extension: string]: TransformerFn }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue