Created
June 6, 2024 06:19
-
-
Save javascripter/147599d3bce3c2da1fa28e6d50c83fe5 to your computer and use it in GitHub Desktop.
StyleX CLI based on https://github.com/facebook/stylex/discussions/583#discussioncomment-9622825
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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