Skip to content

Commit 1f189d7

Browse files
committed
feat: refactor webhook package structure and add model extraction script
Signed-off-by: Innei <tukon479@gmail.com>
1 parent 7d6f453 commit 1f189d7

6 files changed

Lines changed: 563 additions & 28 deletions

File tree

packages/webhook/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"./package.json": "./package.json"
2525
},
2626
"scripts": {
27-
"build": "node scripts/generate.js && tsdown && node scripts/post-build.cjs"
27+
"build": "node scripts/extract-models.js && node scripts/generate.js && tsdown"
2828
},
2929
"devDependencies": {
3030
"express": "4.21.2",
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
import fs from 'node:fs'
2+
import path from 'node:path'
3+
import { fileURLToPath } from 'node:url'
4+
5+
import prettier from 'prettier'
6+
import ts from 'typescript'
7+
8+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
9+
const coreRoot = path.resolve(__dirname, '../../../apps/core/src')
10+
11+
// Local import alias → canonical name in generated file
12+
const importAliases = {
13+
Category: 'CategoryModel',
14+
Count: 'CountModel',
15+
}
16+
17+
// Sources to extract, in dependency order
18+
const sources = [
19+
{
20+
file: 'constants/business-event.constant.ts',
21+
enums: ['BusinessEvents', 'EventScope'],
22+
},
23+
{
24+
file: 'shared/types/content-format.type.ts',
25+
enums: ['ContentFormat'],
26+
},
27+
{
28+
file: 'constants/db.constant.ts',
29+
enums: ['CollectionRefTypes'],
30+
},
31+
{
32+
file: 'shared/model/image.model.ts',
33+
classes: ['ImageModel'],
34+
},
35+
{
36+
file: 'shared/model/count.model.ts',
37+
classes: ['CountModel'],
38+
},
39+
{
40+
file: 'shared/model/base.model.ts',
41+
classes: ['BaseModel'],
42+
},
43+
{
44+
file: 'shared/model/base-comment.model.ts',
45+
classes: ['BaseCommentIndexModel'],
46+
},
47+
{
48+
file: 'shared/model/write-base.model.ts',
49+
classes: ['WriteBaseModel'],
50+
},
51+
{
52+
file: 'modules/note/models/coordinate.model.ts',
53+
classes: ['Coordinate'],
54+
},
55+
{
56+
file: 'modules/comment/comment.model.ts',
57+
enums: ['CommentState', 'CommentAnchorMode'],
58+
classes: ['CommentAnchorModel', 'CommentModel'],
59+
},
60+
{
61+
file: 'modules/category/category.model.ts',
62+
enums: ['CategoryType'],
63+
classes: ['CategoryModel'],
64+
},
65+
{
66+
file: 'modules/topic/topic.model.ts',
67+
classes: ['TopicModel'],
68+
},
69+
{
70+
file: 'modules/link/link.model.ts',
71+
enums: ['LinkType', 'LinkState'],
72+
classes: ['LinkModel'],
73+
},
74+
{
75+
file: 'modules/note/note.model.ts',
76+
classes: ['NoteModel'],
77+
},
78+
{
79+
file: 'modules/page/page.model.ts',
80+
classes: ['PageModel'],
81+
},
82+
{
83+
file: 'modules/post/post.model.ts',
84+
classes: ['PostModel'],
85+
},
86+
{
87+
file: 'modules/recently/recently.model.ts',
88+
types: ['RefType'],
89+
classes: ['RecentlyModel'],
90+
},
91+
{
92+
file: 'modules/say/say.model.ts',
93+
classes: ['SayModel'],
94+
},
95+
{
96+
file: 'modules/reader/reader.model.ts',
97+
classes: ['ReaderModel'],
98+
},
99+
{
100+
file: 'modules/note/note.type.ts',
101+
types: ['NormalizedNote'],
102+
},
103+
{
104+
file: 'modules/post/post.type.ts',
105+
types: ['NormalizedPost'],
106+
},
107+
]
108+
109+
function transformType(typeText) {
110+
let result = typeText
111+
result = result.replaceAll(/Ref<[^>]+>/g, 'string')
112+
result = result.replaceAll('Types.ObjectId', 'string')
113+
for (const [alias, canonical] of Object.entries(importAliases)) {
114+
result = result.replaceAll(new RegExp(`\\b${alias}\\b`, 'g'), canonical)
115+
}
116+
return result
117+
}
118+
119+
function collectConstValues(sourceFile) {
120+
const values = {}
121+
ts.forEachChild(sourceFile, (node) => {
122+
if (ts.isVariableStatement(node)) {
123+
for (const decl of node.declarationList.declarations) {
124+
if (
125+
ts.isIdentifier(decl.name) &&
126+
decl.initializer &&
127+
ts.isStringLiteral(decl.initializer)
128+
) {
129+
values[decl.name.text] = `'${decl.initializer.text}'`
130+
}
131+
}
132+
}
133+
})
134+
return values
135+
}
136+
137+
function extractEnum(sourceFile, node, constValues) {
138+
const name = node.name.text
139+
const members = []
140+
141+
for (const member of node.members) {
142+
const memberName = member.name.getText(sourceFile)
143+
if (member.initializer) {
144+
let value = member.initializer.getText(sourceFile)
145+
if (constValues[value] !== undefined) {
146+
value = constValues[value]
147+
}
148+
members.push(` ${memberName} = ${value}`)
149+
} else {
150+
members.push(` ${memberName}`)
151+
}
152+
}
153+
154+
return `export enum ${name} {\n${members.join(',\n')},\n}`
155+
}
156+
157+
function extractClassAsInterface(sourceFile, node) {
158+
const name = node.name.text
159+
160+
let heritage = ''
161+
if (node.heritageClauses) {
162+
for (const clause of node.heritageClauses) {
163+
if (clause.token === ts.SyntaxKind.ExtendsKeyword) {
164+
const types = clause.types.map((t) => {
165+
const typeName = t.expression.getText(sourceFile)
166+
return importAliases[typeName] || typeName
167+
})
168+
heritage = ` extends ${types.join(', ')}`
169+
}
170+
}
171+
}
172+
173+
const properties = []
174+
for (const member of node.members) {
175+
if (!ts.isPropertyDeclaration(member)) continue
176+
if (member.modifiers?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword))
177+
continue
178+
179+
const propName = member.name.getText(sourceFile)
180+
const optional = member.questionToken ? '?' : ''
181+
182+
let typeText = 'any'
183+
if (member.type) {
184+
typeText = member.type.getText(sourceFile)
185+
typeText = transformType(typeText)
186+
}
187+
188+
properties.push(` ${propName}${optional}: ${typeText}`)
189+
}
190+
191+
return `export interface ${name}${heritage} {\n${properties.join('\n')}\n}`
192+
}
193+
194+
function extractTypeAlias(sourceFile, node) {
195+
let text = node.getText(sourceFile)
196+
text = transformType(text)
197+
if (!text.startsWith('export')) {
198+
text = `export ${text}`
199+
}
200+
return text
201+
}
202+
203+
async function main() {
204+
const output = [
205+
'// Auto-generated from core model definitions',
206+
'// Do not edit manually - run `node scripts/extract-models.js` to regenerate',
207+
'',
208+
]
209+
210+
for (const source of sources) {
211+
const filePath = path.join(coreRoot, source.file)
212+
const content = fs.readFileSync(filePath, 'utf-8')
213+
const sourceFile = ts.createSourceFile(
214+
filePath,
215+
content,
216+
ts.ScriptTarget.Latest,
217+
true,
218+
)
219+
220+
const constValues = collectConstValues(sourceFile)
221+
222+
ts.forEachChild(sourceFile, (node) => {
223+
if (
224+
ts.isEnumDeclaration(node) &&
225+
source.enums?.includes(node.name.text)
226+
) {
227+
output.push(extractEnum(sourceFile, node, constValues))
228+
output.push('')
229+
}
230+
231+
if (
232+
ts.isClassDeclaration(node) &&
233+
node.name &&
234+
source.classes?.includes(node.name.text)
235+
) {
236+
output.push(extractClassAsInterface(sourceFile, node))
237+
output.push('')
238+
}
239+
240+
if (
241+
ts.isTypeAliasDeclaration(node) &&
242+
source.types?.includes(node.name.text)
243+
) {
244+
output.push(extractTypeAlias(sourceFile, node))
245+
output.push('')
246+
}
247+
})
248+
}
249+
250+
const formatted = await prettier.format(output.join('\n'), {
251+
parser: 'typescript',
252+
semi: false,
253+
tabWidth: 2,
254+
printWidth: 80,
255+
singleQuote: true,
256+
trailingComma: 'all',
257+
})
258+
259+
const outputPath = path.resolve(__dirname, '../src/models.generated.ts')
260+
fs.writeFileSync(outputPath, formatted)
261+
console.log(`Generated ${outputPath}`)
262+
}
263+
264+
main()

packages/webhook/src/event.enum.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1 @@
1-
export {
2-
BusinessEvents,
3-
EventScope,
4-
} from '@core/constants/business-event.constant'
1+
export { BusinessEvents, EventScope } from './models.generated'

0 commit comments

Comments
 (0)