Normalize and resolve content paths earlier
Not just for PostCSS dependencies
This commit is contained in:
parent
2102566df8
commit
3c07c8b92d
4 changed files with 226 additions and 123 deletions
166
src/lib/content.js
Normal file
166
src/lib/content.js
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
// @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
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
return files.flatMap((contentPath) => parseContentPath(context, contentPath))
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {any} context
|
||||
* @param {RawFile | FilePath} filePath
|
||||
* @returns {ContentPath[]}
|
||||
*/
|
||||
function parseContentPath(context, filePath) {
|
||||
if (typeof filePath !== 'string') {
|
||||
return []
|
||||
}
|
||||
|
||||
filePath = normalizePath(filePath)
|
||||
|
||||
/** @type {ContentPath[]} */
|
||||
let paths = []
|
||||
|
||||
if (isGlob(filePath)) {
|
||||
let { base, glob } = parseGlob(filePath)
|
||||
|
||||
paths.push({ original: filePath, base: base, glob })
|
||||
} else {
|
||||
paths.push({ original: filePath, base: filePath, glob: null })
|
||||
}
|
||||
|
||||
paths = resolveRelativePaths(context, paths)
|
||||
paths = paths.flatMap(resolvePathSymlinks)
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 &&
|
||||
flagEnabled(context.tailwindConfig, 'resolveContentRelativeToConfig')
|
||||
) {
|
||||
resolveFrom = [path.dirname(context.userConfigPath)]
|
||||
}
|
||||
|
||||
return contentPaths.map((contentPath) =>
|
||||
Object.assign(contentPath, {
|
||||
base: path.resolve(...resolveFrom, contentPath.base),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
try {
|
||||
let newPath = fs.realpathSync(contentPath.base, { encoding: 'utf8' })
|
||||
if (newPath !== contentPath.base) {
|
||||
return [contentPath, { ...contentPath, base: newPath }]
|
||||
}
|
||||
} catch {
|
||||
// TODO: log this?
|
||||
}
|
||||
|
||||
return [contentPath]
|
||||
}
|
||||
|
||||
/**
|
||||
* @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.original.startsWith('!')
|
||||
? contentPath.original
|
||||
: contentPath.glob
|
||||
? `${contentPath.base}/${contentPath.glob}`
|
||||
: contentPath.base
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ 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 +28,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 +79,6 @@ function getTailwindConfig(configOrPath, inputPath) {
|
|||
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,8 +130,8 @@ export default function setupTrackingContext(configOrPath) {
|
|||
let fileModifiedMap = getFileModifiedMap(context)
|
||||
|
||||
// Add template paths as postcss dependencies.
|
||||
for (let fileOrGlob of candidateFiles) {
|
||||
for (let dependency of parseDependency(context, fileOrGlob)) {
|
||||
for (let contentPath of candidateFiles) {
|
||||
for (let dependency of parseDependency(contentPath)) {
|
||||
registerDependency(dependency)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,100 +1,44 @@
|
|||
import isGlob from 'is-glob'
|
||||
import globParent from 'glob-parent'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { flagEnabled } from '../featureFlags'
|
||||
// @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.original.startsWith('!')) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (glob.substr(0, 2) === './') {
|
||||
glob = glob.substr(2)
|
||||
}
|
||||
if (glob.charAt(0) === '/') {
|
||||
glob = glob.substr(1)
|
||||
}
|
||||
|
||||
return { base, glob }
|
||||
}
|
||||
|
||||
function resolvePathSymlinks(pathDesc) {
|
||||
try {
|
||||
let newPath = fs.realpathSync(pathDesc.base, { encoding: 'utf8' })
|
||||
if (newPath !== pathDesc.base) {
|
||||
return [pathDesc, { ...pathDesc, base: newPath }]
|
||||
}
|
||||
} catch {
|
||||
// TODO: log this?
|
||||
}
|
||||
|
||||
return [pathDesc]
|
||||
}
|
||||
|
||||
function toDependency(pathDesc) {
|
||||
if (!pathDesc.glob) {
|
||||
return {
|
||||
type: 'dependency',
|
||||
file: pathDesc.base,
|
||||
}
|
||||
if (!contentPath.glob) {
|
||||
return [
|
||||
{
|
||||
type: 'dependency',
|
||||
file: contentPath.base,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
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: pathDesc.base,
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: 'dependency',
|
||||
file: contentPath.base,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'dir-dependency',
|
||||
dir: pathDesc.base,
|
||||
glob: pathDesc.glob,
|
||||
}
|
||||
}
|
||||
|
||||
export default function parseDependency(context, normalizedFileOrGlob) {
|
||||
if (normalizedFileOrGlob.startsWith('!')) {
|
||||
return []
|
||||
}
|
||||
|
||||
let paths = []
|
||||
|
||||
if (isGlob(normalizedFileOrGlob)) {
|
||||
let { base, glob } = parseGlob(normalizedFileOrGlob)
|
||||
|
||||
paths.push({ base: base, glob })
|
||||
} else {
|
||||
paths.push({ base: normalizedFileOrGlob, glob: null })
|
||||
}
|
||||
|
||||
// Resolve base paths relative to the config file or current working directory
|
||||
let resolveFrom = flagEnabled(context.tailwindConfig, 'resolveContentRelativeToConfig')
|
||||
? [context.userConfigPath ?? process.cwd()]
|
||||
: [process.cwd()]
|
||||
|
||||
paths = paths.map((pathDesc) =>
|
||||
Object.assign(pathDesc, {
|
||||
base: path.resolve(...resolveFrom, pathDesc.base),
|
||||
})
|
||||
)
|
||||
|
||||
// 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.
|
||||
paths = paths.flatMap(resolvePathSymlinks)
|
||||
|
||||
return paths.map(toDependency)
|
||||
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 }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue