Skip to content

Commit

Permalink
♻️ Rewrite mcdoc CLI (#1672)
Browse files Browse the repository at this point in the history
  • Loading branch information
misode authored Dec 12, 2024
1 parent 4eabfcc commit ece68f6
Show file tree
Hide file tree
Showing 7 changed files with 306 additions and 1,077 deletions.
52 changes: 52 additions & 0 deletions packages/mcdoc-cli/src/commands/export.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { fileUtil } from '@spyglassmc/core'
import * as mcdoc from '@spyglassmc/mcdoc'
import { resolve } from 'path'
import { pathToFileURL } from 'url'
import { createLogger, createProject as createProject, sortMaps, writeJson } from '../common.js'

interface Export {
mcdoc: Map<string, mcdoc.McdocType>
'mcdoc/dispatcher': Map<string, Map<string, mcdoc.McdocType>>
}

interface Args {
source: string
output: string
gzip: boolean
verbose: boolean
}
export async function exportCommand(args: Args) {
const logger = createLogger(args.verbose)
const project = await createProject(logger, args.source)

const data: Export = {
mcdoc: new Map(),
'mcdoc/dispatcher': new Map(),
}

const symbols = project.symbols.getVisibleSymbols('mcdoc')
for (const [name, symbol] of Object.entries(symbols)) {
if (mcdoc.binder.TypeDefSymbolData.is(symbol.data)) {
data.mcdoc.set(name, symbol.data.typeDef)
}
}
const dispatchers = project.symbols.getVisibleSymbols('mcdoc/dispatcher')
for (const [name, symbol] of Object.entries(dispatchers)) {
const dispatcherMap = new Map()
data['mcdoc/dispatcher'].set(name, dispatcherMap)
for (const [id, member] of Object.entries(symbol.members ?? {})) {
if (mcdoc.binder.TypeDefSymbolData.is(member.data)) {
dispatcherMap.set(id, member.data.typeDef)
}
}
}

const output = {
mcdoc: sortMaps(data.mcdoc),
'mcdoc/dispatcher': sortMaps(data['mcdoc/dispatcher']),
}
const outputFile = pathToFileURL(resolve(process.cwd(), args.output)).toString()
await writeJson(outputFile, output, args.gzip)

await project.close()
}
159 changes: 159 additions & 0 deletions packages/mcdoc-cli/src/commands/locale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import type { Logger } from '@spyglassmc/core'
import { fileUtil } from '@spyglassmc/core'
import { NodeJsExternals } from '@spyglassmc/core/lib/nodejs.js'
import * as mcdoc from '@spyglassmc/mcdoc'
import { resolve } from 'path'
import { pathToFileURL } from 'url'
import { createLogger, createProject, sortMaps, writeJson } from '../common.js'

interface Args {
source: string
output: string
upgrade?: boolean
gzip: boolean
verbose: boolean
}
export async function localeCommand(args: Args) {
const logger = createLogger(args.verbose)
const project = await createProject(logger, args.source)

const locale = new Map<string, string>()

function add(name: string, desc: string) {
if (locale.has(name)) {
logger.warn(`Duplicate key ${name}`)
}
locale.set(
name,
desc.replaceAll('\r', '').trim()
.split('\n\n').map(line => line.trimStart()).join('\n'),
)
}

function collect(name: string, type: mcdoc.McdocType) {
if (type.kind === 'struct') {
for (const field of type.fields) {
if (field.kind === 'spread') {
collect(name, field.type)
} else if (field.desc) {
const fieldName = typeof field.key === 'string'
? `${name}.${field.key}`
: `${name}.[${field.key.kind}]`
add(fieldName, field.desc)
collect(fieldName, field.type)
}
}
} else if (type.kind === 'union') {
type.members.forEach((member, i) => {
collect(name, member)
})
} else if (type.kind === 'enum') {
// for (const field of type.values) {
// if (field.desc) {
// add(`${name}.${field.identifier}`, field.desc)
// }
// }
} else if (type.kind === 'list') {
collect(name, type.item)
} else if (type.kind === 'tuple') {
for (const item of type.items) {
collect(name, item)
}
} else if (type.kind === 'template') {
collect(name, type.child)
} else if (type.kind === 'concrete') {
collect(name, type.child)
for (const arg of type.typeArgs) {
collect(name, arg)
}
} else if (type.kind === 'indexed') {
collect(name, type.child)
}
}

const symbols = project.symbols.getVisibleSymbols('mcdoc')
for (const [name, symbol] of Object.entries(symbols)) {
if (name.match(/<anonymous \d+>$/)) {
continue
}
if (symbol.desc) {
add(name, symbol.desc)
}
if (mcdoc.binder.TypeDefSymbolData.is(symbol.data)) {
collect(name, symbol.data.typeDef)
}
}

const outputFile = pathToFileURL(resolve(process.cwd(), args.output)).toString()

if (args.upgrade) {
const oldLocale = await readLocale(outputFile)
const outputDir = fileUtil.getParentOfFile(NodeJsExternals, outputFile).toString()
const entries = await NodeJsExternals.fs.readdir(outputDir)
const others = entries.filter(e => e.isFile() && e.name.endsWith('.json'))
.map(e => e.name.slice(0, e.name.length - '.json'.length))
for (const key of others) {
const otherFile = `${outputDir}${key}.json`
if (otherFile === outputFile) {
continue
}
logger.info(`Upgrading ${otherFile}`)
const oldOther = await readLocale(otherFile)
const other = upgradeLocale(oldOther, oldLocale, locale, logger)
await writeJson(otherFile, sortMaps(other), args.gzip)
}
}

await writeJson(outputFile, sortMaps(locale), args.gzip)

await project.close()
}

type Locale = Map<string, string>

async function readLocale(path: string): Promise<Locale> {
const data = await fileUtil.readJson(NodeJsExternals, path)
const locale = new Map<string, string>()
for (const [key, value] of Object.entries(data ?? {})) {
if (typeof value === 'string') {
locale.set(key, value)
}
}
return locale
}

function upgradeLocale(other: Locale, oldBase: Locale, newBase: Locale, logger: Logger): Locale {
const invertedNew = new Map<string, string[]>()
for (const [key, value] of newBase.entries()) {
const otherKeys = invertedNew.get(value)
if (otherKeys) {
invertedNew.set(value, [...otherKeys, key])
} else {
invertedNew.set(value, [key])
}
}

const upgraded = new Map<string, string>()
for (const [key, value] of other.entries()) {
if (newBase.has(key)) {
// Key exists, base value may have been altered.
upgraded.set(key, value)
continue
}
const baseValue = oldBase.get(key)
if (!baseValue) {
// Key was already removed previously. Prune.
continue
}
const possibleKeys = invertedNew.get(baseValue) ?? []
if (possibleKeys.length === 1) {
logger.info('Moved key', key, '->', possibleKeys[0])
upgraded.set(possibleKeys[0], value)
} else {
// Unknown move or removal. Keeping unused key for one cycle.
upgraded.set(key, value)
}
}

return upgraded
}
62 changes: 62 additions & 0 deletions packages/mcdoc-cli/src/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import * as core from '@spyglassmc/core'
import { NodeJsExternals } from '@spyglassmc/core/lib/nodejs.js'
import * as mcdoc from '@spyglassmc/mcdoc'
import { resolve } from 'path'
import { pathToFileURL } from 'url'

export function createLogger(verbose?: boolean): core.Logger {
return {
log: (...args: any[]) => verbose ? console.log(...args) : {},
info: (...args: any[]) => verbose ? console.info(...args) : {},
warn: (...args: any[]) => console.warn(...args),
error: (...args: any[]) => console.error(...args),
}
}

export async function createProject(
logger: core.Logger,
root: string,
): Promise<core.Project> {
const cacheRoot = resolve(process.cwd(), '.cache')
const projectRoot = resolve(process.cwd(), root)

const project = new core.Project({
logger,
profilers: new core.ProfilerFactory(logger, [
'project#init',
'project#ready',
'project#ready#bind',
]),
cacheRoot: core.fileUtil.ensureEndingSlash(pathToFileURL(cacheRoot).toString()),
defaultConfig: core.ConfigService.merge(core.VanillaConfig, {
env: { dependencies: [] },
}),
externals: NodeJsExternals,
initializers: [mcdoc.initialize],
projectRoots: [core.fileUtil.ensureEndingSlash(pathToFileURL(projectRoot).toString())],
})

await project.ready()
await project.cacheService.save()

return project
}

export function sortMaps(data: unknown): unknown {
if (data instanceof Map) {
return Object.fromEntries(
[...data.entries()]
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([k, v]) => [k, sortMaps(v)]),
)
}
return data
}

export async function writeJson(path: string, data: unknown, gzip: boolean) {
const contents = JSON.stringify(data, undefined, '\t') + '\n'
await core.fileUtil.writeFile(NodeJsExternals, path, contents)
if (gzip) {
await core.fileUtil.writeGzippedJson(NodeJsExternals, `${path}.gz`, data)
}
}
Loading

0 comments on commit ece68f6

Please sign in to comment.