diff --git a/__fixtures__/decorators/src/class.ts b/__fixtures__/schema-data/decorators/class.ts similarity index 100% rename from __fixtures__/decorators/src/class.ts rename to __fixtures__/schema-data/decorators/class.ts diff --git a/__fixtures__/decorators/src/index.ts b/__fixtures__/schema-data/decorators/index.ts similarity index 100% rename from __fixtures__/decorators/src/index.ts rename to __fixtures__/schema-data/decorators/index.ts diff --git a/__fixtures__/decorators/src/object.ts b/__fixtures__/schema-data/decorators/object.ts similarity index 100% rename from __fixtures__/decorators/src/object.ts rename to __fixtures__/schema-data/decorators/object.ts 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__/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/src/decorators.ts b/packages/build/src/decorators.ts index a515e94..a58e50b 100644 --- a/packages/build/src/decorators.ts +++ b/packages/build/src/decorators.ts @@ -1,17 +1,11 @@ -import generate from '@babel/generator'; -import * as parser from '@babel/parser'; -import traverse from '@babel/traverse'; -import * as t from '@babel/types'; -import { Plugin } from 'esbuild'; +import * as ts from 'typescript'; import * as path from 'path'; -import { HyperwebBuildOptions } from './index'; - -interface DecoratorInfo { +export interface DecoratorInfo { name: string; args: any[]; targetName: string; - targetType?: 'method' | 'function'; + targetType?: 'class' | 'method' | 'property' | 'parameter' | 'function' | 'unknown'; location?: { file: string; line: number; @@ -19,132 +13,87 @@ interface DecoratorInfo { }; } -interface DecoratorExtractorOptions { - outputPath?: string; // Where to save the decorator metadata - include?: RegExp[]; // Patterns for files to process - exclude?: RegExp[]; // Patterns for files to ignore -} - -const decoratorMetadata: Record = {}; - - -function normalizeFilePath(filePath: string, rootDir?: string): string { - const projectRoot = rootDir || process.cwd(); - const relativePath = path.relative(projectRoot, filePath); - return relativePath - .replace(/\\/g, '/') - .replace(/^\.\//, ''); - } - -export const createDecoratorExtractorPlugin = ( - pluginOptions: DecoratorExtractorOptions = {}, - hyperwebOptions?: HyperwebBuildOptions -): Plugin => ({ - name: 'decorator-extractor', - - setup(build) { - - // Set up the file filter - const filter = { - include: pluginOptions.include || [/\.[jt]sx?$/], - exclude: pluginOptions.exclude || [/node_modules/], - }; - - build.onLoad({ filter: new RegExp(filter.include.map(r => r.source).join('|')) }, async (args) => { - // Skip excluded files - if (filter.exclude.some(pattern => pattern.test(args.path))) { - return null; - } - - // Read the file - const source = await require('fs').promises.readFile(args.path, 'utf8'); - - // Parse the code with babel - const ast = parser.parse(source, { - sourceType: 'module', - plugins: ['typescript', 'decorators-legacy'], - }); - - // Track if we made any modifications - let modified = false; - - // Traverse the AST - traverse(ast, { - Decorator(path) { - const decorator = path.node; - const parent = path.parent; - - // Only process method or function decorators - if (!t.isClassMethod(parent as any) && !t.isFunctionDeclaration(parent as any)) { - return; +// 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); } - - const rootDir = hyperwebOptions?.absWorkingDir || process.cwd(); - const normalizedPath = normalizeFilePath(args.path, rootDir); - - // Get decorator information - const decoratorInfo: DecoratorInfo = { - // @ts-ignore - name: t.isIdentifier(decorator.expression) - ? decorator.expression.name - // @ts-ignore - : t.isCallExpression(decorator.expression) - ? (decorator.expression.callee as t.Identifier).name - : 'unknown', - // @ts-ignore - args: t.isCallExpression(decorator.expression) - ? decorator.expression.arguments.map(arg => - // @ts-ignore - t.isLiteral(arg) ? (arg as any).value : null) - : [], - // @ts-ignore - targetName: t.isClassMethod(parent) - ? (parent.key as t.Identifier).name - : (parent as t.FunctionDeclaration).id?.name || 'anonymous', - // @ts-ignore - // targetType: t.isClassMethod(parent) ? 'method' : 'function', - - // location: { - // file: normalizedPath, - // line: decorator.loc?.start.line || 0, - // column: decorator.loc?.start.column || 0, - // } - }; - - // Store the metadata - if (!decoratorMetadata[normalizedPath]) { - decoratorMetadata[normalizedPath] = []; - } - decoratorMetadata[normalizedPath].push(decoratorInfo); - - // Remove the decorator - path.remove(); - modified = true; - } - }); - - // If we made modifications, generate new code - if (modified) { - const output = generate(ast, {}, source); - - return { - contents: output.code, - loader: args.path.endsWith('.ts') ? 'ts' : 'js', - }; + }); } + } - return null; - }); + ts.forEachChild(node, visitNode); + }; - build.onEnd(async () => { - if (pluginOptions.outputPath) { - // Save the metadata to the specified file - await require('fs').promises.writeFile( - pluginOptions.outputPath, - JSON.stringify(decoratorMetadata, null, 2), - 'utf8' - ); - } - }); + 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' + ); } -}); \ No newline at end of file + + 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 =