Public commit

This commit is contained in:
Alex
2023-08-03 20:06:58 -04:00
commit d96c6c7caf
333 changed files with 44633 additions and 0 deletions

View File

@ -0,0 +1,37 @@
import type { BabelPlugin } from "./esbuild-babel"
export const componentName =
(): BabelPlugin =>
({ types: t }) => ({
name: "babel-plugin-component-name",
visitor: {
ReturnStatement(path) {
if (!t.isJSXElement(path.node.argument)) return
const funcParent = path.getFunctionParent()
if (funcParent == null || !t.isArrowFunctionExpression(funcParent.node)) return
let id: string | undefined
if (
t.isCallExpression(funcParent.parent) &&
t.isIdentifier(funcParent.parent.callee) &&
t.isVariableDeclarator(funcParent.parentPath.parent) &&
t.isIdentifier(funcParent.parentPath.parent.id)
) {
id = funcParent.parentPath.parent.id.name
} else if (
t.isVariableDeclarator(funcParent.parent) &&
t.isIdentifier(funcParent.parent.id)
) {
id = funcParent.parent.id.name
}
if (id != null) {
path.node.argument.openingElement.attributes.unshift(
t.jsxAttribute(t.jsxIdentifier("data-component"), t.stringLiteral(id)),
)
}
},
},
})

View File

@ -0,0 +1,100 @@
// https://github.com/babel/babel/blob/c38bf12f010520ea7abe8a286f62922b2d1e1f1b/packages/babel-plugin-transform-react-constant-elements/src/index.ts
import type { Visitor } from "@babel/core"
import type { PluginObj, types as t } from "@babel/core"
interface PluginState {
isImmutable: boolean
mutablePropsAllowed?: boolean
}
export default (allowMutablePropsOnTags?: string[]): PluginObj => {
const HOISTED = new WeakSet()
const immutabilityVisitor: Visitor<PluginState> = {
enter(path, state) {
const stop = () => {
state.isImmutable = false
path.stop()
}
if (path.isJSXClosingElement()) {
path.skip()
return
}
// Elements with refs are not safe to hoist.
if (
path.isJSXIdentifier({ name: "ref" }) &&
path.parentPath.isJSXAttribute({ name: path.node })
) {
return stop()
}
// Ignore identifiers & JSX expressions.
if (path.isJSXIdentifier() || path.isIdentifier() || path.isJSXMemberExpression()) {
return
}
if (!path.isImmutable()) {
// If it's not immutable, it may still be a pure expression, such as string concatenation.
// It is still safe to hoist that, so long as its result is immutable.
// If not, it is not safe to replace as mutable values (like objects) could be mutated after render.
// https://github.com/facebook/react/issues/3226
if (path.isPure()) {
const { confident, value } = path.evaluate()
if (confident) {
// We know the result; check its mutability.
const isMutable =
(!state.mutablePropsAllowed && value && typeof value === "object") ||
typeof value === "function"
if (!isMutable) {
// It evaluated to an immutable value, so we can hoist it.
path.skip()
return
}
}
}
stop()
}
},
}
return {
name: "transform-react-constant-elements",
visitor: {
Program(program) {
program.traverse({
JSXElement(path) {
if (HOISTED.has(path.node)) return
HOISTED.add(path.node)
const state: PluginState = { isImmutable: true }
// This transform takes the option `allowMutablePropsOnTags`, which is an array
// of JSX tags to allow mutable props (such as objects, functions) on. Use sparingly
// and only on tags you know will never modify their own props.
if (allowMutablePropsOnTags != null) {
// Get the element's name. If it's a member expression, we use the last part of the path.
// So the option ["FormattedMessage"] would match "Intl.FormattedMessage".
let namePath = path.get("openingElement").get("name")
while (namePath.isJSXMemberExpression()) {
namePath = namePath.get("property")
}
const elementName = (namePath.node as t.JSXIdentifier).name
state.mutablePropsAllowed = allowMutablePropsOnTags.includes(elementName)
}
// Traverse all props passed to this element for immutability.
path.traverse(immutabilityVisitor, state)
if (state.isImmutable) {
path.hoist(path.scope)
}
},
})
},
},
}
}

View File

@ -0,0 +1,54 @@
import { dirname } from "path"
import glob from "fast-glob"
import type { types } from "@babel/core"
import type { BabelPlugin } from "./esbuild-babel"
const skip = new WeakSet<types.CallExpression>()
export const dynamicImport =
(filePath: string): BabelPlugin =>
({ types: t }) => ({
name: "dynamic-import",
visitor: {
Import(path) {
if (
!t.isCallExpression(path.parent) ||
path.parent.arguments.length !== 1 ||
skip.has(path.parent)
) {
return
}
const [arg] = path.parent.arguments
if (!t.isTemplateLiteral(arg)) {
return
}
const key = path.scope.generateDeclaredUidIdentifier("key")
const globText = arg.quasis.map(x => x.value.raw).join("*")
const globCandidates = glob.sync(globText, {
cwd: dirname(filePath),
})
const clone = t.cloneNode(path.parent, true)
skip.add(clone)
const cond = globCandidates.reduceRight(
(accum: types.Expression, cur) =>
t.conditionalExpression(
t.binaryExpression("===", key, t.stringLiteral(cur)),
t.callExpression(t.import(), [t.stringLiteral(cur)]),
accum,
),
clone,
)
t.cloneNode(path.parent)
path.parentPath.replaceWith(
t.sequenceExpression([t.assignmentExpression("=", key, arg), cond]),
)
},
},
})

View File

@ -0,0 +1,30 @@
import { basename, dirname } from "path"
import type { BabelPlugin } from "./esbuild-babel"
export const fileName =
({
hasFileName,
hasDirName,
path,
}: {
hasFileName: boolean
hasDirName: boolean
path: string
}): BabelPlugin =>
({ types: t }) => ({
name: "__filename polyfill",
visitor: {
Program(program) {
const assign = (id: string, value: string) =>
t.variableDeclaration("var", [
t.variableDeclarator(t.identifier(id), t.stringLiteral(value)),
])
if (hasFileName) {
program.node.body.unshift(assign("__filename", basename(path)))
}
if (hasDirName) {
program.node.body.unshift(assign("__dirname", dirname(path)))
}
},
},
})

View File

@ -0,0 +1,80 @@
import hash from "@emotion/hash"
import type { types } from "@babel/core"
import { kebabCase } from "lodash"
import type { BabelPlugin } from "./esbuild-babel"
export const inlineCSSVariables =
(): BabelPlugin<{ styles: { id: string; light: string; dark: string }[] }> =>
({ types: t }) => ({
name: "inline CSS variables",
visitor: {
Program: {
enter(_, state) {
state.styles = []
},
exit(path, { styles }) {
if (!styles.length) return
const css =
`body.light {${styles.map(s => `--${s.id}:${s.light}`).join(";")}}\n` +
`body.dark {${styles.map(s => `--${s.id}:${s.dark}`).join(";")}}`
path.node.body.unshift(
t.importDeclaration([], t.stringLiteral(`data:text/css,${encodeURI(css)}`)),
)
},
},
TaggedTemplateExpression(path, state) {
function join(exp: types.Node): string[] | undefined {
if (t.isIdentifier(exp)) return [exp.name]
if (!t.isMemberExpression(exp) || !t.isIdentifier(exp.property)) return
const prev = t.isIdentifier(exp.object) ? [exp.object.name] : join(exp.object)
return prev ? [...prev, exp.property.name] : undefined
}
const { expressions: exps } = path.node.quasi
for (const [i, exp] of exps.entries()) {
if (t.isIdentifier(exp)) {
if (exp.name === "DARK_MODE") {
exps[i] = t.stringLiteral("body.dark &")
} else if (exp.name === "LIGHT_MODE") {
exps[i] = t.stringLiteral("body.light &")
}
continue
}
if (
t.isCallExpression(exp) &&
t.isIdentifier(exp.callee, { name: "color" }) &&
exp.arguments.length === 2 &&
t.isStringLiteral(exp.arguments[0]) &&
t.isStringLiteral(exp.arguments[1])
) {
const [light, dark] = (exp.arguments as babel.types.StringLiteral[]).map(
arg => arg.value,
)
const id = hash(`${light}-${dark}`)
state.styles.push({ id, light, dark })
exps[i] = t.stringLiteral(`var(--${id})`)
continue
}
let ids: string[] | undefined
if (
t.isMemberExpression(exp) &&
t.isIdentifier(exp.property) &&
(ids = join(exp))
) {
const rest = ids.slice(1).join(".")
if (ids[0] === "vars") {
exps[i] = t.stringLiteral(`var(--${kebabCase(rest)})`)
}
if (ids[0] === "token") {
exps[i] = t.stringLiteral(`var(--color-${kebabCase(rest)})`)
}
}
}
},
},
})

View File

@ -0,0 +1,106 @@
import type { NodePath, types } from "@babel/core"
import type { Node } from "@babel/core"
import type { BabelPlugin } from "./esbuild-babel"
// useWhyDidYouUpdate
export const whyDidYouRender =
({
hookName,
hookPath,
ignoredHooks,
}: {
hookName: string
hookPath: string
ignoredHooks: string[]
}): BabelPlugin =>
({ types: t }) => {
const ignored = new WeakSet<Node>()
function ignore(node: Node) {
ignored.add(node)
return node
}
return {
name: "why-did-you-render",
visitor: {
Program(path, state) {
const id = path.scope.generateUidIdentifier(hookName)
path.node.body.unshift(
t.importDeclaration(
[t.importSpecifier(id, t.identifier(hookName))], //
t.stringLiteral(hookPath),
),
)
state.whyDidYouRenderId = id
},
VariableDeclaration(path, state) {
if (ignored.has(path.node)) return
const decls = path.node.declarations
if (decls.length !== 1) return
const [{ init, id }] = decls
if (
!t.isCallExpression(init) ||
!t.isIdentifier(init.callee) ||
!init.callee.name.startsWith("use") ||
init.callee.name.length <= 3
) {
return
}
if (ignoredHooks.includes(init.callee.name)) {
return
}
const findParent = <T extends types.Node>(
predicate: (node: types.Node) => node is T,
) => path.findParent(path => predicate(path.node)) as NodePath<T> | undefined
const parentId =
findParent(t.isFunctionDeclaration)?.node.id!.name ??
(<types.Identifier>(
(<types.VariableDeclarator>findParent(t.isArrowFunctionExpression)?.parent)
?.id
))?.name
if (!parentId || parentId.startsWith("use")) {
return
}
const callee = t.cloneNode(state.whyDidYouRenderId as types.Identifier)
if (t.isIdentifier(id)) {
path.insertAfter(
t.callExpression(callee, [
t.stringLiteral(parentId),
t.stringLiteral(init.callee.name),
id,
]),
)
return
}
const temporaryId = path.scope.generateUidIdentifier(init.callee.name)
path.replaceWithMultiple([
ignore(
t.variableDeclaration(path.node.kind, [
t.variableDeclarator(temporaryId, init),
t.variableDeclarator(id, temporaryId),
]),
),
t.expressionStatement(
t.callExpression(callee, [
t.stringLiteral(parentId),
t.stringLiteral(init.callee.name),
temporaryId,
]),
),
])
},
},
}
}

View File

@ -0,0 +1,24 @@
// https://github.com/igoradamenko/esbuild-plugin-alias/blob/master/index.js
// MIT License. Copyright (c) 2021 Igor Adamenko
import type { Plugin } from "esbuild"
export function alias(options: Record<string, string>): Plugin {
const aliases = Object.keys(options)
const re = new RegExp(`^(${aliases.map(x => escapeRegExp(x)).join("|")})$`)
return {
name: "alias",
setup(build) {
// we do not register 'file' namespace here, because the root file won't be processed
// https://github.com/evanw/esbuild/issues/791
build.onResolve({ filter: re }, args => ({
path: options[args.path],
}))
},
}
}
function escapeRegExp(string: string) {
// $& means the whole matched string
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
}

View File

@ -0,0 +1,101 @@
import fs from "fs"
import { extname } from "path"
import * as babel from "@babel/core"
import type * as esbuild from "esbuild"
import { fileName } from "./babel-filename"
import { dynamicImport } from "./babel-dynamic-import"
import { inlineCSSVariables } from "./babel-inline-css-vars"
import { componentName } from "./babel-component-name"
// import { whyDidYouRender } from "./babel-why-did-you-render";
// import constantElement from "./babel-constant-element";
const __PROD__ = process.env.NODE_ENV === "production"
export type BabelPlugin<T = unknown> = (
Babel: typeof babel,
) => babel.PluginObj<T & babel.PluginPass>
export interface BabelPluginData {
path: string
}
export const babelPlugin = (extraPlugins: babel.PluginItem[] = []): esbuild.Plugin => ({
name: "babel",
setup(build) {
function* getBabelPlugins(
path: string,
content: string,
): Generator<babel.PluginItem, void, unknown> {
const hasFileName = content.includes("__filename")
const hasDirName = content.includes("__dirname")
if (hasFileName || hasDirName) {
yield fileName({ hasFileName, hasDirName, path })
}
if (!__PROD__ && /<\w/.test(content)) {
yield componentName()
}
// if (!__PROD__ && content.includes("use")) {
// yield whyDidYouRender({
// hookName: "useWhyDidYouUpdate",
// hookPath: resolve(__dirname, "../../src/shared/hooks/useWhy.ts"),
// ignoredHooks: ["useTreeContext"],
// });
// }
if (content.includes("vars.") || content.includes("token.")) {
yield inlineCSSVariables()
}
if (content.includes("await import(`")) {
yield dynamicImport(path)
}
if (content.includes('.macro"') || content.includes("/macro")) {
yield ["babel-plugin-macros", { typeGraphQL: { useParameterDecorator: true } }]
}
if (content.includes("@emotion")) {
yield [require("@emotion/babel-plugin"), { sourceMap: false }]
}
if (__PROD__ && content.includes("gql`")) {
yield [require("babel-plugin-transform-minify-gql-template-literals")]
}
}
build.onLoad({ filter: /\.tsx?$/ }, args => {
if (args.path.includes("node_modules/")) {
return null
}
const { path } = args
const file = fs.readFileSync(path, "utf-8")
const plugins: babel.PluginItem[] = Array.from(getBabelPlugins(path, file)).concat(
extraPlugins,
)
let code = file
const pluginData: BabelPluginData = { path }
if (plugins.length) {
const res = babel.transformSync(file, {
filename: path,
babelrc: false,
configFile: false,
parserOpts: {
plugins: ["typescript", "decorators-legacy", "jsx", "importAssertions"],
},
generatorOpts: { decoratorsBeforeExport: true },
plugins,
})!
code = res.code!
} else {
return null
}
return {
contents: code,
loader: extname(path).slice(1) as any,
pluginData,
}
})
},
})

View File

