diff --git a/integrations/tailwindcss-cli/package-lock.json b/integrations/tailwindcss-cli/package-lock.json index e36028945..1bfff83a7 100644 --- a/integrations/tailwindcss-cli/package-lock.json +++ b/integrations/tailwindcss-cli/package-lock.json @@ -12,13 +12,13 @@ } }, "../..": { - "version": "3.0.24", + "version": "3.1.5", "license": "MIT", "dependencies": { - "arg": "^5.0.1", + "arg": "^5.0.2", "chokidar": "^3.5.3", "color-name": "^1.1.4", - "detective": "^5.2.0", + "detective": "^5.2.1", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.11", @@ -36,26 +36,26 @@ "postcss-selector-parser": "^6.0.10", "postcss-value-parser": "^4.2.0", "quick-lru": "^5.1.1", - "resolve": "^1.22.0" + "resolve": "^1.22.1" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" }, "devDependencies": { + "@parcel/css": "^1.11.2", "@swc/cli": "^0.1.57", - "@swc/core": "^1.2.160", + "@swc/core": "^1.2.196", "@swc/jest": "^0.2.21", "@swc/register": "^0.1.10", "autoprefixer": "^10.4.7", - "cssnano": "^5.1.9", - "esbuild": "^0.14.39", - "eslint": "^8.15.0", + "esbuild": "^0.14.48", + "eslint": "^8.18.0", "eslint-config-prettier": "^8.5.0", - "eslint-plugin-prettier": "^4.0.0", - "jest": "^28.0.3", - "jest-diff": "^28.1.0", - "prettier": "^2.6.2", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^28.1.2", + "jest-diff": "^28.1.1", + "prettier": "^2.7.1", "prettier-plugin-tailwindcss": "^0.1.11", "rimraf": "^3.0.0", "source-map-js": "^1.0.2" @@ -76,27 +76,27 @@ "tailwindcss": { "version": "file:../..", "requires": { + "@parcel/css": "^1.11.2", "@swc/cli": "^0.1.57", - "@swc/core": "^1.2.160", + "@swc/core": "^1.2.196", "@swc/jest": "^0.2.21", "@swc/register": "^0.1.10", - "arg": "^5.0.1", + "arg": "^5.0.2", "autoprefixer": "^10.4.7", "chokidar": "^3.5.3", "color-name": "^1.1.4", - "cssnano": "^5.1.9", - "detective": "^5.2.0", + "detective": "^5.2.1", "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "esbuild": "^0.14.39", - "eslint": "^8.15.0", + "esbuild": "^0.14.48", + "eslint": "^8.18.0", "eslint-config-prettier": "^8.5.0", - "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-prettier": "^4.2.1", "fast-glob": "^3.2.11", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jest": "^28.0.3", - "jest-diff": "^28.1.0", + "jest": "^28.1.2", + "jest-diff": "^28.1.1", "lilconfig": "^2.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", @@ -108,10 +108,10 @@ "postcss-nested": "5.0.6", "postcss-selector-parser": "^6.0.10", "postcss-value-parser": "^4.2.0", - "prettier": "^2.6.2", + "prettier": "^2.7.1", "prettier-plugin-tailwindcss": "^0.1.11", "quick-lru": "^5.1.1", - "resolve": "^1.22.0", + "resolve": "^1.22.1", "rimraf": "^3.0.0", "source-map-js": "^1.0.2" } diff --git a/integrations/tailwindcss-cli/tests/cli.test.js b/integrations/tailwindcss-cli/tests/cli.test.js index 9ebcd3012..7c8200225 100644 --- a/integrations/tailwindcss-cli/tests/cli.test.js +++ b/integrations/tailwindcss-cli/tests/cli.test.js @@ -96,6 +96,24 @@ describe('Build command', () => { expect(withoutMinify.length).toBeGreaterThan(withMinify.length) }) + test('--minify applies Lightning CSS optimizations', async () => { + await writeInputFile('index.html', html`
`) + 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 () => { await writeInputFile('index.html', html`
`) @@ -459,6 +477,7 @@ describe('Build command', () => { -m, --minify Minify the output -c, --config Path to a custom config file --no-autoprefixer Disable autoprefixer + --experimental-contextual-paths Enable experimental contextual path resolution -h, --help Display usage information `) ) diff --git a/package.json b/package.json index 5b449cc86..59f147669 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "scripts": { "preswcify": "npm run generate && rimraf lib", "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", "prepublishOnly": "npm install --force && npm run swcify", "style": "eslint .", @@ -47,13 +47,13 @@ "@swc/jest": "^0.2.22", "@swc/register": "^0.1.10", "autoprefixer": "^10.4.8", - "cssnano": "^5.1.13", "esbuild": "^0.15.7", "eslint": "^8.23.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", "jest": "^28.1.3", "jest-diff": "^28.1.3", + "lightningcss": "^1.11.2", "prettier": "^2.7.1", "prettier-plugin-tailwindcss": "^0.1.13", "rimraf": "^3.0.0", diff --git a/src/cli-peer-dependencies.js b/src/cli-peer-dependencies.js index 6b9f986aa..4c9c6c339 100644 --- a/src/cli-peer-dependencies.js +++ b/src/cli-peer-dependencies.js @@ -10,6 +10,6 @@ export function lazyAutoprefixer() { return require('autoprefixer') } -export function lazyCssnano() { - return require('cssnano') +export function lazyLightningCss() { + return require('lightningcss') } diff --git a/src/cli.js b/src/cli.js index 2851a79f1..657321bb7 100644 --- a/src/cli.js +++ b/src/cli.js @@ -1,6 +1,11 @@ #!/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 path from 'path' @@ -19,6 +24,8 @@ import packageJson from '../package.json' import normalizePath from 'normalize-path' import micromatch from 'micromatch' import { validateConfig } from './util/validateConfig.js' +import { parseCandidateFiles } from './lib/content.js' +import resolveConfigPath from './util/resolveConfigPath.js' let env = { DEBUG: process.env.DEBUG !== undefined && process.env.DEBUG !== '0', @@ -219,6 +226,10 @@ let commands = { type: Boolean, description: 'Disable autoprefixer', }, + '--experimental-contextual-paths': { + type: Boolean, + description: 'Enable experimental contextual path resolution', + }, '-c': '--config', '-i': '--input', '-o': '--output', @@ -353,6 +364,67 @@ if (args['--help']) { 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() { @@ -429,6 +501,7 @@ async function build() { process.exit(9) } + // TODO: Reference the @config path here if exists let configPath = args['--config'] ? args['--config'] : ((defaultPath) => (fs.existsSync(defaultPath) ? defaultPath : null))( @@ -551,14 +624,14 @@ async function build() { } function extractFileGlobs(config) { - return config.content.files - .filter((file) => { - // Strings in this case are files / globs. If it is something else, - // 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' - }) - .map((glob) => normalizePath(glob)) + let context = { + tailwindConfig: config, + userConfigPath: configPath, + } + + let contentPaths = parseCandidateFiles(context, config) + + return contentPaths.map((contentPath) => contentPath.pattern) } function extractRawContent(config) { @@ -626,17 +699,6 @@ async function build() { 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) let postcss = loadPostcss() @@ -647,6 +709,7 @@ async function build() { return Promise.resolve() .then(() => (output ? fs.promises.mkdir(path.dirname(output), { recursive: true }) : null)) .then(() => processor.process(css, { ...postcssOptions, from: input, to: output })) + .then((result) => minifyCss(!!args['--minify'], result)) .then((result) => { if (!output) { return process.stdout.write(result.css) @@ -728,17 +791,6 @@ async function build() { 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) async function rebuild(config) { @@ -785,6 +837,7 @@ async function build() { output ? fs.promises.mkdir(path.dirname(output), { recursive: true }) : null ) .then(() => processor.process(css, { from: input, to: output })) + .then((result) => minifyCss(!!args['--minify'], result)) .then(async (result) => { for (let message of result.messages) { if (message.type === 'dependency') { @@ -845,6 +898,7 @@ async function build() { * @return {{all: string[], dynamic: string[], static: string[]}} **/ function refreshContentPatterns(config) { + // TODO: This can be optimized since we're using `fastGlob.generateTasks` indirectly in extractFileGlobs let globs = extractFileGlobs(config) let tasks = fastGlob.generateTasks(globs, { absolute: true }) let dynamicPatterns = tasks.filter((task) => task.dynamic).flatMap((task) => task.patterns) diff --git a/src/featureFlags.js b/src/featureFlags.js index 5e9d73583..a03916c5b 100644 --- a/src/featureFlags.js +++ b/src/featureFlags.js @@ -10,8 +10,13 @@ let featureFlags = { 'hoverOnlyWhenSupported', 'respectDefaultRingColorOpacity', 'disableColorOpacityUtilitiesByDefault', + 'relativeContentPathsByDefault', + ], + experimental: [ + 'optimizeUniversalDefaults', + 'matchVariant', + // 'variantGrouping', ], - experimental: ['optimizeUniversalDefaults', 'matchVariant' /* , 'variantGrouping' */], } export function flagEnabled(config, flag) { diff --git a/src/index.js b/src/index.js index 9e9e291eb..7dc41c317 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,7 @@ import setupTrackingContext from './lib/setupTrackingContext' import processTailwindFeatures from './processTailwindFeatures' import { env } from './lib/sharedState' +import { findAtConfigPath } from './lib/findAtConfigPath' module.exports = function tailwindcss(configOrPath) { return { @@ -13,6 +14,10 @@ module.exports = function tailwindcss(configOrPath) { return root }, 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) if (root.type === 'document') { diff --git a/src/lib/content.js b/src/lib/content.js new file mode 100644 index 000000000..96d03d345 --- /dev/null +++ b/src/lib/content.js @@ -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} 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} fileModifiedMap + * @returns {Set} + */ +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 +} diff --git a/src/lib/findAtConfigPath.js b/src/lib/findAtConfigPath.js new file mode 100644 index 000000000..22702bc2c --- /dev/null +++ b/src/lib/findAtConfigPath.js @@ -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 +} diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index cb3bd1736..2b0fadabd 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -997,6 +997,10 @@ export function getContext( let context = createContext(tailwindConfig, [], root) + Object.assign(context, { + userConfigPath, + }) + trackModified([...contextDependencies], getFileModifiedMap(context)) // --- diff --git a/src/lib/setupTrackingContext.js b/src/lib/setupTrackingContext.js index 37b42cdba..3e51bd9e1 100644 --- a/src/lib/setupTrackingContext.js +++ b/src/lib/setupTrackingContext.js @@ -1,22 +1,14 @@ import fs from 'fs' -import path from 'path' - -import fastGlob from 'fast-glob' import LRU from 'quick-lru' -import normalizePath from 'normalize-path' import hash from '../util/hashConfig' import getModuleDependencies from '../lib/getModuleDependencies' - import resolveConfig from '../public/resolve-config' - import resolveConfigPath from '../util/resolveConfigPath' - -import { env } from './sharedState' - import { getContext, getFileModifiedMap } from './setupContextUtils' import parseDependency from '../util/parseDependency' import { validateConfig } from '../util/validateConfig.js' +import { parseCandidateFiles, resolvedChangedContent } from './content.js' let configPathCache = new LRU({ maxSize: 100 }) @@ -27,9 +19,7 @@ function getCandidateFiles(context, tailwindConfig) { return candidateFilesCache.get(context) } - let candidateFiles = tailwindConfig.content.files - .filter((item) => typeof item === 'string') - .map((contentPath) => normalizePath(contentPath)) + let candidateFiles = parseCandidateFiles(context, tailwindConfig) return candidateFilesCache.set(context, candidateFiles).get(context) } @@ -80,36 +70,6 @@ function getTailwindConfig(configOrPath) { 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 // 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) // Add template paths as postcss dependencies. - for (let fileOrGlob of candidateFiles) { - let dependency = parseDependency(fileOrGlob) - if (dependency) { + for (let contentPath of candidateFiles) { + for (let dependency of parseDependency(contentPath)) { registerDependency(dependency) } } diff --git a/src/util/normalizeConfig.js b/src/util/normalizeConfig.js index 2d23a864d..baee48394 100644 --- a/src/util/normalizeConfig.js +++ b/src/util/normalizeConfig.js @@ -56,9 +56,11 @@ export function normalizeConfig(config) { // When `config.content` is an object 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 ( - 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 } @@ -112,6 +114,14 @@ export function normalizeConfig(config) { ) { 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 @@ -154,6 +164,16 @@ export function normalizeConfig(config) { // Normalize the `content` config.content = { + relative: (() => { + let { content } = config + + if (content?.relative) { + return content.relative + } + + return config.future?.relativeContentPathsByDefault ?? false + })(), + files: (() => { let { content, purge } = config diff --git a/src/util/parseDependency.js b/src/util/parseDependency.js index 1051df473..f26eb1a29 100644 --- a/src/util/parseDependency.js +++ b/src/util/parseDependency.js @@ -1,49 +1,44 @@ -import isGlob from 'is-glob' -import globParent from 'glob-parent' -import path from 'path' +// @ts-check -// Based on `glob-base` -// https://github.com/micromatch/glob-base/blob/master/index.js -function parseGlob(pattern) { - let glob = pattern - let base = globParent(pattern) +/** + * @typedef {{type: 'dependency', file: string} | {type: 'dir-dependency', dir: string, glob: string}} Dependency + */ - if (base !== '.') { - glob = pattern.substr(base.length) - if (glob.charAt(0) === '/') { - glob = glob.substr(1) - } +/** + * + * @param {import('../lib/content.js').ContentPath} contentPath + * @returns {Dependency[]} + */ +export default function parseDependency(contentPath) { + if (contentPath.ignore) { + return [] } - if (glob.substr(0, 2) === './') { - glob = glob.substr(2) - } - if (glob.charAt(0) === '/') { - glob = glob.substr(1) + if (!contentPath.glob) { + return [ + { + type: 'dependency', + 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) } - } - - // rollup-plugin-postcss does not support dir-dependency messages - // but directories can be watched in the same way as files - if (message.type === 'dir-dependency' && process.env.ROLLUP_WATCH === 'true') { - message = { type: 'dependency', file: message.dir } - } - - return message + if (process.env.ROLLUP_WATCH === 'true') { + // rollup-plugin-postcss does not support dir-dependency messages + // but directories can be watched in the same way as files + return [ + { + type: 'dependency', + file: contentPath.base, + }, + ] + } + + return [ + { + type: 'dir-dependency', + dir: contentPath.base, + glob: contentPath.glob, + }, + ] } diff --git a/src/util/parseGlob.js b/src/util/parseGlob.js new file mode 100644 index 000000000..5c03f413d --- /dev/null +++ b/src/util/parseGlob.js @@ -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 } +} diff --git a/types/config.d.ts b/types/config.d.ts index a7d5104cf..0fff33918 100644 --- a/types/config.d.ts +++ b/types/config.d.ts @@ -31,6 +31,7 @@ type ContentConfig = | (FilePath | RawFile)[] | { files: (FilePath | RawFile)[] + relative?: boolean extract?: ExtractorFn | { [extension: string]: ExtractorFn } transform?: TransformerFn | { [extension: string]: TransformerFn } }