copy cli to oxide/cli
This commit is contained in:
parent
a92f798117
commit
fcb96f6847
14 changed files with 1263 additions and 32 deletions
66
integrations/tailwindcss-cli/package-lock.json
generated
66
integrations/tailwindcss-cli/package-lock.json
generated
|
|
@ -12,28 +12,30 @@
|
|||
}
|
||||
},
|
||||
"../..": {
|
||||
"version": "3.1.5",
|
||||
"version": "3.2.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tailwindcss/oxide": "file:oxide/crates/node",
|
||||
"arg": "^5.0.2",
|
||||
"chokidar": "^3.5.3",
|
||||
"color-name": "^1.1.4",
|
||||
"detective": "^5.2.1",
|
||||
"didyoumean": "^1.2.2",
|
||||
"dlv": "^1.1.3",
|
||||
"fast-glob": "^3.2.11",
|
||||
"fast-glob": "^3.2.12",
|
||||
"glob-parent": "^6.0.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"lilconfig": "^2.0.5",
|
||||
"lilconfig": "^2.0.6",
|
||||
"micromatch": "^4.0.5",
|
||||
"normalize-path": "^3.0.0",
|
||||
"object-hash": "^3.0.0",
|
||||
"picocolors": "^1.0.0",
|
||||
"postcss": "^8.0.9",
|
||||
"postcss": "^8.4.19",
|
||||
"postcss-import": "^14.1.0",
|
||||
"postcss-js": "^4.0.0",
|
||||
"postcss-load-config": "^3.1.4",
|
||||
"postcss-nested": "5.0.6",
|
||||
"postcss-selector-parser": "^6.0.10",
|
||||
"postcss-nested": "6.0.0",
|
||||
"postcss-selector-parser": "^6.0.11",
|
||||
"postcss-value-parser": "^4.2.0",
|
||||
"quick-lru": "^5.1.1",
|
||||
"resolve": "^1.22.1"
|
||||
|
|
@ -43,20 +45,20 @@
|
|||
"tailwindcss": "lib/cli.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@parcel/css": "^1.11.2",
|
||||
"@swc/cli": "^0.1.57",
|
||||
"@swc/core": "^1.2.196",
|
||||
"@swc/jest": "^0.2.21",
|
||||
"@swc/core": "^1.3.11",
|
||||
"@swc/jest": "^0.2.23",
|
||||
"@swc/register": "^0.1.10",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"esbuild": "^0.14.48",
|
||||
"eslint": "^8.18.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"concurrently": "^7.5.0",
|
||||
"cssnano": "^5.1.14",
|
||||
"esbuild": "^0.15.15",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"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",
|
||||
"jest": "^28.1.3",
|
||||
"jest-diff": "^28.1.3",
|
||||
"prettier": "^2.8.0",
|
||||
"rimraf": "^3.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
|
|
@ -76,40 +78,42 @@
|
|||
"tailwindcss": {
|
||||
"version": "file:../..",
|
||||
"requires": {
|
||||
"@parcel/css": "^1.11.2",
|
||||
"@swc/cli": "^0.1.57",
|
||||
"@swc/core": "^1.2.196",
|
||||
"@swc/jest": "^0.2.21",
|
||||
"@swc/core": "^1.3.11",
|
||||
"@swc/jest": "^0.2.23",
|
||||
"@swc/register": "^0.1.10",
|
||||
"@tailwindcss/oxide": "file:oxide/crates/node",
|
||||
"arg": "^5.0.2",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"chokidar": "^3.5.3",
|
||||
"color-name": "^1.1.4",
|
||||
"concurrently": "^7.5.0",
|
||||
"cssnano": "^5.1.14",
|
||||
"detective": "^5.2.1",
|
||||
"didyoumean": "^1.2.2",
|
||||
"dlv": "^1.1.3",
|
||||
"esbuild": "^0.14.48",
|
||||
"eslint": "^8.18.0",
|
||||
"esbuild": "^0.15.15",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"fast-glob": "^3.2.11",
|
||||
"fast-glob": "^3.2.12",
|
||||
"glob-parent": "^6.0.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"jest": "^28.1.2",
|
||||
"jest-diff": "^28.1.1",
|
||||
"lilconfig": "^2.0.5",
|
||||
"jest": "^28.1.3",
|
||||
"jest-diff": "^28.1.3",
|
||||
"lilconfig": "^2.0.6",
|
||||
"micromatch": "^4.0.5",
|
||||
"normalize-path": "^3.0.0",
|
||||
"object-hash": "^3.0.0",
|
||||
"picocolors": "^1.0.0",
|
||||
"postcss": "^8.0.9",
|
||||
"postcss": "^8.4.19",
|
||||
"postcss-import": "^14.1.0",
|
||||
"postcss-js": "^4.0.0",
|
||||
"postcss-load-config": "^3.1.4",
|
||||
"postcss-nested": "5.0.6",
|
||||
"postcss-selector-parser": "^6.0.10",
|
||||
"postcss-nested": "6.0.0",
|
||||
"postcss-selector-parser": "^6.0.11",
|
||||
"postcss-value-parser": "^4.2.0",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-plugin-tailwindcss": "^0.1.11",
|
||||
"prettier": "^2.8.0",
|
||||
"quick-lru": "^5.1.1",
|
||||
"resolve": "^1.22.1",
|
||||
"rimraf": "^3.0.0",
|
||||
|
|
|
|||
18
integrations/tailwindcss-cli/src/tailwind.2.config.js
Normal file
18
integrations/tailwindcss-cli/src/tailwind.2.config.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
|
||||
module.exports = {
|
||||
content: {
|
||||
relative: true,
|
||||
files: ['./index.html'],
|
||||
},
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
yellow: '#ff7',
|
||||
}
|
||||
},
|
||||
},
|
||||
corePlugins: {
|
||||
preflight: false,
|
||||
},
|
||||
}
|
||||
|
||||
0
src/oxide/cli.d.ts
vendored
0
src/oxide/cli.d.ts
vendored
|
|
@ -1 +1 @@
|
|||
require('../cli/index.js')
|
||||
require('./cli/index')
|
||||
|
|
|
|||
56
src/oxide/cli/build/deps.js
Normal file
56
src/oxide/cli/build/deps.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// @ts-check
|
||||
|
||||
import {
|
||||
// @ts-ignore
|
||||
lazyPostcss,
|
||||
|
||||
// @ts-ignore
|
||||
lazyPostcssImport,
|
||||
|
||||
// @ts-ignore
|
||||
lazyCssnano,
|
||||
|
||||
// @ts-ignore
|
||||
lazyAutoprefixer,
|
||||
} from '../../../../peers/index.js'
|
||||
|
||||
/**
|
||||
* @returns {import('postcss')}
|
||||
*/
|
||||
export function loadPostcss() {
|
||||
// Try to load a local `postcss` version first
|
||||
try {
|
||||
return require('postcss')
|
||||
} catch {}
|
||||
|
||||
return lazyPostcss()
|
||||
}
|
||||
|
||||
export function loadPostcssImport() {
|
||||
// Try to load a local `postcss-import` version first
|
||||
try {
|
||||
return require('postcss-import')
|
||||
} catch {}
|
||||
|
||||
return lazyPostcssImport()
|
||||
}
|
||||
|
||||
export function loadCssNano() {
|
||||
let options = { preset: ['default', { cssDeclarationSorter: false }] }
|
||||
|
||||
// Try to load a local `cssnano` version first
|
||||
try {
|
||||
return require('cssnano')
|
||||
} catch {}
|
||||
|
||||
return lazyCssnano()(options)
|
||||
}
|
||||
|
||||
export function loadAutoprefixer() {
|
||||
// Try to load a local `autoprefixer` version first
|
||||
try {
|
||||
return require('autoprefixer')
|
||||
} catch {}
|
||||
|
||||
return lazyAutoprefixer()
|
||||
}
|
||||
49
src/oxide/cli/build/index.js
Normal file
49
src/oxide/cli/build/index.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// @ts-check
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { createProcessor } from './plugin.js'
|
||||
|
||||
export async function build(args, configs) {
|
||||
let input = args['--input']
|
||||
let shouldWatch = args['--watch']
|
||||
|
||||
// TODO: Deprecate this in future versions
|
||||
if (!input && args['_'][1]) {
|
||||
console.error('[deprecation] Running tailwindcss without -i, please provide an input file.')
|
||||
input = args['--input'] = args['_'][1]
|
||||
}
|
||||
|
||||
if (input && input !== '-' && !fs.existsSync((input = path.resolve(input)))) {
|
||||
console.error(`Specified input file ${args['--input']} does not exist.`)
|
||||
process.exit(9)
|
||||
}
|
||||
|
||||
if (args['--config'] && !fs.existsSync((args['--config'] = path.resolve(args['--config'])))) {
|
||||
console.error(`Specified config file ${args['--config']} does not exist.`)
|
||||
process.exit(9)
|
||||
}
|
||||
|
||||
// TODO: Reference the @config path here if exists
|
||||
let configPath = args['--config']
|
||||
? args['--config']
|
||||
: ((defaultPath) => (fs.existsSync(defaultPath) ? defaultPath : null))(
|
||||
path.resolve(`./${configs.tailwind}`)
|
||||
)
|
||||
|
||||
let processor = await createProcessor(args, configPath)
|
||||
|
||||
if (shouldWatch) {
|
||||
// Abort the watcher if stdin is closed to avoid zombie processes
|
||||
// You can disable this behavior with --watch=always
|
||||
if (args['--watch'] !== 'always') {
|
||||
process.stdin.on('end', () => process.exit(0))
|
||||
}
|
||||
|
||||
process.stdin.resume()
|
||||
|
||||
await processor.watch()
|
||||
} else {
|
||||
await processor.build()
|
||||
}
|
||||
}
|
||||
439
src/oxide/cli/build/plugin.js
Normal file
439
src/oxide/cli/build/plugin.js
Normal file
|
|
@ -0,0 +1,439 @@
|
|||
// @ts-check
|
||||
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import postcssrc from 'postcss-load-config'
|
||||
import { lilconfig } from 'lilconfig'
|
||||
import loadPlugins from 'postcss-load-config/src/plugins' // Little bit scary, looking at private/internal API
|
||||
import loadOptions from 'postcss-load-config/src/options' // Little bit scary, looking at private/internal API
|
||||
|
||||
import tailwind from '../../../processTailwindFeatures'
|
||||
import { loadAutoprefixer, loadCssNano, loadPostcss, loadPostcssImport } from './deps'
|
||||
import { formatNodes, drainStdin, outputFile } from './utils'
|
||||
import { env } from '../shared'
|
||||
import resolveConfig from '../../../../resolveConfig.js'
|
||||
import getModuleDependencies from '../../../lib/getModuleDependencies.js'
|
||||
import { parseCandidateFiles } from '../../../lib/content.js'
|
||||
import { createWatcher } from './watching.js'
|
||||
import fastGlob from 'fast-glob'
|
||||
import { findAtConfigPath } from '../../../lib/findAtConfigPath.js'
|
||||
import log from '../../../util/log'
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} [customPostCssPath ]
|
||||
* @returns
|
||||
*/
|
||||
async function loadPostCssPlugins(customPostCssPath) {
|
||||
let config = customPostCssPath
|
||||
? await (async () => {
|
||||
let file = path.resolve(customPostCssPath)
|
||||
|
||||
// Implementation, see: https://unpkg.com/browse/postcss-load-config@3.1.0/src/index.js
|
||||
// @ts-ignore
|
||||
let { config = {} } = await lilconfig('postcss').load(file)
|
||||
if (typeof config === 'function') {
|
||||
config = config()
|
||||
} else {
|
||||
config = Object.assign({}, config)
|
||||
}
|
||||
|
||||
if (!config.plugins) {
|
||||
config.plugins = []
|
||||
}
|
||||
|
||||
return {
|
||||
file,
|
||||
plugins: loadPlugins(config, file),
|
||||
options: loadOptions(config, file),
|
||||
}
|
||||
})()
|
||||
: await postcssrc()
|
||||
|
||||
let configPlugins = config.plugins
|
||||
|
||||
let configPluginTailwindIdx = configPlugins.findIndex((plugin) => {
|
||||
if (typeof plugin === 'function' && plugin.name === 'tailwindcss') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (typeof plugin === 'object' && plugin !== null && plugin.postcssPlugin === 'tailwindcss') {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
let beforePlugins =
|
||||
configPluginTailwindIdx === -1 ? [] : configPlugins.slice(0, configPluginTailwindIdx)
|
||||
let afterPlugins =
|
||||
configPluginTailwindIdx === -1
|
||||
? configPlugins
|
||||
: configPlugins.slice(configPluginTailwindIdx + 1)
|
||||
|
||||
return [beforePlugins, afterPlugins, config.options]
|
||||
}
|
||||
|
||||
function loadBuiltinPostcssPlugins() {
|
||||
let postcss = loadPostcss()
|
||||
let IMPORT_COMMENT = '__TAILWIND_RESTORE_IMPORT__: '
|
||||
return [
|
||||
[
|
||||
(root) => {
|
||||
root.walkAtRules('import', (rule) => {
|
||||
if (rule.params.slice(1).startsWith('tailwindcss/')) {
|
||||
rule.after(postcss.comment({ text: IMPORT_COMMENT + rule.params }))
|
||||
rule.remove()
|
||||
}
|
||||
})
|
||||
},
|
||||
loadPostcssImport(),
|
||||
(root) => {
|
||||
root.walkComments((rule) => {
|
||||
if (rule.text.startsWith(IMPORT_COMMENT)) {
|
||||
rule.after(
|
||||
postcss.atRule({
|
||||
name: 'import',
|
||||
params: rule.text.replace(IMPORT_COMMENT, ''),
|
||||
})
|
||||
)
|
||||
rule.remove()
|
||||
}
|
||||
})
|
||||
},
|
||||
],
|
||||
[],
|
||||
{},
|
||||
]
|
||||
}
|
||||
|
||||
let state = {
|
||||
/** @type {any} */
|
||||
context: null,
|
||||
|
||||
/** @type {ReturnType<typeof createWatcher> | null} */
|
||||
watcher: null,
|
||||
|
||||
/** @type {{content: string, extension: string}[]} */
|
||||
changedContent: [],
|
||||
|
||||
configDependencies: new Set(),
|
||||
contextDependencies: new Set(),
|
||||
|
||||
/** @type {import('../../lib/content.js').ContentPath[]} */
|
||||
contentPaths: [],
|
||||
|
||||
refreshContentPaths() {
|
||||
this.contentPaths = parseCandidateFiles(this.context, this.context?.tailwindConfig)
|
||||
},
|
||||
|
||||
get config() {
|
||||
return this.context.tailwindConfig
|
||||
},
|
||||
|
||||
get contentPatterns() {
|
||||
return {
|
||||
all: this.contentPaths.map((contentPath) => contentPath.pattern),
|
||||
dynamic: this.contentPaths
|
||||
.filter((contentPath) => contentPath.glob !== undefined)
|
||||
.map((contentPath) => contentPath.pattern),
|
||||
}
|
||||
},
|
||||
|
||||
loadConfig(configPath, content) {
|
||||
if (this.watcher && configPath) {
|
||||
this.refreshConfigDependencies(configPath)
|
||||
}
|
||||
|
||||
let config = configPath ? require(configPath) : {}
|
||||
|
||||
// @ts-ignore
|
||||
config = resolveConfig(config, { content: { files: [] } })
|
||||
|
||||
// Override content files if `--content` has been passed explicitly
|
||||
if (content?.length > 0) {
|
||||
config.content.files = content
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
|
||||
refreshConfigDependencies(configPath) {
|
||||
env.DEBUG && console.time('Module dependencies')
|
||||
|
||||
for (let file of this.configDependencies) {
|
||||
delete require.cache[require.resolve(file)]
|
||||
}
|
||||
|
||||
if (configPath) {
|
||||
let deps = getModuleDependencies(configPath).map(({ file }) => file)
|
||||
|
||||
for (let dependency of deps) {
|
||||
this.configDependencies.add(dependency)
|
||||
}
|
||||
}
|
||||
|
||||
env.DEBUG && console.timeEnd('Module dependencies')
|
||||
},
|
||||
|
||||
readContentPaths() {
|
||||
let content = []
|
||||
|
||||
// Resolve globs from the content config
|
||||
// TODO: When we make the postcss plugin async-capable this can become async
|
||||
let files = fastGlob.sync(this.contentPatterns.all)
|
||||
|
||||
for (let file of files) {
|
||||
if (env.OXIDE) {
|
||||
content.push({
|
||||
file,
|
||||
extension: path.extname(file).slice(1),
|
||||
})
|
||||
} else {
|
||||
content.push({
|
||||
content: fs.readFileSync(path.resolve(file), 'utf8'),
|
||||
extension: path.extname(file).slice(1),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve raw content in the tailwind config
|
||||
let rawContent = this.config.content.files.filter((file) => {
|
||||
return file !== null && typeof file === 'object'
|
||||
})
|
||||
|
||||
for (let { raw: htmlContent, extension = 'html' } of rawContent) {
|
||||
content.push({ content: htmlContent, extension })
|
||||
}
|
||||
|
||||
return content
|
||||
},
|
||||
|
||||
getContext({ createContext, cliConfigPath, root, result, content }) {
|
||||
if (this.context) {
|
||||
this.context.changedContent = this.changedContent.splice(0)
|
||||
|
||||
return this.context
|
||||
}
|
||||
|
||||
env.DEBUG && console.time('Searching for config')
|
||||
let configPath = findAtConfigPath(root, result) ?? cliConfigPath
|
||||
env.DEBUG && console.timeEnd('Searching for config')
|
||||
|
||||
env.DEBUG && console.time('Loading config')
|
||||
let config = this.loadConfig(configPath, content)
|
||||
env.DEBUG && console.timeEnd('Loading config')
|
||||
|
||||
env.DEBUG && console.time('Creating context')
|
||||
this.context = createContext(config, [])
|
||||
Object.assign(this.context, {
|
||||
userConfigPath: configPath,
|
||||
})
|
||||
env.DEBUG && console.timeEnd('Creating context')
|
||||
|
||||
env.DEBUG && console.time('Resolving content paths')
|
||||
this.refreshContentPaths()
|
||||
env.DEBUG && console.timeEnd('Resolving content paths')
|
||||
|
||||
if (this.watcher) {
|
||||
env.DEBUG && console.time('Watch new files')
|
||||
this.watcher.refreshWatchedFiles()
|
||||
env.DEBUG && console.timeEnd('Watch new files')
|
||||
}
|
||||
|
||||
for (let file of this.readContentPaths()) {
|
||||
this.context.changedContent.push(file)
|
||||
}
|
||||
|
||||
return this.context
|
||||
},
|
||||
}
|
||||
|
||||
export async function createProcessor(args, cliConfigPath) {
|
||||
let postcss = loadPostcss()
|
||||
|
||||
let input = args['--input']
|
||||
let output = args['--output']
|
||||
let includePostCss = args['--postcss']
|
||||
let customPostCssPath = typeof args['--postcss'] === 'string' ? args['--postcss'] : undefined
|
||||
|
||||
let [beforePlugins, afterPlugins, postcssOptions] = includePostCss
|
||||
? await loadPostCssPlugins(customPostCssPath)
|
||||
: loadBuiltinPostcssPlugins()
|
||||
|
||||
if (args['--purge']) {
|
||||
log.warn('purge-flag-deprecated', [
|
||||
'The `--purge` flag has been deprecated.',
|
||||
'Please use `--content` instead.',
|
||||
])
|
||||
|
||||
if (!args['--content']) {
|
||||
args['--content'] = args['--purge']
|
||||
}
|
||||
}
|
||||
|
||||
let content = args['--content']?.split(/(?<!{[^}]+),/) ?? []
|
||||
|
||||
let tailwindPlugin = () => {
|
||||
return {
|
||||
postcssPlugin: 'tailwindcss',
|
||||
Once(root, { result }) {
|
||||
env.DEBUG && console.time('Compiling CSS')
|
||||
tailwind(({ createContext }) => {
|
||||
console.error()
|
||||
console.error('Rebuilding...')
|
||||
|
||||
return () => {
|
||||
return state.getContext({
|
||||
createContext,
|
||||
cliConfigPath,
|
||||
root,
|
||||
result,
|
||||
content,
|
||||
})
|
||||
}
|
||||
})(root, result)
|
||||
env.DEBUG && console.timeEnd('Compiling CSS')
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
tailwindPlugin.postcss = true
|
||||
|
||||
let plugins = [
|
||||
...beforePlugins,
|
||||
tailwindPlugin,
|
||||
!args['--minify'] && formatNodes,
|
||||
...afterPlugins,
|
||||
!args['--no-autoprefixer'] && loadAutoprefixer(),
|
||||
args['--minify'] && loadCssNano(),
|
||||
].filter(Boolean)
|
||||
|
||||
/** @type {import('postcss').Processor} */
|
||||
// @ts-ignore
|
||||
let processor = postcss(plugins)
|
||||
|
||||
async function readInput() {
|
||||
// Piping in data, let's drain the stdin
|
||||
if (input === '-') {
|
||||
return drainStdin()
|
||||
}
|
||||
|
||||
// Input file has been provided
|
||||
if (input) {
|
||||
return fs.promises.readFile(path.resolve(input), 'utf8')
|
||||
}
|
||||
|
||||
// No input file provided, fallback to default atrules
|
||||
return '@tailwind base; @tailwind components; @tailwind utilities'
|
||||
}
|
||||
|
||||
async function build() {
|
||||
let start = process.hrtime.bigint()
|
||||
|
||||
return readInput()
|
||||
.then((css) => processor.process(css, { ...postcssOptions, from: input, to: output }))
|
||||
.then((result) => {
|
||||
if (!state.watcher) {
|
||||
return result
|
||||
}
|
||||
|
||||
env.DEBUG && console.time('Recording PostCSS dependencies')
|
||||
for (let message of result.messages) {
|
||||
if (message.type === 'dependency') {
|
||||
state.contextDependencies.add(message.file)
|
||||
}
|
||||
}
|
||||
env.DEBUG && console.timeEnd('Recording PostCSS dependencies')
|
||||
|
||||
// TODO: This needs to be in a different spot
|
||||
env.DEBUG && console.time('Watch new files')
|
||||
state.watcher.refreshWatchedFiles()
|
||||
env.DEBUG && console.timeEnd('Watch new files')
|
||||
|
||||
return result
|
||||
})
|
||||
.then((result) => {
|
||||
if (!output) {
|
||||
process.stdout.write(result.css)
|
||||
return
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
outputFile(result.opts.to, result.css),
|
||||
result.map && outputFile(result.opts.to + '.map', result.map.toString()),
|
||||
])
|
||||
})
|
||||
.then(() => {
|
||||
let end = process.hrtime.bigint()
|
||||
console.error()
|
||||
console.error('Done in', (end - start) / BigInt(1e6) + 'ms.')
|
||||
})
|
||||
.then(
|
||||
() => {},
|
||||
(err) => {
|
||||
// TODO: If an initial build fails we can't easily pick up any PostCSS dependencies
|
||||
// that were collected before the error occurred
|
||||
// The result is not stored on the error so we have to store it externally
|
||||
// and pull the messages off of it here somehow
|
||||
|
||||
// This results in a less than ideal DX because the watcher will not pick up
|
||||
// changes to imported CSS if one of them caused an error during the initial build
|
||||
// If you fix it and then save the main CSS file so there's no error
|
||||
// The watcher will start watching the imported CSS files and will be
|
||||
// resilient to future errors.
|
||||
|
||||
console.error(err)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{file: string, content(): Promise<string>, extension: string}[]} changes
|
||||
*/
|
||||
async function parseChanges(changes) {
|
||||
return Promise.all(
|
||||
changes.map(async (change) => ({
|
||||
content: await change.content(),
|
||||
extension: change.extension,
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
if (input !== undefined && input !== '-') {
|
||||
state.contextDependencies.add(path.resolve(input))
|
||||
}
|
||||
|
||||
return {
|
||||
build,
|
||||
watch: async () => {
|
||||
state.watcher = createWatcher(args, {
|
||||
state,
|
||||
|
||||
/**
|
||||
* @param {{file: string, content(): Promise<string>, extension: string}[]} changes
|
||||
*/
|
||||
async rebuild(changes) {
|
||||
let needsNewContext = changes.some((change) => {
|
||||
return (
|
||||
state.configDependencies.has(change.file) ||
|
||||
state.contextDependencies.has(change.file)
|
||||
)
|
||||
})
|
||||
|
||||
if (needsNewContext) {
|
||||
state.context = null
|
||||
} else {
|
||||
for (let change of await parseChanges(changes)) {
|
||||
state.changedContent.push(change)
|
||||
}
|
||||
}
|
||||
|
||||
return build()
|
||||
},
|
||||
})
|
||||
|
||||
await build()
|
||||
},
|
||||
}
|
||||
}
|
||||
76
src/oxide/cli/build/utils.js
Normal file
76
src/oxide/cli/build/utils.js
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
// @ts-check
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export function indentRecursive(node, indent = 0) {
|
||||
node.each &&
|
||||
node.each((child, i) => {
|
||||
if (!child.raws.before || !child.raws.before.trim() || child.raws.before.includes('\n')) {
|
||||
child.raws.before = `\n${node.type !== 'rule' && i > 0 ? '\n' : ''}${' '.repeat(indent)}`
|
||||
}
|
||||
child.raws.after = `\n${' '.repeat(indent)}`
|
||||
indentRecursive(child, indent + 1)
|
||||
})
|
||||
}
|
||||
|
||||
export function formatNodes(root) {
|
||||
indentRecursive(root)
|
||||
if (root.first) {
|
||||
root.first.raws.before = ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When rapidly saving files atomically a couple of situations can happen:
|
||||
* - The file is missing since the external program has deleted it by the time we've gotten around to reading it from the earlier save.
|
||||
* - The file is being written to by the external program by the time we're going to read it and is thus treated as busy because a lock is held.
|
||||
*
|
||||
* To work around this we retry reading the file a handful of times with a delay between each attempt
|
||||
*
|
||||
* @param {string} path
|
||||
* @param {number} tries
|
||||
* @returns {Promise<string | undefined>}
|
||||
* @throws {Error} If the file is still missing or busy after the specified number of tries
|
||||
*/
|
||||
export async function readFileWithRetries(path, tries = 5) {
|
||||
for (let n = 0; n <= tries; n++) {
|
||||
try {
|
||||
return await fs.promises.readFile(path, 'utf8')
|
||||
} catch (err) {
|
||||
if (n !== tries) {
|
||||
if (err.code === 'ENOENT' || err.code === 'EBUSY') {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function drainStdin() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let result = ''
|
||||
process.stdin.on('data', (chunk) => {
|
||||
result += chunk
|
||||
})
|
||||
process.stdin.on('end', () => resolve(result))
|
||||
process.stdin.on('error', (err) => reject(err))
|
||||
})
|
||||
}
|
||||
|
||||
export async function outputFile(file, newContents) {
|
||||
try {
|
||||
let currentContents = await fs.promises.readFile(file, 'utf8')
|
||||
if (currentContents === newContents) {
|
||||
return // Skip writing the file
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Write the file
|
||||
await fs.promises.mkdir(path.dirname(file), { recursive: true })
|
||||
await fs.promises.writeFile(file, newContents, 'utf8')
|
||||
}
|
||||
227
src/oxide/cli/build/watching.js
Normal file
227
src/oxide/cli/build/watching.js
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
// @ts-check
|
||||
|
||||
import chokidar from 'chokidar'
|
||||
import fs from 'fs'
|
||||
import micromatch from 'micromatch'
|
||||
import normalizePath from 'normalize-path'
|
||||
import path from 'path'
|
||||
|
||||
import { readFileWithRetries } from './utils.js'
|
||||
|
||||
/**
|
||||
* The core idea of this watcher is:
|
||||
* 1. Whenever a file is added, changed, or renamed we queue a rebuild
|
||||
* 2. Perform as few rebuilds as possible by batching them together
|
||||
* 3. Coalesce events that happen in quick succession to avoid unnecessary rebuilds
|
||||
* 4. Ensure another rebuild happens _if_ changed while a rebuild is in progress
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} args
|
||||
* @param {{ state, rebuild(changedFiles: any[]): Promise<any> }} param1
|
||||
* @returns {{
|
||||
* fswatcher: import('chokidar').FSWatcher,
|
||||
* refreshWatchedFiles(): void,
|
||||
* }}
|
||||
*/
|
||||
export function createWatcher(args, { state, rebuild }) {
|
||||
let shouldPoll = args['--poll']
|
||||
let shouldCoalesceWriteEvents = shouldPoll || process.platform === 'win32'
|
||||
|
||||
// Polling interval in milliseconds
|
||||
// Used only when polling or coalescing add/change events on Windows
|
||||
let pollInterval = 10
|
||||
|
||||
let watcher = chokidar.watch([], {
|
||||
// Force checking for atomic writes in all situations
|
||||
// This causes chokidar to wait up to 100ms for a file to re-added after it's been unlinked
|
||||
// This only works when watching directories though
|
||||
atomic: true,
|
||||
|
||||
usePolling: shouldPoll,
|
||||
interval: shouldPoll ? pollInterval : undefined,
|
||||
ignoreInitial: true,
|
||||
awaitWriteFinish: shouldCoalesceWriteEvents
|
||||
? {
|
||||
stabilityThreshold: 50,
|
||||
pollInterval: pollInterval,
|
||||
}
|
||||
: false,
|
||||
})
|
||||
|
||||
// A queue of rebuilds, file reads, etc… to run
|
||||
let chain = Promise.resolve()
|
||||
|
||||
/**
|
||||
* A list of files that have been changed since the last rebuild
|
||||
*
|
||||
* @type {{file: string, content: () => Promise<string>, extension: string}[]}
|
||||
*/
|
||||
let changedContent = []
|
||||
|
||||
/**
|
||||
* A list of files for which a rebuild has already been queued.
|
||||
* This is used to prevent duplicate rebuilds when multiple events are fired for the same file.
|
||||
* The rebuilt file is cleared from this list when it's associated rebuild has _started_
|
||||
* This is because if the file is changed during a rebuild it won't trigger a new rebuild which it should
|
||||
**/
|
||||
let pendingRebuilds = new Set()
|
||||
|
||||
let _timer
|
||||
let _reject
|
||||
|
||||
/**
|
||||
* Rebuilds the changed files and resolves when the rebuild is
|
||||
* complete regardless of whether it was successful or not
|
||||
*/
|
||||
async function rebuildAndContinue() {
|
||||
let changes = changedContent.splice(0)
|
||||
|
||||
// There are no changes to rebuild so we can just do nothing
|
||||
if (changes.length === 0) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
// Clear all pending rebuilds for the about-to-be-built files
|
||||
changes.forEach((change) => pendingRebuilds.delete(change.file))
|
||||
|
||||
// Resolve the promise even when the rebuild fails
|
||||
return rebuild(changes).then(
|
||||
() => {},
|
||||
() => {}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} file
|
||||
* @param {(() => Promise<string>) | null} content
|
||||
* @param {boolean} skipPendingCheck
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function recordChangedFile(file, content = null, skipPendingCheck = false) {
|
||||
file = path.resolve(file)
|
||||
|
||||
// Applications like Vim/Neovim fire both rename and change events in succession for atomic writes
|
||||
// In that case rebuild has already been queued by rename, so can be skipped in change
|
||||
if (pendingRebuilds.has(file) && !skipPendingCheck) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
// Mark that a rebuild of this file is going to happen
|
||||
// It MUST happen synchronously before the rebuild is queued for this to be effective
|
||||
pendingRebuilds.add(file)
|
||||
|
||||
changedContent.push({
|
||||
file,
|
||||
content: content ?? (() => fs.promises.readFile(file, 'utf8')),
|
||||
extension: path.extname(file).slice(1),
|
||||
})
|
||||
|
||||
if (_timer) {
|
||||
clearTimeout(_timer)
|
||||
_reject()
|
||||
}
|
||||
|
||||
// If a rebuild is already in progress we don't want to start another one until the 10ms timer has expired
|
||||
chain = chain.then(
|
||||
() =>
|
||||
new Promise((resolve, reject) => {
|
||||
_timer = setTimeout(resolve, 10)
|
||||
_reject = reject
|
||||
})
|
||||
)
|
||||
|
||||
// Resolves once this file has been rebuilt (or the rebuild for this file has failed)
|
||||
// This queues as many rebuilds as there are changed files
|
||||
// But those rebuilds happen after some delay
|
||||
// And will immediately resolve if there are no changes
|
||||
chain = chain.then(rebuildAndContinue, rebuildAndContinue)
|
||||
|
||||
return chain
|
||||
}
|
||||
|
||||
watcher.on('change', (file) => recordChangedFile(file))
|
||||
watcher.on('add', (file) => recordChangedFile(file))
|
||||
|
||||
// Restore watching any files that are "removed"
|
||||
// This can happen when a file is pseudo-atomically replaced (a copy is created, overwritten, the old one is unlinked, and the new one is renamed)
|
||||
// TODO: An an optimization we should allow removal when the config changes
|
||||
watcher.on('unlink', (file) => {
|
||||
file = normalizePath(file)
|
||||
|
||||
// Only re-add the file if it's not covered by a dynamic pattern
|
||||
if (!micromatch.some([file], state.contentPatterns.dynamic)) {
|
||||
watcher.add(file)
|
||||
}
|
||||
})
|
||||
|
||||
// Some applications such as Visual Studio (but not VS Code)
|
||||
// will only fire a rename event for atomic writes and not a change event
|
||||
// This is very likely a chokidar bug but it's one we need to work around
|
||||
// We treat this as a change event and rebuild the CSS
|
||||
watcher.on('raw', (evt, filePath, meta) => {
|
||||
if (evt !== 'rename') {
|
||||
return
|
||||
}
|
||||
|
||||
let watchedPath = meta.watchedPath
|
||||
|
||||
// Watched path might be the file itself
|
||||
// Or the directory it is in
|
||||
filePath = watchedPath.endsWith(filePath) ? watchedPath : path.join(watchedPath, filePath)
|
||||
|
||||
// Skip this event since the files it is for does not match any of the registered content globs
|
||||
if (!micromatch.some([filePath], state.contentPatterns.all)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip since we've already queued a rebuild for this file that hasn't happened yet
|
||||
if (pendingRebuilds.has(filePath)) {
|
||||
return
|
||||
}
|
||||
|
||||
// We'll go ahead and add the file to the pending rebuilds list here
|
||||
// It'll be removed when the rebuild starts unless the read fails
|
||||
// which will be taken care of as well
|
||||
pendingRebuilds.add(filePath)
|
||||
|
||||
async function enqueue() {
|
||||
try {
|
||||
// We need to read the file as early as possible outside of the chain
|
||||
// because it may be gone by the time we get to it. doing the read
|
||||
// immediately increases the chance that the file is still there
|
||||
let content = await readFileWithRetries(path.resolve(filePath))
|
||||
|
||||
if (content === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
// This will push the rebuild onto the chain
|
||||
// We MUST skip the rebuild check here otherwise the rebuild will never happen on Linux
|
||||
// This is because the order of events and timing is different on Linux
|
||||
// @ts-ignore: TypeScript isn't picking up that content is a string here
|
||||
await recordChangedFile(filePath, () => content, true)
|
||||
} catch {
|
||||
// If reading the file fails, it's was probably a deleted temporary file
|
||||
// So we can ignore it and no rebuild is needed
|
||||
}
|
||||
}
|
||||
|
||||
enqueue().then(() => {
|
||||
// If the file read fails we still need to make sure the file isn't stuck in the pending rebuilds list
|
||||
pendingRebuilds.delete(filePath)
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
fswatcher: watcher,
|
||||
|
||||
refreshWatchedFiles() {
|
||||
watcher.add(Array.from(state.contextDependencies))
|
||||
watcher.add(Array.from(state.configDependencies))
|
||||
watcher.add(state.contentPatterns.all)
|
||||
},
|
||||
}
|
||||
}
|
||||
70
src/oxide/cli/help/index.js
Normal file
70
src/oxide/cli/help/index.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
// @ts-check
|
||||
import packageJson from '../../../../package.json'
|
||||
|
||||
export function help({ message, usage, commands, options }) {
|
||||
let indent = 2
|
||||
|
||||
// Render header
|
||||
console.log()
|
||||
console.log(`${packageJson.name} v${packageJson.version}`)
|
||||
|
||||
// Render message
|
||||
if (message) {
|
||||
console.log()
|
||||
for (let msg of message.split('\n')) {
|
||||
console.log(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Render usage
|
||||
if (usage && usage.length > 0) {
|
||||
console.log()
|
||||
console.log('Usage:')
|
||||
for (let example of usage) {
|
||||
console.log(' '.repeat(indent), example)
|
||||
}
|
||||
}
|
||||
|
||||
// Render commands
|
||||
if (commands && commands.length > 0) {
|
||||
console.log()
|
||||
console.log('Commands:')
|
||||
for (let command of commands) {
|
||||
console.log(' '.repeat(indent), command)
|
||||
}
|
||||
}
|
||||
|
||||
// Render options
|
||||
if (options) {
|
||||
let groupedOptions = {}
|
||||
for (let [key, value] of Object.entries(options)) {
|
||||
if (typeof value === 'object') {
|
||||
groupedOptions[key] = { ...value, flags: [key] }
|
||||
} else {
|
||||
groupedOptions[value].flags.push(key)
|
||||
}
|
||||
}
|
||||
|
||||
console.log()
|
||||
console.log('Options:')
|
||||
for (let { flags, description, deprecated } of Object.values(groupedOptions)) {
|
||||
if (deprecated) continue
|
||||
|
||||
if (flags.length === 1) {
|
||||
console.log(
|
||||
' '.repeat(indent + 4 /* 4 = "-i, ".length */),
|
||||
flags.slice().reverse().join(', ').padEnd(20, ' '),
|
||||
description
|
||||
)
|
||||
} else {
|
||||
console.log(
|
||||
' '.repeat(indent),
|
||||
flags.slice().reverse().join(', ').padEnd(24, ' '),
|
||||
description
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log()
|
||||
}
|
||||
236
src/oxide/cli/index.js
Normal file
236
src/oxide/cli/index.js
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import path from 'path'
|
||||
import arg from 'arg'
|
||||
import fs from 'fs'
|
||||
|
||||
import { build } from './build'
|
||||
import { help } from './help'
|
||||
import { init } from './init'
|
||||
|
||||
console.log('HELLO FROM OXIDE')
|
||||
|
||||
function isESM() {
|
||||
const pkgPath = path.resolve('./package.json')
|
||||
|
||||
try {
|
||||
let pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
||||
return pkg.type && pkg.type === 'module'
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
let configs = isESM()
|
||||
? {
|
||||
tailwind: 'tailwind.config.cjs',
|
||||
postcss: 'postcss.config.cjs',
|
||||
}
|
||||
: {
|
||||
tailwind: 'tailwind.config.js',
|
||||
postcss: 'postcss.config.js',
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
function oneOf(...options) {
|
||||
return Object.assign(
|
||||
(value = true) => {
|
||||
for (let option of options) {
|
||||
let parsed = option(value)
|
||||
if (parsed === value) {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('...')
|
||||
},
|
||||
{ manualParsing: true }
|
||||
)
|
||||
}
|
||||
|
||||
let commands = {
|
||||
init: {
|
||||
run: init,
|
||||
args: {
|
||||
'--full': { type: Boolean, description: `Initialize a full \`${configs.tailwind}\` file` },
|
||||
'--postcss': { type: Boolean, description: `Initialize a \`${configs.postcss}\` file` },
|
||||
'-f': '--full',
|
||||
'-p': '--postcss',
|
||||
},
|
||||
},
|
||||
build: {
|
||||
run: build,
|
||||
args: {
|
||||
'--input': { type: String, description: 'Input file' },
|
||||
'--output': { type: String, description: 'Output file' },
|
||||
'--watch': {
|
||||
type: oneOf(String, Boolean),
|
||||
description: 'Watch for changes and rebuild as needed',
|
||||
},
|
||||
'--poll': {
|
||||
type: Boolean,
|
||||
description: 'Use polling instead of filesystem events when watching',
|
||||
},
|
||||
'--content': {
|
||||
type: String,
|
||||
description: 'Content paths to use for removing unused classes',
|
||||
},
|
||||
'--purge': {
|
||||
type: String,
|
||||
deprecated: true,
|
||||
},
|
||||
'--postcss': {
|
||||
type: oneOf(String, Boolean),
|
||||
description: 'Load custom PostCSS configuration',
|
||||
},
|
||||
'--minify': { type: Boolean, description: 'Minify the output' },
|
||||
'--config': {
|
||||
type: String,
|
||||
description: 'Path to a custom config file',
|
||||
},
|
||||
'--no-autoprefixer': {
|
||||
type: Boolean,
|
||||
description: 'Disable autoprefixer',
|
||||
},
|
||||
'-c': '--config',
|
||||
'-i': '--input',
|
||||
'-o': '--output',
|
||||
'-m': '--minify',
|
||||
'-w': '--watch',
|
||||
'-p': '--poll',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
let sharedFlags = {
|
||||
'--help': { type: Boolean, description: 'Display usage information' },
|
||||
'-h': '--help',
|
||||
}
|
||||
|
||||
if (
|
||||
process.stdout.isTTY /* Detect redirecting output to a file */ &&
|
||||
(process.argv[2] === undefined ||
|
||||
process.argv.slice(2).every((flag) => sharedFlags[flag] !== undefined))
|
||||
) {
|
||||
help({
|
||||
usage: [
|
||||
'tailwindcss [--input input.css] [--output output.css] [--watch] [options...]',
|
||||
'tailwindcss init [--full] [--postcss] [options...]',
|
||||
],
|
||||
commands: Object.keys(commands)
|
||||
.filter((command) => command !== 'build')
|
||||
.map((command) => `${command} [options]`),
|
||||
options: { ...commands.build.args, ...sharedFlags },
|
||||
})
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
let command = ((arg = '') => (arg.startsWith('-') ? undefined : arg))(process.argv[2]) || 'build'
|
||||
|
||||
if (commands[command] === undefined) {
|
||||
if (fs.existsSync(path.resolve(command))) {
|
||||
// TODO: Deprecate this in future versions
|
||||
// Check if non-existing command, might be a file.
|
||||
command = 'build'
|
||||
} else {
|
||||
help({
|
||||
message: `Invalid command: ${command}`,
|
||||
usage: ['tailwindcss <command> [options]'],
|
||||
commands: Object.keys(commands)
|
||||
.filter((command) => command !== 'build')
|
||||
.map((command) => `${command} [options]`),
|
||||
options: sharedFlags,
|
||||
})
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute command
|
||||
let { args: flags, run } = commands[command]
|
||||
let args = (() => {
|
||||
try {
|
||||
let result = arg(
|
||||
Object.fromEntries(
|
||||
Object.entries({ ...flags, ...sharedFlags })
|
||||
.filter(([_key, value]) => !value?.type?.manualParsing)
|
||||
.map(([key, value]) => [key, typeof value === 'object' ? value.type : value])
|
||||
),
|
||||
{ permissive: true }
|
||||
)
|
||||
|
||||
// Manual parsing of flags to allow for special flags like oneOf(Boolean, String)
|
||||
for (let i = result['_'].length - 1; i >= 0; --i) {
|
||||
let flag = result['_'][i]
|
||||
if (!flag.startsWith('-')) continue
|
||||
|
||||
let [flagName, flagValue] = flag.split('=')
|
||||
let handler = flags[flagName]
|
||||
|
||||
// Resolve flagName & handler
|
||||
while (typeof handler === 'string') {
|
||||
flagName = handler
|
||||
handler = flags[handler]
|
||||
}
|
||||
|
||||
if (!handler) continue
|
||||
|
||||
let args = []
|
||||
let offset = i + 1
|
||||
|
||||
// --flag value syntax was used so we need to pull `value` from `args`
|
||||
if (flagValue === undefined) {
|
||||
// Parse args for current flag
|
||||
while (result['_'][offset] && !result['_'][offset].startsWith('-')) {
|
||||
args.push(result['_'][offset++])
|
||||
}
|
||||
|
||||
// Cleanup manually parsed flags + args
|
||||
result['_'].splice(i, 1 + args.length)
|
||||
|
||||
// No args were provided, use default value defined in handler
|
||||
// One arg was provided, use that directly
|
||||
// Multiple args were provided so pass them all in an array
|
||||
flagValue = args.length === 0 ? undefined : args.length === 1 ? args[0] : args
|
||||
} else {
|
||||
// Remove the whole flag from the args array
|
||||
result['_'].splice(i, 1)
|
||||
}
|
||||
|
||||
// Set the resolved value in the `result` object
|
||||
result[flagName] = handler.type(flagValue, flagName)
|
||||
}
|
||||
|
||||
// Ensure that the `command` is always the first argument in the `args`.
|
||||
// This is important so that we don't have to check if a default command
|
||||
// (build) was used or not from within each plugin.
|
||||
//
|
||||
// E.g.: tailwindcss input.css -> _: ['build', 'input.css']
|
||||
// E.g.: tailwindcss build input.css -> _: ['build', 'input.css']
|
||||
if (result['_'][0] !== command) {
|
||||
result['_'].unshift(command)
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (err) {
|
||||
if (err.code === 'ARG_UNKNOWN_OPTION') {
|
||||
help({
|
||||
message: err.message,
|
||||
usage: ['tailwindcss <command> [options]'],
|
||||
options: sharedFlags,
|
||||
})
|
||||
process.exit(1)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
})()
|
||||
|
||||
if (args['--help']) {
|
||||
help({
|
||||
options: { ...flags, ...sharedFlags },
|
||||
usage: [`tailwindcss ${command} [options]`],
|
||||
})
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
run(args, configs)
|
||||
50
src/oxide/cli/init/index.js
Normal file
50
src/oxide/cli/init/index.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// @ts-check
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export function init(args, configs) {
|
||||
let messages = []
|
||||
|
||||
let tailwindConfigLocation = path.resolve(args['_'][1] ?? `./${configs.tailwind}`)
|
||||
if (fs.existsSync(tailwindConfigLocation)) {
|
||||
messages.push(`${path.basename(tailwindConfigLocation)} already exists.`)
|
||||
} else {
|
||||
let stubFile = fs.readFileSync(
|
||||
args['--full']
|
||||
? path.resolve(__dirname, '../../../stubs/defaultConfig.stub.js')
|
||||
: path.resolve(__dirname, '../../../stubs/simpleConfig.stub.js'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
// Change colors import
|
||||
stubFile = stubFile.replace('../colors', 'tailwindcss/colors')
|
||||
|
||||
fs.writeFileSync(tailwindConfigLocation, stubFile, 'utf8')
|
||||
|
||||
messages.push(`Created Tailwind CSS config file: ${path.basename(tailwindConfigLocation)}`)
|
||||
}
|
||||
|
||||
if (args['--postcss']) {
|
||||
let postcssConfigLocation = path.resolve(`./${configs.postcss}`)
|
||||
if (fs.existsSync(postcssConfigLocation)) {
|
||||
messages.push(`${path.basename(postcssConfigLocation)} already exists.`)
|
||||
} else {
|
||||
let stubFile = fs.readFileSync(
|
||||
path.resolve(__dirname, '../../../stubs/defaultPostCssConfig.stub.js'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
fs.writeFileSync(postcssConfigLocation, stubFile, 'utf8')
|
||||
|
||||
messages.push(`Created PostCSS config file: ${path.basename(postcssConfigLocation)}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (messages.length > 0) {
|
||||
console.log()
|
||||
for (let message of messages) {
|
||||
console.log(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
6
src/oxide/cli/shared.js
Normal file
6
src/oxide/cli/shared.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// @ts-check
|
||||
|
||||
export const env = {
|
||||
DEBUG: process.env.DEBUG !== undefined && process.env.DEBUG !== '0',
|
||||
OXIDE: process.env.OXIDE,
|
||||
}
|
||||
0
src/oxide/postcss-plugin.d.ts
vendored
0
src/oxide/postcss-plugin.d.ts
vendored
Loading…
Add table
Add a link
Reference in a new issue