@ -0,0 +1,107 @@
// https://github.com/nihalgonsalves/esbuild-plugin-browserslist
// MIT License. Copyright (c) 2021 Nihal Gonsalves
import { z } from "zod"
import browserslist from "browserslist"
export function getDefaultTarget() {
return getTarget(
["Chrome", "Firefox", "Edge", "Safari"].map(browser => `last 2 ${browser} versions`),
)
}
enum BrowserslistKind {
Edge = "edge",
Firefox = "firefox",
Chrome = "chrome",
Safari = "safari",
iOS = "ios_saf",
Android = "android",
AndroidChrome = "and_chr",
AndroidFirefox = "and_ff",
AndroidUC = "and_uc",
AndroidQQ = "and_qq",
Samsung = "samsung",
Opera = "opera",
OperaMini = "op_mini",
OperaMobile = "op_mob",
IE = "ie",
IEMobile = "ie_mob",
BlackBerry = "bb",
Baidu = "baidu",
Kaios = "kaios",
Node = "node",
}
const enum EsbuildEngine {
Chrome = "chrome",
Edge = "edge",
ES = "es",
Firefox = "firefox",
Hermes = "hermes",
IE = "ie",
IOS = "ios",
Node = "node",
Opera = "opera",
Rhino = "rhino",
Safari = "safari",
}
const BrowserslistEsbuildMapping: Partial<Record<BrowserslistKind, EsbuildEngine>> = {
// exact map
[BrowserslistKind.Edge]: EsbuildEngine.Edge,
[BrowserslistKind.Firefox]: EsbuildEngine.Firefox,
[BrowserslistKind.Chrome]: EsbuildEngine.Chrome,
[BrowserslistKind.Safari]: EsbuildEngine.Safari,
[BrowserslistKind.iOS]: EsbuildEngine.IOS,
[BrowserslistKind.Node]: EsbuildEngine.Node,
[BrowserslistKind.IE]: EsbuildEngine.IE,
[BrowserslistKind.Opera]: EsbuildEngine.Opera,
// approximate mapping
[BrowserslistKind.Android]: EsbuildEngine.Chrome,
[BrowserslistKind.AndroidChrome]: EsbuildEngine.Chrome,
[BrowserslistKind.AndroidFirefox]: EsbuildEngine.Firefox,
// the rest have no equivalent for esbuild
}
const BrowserSchema = z.nativeEnum(BrowserslistKind)
/** 123 or 123.456 or 123.456.789 */
const VersionSchema = z.string().regex(/^(\d+\.\d+\.\d+|\d+\.\d+|\d+)$/)
export function getTarget(targets: string[]): string[] {
const result = browserslist(targets)
.map(entry => {
const [rawBrowser, rawVersionOrRange] = entry.split(" ")
const rawVersionNormalized = rawVersionOrRange
// e.g. 13.4-13.7, take the lower range
?.replace(/-[\d.]+$/, "")
// all => replace with 1
?.replace("all", "1")
const browserResult = BrowserSchema.safeParse(rawBrowser)
const versionResult = VersionSchema.safeParse(rawVersionNormalized)
if (!browserResult.success || !versionResult.success) {
return
}
const { data: browser } = browserResult
const { data: version } = versionResult
const esbuildTarget = BrowserslistEsbuildMapping[browser]
if (!esbuildTarget) {
return
}
return { target: esbuildTarget, version }
})
.filter(Boolean)
.map(({ target, version }) => `${target}${version}`)
if (result.length === 0) {
throw new Error("Could not resolve any esbuild targets")
}
return result
}

View File

