Public commit
This commit is contained in:
37
scripts/plugins/babel-component-name.ts
Normal file
37
scripts/plugins/babel-component-name.ts
Normal 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)),
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
100
scripts/plugins/babel-constant-element.ts
Normal file
100
scripts/plugins/babel-constant-element.ts
Normal 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)
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
54
scripts/plugins/babel-dynamic-import.ts
Normal file
54
scripts/plugins/babel-dynamic-import.ts
Normal 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]),
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
30
scripts/plugins/babel-filename.ts
Normal file
30
scripts/plugins/babel-filename.ts
Normal 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)))
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
80
scripts/plugins/babel-inline-css-vars.ts
Normal file
80
scripts/plugins/babel-inline-css-vars.ts
Normal 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)})`)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
106
scripts/plugins/babel-why-did-you-render.ts
Normal file
106
scripts/plugins/babel-why-did-you-render.ts
Normal 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,
|
||||
]),
|
||||
),
|
||||
])
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
24
scripts/plugins/esbuild-alias.ts
Normal file
24
scripts/plugins/esbuild-alias.ts
Normal 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, "\\$&")
|
||||
}
|
101
scripts/plugins/esbuild-babel.ts
Normal file
101
scripts/plugins/esbuild-babel.ts
Normal 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,
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
107
scripts/plugins/esbuild-browserslist.ts
Normal file
107
scripts/plugins/esbuild-browserslist.ts
Normal 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
|
||||
}
|
338
scripts/plugins/esbuild-css-extract.ts
Normal file
338
scripts/plugins/esbuild-css-extract.ts
Normal 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
|
||||
}
|
92
scripts/plugins/esbuild-css-modules.ts
Normal file
92
scripts/plugins/esbuild-css-modules.ts
Normal 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 }
|
14
scripts/plugins/esbuild-external-dep.ts
Normal file
14
scripts/plugins/esbuild-external-dep.ts
Normal 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)
|
||||
}
|
||||
},
|
||||
})
|
1
scripts/plugins/esbuild-monaco-global.ts
Normal file
1
scripts/plugins/esbuild-monaco-global.ts
Normal file
@ -0,0 +1 @@
|
||||
module.exports = globalThis.monaco
|
32
scripts/plugins/esbuild-string-import.ts
Normal file
32
scripts/plugins/esbuild-string-import.ts
Normal 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",
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
42
scripts/plugins/esbuild-yaml.ts
Normal file
42
scripts/plugins/esbuild-yaml.ts
Normal 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",
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
Reference in New Issue
Block a user