Skip to content

Commit

Permalink
feat: implmeent inject style
Browse files Browse the repository at this point in the history
  • Loading branch information
nonzzz committed Apr 17, 2024
1 parent 7f9268e commit 9c2a4ef
Show file tree
Hide file tree
Showing 14 changed files with 237 additions and 104 deletions.
Binary file modified .yarn/install-state.gz
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import { create as _create, props as _props } from "@stylexjs/stylex";
import { injectGlobalStyle } from "@stylex-extend/core";
import { expression } from "./expression.stylex";
export const styles = injectGlobalStyle({
html: {
fontSize: expression.font,
},
});
export const styles = "";
4 changes: 3 additions & 1 deletion packages/babel-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@
"devDependencies": {
"@stylex-extend/react": "0.1.3",
"@types/babel__core": "^7.20.5",
"@types/stylis": "^4.2.5",
"babel-plugin-tester": "^11.0.4"
},
"dependencies": {
"@babel/core": "^7.23.9",
"@stylexjs/shared": "^0.5.1"
"@stylexjs/shared": "^0.5.1",
"stylis": "^4.3.1"
},
"files": [
"dist",
Expand Down
29 changes: 12 additions & 17 deletions packages/babel-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as b from '@babel/core'
import type { PluginObj } from '@babel/core'
import { transformInjectGlobalStyle, transformStylexAttrs } from './visitor'
import { scanImportStmt, transformInjectGlobalStyle, transformStylexAttrs } from './visitor'
import { Context } from './state-context'
import type { StylexExtendBabelPluginOptions } from './interface'
import type { ImportIdentifiers, InternalPluginOptions } from './state-context'
Expand All @@ -11,6 +11,7 @@ const defaultOptions: InternalPluginOptions = {
stylex: {
helper: 'props'
},
enableInjectGlobalStyle: true,
classNamePrefix: 'x',
unstable_moduleResolution: {
type: 'commonJS',
Expand Down Expand Up @@ -45,36 +46,30 @@ function declare({ types: t }: typeof b): PluginObj {
if (typeof pluginOptions.stylex === 'boolean') {
pluginOptions.stylex = { helper: pluginOptions.stylex ? 'props' : '' }
}

ctx.filename = state.filename || (state.file.opts?.sourceFileName ?? undefined)
const body = path.get('body')
for (const stmt of body) {
if (stmt.isImportDeclaration()) {
for (const decl of stmt.node.specifiers) {
if (decl.type === 'ImportSpecifier') {
ctx.imports.add(decl.local.name)
}
}
}
}
const modules = ['create']
if (pluginOptions.stylex.helper) modules.push(pluginOptions.stylex.helper)
const identifiers = modules.reduce<ImportIdentifiers>((acc, cur) => ({ ...acc, [cur]: path.scope.generateUidIdentifier(cur) }), {})
const importSpecs = Object.values(identifiers).map((a, i) => t.importSpecifier(a, t.identifier(modules[i])))
const importStmt = t.importDeclaration(importSpecs, t.stringLiteral('@stylexjs/stylex'))
path.unshiftContainer('body', importStmt)
if (pluginOptions.stylex.helper) {
const importSpecs = Object.values(identifiers).map((a, i) => t.importSpecifier(a, t.identifier(modules[i])))
const importStmt = t.importDeclaration(importSpecs, t.stringLiteral('@stylexjs/stylex'))
path.unshiftContainer('body', importStmt)
}
const anchor = body.findIndex(p => t.isImportDeclaration(p.node))
ctx.setupOptions(pluginOptions, identifiers, anchor === -1 ? 0 : anchor)
ctx.filename = this.filename
scanImportStmt(body, ctx)
path.traverse({
CallExpression(path) {
transformInjectGlobalStyle(path, ctx)
const CSS = transformInjectGlobalStyle(path, ctx)
Reflect.set(state.file.metadata, 'globalStyle', CSS)
}
})
},
exit(path) {
const body = path.get('body')
const anchor = ctx.anchor + ctx.lastBindingPos
if (anchor !== -1) body[anchor].insertAfter(ctx.stmts)
if (anchor !== -1 && ctx.stmts.length) body[anchor].insertAfter(ctx.stmts)
ctx.stmts = []
}
},
Expand Down
1 change: 1 addition & 0 deletions packages/babel-plugin/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type ModuleResolution = ModuleResolutionCommonJS | MOduleResolutionHaste

export interface StylexExtendBabelPluginOptions {
stylex: boolean | StylexBindingMeta
enableInjectGlobalStyle?: boolean
/**
* @default 'x'
* @see {@link https://stylexjs.com/docs/api/configuration/babel-plugin/#classnameprefix}
Expand Down
55 changes: 50 additions & 5 deletions packages/babel-plugin/src/state-context.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,43 @@
import path from 'path'
import { types } from '@babel/core'
import type { StylexBindingMeta, StylexExtendBabelPluginOptions } from './interface'

export type InternalPluginOptions = Required<Omit<StylexExtendBabelPluginOptions, 'stylex'>> & { stylex: StylexBindingMeta }

export type ImportIdentifiers = Record<string, types.Identifier>

interface FileNamesForHashing {
fileName: string
exportName: string
}

export function matchesFileSuffix(allowedSuffix: string) {
return (filename: string) => filename.endsWith(`${allowedSuffix}.js`) ||
filename.endsWith(`${allowedSuffix}.ts`) ||
filename.endsWith(`${allowedSuffix}.tsx`) ||
filename.endsWith(`${allowedSuffix}.jsx`) ||
filename.endsWith(`${allowedSuffix}.mjs`) ||
filename.endsWith(`${allowedSuffix}.cjs`) ||
filename.endsWith(allowedSuffix)
}

const EXTENSIONS = ['.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs']

function filePathResolver(relativePath: string, sourceFilePath: string) {
for (const ext of ['', ...EXTENSIONS]) {
const importPathStr = relativePath + ext
if (importPathStr.startsWith('.')) {
try {
return require.resolve(importPathStr, {
paths: [path.dirname(sourceFilePath)]
})
} catch {
return null
}
}
}
}

export class Context {
options: InternalPluginOptions
importIdentifiers: ImportIdentifiers
Expand All @@ -13,6 +46,7 @@ export class Context {
anchor: number
imports: Set<string>
filename: string | undefined
fileNamesForHashing: Map<string, FileNamesForHashing>
constructor() {
this.options = Object.create(null)
this.importIdentifiers = Object.create(null)
Expand All @@ -21,10 +55,11 @@ export class Context {
this.imports = new Set()
this.lastBindingPos = 0
this.filename = undefined
this.fileNamesForHashing = new Map()
}

setupOptions(pluginOptions: InternalPluginOptions, identifiers: ImportIdentifiers, anchor: number) {
this.options = pluginOptions
this.options = { ...this.options, ...pluginOptions }
this.importIdentifiers = identifiers
this.anchor = anchor
}
Expand All @@ -41,11 +76,21 @@ export class Context {
return !!this.options.stylex.helper
}

get fileNamesForHashing() {
return []
importPathResolver(importPath: string) {
if (!this.filename) throw new Error('filename is not defined')
const importerPath = filePathResolver(importPath, this.filename)
if (!importerPath) throw new Error(`[stylex-extend]: Cannot resolve module ${importPath}`)
switch (this.options.unstable_moduleResolution.type) {
case 'commonJS':
return [importerPath, path.relative(this.options.unstable_moduleResolution.rootDir, importerPath)]
case 'haste':
return [importerPath, importerPath]
case 'experimental_crossFileParsing':
return [importerPath, importerPath]
}
}

importPathResolver(importPath: string) {

addImports(next: string[]) {
this.imports = new Set([...this.imports, ...next])
}
}
117 changes: 69 additions & 48 deletions packages/babel-plugin/src/visitor/global-style.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,78 @@
// API InjectGlobalStyle
// syntax: injectGlobalStyle()

import { types } from '@babel/core'
import { utils } from '@stylexjs/shared'
import type { NodePath } from '@babel/core'
import type { CSSObject } from '@stylex-extend/shared'
import type { Context } from '../state-context'
import { compile, serialize, stringify } from 'stylis'
import { Context } from '../state-context'
import type { CSSObjectValue } from '../interface'
import { CSSContext, createCSSContext, scanObjectExpression } from './jsx-attribute'
import { createCSSContext, scanObjectExpression } from './jsx-attribute'

// This function is base on the @stylexjs/shared defineVars
// themeName: utils.genFileBasedIdentifier({ fileName, exportName }),
function getCSSVarName(key: string, seed: string) {
if (key[0] === '_' && key[1] === '#') {
return `var(--${seed + utils.hash(key.slice(2))})`
}
return key
function getCSSVarName(themeName: string, key: string, classNamePrefix: string) {
return `var(--${classNamePrefix + utils.hash(`${themeName}.${key}`)})`
}

function traverseCSSObject(CSSRule: CSSObjectValue, seed: string) {
const obj = {}
if (!CSSRule) return obj
for (const selector in CSSRule) {
const decls = CSSRule[selector]
const next = {}
if (!decls || typeof decls !== 'object') continue
for (const decl in decls) {
const attr = decls[decl]
switch (typeof attr) {
case 'object':
Object.assign(next, traverseCSSObject(attr!, seed))
break
case 'string': {
Object.assign(next, { [decl]: getCSSVarName(attr, seed) })
break
}
default:
Object.assign(next, { [decl]: attr })
}
}
Object.assign(obj, { [selector]: next })
}
const hyphenateRegex = /[A-Z]|^ms/g

function isCustomProperty(prop: string) {
return prop.charCodeAt(1) === 45
}

return obj
function processStyleName(prop: string) {
return isCustomProperty(prop) ? prop : prop.replace(hyphenateRegex, '-$&').toLowerCase()
}

export function CombiningStyleSheets(CSSContext: CSSContext, seed: string) {
const CSSRules = CSSContext.rules
const sheets: Record<string, CSSObject> = {}
for (const CSSRule of CSSRules) {
Object.assign(sheets, traverseCSSObject(CSSRule, seed))
class Stringify {
css: string
ctx: Context
constructor(rules: CSSObjectValue[], ctx: Context) {
this.css = ''
this.ctx = ctx
this.run(rules)
}

print(s: string | number) {
this.css += s
}

get classNamePrefix() {
return this.ctx.options.classNamePrefix
}

run(rule: CSSObjectValue[] | CSSObjectValue) {
if (Array.isArray(rule)) {
for (const r of rule) {
this.run(r)
}
} else {
for (const selector in rule) {
const content = rule[selector]
if (!content) continue
if (typeof content === 'object') {
this.print(selector)
this.print('{')
this.run(content)
this.print('}')
} else {
this.print(processStyleName(selector))
this.print(':')
if (typeof content === 'string') {
let c = content
if (content[0] === '_' && content[1] === '~') {
const [belong, attr] = content.slice(2).split('.')
if (this.ctx.fileNamesForHashing.has(belong)) {
const { fileName, exportName } = this.ctx.fileNamesForHashing.get(belong)!
const themeName = utils.genFileBasedIdentifier({ fileName, exportName })
c = getCSSVarName(themeName, attr, this.classNamePrefix)
}
}
this.print(`${c}`)
} else {
this.print(content)
}
this.print(';')
}
}
}
}
return sheets
}

export function transformInjectGlobalStyle(path: NodePath<types.CallExpression>, ctx: Context) {
Expand All @@ -60,11 +81,11 @@ export function transformInjectGlobalStyle(path: NodePath<types.CallExpression>,
const args = path.get('arguments')
if (args.length > 1) throw new Error(`[stylex-extend]: ${node.callee.name} only accept one argument`)
if (!args[0].isObjectExpression()) throw new Error('[stylex-extend]: can\'t pass not object value for attribute \'stylex\'.')
const { anchor, options } = ctx
const { classNamePrefix } = options
const expression = args[0]
const CSSContext = createCSSContext(expression.node.properties.length, anchor)
const CSSContext = createCSSContext(expression.node.properties.length, ctx.anchor)
scanObjectExpression(expression, CSSContext)
const sheets = CombiningStyleSheets(CSSContext, classNamePrefix)
console.log(sheets)
const sb = new Stringify(CSSContext.rules, ctx)
const CSS = serialize(compile(sb.css), stringify)
path.replaceWith(types.stringLiteral(''))
return CSS
}
44 changes: 44 additions & 0 deletions packages/babel-plugin/src/visitor/import-stmt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import fs from 'fs'
import { parseSync, traverse } from '@babel/core'
import { types } from '@babel/core'
import type { NodePath } from '@babel/core'
import { Context, matchesFileSuffix } from '../state-context'
import { getStringValue } from './jsx-attribute'

const STYLEX_EXTEND = '@stylex-extend/core'

function isTopLevelCalled(p: NodePath) {
return types.isProgram(p.parent) || types.isExportDefaultDeclaration(p.parent) || types.isExportNamedDeclaration(p.parent)
}

export function scanImportStmt(stmts: NodePath<types.Statement>[], ctx: Context) {
const matchers = matchesFileSuffix(ctx.options.unstable_moduleResolution.themeFileExtension!)
for (const stmt of stmts) {
if (!stmt.isImportDeclaration()) continue
if (stmt.node.source.value === STYLEX_EXTEND || matchers(stmt.node.source.value)) {
const specs = stmt.node.specifiers.filter((s) => s.type === 'ImportSpecifier') as types.ImportSpecifier[]
if (stmt.node.source.value === STYLEX_EXTEND) {
ctx.addImports(specs.map((s) => s.local.name))
} else {
const [filePath, fileName] = ctx.importPathResolver(stmt.node.source.value)
const codeContent = fs.readFileSync(filePath, 'utf-8')
const ast = parseSync(codeContent, { babelrc: true })
const seens = new Set<string>(specs.map((s) => getStringValue(s.imported)))
traverse(ast!, {
VariableDeclaration(path) {
if (isTopLevelCalled(path)) {
const { node } = path
for (const decl of node.declarations) {
if (types.isIdentifier(decl.id) && seens.has(decl.id.name)) {
const varId = decl.id.name
ctx.fileNamesForHashing.set(decl.id.name, { fileName, exportName: varId })
}
}
}
path.skip()
}
})
}
}
}
}
1 change: 1 addition & 0 deletions packages/babel-plugin/src/visitor/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { transformStylexAttrs } from './jsx-attribute'
export { transformInjectGlobalStyle } from './global-style'
export { scanImportStmt } from './import-stmt'
Loading

0 comments on commit 9c2a4ef

Please sign in to comment.