@ -0,0 +1,338 @@
// import { relative, resolve } from "path";
import type { PluginBuild } from "esbuild"
import type * as babel from "@babel/core"
import type { NodePath, types } from "@babel/core"
import { expression } from "@babel/template"
import hash from "@emotion/hash"
import { compile, serialize, stringify } from "stylis"
import { dropRightWhile } from "lodash"
const NS = "extract-css"
const runtimeArgs = ["Tag", "className", "vars"] as const
// const root = resolve(__dirname, "../..");
const shared = /* jsx */ `
({ as = Tag, className: cls, style, ...rest }, ref) => {
return _jsx(as, {
...rest,
ref,
style: vars != null ? getStyle({ ...style }, className, vars) : style,
className: cx([className, cls])
})
}
`
const runtime = /* jsx */ `
import { jsx } from "react/jsx-runtime";
import { forwardRef, memo } from "react";
import { cx } from "@emotion/css";
const supportsAs = new WeakSet();
const _jsx = jsx;
function getStyle(style, className, vars) {
for (let i = 0; i < vars.length; i++) {
const name = "--" + className + "-" + i;
const variable = vars[i];
if (variable !== null) {
style[name] = typeof variable === "function" ? variable(props) : variable;
}
}
return style;
}
export default function create(${runtimeArgs.join(", ")}) {
const Component = memo(
forwardRef(
supportsAs.has(Tag)
? ${shared.replace("as, {", "Tag, { as,")}
: ${shared}
)
);
Component.withComponent = function (Tag) {
return create(${runtimeArgs.join(", ")});
};
supportsAs.add(Component);
return Component;
}
`
const flattenArgs = (args: { [key in (typeof runtimeArgs)[number]]: string }) =>
runtimeArgs.map(key => args[key]).join(", ")
const styledComponent = expression({
// plugins: ["jsx"],
syntacticPlaceholders: true,
})(
"%%create%%(" +
flattenArgs({
className: "%%className%%",
Tag: "%%tag%%",
vars: "%%vars%%",
}) +
")",
)
const validParentTypes = new Set<types.Node["type"]>([
"ArrayExpression",
"BinaryExpression",
"CallExpression",
"JSXExpressionContainer",
"LogicalExpression",
"ObjectProperty",
"VariableDeclarator",
])
interface State extends babel.PluginPass {
styles: string[]
program: types.Program
runtimeHelper?: types.Identifier
}
export function extractCSS(build: PluginBuild, { className }: { className: string }) {
const RUNTIME = /^@extract-css\/runtime$/
build.onResolve({ filter: RUNTIME }, ({ path }) => ({ namespace: NS, path }))
build.onLoad({ filter: RUNTIME, namespace: NS }, () => ({
contents: runtime,
loader: "jsx",
resolveDir: __dirname,
}))
const plugin = ({ types: t }: typeof babel): babel.PluginObj<State> => {
const classNameMap = new WeakMap<types.Expression, string>()
function isImportedSpecifierFrom(path: NodePath, name: string, module: string) {
const declPath = path.scope.getBinding(name)!.path
return (
t.isImportSpecifier(declPath.node) &&
t.isImportDeclaration(declPath.parent) &&
declPath.parent.source.value === module
)
}
function isDefaultImportedFrom(path: NodePath, name: string, module: string) {
const binding = path.scope.getBinding(name)
if (!binding) return false
const declPath = binding.path
return (
t.isImportDefaultSpecifier(declPath.node) &&
t.isImportDeclaration(declPath.parent) &&
declPath.parent.source.value === module
)
}
function buildClassName(hash: string, componentName = "styled") {
return className.replaceAll("[hash]", hash).replaceAll("[name]", componentName)
}
function getComponentName(path: NodePath): string | undefined {
if (t.isVariableDeclarator(path.parent) && t.isIdentifier(path.parent.id)) {
return path.parent.id.name
}
}
const visitor: babel.Visitor<State> = {
TaggedTemplateExpression(path, state) {
const { styles } = state
const quasiExps = path
.get("quasi")
.get("expressions") as NodePath<types.Expression>[]
const { tag, quasi } = path.node
if (!validParentTypes.has(path.parent.type)) {
return
}
function extract(getPrefix: (className: string) => string) {
let bailout = false
const raws = quasi.quasis.map(q => q.value.raw)
const className = buildClassName(
hash(JSON.stringify(raws)),
getComponentName(path),
)
const skipInterpolations = new Set<number>()
const cssText = raws
.map((left, i) => {
if (bailout) {
return null!
}
if (i === raws.length - 1) {
return left
}
const exp = quasi.expressions[i]
const evaluated = quasiExps[i].evaluate()
if (evaluated.confident) {
if (
typeof evaluated.value === "string" ||
typeof evaluated.value === "number"
) {
skipInterpolations.add(i)
return left + String(evaluated.value)
}
}
if (t.isIdentifier(exp)) {
const binding = path.scope.getBinding(exp.name)
if (binding) {
const { node } = binding.path
if (t.isVariableDeclarator(node)) {
const cls = classNameMap.get(node.init!)
if (cls) {
skipInterpolations.add(i)
return left + cls
}
if (t.isStringLiteral(node.init)) {
skipInterpolations.add(i)
return left + node.init.value
}
}
bailout = true
}
} else if (t.isStringLiteral(exp)) {
skipInterpolations.add(i)
return left + exp.value
}
return `${left}var(--${className}-${i})`
})
.join("")
const compiled = compile(`${getPrefix(className)}{${cssText}}}`)
return {
className,
skipInterpolations,
bailout,
style: serialize(compiled, stringify),
}
}
function processStyled(tag: types.Expression) {
const { className, skipInterpolations, style, bailout } = extract(c => `.${c}`)
if (bailout) {
return
}
styles.push(style)
if (!state.runtimeHelper) {
state.runtimeHelper = path.scope.generateUidIdentifier("create")
state.program.body.unshift(
t.importDeclaration(
[t.importDefaultSpecifier(state.runtimeHelper)],
t.stringLiteral("@extract-css/runtime"),
),
)
}
const fn = styledComponent({
create: state.runtimeHelper,
className: t.stringLiteral(className),
tag:
t.isIdentifier(tag) && /^[a-z]/.test(tag.name)
? t.stringLiteral(tag.name)
: tag,
vars: quasi.expressions.length
? t.arrayExpression(
dropRightWhile(
quasi.expressions.map((e, i) =>
skipInterpolations.has(i)
? t.nullLiteral()
: (e as types.Expression),
),
n => t.isNullLiteral(n),
),
)
: t.nullLiteral(),
})
classNameMap.set(fn, className)
const [newPath] = path.replaceWith(fn)
newPath.addComment("leading", " @__PURE__ ")
}
if (t.isIdentifier(tag) && tag.name === "keyframes") {
if (!isImportedSpecifierFrom(path, tag.name, "@emotion/css")) {
return
}
const { className, style, bailout } = extract(c => `@keyframes ${c}`)
if (bailout) {
return
}
styles.push(style)
path.replaceWith(t.stringLiteral(className))
} else if (t.isIdentifier(tag) && tag.name === "css") {
if (!isImportedSpecifierFrom(path, tag.name, "@emotion/css")) {
return
}
const { className, style, bailout } = extract(c => `.${c}`)
if (bailout) {
return
}
styles.push(style)
path.replaceWith(t.stringLiteral(className))
} else if (
t.isMemberExpression(tag) &&
t.isIdentifier(tag.object, { name: "styled" }) &&
!t.isPrivateName(tag.property)
) {
if (!isDefaultImportedFrom(path, tag.object.name, "@emotion/styled")) {
return
}
processStyled(tag.property)
} else if (
t.isCallExpression(tag) &&
t.isIdentifier(tag.callee, { name: "styled" }) &&
tag.arguments.length === 1 &&
t.isExpression(tag.arguments[0])
) {
if (!isDefaultImportedFrom(path, tag.callee.name, "@emotion/styled")) {
return
}
processStyled(tag.arguments[0])
}
},
}
return {
name: "plugin",
visitor: {
Program: {
enter(program, state) {
state.styles = []
state.program = program.node
},
exit(program, state) {
program.traverse(visitor, state)
if (state.styles.length) {
const css =
// `/*./${relative(root, state.filename!)}*/` +
state.styles.join("\n")
program.node.body.unshift(
t.importDeclaration(
[],
t.stringLiteral(`data:text/css,${encodeURI(css)}`),
),
)
}
},
},
},
}
}
return plugin
}

View File

@ -0,0 +1,92 @@
import { relative, resolve } from "path"
import { promises as fs } from "fs"
import type { Plugin } from "esbuild"
import postcss from "postcss"
// @ts-expect-error
import postcssSass from "@csstools/postcss-sass"
import cssModules from "postcss-modules"
type Options = Parameters<typeof cssModules>[0]
const PLUGIN_NAME = "esbuild-css-modules"
async function buildCSSModule(cssFullPath: string, options: Options) {
options = {
localsConvention: "camelCaseOnly",
...options,
}
const source = await fs.readFile(cssFullPath)
let classNames = {}
const { css } = await postcss([
postcssSass(),
cssModules({
getJSON(_, json) {
classNames = json
return classNames
},
...options,
}),
]).process(source, {
from: cssFullPath,
map: false,
})
return {
css,
classNames,
}
}
const srcDir = resolve(__dirname, "../../src")
const plugin = (options: Options = {}): Plugin => ({
name: PLUGIN_NAME,
async setup(build) {
const memfs = new Map<string, string>()
const FS_NAMESPACE = PLUGIN_NAME + "-fs"
build.onResolve({ filter: /\.modules?\.s?css$/, namespace: "file" }, async args => {
const res = await build.resolve(args.path, {
kind: "import-statement",
resolveDir: args.resolveDir,
})
// This is just the unique ID for this CSS module. We use a relative path to make it easier to debug.
const path = relative(srcDir, res.path)
return {
path,
namespace: PLUGIN_NAME,
pluginData: {
realPath: res.path,
},
}
})
build.onResolve({ filter: /^@css-modules\/.*/ }, ({ path }) => ({
path,
namespace: FS_NAMESPACE,
}))
build.onLoad({ filter: /./, namespace: FS_NAMESPACE }, ({ path }) => ({
contents: memfs.get(path)!,
loader: "css",
}))
build.onLoad({ filter: /./, namespace: PLUGIN_NAME }, async args => {
const tmpFilePath = "@css-modules/" + args.path
const { classNames, css } = await buildCSSModule(args.pluginData.realPath, options)
memfs.set(tmpFilePath, css)
return {
contents:
`import ${JSON.stringify(tmpFilePath)};\n` +
`module.exports = ${JSON.stringify(classNames)};`,
loader: "js",
}
})
},
})
export { plugin as cssModules }

