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==}