diff --git a/__fixtures__/schema-data/decorators/class.ts b/__fixtures__/schema-data/decorators/class.ts new file mode 100644 index 0000000..483cc07 --- /dev/null +++ b/__fixtures__/schema-data/decorators/class.ts @@ -0,0 +1,37 @@ +import { admin, creator, external, internal } from '@hyperweb/decorators'; +import { BigNumber } from 'jsd-std'; + +export interface State { + count: BigNumber; +} + +export class Counter { + private state: State; + + constructor(initialState: State) { + this.state.count = initialState; + } + + // Public by default (no decorator needed) + @external + @admin + public getCount(): BigNumber { + return this.state.count; + } + + // Only admin and creator can increment + @internal + @admin + public increment(amount: BigNumber): void { + this.state.count = this.state.count.add(amount); + } + + // Only creator can decrement + @creator + public decrement(amount: BigNumber): void { + if (this.state.count.lt(amount)) { + throw new Error('Count cannot be negative'); + } + this.state.count = this.state.count.sub(amount); + } +} \ No newline at end of file diff --git a/__fixtures__/schema-data/decorators/index.ts b/__fixtures__/schema-data/decorators/index.ts new file mode 100644 index 0000000..8a3bce7 --- /dev/null +++ b/__fixtures__/schema-data/decorators/index.ts @@ -0,0 +1,12 @@ +export * from './class'; +export * from './object'; +class MyClass { + @permission('debug', 'level') + @performance + async fetchData() { + // ... method implementation + } +} + +export default MyClass; +export {MyClass}; \ No newline at end of file diff --git a/__fixtures__/schema-data/decorators/object.ts b/__fixtures__/schema-data/decorators/object.ts new file mode 100644 index 0000000..3a6a830 --- /dev/null +++ b/__fixtures__/schema-data/decorators/object.ts @@ -0,0 +1,30 @@ +import { BigNumber } from "jsd-std"; + +interface State { + count: BigNumber; +} + +// Core contract logic +export const start = (initialCount: BigNumber) => { + let state: State = { + count: initialCount + }; + + // HOW TO EVEN DO DECORATORS + return { + getCount: () => state.count, + + increment: (amount: BigNumber) => { + state.count = state.count.add(amount); + return state.count; + }, + + decrement: (amount: BigNumber) => { + if (state.count.lt(amount)) { + throw new Error("Count cannot be negative"); + } + state.count = state.count.sub(amount); + return state.count; + } + }; +}; \ No newline at end of file diff --git a/__output__/schema-data/decorators.bundle.js b/__output__/schema-data/decorators.bundle.js new file mode 100644 index 0000000..9e9b086 --- /dev/null +++ b/__output__/schema-data/decorators.bundle.js @@ -0,0 +1,120 @@ +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name); +var __typeError = (msg) => { + throw TypeError(msg); +}; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); +var __decoratorStart = (base) => [, , , __create(base?.[__knownSymbol("metadata")] ?? null)]; +var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"]; +var __expectFn = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError("Function expected") : fn; +var __decoratorContext = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError("Already initialized") : fns.push(__expectFn(fn || null)) }); +var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]); +var __runInitializers = (array, flags, self, value) => { + for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) flags & 1 ? fns[i].call(self) : value = fns[i].call(self, value); + return value; +}; +var __decorateElement = (array, flags, name, decorators, target, extra) => { + var fn, it, done, ctx, access, k = flags & 7, s = !!(flags & 8), p = !!(flags & 16); + var j = k > 3 ? array.length + 1 : k ? s ? 1 : 2 : 0, key = __decoratorStrings[k + 5]; + var initializers = k > 3 && (array[j - 1] = []), extraInitializers = array[j] || (array[j] = []); + var desc = k && (!p && !s && (target = target.prototype), k < 5 && (k > 3 || !p) && __getOwnPropDesc(k < 4 ? target : { get [name]() { + return __privateGet(this, extra); + }, set [name](x) { + return __privateSet(this, extra, x); + } }, name)); + k ? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name) : __name(target, name); + for (var i = decorators.length - 1; i >= 0; i--) { + ctx = __decoratorContext(k, name, done = {}, array[3], extraInitializers); + if (k) { + ctx.static = s, ctx.private = p, access = ctx.access = { has: p ? (x) => __privateIn(target, x) : (x) => name in x }; + if (k ^ 3) access.get = p ? (x) => (k ^ 1 ? __privateGet : __privateMethod)(x, target, k ^ 4 ? extra : desc.get) : (x) => x[name]; + if (k > 2) access.set = p ? (x, y) => __privateSet(x, target, y, k ^ 4 ? extra : desc.set) : (x, y) => x[name] = y; + } + it = (0, decorators[i])(k ? k < 4 ? p ? extra : desc[key] : k > 4 ? void 0 : { get: desc.get, set: desc.set } : target, ctx), done._ = 1; + if (k ^ 4 || it === void 0) __expectFn(it) && (k > 4 ? initializers.unshift(it) : k ? p ? extra = it : desc[key] = it : target = it); + else if (typeof it !== "object" || it === null) __typeError("Object expected"); + else __expectFn(fn = it.get) && (desc.get = fn), __expectFn(fn = it.set) && (desc.set = fn), __expectFn(fn = it.init) && initializers.unshift(fn); + } + return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target; +}; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg); +var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use the "in" operator on this value') : member.has(obj); +var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj)); +var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value); +var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method); + +// ../../__fixtures__/schema-data/decorators/class.ts +var _decrement_dec, _increment_dec, _getCount_dec, _init; +import { admin, creator, external, internal } from "@hyperweb/decorators"; +_getCount_dec = [external, admin], _increment_dec = [internal, admin], _decrement_dec = [creator]; +var Counter = class { + constructor(initialState) { + __runInitializers(_init, 5, this); + __publicField(this, "state"); + this.state.count = initialState; + } + getCount() { + return this.state.count; + } + increment(amount) { + this.state.count = this.state.count.add(amount); + } + decrement(amount) { + if (this.state.count.lt(amount)) { + throw new Error("Count cannot be negative"); + } + this.state.count = this.state.count.sub(amount); + } +}; +_init = __decoratorStart(null); +__decorateElement(_init, 1, "getCount", _getCount_dec, Counter); +__decorateElement(_init, 1, "increment", _increment_dec, Counter); +__decorateElement(_init, 1, "decrement", _decrement_dec, Counter); +__decoratorMetadata(_init, Counter); + +// ../../__fixtures__/schema-data/decorators/object.ts +var start = (initialCount) => { + let state = { + count: initialCount + }; + return { + getCount: () => state.count, + increment: (amount) => { + state.count = state.count.add(amount); + return state.count; + }, + decrement: (amount) => { + if (state.count.lt(amount)) { + throw new Error("Count cannot be negative"); + } + state.count = state.count.sub(amount); + return state.count; + } + }; +}; + +// ../../__fixtures__/schema-data/decorators/index.ts +var _fetchData_dec, _init2; +_fetchData_dec = [permission("debug", "level"), performance]; +var MyClass = class { + constructor() { + __runInitializers(_init2, 5, this); + } + async fetchData() { + } +}; +_init2 = __decoratorStart(null); +__decorateElement(_init2, 1, "fetchData", _fetchData_dec, MyClass); +__decoratorMetadata(_init2, MyClass); +var decorators_default = MyClass; +export { + Counter, + MyClass, + decorators_default as default, + start +}; +//# sourceMappingURL=decorators.bundle.js.map diff --git a/__output__/schema-data/decorators.bundle.js.map b/__output__/schema-data/decorators.bundle.js.map new file mode 100644 index 0000000..924a954 --- /dev/null +++ b/__output__/schema-data/decorators.bundle.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../../__fixtures__/schema-data/decorators/class.ts", "../../__fixtures__/schema-data/decorators/object.ts", "../../__fixtures__/schema-data/decorators/index.ts"], + "sourcesContent": ["import { admin, creator, external, internal } from '@hyperweb/decorators';\nimport { BigNumber } from 'jsd-std';\n\nexport interface State {\n count: BigNumber;\n}\n\nexport class Counter {\n private state: State;\n\n constructor(initialState: State) {\n this.state.count = initialState;\n }\n\n // Public by default (no decorator needed)\n @external\n @admin\n public getCount(): BigNumber {\n return this.state.count;\n }\n \n // Only admin and creator can increment\n @internal\n @admin\n public increment(amount: BigNumber): void {\n this.state.count = this.state.count.add(amount);\n }\n\n // Only creator can decrement\n @creator\n public decrement(amount: BigNumber): void {\n if (this.state.count.lt(amount)) {\n throw new Error('Count cannot be negative');\n }\n this.state.count = this.state.count.sub(amount);\n }\n}", "import { BigNumber } from \"jsd-std\";\n\ninterface State {\n count: BigNumber;\n}\n\n// Core contract logic\nexport const start = (initialCount: BigNumber) => {\n let state: State = {\n count: initialCount\n };\n\n // HOW TO EVEN DO DECORATORS\n return {\n getCount: () => state.count,\n \n increment: (amount: BigNumber) => {\n state.count = state.count.add(amount);\n return state.count;\n },\n \n decrement: (amount: BigNumber) => {\n if (state.count.lt(amount)) {\n throw new Error(\"Count cannot be negative\");\n }\n state.count = state.count.sub(amount);\n return state.count;\n }\n };\n};", "export * from './class';\nexport * from './object';\nclass MyClass {\n @permission('debug', 'level')\n @performance\n async fetchData() {\n // ... method implementation\n }\n}\n\nexport default MyClass;\nexport {MyClass};"], + "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA,SAAS,OAAO,SAAS,UAAU,gBAAgB;AAejD,iBAAC,UACA,QAMD,kBAAC,UACA,QAMD,kBAAC;AAtBI,IAAM,UAAN,MAAc;AAAA,EAGnB,YAAY,cAAqB;AAH5B;AACL,wBAAQ;AAGN,SAAK,MAAM,QAAQ;AAAA,EACrB;AAAA,EAKO,WAAsB;AAC3B,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA,EAKO,UAAU,QAAyB;AACxC,SAAK,MAAM,QAAQ,KAAK,MAAM,MAAM,IAAI,MAAM;AAAA,EAChD;AAAA,EAIO,UAAU,QAAyB;AACxC,QAAI,KAAK,MAAM,MAAM,GAAG,MAAM,GAAG;AAC/B,YAAM,IAAI,MAAM,0BAA0B;AAAA,IAC5C;AACA,SAAK,MAAM,QAAQ,KAAK,MAAM,MAAM,IAAI,MAAM;AAAA,EAChD;AACF;AA7BO;AAUL,4BAAO,YAFP,eARW;AAiBX,4BAAO,aAFP,gBAfW;AAuBX,4BAAO,aADP,gBAtBW;AAAN,2BAAM;;;ACAN,IAAM,QAAQ,CAAC,iBAA4B;AAChD,MAAI,QAAe;AAAA,IACjB,OAAO;AAAA,EACT;AAGA,SAAO;AAAA,IACL,UAAU,MAAM,MAAM;AAAA,IAEtB,WAAW,CAAC,WAAsB;AAChC,YAAM,QAAQ,MAAM,MAAM,IAAI,MAAM;AACpC,aAAO,MAAM;AAAA,IACf;AAAA,IAEA,WAAW,CAAC,WAAsB;AAChC,UAAI,MAAM,MAAM,GAAG,MAAM,GAAG;AAC1B,cAAM,IAAI,MAAM,0BAA0B;AAAA,MAC5C;AACA,YAAM,QAAQ,MAAM,MAAM,IAAI,MAAM;AACpC,aAAO,MAAM;AAAA,IACf;AAAA,EACF;AACF;;;AC7BA,oBAAAA;AAGE,kBAAC,WAAW,SAAS,OAAO,GAC3B;AAFH,IAAM,UAAN,MAAc;AAAA,EAAd;AAAA,sBAAAA,QAAA;AAAA;AAAA,EAGE,MAAM,YAAY;AAAA,EAElB;AACF;AANAA,SAAA;AAGE,kBAAAA,QAAA,GAAM,aAFN,gBADI;AAAN,oBAAAA,QAAM;AAQN,IAAO,qBAAQ;", + "names": ["_init"] +} diff --git a/__output__/schema-data/decorators.schema.json b/__output__/schema-data/decorators.schema.json new file mode 100644 index 0000000..7317e7c --- /dev/null +++ b/__output__/schema-data/decorators.schema.json @@ -0,0 +1,115 @@ +{ + "state": { + "type": "object", + "properties": { + "count": { + "type": "any" + } + } + }, + "methods": [ + { + "name": "fetchData", + "parameters": [], + "returnType": { + "type": "object", + "properties": { + "then": { + "type": "any" + }, + "catch": { + "type": "any" + }, + "finally": { + "type": "any" + }, + "__@toStringTag@159": { + "type": "string" + } + } + } + } + ], + "decorators": [ + { + "name": "external", + "args": [], + "targetName": "getCount", + "targetType": "method", + "location": { + "file": "class.ts", + "line": 16, + "column": 3 + } + }, + { + "name": "admin", + "args": [], + "targetName": "getCount", + "targetType": "method", + "location": { + "file": "class.ts", + "line": 17, + "column": 3 + } + }, + { + "name": "internal", + "args": [], + "targetName": "increment", + "targetType": "method", + "location": { + "file": "class.ts", + "line": 23, + "column": 3 + } + }, + { + "name": "admin", + "args": [], + "targetName": "increment", + "targetType": "method", + "location": { + "file": "class.ts", + "line": 24, + "column": 3 + } + }, + { + "name": "creator", + "args": [], + "targetName": "decrement", + "targetType": "method", + "location": { + "file": "class.ts", + "line": 30, + "column": 3 + } + }, + { + "name": "permission", + "args": [ + "debug", + "level" + ], + "targetName": "fetchData", + "targetType": "method", + "location": { + "file": "index.ts", + "line": 4, + "column": 3 + } + }, + { + "name": "performance", + "args": [], + "targetName": "fetchData", + "targetType": "method", + "location": { + "file": "index.ts", + "line": 5, + "column": 3 + } + } + ] +} \ No newline at end of file diff --git a/__output__/schema-data/inheritance-contract.schema.json b/__output__/schema-data/inheritance-contract.schema.json index 9a155bd..f412f0e 100644 --- a/__output__/schema-data/inheritance-contract.schema.json +++ b/__output__/schema-data/inheritance-contract.schema.json @@ -22,5 +22,6 @@ "type": "any" } } - ] + ], + "decorators": [] } \ No newline at end of file diff --git a/__output__/schema-data/public-methods.bundle.js.map b/__output__/schema-data/public-methods.bundle.js.map index 6f48365..a15bb57 100644 --- a/__output__/schema-data/public-methods.bundle.js.map +++ b/__output__/schema-data/public-methods.bundle.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../__fixtures__/schema-data/public-methods/contract.ts", "../../__fixtures__/schema-data/public-methods/index.ts"], - "sourcesContent": ["import { State } from './state';\n\nexport class MyContract {\n private state: State;\n\n constructor() {\n this.state = {\n count: 0,\n startCoin: {\n denom: 'uatom',\n amount: '1000'\n },\n tokens: []\n };\n }\n\n public increment() {\n this.state.count++;\n }\n\n private reset() {\n this.state.count = 0;\n }\n\n public addToken(denom: string, amount: string) {\n this.state.tokens.push({ denom, amount });\n }\n\n public removeToken(index: number) {\n this.state.tokens.splice(index, 1);\n }\n}", "import { MyContract } from \"./contract\";\nexport type { State } from \"./state\";\n\nexport default MyContract;\n"], + "sourcesContent": ["import { State } from './state';\n\nexport class MyContract {\n private state: State;\n\n constructor() {\n this.state = {\n count: 0,\n startCoin: {\n denom: 'uatom',\n amount: '1000'\n },\n tokens: []\n };\n }\n\n public increment() {\n this.state.count++;\n }\n\n private reset() {\n this.state.count = 0;\n }\n\n public addToken(denom: string, amount: string) {\n this.state.tokens.push({ denom, amount });\n }\n\n public removeToken(index: number) {\n this.state.tokens.splice(index, 1);\n }\n}\n", "import { MyContract } from \"./contract\";\nexport type { State } from \"./state\";\n\nexport default MyContract;\n"], "mappings": ";AAEO,IAAM,aAAN,MAAiB;AAAA,EACd;AAAA,EAER,cAAc;AACZ,SAAK,QAAQ;AAAA,MACX,OAAO;AAAA,MACP,WAAW;AAAA,QACT,OAAO;AAAA,QACP,QAAQ;AAAA,MACV;AAAA,MACA,QAAQ,CAAC;AAAA,IACX;AAAA,EACF;AAAA,EAEO,YAAY;AACjB,SAAK,MAAM;AAAA,EACb;AAAA,EAEQ,QAAQ;AACd,SAAK,MAAM,QAAQ;AAAA,EACrB;AAAA,EAEO,SAAS,OAAe,QAAgB;AAC7C,SAAK,MAAM,OAAO,KAAK,EAAE,OAAO,OAAO,CAAC;AAAA,EAC1C;AAAA,EAEO,YAAY,OAAe;AAChC,SAAK,MAAM,OAAO,OAAO,OAAO,CAAC;AAAA,EACnC;AACF;;;AC5BA,IAAO,yBAAQ;", "names": [] } diff --git a/__output__/schema-data/public-methods.schema.json b/__output__/schema-data/public-methods.schema.json index b226118..4aaa6ea 100644 --- a/__output__/schema-data/public-methods.schema.json +++ b/__output__/schema-data/public-methods.schema.json @@ -74,5 +74,6 @@ "type": "any" } } - ] + ], + "decorators": [] } \ No newline at end of file diff --git a/__output__/schema-data/state-export.schema.json b/__output__/schema-data/state-export.schema.json index ff224e2..8a669b5 100644 --- a/__output__/schema-data/state-export.schema.json +++ b/__output__/schema-data/state-export.schema.json @@ -32,5 +32,6 @@ } } }, - "methods": [] + "methods": [], + "decorators": [] } \ No newline at end of file diff --git a/packages/build/__tests__/__snapshots__/schemaExtractor.test.ts.snap b/packages/build/__tests__/__snapshots__/schemaExtractor.test.ts.snap index bc3c165..f15aeb7 100644 --- a/packages/build/__tests__/__snapshots__/schemaExtractor.test.ts.snap +++ b/packages/build/__tests__/__snapshots__/schemaExtractor.test.ts.snap @@ -2,6 +2,7 @@ exports[`schemaExtractorPlugin should extract a basic contract with public and private methods 1`] = ` { + "decorators": [], "methods": [], "state": { "properties": { @@ -39,8 +40,127 @@ exports[`schemaExtractorPlugin should extract a basic contract with public and p } `; +exports[`schemaExtractorPlugin should extract decorators and state from source 1`] = ` +{ + "decorators": [ + { + "args": [], + "location": { + "column": 3, + "file": "class.ts", + "line": 16, + }, + "name": "external", + "targetName": "getCount", + "targetType": "method", + }, + { + "args": [], + "location": { + "column": 3, + "file": "class.ts", + "line": 17, + }, + "name": "admin", + "targetName": "getCount", + "targetType": "method", + }, + { + "args": [], + "location": { + "column": 3, + "file": "class.ts", + "line": 23, + }, + "name": "internal", + "targetName": "increment", + "targetType": "method", + }, + { + "args": [], + "location": { + "column": 3, + "file": "class.ts", + "line": 24, + }, + "name": "admin", + "targetName": "increment", + "targetType": "method", + }, + { + "args": [], + "location": { + "column": 3, + "file": "class.ts", + "line": 30, + }, + "name": "creator", + "targetName": "decrement", + "targetType": "method", + }, + { + "args": [ + "debug", + "level", + ], + "location": { + "column": 3, + "file": "index.ts", + "line": 4, + }, + "name": "permission", + "targetName": "fetchData", + "targetType": "method", + }, + { + "args": [], + "location": { + "column": 3, + "file": "index.ts", + "line": 5, + }, + "name": "performance", + "targetName": "fetchData", + "targetType": "method", + }, + ], + "methods": [ + { + "name": "fetchData", + "parameters": [], + "returnType": { + "properties": { + "__@toStringTag@159": { + "type": "string", + }, + "catch": { + "type": "any", + }, + "finally": { + "type": "any", + }, + "then": { + "type": "any", + }, + }, + "type": "object", + }, + }, + ], + "state": { + "properties": { + "count": { + "type": "any", + }, + }, + "type": "object", + }, +} +`; + exports[`schemaExtractorPlugin should extract methods and state from classes inheritance-contract 1`] = ` { + "decorators": [], "methods": [ { "name": "increment", @@ -70,6 +190,7 @@ exports[`schemaExtractorPlugin should extract methods and state from classes inh exports[`schemaExtractorPlugin should extract methods from classes public methods 1`] = ` { + "decorators": [], "methods": [ { "name": "increment", diff --git a/packages/build/__tests__/second.test.ts b/packages/build/__tests__/imports.test.ts similarity index 100% rename from packages/build/__tests__/second.test.ts rename to packages/build/__tests__/imports.test.ts diff --git a/packages/build/__tests__/schemaExtractor.test.ts b/packages/build/__tests__/schemaExtractor.test.ts index 9fb446e..ce01ed3 100644 --- a/packages/build/__tests__/schemaExtractor.test.ts +++ b/packages/build/__tests__/schemaExtractor.test.ts @@ -12,6 +12,7 @@ const runTest = async (fixtureName: string) => { const buildOptions: Partial = { entryPoints: [join(fixtureDir, 'index.ts')], outfile: join(outputDir, `${fixtureName}.bundle.js`), + external: ['@hyperweb/decorators'], customPlugins: [ schemaExtractorPlugin({ outputPath: schemaOutputPath, @@ -71,4 +72,17 @@ describe('schemaExtractorPlugin', () => { expect(schemaData).toMatchSnapshot(); }); + + + it('should extract decorators and state from source', async () => { + const schemaData = await runTest('decorators'); + + expect(schemaData).toHaveProperty('state'); + expect(schemaData.state).toHaveProperty('type', 'object'); + expect(schemaData.state).toHaveProperty('properties'); + + expect(schemaData).toHaveProperty('decorators'); + + expect(schemaData).toMatchSnapshot(); + }); }); diff --git a/packages/build/package.json b/packages/build/package.json index 8a26362..1ecf0ef 100644 --- a/packages/build/package.json +++ b/packages/build/package.json @@ -31,6 +31,10 @@ }, "keywords": [], "dependencies": { + "@babel/generator": "7.22.9", + "@babel/parser": "^7.22.7", + "@babel/traverse": "7.22.8", + "@babel/types": "7.22.5", "esbuild": "^0.23.1" } } diff --git a/packages/build/src/decorators.ts b/packages/build/src/decorators.ts new file mode 100644 index 0000000..a58e50b --- /dev/null +++ b/packages/build/src/decorators.ts @@ -0,0 +1,99 @@ +import * as ts from 'typescript'; +import * as path from 'path'; + +export interface DecoratorInfo { + name: string; + args: any[]; + targetName: string; + targetType?: 'class' | 'method' | 'property' | 'parameter' | 'function' | 'unknown'; + location?: { + file: string; + line: number; + column: number; + }; +} + +// Extract decorators using TypeScript AST +export function extractDecoratorsFromSourceFile( + sourceFile: ts.SourceFile, + checker: ts.TypeChecker, + schemaData: Record, + baseDir: string +) { + const decorators: DecoratorInfo[] = []; + + const visitNode = (node: ts.Node) => { + if (ts.canHaveDecorators(node)) { + const nodeDecorators = ts.getDecorators(node); + if (nodeDecorators) { + nodeDecorators.forEach((decorator) => { + const decoratorInfo = extractDecoratorInfo(decorator, node, sourceFile, baseDir); + if (decoratorInfo) { + decorators.push(decoratorInfo); + } + }); + } + } + + ts.forEachChild(node, visitNode); + }; + + visitNode(sourceFile); + schemaData.decorators.push(...decorators); +} + +// Extract detailed decorator information +function extractDecoratorInfo( + decorator: ts.Decorator, + targetNode: ts.Node, + sourceFile: ts.SourceFile, + baseDir: string +): DecoratorInfo | null { + const decoratorExpression = decorator.expression; + + let decoratorName = 'unknown'; + let decoratorArgs: any[] = []; + + if (ts.isIdentifier(decoratorExpression)) { + decoratorName = decoratorExpression.text; + } else if (ts.isCallExpression(decoratorExpression)) { + if (ts.isIdentifier(decoratorExpression.expression)) { + decoratorName = decoratorExpression.expression.text; + } + decoratorArgs = decoratorExpression.arguments.map((arg) => + ts.isStringLiteral(arg) || ts.isNumericLiteral(arg) + ? arg.text + : 'complex' + ); + } + + const targetName = (ts.isClassDeclaration(targetNode) && targetNode.name?.text) || + (ts.isMethodDeclaration(targetNode) && targetNode.name.getText()) || + (ts.isPropertyDeclaration(targetNode) && targetNode.name.getText()) || + (ts.isParameter(targetNode) && `parameter_${targetNode.getStart()}`) || + 'unknown'; + + const targetType = ts.isClassDeclaration(targetNode) + ? 'class' + : ts.isMethodDeclaration(targetNode) + ? 'method' + : ts.isPropertyDeclaration(targetNode) + ? 'property' + : ts.isParameter(targetNode) + ? 'parameter' + : 'unknown'; + + const { line, character } = sourceFile.getLineAndCharacterOfPosition(decorator.getStart()); + + return { + name: decoratorName, + args: decoratorArgs, + targetName: targetName || 'unknown', + targetType, + location: { + file: path.relative(baseDir, sourceFile.fileName), + line: line + 1, + column: character + 1, + }, + }; +} diff --git a/packages/build/src/schemaExtractor.ts b/packages/build/src/schemaExtractor.ts index 044201f..f1e0a60 100644 --- a/packages/build/src/schemaExtractor.ts +++ b/packages/build/src/schemaExtractor.ts @@ -4,6 +4,7 @@ import * as path from 'path'; import * as ts from 'typescript'; import { HyperwebBuildOptions } from './build'; +import { extractDecoratorsFromSourceFile } from "./decorators"; interface SchemaExtractorOptions { outputPath?: string; @@ -31,7 +32,7 @@ export const schemaExtractorPlugin = ( }); const checker = program.getTypeChecker(); - const schemaData: Record = { state: {}, methods: [] }; + const schemaData: Record = { state: {}, methods: [], decorators: [] }; // Extract state and methods from the contract's default export program.getSourceFiles().forEach((sourceFile) => { @@ -39,6 +40,7 @@ export const schemaExtractorPlugin = ( extractDefaultExport(sourceFile, checker, schemaData); extractStateInterface(sourceFile, checker, schemaData); + extractDecoratorsFromSourceFile(sourceFile, checker, schemaData, baseDir); }); const outputPath = diff --git a/yarn.lock b/yarn.lock index 514498c..9d766c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18,6 +18,15 @@ "@babel/highlight" "^7.25.7" picocolors "^1.0.0" +"@babel/code-frame@^7.22.5": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.0.tgz#9374b5cd068d128dac0b94ff482594273b1c2815" + integrity sha512-INCKxTtbXtcNbUZ3YXutwMpEleqttcswhAdee7dhuoVrD2cnuc3PqtERBtxkX5nziX9vnBL8WXmSGwv8CuPV6g== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + "@babel/compat-data@^7.17.7", "@babel/compat-data@^7.18.8", "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.25.7.tgz#b8479fe0018ef0ac87b6b7a5c6916fcd67ae2c9c" @@ -74,6 +83,16 @@ "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" +"@babel/generator@7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.9.tgz#572ecfa7a31002fa1de2a9d91621fd895da8493d" + integrity sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw== + dependencies: + "@babel/types" "^7.22.5" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + "@babel/generator@^7.18.10", "@babel/generator@^7.23.6", "@babel/generator@^7.25.7", "@babel/generator@^7.7.2": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.25.7.tgz#de86acbeb975a3e11ee92dd52223e6b03b479c56" @@ -84,6 +103,17 @@ "@jridgewell/trace-mapping" "^0.3.25" jsesc "^3.0.2" +"@babel/generator@^7.22.7": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.0.tgz#505cc7c90d92513f458a477e5ef0703e7c91b8d7" + integrity sha512-/AIkAmInnWwgEAJGQr9vY0c66Mj6kjkE2ZPB1PurTRaRAh3U+J45sAQMjQDJdh4WbR3l0x5xkimXBKyBXXAu2w== + dependencies: + "@babel/parser" "^7.26.0" + "@babel/types" "^7.26.0" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" + "@babel/helper-annotate-as-pure@^7.18.6", "@babel/helper-annotate-as-pure@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz#63f02dbfa1f7cb75a9bdb832f300582f30bb8972" @@ -144,14 +174,14 @@ resolve "^1.14.2" semver "^6.1.2" -"@babel/helper-environment-visitor@^7.18.9", "@babel/helper-environment-visitor@^7.22.20": +"@babel/helper-environment-visitor@^7.18.9", "@babel/helper-environment-visitor@^7.22.20", "@babel/helper-environment-visitor@^7.22.5": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz#4b31ba9551d1f90781ba83491dd59cf9b269f7d9" integrity sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ== dependencies: "@babel/types" "^7.24.7" -"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.23.0": +"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.22.5", "@babel/helper-function-name@^7.23.0": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz#75f1e1725742f39ac6584ee0b16d94513da38dd2" integrity sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA== @@ -250,11 +280,21 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz#d50e8d37b1176207b4fe9acedec386c565a44a54" integrity sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g== +"@babel/helper-string-parser@^7.22.5", "@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.22.20", "@babel/helper-validator-identifier@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz#77b7f60c40b15c97df735b38a66ba1d7c3e93da5" integrity sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg== +"@babel/helper-validator-identifier@^7.22.5", "@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + "@babel/helper-validator-option@^7.18.6", "@babel/helper-validator-option@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz#97d1d684448228b30b506d90cace495d6f492729" @@ -299,6 +339,13 @@ dependencies: "@babel/types" "^7.25.7" +"@babel/parser@^7.22.7", "@babel/parser@^7.26.0": + version "7.26.1" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.1.tgz#44e02499960df2cdce2c456372a3e8e0c3c5c975" + integrity sha512-reoQYNiAJreZNsJzyrDNzFQ+IQ5JFiIzAHJg9bn94S3l+4++J7RsIhNMoB+lgP/9tpmiAQqspv+xfdxTSzREOw== + dependencies: + "@babel/types" "^7.26.0" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.7.tgz#c5f755e911dfac7ef6957300c0f9c4a8c18c06f4" @@ -1026,6 +1073,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@7.22.8": + version "7.22.8" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.8.tgz#4d4451d31bc34efeae01eac222b514a77aa4000e" + integrity sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw== + dependencies: + "@babel/code-frame" "^7.22.5" + "@babel/generator" "^7.22.7" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.22.7" + "@babel/types" "^7.22.5" + debug "^4.1.0" + globals "^11.1.0" + "@babel/traverse@7.23.6": version "7.23.6" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.6.tgz#b53526a2367a0dd6edc423637f3d2d0f2521abc5" @@ -1064,6 +1127,15 @@ "@babel/helper-validator-identifier" "^7.18.6" to-fast-properties "^2.0.0" +"@babel/types@7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.5.tgz#cd93eeaab025880a3a47ec881f4b096a5b786fbe" + integrity sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.5" + to-fast-properties "^2.0.0" + "@babel/types@7.23.6": version "7.23.6" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.6.tgz#be33fdb151e1f5a56877d704492c240fc71c7ccd" @@ -1082,6 +1154,14 @@ "@babel/helper-validator-identifier" "^7.25.7" to-fast-properties "^2.0.0" +"@babel/types@^7.22.5", "@babel/types@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.0.tgz#deabd08d6b753bc8e0f198f8709fb575e31774ff" + integrity sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -1861,7 +1941,7 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.13", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.13", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": version "0.3.25" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==