diff --git a/.changeset/slimy-items-care.md b/.changeset/slimy-items-care.md new file mode 100644 index 00000000..fa6488ca --- /dev/null +++ b/.changeset/slimy-items-care.md @@ -0,0 +1,5 @@ +--- +"@nx.js/patch-nacp": major +--- + +Support parsers and formatters diff --git a/packages/patch-nacp/src/index.ts b/packages/patch-nacp/src/index.ts index 0344e511..79150229 100644 --- a/packages/patch-nacp/src/index.ts +++ b/packages/patch-nacp/src/index.ts @@ -1,19 +1,103 @@ import parseAuthor from 'parse-author'; import { readFileSync } from 'node:fs'; -import { NACP } from '@tootallnate/nacp'; +import { NACP, VideoCapture } from '@tootallnate/nacp'; import { titleCase } from 'title-case'; import type { PackageJson as BasePackageJson } from 'types-package-json'; export type PackageJsonNacp = Omit; +// TODO: move this to `@tootallnate/nacp` +enum Screenshot { + Enabled, + Disabled, +} + export interface PackageJson extends BasePackageJson { + /** + * @deprecated Use `nacp.id` instead. + */ titleId?: string; + /** + * @deprecated Use `nacp.title` instead. + */ productName?: string; + /** + * Additional NACP properties to set. + */ nacp?: PackageJsonNacp; } +export type NacpProperty = Exclude; + const VALID_NACP_PROPERTIES = Object.getOwnPropertyNames(NACP.prototype); +const parseBigInt = (k: string, v: unknown): bigint => { + if (typeof v === 'number') return BigInt(v); + if (typeof v === 'string') return BigInt(v); + throw new Error(`Invalid BigInt value for "${k}": ${v}`); +}; + +const parseTitleId = (k: string, v: unknown): bigint => { + if (typeof v === 'string') { + if (v.length !== 16) { + throw new Error(`"${k}" must be 16 hexadecimal digits`); + } + return BigInt(`0x${v}`); + } + throw new Error(`Invalid Title ID value for "${k}": ${v}`); +}; + +const parseEnum = + < + T extends { + [K in keyof T]: K extends string ? number : string; + }, + >( + Enum: T, + trueValue?: keyof T, + falseValue?: keyof T, + ) => + (k: string, v: unknown): number => { + let val: number | undefined; + if (typeof v === 'string') { + val = Enum[v as keyof T] as number; + } else if (typeof v === 'boolean') { + val = Enum[v ? trueValue : falseValue] as number; + } else if (typeof v === 'number') { + val = v; + } + if ( + typeof val === 'undefined' || + typeof Enum[val as keyof T] !== 'string' + ) { + throw new Error(`Invalid "${k}" value: ${v}`); + } + return val; + }; + +const parsers: { + [T in NacpProperty]?: (k: T, v: unknown) => NACP[T]; +} = { + id: parseTitleId, + saveDataOwnerId: parseTitleId, + userAccountSaveDataSize: parseBigInt, + videoCapture: parseEnum(VideoCapture, 'Enabled', 'Disabled'), + screenshot: parseEnum(Screenshot, 'Enabled', 'Disabled'), +}; + +const formatTitleId = (v: bigint) => v.toString(16).padStart(16, '0'); + +const formatters: { + [T in NacpProperty]?: (v: NACP[T]) => string; +} = { + id: formatTitleId, + saveDataOwnerId: formatTitleId, + userAccountSaveDataSize: (v) => + v === 0n ? `${v} (\`localStorage\` disabled)` : String(v), + videoCapture: (v) => VideoCapture[v], + screenshot: (v) => Screenshot[v], +}; + export function patchNACP(nacp: NACP, packageJsonUrl: URL) { const warnings: string[] = []; const updated = new Map(); @@ -33,7 +117,7 @@ export function patchNACP(nacp: NACP, packageJsonUrl: URL) { 'The "titleId" property is deprecated. Use "nacp.id" instead.', ); nacp.id = titleId; - updated.set('ID', titleId); + updated.set('ID', getFormatter('id')(nacp.id)); } const title = productName || name; @@ -43,20 +127,20 @@ export function patchNACP(nacp: NACP, packageJsonUrl: URL) { 'The "productName" property is deprecated. Use "nacp.title" instead.', ); } - updated.set('Title', title); nacp.title = title; + updated.set('Title', getFormatter('title')(nacp.title)); } if (version) { nacp.version = version; - updated.set('Version', version); + updated.set('Version', getFormatter('version')(nacp.version)); } const author = typeof rawAuthor === 'string' ? parseAuthor(rawAuthor) : rawAuthor; if (author?.name) { nacp.author = author.name; - updated.set('Author', author.name); + updated.set('Author', getFormatter('author')(nacp.author)); } for (const [k, v] of Object.entries(pkgNacp)) { @@ -65,23 +149,27 @@ export function patchNACP(nacp: NACP, packageJsonUrl: URL) { continue; } - // @ts-expect-error - const oldValue = nacp[k]; - // @ts-expect-error - nacp[k] = v; - // @ts-expect-error - const newValue = nacp[k]; - - if (newValue !== oldValue) { - const titleCased = - k === 'id' ? 'ID' : titleCase(k.replace(/([A-Z])/g, ' $1')); - let label = newValue; - if (k === 'userAccountSaveDataSize' && newValue === 0n) { - label = `${newValue} (\`localStorage\` disabled)`; - } - updated.set(titleCased, label); - } + const newValue = updateValue(nacp, k as NacpProperty, v); + + const titleCased = + k === 'id' ? 'ID' : titleCase(k.replace(/([A-Z])/g, ' $1')); + const formatter = getFormatter(k as NacpProperty); + updated.set(titleCased, formatter(newValue)); } return { packageJson, updated, warnings }; } + +function getFormatter(key: T): (v: NACP[T]) => string { + return formatters[key] ?? String; +} + +function updateValue( + nacp: NACP, + key: T, + value: unknown, +): NACP[T] { + const parser = parsers[key] ?? ((_, v): NACP[T] => v as NACP[T]); + nacp[key] = parser(key, value); + return nacp[key]; +} diff --git a/packages/repl/package.json b/packages/repl/package.json index c2fd0154..d0561c00 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -22,7 +22,7 @@ "access": "public" }, "dependencies": { - "kleur": "^4.1.5", + "kleur": "github:TooTallNate/kleur#rgb", "sisteransi": "^1.0.5" }, "devDependencies": { diff --git a/packages/repl/test/repl.test.ts b/packages/repl/test/repl.test.ts index f6049634..f484a9ae 100644 --- a/packages/repl/test/repl.test.ts +++ b/packages/repl/test/repl.test.ts @@ -1,3 +1,6 @@ +import { $ } from 'kleur/colors'; +$.enabled = true; + import { createInspect } from '@nx.js/inspect'; import { describe, expect, test } from 'vitest'; import { REPL, type REPLOptions } from '../src/index'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ca7e5ac..692c5088 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -667,8 +667,8 @@ importers: packages/repl: dependencies: kleur: - specifier: ^4.1.5 - version: 4.1.5 + specifier: github:TooTallNate/kleur#rgb + version: github.com/TooTallNate/kleur/aa22b05da68d29ff6d2ce5eba5895ac654b7f25a sisteransi: specifier: ^1.0.5 version: 1.0.5