mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-04-23 10:58:13 +08:00
380 lines
11 KiB
TypeScript
380 lines
11 KiB
TypeScript
/**
|
|
* 只写一个 npm 代码块,自动转为 代码块分组 [npm, pnpm, yarn, bun, deno]
|
|
*
|
|
* ::: npm-to
|
|
* ``` sh
|
|
* npm i -D vuepress-theme-plume
|
|
* ```
|
|
* :::
|
|
* ↓ ↓ ↓ ↓ ↓
|
|
* ::: code-tabs
|
|
* @tab npm
|
|
* ```sh
|
|
* npm i -D vuepress-theme-plume
|
|
* ```
|
|
* @tab pnpm
|
|
* ```sh
|
|
* pnpm add -D vuepress-theme-plume
|
|
* ```
|
|
* @tab yarn
|
|
* ```sh
|
|
* yarn add -D vuepress-theme-plume
|
|
* ```
|
|
* :::
|
|
*/
|
|
import type Token from 'markdown-it/lib/token.mjs'
|
|
import type { Markdown } from 'vuepress/markdown'
|
|
import type { NpmToOptions, NpmToPackageManager } from '../../shared/index.js'
|
|
import { isArray } from '@vuepress/helper'
|
|
import container from 'markdown-it-container'
|
|
import { resolveAttrs } from '../utils/resolveAttrs.js'
|
|
|
|
type PackageCommand = 'install' | 'add' | 'remove' | 'run' | 'create' | 'init' | 'npx' | 'ci'
|
|
interface CommandConfigItem {
|
|
cli: string
|
|
flags?: Record<string, string>
|
|
}
|
|
type CommandConfig = Record<Exclude<NpmToPackageManager, 'npm'>, CommandConfigItem | false>
|
|
type CommandConfigs = Record<PackageCommand, { pattern: RegExp } & CommandConfig>
|
|
|
|
const ALLOW_LIST = ['npm', 'pnpm', 'yarn', 'bun', 'deno'] as const
|
|
const BOOL_FLAGS: string[] = ['--no-save', '-B', '--save-bundle', '--save-dev', '-D', '--save-prod', '-P', '--save-peer', '-O', '--save-optional', '-E', '--save-exact', '-y', '--yes', '-g', '--global']
|
|
|
|
const DEFAULT_TABS: NpmToPackageManager[] = ['npm', 'pnpm', 'yarn']
|
|
|
|
const MANAGERS_CONFIG: CommandConfigs = {
|
|
install: {
|
|
pattern: /(?:^|\s)npm\s+(?:install|i)$/,
|
|
pnpm: { cli: 'pnpm install' },
|
|
yarn: { cli: 'yarn' },
|
|
bun: { cli: 'bun install' },
|
|
deno: { cli: 'deno install' },
|
|
},
|
|
add: {
|
|
pattern: /(?:^|\s)npm\s+(?:install|i|add)(?:\s|$)/,
|
|
pnpm: {
|
|
cli: 'pnpm add',
|
|
flags: {
|
|
'--no-save': '', // unsupported
|
|
'-B': '', // unsupported
|
|
'--save-bundle': '', // unsupported
|
|
},
|
|
},
|
|
yarn: {
|
|
cli: 'yarn add',
|
|
flags: {
|
|
'--save-dev': '--dev',
|
|
'--save-prod': '--prod',
|
|
'-P': '', // in npm, `-P` same as `--save-prod`. but in yarn, `-P` same as `--peer`
|
|
'--save-peer': '--peer',
|
|
'--save-optional': '--optional',
|
|
'--no-save': '', // unsupported
|
|
'--save-exact': '--exact',
|
|
'-B': '', // unsupported
|
|
'--save-bundle': '', // unsupported
|
|
},
|
|
},
|
|
bun: {
|
|
cli: 'bun add',
|
|
flags: {
|
|
'--save-dev': '--development',
|
|
'-P': '', // it's default
|
|
'--save-prod': '', // it's default
|
|
'--save-peer': '', // unsupported
|
|
'-O': '--optional',
|
|
'--save-optional': '--optional',
|
|
'--no-save': '', // unsupported
|
|
'--save-exact': '--exact',
|
|
'-B': '', // unsupported
|
|
'--save-bundle': '', // unsupported
|
|
},
|
|
},
|
|
deno: {
|
|
cli: 'deno add',
|
|
flags: {
|
|
'-g': '', // unsupported
|
|
'--global': '', // unsupported
|
|
'--save-dev': '--dev',
|
|
'-P': '', // unsupported
|
|
'--save-prod': '', // unsupported
|
|
'--save-peer': '', // unsupported
|
|
'-O': '', // unsupported
|
|
'--save-optional': '', // unsupported
|
|
'--no-save': '', // unsupported
|
|
'-E': '', // unsupported
|
|
'--save-exact': '', // unsupported
|
|
'-B': '', // unsupported
|
|
'--save-bundle': '', // unsupported
|
|
},
|
|
},
|
|
},
|
|
run: {
|
|
pattern: /(?:^|\s)npm\s+(?:run|run-script|rum|urn)(?:\s|$)/,
|
|
pnpm: {
|
|
cli: 'pnpm',
|
|
flags: {
|
|
'-w': '-F', // same as `--workspace`
|
|
'--workspace': '--filter', // filter by workspaces
|
|
'--': '', // scripts flags
|
|
},
|
|
},
|
|
yarn: {
|
|
cli: 'yarn',
|
|
flags: {
|
|
'-w': '', // unsupported
|
|
'--workspace': '', // unsupported
|
|
},
|
|
},
|
|
bun: {
|
|
cli: 'bun run',
|
|
flags: {
|
|
'-w': '--filter', // same as `--workspace`
|
|
'--workspace': '--filter', // filter by workspaces
|
|
},
|
|
},
|
|
deno: {
|
|
cli: 'deno run',
|
|
flags: {
|
|
'-w': '', // unsupported
|
|
'--workspace': '', // unsupported
|
|
},
|
|
},
|
|
},
|
|
create: {
|
|
pattern: /(?:^|\s)npm\s+create\s/,
|
|
pnpm: { cli: 'pnpm create', flags: { '-y': '', '--yes': '' } },
|
|
yarn: { cli: 'yarn create', flags: { '-y': '', '--yes': '' } },
|
|
bun: { cli: 'bun create', flags: { '-y': '', '--yes': '' } },
|
|
deno: { cli: 'deno run -A ', flags: { '-y': '', '--yes': '' } },
|
|
},
|
|
init: {
|
|
pattern: /(?:^|\s)npm\s+init/,
|
|
pnpm: { cli: 'pnpm init', flags: { '-y': '', '--yes': '' } },
|
|
yarn: { cli: 'yarn init', flags: { '-y': '', '--yes': '' } },
|
|
bun: { cli: 'bun init', flags: { '-y': '', '--yes': '' } },
|
|
deno: { cli: 'deno init', flags: { '-y': '', '--yes': '' } },
|
|
},
|
|
npx: {
|
|
pattern: /(?:^|\s)npx\s+/,
|
|
pnpm: { cli: 'pnpm dlx' },
|
|
yarn: { cli: 'yarn dlx' },
|
|
bun: { cli: 'bunx' },
|
|
deno: { cli: 'deno run -A' },
|
|
},
|
|
remove: {
|
|
pattern: /(?:^|\s)npm\s+(?:uninstall|r|rm|remove|unlink|un)(?:\s|$)/,
|
|
pnpm: {
|
|
cli: 'pnpm remove',
|
|
flags: { '--no-save': '', '--save': '', '-S': '' },
|
|
},
|
|
yarn: {
|
|
cli: 'yarn remove',
|
|
flags: { '--save-dev': '--dev', '--save': '', '-S': '', '-g': '', '--global': '' },
|
|
},
|
|
bun: {
|
|
cli: 'bun remove',
|
|
flags: { '--save-dev': '--development', '--save': '', '-S': '', '-g': '', '--global': '' },
|
|
},
|
|
deno: {
|
|
cli: 'deno uninstall',
|
|
flags: { '--save-dev': '--dev', '--save': '', '-S': '' },
|
|
},
|
|
},
|
|
ci: {
|
|
pattern: /(?:^|\s)npm\s+ci$/,
|
|
pnpm: { cli: 'pnpm install --frozen-lockfile' },
|
|
yarn: { cli: 'yarn install --immutable' },
|
|
bun: { cli: 'bun install --frozen-lockfile' },
|
|
deno: { cli: 'deno install --frozen' },
|
|
},
|
|
}
|
|
|
|
export function npmToPlugins(md: Markdown, options: NpmToOptions = {}): void {
|
|
const type = 'npm-to'
|
|
const validate = (info: string): boolean => info.trim().startsWith(type)
|
|
|
|
const opt = isArray(options) ? { tabs: options } : options
|
|
const defaultTabs = opt.tabs?.length ? opt.tabs : DEFAULT_TABS
|
|
|
|
const render = (tokens: Token[], idx: number): string => {
|
|
const { attrs } = resolveAttrs(tokens[idx].info.trim().slice(type.length))
|
|
const tabs = (attrs.tabs ? attrs.tabs.split(/,\s*/) : defaultTabs) as NpmToPackageManager[]
|
|
if (tokens[idx].nesting === 1) {
|
|
const token = tokens[idx + 1]
|
|
const info = token.info.trim()
|
|
if (token.type === 'fence') {
|
|
const content = token.content
|
|
token.hidden = true
|
|
token.type = 'text'
|
|
token.content = ''
|
|
const lines = content.split(/(\n|\s*&&\s*)/)
|
|
return md.render(resolveNpmTo(lines, info, idx, tabs), {})
|
|
}
|
|
}
|
|
return ''
|
|
}
|
|
|
|
md.use(container, type, { validate, render })
|
|
}
|
|
|
|
function resolveNpmTo(lines: string[], info: string, idx: number, tabs: NpmToPackageManager[]): string {
|
|
tabs = validateTabs(tabs)
|
|
const res: string[] = []
|
|
const map: Record<string, LineParsed | false> = {}
|
|
for (const tab of tabs) {
|
|
const newLines: string[] = []
|
|
for (const line of lines) {
|
|
const config = findConfig(line)
|
|
if (config && config[tab]) {
|
|
const parsed = (map[line] ??= parseLine(line)) as LineParsed
|
|
const { cli, flags } = config[tab] as CommandConfigItem
|
|
|
|
let newLine = `${parsed.env ? `${parsed.env} ` : ''}${cli}`
|
|
if (parsed.args && flags) {
|
|
let args = parsed.args
|
|
for (const [key, value] of Object.entries(flags)) {
|
|
args = args.replaceAll(key, value)
|
|
}
|
|
newLine += ` ${args.replace(/\s+-/g, ' -').trim()}`
|
|
}
|
|
|
|
if (parsed.cmd)
|
|
newLine += ` ${parsed.cmd}`
|
|
|
|
if (parsed.scriptArgs)
|
|
newLine += ` ${parsed.scriptArgs}`
|
|
newLines.push(newLine.trim())
|
|
}
|
|
else {
|
|
newLines.push(line)
|
|
}
|
|
}
|
|
res.push(`@tab ${tab}\n\`\`\`${info}\n${newLines.join('')}\n\`\`\``)
|
|
}
|
|
return `:::code-tabs#npm-to-${idx}\n${res.join('\n')}\n:::`
|
|
}
|
|
|
|
function findConfig(line: string): CommandConfig | undefined {
|
|
for (const { pattern, ...config } of Object.values(MANAGERS_CONFIG)) {
|
|
if (pattern.test(line)) {
|
|
return config
|
|
}
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function validateTabs(tabs: NpmToPackageManager[]): NpmToPackageManager[] {
|
|
tabs = tabs.filter(tab => ALLOW_LIST.includes(tab))
|
|
if (tabs.length === 0) {
|
|
return DEFAULT_TABS
|
|
}
|
|
return tabs
|
|
}
|
|
|
|
interface LineParsed {
|
|
env: string
|
|
cli: string
|
|
cmd: string
|
|
args?: string
|
|
scriptArgs?: string
|
|
}
|
|
|
|
const LINE_REG = /(.*)(npm|npx)\s+(.*)/
|
|
export function parseLine(line: string): false | LineParsed {
|
|
const match = line.match(LINE_REG)
|
|
if (!match)
|
|
return false
|
|
|
|
const [, env, cli, rest] = match
|
|
const idx = rest.trim().indexOf(' ')
|
|
if (cli === 'npx') {
|
|
let cmd = ''
|
|
let scriptArgs = ''
|
|
if (idx !== -1) {
|
|
cmd = rest.slice(0, idx)
|
|
scriptArgs = rest.slice(idx + 1).trim()
|
|
}
|
|
else {
|
|
cmd = rest
|
|
}
|
|
return { env, cli, cmd, scriptArgs }
|
|
}
|
|
|
|
if (idx === -1)
|
|
return { env, cli: `${cli} ${rest.trim()}`, cmd: '' }
|
|
|
|
return { env, cli: `${cli} ${rest.slice(0, idx)}`, ...parseArgs(rest.slice(idx + 1)) }
|
|
}
|
|
|
|
function parseArgs(line: string): { cmd: string, args?: string, scriptArgs?: string } {
|
|
line = line?.trim()
|
|
|
|
const [npmArgs, scriptArgs] = line.split(/\s+--\s+/)
|
|
let cmd = ''
|
|
let args = ''
|
|
if (npmArgs[0] !== '-') {
|
|
if (npmArgs[0] === '"' || npmArgs[0] === '\'') {
|
|
const idx = npmArgs.slice(1).indexOf(npmArgs[0])
|
|
cmd = npmArgs.slice(0, idx + 2)
|
|
args = npmArgs.slice(idx + 2)
|
|
}
|
|
else {
|
|
const idx = npmArgs.indexOf(' -')
|
|
if (idx === -1) {
|
|
cmd = npmArgs
|
|
}
|
|
else {
|
|
cmd = npmArgs.slice(0, idx)
|
|
args = npmArgs.slice(idx + 1)
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
let newLine = ''
|
|
let value = ''
|
|
let isQuote = false
|
|
let isBool = false
|
|
let isNextValue = false
|
|
let quote = ''
|
|
for (let i = 0; i < npmArgs.length; i++) {
|
|
const v = npmArgs[i]
|
|
if (!isQuote && (v === '"' || v === '\'')) {
|
|
quote = v
|
|
isQuote = true
|
|
value += v
|
|
}
|
|
else if (isQuote && v === quote) {
|
|
isQuote = false
|
|
value += v
|
|
}
|
|
else if ((v === ' ' || v === '=' || i === npmArgs.length - 1) && !isQuote && value) {
|
|
if (i === npmArgs.length - 1) {
|
|
value += v
|
|
}
|
|
|
|
const isKey = value[0] === '-'
|
|
if (isKey) {
|
|
isBool = BOOL_FLAGS.includes(value)
|
|
isNextValue = !isBool
|
|
}
|
|
if (!isKey && !isNextValue) {
|
|
cmd += `${value}`
|
|
}
|
|
else {
|
|
newLine += `${value}${i !== npmArgs.length - 1 ? v : ''}`
|
|
if (!isKey && isNextValue) {
|
|
isNextValue = false
|
|
}
|
|
}
|
|
value = ''
|
|
}
|
|
else {
|
|
value += v
|
|
}
|
|
}
|
|
args = newLine
|
|
}
|
|
|
|
return { cmd: cmd.trim(), args: args.trim(), scriptArgs }
|
|
}
|