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

300
scripts/build-apps.ts Executable file
View File

@ -0,0 +1,300 @@
#!/usr/bin/env -S node -r esbin
import "dotenv/config"
import { promises as fs } from "fs"
import { resolve } from "path"
import glob from "fast-glob"
import { program } from "commander"
import sass from "esbuild-plugin-sass"
import * as esbuild from "esbuild"
import type { PluginItem } from "@babel/core"
import { c } from "./utils"
import { cssModules } from "./plugins/esbuild-css-modules"
import { babelPlugin as babel } from "./plugins/esbuild-babel"
import { externalDep } from "./plugins/esbuild-external-dep"
import { stringImport } from "./plugins/esbuild-string-import"
import { yamlPlugin as yaml } from "./plugins/esbuild-yaml"
import { alias } from "./plugins/esbuild-alias"
import { getDefaultTarget } from "./plugins/esbuild-browserslist"
const dist = "dist"
const SERVE_PORT = 3114
program
.option("-g, --grep <pattern>", "only build packages that match <pattern>")
.option("-s, --silent", "hide all output")
.option(
"-e, --env <env>",
"set the NODE_ENV environment variable",
process.env.NODE_ENV || "production",
)
.option("-w, --watch", "watch for changes and rebuild")
.parse()
const { grep, silent, env, watch } = program.opts()
const __PROD__ = env === "production"
const log = silent ? () => {} : console.log
async function main() {
const queue = new Queue([
new Build("src/options/index.tsx", `${dist}/app/options`, { serve: true }),
new Build("src/popup/index.tsx", `${dist}/app/popup`),
new Build("src/server/index.ts", `${dist}/app/server`, {
platform: "node",
format: "cjs",
splitting: false,
minify: false,
}),
new Build("src/background/index.ts", `${dist}/app/background`),
new Build("src/background/compiler.ts", `${dist}/app/background/compiler`),
new Build("src/context/index.ts", `${dist}/app/context`, {
format: "iife",
splitting: false,
}),
new Build("src/devtools/index.ts", `${dist}/app/devtools`),
new Build("src/install/index.ts", `${dist}/app/install`),
new Build("src/graphiql/index.tsx", `${dist}/app/playground`),
new Build("src/reference/index.tsx", `${dist}/app/reference`, {
format: "iife",
splitting: false,
}),
]).filter(grep ? task => task.entry.includes(grep) : () => true)
if (!queue.length) {
return
}
await queue.run(task => task.clean())
log(
watch ? "Watching" : "Building",
`${c.blue(queue.length)} package(s) in ${c.blue(env)}`,
)
if (queue.length < 5) {
log(queue.tasks.map((t, i) => ` ${i + 1}. ${c.green(t.entry)}`).join("\n"))
}
await queue.build()
if (__PROD__) {
await printBundleSize()
}
}
if (require.main === module) {
Promise.resolve()
.then(() => main())
.catch(e => {
console.error(e)
process.exit(1)
})
}
type BuildOptions = Pick<
esbuild.BuildOptions,
"entryPoints" | "outdir" | "format" | "splitting" | "platform" | "minify"
> & {
serve?: boolean
}
export const getBasicOptions = ({
entryPoints,
minify = __PROD__,
plugins = [],
babelPlugins = [],
}: {
entryPoints: [string]
minify?: boolean
plugins?: esbuild.Plugin[]
babelPlugins?: PluginItem[]
}): Partial<esbuild.BuildOptions> => ({
entryPoints,
define: {
...Object.entries(process.env).reduce(
(acc, [key, value]) =>
key.startsWith("NEXT_PUBLIC_")
? { ...acc, [`process.env.${key}`]: JSON.stringify(value) }
: acc,
{} as Record<string, string>,
),
"process.env.NODE_ENV": JSON.stringify(env),
"process.env.NODE_DEBUG": "false",
"process.env.SERVE_PORT": String(SERVE_PORT),
"process.browser": "true",
global: "globalThis",
},
bundle: true,
format: "esm",
external: ["path", "glob", "fs", "util", "monaco-editor"],
plugins: [
...plugins,
externalDep(["prettier", "sass"]),
babel(babelPlugins),
yaml(),
stringImport(),
alias({
lodash: require.resolve("lodash-es"),
"webextension-polyfill": require.resolve("../src/vendor/webextension-polyfill"),
"monaco-editor": require.resolve("../src/options/monaco.ts"),
// https://github.com/MichalLytek/type-graphql/issues/366#issuecomment-511075437
"libphonenumber-js": require.resolve("lodash.noop"),
// we never use graphql-subscriptions
"graphql-subscriptions": require.resolve("lodash.noop"),
// Make esbuild use the module version
graphql: require.resolve("graphql/index.mjs"),
...(entryPoints[0].includes("graphiql") || __PROD__
? {}
: {
react: require.resolve("../src/vendor/why-did-you-render/index.js"),
"react/jsx-runtime": require.resolve(
"../src/vendor/why-did-you-render/jsx-runtime",
),
}),
}),
cssModules({
generateScopedName: __PROD__
? "[hash:base64:6]"
: "[name]__[local]___[hash:base64:5]",
localsConvention: "camelCaseOnly",
}),
sass(),
].filter(Boolean),
target: getDefaultTarget(),
banner: {
js: "/* eslint-disable */",
},
legalComments: "none",
keepNames: false,
tsconfig: "./tsconfig.json",
sourcemap: "linked",
minify,
splitting: true,
metafile: true,
loader: {
".eot": "file",
".png": "file",
".ttf": "file",
".woff": "file",
".woff2": "file",
},
})
async function printBundleSize() {
const root = resolve(dist, "app")
const files = await glob(["**/*", "!**/*.map", "!**/meta.json"], {
cwd: root,
onlyFiles: true,
})
const sizes = await Promise.all(
files.map(async file => {
const { size } = await fs.stat(resolve(root, file))
return { file, size }
}),
)
const list = sizes
.filter(a => a.size > 50000)
.sort((a, b) => b.size - a.size)
.slice(0, 10)
for (const { file, size } of list) {
log(
c.blue(`./dist/app/${file.padEnd(38, " ")}`),
c.green(`${humanFileSize(size).padStart(9, " ")}`),
)
}
}
function humanFileSize(bytes: number, si = false, dp = 1) {
const thresh = si ? 1000 : 1024
if (Math.abs(bytes) < thresh) {
return bytes + " B"
}
const units = si
? ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
: ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]
let u = -1
const r = 10 ** dp
do {
bytes /= thresh
++u
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1)
return bytes.toFixed(dp) + " " + units[u]
}
class Build {
enabled = true
constructor(
readonly entry: string,
readonly outdir: string,
readonly options?: BuildOptions,
) {}
disable() {
this.enabled = false
return this
}
async clean() {
if (!this.enabled) return
await fs.rm(this.outdir, { recursive: true, force: true })
}
async build() {
if (!this.enabled) return
const { outdir, options: { serve, ...options } = {} } = this
const ctx = await esbuild.context({
...getBasicOptions({
entryPoints: [this.entry],
}),
...options,
outdir,
})
const { metafile } = await ctx.rebuild()
if (__PROD__) {
await fs.writeFile(resolve(outdir, "meta.json"), JSON.stringify(metafile))
}
if (watch) {
await ctx.watch()
if (serve) {
await ctx.serve({ port: SERVE_PORT })
}
} else {
await ctx.dispose()
}
}
}
class Queue {
constructor(public tasks: readonly Build[]) {}
get length() {
return this.tasks.length
}
filter(predicate: (task: Build) => boolean) {
return new Queue(this.tasks.filter(predicate))
}
run(fn: (task: Build) => Promise<void>) {
return Promise.all(this.tasks.map(fn))
}
build() {
return this.run(task => task.build())
}
}

