Public commit
This commit is contained in:
300
scripts/build-apps.ts
Executable file
300
scripts/build-apps.ts
Executable 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
29
scripts/build-css-module-def.ts
Executable 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
91
scripts/build-gql.ts
Executable 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
91
scripts/build-htmls.tsx
Executable 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
145
scripts/build-license.ts
Executable 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
42
scripts/build-locales.ts
Executable 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
116
scripts/build-makefile.ts
Executable 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
68
scripts/build-manifest.ts
Executable 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
321
scripts/build-models.ts
Executable 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
17
scripts/build-primer-ref.ts
Executable 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
86
scripts/build-sass.ts
Executable 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
103
scripts/build-schema-file.ts
Executable 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
59
scripts/build-shims.js
Executable 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
110
scripts/build-theme.tsx
Executable 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
29
scripts/build.sh
Executable 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
|
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",
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
89
scripts/utils.ts
Normal file
89
scripts/utils.ts
Normal 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")
|
||||
}
|
Reference in New Issue
Block a user