diff --git a/examples/molecule-codegen-dir/customized.ts b/examples/molecule-codegen-dir/customized.ts
new file mode 100644
index 000000000..3b64e359d
--- /dev/null
+++ b/examples/molecule-codegen-dir/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-dir/generated/common/basic_types.ts b/examples/molecule-codegen-dir/generated/common/basic_types.ts
new file mode 100644
index 000000000..19269563c
--- /dev/null
+++ b/examples/molecule-codegen-dir/generated/common/basic_types.ts
@@ -0,0 +1,29 @@
+// 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 AttrValue = createFallbackFixedBytesCodec(1);
+
+export const SkillLevel = createFallbackFixedBytesCodec(1);
+
+export const Uint8 = createFallbackFixedBytesCodec(1);
+
+export const Uint16 = createFallbackFixedBytesCodec(2);
diff --git a/examples/molecule-codegen-dir/generated/skills.ts b/examples/molecule-codegen-dir/generated/skills.ts
new file mode 100644
index 000000000..60c92b0c7
--- /dev/null
+++ b/examples/molecule-codegen-dir/generated/skills.ts
@@ -0,0 +1,57 @@
+// 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'
+import { AttrValue, SkillLevel, Uint8, Uint16 } from './common/basic_types'
+
+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 ArmorLight = option(SkillLevel);
+
+export const ArmorHeavy = option(SkillLevel);
+
+export const ArmorShields = option(SkillLevel);
+
+export const WeaponSwords = option(SkillLevel);
+
+export const WeaponBows = option(SkillLevel);
+
+export const WeaponBlunt = option(SkillLevel);
+
+export const Dodge = option(SkillLevel);
+
+export const PickLocks = option(SkillLevel);
+
+export const Mercantile = option(SkillLevel);
+
+export const Survival = option(SkillLevel);
+
+export const Skill = union({
+ ArmorLight,
+ ArmorHeavy,
+ ArmorShields,
+ WeaponSwords,
+ WeaponBows,
+ WeaponBlunt,
+ Dodge,
+ PickLocks,
+ Mercantile,
+ Survival
+}, ['ArmorLight', 'ArmorHeavy', 'ArmorShields', 'WeaponSwords', 'WeaponBows', 'WeaponBlunt', 'Dodge', 'PickLocks', 'Mercantile', 'Survival']);
+
+export const Skills = vector(Skill);
diff --git a/examples/molecule-codegen-dir/lumos-molecule-codegen.json b/examples/molecule-codegen-dir/lumos-molecule-codegen.json
new file mode 100644
index 000000000..1115d2ba0
--- /dev/null
+++ b/examples/molecule-codegen-dir/lumos-molecule-codegen.json
@@ -0,0 +1,6 @@
+{
+ "objectKeyFormat": "camelcase",
+ "prepend": "import { Uint32, Uint64, Uint128, DepType, HashType } from './customized'",
+ "schemaDir": "schemas",
+ "outDir": "generated"
+}
diff --git a/examples/molecule-codegen-dir/package.json b/examples/molecule-codegen-dir/package.json
new file mode 100644
index 000000000..43148a372
--- /dev/null
+++ b/examples/molecule-codegen-dir/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@lumos-example/molecule-codegen-dir",
+ "private": true,
+ "version": "1.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "@ckb-lumos/codec": "canary"
+ },
+ "devDependencies": {
+ "@ckb-lumos/molecule": "canary"
+ }
+}
diff --git a/examples/molecule-codegen-dir/schemas/common/basic_types.mol b/examples/molecule-codegen-dir/schemas/common/basic_types.mol
new file mode 100644
index 000000000..a11426483
--- /dev/null
+++ b/examples/molecule-codegen-dir/schemas/common/basic_types.mol
@@ -0,0 +1,26 @@
+// https://github.com/nervosnetwork/molecule/blob/master/docs/schemas/common/basic_types.mol
+// AttrValue is an alias of `byte`.
+//
+// Since Molecule data are strongly-typed, it can gives compile time guarantees
+// that the right type of value is supplied to a method.
+//
+// In this example, we use this alias to define an unsigned integer which
+// has an upper limit: 100.
+// So it's easy to distinguish between this type and a real `byte`.
+// Of course, the serialization wouldn't do any checks for this upper limit
+// automatically. You have to implement it by yourself.
+//
+// **NOTE**:
+// - This feature is dependent on the exact implementation.
+// In official Rust generated code, we use new type to implement this feature.
+array AttrValue [byte; 1];
+
+// SkillLevel is an alias of `byte`, too.
+//
+// Each skill has only 10 levels, so we use another alias of `byte` to distinguish.
+array SkillLevel [byte; 1];
+
+// Define several unsigned integers.
+array Uint8 [byte; 1];
+array Uint16 [byte; 2];
+array Uint32 [byte; 4];
diff --git a/examples/molecule-codegen-dir/schemas/skills.mol b/examples/molecule-codegen-dir/schemas/skills.mol
new file mode 100644
index 000000000..04488d333
--- /dev/null
+++ b/examples/molecule-codegen-dir/schemas/skills.mol
@@ -0,0 +1,33 @@
+// https://github.com/nervosnetwork/molecule/blob/master/docs/schemas/skills.mol
+import common/basic_types;
+
+// We define several skills.
+// None means the role can learn a skill but he/she doesn't learn it.
+option ArmorLight (SkillLevel);
+option ArmorHeavy (SkillLevel); // only Fighter can learn this
+option ArmorShields (SkillLevel); // only Fighter can learn this
+option WeaponSwords (SkillLevel); // only Mage can't learn this
+option WeaponBows (SkillLevel); // only Ranger can learn this
+option WeaponBlunt (SkillLevel);
+option Dodge (SkillLevel);
+option PickLocks (SkillLevel);
+option Mercantile (SkillLevel);
+option Survival (SkillLevel);
+// ... omit other skills ...
+
+// Any skill which is defined above.
+union Skill {
+ ArmorLight,
+ ArmorHeavy,
+ ArmorShields,
+ WeaponSwords,
+ WeaponBows,
+ WeaponBlunt,
+ Dodge,
+ PickLocks,
+ Mercantile,
+ Survival,
+}
+
+// A hero can learn several skills. The size of learned skills is dynamic.
+vector Skills ;
diff --git a/examples/molecule-codegen/package.json b/examples/molecule-codegen/package.json
index 5e26c6532..2ed6a43c7 100644
--- a/examples/molecule-codegen/package.json
+++ b/examples/molecule-codegen/package.json
@@ -1,5 +1,5 @@
{
- "name": "@lumos-example/misc",
+ "name": "@lumos-example/molecule-codegen",
"private": true,
"version": "1.0.0",
"license": "MIT",
diff --git a/examples/pnpm-lock.yaml b/examples/pnpm-lock.yaml
index 4842c1176..fbafbc039 100644
--- a/examples/pnpm-lock.yaml
+++ b/examples/pnpm-lock.yaml
@@ -512,15 +512,27 @@ importers:
'@ckb-lumos/codec':
specifier: 0.22.0-next.4
version: link:../codec
+ '@types/moo':
+ specifier: ^0.5.9
+ version: 0.5.9
'@types/nearley':
specifier: ^2.11.2
version: 2.11.2
+ glob:
+ specifier: ^10.3.10
+ version: 10.3.10
+ minimist:
+ specifier: ^1.2.8
+ version: 1.2.8
moo:
specifier: ^0.5.1
version: 0.5.1
nearley:
specifier: ^2.20.1
version: 2.20.1
+ relative:
+ specifier: ^3.0.2
+ version: 3.0.2
devDependencies:
'@ckb-lumos/base':
specifier: 0.22.0-next.4
@@ -781,6 +793,16 @@ importers:
specifier: canary
version: link:../../packages/molecule
+ molecule-codegen-dir:
+ 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':
@@ -1273,6 +1295,18 @@ packages:
resolution: {integrity: sha512-pWfNQaCwmFFGkbQMVHpDeMZJD7LThPfxqZJyZg0PRke6P6pwHKx2fZoXXEryqmwA1RwsvQ99S/FesQMLmcEVfw==}
dev: false
+ /@isaacs/cliui@8.0.2:
+ resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
+ engines: {node: '>=12'}
+ dependencies:
+ string-width: 5.1.2
+ string-width-cjs: /string-width@4.2.3
+ strip-ansi: 7.1.0
+ strip-ansi-cjs: /strip-ansi@6.0.1
+ wrap-ansi: 8.1.0
+ wrap-ansi-cjs: /wrap-ansi@7.0.0
+ dev: false
+
/@istanbuljs/load-nyc-config@1.1.0:
resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}
engines: {node: '>=8'}
@@ -3024,6 +3058,13 @@ packages:
nullthrows: 1.1.1
dev: true
+ /@pkgjs/parseargs@0.11.0:
+ resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
+ engines: {node: '>=14'}
+ requiresBuild: true
+ dev: false
+ optional: true
+
/@sinclair/typebox@0.24.51:
resolution: {integrity: sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==}
dev: true
@@ -3413,6 +3454,10 @@ packages:
resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==}
dev: true
+ /@types/moo@0.5.9:
+ resolution: {integrity: sha512-ZsFVecFi66jGQ6L41TonEaBhsIVeVftTz6iQKWTctzacHhzYHWvv9S0IyAJi4BhN7vb9qCQ3+kpStP2vbZqmDg==}
+ dev: false
+
/@types/nearley@2.11.2:
resolution: {integrity: sha512-jeyIDNBxxyWyEk6HemDC+t32b4fxthVsgWDxf88qD2WpX0QpOyY8JvA80AElPTBGRUsO0EKnr6OeVOjK3ZDdnA==}
dev: false
@@ -3611,7 +3656,11 @@ packages:
/ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
- dev: true
+
+ /ansi-regex@6.0.1:
+ resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
+ engines: {node: '>=12'}
+ dev: false
/ansi-styles@3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
@@ -3625,13 +3674,17 @@ packages:
engines: {node: '>=8'}
dependencies:
color-convert: 2.0.1
- dev: true
/ansi-styles@5.2.0:
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
engines: {node: '>=10'}
dev: true
+ /ansi-styles@6.2.1:
+ resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
+ engines: {node: '>=12'}
+ dev: false
+
/anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
@@ -3925,6 +3978,12 @@ packages:
balanced-match: 1.0.2
concat-map: 0.0.1
+ /brace-expansion@2.0.1:
+ resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
+ dependencies:
+ balanced-match: 1.0.2
+ dev: false
+
/braces@3.0.2:
resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
engines: {node: '>=8'}
@@ -4209,7 +4268,6 @@ packages:
engines: {node: '>=7.0.0'}
dependencies:
color-name: 1.1.4
- dev: true
/color-name@1.1.3:
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
@@ -4217,7 +4275,6 @@ packages:
/color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
- dev: true
/combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
@@ -4334,7 +4391,6 @@ packages:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
- dev: true
/crypto-browserify@3.12.0:
resolution: {integrity: sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==}
@@ -4627,6 +4683,10 @@ packages:
resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==}
dev: false
+ /eastasianwidth@0.2.0:
+ resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
+ dev: false
+
/ecc-jsbn@0.1.2:
resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==}
dependencies:
@@ -4675,7 +4735,10 @@ packages:
/emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
- dev: true
+
+ /emoji-regex@9.2.2:
+ resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+ dev: false
/encodeurl@1.0.2:
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
@@ -4975,6 +5038,14 @@ packages:
path-exists: 4.0.0
dev: true
+ /foreground-child@3.1.1:
+ resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==}
+ engines: {node: '>=14'}
+ dependencies:
+ cross-spawn: 7.0.3
+ signal-exit: 4.1.0
+ dev: false
+
/forever-agent@0.6.1:
resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==}
dev: true
@@ -5093,6 +5164,18 @@ packages:
assert-plus: 1.0.0
dev: true
+ /glob@10.3.10:
+ resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ hasBin: true
+ dependencies:
+ foreground-child: 3.1.1
+ jackspeak: 2.3.6
+ minimatch: 9.0.3
+ minipass: 7.0.4
+ path-scurry: 1.10.1
+ dev: false
+
/glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
dependencies:
@@ -5411,7 +5494,6 @@ packages:
/is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
- dev: true
/is-generator-fn@2.1.0:
resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==}
@@ -5476,7 +5558,13 @@ packages:
/isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
- dev: true
+
+ /isobject@2.1.0:
+ resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ isarray: 1.0.0
+ dev: false
/isstream@0.1.2:
resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==}
@@ -5536,6 +5624,15 @@ packages:
is-object: 1.0.2
dev: false
+ /jackspeak@2.3.6:
+ resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==}
+ engines: {node: '>=14'}
+ dependencies:
+ '@isaacs/cliui': 8.0.2
+ optionalDependencies:
+ '@pkgjs/parseargs': 0.11.0
+ dev: false
+
/jest-changed-files@28.1.3:
resolution: {integrity: sha512-esaOfUWJXk2nfZt9SPyC8gA1kNfdKLkQWyzsMlqq8msYSlNKfmZxfRgZn4Cd4MGVUF+7v6dBs0d5TOAKa7iIiA==}
engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0}
@@ -6283,6 +6380,11 @@ packages:
engines: {node: '>=0.10.0'}
dev: false
+ /lru-cache@10.2.0:
+ resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==}
+ engines: {node: 14 || >=16.14}
+ dev: false
+
/lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
dependencies:
@@ -6402,6 +6504,22 @@ packages:
dependencies:
brace-expansion: 1.1.11
+ /minimatch@9.0.3:
+ resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ dependencies:
+ brace-expansion: 2.0.1
+ dev: false
+
+ /minimist@1.2.8:
+ resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+ dev: false
+
+ /minipass@7.0.4:
+ resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ dev: false
+
/moo@0.5.1:
resolution: {integrity: sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w==}
dev: false
@@ -6766,11 +6884,18 @@ packages:
/path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
- dev: true
/path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+ /path-scurry@1.10.1:
+ resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ dependencies:
+ lru-cache: 10.2.0
+ minipass: 7.0.4
+ dev: false
+
/path-to-regexp@0.1.7:
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
@@ -7141,6 +7266,13 @@ packages:
/regenerator-runtime@0.13.11:
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
+ /relative@3.0.2:
+ resolution: {integrity: sha512-Q5W2qeYtY9GbiR8z1yHNZ1DGhyjb4AnLEjt8iE6XfcC1QIu+FAtj3HQaO0wH28H1mX6cqNLvAqWhP402dxJGyA==}
+ engines: {node: '>= 0.8.0'}
+ dependencies:
+ isobject: 2.1.0
+ dev: false
+
/request@2.88.2:
resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==}
engines: {node: '>= 6'}
@@ -7364,12 +7496,10 @@ packages:
engines: {node: '>=8'}
dependencies:
shebang-regex: 3.0.0
- dev: true
/shebang-regex@3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
- dev: true
/shell-exec@1.0.2:
resolution: {integrity: sha512-jyVd+kU2X+mWKMmGhx4fpWbPsjvD53k9ivqetutVW/BQ+WIZoDoP4d8vUMGezV6saZsiNoW2f9GIhg9Dondohg==}
@@ -7400,6 +7530,11 @@ packages:
/signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
+ /signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+ dev: false
+
/sinon@15.0.4:
resolution: {integrity: sha512-uzmfN6zx3GQaria1kwgWGeKiXSSbShBbue6Dcj0SI8fiCNFbiUDqKl57WFlY5lyhxZVUKmXvzgG2pilRQCBwWg==}
dependencies:
@@ -7572,7 +7707,15 @@ packages:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
- dev: true
+
+ /string-width@5.1.2:
+ resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
+ engines: {node: '>=12'}
+ dependencies:
+ eastasianwidth: 0.2.0
+ emoji-regex: 9.2.2
+ strip-ansi: 7.1.0
+ dev: false
/string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
@@ -7591,7 +7734,13 @@ packages:
engines: {node: '>=8'}
dependencies:
ansi-regex: 5.0.1
- dev: true
+
+ /strip-ansi@7.1.0:
+ resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ ansi-regex: 6.0.1
+ dev: false
/strip-bom@4.0.0:
resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==}
@@ -7972,7 +8121,6 @@ packages:
hasBin: true
dependencies:
isexe: 2.0.0
- dev: true
/wif@2.0.6:
resolution: {integrity: sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ==}
@@ -7987,7 +8135,15 @@ packages:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
- dev: true
+
+ /wrap-ansi@8.1.0:
+ resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ ansi-styles: 6.2.1
+ string-width: 5.1.2
+ strip-ansi: 7.1.0
+ dev: false
/wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
diff --git a/packages/molecule/README.md b/packages/molecule/README.md
index 2dcd3a5e6..f801b0a5e 100644
--- a/packages/molecule/README.md
+++ b/packages/molecule/README.md
@@ -25,6 +25,10 @@ npm install @ckb-lumos/codec
Then you can create a `lumos-molecule-codegen.json` file to configure the codegen.
+> Note: if you find the npx lumos-molecule-codegen command is not found, please try to replace npx with `node_modules/.bin/lumos-molecule-codegen`.
+
+### Single File
+
```json5
// lumos-molecule-codegen.json
{
@@ -42,3 +46,33 @@ Finally, run the following command to generate code to write to `generated.ts`.
```sh
npx lumos-molecule-codegen > generated.ts
```
+
+### Codegen Directory
+
+To generate all the molecule files in a directory that match the pattern of `**/*.mol`, you can use the following configuration.
+
+```json5
+{
+ // keep | camelcase
+ objectKeyFormat: "camelcase",
+ // prepend the import statement to custom and override the generated codec
+ // to make the relative import work, you need to run the command in the same directory as the `customized` directory
+ prepend: "import { Uint32, Uint64, Uint128 } from './customized'",
+ // the input schema directory, all **/*.mol in the directory will be processed
+ schemaDir: "schemas",
+ outputDir: "generated",
+}
+```
+
+Then run the following command to generate code to the `generated` directory.
+
+```sh
+npx lumos-molecule-codegen
+```
+
+## Known Issues
+
+The parser inside `@ckb-lumos/molecule` is based on the [EBNF](https://github.com/nervosnetwork/molecule/blob/37748b1124181a3260a0668693c43c8d38c98723/docs/grammar/grammar.ebnf), but the Rust implementation is based on the [Pest](https://github.com/nervosnetwork/molecule/blob/37748b1124181a3260a0668693c43c8d38c98723/tools/codegen/src/grammar.pest), there are some differences between them, such as
+
+- comment starts with `#`
+- comment defined in the `struct`, `table`, or `union` is not supported well
diff --git a/packages/molecule/package.json b/packages/molecule/package.json
index 0b4e58ffd..692f4408e 100644
--- a/packages/molecule/package.json
+++ b/packages/molecule/package.json
@@ -50,9 +50,13 @@
"dependencies": {
"@ckb-lumos/bi": "0.22.0-next.4",
"@ckb-lumos/codec": "0.22.0-next.4",
+ "@types/moo": "^0.5.9",
"@types/nearley": "^2.11.2",
+ "glob": "^10.3.10",
+ "minimist": "^1.2.8",
"moo": "^0.5.1",
- "nearley": "^2.20.1"
+ "nearley": "^2.20.1",
+ "relative": "^3.0.2"
},
"devDependencies": {
"@ckb-lumos/base": "0.22.0-next.4",
diff --git a/packages/molecule/src/cli.ts b/packages/molecule/src/cli.ts
index e3f798a8e..9062659b0 100644
--- a/packages/molecule/src/cli.ts
+++ b/packages/molecule/src/cli.ts
@@ -1,6 +1,11 @@
#!/usr/bin/env node
-import { codegen } from "./codegen";
+import { codegen, codegenProject, ProjectSchemaOptions } from "./codegen";
import * as fs from "node:fs";
+import * as path from "node:path";
+import { globSync } from "glob";
+// eslint-disable-next-line
+// @ts-ignore
+import relative from "relative";
const DEFAULT_CONFIG_FILE_NAME = "lumos-molecule-codegen.json";
@@ -8,10 +13,22 @@ function camelcase(str: string): string {
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
}
+function writeFile(file: string, contents: string) {
+ if (!fs.existsSync(path.dirname(file))) {
+ fs.mkdirSync(path.dirname(file), { recursive: true });
+ }
+
+ fs.writeFileSync(file, contents, "utf-8");
+}
+
type Config = {
objectKeyFormat: "camelcase" | "keep";
+ // ES6 import statements, to override the
prepend: string;
- schemaFile: string;
+
+ schemaFile?: string;
+ schemaDir?: string;
+ outDir?: string;
};
const fileConfig: Partial = (() => {
@@ -24,21 +41,83 @@ const fileConfig: Partial = (() => {
const config: Config = {
objectKeyFormat: fileConfig.objectKeyFormat || "keep",
prepend: fileConfig.prepend || "",
- schemaFile: fileConfig.schemaFile || "schema.mol",
+ schemaFile: fileConfig.schemaFile,
+ schemaDir: fileConfig.schemaDir,
+ outDir: fileConfig.outDir,
};
-// 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"), {
+const codegenOption = {
prepend: config.prepend,
formatObjectKeys:
config.objectKeyFormat === "camelcase" ? camelcase : undefined,
-});
+};
+
+if (config.schemaFile) {
+ outputSingleToConsole(config.schemaFile);
+} else if (config.outDir && config.schemaDir) {
+ buildProject(config.schemaDir, config.outDir);
+} else {
+ throw new Error(
+ `Invalid configuration. Please provide either schemaFile or schemaDir and outDir`
+ );
+}
+
+function buildProject(schemaDir: string, outDir: string) {
+ // https://stackoverflow.com/a/69867053
+ // $1 imported variables
+ // $2 quotes used for the import
+ // $3 import path
+
+ const importStatementRegex =
+ // eslint-disable-next-line no-useless-escape
+ /import([ \n\t]*(?:[^ \n\t\{\}]+[ \n\t]*,?)?(?:[ \n\t]*\{(?:[ \n\t]*[^ \n\t"'\{\}]+[ \n\t]*,?)+\})?[ \n\t]*)from[ \n\t]*(['"])([^'"\n]+)(?:['"])/;
-console.log(generated);
+ // get the parsed generated typescript path for a given mol file
+ function getOutputPath(molPath: string): string {
+ const outputRelativePath = molPath
+ .replace(schemaDir, "") // do not include the schemaDir in the output path
+ .replace(/.mol$/, ".ts"); // replace the file extension
+
+ return path.join(outDir, outputRelativePath);
+ }
+
+ // remove the trailing slash for both windows and *nix
+ schemaDir = schemaDir.replace(/[\\/]+$/, "");
+
+ const namedSchemas = globSync(
+ path.join(schemaDir, "**/*.mol")
+ ).map((molPath) => {
+ const prepend = (() => {
+ const match = config.prepend.match(importStatementRegex);
+ if (!match) return "";
+
+ const [_, importVariables, quote, importPath] = match;
+
+ if (!importPath.startsWith(".")) {
+ return `import ${importVariables} from ${quote}${importPath}${quote}`;
+ }
+ const relativePath = relative(getOutputPath(molPath), importPath);
+ return `import ${importVariables} from ${quote}${relativePath}${quote}`;
+ })();
+
+ return {
+ path: molPath.replace(schemaDir, ""),
+ content: fs.readFileSync(molPath, "utf-8"),
+ formatObjectKeys: codegenOption.formatObjectKeys,
+ prepend: prepend,
+ };
+ });
+
+ codegenProject(namedSchemas).forEach(({ path: molPath, content }) =>
+ writeFile(getOutputPath(molPath), content)
+ );
+}
+
+function outputSingleToConsole(schemaFile: string) {
+ const generated = codegen(
+ fs.readFileSync(schemaFile, "utf-8"),
+ codegenOption
+ );
+
+ console.log(generated);
+}
diff --git a/packages/molecule/src/codegen.ts b/packages/molecule/src/codegen.ts
index 86d55b564..c6f440b31 100644
--- a/packages/molecule/src/codegen.ts
+++ b/packages/molecule/src/codegen.ts
@@ -1,9 +1,9 @@
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");
+import grammar from "./grammar/mol";
+import path from "node:path";
+import { topologySort } from "./topologySort";
export type Options = {
/**
@@ -33,7 +33,67 @@ export function scanCustomizedTypes(prepend: string): string[] {
);
}
-export function codegen(schema: string, options: Options = {}): string {
+export type ResolveResult = {
+ code: string;
+ // imported schema files
+ importSchemas: string[];
+};
+
+/**
+ * resolve the import statement from a molecule schema and erase them for continuing codegen
+ * @param inputCode
+ */
+export function resolveAndEraseImports(inputCode: string): ResolveResult {
+ // https://github.com/nervosnetwork/molecule/blob/37748b1124181a3260a0668693c43c8d38c98723/docs/grammar/grammar.ebnf#L70
+ const importRegex = /^import\s+([^;]+);/;
+ const imports: string[] = [];
+ const lines = inputCode.split("\n");
+ const updatedLines: string[] = [];
+ let blockComment = false;
+
+ // fill the imports and erase the import statements from the code
+ for (const line of lines) {
+ const trimmedLine = line.trim();
+
+ // https://github.com/nervosnetwork/molecule/blob/master/docs/schema_language.md#comments
+ if (trimmedLine.startsWith("/*")) {
+ blockComment = true;
+ }
+
+ if (trimmedLine.endsWith("*/")) {
+ blockComment = false;
+ updatedLines.push(line);
+ continue;
+ }
+
+ if (blockComment) {
+ updatedLines.push(line);
+ continue;
+ }
+
+ const match = trimmedLine.match(importRegex);
+ if (match) {
+ imports.push(match[1]);
+ } else {
+ updatedLines.push(line);
+ }
+ }
+
+ const code = updatedLines.join("\n");
+ return { code, importSchemas: imports };
+}
+
+export type CodegenResult = {
+ code: string;
+ elements: string[];
+};
+
+/**
+ * generate TypeScript code from molecule schema
+ * @param schema
+ * @param options
+ */
+export function codegen(schema: string, options: Options = {}): CodegenResult {
const parser = new NearleyParser(NearleyGrammar.fromCompiled(grammar));
parser.feed(schema);
@@ -45,10 +105,14 @@ export function codegen(schema: string, options: Options = {}): string {
importedModules
);
+ const elements: string[] = [];
+
const codecs = molTypes
.map((molType) => {
if (importedModules.includes(molType.name)) return "";
+ elements.push(molType.name);
+
if (molType.type === "array") {
if (molType.item === "byte") {
return `export const ${molType.name} = createFallbackFixedBytesCodec(${molType.item_count});`;
@@ -122,9 +186,15 @@ export function codegen(schema: string, options: Options = {}): string {
.filter(Boolean)
.join("\n\n");
- return `// This file is generated by @ckb-lumos/molecule, please do not modify it manually.
-/* eslint-disable */
+ // the JS multiple-line comment in string could break some renderer,
+ // such as the editor of GitHub and WebStorm,
+ // so the header is extracted separately to avoid error rendering
+ const header = [
+ "// This file is generated by @ckb-lumos/molecule, please do not modify it manually.",
+ "/* eslint-disable */",
+ ].join("\n");
+ const code = `${header}
import { bytes, createBytesCodec, createFixedBytesCodec, molecule } from "@ckb-lumos/codec";
${options.prepend || ""}
@@ -147,6 +217,89 @@ const byte = createFallbackFixedBytesCodec(1);
${codecs}
`;
+
+ return {
+ code,
+ elements,
+ };
+}
+
+export type ProjectSchemaOptions = Options & {
+ // a path to the schema file that ends with `.mol`,
+ // it could be a virtual path instead of a physical path,
+ // for example, a file located at `schemas/a.mol` could be represented as `a.mol`
+ path: string;
+ // schema content
+ content: string;
+};
+
+/**
+ * build multiple schemas
+ * @param schemas
+ */
+export function codegenProject(
+ schemas: ProjectSchemaOptions[]
+): ProjectSchemaOptions[] {
+ const resolvedSchemas = schemas.map<
+ ResolveResult & { path: string } & Options
+ >(({ content, path, prepend, formatObjectKeys }) => ({
+ ...resolveAndEraseImports(content),
+ path,
+ prepend,
+ formatObjectKeys,
+ }));
+
+ // resolve a relative import path in molecule to a virtual absolute path
+ const resolveImportPath = (
+ importFromFile: string,
+ relativeImportPath: string
+ ) => path.join(path.dirname(importFromFile), relativeImportPath + ".mol");
+
+ const sortedGraph = topologySort(resolvedSchemas, (item) => ({
+ id: item.path,
+ dependencies: item.importSchemas.map((relativeImportPath) =>
+ resolveImportPath(item.path, relativeImportPath)
+ ),
+ }));
+
+ const tsImportMap: Record<
+ string /* virtual absolute path */,
+ string[] /* exported elements */
+ > = {};
+
+ const result = sortedGraph.map((option) => {
+ const tsImportClauses = option.importSchemas.map((relativeImportPath) => {
+ const virtualAbsolutePath = resolveImportPath(
+ option.path,
+ relativeImportPath
+ );
+ const exportedElements = tsImportMap[virtualAbsolutePath];
+ if (!exportedElements) {
+ throw new Error("Import not found at " + option.path);
+ }
+
+ const elements = exportedElements.join(", ");
+ const importPath = relativeImportPath.startsWith(".")
+ ? relativeImportPath
+ : "./" + relativeImportPath;
+ return `import { ${elements} } from '${importPath}'`;
+ });
+
+ const prepend = [option.prepend || "", ...tsImportClauses]
+ .filter(Boolean)
+ .join("\n");
+ const message = codegen(option.code, {
+ formatObjectKeys: option.formatObjectKeys,
+ prepend,
+ });
+
+ const { code, elements } = message;
+ tsImportMap[option.path] = elements;
+
+ return { path: option.path, content: code };
+ });
+
+ return result;
}
// sort molecule types by their dependencies, to make sure the known types can be used in the front
diff --git a/packages/molecule/src/custom.d.ts b/packages/molecule/src/custom.d.ts
new file mode 100644
index 000000000..e59cc1a34
--- /dev/null
+++ b/packages/molecule/src/custom.d.ts
@@ -0,0 +1,3 @@
+declare module "relative" {
+ export default function (from: string, to: string): string;
+}
diff --git a/packages/molecule/src/grammar/mol.d.ts b/packages/molecule/src/grammar/mol.d.ts
new file mode 100644
index 000000000..62c179283
--- /dev/null
+++ b/packages/molecule/src/grammar/mol.d.ts
@@ -0,0 +1,5 @@
+import { CompiledRules } from "nearley";
+
+declare const grammer: CompiledRules;
+
+export default grammer;
diff --git a/packages/molecule/src/topologySort.ts b/packages/molecule/src/topologySort.ts
new file mode 100644
index 000000000..0579a3f66
--- /dev/null
+++ b/packages/molecule/src/topologySort.ts
@@ -0,0 +1,64 @@
+type ID = string | number;
+
+interface Node {
+ id: ID;
+ dependencies: ID[];
+}
+
+/**
+ * topological sort with circular check
+ * @param graph
+ */
+export function topologySort(graph: T[]): T[];
+/**
+ * topological sort with circular check and custom node transformation
+ * @param graph
+ * @param cb
+ */
+export function topologySort(graph: T[], cb: (element: T) => Node): T[];
+// topological sort with circular check
+export function topologySort(graph: T[], cb?: (element: T) => Node): T[] {
+ const sorted: T[] = [];
+ const visited: Set = new Set();
+ const visiting: Set = new Set();
+
+ const toNode = (cb ? cb : id) as (value: T) => Node;
+
+ function visit(node: T, path: ID[]) {
+ const { id, dependencies } = toNode(node);
+
+ if (visiting.has(id)) {
+ const cycle = path.slice(path.indexOf(id)).concat(id).join(" -> ");
+ throw new Error(`Circular dependency detected: ${cycle}`);
+ }
+
+ if (!visited.has(id)) {
+ visiting.add(id);
+ path.push(id);
+
+ for (const depId of dependencies) {
+ const dependency = graph.find((n) => toNode(n).id === depId);
+
+ if (dependency) {
+ visit(dependency, [...path]);
+ } else {
+ throw new Error(`Dependency not found: ${depId}`);
+ }
+ }
+
+ visited.add(id);
+ visiting.delete(id);
+ sorted.push(node);
+ }
+ }
+
+ for (const node of graph) {
+ visit(node, []);
+ }
+
+ return sorted;
+}
+
+function id(value: T): T {
+ return value;
+}
diff --git a/packages/molecule/tests/codegen.test.ts b/packages/molecule/tests/codegen.test.ts
index 570837eac..787a208bc 100644
--- a/packages/molecule/tests/codegen.test.ts
+++ b/packages/molecule/tests/codegen.test.ts
@@ -1,5 +1,14 @@
import test from "ava";
-import { codegen } from "../src/codegen";
+import {
+ codegen as originalCodegen,
+ codegenProject,
+ Options,
+ resolveAndEraseImports,
+} from "../src/codegen";
+
+const codegen = (schema: string, options?: Options) => {
+ return originalCodegen(schema, options).code;
+};
function expectGenerated(schema: string, ...expected: string[]) {
const generated = codegen(schema);
@@ -164,3 +173,82 @@ union Something {
`);
});
});
+
+test("should erase if import statement exists", (t) => {
+ const { code, importSchemas } = resolveAndEraseImports(`
+import a;
+ import b;
+// import c;
+
+struct X {
+ value: byte,
+}
+`);
+
+ t.deepEqual(importSchemas, ["a", "b"]);
+ t.is(
+ code,
+ `
+// import c;
+
+struct X {
+ value: byte,
+}
+`
+ );
+});
+
+test("codegenProject", (t) => {
+ const project = codegenProject([
+ {
+ path: "a.mol",
+ content: `
+import b;
+import c/c1;
+
+array A [byte;1];
+`,
+ },
+ {
+ path: "b.mol",
+ content: `
+import c/c1;
+
+array B [byte;1];
+`,
+ },
+
+ {
+ path: "c/c1.mol",
+ content: `
+import ../d;
+
+array C1 [byte;1];
+`,
+ },
+
+ {
+ path: "d.mol",
+ content: `
+/*
+a block comment
+*/
+
+array D [byte; 1];
+`,
+ },
+ ]);
+
+ const a = project.find((item) => item.path === "a.mol")!.content;
+ t.true(a.includes(`import { B } from './b'`));
+ t.true(a.includes(`import { C1 } from './c/c1'`));
+
+ const b = project.find((item) => item.path === "b.mol")!.content;
+ t.true(b.includes(`import { C1 } from './c/c1'`));
+
+ const c1 = project.find((item) => item.path === "c/c1.mol")!.content;
+ t.true(c1.includes(`import { D } from '../d'`));
+
+ const d = project.find((item) => item.path === "d.mol")!.content;
+ t.truthy(d);
+});
diff --git a/packages/molecule/tests/topologicalSort.test.ts b/packages/molecule/tests/topologicalSort.test.ts
new file mode 100644
index 000000000..1299e51e6
--- /dev/null
+++ b/packages/molecule/tests/topologicalSort.test.ts
@@ -0,0 +1,76 @@
+import test from "ava";
+import { topologySort } from "../src/topologySort";
+
+test("Topological sort should return an empty array for an empty graph", (t) => {
+ const graph = [] as any[];
+ const result = topologySort(graph);
+ t.deepEqual(result, []);
+});
+
+test("Topological sort should return the correct order for a simple acyclic graph", (t) => {
+ const graph = [
+ { id: 1, dependencies: [] },
+ { id: 2, dependencies: [1] },
+ { id: 3, dependencies: [1] },
+ { id: 4, dependencies: [2, 3] },
+ { id: 5, dependencies: [4] },
+ ];
+ const result = topologySort(graph);
+ t.deepEqual(
+ result.map((node) => node.id),
+ [1, 2, 3, 4, 5]
+ );
+});
+
+test("Topological sort should throw an error for a graph with circular dependencies", (t) => {
+ const graph = [
+ { id: 1, dependencies: [2] },
+ { id: 2, dependencies: [3] },
+ { id: 3, dependencies: [1] },
+ ];
+ t.throws(() => topologySort(graph), {
+ message: "Circular dependency detected: 1 -> 2 -> 3 -> 1",
+ });
+});
+
+test("Topological sort should throw an error for a graph with missing dependencies", (t) => {
+ const graph = [
+ { id: 1, dependencies: [2] },
+ { id: 3, dependencies: [4] },
+ ];
+ t.throws(() => topologySort(graph), {
+ message: "Dependency not found: 2",
+ });
+});
+
+test("Topological sort should return the correct order for a simple acyclic graph with custom structure", (t) => {
+ type File = {
+ name: string;
+ imports: string[];
+ };
+
+ const files: File[] = [
+ { name: "file1", imports: ["file2"] },
+ { name: "file2", imports: [] },
+ { name: "file3", imports: ["file1"] },
+ { name: "file4", imports: ["file2", "file3"] },
+ { name: "file5", imports: ["file4"] },
+ { name: "file6", imports: ["file3"] },
+ ];
+
+ const result = topologySort(files, (file) => ({
+ id: file.name,
+ dependencies: file.imports,
+ }));
+
+ const sortedFileNames = result.map((node) => node.name);
+
+ t.deepEqual(sortedFileNames, [
+ "file2",
+ "file1",
+ "file3",
+ "file4",
+ "file5",
+ "file6",
+ ]);
+});
diff --git a/packages/molecule/tsconfig.json b/packages/molecule/tsconfig.json
index 1b551f4e9..fe1ee32af 100644
--- a/packages/molecule/tsconfig.json
+++ b/packages/molecule/tsconfig.json
@@ -1,7 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
- "outDir": "lib"
+ "outDir": "lib",
+ "typeRoots": ["node_modules/@types", "src/custom.d.ts"]
},
"include": ["src"]
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 827eaf589..85c709cbf 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -596,15 +596,27 @@ importers:
'@ckb-lumos/codec':
specifier: 0.22.0-next.4
version: link:../codec
+ '@types/moo':
+ specifier: ^0.5.9
+ version: 0.5.9
'@types/nearley':
specifier: ^2.11.2
version: 2.11.2
+ glob:
+ specifier: ^10.3.10
+ version: 10.3.10
+ minimist:
+ specifier: ^1.2.8
+ version: 1.2.8
moo:
specifier: ^0.5.1
version: 0.5.1
nearley:
specifier: ^2.20.1
version: 2.20.1
+ relative:
+ specifier: ^3.0.2
+ version: 3.0.2
devDependencies:
'@ckb-lumos/base':
specifier: 0.22.0-next.4
@@ -4883,6 +4895,18 @@ packages:
/@humanwhocodes/object-schema@1.2.1:
resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==}
+ /@isaacs/cliui@8.0.2:
+ resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
+ engines: {node: '>=12'}
+ dependencies:
+ string-width: 5.1.2
+ string-width-cjs: /string-width@4.2.3
+ strip-ansi: 7.0.1
+ strip-ansi-cjs: /strip-ansi@6.0.1
+ wrap-ansi: 8.1.0
+ wrap-ansi-cjs: /wrap-ansi@7.0.0
+ dev: false
+
/@istanbuljs/load-nyc-config@1.1.0:
resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}
engines: {node: '>=8'}
@@ -5468,6 +5492,13 @@ packages:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.15.0
+ /@pkgjs/parseargs@0.11.0:
+ resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
+ engines: {node: '>=14'}
+ requiresBuild: true
+ dev: false
+ optional: true
+
/@pnpm/config.env-replace@1.1.0:
resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==}
engines: {node: '>=12.22.0'}
@@ -6109,6 +6140,10 @@ packages:
resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==}
dev: true
+ /@types/moo@0.5.9:
+ resolution: {integrity: sha512-ZsFVecFi66jGQ6L41TonEaBhsIVeVftTz6iQKWTctzacHhzYHWvv9S0IyAJi4BhN7vb9qCQ3+kpStP2vbZqmDg==}
+ dev: false
+
/@types/ms@0.7.34:
resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==}
dev: false
@@ -7500,7 +7535,6 @@ packages:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
dependencies:
balanced-match: 1.0.2
- dev: true
/braces@3.0.2:
resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
@@ -10163,6 +10197,14 @@ packages:
signal-exit: 3.0.7
dev: true
+ /foreground-child@3.1.1:
+ resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==}
+ engines: {node: '>=14'}
+ dependencies:
+ cross-spawn: 7.0.3
+ signal-exit: 4.1.0
+ dev: false
+
/forever-agent@0.6.1:
resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==}
dev: true
@@ -10444,6 +10486,18 @@ packages:
/glob-to-regexp@0.4.1:
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
+ /glob@10.3.10:
+ resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ hasBin: true
+ dependencies:
+ foreground-child: 3.1.1
+ jackspeak: 2.3.6
+ minimatch: 9.0.3
+ minipass: 7.0.4
+ path-scurry: 1.10.1
+ dev: false
+
/glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
dependencies:
@@ -11601,6 +11655,13 @@ packages:
/isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+ /isobject@2.1.0:
+ resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ isarray: 1.0.0
+ dev: false
+
/isobject@3.0.1:
resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
engines: {node: '>=0.10.0'}
@@ -11663,6 +11724,15 @@ packages:
is-object: 1.0.2
dev: false
+ /jackspeak@2.3.6:
+ resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==}
+ engines: {node: '>=14'}
+ dependencies:
+ '@isaacs/cliui': 8.0.2
+ optionalDependencies:
+ '@pkgjs/parseargs': 0.11.0
+ dev: false
+
/jest-changed-files@28.1.3:
resolution: {integrity: sha512-esaOfUWJXk2nfZt9SPyC8gA1kNfdKLkQWyzsMlqq8msYSlNKfmZxfRgZn4Cd4MGVUF+7v6dBs0d5TOAKa7iIiA==}
engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0}
@@ -12658,6 +12728,11 @@ packages:
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dev: false
+ /lru-cache@10.2.0:
+ resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==}
+ engines: {node: 14 || >=16.14}
+ dev: false
+
/lru-cache@4.1.5:
resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==}
dependencies:
@@ -13547,7 +13622,6 @@ packages:
engines: {node: '>=16 || 14 >=14.17'}
dependencies:
brace-expansion: 2.0.1
- dev: true
/minimist-options@4.1.0:
resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==}
@@ -13561,6 +13635,11 @@ packages:
/minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+ /minipass@7.0.4:
+ resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ dev: false
+
/mixme@0.5.9:
resolution: {integrity: sha512-VC5fg6ySUscaWUpI4gxCBTQMH2RdUpNrk+MsbpCYtIvf9SBJdiUey4qE7BXviJsJR4nDQxCZ+3yaYNW3guz/Pw==}
engines: {node: '>= 8.0.0'}
@@ -14217,6 +14296,14 @@ packages:
/path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+ /path-scurry@1.10.1:
+ resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ dependencies:
+ lru-cache: 10.2.0
+ minipass: 7.0.4
+ dev: false
+
/path-to-regexp@0.1.7:
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
@@ -15461,6 +15548,13 @@ packages:
engines: {node: '>= 0.10'}
dev: false
+ /relative@3.0.2:
+ resolution: {integrity: sha512-Q5W2qeYtY9GbiR8z1yHNZ1DGhyjb4AnLEjt8iE6XfcC1QIu+FAtj3HQaO0wH28H1mX6cqNLvAqWhP402dxJGyA==}
+ engines: {node: '>= 0.8.0'}
+ dependencies:
+ isobject: 2.1.0
+ dev: false
+
/remark-directive@3.0.0:
resolution: {integrity: sha512-l1UyWJ6Eg1VPU7Hm/9tt0zKtReJQNOA4+iDMAxTyZNWnJnFlbS/7zhiel/rogTLQ2vMYwDzSJa4BiVNqGlqIMA==}
dependencies:
@@ -16070,6 +16164,11 @@ packages:
/signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
+ /signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+ dev: false
+
/simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
dependencies:
@@ -17738,7 +17837,6 @@ packages:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
- dev: true
/wrap-ansi@8.1.0:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}