29
scripts/build-css-module-def.ts Executable file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env -S node -r esbin
import fs from "fs"
import { uniq } from "lodash"
import { writeFormatted } from "./utils"
const modules = [
"src/vendor/allotment/src/allotment.module.css",
"src/vendor/allotment/src/sash.module.css",
]
for (const module of modules) {
const source = fs.readFileSync(module, "utf8")
const classNames = uniq(
Array.from(source.matchAll(/\.([a-z][a-z0-9_-]+)/gi), m => m[1]),
)
writeFormatted(
module + ".d.ts",
[
"// This file is generated by scripts/build-css-module-def.ts",
"",
"declare const classNames: {", //
...classNames.map(c => ` "${c}": string;`),
"}",
"",
"export default classNames;",
].join("\n"),
)
}

91
scripts/build-gql.ts Executable file
View File

@ -0,0 +1,91 @@
#!/usr/bin/env -S node -r esbin
import { promises as fs } from "node:fs"
import { format } from "prettier"
import { CodeFileLoader } from "@graphql-tools/code-file-loader"
import { GraphQLFileLoader } from "@graphql-tools/graphql-file-loader"
import * as typescript from "@graphql-codegen/typescript"
import * as typescriptOperations from "@graphql-codegen/typescript-operations"
import * as add from "@graphql-codegen/add"
import { plugin as _, codegen, typedDocumentNodePlugin } from "@aet/gql-tools/codegen"
import { buildSchema } from "./build-schema-file"
async function main() {
await buildSchema()
const scalars: Record<string, string> = {
DateTime: "Date",
}
const context = await codegen({
schema: {
source: "./src/generated/schema.gql",
loaders: [new GraphQLFileLoader()],
},
documents: {
source: ["./src/**/*.{ts,tsx}", "!/src/generated/**/*.{ts,tsx}"],
loaders: [new CodeFileLoader({ pluckConfig: { skipIndent: true } })],
},
})
const content = await context.generate({
plugins: [
_(typescript, {
immutableTypes: true,
useTypeImports: true,
declarationKind: "interface",
allowEnumStringTypes: true,
enumsAsTypes: true,
scalars,
}),
_(typescriptOperations, {
declarationKind: "interface",
allowEnumStringTypes: true,
scalars,
}),
_(add, {
content: /* js */ `
/* eslint-disable */
import { gql as _gql } from "@apollo/client";
import type { DocumentNode } from "graphql";
export { ApolloClient, useQuery, useApolloClient, useLazyQuery, useMutation } from "@apollo/client";
export const gql = _gql as unknown as {
<K extends keyof DocumentMap>(
literals: string | readonly string[],
...args: any[]
): DocumentMap[K];
(
literals: string | readonly string[],
...args: any[]
): DocumentNode;
}
`,
}),
_(typedDocumentNodePlugin, {
createDocumentMap: true,
exportDocumentNodes: true,
}),
],
pipeline: [
source =>
source
.replace(/Scalars\['String']\['(in|out)put']/g, "string")
.replace(/Scalars\['ID']\['(in|out)put']/g, "string")
.replace(/Scalars\['Boolean']\['(in|out)put']/g, "boolean")
.replace(/Scalars\['Int']\['(in|out)put']/g, "number")
.replace(/Scalars\['Float']\['(in|out)put']/g, "number")
.replaceAll("Scalars['DateTime']['input']", "Date")
.replaceAll("Scalars['JSON']['input']", "any")
.replace(/(\w+) \| `\$\{\1}`/g, " $1")
.replace(/: (Input)?Maybe<([['\]\w<> ]+)>/g, ": $2 | null")
.replace(/: Array<([\w<> ]+)>/g, ": $1[]")
.replace(/export type (\w+) = {/g, "export interface $1 {")
.replace(/export type (Make|Maybe|InputMaybe|Exact|Incremental)/g, "type $1"),
code => format(code, { parser: "typescript" }),
],
})
await fs.writeFile("./src/generated/graphql.ts", content)
}
void main()

91
scripts/build-htmls.tsx Executable file
View File

@ -0,0 +1,91 @@
#!/usr/bin/env -S node -r esbin
import fs from "fs"
import React from "react"
import { renderToStaticMarkup } from "react-dom/server"
const __DEV__ = process.env.NODE_ENV !== "production"
const Head: FC<{
children: React.ReactNode
}> = ({ children }) => (
<head>
<meta charSet="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{children}
</head>
)
const files: {
[path: string]: React.ReactElement
} = {
"./dist/background.html": (
<html>
<Head>
<script type="module" src="app/background/index.js" />
</Head>
</html>
),
"./dist/devtools.html": (
<html>
<Head>
<script type="module" src="app/devtools/index.js" />
</Head>
</html>
),
"./dist/options.html": (
<html data-theme="github-dark">
<Head>
<link rel="stylesheet" href="app/options/index.css" />
<link rel="stylesheet" href="/vendor/primer/github-dark.css" />
</Head>
<body>
{__DEV__ && (
// React DevTools
// curl -s http://localhost:8097 | openssl dgst -sha384 -binary | openssl base64 -A
<script src="http://localhost:8097" crossOrigin="anonymous" />
)}
<div id="not-loaded" />
<div id="root" />
<script type="module" src="app/options/index.js" />
</body>
</html>
),
"./dist/playground.html": (
<html>
<Head>
<link rel="stylesheet" href="app/playground/index.css" />
</Head>
<body>
<div id="root" />
<script type="module" src="app/playground/index.js" />
</body>
</html>
),
"./dist/popup.html": (
<html>
<Head>
<link rel="stylesheet" href="app/popup/index.css" />
</Head>
<body>
<div id="root" />
<script type="module" src="app/popup/index.js" />
</body>
</html>
),
"./dist/reference.html": (
<html>
<Head>
<link rel="stylesheet" href="app/reference/index.css" />
</Head>
<body>
<div id="root" />
<script src="app/reference/index.js" />
</body>
</html>
),
}
for (const [path, content] of Object.entries(files)) {
const html = "<!DOCTYPE html>" + renderToStaticMarkup(content)
fs.writeFileSync(path, html)
}

145
scripts/build-license.ts Executable file
View File

@ -0,0 +1,145 @@
#!/usr/bin/env -S node -r esbin
import fs from "node:fs"
import { resolve } from "node:path"
import { load } from "js-yaml"
process.chdir(resolve(__dirname, ".."))
const candidates = [
"master/LICENSE",
"master/LICENCE",
"master/LICENSE.md",
"master/license.md",
"main/LICENSE.txt",
]
async function getLicense(repo: string) {
for (const candidate of candidates) {
const path = `https://raw.githubusercontent.com/${repo}/${candidate}`
const res = await fetch(path)
if (!res.ok) continue
const text = await res.text()
return text
}
throw new Error(`Could not find license for ${repo}`)
}
async function getSDPXLicense(license: string) {
const path = `https://raw.githubusercontent.com/spdx/license-list-data/main/text/${license}.txt`
const res = await fetch(path)
if (!res.ok) throw new Error(`Could not find license ${license}`)
const text = await res.text()
return text
}
function getVendorLicenses() {
return fs
.readdirSync("src/vendor")
.map(name => ({ name, path: `src/vendor/${name}/LICENSE` }))
.filter(({ path }) => fs.existsSync(path))
.map(({ name, path }) => [name, fs.readFileSync(path, "utf8")])
}
async function getPackageLicense(pkg: string) {
const files = ["LICENSE", "LICENCE", "license.md"]
for (const file of files) {
const path = `node_modules/${pkg}/${file}`
if (fs.existsSync(path)) {
return fs.readFileSync(path, "utf8")
}
}
const pkgJson = JSON.parse(fs.readFileSync(`node_modules/${pkg}/package.json`, "utf8"))
const licenseName = pkgJson.license as string
if (!licenseName) throw new Error(`Could not find license for ${pkg}`)
return getSDPXLicense(licenseName)
}
function getPackageLicenses() {
const {
dependencies = {},
devDependencies = {},
peerDependencies = {},
} = JSON.parse(fs.readFileSync("package.json", "utf8"))
const packages = Object.keys({
...dependencies,
...devDependencies,
...peerDependencies,
})
return Promise.all(packages.map(async pkg => [pkg, await getPackageLicense(pkg)]))
}
class Licenses {
libs = new Set<string>()
constructor(readonly json: Record<string, string>) {}
set(name: string, license: string) {
this.libs.add(name)
this.json[name] = license
}
async setIfAbsent(name: string, license: () => Promise<string>) {
if (!this.json[name]) {
const text = await license()
this.set(name, text)
}
}
assign(others: Record<string, string>) {
for (const [name, license] of Object.entries(others)) {
this.set(name, license)
}
}
purge() {
for (const name of Object.keys(this.json)) {
if (!this.libs.has(name)) {
delete this.json[name]
}
}
}
toJSON() {
return this.json
}
}
async function main() {
const outPath = "src/generated/licenses.json"
if (!fs.existsSync(outPath)) {
fs.writeFileSync(outPath, "{}")
}
const res = new Licenses(
JSON.parse(fs.readFileSync(outPath, "utf8")) as Record<string, string>,
)
const config = load(fs.readFileSync("src/licenses.yml", "utf8")) as {
github: string[]
licenses: { [name: string]: string }
}
// res.set("archive", await getSPLicense("AGPL-3.0-or-later"));
for (const repo of config.github) {
await res.setIfAbsent(repo, () => getLicense(repo))
}
for (const [name, license] of Object.entries(config.licenses)) {
await res.setIfAbsent(name, () => getSDPXLicense(license))
}
res.assign(Object.fromEntries(getVendorLicenses()))
res.assign(Object.fromEntries(await getPackageLicenses()))
res.purge()
fs.writeFileSync(
outPath,
JSON.stringify(
Object.fromEntries(
Object.entries(res.toJSON()).sort(([a], [b]) => a.localeCompare(b)),
),
null,
2,
) + "\n",
)
}
void main()

42
scripts/build-locales.ts Executable file
View File

@ -0,0 +1,42 @@
#!/usr/bin/env -S node -r esbin
import { execSync } from "child_process"
import { resolve } from "path"
import en from "../src/shared/i18n/resources/en-US/index.json"
import { writeFormatted } from "./utils"
const languages = process.argv.slice(2).includes("-f")
? execSync("crowdin list languages --code=locale --plain")
.toString()
.trim()
.split("\n")
.concat("en-US")
.sort()
: ["de-DE", "en-US", "es-ES", "fr-CA", "fr-FR", "ja-JP", "ko-KR", "zh-CN", "zh-TW"]
const keys: string[] = Array.from(
(function* keys(
obj: Record<string, unknown>,
prefixes: string[] = [],
): Generator<string, void, undefined> {
for (const [key, value] of Object.entries(obj)) {
if (value != null && typeof value === "object") {
yield* keys(value as any, prefixes.concat(key))
} else {
yield prefixes.concat(key).join(".")
}
}
})(en),
).sort()
const code = [
`export const languages = ${JSON.stringify(languages, null, 2)};`,
"",
"export type TranslationKey =",
keys.map(key => " | " + JSON.stringify(key)).join("\n"),
";\n",
]
writeFormatted(
resolve(__dirname, "../src/shared/i18n/data.generated.ts"),
code.join("\n"),
)

116
scripts/build-makefile.ts Executable file
View File

@ -0,0 +1,116 @@
#!/usr/bin/env -S node -r esbin
import { promises as fs } from "fs"
import { resolve } from "path"
import { kebabCase } from "lodash"
import { sync as glob } from "fast-glob"
import { ensureArray as arr } from "~/shared/ensureArray"
// import _ from "dedent";
interface Task {
deps: string | string[]
target: string | string[]
command: string | string[]
}
// Variables for Makefile
// $@: The file name of the target of the rule.
// $<: The name of the first prerequisite.
// $?: The names of all the prerequisites that are newer than the target, with spaces between them.
// $^: The names of all the prerequisites, with spaces between them.
const tasks: { [name: string]: () => Task } = {
Makefile: (script = "scripts/build-makefile.ts") => ({
command: script,
target: "Makefile",
deps: [script],
}),
theme: (script = "scripts/build-theme.tsx") => ({
command: `${script} -o $(@D)`,
target: [
"src/shared/theme/tokens.generated.ts",
"src/shared/theme/tokens.generated.css",
],
deps: ["src/shared/theme/tokens.tsx", script],
}),
models: (
models = "src/shared/models",
source = `${models}/models.yml`,
script = "./scripts/build-models.ts",
) => ({
command: `${script}`,
target: glob(`${models}/*.generated.ts`),
deps: [source, script],
}),
WebExtensionPolyfill: (base = "src/vendor/webextension-polyfill") => ({
target: `${base}/api-metadata.generated.json`,
deps: glob(`${base}/**/*`),
command: `./${base}/scripts/compress.ts`,
}),
HTML: (script = "scripts/build-htmls.tsx") => ({
target: glob("dist/*.html"),
deps: script,
command: script,
}),
locales: (script = "scripts/build-locales.ts") => ({
target: [
...glob(["dist/_locales/**/*.json", "src/shared/i18n/resources"]),
"src/shared/i18n/data.generated.ts",
],
deps: script,
command: script,
}),
// monaco: (
// script = "./scripts/build-monaco.ts",
// dist = "dist/vendor/monaco-editor/index.js",
// ) => ({
// target: dist,
// deps: ["package.json", script],
// command: [script, `touch ${dist}`],
// }),
sass: (script = "./scripts/build-sass.ts") => ({
target: "dist/vendor/sass/index.js",
deps: ["package.json", script],
command: script,
}),
}
async function main() {
const sep = " \\\n "
function* generate() {
yield "# This file is generated by `make Makefile`"
const instances = Object.entries(tasks).map(([name, t]) => [name, t()] as const)
for (const [name, task] of instances) {
const { target } = task
yield arr(target).join(" ") +
": " +
arr(task.deps)
.filter(d => !glob(d).some(file => target.includes(file)))
.join(sep)
yield `\t@echo "✨ Building ${name}..."`
for (const cmd of arr(task.command)) {
yield `\t@${cmd}`
}
yield ""
yield `${kebabCase(name)}: ${arr(target)[0]}`
yield ""
}
yield "all: " + instances.map(task => kebabCase(task[0])).join(sep)
yield ""
}
const makefile = [...generate()].join("\n")
await fs.writeFile(resolve(__dirname, "../Makefile"), makefile)
}
main()

68
scripts/build-manifest.ts Executable file
View File

@ -0,0 +1,68 @@
#!/usr/bin/env -S node -r esbin
import "dotenv/config"
import fs from "fs"
import { resolve } from "path"
import type { Manifest } from "webextension-polyfill"
const manifest: Manifest.WebExtensionManifest = {
name: "__MSG_extName__",
version: "19.11.6",
description: "__MSG_description__",
manifest_version: 2,
default_locale: "en_US",
icons: {
64: "assets/images/64.png",
128: "assets/images/128.png",
256: "assets/images/256.png",
},
permissions: [
"tabs",
"downloads",
"webNavigation",
"storage",
"http://*/",
"https://*/",
"chrome://favicon/",
],
content_security_policy:
"script-src 'unsafe-inline' 'self' http://localhost:8097/; object-src 'self'",
minimum_chrome_version: "90.0",
background: {
page: "background.html",
persistent: true,
},
devtools_page: "devtools.html",
content_scripts: [
{
matches: ["<all_urls>"],
run_at: "document_start",
all_frames: true,
js: ["app/context/index.js"],
},
{
matches: ["http://userstyles.org/*", "https://userstyles.org/*"],
run_at: "document_end",
all_frames: false,
js: ["app/install/index.js"],
},
],
options_ui: {
page: "options.html",
open_in_tab: true,
},
browser_action: {
default_icon: {
16: "assets/images/16w.png",
19: "assets/images/19w.png",
32: "assets/images/32w.png",
38: "assets/images/38w.png",
},
default_title: "__MSG_extName__",
default_popup: "popup.html",
},
}
fs.writeFileSync(
resolve(__dirname, "../dist", "manifest.json"),
JSON.stringify(manifest, null, 2),
)

321
scripts/build-models.ts Executable file
View File

@ -0,0 +1,321 @@
#!/usr/bin/env -S node -r esbin
import "dotenv/config"
import { promises as fs } from "fs"
import { resolve } from "path"
import { assign, camelCase } from "lodash"
import yaml from "js-yaml"
import { writeFormatted } from "./utils"
// type Outputs = "partial input" | "input" | "object" | "inputAndObject" | "args" | "zod";
type Output =
| ["input", { partial?: boolean; name?: string }] //
| ["object"]
| ["args"]
| ["zod"]
interface Model {
as: Output[]
extends?: string
fields: string
extra?: Record<string, string>
}
const FIELD_REGEX =
/^(?<name>\w+)(?<optional>\?)?(:\s*(?<type>[\w\s[\]|]+))?(\s*=\s*(?<defaultValue>.+))?$/
function parseField(field: string) {
const groups = field.match(FIELD_REGEX)?.groups
if (!groups) {
throw new Error(`Invalid field: ${field}`)
}
if (!groups.type && groups.defaultValue) {
const v = groups.defaultValue
if (v === "true" || v === "false") {
groups.type = "boolean"
}
}
return groups as {
name: string
optional?: "?"
type: string
defaultValue: string
}
}
function parseFields(value: any) {
if (typeof value === "object") {
value = Object.entries(value)
.map(([name, type]) => `${name}: ${type}`)
.join("\n")
}
return (value as string).trim().split("\n").map(parseField)
}
const getInputName = (name: string) => (name.startsWith("Input") ? name : `Input${name}`)
type Generate = Generator<string | false | undefined, void, unknown>
async function main() {
const folder = resolve(__dirname, "../src/shared/models")
const source = await fs.readFile(resolve(folder, "models.yml"), "utf8")
const json = yaml.load(source) as {
enums: Record<string, string[]>
imports?: string
models: Record<string, Model>
}
const models = Object.entries(json.models).map(
([name, { as, extends: _, fields, extra }]) => ({
name,
as: as.map(t =>
Array.isArray(t) ? [t[0], (assign as any)(...t.slice(1))] : [t, {}],
) as Output[],
extends: _,
fields: parseFields(fields),
extra,
}),
)
for (const model of models) {
if (model.extends) {
const parent = models.find(m => m.name === model.extends)
if (!parent) {
throw new Error(`Could not find parent ${model.extends}`)
}
model.fields = parent.fields.concat(model.fields)
}
}
const importedEnums = new Set<string>()
const enumNames = Object.keys(json.enums)
function getReifiedType(type: string): string {
if (type.endsWith("| null")) {
type = type.slice(0, -6).trim()
}
if (type.endsWith("[]")) {
return `[${getReifiedType(type.slice(0, -2))}]`
}
switch (type) {
case "string":
case "String":
return "String"
case "int":
case "Int":
return "Int"
case "boolean":
case "Boolean":
return "Boolean"
default:
if (enumNames.includes(type)) {
importedEnums.add(type)
}
if (/^\d+$/.test(type)) {
return "Int"
}
return type
}
}
function getVirtualType(type: string): string {
if (type.endsWith("[]")) {
return `${getVirtualType(type.slice(0, -2))}[]`
} else if (type.endsWith("| null")) {
return `${getVirtualType(type.slice(0, -6).trim())} | null`
}
switch (type) {
case "int":
case "Int":
return "number"
case "ID":
case "String":
return "string"
case "Boolean":
return "boolean"
default:
if (enumNames.includes(type)) {
importedEnums.add(type)
}
return type
}
}
function getZodType(type: string): string {
if (type.endsWith("[]")) {
return `z.array(${getZodType(type.slice(0, -2))})`
} else if (type.endsWith("| null")) {
return `${getZodType(type.slice(0, -6).trim())}.optional()`
}
switch (type) {
case "int":
case "Int":
return "z.number()"
case "string":
return "z.string()"
case "boolean":
return "z.boolean()"
default:
if (/^[\d.]+$/.test(type)) {
return `z.literal(${type})`
}
if (enumNames.includes(type)) {
return `z.nativeEnum(${type})`
}
return `z.instanceof(${type})`
}
}
const banners: string[] = [
`import { ArgsType, Field, ID, InputType, Int, ObjectType } from "@aet/gql-tools/macro";`,
// `import { z } from "zod";`,
]
/* eslint-disable @typescript-eslint/consistent-type-imports, @typescript-eslint/no-unused-vars, @typescript-eslint/no-inferrable-types */
const getModels = function* (): Generate {
for (const model of models) {
let attachInputToObject = false
for (const type of model.as) {
let { name } = model
let isInput = false
let isPartialInput = false
switch (type[0]) {
case "input":
isInput = true
isPartialInput = type[1].partial ?? false
if (
Object.keys(type[1]).length === 0 &&
model.as.some(t => t[0] === "object")
) {
attachInputToObject = true
continue
} else {
name = type[1].name ?? getInputName(name)
yield `@InputType("${name}")\n`
}
break
case "object":
if (attachInputToObject) {
yield `/** Input and object type for ${name} */\n`
yield `@InputType("${getInputName(name)}")\n`
}
yield `@ObjectType("${name}")\n`
break
case "args":
yield `@ArgsType()\n`
break
case "zod":
continue
}
yield `export class ${name} {`
for (const field of model.fields) {
const optional = field.optional || isPartialInput
yield "@Field("
yield `() => ${getReifiedType(field.type)},`
if (optional) {
yield "{ nullable: true },"
}
yield ")\n"
if (!field.defaultValue) {
yield "declare "
}
yield `${field.name}`
if (optional) {
yield "?"
}
yield ":"
yield getVirtualType(field.type)
if (isInput && field.defaultValue) {
yield ` = ${field.defaultValue}`
}
yield ";\n\n"
}
let constructorType = name
if (model.extra) {
const omits: string[] = []
for (const [name, type] of Object.entries(model.extra)) {
if (type.includes("=")) {
omits.push(name.replace(/\?$/, ""))
}
yield `${name}: ${type}\n`
}
constructorType = `Omit<${name}, ${omits.map(o => `"${o}"`).join(" | ")}>`
}
yield /* js */ `
/**
* Create an instance of ${name} from a plain object.
*/
constructor(fields: ${constructorType}) {
Object.assign(this, fields);
}
`
// if (1 === 3) {
// yield "\n";
// yield `/** Zod schema for ${name} */\n`;
// yield "static zod = z.object({\n";
// for (const field of model.fields) {
// yield `${field.name}: ${getZodType(field.type)},\n`;
// }
// yield `}).strict();`;
// }
yield "}\n\n"
}
}
}
function* getEnums() {
for (const [name, values] of Object.entries(json.enums)) {
yield `export const ${name} = {`
for (const value of values) {
yield `${value}: "${value}",\n`
}
yield "} as const;\n"
yield
yield `export type ${name} = typeof ${name}[keyof typeof ${name}];\n`
yield
yield `export const ${camelCase(name)}s = [${values
.map(v => `"${v}"`)
.join(", ")}] as ${name}[];\n`
yield
}
}
await writeTS(resolve(folder, "models.generated.ts"), getModels, () =>
banners.concat(
importedEnums.size
? `import { ${Array.from(importedEnums)
.sort()
.join(", ")} } from "./enums.generated";`
: [],
),
)
await writeTS(resolve(folder, "enums.generated.ts"), getEnums, () => [])
}
async function writeTS(path: string, generator: () => Generate, banner: () => string[]) {
const code = Array.from(generator(), x => x ?? "\n").join("")
await writeFormatted(path, banner().join("\n") + "\n\n" + code)
}
if (require.main === module) {
Promise.resolve()
.then(() => main())
.catch(e => {
console.error(e)
process.exit(1)
})
}

17
scripts/build-primer-ref.ts Executable file
View File

@ -0,0 +1,17 @@
#!/usr/bin/env -S node -r esbin
import fs from "fs"
import { resolve } from "path"
const path = resolve(__dirname, "../dist/reference.html")
const html = fs
.readFileSync(path, "utf8")
.replace(/<script src="([\w/.]+)"><\/script>/g, (_, $1) => {
const script = fs.readFileSync(resolve(__dirname, "../dist", $1), "utf8")
return `<script>${script}</script>`
})
.replace(/<link rel="stylesheet" href="([\w/.]+)"\/>/, (_, $1) => {
const css = fs.readFileSync(resolve(__dirname, "../dist", $1), "utf8")
return `<style>${css}</style>`
})
fs.writeFileSync(path.replace(".html", "-bundled.html"), html)

86
scripts/build-sass.ts Executable file
View File

@ -0,0 +1,86 @@
#!/usr/bin/env -S node -r esbin
import fs from "fs"
import { resolve } from "path"
import * as esbuild from "esbuild"
import { file } from "./utils"
import { getDefaultTarget } from "./plugins/esbuild-browserslist"
const outdir = resolve(__dirname, "../dist/vendor/sass")
fs.rmSync(outdir, { recursive: true, force: true })
const prefix = `
let window = globalThis;
let process = {
env: {},
stdout: {
write: console.log
},
stderr: {
write: console.error,
},
};
`
const sass = fs.readFileSync(
resolve(__dirname, "../node_modules", "sass/sass.dart.js"),
"utf-8",
)
const replaced: string = sass
.replaceAll(/typeof Buffer/g, '"undefined"')
.replaceAll("process.stdout.isTTY", "undefined")
.replaceAll("self.location", "window.location")
.replace(/require\("(chokidar|readline|fs|util)"\)/g, "{}")
.replace(
/(accept|accept\$1|add|call|toString|parse|write|scope\$1|parse[A-Z]\w+)\$(\d): function\(/g,
"$1$$$2(",
)
.replace(/\w+\.util\.inspect\.custom/g, "Symbol()")
.replace(/(\$eq|toString|get\$\w+|join\$0): function\(/g, "$1(")
.replace("if (dartNodeIsActuallyNode) {", "if (false) {")
// CSP
.replaceAll(
/new\s+self\.Function\(\s*("[^"\\]*(?:\\.[^"\\]*)*")\s*,\s*("[^"\\]*(?:\\.[^"\\]*)*")\s*\)/g,
(_, $1, $2) => `((${JSON.parse($1)}) => { ${JSON.parse($2)} })`,
)
.replaceAll(
/new\s+self\.Function\(\s*("[^"\\]*(?:\\.[^"\\]*)*")\s*,\s*('[^'\\]*(?:\\.[^'\\]*)*')\s*\)/g,
(_, $1, $2) =>
`((${JSON.parse($1)}) => { ${JSON.parse(
'"' + $2.slice(1, -1).replaceAll('"', '\\"') + '"',
)} })`,
)
.trim()
const temporary = file.js(prefix + replaced)
const entry = file.js(/* js */ `
import "${temporary.path}";
const sass = globalThis._cliPkgExports.pop();
sass.load({});
export default sass;
`)
try {
esbuild.buildSync({
entryPoints: [entry.path],
outfile: resolve(outdir, "index.js"),
bundle: true,
minify: true,
format: "esm",
sourcemap: "external",
target: getDefaultTarget(),
external: ["fs", "chokidar", "readline"],
banner: { js: "/* eslint-disable */" },
legalComments: "linked",
define: {
"process.env.NODE_ENV": '"production"',
"process.env.NODE_DEBUG": "undefined",
},
})
} finally {
temporary.rm()
entry.rm()
}

103
scripts/build-schema-file.ts Executable file
View File

@ -0,0 +1,103 @@
#!/usr/bin/env -S node -r esbin
import fs from "fs"
import { build } from "esbuild"
import { printSchema } from "graphql"
import type { Visitor } from "@babel/core"
import { getBasicOptions } from "./build-apps"
import { file } from "./utils"
import { dependencies, devDependencies } from "../package.json"
import type * as schema from "../src/background/graphql/schema"
const output = file.js()
const DEBUG = !!process.env.DEBUG
const babelPlugin: Visitor<{ filename: string }> = {
ClassMethod({ node }) {
node.params = []
node.async = false
node.body.body = []
},
ClassPrivateMethod(path) {
path.remove()
},
ClassProperty({ node }) {
node.value = null
},
FunctionDeclaration({ node }, { filename }) {
if (!filename.includes("graphql")) {
node.body.body = []
}
},
}
export async function buildSchema() {
await build({
...getBasicOptions({
entryPoints: ["src/background/graphql/schema.ts"],
babelPlugins: [() => ({ visitor: babelPlugin })],
plugins: [
{
name: "no-lib",
setup(build) {
const noop = require.resolve("lodash/noop")
const whitelist = new Set([
"graphql-type-json",
"reflect-metadata",
"tslib",
"lodash",
"@aet/gql-tools/marco",
"@aet/gql-tools/type-graphql",
])
const noopRes = { path: noop, external: false }
build.onResolve({ filter: /.*/ }, ({ path }) => {
if (
path.includes("couchdb") ||
path.includes("idb") ||
path.includes("compiler") ||
path.includes("webextension")
) {
return noopRes
}
if (/^(~|\.\.?)\//.test(path)) {
return null
}
if (whitelist.has(path)) {
return { path: require.resolve(path), external: true }
}
return noopRes
})
},
},
],
}),
outfile: output.path,
target: "node20",
format: "cjs",
minify: !DEBUG,
keepNames: true,
legalComments: "none",
sourcemap: false,
splitting: false,
external: Object.keys(dependencies || {})
.concat(Object.keys(devDependencies))
.concat("@aet/gql-tools/type-graphql"),
})
.then(() => {
const { getSchema } = require("../" + output.path) as typeof schema
const result = printSchema(getSchema())
fs.writeFileSync("./src/generated/schema.gql", result)
return result
})
.finally(() => {
if (!DEBUG) {
output.rm()
}
})
}
if (require.main === module) {
void buildSchema()
}

59
scripts/build-shims.js Executable file
View File

@ -0,0 +1,59 @@
#!/usr/bin/env node
/* eslint-disable spaced-comment */
// @ts-check
const fs = require("fs")
const { resolve } = require("path")
const pkg = require("../package.json")
/**
* @param {TemplateStringsArray} args
*/
function _([source]) {
return {
"index.js": `module.exports = ${source};\n`,
"index.mjs": `export default ${source};\n`,
"package.json": JSON.stringify({ module: "index.mjs", version: "1.0.0" }),
}
}
const { entries, fromEntries } = Object
/**
* @param {Record<string, Record<string, string>>} list
*/
function createShims(list) {
/** @type {Record<string, string>} */
const resolutions = fromEntries(
entries(pkg.resolutions).filter(([, path]) => !path.startsWith("./shims")),
)
for (const [name, content] of entries(list)) {
const dir = resolve(__dirname, "../shims", name)
fs.mkdirSync(dir, { recursive: true })
content["package.json"] ??= "{}"
for (let [fileName, text] of entries(content)) {
if (fileName === "package.json") {
text = JSON.stringify({ name, ...JSON.parse(text) })
}
fs.writeFileSync(resolve(dir, fileName), text)
}
resolutions[name] = `./shims/${name}`
}
pkg.resolutions =
/** @type {any} */
(fromEntries(entries(resolutions).sort(([a], [b]) => a.localeCompare(b))))
fs.writeFileSync(resolve(__dirname, "../package.json"), JSON.stringify(pkg, null, 2))
}
createShims({
"cross-fetch": {
"index.js": /*js*/ `module.exports = fetch; const headers = Headers; module.exports.Headers = headers;`,
"index.mjs": /*js*/ `export default fetch; const headers = Headers; export { headers as Headers };`,
"package.json": JSON.stringify({ module: "index.mjs", version: "1.0.0" }),
},
"is-number": _/*js*/ `value => typeof value === "number"`,
})

110
scripts/build-theme.tsx Executable file
View File

@ -0,0 +1,110 @@
#!/usr/bin/env -S node -r esbin
import { promises as fs } from "fs"
import { resolve } from "path"
import sass from "sass"
import { kebabCase } from "lodash"
import { tokenSource } from "~/shared/theme/tokens"
const trim = (s: string) => s.trim()
const [, outdir] = process.argv.slice(2)
// This file is used to `make theme`.
namespace primer {
// https://github.com/primer/github-vscode-theme/commit/5f08d0cc4de8abc33e13c3f63fe92288824a11cf
// prettier-ignore
const classic = {
black: "#1b1f23",
white: "#fff",
gray: ["#fafbfc", "#f6f8fa", "#e1e4e8", "#d1d5da", "#959da5", "#6a737d", "#586069", "#444d56", "#2f363d", "#24292e"],
blue: ["#f1f8ff", "#dbedff", "#c8e1ff", "#79b8ff", "#2188ff", "#0366d6", "#005cc5", "#044289", "#032f62", "#05264c"],
green: ["#f0fff4", "#dcffe4", "#bef5cb", "#85e89d", "#34d058", "#28a745", "#22863a", "#176f2c", "#165c26", "#144620"],
yellow: ["#fffdef", "#fffbdd", "#fff5b1", "#ffea7f", "#ffdf5d", "#ffd33d", "#f9c513", "#dbab09", "#b08800", "#735c0f"],
orange: ["#fff8f2", "#ffebda", "#ffd1ac", "#ffab70", "#fb8532", "#f66a0a", "#e36209", "#d15704", "#c24e00", "#a04100"],
red: ["#ffeef0", "#ffdce0", "#fdaeb7", "#f97583", "#ea4a5a", "#d73a49", "#cb2431", "#b31d28", "#9e1c23", "#86181d"],
purple: ["#f5f0ff", "#e6dcfd", "#d1bcf9", "#b392f0", "#8a63d2", "#6f42c1", "#5a32a3", "#4c2889", "#3a1d6e", "#29134e"],
pink: ["#ffeef8", "#fedbf0", "#f9b3dd", "#f692ce", "#ec6cb9", "#ea4aaa", "#d03592", "#b93a86", "#99306f", "#6d224f"]
};
function addClassic(css: string, dark: boolean) {
const r = (name: string, value?: string) =>
(css = css.replace(
RegExp(/(--color-NAME: )([^;]+);/.source.replace("NAME", name)),
(_, name, originalValue) => `${name}${value ?? originalValue};`,
))
r("canvas-default", dark ? "#1f2428" : undefined)
return css
}
function sassCompile(code: string) {
return sass.compileString(code, {
loadPaths: [resolve(__dirname, "../node_modules")],
style: "compressed",
}).css
}
export async function main() {
const outputDir = resolve(__dirname, "../dist/vendor/primer")
await fs.mkdir(outputDir, { recursive: true })
const themes = {
dark: "dark-default",
dark_colorblind: undefined,
dark_dimmed: undefined,
dark_high_contrast: undefined,
dark_tritanopia: undefined,
light: "light-default",
light_colorblind: undefined,
light_high_contrast: undefined,
light_tritanopia: undefined,
}
const toRoot = (name: string, themeName = name) =>
sassCompile(/* scss */ `
@use "@primer/primitives/dist/scss/colors/_${name}.scss" as *;
:root[data-theme="github-${themeName}"] {
@include primer-colors-${name};
}
`)
await Promise.all([
...Object.entries(themes).map(([source, name = source]) =>
fs.writeFile(resolve(outputDir, `github-${name}.css`), toRoot(source, name)),
),
fs.writeFile(
resolve(outputDir, "github-dark.css"),
addClassic(toRoot("dark"), true),
),
fs.writeFile(
resolve(outputDir, "github-light.css"),
addClassic(toRoot("light"), false),
),
])
}
}
async function tokens() {
const declarations = Object.entries(tokenSource).map(([name, value]) => {
const [light, dark] = value.trim().split(",").map(trim)
return { name, light, dark }
})
await fs.writeFile(
resolve(outdir, "tokens.generated.css"),
[
"body{",
...declarations.map(({ name, light }) => `--${kebabCase(name)}:${light};`),
"}",
"body.dark{",
...declarations.map(({ name, dark }) => `--${kebabCase(name)}:${dark};`),
"}",
].join(""),
)
}
async function main() {
await tokens()
await primer.main()
}
main()

29
scripts/build.sh Executable file
View File

@ -0,0 +1,29 @@
#!/bin/sh
set -e
if [ "$1" = "monaco" ]; then
rm -rf "dist/vendor/vs"
path=$(dirname "$(node -p "require.resolve('monaco-editor/package.json')")")/min/vs
cp -r "$path" "dist/vendor/vs"
exit 0
fi
if [ -z "$1" ]; then
export NODE_ENV=production
make all
echo "✨ Downloading locales..."
# crowdin download
echo "✨ Building gql..."
./scripts/build-gql.ts
echo "✨ Building apps..."
./scripts/build-apps.ts -s
echo "✨ Building license..."
./scripts/build-license.ts
else
path=$(find ./scripts -type f -name "build-$1.*")
if [ -z "$path" ]; then
echo "Unknown task '$1'."
exit 1
fi
$path "$@"
fi

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",
}
})
},
})

89
scripts/utils.ts Normal file
View File

@ -0,0 +1,89 @@
import fs from "fs"
import { extname } from "path"
import { format } from "prettier"
export function file(extension: string, content = "") {
const path = `./.${(Date.now() - 1677e9).toString(36)}${extension}`
fs.writeFileSync(path, content)
process.on("SIGINT", () => fs.unlinkSync(path))
return {
path,
rm: () => fs.unlinkSync(path),
}
}
file.js = (content?: string) => file(".js", content)
export function map<T, R>(list: T[], fn: (item: T, index: number) => Promise<R>) {
return Promise.all(list.map(fn))
}
/**
* Write a file with Prettier formatting.
*/
export async function writeFormatted(path: string, content: string) {
const ext = extname(path)
const formatted = await format(content, {
parser: ext === ".ts" ? "babel-ts" : ext === ".js" ? "babel" : undefined,
})
return fs.promises.writeFile(path, formatted)
}
// https://github.com/nathanbabcock/picocolors-browser/tree/d98747d1e7dd58390044ff0696b6d10995a94de3
// ISC License. Copyright (c) 2021 Alexey Raspopov, Kostiantyn Denysov, Anton Verinov
export namespace c {
const enabled = !("NO_COLOR" in process.env)
const formatter = (open: string, close: string, replace = open) =>
enabled
? (input: number | string) => {
const string = "" + input
const index = string.indexOf(close, open.length)
return index !== -1
? open + replaceClose(string, close, replace, index) + close
: open + string + close
}
: String
function replaceClose(
string: string,
close: string,
replace: string,
index: number,
): string {
const start = string.substring(0, index) + replace
const end = string.substring(index + close.length)
const nextIndex = end.indexOf(close)
return nextIndex !== -1
? start + replaceClose(end, close, replace, nextIndex)
: start + end
}
export const reset = enabled ? (s: string) => `\x1b[0m${s}\x1b[0m` : String
export const bold = formatter("\x1b[1m", "\x1b[22m", "\x1b[22m\x1b[1m")
export const dim = formatter("\x1b[2m", "\x1b[22m", "\x1b[22m\x1b[2m")
export const italic = formatter("\x1b[3m", "\x1b[23m")
export const underline = formatter("\x1b[4m", "\x1b[24m")
export const inverse = formatter("\x1b[7m", "\x1b[27m")
export const hidden = formatter("\x1b[8m", "\x1b[28m")
export const strikethrough = formatter("\x1b[9m", "\x1b[29m")
export const black = formatter("\x1b[30m", "\x1b[39m")
export const red = formatter("\x1b[31m", "\x1b[39m")
export const green = formatter("\x1b[32m", "\x1b[39m")
export const yellow = formatter("\x1b[33m", "\x1b[39m")
export const blue = formatter("\x1b[34m", "\x1b[39m")
export const magenta = formatter("\x1b[35m", "\x1b[39m")
export const cyan = formatter("\x1b[36m", "\x1b[39m")
export const white = formatter("\x1b[37m", "\x1b[39m")
export const gray = formatter("\x1b[90m", "\x1b[39m")
export const bgBlack = formatter("\x1b[40m", "\x1b[49m")
export const bgRed = formatter("\x1b[41m", "\x1b[49m")
export const bgGreen = formatter("\x1b[42m", "\x1b[49m")
export const bgYellow = formatter("\x1b[43m", "\x1b[49m")
export const bgBlue = formatter("\x1b[44m", "\x1b[49m")
export const bgMagenta = formatter("\x1b[45m", "\x1b[49m")
export const bgCyan = formatter("\x1b[46m", "\x1b[49m")
export const bgWhite = formatter("\x1b[47m", "\x1b[49m")
}