View File

@ -0,0 +1,14 @@
import type * as esbuild from "esbuild"
export const externalDep = (externals: string[]): esbuild.Plugin => ({
name: "externalDep",
setup(build) {
for (const module of externals) {
const resolved: esbuild.OnResolveResult = {
path: `/vendor/${module}/index.js`,
external: true,
}
build.onResolve({ filter: RegExp(`^${module}$`) }, () => resolved)
}
},
})

View File

@ -0,0 +1 @@
module.exports = globalThis.monaco

View File

@ -0,0 +1,32 @@
import { dirname } from "path"
import { promises as fs } from "fs"
import * as esbuild from "esbuild"
export const stringImport = (): esbuild.Plugin => ({
name: "stringImport",
setup(build) {
build.onResolve({ filter: /\?string$/ }, async ({ path, importer }) => {
const resolved = await build.resolve(path.replace(/\?string$/, ""), {
kind: "import-statement",
importer,
resolveDir: dirname(importer),
})
return {
namespace: stringImport.name,
path: resolved.path,
}
})
build.onLoad({ filter: /.*/, namespace: stringImport.name }, async ({ path }) => {
let code = await fs.readFile(path, "utf-8")
if (build.initialOptions.minify && path.endsWith(".css")) {
;({ code } = await esbuild.transform(code, { loader: "css", minify: true }))
}
return {
contents: code,
loader: "text",
}
})
},
})

View File

@ -0,0 +1,42 @@
// MIT License
// Copyright (c) 2021 Marton Lederer
// https://github.com/martonlederer/esbuild-plugin-yaml/tree/d8d14d18c999f6507e906a7015ace0c991b507b4
import { isAbsolute, join } from "path"
import { TextDecoder } from "util"
import { promises as fs } from "fs"
import type { Plugin } from "esbuild"
import type { LoadOptions } from "js-yaml"
import yaml from "js-yaml"
interface YamlPluginOptions {
loadOptions?: LoadOptions
}
export const yamlPlugin = (options: YamlPluginOptions = {}): Plugin => ({
name: "yaml",
setup(build) {
// resolve .yaml and .yml files
build.onResolve({ filter: /\.(yml|yaml)$/ }, ({ path, resolveDir }) => {
if (resolveDir === "") return
return {
path: isAbsolute(path) ? path : join(resolveDir, path),
namespace: "yaml",
}
})
// load files with "yaml" namespace
build.onLoad({ filter: /.*/, namespace: "yaml" }, async ({ path }) => {
const yamlContent = await fs.readFile(path)
const parsed = yaml.load(
new TextDecoder().decode(yamlContent),
options?.loadOptions,
) as any
return {
contents: JSON.stringify(parsed),
loader: "json",
}
})
},
})