Format dev-tools and webpack folder

This commit is contained in:
the1812 2022-10-12 23:25:10 +08:00
parent 92314cef6f
commit e6e2ccd48a
23 changed files with 275 additions and 302 deletions

View File

@ -3,5 +3,4 @@ typings/
dist/
dev/
node_modules/
*.config.*
.eslintrc.*

View File

@ -4,11 +4,8 @@ interface DevServerConfig {
port?: number
maxWatchers?: number
}
const configFile = (path: string) => () => (
existsSync(path)
? JSON.parse(readFileSync(path, { encoding: 'utf-8' }))
: {}
)
const configFile = (path: string) => () =>
existsSync(path) ? JSON.parse(readFileSync(path, { encoding: 'utf-8' })) : {}
const configSource: (() => DevServerConfig)[] = [
() => ({
port: 23333,
@ -16,6 +13,7 @@ const configSource: (() => DevServerConfig)[] = [
}),
configFile('dev/dev-server.json'),
]
export const devServerConfig = configSource.reduce((previous, current) => (
{ ...previous, ...current() }
), {} as DevServerConfig)
export const devServerConfig = configSource.reduce(
(previous, current) => ({ ...previous, ...current() }),
{} as DevServerConfig,
)

View File

@ -4,20 +4,26 @@ import webpackConfig from '../../webpack/webpack.dev'
import { sendMessage } from './web-socket-server'
import { defaultWatcherHandler } from './watcher-common'
export const startCoreWatcher = () => new Promise<void>(resolve => {
const compiler = webpack(webpackConfig as webpack.Configuration)
console.log('本体编译中...')
const instance = compiler.watch({}, defaultWatcherHandler(
() => resolve(),
result => {
console.log('本体已编译:', result.hash)
sendMessage({
type: 'coreUpdate',
})
},
))
exitHook(exit => instance.close(() => {
console.log('本体编译器已退出')
exit()
}))
})
export const startCoreWatcher = () =>
new Promise<void>(resolve => {
const compiler = webpack(webpackConfig as webpack.Configuration)
console.log('本体编译中...')
const instance = compiler.watch(
{},
defaultWatcherHandler(
() => resolve(),
result => {
console.log('本体已编译:', result.hash)
sendMessage({
type: 'coreUpdate',
})
},
),
)
exitHook(exit =>
instance.close(() => {
console.log('本体编译器已退出')
exit()
}),
)
})

View File

@ -18,14 +18,13 @@ export type QuerySessionsResponsePayload = PayloadBase<'querySessionsResponse'>
}
export type StopPayload = PayloadBase<'stop'>
export type Payload = (
StartPayload |
CoreUpdatePayload |
ItemUpdatePayload |
StopPayload |
ItemStopPayload |
QuerySessionsPayload |
QuerySessionsResponsePayload
)
export type Payload =
| StartPayload
| CoreUpdatePayload
| ItemUpdatePayload
| StopPayload
| ItemStopPayload
| QuerySessionsPayload
| QuerySessionsResponsePayload
export type MessageHandler<P extends Payload = Payload> = (payload: P) => void

View File

@ -34,24 +34,27 @@ export const stopInstance = (instance: Watching, onClose: () => void) => {
}
}
export const startRegistryWatcher = (url: string, config: Configuration) => new Promise<void>(
resolve => {
export const startRegistryWatcher = (url: string, config: Configuration) =>
new Promise<void>(resolve => {
console.log(`功能编译中... ${url}`)
const { maxWatchers } = devServerConfig
const watcher = webpack(config)
const instance = watcher.watch({}, defaultWatcherHandler(
() => {
resolve()
},
result => {
console.log('功能已编译:', result.hash, url)
sendMessage({
type: 'itemUpdate',
path: url,
sessions: watchers.map(it => it.url),
})
},
))
const instance = watcher.watch(
{},
defaultWatcherHandler(
() => {
resolve()
},
result => {
console.log('功能已编译:', result.hash, url)
sendMessage({
type: 'itemUpdate',
path: url,
sessions: watchers.map(it => it.url),
})
},
),
)
exitHook(exit => {
if (!instance.closed) {
instance.close(() => {
@ -63,9 +66,10 @@ export const startRegistryWatcher = (url: string, config: Configuration) => new
if (watchers.length >= maxWatchers) {
const oldInstance = watchers.shift()
stopInstance(oldInstance.instance, () => {
console.log(`已达到 maxWatchers 数量 (${maxWatchers}), 退出了功能编译器: ${oldInstance.url}`)
console.log(
`已达到 maxWatchers 数量 (${maxWatchers}), 退出了功能编译器: ${oldInstance.url}`,
)
})
}
watchers.push({ url, instance })
},
)
})

View File

@ -7,57 +7,54 @@ import { buildByEntry } from '../../registry/webpack/config'
import { exitWebSocketServer } from './web-socket-server'
import { watchers, parseRegistryUrl, startRegistryWatcher } from './registry-watcher'
export const startDevServer = () => new Promise<Server>(resolve => {
const { port } = devServerConfig
export const startDevServer = () =>
new Promise<Server>(resolve => {
const { port } = devServerConfig
const server = createServer((request, response) => {
const { url } = request
console.log('请求:', url)
const callHandler = () => {
handler(request, response, {
public: '.',
directoryListing: [
'/dist',
'/dist/**',
'/registry/dist',
'/registry/dist/**',
],
})
}
if (url.startsWith('/registry')) {
const existingWatcher = watchers.find(w => w.url === url)
const registryInfo = parseRegistryUrl(url)
if (existingWatcher && registryInfo) {
console.log(`已复用功能编译器: ${url}`)
const server = createServer((request, response) => {
const { url } = request
console.log('请求:', url)
const callHandler = () => {
handler(request, response, {
public: '.',
directoryListing: ['/dist', '/dist/**', '/registry/dist', '/registry/dist/**'],
})
}
if (existingWatcher || !registryInfo) {
callHandler()
if (url.startsWith('/registry')) {
const existingWatcher = watchers.find(w => w.url === url)
const registryInfo = parseRegistryUrl(url)
if (existingWatcher && registryInfo) {
console.log(`已复用功能编译器: ${url}`)
}
if (existingWatcher || !registryInfo) {
callHandler()
} else {
startRegistryWatcher(
url,
buildByEntry({
...registryInfo,
mode: 'development',
}) as Configuration,
).then(() => callHandler())
}
} else {
startRegistryWatcher(url, buildByEntry({
...registryInfo,
mode: 'development',
}) as Configuration).then(
() => callHandler(),
)
callHandler()
}
} else {
callHandler()
}
})
exitHook(exit => {
exitWebSocketServer()
server.close(error => {
if (error) {
console.error(error)
})
exitHook(exit => {
exitWebSocketServer()
server.close(error => {
if (error) {
console.error(error)
exit()
return
}
console.log('DevServer 已退出')
exit()
return
}
console.log('DevServer 已退出')
exit()
})
})
server.listen(port, () => {
console.log(`DevServer 已启动, 端口: ${port}`)
resolve(server)
})
})
server.listen(port, () => {
console.log(`DevServer 已启动, 端口: ${port}`)
resolve(server)
})
})

View File

@ -15,13 +15,15 @@ export const defaultWatcherHandler = (
}
const needLogging = result.hasErrors() || result.hasWarnings()
if (needLogging) {
console.log(result.toString({
hash: false,
assets: false,
modules: false,
chunks: false,
color: true,
}))
console.log(
result.toString({
hash: false,
assets: false,
modules: false,
chunks: false,
color: true,
}),
)
}
if (result.hasErrors()) {
lastHash = ''

View File

@ -22,45 +22,48 @@ export const exitWebSocketServer = () => {
sendMessage({ type: 'stop' })
server.clients.forEach(c => c.close())
}
export const startWebSocketServer = (httpServer: Server) => new Promise<void>(resolve => {
server = new WebSocketServer({ server: httpServer })
server.on('connection', client => {
sendMessage({ type: 'start', sessions: watchers.map(it => it.url) })
client.on('message', data => {
try {
const payload: Payload = JSON.parse(data.toString())
console.log('收到 DevClient 消息:', payload)
switch (payload.type) {
default: {
break
}
case 'itemStop': {
const { path } = payload
const watcherIndex = watchers.findIndex(it => it.url === path)
if (watcherIndex !== -1) {
const [watcher] = watchers.splice(watcherIndex, 1)
stopInstance(watcher.instance, () => {
console.log(`功能编译器已退出: ${watcher.url}`)
})
export const startWebSocketServer = (httpServer: Server) =>
new Promise<void>(resolve => {
server = new WebSocketServer({ server: httpServer })
server.on('connection', client => {
sendMessage({ type: 'start', sessions: watchers.map(it => it.url) })
client.on('message', data => {
try {
const payload: Payload = JSON.parse(data.toString())
console.log('收到 DevClient 消息:', payload)
switch (payload.type) {
default: {
break
}
case 'itemStop': {
const { path } = payload
const watcherIndex = watchers.findIndex(it => it.url === path)
if (watcherIndex !== -1) {
const [watcher] = watchers.splice(watcherIndex, 1)
stopInstance(watcher.instance, () => {
console.log(`功能编译器已退出: ${watcher.url}`)
})
}
break
}
case 'querySessions': {
sendMessage({ type: 'querySessionsResponse', sessions: watchers.map(it => it.url) })
break
}
break
}
case 'querySessions': {
sendMessage({ type: 'querySessionsResponse', sessions: watchers.map(it => it.url) })
break
}
} catch (error) {
console.error('无效信息', data)
}
} catch (error) {
console.error('无效信息', data)
}
})
})
server.on('error', error => console.error(error))
exitHook(exit =>
server.close(error => {
if (error) {
console.error(error)
}
exit()
}),
)
resolve()
})
server.on('error', error => console.error(error))
exitHook(exit => server.close(error => {
if (error) {
console.error(error)
}
exit()
}))
resolve()
})

View File

@ -11,44 +11,53 @@ const parseAliPay = (csv: Record<string, string>[]) => {
name += `${item.} `
}
name += `${item.} ${item.}`
return `| ${item..replace(/-/g, '.')} | ${name} | ${item..substring(item..length - 4)} | ¥${item['订单金额(元)']} |`
return `| ${item..replace(/-/g, '.')} | ${name} | ${item..substring(
item..length - 4,
)} | ¥${item['订单金额(元)']} |`
}
})
return csv
}
const parseWeChat = (csv: Record<string, string>[]) => {
const items = csv.filter(item => item. === '赞赏码').map(item => ({
...item,
sortKey: Number(new Date(item.)).toString(),
toString: () => {
let name = item.
if (name === '/') {
name = '匿名'
}
const noteMatch = item..match(/付款方留言:(.+)/)
if (noteMatch) {
name += ` ${noteMatch[1]}`
}
if (item..trim() !== '/') {
name += ` ${item.}`
}
item. = item..trim()
console.log(item, item.)
return `| ${item..replace(/-/g, '.')} | ${name} | ${item..substring(item..length - 4)} | ${item['金额(元)']} |`
},
}))
const items = csv
.filter(item => item. === '赞赏码')
.map(item => ({
...item,
sortKey: Number(new Date(item.)).toString(),
toString: () => {
let name = item.
if (name === '/') {
name = '匿名'
}
const noteMatch = item..match(/付款方留言:(.+)/)
if (noteMatch) {
name += ` ${noteMatch[1]}`
}
if (item..trim() !== '/') {
name += ` ${item.}`
}
item. = item..trim()
console.log(item, item.)
return `| ${item..replace(/-/g, '.')} | ${name} | ${item..substring(
item..length - 4,
)} | ${item['金额(元)']} |`
},
}))
return items
}
const items = files.map(file => {
const text = fs.readFileSync(file, { encoding: 'utf-8' })
const csv = parse(text, { columns: true })
if (file.includes('支付宝')) {
return parseAliPay(csv)
}
if (file.includes('微信')) {
return parseWeChat(csv)
}
console.warn(`not parse method for ${file}`)
return []
}).flat().sort((a, b) => parseInt(b.sortKey) - parseInt(a.sortKey))
const items = files
.map(file => {
const text = fs.readFileSync(file, { encoding: 'utf-8' })
const csv = parse(text, { columns: true })
if (file.includes('支付宝')) {
return parseAliPay(csv)
}
if (file.includes('微信')) {
return parseWeChat(csv)
}
console.warn(`not parse method for ${file}`)
return []
})
.flat()
.sort((a, b) => parseInt(b.sortKey) - parseInt(a.sortKey))
fs.outputFileSync('dist/output.md', items.join('\n'))

View File

@ -4,9 +4,12 @@ import { Configuration } from 'webpack'
import { getDefaultConfig } from '../../webpack/webpack.config'
import { getId } from '../lib/id'
export const buildByEntry = (
params: { src: string; type: string; entry: string, mode?: Configuration['mode'] },
) => {
export const buildByEntry = (params: {
src: string
type: string
entry: string
mode?: Configuration['mode']
}) => {
// const match = entry.match(/\/?registry\/dist\/([^\/]+)\/(.+)\/index\.ts/)
// if (!match) {
// throw new Error(`Invalid entry path: ${entry}`)
@ -36,12 +39,14 @@ export const buildByEntry = (
const regexMatch = (regex: RegExp, base: string[]) => {
const match = request.match(regex)
if (match) {
const subModules = match[1] ? match[1].split('/').map(name => {
if (name.match(/\.vue$/)) {
return name.replace(/\.vue$/, '')
}
return lodash.camelCase(name)
}) : []
const subModules = match[1]
? match[1].split('/').map(name => {
if (name.match(/\.vue$/)) {
return name.replace(/\.vue$/, '')
}
return lodash.camelCase(name)
})
: []
return () => (callback as any)(null, ['coreApis', ...base, ...subModules], 'root')
}
return null

View File

@ -17,5 +17,6 @@ export const github: CdnConfig = {
},
smallLogo: `https://${host}/${owner}/Bilibili-Evolved/preview/images/logo-small.png`,
logo: `https://${host}/${owner}/Bilibili-Evolved/preview/images/logo.png`,
root: (branch, ownerOverride) => `https://${host}/${ownerOverride || owner}/Bilibili-Evolved/${branch}/`,
root: (branch, ownerOverride) =>
`https://${host}/${ownerOverride || owner}/Bilibili-Evolved/${branch}/`,
}

View File

@ -17,5 +17,6 @@ export const jsDelivr: CdnConfig = {
},
smallLogo: `https://${host}/gh/${owner}/Bilibili-Evolved@preview/images/logo-small.png`,
logo: `https://${host}/gh/${owner}/Bilibili-Evolved@preview/images/logo.png`,
root: (branch, ownerOverride) => `https://${host}/gh/${ownerOverride || owner}/Bilibili-Evolved@${branch}/`,
root: (branch, ownerOverride) =>
`https://${host}/gh/${ownerOverride || owner}/Bilibili-Evolved@${branch}/`,
}

View File

@ -10,7 +10,7 @@ export interface CdnConfig {
jszip: string
sortable: string
mdi: string
},
}
smallLogo: string
logo: string
root: (branch: string, ownerOverride: string) => string

View File

@ -1,18 +1,9 @@
import process from 'child_process'
export const commitHash = process
.execSync('git rev-parse HEAD')
.toString()
.trim()
export const branch = process
.execSync('git rev-parse --abbrev-ref HEAD')
.toString()
.trim()
export const commitHash = process.execSync('git rev-parse HEAD').toString().trim()
export const branch = process.execSync('git rev-parse --abbrev-ref HEAD').toString().trim()
export const nearestTag = process
.execSync('git describe --abbrev=0 --tags --always')
.toString()
.trim()
export const versionWithTag = process
.execSync('git describe --tags --always')
.toString()
.trim()
export const versionWithTag = process.execSync('git describe --tags --always').toString().trim()

View File

@ -14,11 +14,7 @@ const excludePatterns = [
]
export const isSourceChanged = () => {
const lastDiff = process
.execSync('git diff --name-only HEAD^')
.toString()
.trim()
.split('\n')
const lastDiff = process.execSync('git diff --name-only HEAD^').toString().trim().split('\n')
const isAllExcluded = lastDiff.every(path => excludePatterns.some(p => p.test(path)))
return !isAllExcluded

View File

@ -1,19 +1,9 @@
import {
objectProperty,
identifier,
stringLiteral,
} from '@babel/types'
import { objectProperty, identifier, stringLiteral } from '@babel/types'
import { runtimeInfo } from '../compilation-info/runtime'
import { commitHash } from '../compilation-info/git'
import { InjectMetadataAction } from './types'
export const injectCoreInfo: InjectMetadataAction = () => [
objectProperty(
identifier('commitHash'),
stringLiteral(commitHash),
),
objectProperty(
identifier('coreVersion'),
stringLiteral(runtimeInfo.version),
),
objectProperty(identifier('commitHash'), stringLiteral(commitHash)),
objectProperty(identifier('coreVersion'), stringLiteral(runtimeInfo.version)),
]

View File

@ -1,9 +1,6 @@
import { existsSync } from 'fs'
import { dirname, join } from 'path'
import {
objectProperty,
identifier,
} from '@babel/types'
import { objectProperty, identifier } from '@babel/types'
import { parseExpression } from '@babel/parser'
import { InjectMetadataAction } from './types'
@ -29,7 +26,8 @@ export const injectDescription: InjectMetadataAction = ({ filename }) => {
return [
objectProperty(
identifier('description'),
parseExpression(`
parseExpression(
`
(() => {
const context = require.context('./', false, ${regex})
return {
@ -43,7 +41,9 @@ export const injectDescription: InjectMetadataAction = ({ filename }) => {
'zh-CN': () => import('./index.md').then(m => m.default),
}
})()
`, { plugins: ['typescript'] }),
`,
{ plugins: ['typescript'] },
),
),
]
}

View File

@ -1,8 +1,5 @@
import { dirname } from 'path'
import {
objectProperty,
identifier,
} from '@babel/types'
import { objectProperty, identifier } from '@babel/types'
import { parseExpression } from '@babel/parser'
import { readdirSync } from 'fs'
import { InjectMetadataAction } from './types'
@ -28,7 +25,8 @@ export const injectI18n: InjectMetadataAction = ({ filename }) => {
return [
objectProperty(
identifier('i18n'),
parseExpression(`
parseExpression(
`
(() => {
const context = require.context('./', false, ${regex})
return {
@ -41,7 +39,9 @@ export const injectI18n: InjectMetadataAction = ({ filename }) => {
})),
}
})()
`, { plugins: ['typescript'] }),
`,
{ plugins: ['typescript'] },
),
),
]
}

View File

@ -5,24 +5,16 @@ import { injectCoreInfo } from './core-info'
import { injectDescription } from './description'
import { injectI18n } from './i18n'
const injectActions: InjectMetadataAction[] = [
injectCoreInfo,
injectDescription,
injectI18n,
]
const injectActions: InjectMetadataAction[] = [injectCoreInfo, injectDescription, injectI18n]
export const injectMetadata = (): PluginObj => ({
visitor: {
ExportNamedDeclaration(path, state) {
const { filename } = state.file.opts
const isFromRegistry = filename.startsWith(
nodePath.resolve('./registry'),
)
const isFromCore = filename.startsWith(
nodePath.resolve('./src/components'),
) || filename.startsWith(
nodePath.resolve('./src/plugins'),
)
const isFromRegistry = filename.startsWith(nodePath.resolve('./registry'))
const isFromCore =
filename.startsWith(nodePath.resolve('./src/components')) ||
filename.startsWith(nodePath.resolve('./src/plugins'))
const isEntryFile = nodePath.basename(filename) === 'index.ts'
if (!((isFromRegistry || isFromCore) && isEntryFile)) {
return
@ -32,7 +24,8 @@ export const injectMetadata = (): PluginObj => ({
return
}
node.declaration.declarations?.forEach(d => {
const isNameValid = d.id?.type === 'Identifier' && ['component', 'plugin'].includes(d.id.name)
const isNameValid =
d.id?.type === 'Identifier' && ['component', 'plugin'].includes(d.id.name)
if (!isNameValid) {
return
}
@ -41,10 +34,12 @@ export const injectMetadata = (): PluginObj => ({
return
}
targetExpression.properties.push(
...injectActions.flatMap(action => action({
expression: targetExpression,
filename,
})),
...injectActions.flatMap(action =>
action({
expression: targetExpression,
filename,
}),
),
)
})
},

View File

@ -1,10 +1,7 @@
import {
ObjectExpression,
ObjectProperty,
} from '@babel/types'
import { ObjectExpression, ObjectProperty } from '@babel/types'
export interface InjectMetadataContext {
expression: ObjectExpression
filename: string
}
export type InjectMetadataAction = ((context: InjectMetadataContext) => ObjectProperty[])
export type InjectMetadataAction = (context: InjectMetadataContext) => ObjectProperty[]

View File

@ -12,10 +12,7 @@ export const postCssLoader: RuleSetUseItem = {
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
postcssPresetEnv(),
autoPrefixer(),
],
plugins: [postcssPresetEnv(), autoPrefixer()],
},
},
}

View File

@ -13,10 +13,7 @@ const babelLoader: RuleSetUseItem = {
},
],
],
plugins: [
['@babel/plugin-proposal-class-properties'],
injectMetadata,
],
plugins: [['@babel/plugin-proposal-class-properties'], injectMetadata],
},
}

View File

@ -7,6 +7,7 @@ import { cssStyleLoaders, sassStyleLoaders } from './loaders/style-loaders'
import { tsLoaders } from './loaders/ts-loader'
import { runtimeInfo } from './compilation-info/runtime'
import commonMeta from '../src/client/common.meta.json'
import * as gitInfo from './compilation-info/git'
const relativePath = (p: string) => path.join(process.cwd(), p)
export const getDefaultConfig = (src = relativePath('src')): Configuration => {
@ -26,7 +27,7 @@ export const getDefaultConfig = (src = relativePath('src')): Configuration => {
alias: {
'@': relativePath('src'),
'fuse.js$': 'fuse.js/dist/fuse.basic.esm.min.js',
'vue$': 'vue/dist/vue.esm.js',
vue$: 'vue/dist/vue.esm.js',
},
},
performance: {
@ -58,10 +59,7 @@ export const getDefaultConfig = (src = relativePath('src')): Configuration => {
},
{
test: /\.css$/,
use: [
'style-loader',
...cssStyleLoaders,
],
use: ['style-loader', ...cssStyleLoaders],
include: /node_modules/,
},
{
@ -69,16 +67,10 @@ export const getDefaultConfig = (src = relativePath('src')): Configuration => {
oneOf: [
{
resourceQuery: /vue/,
use: [
'style-loader',
...cssStyleLoaders,
],
use: ['style-loader', ...cssStyleLoaders],
},
{
use: [
'to-string-loader',
...cssStyleLoaders,
],
use: ['to-string-loader', ...cssStyleLoaders],
},
],
include: [src],
@ -88,30 +80,18 @@ export const getDefaultConfig = (src = relativePath('src')): Configuration => {
oneOf: [
{
resourceQuery: /vue/,
use: [
'style-loader',
...sassStyleLoaders,
],
use: ['style-loader', ...sassStyleLoaders],
},
{
use: [
'to-string-loader',
...sassStyleLoaders,
],
use: ['to-string-loader', ...sassStyleLoaders],
},
],
include: [src],
},
{
test: /\.tsx?$/,
use: [
...tsLoaders,
],
include: [
src,
relativePath('tests'),
relativePath('webpack'),
],
use: [...tsLoaders],
include: [src, relativePath('tests'), relativePath('webpack')],
},
{
test: /\.vue$/,
@ -134,7 +114,7 @@ export const getDefaultConfig = (src = relativePath('src')): Configuration => {
webpackCompilationInfo: [relativePath('webpack/compilation-info'), 'compilationInfo'],
}),
new webpack.DefinePlugin({
webpackGitInfo: JSON.stringify(require('./compilation-info/git')),
webpackGitInfo: JSON.stringify(gitInfo),
}),
// new WebpackBar(),
new webpack.optimize.LimitChunkCountPlugin({
@ -143,11 +123,11 @@ export const getDefaultConfig = (src = relativePath('src')): Configuration => {
// new HardSourcePlugin(),
],
cache: {
type: "filesystem",
type: 'filesystem',
buildDependencies: {
config: [__filename],
}
}
},
},
}
}
@ -160,13 +140,19 @@ const replaceVariables = (text: string) => {
return match
})
}
export const getBanner = (meta: Record<string, string | string[]>) => `// ==UserScript==\n${Object.entries(Object.assign(meta, commonMeta)).map(([key, value]) => {
if (Array.isArray(value)) {
const lines = [...new Set(value.map(item => `// @${key.padEnd(16, ' ')}${replaceVariables(item)}`))]
return lines.join('\n')
}
return `// @${key.padEnd(16, ' ')}${replaceVariables(value)}`
}).join('\n')}
export const getBanner = (
meta: Record<string, string | string[]>,
) => `// ==UserScript==\n${Object.entries(Object.assign(meta, commonMeta))
.map(([key, value]) => {
if (Array.isArray(value)) {
const lines = [
...new Set(value.map(item => `// @${key.padEnd(16, ' ')}${replaceVariables(item)}`)),
]
return lines.join('\n')
}
return `// @${key.padEnd(16, ' ')}${replaceVariables(value)}`
})
.join('\n')}
// ==/UserScript==
/* eslint-disable */ /* spell-checker: disable */
// @[ You can find all source codes in GitHub repo ]`