Skip to content

Instantly share code, notes, and snippets.

@javascripter
Created June 6, 2024 06:19
Show Gist options
  • Save javascripter/147599d3bce3c2da1fa28e6d50c83fe5 to your computer and use it in GitHub Desktop.
Save javascripter/147599d3bce3c2da1fa28e6d50c83fe5 to your computer and use it in GitHub Desktop.
import { parseArgs } from 'node:util'
import watchman, { Client } from 'fb-watchman'
import ansis from 'ansis'
import path from 'node:path'
import fs from 'node:fs'
import { globSync } from 'fast-glob'
import { transformSync } from '@babel/core'
import styleXBabelPlugin from '@stylexjs/babel-plugin'
type Config = {
input: string
output: string
verbose: boolean
state: {
styleXRules: Map<string, any>
}
}
function help() {
console.log(`
Usage: stylex [options]
Options:
-i, --input Input file
-o, --output Output file
-w, --watch Watch mode
-v, --verbose Verbose mode
-h, --help Display this help message
`)
}
function watch(config: Config) {
const client = new watchman.Client()
client.capabilityCheck(
{ optional: [], required: ['relative_root'] },
function (error, _response) {
if (error) {
console.log(error)
client.end()
return
}
client.command(['watch-project', config.input], (error, response) => {
if (error) {
console.error('Error initiating watch:', error)
return
}
if ('warning' in response) {
console.log('warning:', response.warning)
}
subscribe(client, response.watch, response.relative_path, config)
console.log(
'Watching for style changes in',
ansis.green(response.relative_path),
)
})
},
)
}
function subscribe(
client: Client,
watcher: any,
relativePath: string,
config: Config,
) {
const subscription: any = {
expression: [
'anyof',
['match', '*.js'],
['match', '*.ts'],
['match', '*.jsx'],
['match', '*.tsx'],
['match', '*.cjs'],
['match', '*.mjs'],
],
fields: ['name', 'size', 'mtime_ms', 'exists', 'type'],
}
if (relativePath) {
subscription.relative_root = relativePath
}
client.command(
['subscribe', watcher, 'jsFileChanged', subscription],
function (error, _response) {
if (error) {
console.error('failed to subscribe:', error)
return
}
},
)
client.on('subscription', (response) => {
if (response.subscription !== 'jsFileChanged') return
build(
config,
response.files
.filter((file: { exists: boolean }) => file.exists)
.map((file: { name: string }) => file.name),
response.files
.filter((file: { exists: boolean }) => !file.exists)
.map((file: { name: string }) => file.name),
)
})
}
async function build(config: Config, added: string[], deleted: string[]) {
for (const file of deleted) {
config.state.styleXRules.delete(file)
}
for (const file of added) {
console.log(
`${ansis.green('[cli]')} extracting ${path.join(config.input, file)}`,
)
const sourceCode = fs.readFileSync(path.join(config.input, file), 'utf8')
const result = transform(file, sourceCode, {
importSources: [
{
from: 'react-strict-dom',
as: 'css',
},
],
unstable_moduleResolution: {
type: 'commonJS',
rootDir: process.cwd(),
},
})
const rules = (result?.metadata as any)?.stylex ?? null
if (rules != null) {
config.state.styleXRules.set(file, rules)
}
}
console.log(`${ansis.green('[cli]')} writing to ${config.output}`)
const compiledCSS = await styleXBabelPlugin.processStylexRules(
[...config.state.styleXRules.values()].flat(),
true,
)
fs.writeFileSync(config.output, compiledCSS)
}
function transform(filename: string, sourceCode: string, styleXOptions: any) {
return transformSync(sourceCode, {
babelrc: false,
sourceFileName: filename,
filename,
parserOpts: {
plugins: /\.tsx?$/.test(filename)
? ['typescript', 'jsx']
: // TODO: add flow
['jsx'],
},
plugins: [styleXBabelPlugin.withOptions(styleXOptions)],
})
}
function main() {
const { values } = parseArgs({
args: process.argv.slice(2),
options: {
input: { type: 'string', short: 'i' },
output: { type: 'string', short: 'o' },
watch: { type: 'boolean', short: 'w' },
verbose: { type: 'boolean', short: 'v', default: false },
help: { type: 'boolean', short: 'h' },
},
})
if (values.help) {
help()
return
}
if (!values.input) {
console.error('Missing input file')
return
}
if (!values.output) {
console.error('Missing output file')
return
}
const config: Config = {
input: path.resolve(process.cwd(), values.input),
output: path.resolve(process.cwd(), values.output),
verbose: values.verbose ?? false,
state: {
styleXRules: new Map<string, any>(),
},
}
if (values.watch) {
watch(config)
} else {
const files = globSync('**/*.{js,ts,jsx,tsx,cjs,mjs}', {
cwd: config.input,
})
build(config, files, [])
}
}
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment