From fc37c02c088623ac88f9b0a9f31ddeaa8c1a273c Mon Sep 17 00:00:00 2001 From: homura Date: Thu, 22 Feb 2024 16:01:44 +0800 Subject: [PATCH 1/5] feat: export layout def to ensure tsc can infer types --- .changeset/soft-guests-camp.md | 6 +++++ packages/codec/src/molecule/index.ts | 6 +++++ packages/codec/src/molecule/layout.ts | 39 ++++++++++++++++----------- packages/codec/src/number/uint.ts | 2 ++ packages/lumos/src/codec/index.ts | 7 +++++ 5 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 .changeset/soft-guests-camp.md diff --git a/.changeset/soft-guests-camp.md b/.changeset/soft-guests-camp.md new file mode 100644 index 000000000..63b65e7ba --- /dev/null +++ b/.changeset/soft-guests-camp.md @@ -0,0 +1,6 @@ +--- +"@ckb-lumos/codec": patch +"@ckb-lumos/lumos": patch +--- + +feat: export mol layout definition to ensure tsc can infer types diff --git a/packages/codec/src/molecule/index.ts b/packages/codec/src/molecule/index.ts index 4f4bb528d..57e3b913b 100644 --- a/packages/codec/src/molecule/index.ts +++ b/packages/codec/src/molecule/index.ts @@ -1,2 +1,8 @@ export { byteOf, byteArrayOf, byteVecOf } from "./helper"; export { table, array, option, struct, vector, union } from "./layout"; +export type { + ObjectLayoutCodec, + ArrayLayoutCodec, + OptionLayoutCodec, + UnionLayoutCodec, +} from "./layout"; diff --git a/packages/codec/src/molecule/layout.ts b/packages/codec/src/molecule/layout.ts index b0490db56..d207e5949 100644 --- a/packages/codec/src/molecule/layout.ts +++ b/packages/codec/src/molecule/layout.ts @@ -41,22 +41,23 @@ type PartialNullable> = & Partial>> & Pick>; -export type ObjectCodec> = BytesCodec< - PartialNullable<{ [key in keyof T]: UnpackResult }>, - PartialNullable<{ [key in keyof T]: PackParam }> ->; +export type ObjectLayoutCodec> = + BytesCodec< + PartialNullable<{ [key in keyof T]: UnpackResult }>, + PartialNullable<{ [key in keyof T]: PackParam }> + >; -export interface OptionCodec +export interface OptionLayoutCodec extends BytesCodec | undefined> { pack: (packable?: PackParam) => Uint8Array; } -export type ArrayCodec = BytesCodec< +export type ArrayLayoutCodec = BytesCodec< Array>, Array> >; -export type UnionCodec> = BytesCodec< +export type UnionLayoutCodec> = BytesCodec< { [key in keyof T]: { type: key; value: UnpackResult } }[keyof T], { [key in keyof T]: { type: key; value: PackParam } }[keyof T] >; @@ -70,7 +71,7 @@ export type UnionCodec> = BytesCodec< export function array( itemCodec: T, itemCount: number -): ArrayCodec & Fixed { +): ArrayLayoutCodec & Fixed { const enhancedArrayCodec = createArrayCodec(itemCodec); return createFixedBytesCodec({ byteLength: itemCodec.byteLength * itemCount, @@ -117,7 +118,7 @@ function checkShape(shape: T, fields: (keyof T)[]) { export function struct>( shape: T, fields: (keyof T)[] -): ObjectCodec & Fixed { +): ObjectLayoutCodec & Fixed { checkShape(shape, fields); const objectCodec = createObjectCodec(shape); return createFixedBytesCodec({ @@ -153,7 +154,9 @@ export function struct>( * Vector with fixed size item codec * @param itemCodec fixed-size vector item codec */ -export function fixvec(itemCodec: T): ArrayCodec { +export function fixvec( + itemCodec: T +): ArrayLayoutCodec { return createBytesCodec({ pack(items) { const arrayCodec = createArrayCodec(itemCodec); @@ -181,7 +184,9 @@ export function fixvec(itemCodec: T): ArrayCodec { * @param itemCodec the vector item codec. It can be fixed-size or dynamic-size. * For example, you can create a recursive vector with this. */ -export function dynvec(itemCodec: T): ArrayCodec { +export function dynvec( + itemCodec: T +): ArrayLayoutCodec { return createBytesCodec({ pack(obj) { const arrayCodec = createArrayCodec(itemCodec); @@ -241,7 +246,9 @@ export function dynvec(itemCodec: T): ArrayCodec { * General vector codec, if `itemCodec` is fixed size type, it will create a fixvec codec, otherwise a dynvec codec will be created. * @param itemCodec */ -export function vector(itemCodec: T): ArrayCodec { +export function vector( + itemCodec: T +): ArrayLayoutCodec { if (isFixedCodec(itemCodec)) { return fixvec(itemCodec); } @@ -256,7 +263,7 @@ export function vector(itemCodec: T): ArrayCodec { export function table>( shape: T, fields: (keyof T)[] -): ObjectCodec { +): ObjectLayoutCodec { checkShape(shape, fields); return createBytesCodec({ pack(obj) { @@ -335,7 +342,7 @@ export function table>( export function union>( itemCodec: T, fields: (keyof T)[] | Record -): UnionCodec { +): UnionLayoutCodec { checkShape(itemCodec, Array.isArray(fields) ? fields : Object.keys(fields)); // check duplicated id @@ -405,7 +412,9 @@ export function union>( * - if it's not empty, just serialize the inner item (the size is same as the inner item's size). * @param itemCodec */ -export function option(itemCodec: T): OptionCodec { +export function option( + itemCodec: T +): OptionLayoutCodec { return createBytesCodec({ pack(obj?) { const nullableCodec = createNullableCodec(itemCodec); diff --git a/packages/codec/src/number/uint.ts b/packages/codec/src/number/uint.ts index 7c0efe68f..4d07a3cd3 100644 --- a/packages/codec/src/number/uint.ts +++ b/packages/codec/src/number/uint.ts @@ -2,6 +2,8 @@ import { BI, BIish } from "@ckb-lumos/bi"; import { createFixedBytesCodec, FixedBytesCodec } from "../base"; import { CodecBaseParseError } from "../error"; +export { BI, BIish }; + function assertNumberRange( value: BIish, min: BIish, diff --git a/packages/lumos/src/codec/index.ts b/packages/lumos/src/codec/index.ts index 35e244067..2bb8b9718 100644 --- a/packages/lumos/src/codec/index.ts +++ b/packages/lumos/src/codec/index.ts @@ -27,6 +27,13 @@ export { byteVecOf, } from "@ckb-lumos/codec/lib/molecule"; +export type { + ObjectLayoutCodec, + ArrayLayoutCodec, + OptionLayoutCodec, + UnionLayoutCodec, +} from "@ckb-lumos/codec/lib/molecule"; + export { Uint8, Uint16, From ed4e7bf7c6a6da91f8b2b91586850672c57f963b Mon Sep 17 00:00:00 2001 From: homura Date: Thu, 22 Feb 2024 16:06:46 +0800 Subject: [PATCH 2/5] test: debugger doesn't support parallel yet --- packages/common-scripts/tests/omnilock-bitcoin.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/common-scripts/tests/omnilock-bitcoin.test.ts b/packages/common-scripts/tests/omnilock-bitcoin.test.ts index 882385947..c6772357e 100644 --- a/packages/common-scripts/tests/omnilock-bitcoin.test.ts +++ b/packages/common-scripts/tests/omnilock-bitcoin.test.ts @@ -26,20 +26,20 @@ test("Should throw when sign a message without provider", async (t) => { const context = createTestContext(getDefaultConfig()); const managerConfig = { PREFIX: "ckt", SCRIPTS: context.scriptConfigs }; -test("Omnilock#Bitcoin P2PKH", async (t) => { +test.serial("Omnilock#Bitcoin P2PKH", async (t) => { const { provider } = makeProvider(AddressType.P2PKH); const result = await execute(provider); t.is(result.code, 0, result.message); }); -test("Omnilock#Bitcoin P2WPKH", async (t) => { +test.serial("Omnilock#Bitcoin P2WPKH", async (t) => { const { provider } = makeProvider(AddressType.P2WPKH); const result = await execute(provider); t.is(result.code, 0, result.message); }); -test("Omnilock#Bitcoin P2SH_P2WPKH", async (t) => { +test.serial("Omnilock#Bitcoin P2SH_P2WPKH", async (t) => { const { provider } = makeProvider(AddressType.P2SH_P2WPKH); const result = await execute(provider); From 00b586a709ab95bebef4be55060c415825213450 Mon Sep 17 00:00:00 2001 From: homura Date: Thu, 22 Feb 2024 18:52:26 +0800 Subject: [PATCH 3/5] feat(molecule): generate code from molecule schema --- .changeset/many-years-tap.md | 5 + packages/molecule/README.md | 32 +++ packages/molecule/package.json | 5 +- packages/molecule/src/circularIterator.ts | 31 +++ packages/molecule/src/cli.ts | 46 ++++ packages/molecule/src/codegen.ts | 245 ++++++++++++++++++++++ packages/molecule/tests/codegen.test.ts | 166 +++++++++++++++ 7 files changed, 529 insertions(+), 1 deletion(-) create mode 100644 .changeset/many-years-tap.md create mode 100644 packages/molecule/src/circularIterator.ts create mode 100644 packages/molecule/src/cli.ts create mode 100644 packages/molecule/src/codegen.ts create mode 100644 packages/molecule/tests/codegen.test.ts diff --git a/.changeset/many-years-tap.md b/.changeset/many-years-tap.md new file mode 100644 index 000000000..df6ef1114 --- /dev/null +++ b/.changeset/many-years-tap.md @@ -0,0 +1,5 @@ +--- +"@ckb-lumos/molecule": minor +--- + +feat: cli for generating code from mol files diff --git a/packages/molecule/README.md b/packages/molecule/README.md index 6501d6165..089289e6c 100644 --- a/packages/molecule/README.md +++ b/packages/molecule/README.md @@ -12,3 +12,35 @@ const codecMap = parser.parse(` codecMap.Uint8.pack(1); ``` + +## `lumos-molecule-codegen` + +A CLI to generate a set of TypeScript-friendly codec from a Molecule schema file. +To use it, you need to install `@ckb-lumos/molecule` and `@ckb-lumos/codec` first. The `@ckb-lumos/molecule` could be `devDependencies` if you only use it for codegen. + +```sh +npm install -D @ckb-lumos/molecule +npm install @ckb-lumos/codec +``` + +Then you can create a `lumos-molecule-codegen.json` file to configure the codegen. + +```json5 +// lumos-molecule-codegen.json +{ + // keep | camelcase + objectKeyFormat: "camelcase", + // prepend the import statement to custom and override the generated codec + prepend: "import { Uint32, Uint64, Uint128 } from './customized'", + // the output file name + output: "generated.ts", + // the input schema file + schemaFile: "blockchain.mol", +} +``` + +Finally, run the following command to generate code + +```sh +npx lumos-molecule-codegen +``` diff --git a/packages/molecule/package.json b/packages/molecule/package.json index a777408cc..0b4e58ffd 100644 --- a/packages/molecule/package.json +++ b/packages/molecule/package.json @@ -5,7 +5,7 @@ "homepage": "https://github.com/ckb-js/lumos#readme", "license": "MIT", "main": "lib/index.js", - "private": true, + "private": false, "engines": { "node": ">=12.0.0" }, @@ -34,6 +34,9 @@ "bugs": { "url": "https://github.com/ckb-js/lumos/issues" }, + "bin": { + "lumos-molecule-codegen": "lib/cli.js" + }, "scripts": { "test": "ava **/*.test.ts --timeout=2m", "fmt": "prettier --write \"{src,tests,examples}/**/*.ts\" package.json", diff --git a/packages/molecule/src/circularIterator.ts b/packages/molecule/src/circularIterator.ts new file mode 100644 index 000000000..484cbf2bf --- /dev/null +++ b/packages/molecule/src/circularIterator.ts @@ -0,0 +1,31 @@ +type CircularIterator = { + current(): T | undefined; + // move to the next point and return it + next(): T | undefined; + // delete the current element, move and return the next element + removeAndNext(): T | undefined; + hasNext(): boolean; +}; + +export function circularIterator( + elems: T[] +): CircularIterator { + const items = [...elems]; + let current = items[0]; + + return { + current: () => current, + next: () => { + const index = items.indexOf(current); + current = items[(index + 1) % items.length]; + return current; + }, + removeAndNext() { + const index = items.indexOf(current); + items.splice(index, 1); + current = items[index % items.length]; + return current; + }, + hasNext: () => items.length > 0, + }; +} diff --git a/packages/molecule/src/cli.ts b/packages/molecule/src/cli.ts new file mode 100644 index 000000000..627c6276a --- /dev/null +++ b/packages/molecule/src/cli.ts @@ -0,0 +1,46 @@ +#!/usr/bin/env node +import { codegen } from "./codegen"; +import * as fs from "node:fs"; + +const DEFAULT_CONFIG_FILE_NAME = "lumos-molecule-codegen.json"; + +function camelcase(str: string): string { + return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); +} + +type Config = { + objectKeyFormat: "camelcase" | "keep"; + prepend: string; + output: string; + schemaFile: string; +}; + +const fileConfig: Partial = (() => { + if (fs.existsSync(DEFAULT_CONFIG_FILE_NAME)) { + return JSON.parse(fs.readFileSync(DEFAULT_CONFIG_FILE_NAME, "utf8")); + } + return {}; +})(); + +const config: Config = { + objectKeyFormat: fileConfig.objectKeyFormat || "keep", + prepend: fileConfig.prepend || "", + output: fileConfig.output || "generated.ts", + schemaFile: fileConfig.schemaFile || "schema.mol", +}; + +// check if the schema file exists +if (!fs.existsSync(config.schemaFile)) { + console.error( + `Schema file ${config.schemaFile} does not exist. Please configure the \`schemaFile\` in ${DEFAULT_CONFIG_FILE_NAME}` + ); + process.exit(1); +} + +const generated = codegen(fs.readFileSync(config.schemaFile, "utf-8"), { + prepend: config.prepend, + formatObjectKeys: + config.objectKeyFormat === "camelcase" ? camelcase : undefined, +}); + +fs.writeFileSync(config.output, generated); diff --git a/packages/molecule/src/codegen.ts b/packages/molecule/src/codegen.ts new file mode 100644 index 000000000..d56cfcbda --- /dev/null +++ b/packages/molecule/src/codegen.ts @@ -0,0 +1,245 @@ +import { MolType } from "./type"; +import { Grammar as NearleyGrammar, Parser as NearleyParser } from "nearley"; +import { circularIterator } from "./circularIterator"; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const grammar = require("./grammar/mol.js"); + +export type Options = { + /** + * an import statement to prepend to the generated code, used to import and override
+ * e.g. `"import { Uint32, Uint64 } from './customized'"` to override the default `Uint32` and `Uint64` types + */ + prepend?: string; + formatObjectKeys?: (key: string, molType: MolType) => string; +}; + +function id(value: T): T { + return value; +} + +export function codegen(schema: string, options: Options = {}): string { + const parser = new NearleyParser(NearleyGrammar.fromCompiled(grammar)); + parser.feed(schema); + + // the items that don't need to be generated + const importedModules: string[] = (() => { + if (!options.prepend) return []; + + // parse the override import statements to get the items + const importedCodecs = options.prepend + .match(/(?<={)([^}]+)(?=})/g) + ?.flatMap((item) => + item + .split(",") + .map((x) => x.trim()) + .filter(Boolean) + ); + return importedCodecs || []; + })(); + + const molTypes = prepareMolTypes( + parser.results[0].filter(Boolean), + importedModules + ); + + const codecs = molTypes + .map((molType) => { + if (importedModules.includes(molType.name)) return ""; + + if (molType.type === "array") { + if (molType.item === "byte") { + return `export const ${molType.name} = createFallbackFixedBytesCodec(${molType.item_count});`; + } + + return `export const ${molType.name} = array(${molType.item}, ${molType.item_count});`; + } + + if (molType.type === "vector") { + if (molType.item === "byte") { + return `export const ${molType.name} = fallbackBytesCodec;`; + } + + return `export const ${molType.name} = vector(${molType.item});`; + } + + if (molType.type === "option") { + return `export const ${molType.name} = option(${molType.item});`; + } + + const formatObjectKey = (str: string) => + (options.formatObjectKeys || id)(str, molType); + + if (molType.type === "struct") { + const fields = molType.fields + .map((field) => ` ${formatObjectKey(field.name)}: ${field.type}`) + .join(",\n"); + const keys = molType.fields + .map((field) => `'${formatObjectKey(field.name)}'`) + .join(", "); + return `export const ${molType.name} = struct({\n${fields}\n}, [${keys}]);`; + } + + if (molType.type === "table") { + const fields = molType.fields + .map((field) => ` ${formatObjectKey(field.name)}: ${field.type}`) + .join(",\n"); + const keys = molType.fields + .map((field) => `'${formatObjectKey(field.name)}'`) + .join(", "); + return `export const ${molType.name} = table({\n${fields}\n}, [${keys}]);`; + } + + if (molType.type === "union") { + if (Array.isArray(molType.items[0])) { + const items = molType.items as [string, number][]; + + const fields = items + .map(([itemName]) => ` ${formatObjectKey(itemName)}`) + .join(",\n"); + + const keys = items + .map(([itemName, key]) => `'${formatObjectKey(itemName)}': ${key}`) + .join(", "); + + return `export const ${molType.name} = union({\n${fields}\n}, {${keys}});`; + } + + if (typeof molType.items[0] === "string") { + const items = (molType.items as string[]).map((itemName) => + formatObjectKey(itemName) + ); + + const fields = items.map((itemName) => ` ${itemName}`).join(",\n"); + const keys = items.map((itemName) => `'${itemName}'`).join(", "); + + return `export const ${molType.name} = union({\n${fields}\n}, [${keys}]);`; + } + } + }) + .filter(Boolean) + .join("\n\n"); + + return `// This file is generated by @ckb-lumos/molecule, please do not modify it manually. +/* eslint-disable */ + +import { bytes, createBytesCodec, createFixedBytesCodec, molecule } from "@ckb-lumos/codec"; +${options.prepend || "\b"} + +const { array, vector, union, option, struct, table } = molecule; + +const fallbackBytesCodec = createBytesCodec({ + pack: bytes.bytify, + unpack: bytes.hexify, +}); + +function createFallbackFixedBytesCodec(byteLength: number) { + return createFixedBytesCodec({ + pack: bytes.bytify, + unpack: bytes.hexify, + byteLength, + }); +} + +const byte = createFallbackFixedBytesCodec(1); + +${codecs} +`; +} + +// sort molecule types by their dependencies, to make sure the known types can be used in the front +function prepareMolTypes( + types: MolType[], + importedTypes: string[] = [] +): MolType[] { + // check if the molecule definition can be parsed + function checkCanParse(molType: MolType): boolean { + if (availableTypes.has(molType.name)) { + return true; + } + + const layoutType = molType.type; + + switch (layoutType) { + case "array": + case "vector": + case "option": { + if (!availableTypes.has(molType.item)) { + return false; + } + availableTypes.add(molType.name); + return true; + } + + case "struct": + case "table": { + const fieldsAreKnown = molType.fields.every((field) => + availableTypes.has(field.type) + ); + if (!fieldsAreKnown) { + return false; + } + availableTypes.add(molType.name); + return true; + } + + case "union": { + const itemsAreKnown = molType.items.every((item) => + Array.isArray(item) + ? availableTypes.has(item[0]) + : availableTypes.has(item) + ); + if (!itemsAreKnown) { + return false; + } + + availableTypes.add(molType.name); + return true; + } + + default: { + throw new Error(`Unknown molecule layout ${layoutType}`); + } + } + } + + // temp set to store the known types + const availableTypes = new Set(importedTypes.concat("byte")); + const sortedTypes: MolType[] = []; + + const iterator = circularIterator(types); + + // the worst case is that the known types are at the end of the list, + // therefore, the max scan times is the sum of 1 to n + // sigma(n) = n * (n + 1) / 2 + const maxScanTimes = ((1 + types.length) * types.length) / 2; + let scanTimes = 0; + while (iterator.current() != null && scanTimes < maxScanTimes) { + scanTimes++; + + const molType = iterator.current()!; + if (checkCanParse(molType)) { + sortedTypes.push(molType); + availableTypes.add(molType.name); + iterator.removeAndNext(); + continue; + } + + iterator.next(); + } + + if (scanTimes >= maxScanTimes) { + const unknownTypes = types + .filter((type) => !availableTypes.has(type.name)) + .map((type) => type.name) + .join(", "); + + if (unknownTypes) { + throw new Error( + `Circular dependency or unknown type found in ${unknownTypes}` + ); + } + } + + return sortedTypes; +} diff --git a/packages/molecule/tests/codegen.test.ts b/packages/molecule/tests/codegen.test.ts new file mode 100644 index 000000000..570837eac --- /dev/null +++ b/packages/molecule/tests/codegen.test.ts @@ -0,0 +1,166 @@ +import test from "ava"; +import { codegen } from "../src/codegen"; + +function expectGenerated(schema: string, ...expected: string[]) { + const generated = codegen(schema); + + return expected.every((item) => generated.includes(item)); +} + +test("fallback byte related generated", (t) => { + t.true( + expectGenerated( + `array Byte32 [byte; 32];`, + `export const Byte32 = createFallbackFixedBytesCodec(32);` + ) + ); + + t.true( + expectGenerated( + `vector Bytes ;`, + `export const Bytes = fallbackBytesCodec;` + ) + ); +}); + +test("array", (t) => { + t.true( + expectGenerated( + `array Uint8 [byte; 1]; +array RGB [Uint8; 3];`, + `export const Uint8 = createFallbackFixedBytesCodec(1);`, + `export const RGB = array(Uint8, 3);` + ) + ); +}); + +test("vector", (t) => { + t.true( + expectGenerated( + `vector Bytes ; +vector BytesVec ;`, + `export const BytesVec = vector(Bytes);` + ) + ); +}); + +// vector Bytes ; +// option BytesOpt (Bytes); +test("option", (t) => { + t.true( + expectGenerated( + `vector Bytes ; +option BytesOpt (Bytes);`, + `export const BytesOpt = option(Bytes);` + ) + ); +}); + +test("struct", (t) => { + t.true( + expectGenerated( + ` +array Byte32 [byte; 32]; +array Uint32 [byte; 4]; +struct OutPoint { + tx_hash: Byte32, + index: Uint32, +} +`, + ` +export const OutPoint = struct({ + tx_hash: Byte32, + index: Uint32 +}, ['tx_hash', 'index'])` + ) + ); +}); + +test("union", (t) => { + const result1 = expectGenerated( + ` +array Uint8 [byte; 1]; +array Uint16 [byte; 2]; +union Number { + Uint8, + Uint16, +} +`, + `export const Number = union({ + Uint8, + Uint16 +}, ['Uint8', 'Uint16'])` + ); + + t.true(result1); + + const result2 = expectGenerated( + ` +array Uint8 [byte; 1]; +array Uint16 [byte; 2]; +union Number { + Uint8: 8, + Uint16: 16, +}`, + ` +export const Number = union({ + Uint8, + Uint16 +}, {'Uint8': 8, 'Uint16': 16})` + ); + t.true(result2); +}); + +test("table", (t) => { + const generated = codegen(` +array Byte32 [byte; 32]; +vector Bytes ; +array HashType [byte; 1]; + +table Script { + code_hash: Byte32, + hash_type: HashType, + args: Bytes, +} +`); + + t.true( + generated.includes(`export const Byte32 = createFallbackFixedBytesCodec(32); + +export const Bytes = fallbackBytesCodec; + +export const HashType = createFallbackFixedBytesCodec(1); + +export const Script = table({ + code_hash: Byte32, + hash_type: HashType, + args: Bytes +}, ['code_hash', 'hash_type', 'args'])`) + ); +}); + +test("prepend", (t) => { + const generated = codegen( + ` +array Uint8 [byte; 1]; +array Uint16 [byte; 2]; +`, + { + prepend: "import { Uint8 } from './customized'", + } + ); + + t.false(generated.includes(`export const Uint8`)); + t.true(generated.includes(`export const Uint16`)); +}); + +test("should throw when unknown type found", (t) => { + t.throws(() => { + codegen(` +union Something { + Item1, + Item2, +} +`); + }); +}); From 7c3f7a9fd6f31e8ae509d66d203ecf292f17a411 Mon Sep 17 00:00:00 2001 From: homura Date: Thu, 22 Feb 2024 18:53:28 +0800 Subject: [PATCH 4/5] chore: example for molecule codegen --- examples/molecule-codegen/README.md | 32 +++ examples/molecule-codegen/blockchain.mol | 122 +++++++++++ examples/molecule-codegen/customized.ts | 55 +++++ examples/molecule-codegen/generated.ts | 142 ++++++++++++ .../lumos-molecule-codegen.json | 6 + examples/molecule-codegen/main.ts | 7 + examples/molecule-codegen/package.json | 12 + examples/pnpm-lock.yaml | 205 +++++++++--------- 8 files changed, 482 insertions(+), 99 deletions(-) create mode 100644 examples/molecule-codegen/README.md create mode 100644 examples/molecule-codegen/blockchain.mol create mode 100644 examples/molecule-codegen/customized.ts create mode 100644 examples/molecule-codegen/generated.ts create mode 100644 examples/molecule-codegen/lumos-molecule-codegen.json create mode 100644 examples/molecule-codegen/main.ts create mode 100644 examples/molecule-codegen/package.json diff --git a/examples/molecule-codegen/README.md b/examples/molecule-codegen/README.md new file mode 100644 index 000000000..7925c4586 --- /dev/null +++ b/examples/molecule-codegen/README.md @@ -0,0 +1,32 @@ +# Codegen from Molecule Schema + +This example demonstrates how to use the `lumos-molecule-codegen` from the `@ckb-lumos/molecule` package to generate TypeScript code from a Molecule schema. + +The [`blockchain.mol`](blockchain.mol) is a revised version from the original one in the nervosnetwork/ckb repository. It has been modified to make the unpacked `HashType` and `DepType` fields human-readable. + +The difference is shown below: + +```diff ++array HashType [byte;1]; + +table Script { + code_hash: Bytes32; +- hash_type: byte; ++ hash_type: HashType; + args: Bytes; +} +``` + +And we can provide a [`custmoized.ts`](customized.ts) file to override the original `HashType` and `DepType` fields. + +To generate the [`generated.ts`](generated.ts) file from the `blockchain.mol` schema, run the following command: + +```sh +npx lumos-molecule-codegen +``` + +To test whether the generated code works, run the following command: + +```sh +ts-node main.ts +``` diff --git a/examples/molecule-codegen/blockchain.mol b/examples/molecule-codegen/blockchain.mol new file mode 100644 index 000000000..e4d7a33c6 --- /dev/null +++ b/examples/molecule-codegen/blockchain.mol @@ -0,0 +1,122 @@ +/* Basic Types */ + +// The `UintN` is used to store a `N` bits unsigned integer +// as a byte array in little endian. +array Uint32 [byte; 4]; +array Uint64 [byte; 8]; +array Uint128 [byte; 16]; +array Byte32 [byte; 32]; +array Uint256 [byte; 32]; + +vector Bytes ; +option BytesOpt (Bytes); +vector BytesOptVec ; +vector BytesVec ; +vector Byte32Vec ; + +/* Types for Chain */ + +option ScriptOpt (Script); + +array ProposalShortId [byte; 10]; + +vector UncleBlockVec ; +vector TransactionVec ; +vector ProposalShortIdVec ; +vector CellDepVec ; +vector CellInputVec ; +vector CellOutputVec ; + +array HashType [byte; 1]; + +table Script { + code_hash: Byte32, + hash_type: HashType, + args: Bytes, +} + +struct OutPoint { + tx_hash: Byte32, + index: Uint32, +} + +struct CellInput { + since: Uint64, + previous_output: OutPoint, +} + +table CellOutput { + capacity: Uint64, + lock: Script, + type_: ScriptOpt, +} + +array DepType [byte; 1]; + +struct CellDep { + out_point: OutPoint, + dep_type: DepType, +} + +table RawTransaction { + version: Uint32, + cell_deps: CellDepVec, + header_deps: Byte32Vec, + inputs: CellInputVec, + outputs: CellOutputVec, + outputs_data: BytesVec, +} + +table Transaction { + raw: RawTransaction, + witnesses: BytesVec, +} + +struct RawHeader { + version: Uint32, + compact_target: Uint32, + timestamp: Uint64, + number: Uint64, + epoch: Uint64, + parent_hash: Byte32, + transactions_root: Byte32, + proposals_hash: Byte32, + extra_hash: Byte32, + dao: Byte32, +} + +struct Header { + raw: RawHeader, + nonce: Uint128, +} + +table UncleBlock { + header: Header, + proposals: ProposalShortIdVec, +} + +table Block { + header: Header, + uncles: UncleBlockVec, + transactions: TransactionVec, + proposals: ProposalShortIdVec, +} + +table BlockV1 { + header: Header, + uncles: UncleBlockVec, + transactions: TransactionVec, + proposals: ProposalShortIdVec, + extension: Bytes, +} + +table CellbaseWitness { + lock: Script, + message: Bytes, +} + +table WitnessArgs { + lock: BytesOpt, // Lock args + input_type: BytesOpt, // Type args for input + output_type: BytesOpt, // Type args for output +} diff --git a/examples/molecule-codegen/customized.ts b/examples/molecule-codegen/customized.ts new file mode 100644 index 000000000..3b64e359d --- /dev/null +++ b/examples/molecule-codegen/customized.ts @@ -0,0 +1,55 @@ +import { createFixedBytesCodec, number } from "@ckb-lumos/codec"; + +const { Uint32, Uint64, Uint128 } = number; + +/** + *
+ *  0b0000000 0
+ *    ───┬─── │
+ *       │    ▼
+ *       │   type - the last bit indicates locating contract(script) via type hash and runs in the latest version of the CKB-VM
+ *       │
+ *       ▼
+ * data* - the first 7 bits indicate locating contract(script) via code hash and runs in the specified version of the CKB-VM
+ * 
+ * + */ +const HashType = createFixedBytesCodec<"data" | "type" | "data1" | "data2">({ + byteLength: 1, + // prettier-ignore + pack: (hashType) => { + if (hashType === "type") return new Uint8Array([0b0000000_1]); + if (hashType === "data") return new Uint8Array([0b0000000_0]); + if (hashType === "data1") return new Uint8Array([0b0000001_0]); + if (hashType === "data2") return new Uint8Array([0b0000010_0]); + + throw new Error('Unknown hash type') + }, + unpack: (byte) => { + if (byte[0] === 0b0000000_1) return "type"; + if (byte[0] === 0b0000000_0) return "data"; + if (byte[0] === 0b0000001_0) return "data1"; + if (byte[0] === 0b0000010_0) return "data2"; + + throw new Error("Unknown hash type"); + }, +}); + +const DepType = createFixedBytesCodec<"code" | "depGroup">({ + byteLength: 1, + // prettier-ignore + pack: (depType) => { + if (depType === "code") return new Uint8Array([0]); + if (depType === "depGroup") return new Uint8Array([1]); + + throw new Error("Unknown dep type"); + }, + unpack: (byte) => { + if (byte[0] === 0) return "code"; + if (byte[0] === 1) return "depGroup"; + + throw new Error("Unknown dep type"); + }, +}); + +export { Uint32, Uint64, Uint128, DepType, HashType }; diff --git a/examples/molecule-codegen/generated.ts b/examples/molecule-codegen/generated.ts new file mode 100644 index 000000000..cffb4fd3d --- /dev/null +++ b/examples/molecule-codegen/generated.ts @@ -0,0 +1,142 @@ +// This file is generated by @ckb-lumos/molecule, please do not modify it manually. +/* eslint-disable */ + +import { bytes, createBytesCodec, createFixedBytesCodec, molecule } from "@ckb-lumos/codec"; +import { Uint32, Uint64, Uint128, DepType, HashType } from './customized' + +const { array, vector, union, option, struct, table } = molecule; + +const fallbackBytesCodec = createBytesCodec({ + pack: bytes.bytify, + unpack: bytes.hexify, +}); + +function createFallbackFixedBytesCodec(byteLength: number) { + return createFixedBytesCodec({ + pack: bytes.bytify, + unpack: bytes.hexify, + byteLength, + }); +} + +const byte = createFallbackFixedBytesCodec(1); + +export const Byte32 = createFallbackFixedBytesCodec(32); + +export const Uint256 = createFallbackFixedBytesCodec(32); + +export const Bytes = fallbackBytesCodec; + +export const BytesOpt = option(Bytes); + +export const BytesOptVec = vector(BytesOpt); + +export const BytesVec = vector(Bytes); + +export const Byte32Vec = vector(Byte32); + +export const ProposalShortId = createFallbackFixedBytesCodec(10); + +export const ProposalShortIdVec = vector(ProposalShortId); + +export const Script = table({ + codeHash: Byte32, + hashType: HashType, + args: Bytes +}, ['codeHash', 'hashType', 'args']); + +export const OutPoint = struct({ + txHash: Byte32, + index: Uint32 +}, ['txHash', 'index']); + +export const CellInput = struct({ + since: Uint64, + previousOutput: OutPoint +}, ['since', 'previousOutput']); + +export const CellDep = struct({ + outPoint: OutPoint, + depType: DepType +}, ['outPoint', 'depType']); + +export const RawHeader = struct({ + version: Uint32, + compactTarget: Uint32, + timestamp: Uint64, + number: Uint64, + epoch: Uint64, + parentHash: Byte32, + transactionsRoot: Byte32, + proposalsHash: Byte32, + extraHash: Byte32, + dao: Byte32 +}, ['version', 'compactTarget', 'timestamp', 'number', 'epoch', 'parentHash', 'transactionsRoot', 'proposalsHash', 'extraHash', 'dao']); + +export const Header = struct({ + raw: RawHeader, + nonce: Uint128 +}, ['raw', 'nonce']); + +export const UncleBlock = table({ + header: Header, + proposals: ProposalShortIdVec +}, ['header', 'proposals']); + +export const CellbaseWitness = table({ + lock: Script, + message: Bytes +}, ['lock', 'message']); + +export const WitnessArgs = table({ + lock: BytesOpt, + inputType: BytesOpt, + outputType: BytesOpt +}, ['lock', 'inputType', 'outputType']); + +export const ScriptOpt = option(Script); + +export const UncleBlockVec = vector(UncleBlock); + +export const CellDepVec = vector(CellDep); + +export const CellInputVec = vector(CellInput); + +export const CellOutput = table({ + capacity: Uint64, + lock: Script, + type_: ScriptOpt +}, ['capacity', 'lock', 'type_']); + +export const CellOutputVec = vector(CellOutput); + +export const RawTransaction = table({ + version: Uint32, + cellDeps: CellDepVec, + headerDeps: Byte32Vec, + inputs: CellInputVec, + outputs: CellOutputVec, + outputsData: BytesVec +}, ['version', 'cellDeps', 'headerDeps', 'inputs', 'outputs', 'outputsData']); + +export const Transaction = table({ + raw: RawTransaction, + witnesses: BytesVec +}, ['raw', 'witnesses']); + +export const TransactionVec = vector(Transaction); + +export const Block = table({ + header: Header, + uncles: UncleBlockVec, + transactions: TransactionVec, + proposals: ProposalShortIdVec +}, ['header', 'uncles', 'transactions', 'proposals']); + +export const BlockV1 = table({ + header: Header, + uncles: UncleBlockVec, + transactions: TransactionVec, + proposals: ProposalShortIdVec, + extension: Bytes +}, ['header', 'uncles', 'transactions', 'proposals', 'extension']); diff --git a/examples/molecule-codegen/lumos-molecule-codegen.json b/examples/molecule-codegen/lumos-molecule-codegen.json new file mode 100644 index 000000000..b201f7d7b --- /dev/null +++ b/examples/molecule-codegen/lumos-molecule-codegen.json @@ -0,0 +1,6 @@ +{ + "objectKeyFormat": "camelcase", + "prepend": "import { Uint32, Uint64, Uint128, DepType, HashType } from './customized'", + "output": "generated.ts", + "schemaFile": "blockchain.mol" +} diff --git a/examples/molecule-codegen/main.ts b/examples/molecule-codegen/main.ts new file mode 100644 index 000000000..7eb5fdb49 --- /dev/null +++ b/examples/molecule-codegen/main.ts @@ -0,0 +1,7 @@ +import { Script } from "./generated"; + +const packed = Script.pack({ codeHash: new Uint8Array(32), hashType: "type", args: "0xdeadbeef" }); +const unpacked = Script.unpack(packed); + +console.log("packed script", packed); +console.log("unpacked script", unpacked); diff --git a/examples/molecule-codegen/package.json b/examples/molecule-codegen/package.json new file mode 100644 index 000000000..5e26c6532 --- /dev/null +++ b/examples/molecule-codegen/package.json @@ -0,0 +1,12 @@ +{ + "name": "@lumos-example/misc", + "private": true, + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@ckb-lumos/codec": "canary" + }, + "devDependencies": { + "@ckb-lumos/molecule": "canary" + } +} diff --git a/examples/pnpm-lock.yaml b/examples/pnpm-lock.yaml index b62980324..4842c1176 100644 --- a/examples/pnpm-lock.yaml +++ b/examples/pnpm-lock.yaml @@ -39,13 +39,13 @@ importers: ../packages/base: dependencies: '@ckb-lumos/bi': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../bi '@ckb-lumos/codec': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../codec '@ckb-lumos/toolkit': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../toolkit '@types/blake2b': specifier: ^2.1.0 @@ -76,19 +76,19 @@ importers: ../packages/ckb-indexer: dependencies: '@ckb-lumos/base': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../base '@ckb-lumos/bi': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../bi '@ckb-lumos/codec': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../codec '@ckb-lumos/rpc': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../rpc '@ckb-lumos/toolkit': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../toolkit cross-fetch: specifier: ^3.1.5 @@ -98,7 +98,7 @@ importers: version: 3.3.0 devDependencies: '@ckb-lumos/testkit': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../testkit '@types/lodash.uniqby': specifier: ^4.7.7 @@ -125,7 +125,7 @@ importers: ../packages/codec: dependencies: '@ckb-lumos/bi': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../bi devDependencies: escape-string-regexp: @@ -138,25 +138,25 @@ importers: ../packages/common-scripts: dependencies: '@ckb-lumos/base': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../base '@ckb-lumos/bi': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../bi '@ckb-lumos/codec': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../codec '@ckb-lumos/config-manager': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../config-manager '@ckb-lumos/helpers': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../helpers '@ckb-lumos/rpc': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../rpc '@ckb-lumos/toolkit': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../toolkit bech32: specifier: ^2.0.0 @@ -169,10 +169,10 @@ importers: version: 4.3.0 devDependencies: '@ckb-lumos/debugger': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../debugger '@ckb-lumos/hd': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../hd '@types/keccak': specifier: ^3.0.1 @@ -187,16 +187,16 @@ importers: ../packages/config-manager: dependencies: '@ckb-lumos/base': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../base '@ckb-lumos/bi': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../bi '@ckb-lumos/codec': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../codec '@ckb-lumos/rpc': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../rpc '@types/deep-freeze-strict': specifier: ^1.1.0 @@ -208,22 +208,22 @@ importers: ../packages/debugger: dependencies: '@ckb-lumos/base': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../base '@ckb-lumos/bi': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../bi '@ckb-lumos/codec': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../codec '@ckb-lumos/config-manager': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../config-manager '@ckb-lumos/helpers': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../helpers '@ckb-lumos/rpc': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../rpc '@types/download': specifier: ^8.0.1 @@ -245,55 +245,55 @@ importers: version: 3.0.0 devDependencies: '@ckb-lumos/common-scripts': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../common-scripts '@ckb-lumos/experiment-tx-assembler': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../experiment-tx-assembler '@ckb-lumos/hd': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../hd ../packages/e2e-test: dependencies: '@ckb-lumos/base': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../base '@ckb-lumos/bi': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../bi '@ckb-lumos/ckb-indexer': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../ckb-indexer '@ckb-lumos/codec': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../codec '@ckb-lumos/common-scripts': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../common-scripts '@ckb-lumos/config-manager': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../config-manager '@ckb-lumos/hd': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../hd '@ckb-lumos/helpers': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../helpers '@ckb-lumos/light-client': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../light-client '@ckb-lumos/rpc': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../rpc '@ckb-lumos/runner': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../runner '@ckb-lumos/toolkit': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../toolkit '@ckb-lumos/utils': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../utils '@types/kill-port': specifier: ^2.0.0 @@ -306,7 +306,7 @@ importers: version: 2.0.1 devDependencies: '@ckb-lumos/testkit': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../testkit '@types/request': specifier: ^2.48.8 @@ -327,28 +327,28 @@ importers: ../packages/experiment-tx-assembler: dependencies: '@ckb-lumos/base': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../base '@ckb-lumos/bi': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../bi '@ckb-lumos/config-manager': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../config-manager '@ckb-lumos/helpers': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../helpers '@ckb-lumos/toolkit': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../toolkit ../packages/hd: dependencies: '@ckb-lumos/base': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../base '@ckb-lumos/bi': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../bi bn.js: specifier: ^5.1.3 @@ -379,22 +379,22 @@ importers: ../packages/hd-cache: dependencies: '@ckb-lumos/base': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../base '@ckb-lumos/bi': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../bi '@ckb-lumos/ckb-indexer': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../ckb-indexer '@ckb-lumos/config-manager': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../config-manager '@ckb-lumos/hd': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../hd '@ckb-lumos/rpc': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../rpc immutable: specifier: ^4.3.0 @@ -410,19 +410,19 @@ importers: ../packages/helpers: dependencies: '@ckb-lumos/base': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../base '@ckb-lumos/bi': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../bi '@ckb-lumos/codec': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../codec '@ckb-lumos/config-manager': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../config-manager '@ckb-lumos/toolkit': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../toolkit bech32: specifier: ^2.0.0 @@ -434,13 +434,13 @@ importers: ../packages/light-client: dependencies: '@ckb-lumos/base': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../base '@ckb-lumos/ckb-indexer': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../ckb-indexer '@ckb-lumos/rpc': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../rpc cross-fetch: specifier: ^3.1.5 @@ -450,7 +450,7 @@ importers: version: 3.3.0 devDependencies: '@ckb-lumos/testkit': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../testkit '@types/request': specifier: ^2.48.8 @@ -468,49 +468,49 @@ importers: ../packages/lumos: dependencies: '@ckb-lumos/base': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../base '@ckb-lumos/bi': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../bi '@ckb-lumos/ckb-indexer': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../ckb-indexer '@ckb-lumos/codec': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../codec '@ckb-lumos/common-scripts': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../common-scripts '@ckb-lumos/config-manager': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../config-manager '@ckb-lumos/hd': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../hd '@ckb-lumos/helpers': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../helpers '@ckb-lumos/light-client': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../light-client '@ckb-lumos/rpc': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../rpc '@ckb-lumos/toolkit': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../toolkit '@ckb-lumos/transaction-manager': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../transaction-manager ../packages/molecule: dependencies: '@ckb-lumos/bi': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../bi '@ckb-lumos/codec': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../codec '@types/nearley': specifier: ^2.11.2 @@ -523,7 +523,7 @@ importers: version: 2.20.1 devDependencies: '@ckb-lumos/base': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../base jsbi: specifier: ^4.1.0 @@ -532,10 +532,10 @@ importers: ../packages/rpc: dependencies: '@ckb-lumos/base': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../base '@ckb-lumos/bi': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../bi abort-controller: specifier: ^3.0.0 @@ -563,7 +563,7 @@ importers: ../packages/runner: dependencies: '@ckb-lumos/utils': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../utils '@ltd/j-toml': specifier: ^1.38.0 @@ -599,16 +599,16 @@ importers: ../packages/testkit: dependencies: '@ckb-lumos/base': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../base '@ckb-lumos/bi': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../bi '@ckb-lumos/codec': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../codec '@ckb-lumos/rpc': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../rpc '@types/body-parser': specifier: ^1.19.1 @@ -645,7 +645,7 @@ importers: ../packages/toolkit: dependencies: '@ckb-lumos/bi': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../bi devDependencies: '@babel/plugin-proposal-export-namespace-from': @@ -658,19 +658,19 @@ importers: ../packages/transaction-manager: dependencies: '@ckb-lumos/base': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../base '@ckb-lumos/ckb-indexer': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../ckb-indexer '@ckb-lumos/codec': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../codec '@ckb-lumos/rpc': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../rpc '@ckb-lumos/toolkit': - specifier: 0.22.0-next.3 + specifier: 0.22.0-next.4 version: link:../toolkit immutable: specifier: ^4.3.0 @@ -771,6 +771,16 @@ importers: specifier: canary version: link:../../packages/lumos + molecule-codegen: + dependencies: + '@ckb-lumos/codec': + specifier: canary + version: link:../../packages/codec + devDependencies: + '@ckb-lumos/molecule': + specifier: canary + version: link:../../packages/molecule + omni-lock-metamask: dependencies: '@ckb-lumos/lumos': @@ -6683,9 +6693,6 @@ packages: resolution: {integrity: sha512-2GTVocFkwblV/TIg9AmT7TI2fO4xdWkyN8aFUEVtiVNWt96GTR3FgQyHFValfCbcj1k9Xf962Ws2hYXYUr9k1Q==} engines: {node: '>= 12.0.0'} hasBin: true - peerDependenciesMeta: - '@parcel/core': - optional: true dependencies: '@parcel/config-default': 2.9.3(@parcel/core@2.9.3) '@parcel/core': 2.9.3 From 2142ad6b30a5ad1b9eced90acfac87530351661d Mon Sep 17 00:00:00 2001 From: homura Date: Tue, 27 Feb 2024 11:06:06 +0800 Subject: [PATCH 5/5] refactor: the output option is unnecessary in molecule cli --- examples/molecule-codegen/README.md | 4 +-- .../{generated.ts => blockchain.ts} | 0 .../lumos-molecule-codegen.json | 1 - examples/molecule-codegen/main.ts | 2 +- packages/codec/src/molecule/layout.ts | 12 +++++++ packages/molecule/README.md | 6 ++-- packages/molecule/src/cli.ts | 4 +-- packages/molecule/src/codegen.ts | 32 ++++++++++--------- 8 files changed, 35 insertions(+), 26 deletions(-) rename examples/molecule-codegen/{generated.ts => blockchain.ts} (100%) diff --git a/examples/molecule-codegen/README.md b/examples/molecule-codegen/README.md index 7925c4586..9b2b8141b 100644 --- a/examples/molecule-codegen/README.md +++ b/examples/molecule-codegen/README.md @@ -19,10 +19,10 @@ table Script { And we can provide a [`custmoized.ts`](customized.ts) file to override the original `HashType` and `DepType` fields. -To generate the [`generated.ts`](generated.ts) file from the `blockchain.mol` schema, run the following command: +To generate the [`blockchain.ts`](blockchain.ts) file from the `blockchain.mol` schema, run the following command: ```sh -npx lumos-molecule-codegen +npx lumos-molecule-codegen > blockchain.ts ``` To test whether the generated code works, run the following command: diff --git a/examples/molecule-codegen/generated.ts b/examples/molecule-codegen/blockchain.ts similarity index 100% rename from examples/molecule-codegen/generated.ts rename to examples/molecule-codegen/blockchain.ts diff --git a/examples/molecule-codegen/lumos-molecule-codegen.json b/examples/molecule-codegen/lumos-molecule-codegen.json index b201f7d7b..bb1ed450c 100644 --- a/examples/molecule-codegen/lumos-molecule-codegen.json +++ b/examples/molecule-codegen/lumos-molecule-codegen.json @@ -1,6 +1,5 @@ { "objectKeyFormat": "camelcase", "prepend": "import { Uint32, Uint64, Uint128, DepType, HashType } from './customized'", - "output": "generated.ts", "schemaFile": "blockchain.mol" } diff --git a/examples/molecule-codegen/main.ts b/examples/molecule-codegen/main.ts index 7eb5fdb49..f5f74abdc 100644 --- a/examples/molecule-codegen/main.ts +++ b/examples/molecule-codegen/main.ts @@ -1,4 +1,4 @@ -import { Script } from "./generated"; +import { Script } from "./blockchain"; const packed = Script.pack({ codeHash: new Uint8Array(32), hashType: "type", args: "0xdeadbeef" }); const unpacked = Script.unpack(packed); diff --git a/packages/codec/src/molecule/layout.ts b/packages/codec/src/molecule/layout.ts index d207e5949..ff62d93a7 100644 --- a/packages/codec/src/molecule/layout.ts +++ b/packages/codec/src/molecule/layout.ts @@ -41,22 +41,34 @@ type PartialNullable> = & Partial>> & Pick>; +/** + * A codec for struct and table of Molecule + */ export type ObjectLayoutCodec> = BytesCodec< PartialNullable<{ [key in keyof T]: UnpackResult }>, PartialNullable<{ [key in keyof T]: PackParam }> >; +/** + * A codec for option of Molecule + */ export interface OptionLayoutCodec extends BytesCodec | undefined> { pack: (packable?: PackParam) => Uint8Array; } +/** + * A code for array and vector of Molecule + */ export type ArrayLayoutCodec = BytesCodec< Array>, Array> >; +/** + * A molecule codec for ` + */ export type UnionLayoutCodec> = BytesCodec< { [key in keyof T]: { type: key; value: UnpackResult } }[keyof T], { [key in keyof T]: { type: key; value: PackParam } }[keyof T] diff --git a/packages/molecule/README.md b/packages/molecule/README.md index 089289e6c..2dcd3a5e6 100644 --- a/packages/molecule/README.md +++ b/packages/molecule/README.md @@ -32,15 +32,13 @@ Then you can create a `lumos-molecule-codegen.json` file to configure the codege objectKeyFormat: "camelcase", // prepend the import statement to custom and override the generated codec prepend: "import { Uint32, Uint64, Uint128 } from './customized'", - // the output file name - output: "generated.ts", // the input schema file schemaFile: "blockchain.mol", } ``` -Finally, run the following command to generate code +Finally, run the following command to generate code to write to `generated.ts`. ```sh -npx lumos-molecule-codegen +npx lumos-molecule-codegen > generated.ts ``` diff --git a/packages/molecule/src/cli.ts b/packages/molecule/src/cli.ts index 627c6276a..e3f798a8e 100644 --- a/packages/molecule/src/cli.ts +++ b/packages/molecule/src/cli.ts @@ -11,7 +11,6 @@ function camelcase(str: string): string { type Config = { objectKeyFormat: "camelcase" | "keep"; prepend: string; - output: string; schemaFile: string; }; @@ -25,7 +24,6 @@ const fileConfig: Partial = (() => { const config: Config = { objectKeyFormat: fileConfig.objectKeyFormat || "keep", prepend: fileConfig.prepend || "", - output: fileConfig.output || "generated.ts", schemaFile: fileConfig.schemaFile || "schema.mol", }; @@ -43,4 +41,4 @@ const generated = codegen(fs.readFileSync(config.schemaFile, "utf-8"), { config.objectKeyFormat === "camelcase" ? camelcase : undefined, }); -fs.writeFileSync(config.output, generated); +console.log(generated); diff --git a/packages/molecule/src/codegen.ts b/packages/molecule/src/codegen.ts index d56cfcbda..86d55b564 100644 --- a/packages/molecule/src/codegen.ts +++ b/packages/molecule/src/codegen.ts @@ -18,25 +18,27 @@ function id(value: T): T { return value; } +export function scanCustomizedTypes(prepend: string): string[] { + if (!prepend) return []; + + const matched = prepend.match(/(?<={)([^}]+)(?=})/g); + if (!matched) return []; + + // parse the override import statements to get the items + return matched.flatMap((item) => + item + .split(",") + .map((x) => x.trim()) + .filter(Boolean) + ); +} + export function codegen(schema: string, options: Options = {}): string { const parser = new NearleyParser(NearleyGrammar.fromCompiled(grammar)); parser.feed(schema); // the items that don't need to be generated - const importedModules: string[] = (() => { - if (!options.prepend) return []; - - // parse the override import statements to get the items - const importedCodecs = options.prepend - .match(/(?<={)([^}]+)(?=})/g) - ?.flatMap((item) => - item - .split(",") - .map((x) => x.trim()) - .filter(Boolean) - ); - return importedCodecs || []; - })(); + const importedModules: string[] = scanCustomizedTypes(options.prepend || ""); const molTypes = prepareMolTypes( parser.results[0].filter(Boolean), @@ -124,7 +126,7 @@ export function codegen(schema: string, options: Options = {}): string { /* eslint-disable */ import { bytes, createBytesCodec, createFixedBytesCodec, molecule } from "@ckb-lumos/codec"; -${options.prepend || "\b"} +${options.prepend || ""} const { array, vector, union, option, struct, table } = molecule;