Skip to content

Commit 49dfe7f

Browse files
committed
add facade versioning
1 parent 3cba75e commit 49dfe7f

File tree

1 file changed

+159
-38
lines changed

1 file changed

+159
-38
lines changed

etc/facadeBuilder/facadeBuilder.ts

Lines changed: 159 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ interface ErrorSignature {
3131
name: string;
3232
parameters: string;
3333
origin: string; // Name of the origin file
34+
imported: boolean;
3435
}
3536
interface EventSignature {
3637
name: string;
3738
parameters: string;
3839
origin: string; // Name of the origin file
40+
imported: boolean;
3941
}
4042
interface FunctionSignature {
4143
name: string;
@@ -47,36 +49,29 @@ interface FunctionSignature {
4749
}
4850

4951
const facadeConfigPath = './facade.yaml'; // Configuration file path
50-
const outputDir = './generated'; // Output directory for generated files
52+
const facadeDir = './generated'; // Output directory for generated files
5153

5254
async function main() {
5355
const projectRoot = path.resolve(__dirname, '../../');
5456
process.chdir(projectRoot);
5557

5658
// Ensure output directory exists
57-
await fs.ensureDir(path.resolve(outputDir));
59+
await fs.ensureDir(path.resolve(facadeDir));
60+
const facadeFiles = await getSolidityFiles(facadeDir);
5861

5962
// Load facade configuration
6063
const facadeConfigs: FacadeConfig[] = await loadFacadeConfig();
6164

6265
for (const facadeConfig of facadeConfigs) {
63-
// Ensure required fields are present
64-
if (!facadeConfig.bundleName || !facadeConfig.bundleDirName) {
65-
console.error('Error: bundleName and bundleDirName are required in facade.yaml');
66-
process.exit(1);
67-
}
66+
validateFacadeConfig(facadeConfig);
6867

69-
if (!facadeConfig.facades || facadeConfig.facades.length === 0) {
70-
console.error('Error: At least one facade must be defined in facade.yaml');
71-
process.exit(1);
72-
}
68+
const functionDir = `./src/${facadeConfig.bundleDirName}/functions`; // Adjust the path as needed
69+
const interfaceDir = `./src/${facadeConfig.bundleDirName}/interfaces`; // Adjust the path as needed
70+
// Recursively read all Solidity files in the source directory
71+
const functionFiles = await getSolidityFiles(functionDir);
72+
const interfaceFiles = await getSolidityFiles(interfaceDir);
7373

74-
for (const facade of facadeConfig.facades) {
75-
if (!facade.name) {
76-
console.error('Error: Each facade must have a name');
77-
process.exit(1);
78-
}
79-
}
74+
const regex = /^(I)?.*(Errors|Events)\.sol$/;
8075

8176
// Process each facade
8277
for (const facade of facadeConfig.facades) {
@@ -86,19 +81,24 @@ async function main() {
8681
events: [],
8782
functions: []
8883
};
89-
const functionDir = `./src/${facadeConfig.bundleDirName}/functions`; // Adjust the path as needed
90-
const interfaceDir = `./src/${facadeConfig.bundleDirName}/interfaces`; // Adjust the path as needed
91-
// Recursively read all Solidity files in the source directory
92-
const functionFiles = await getSolidityFiles(functionDir);
93-
const interfaceFiles = await getSolidityFiles(interfaceDir);
94-
95-
const regex = /^(I)?.*(Errors|Events)\.sol$/;
9684

9785
for (const file of interfaceFiles) {
9886
const fileName = path.basename(file);
9987

100-
if (regex.test(fileName)) {
101-
facadeObject.files.push({ name: fileName.replace(/\.sol$/, ""), origin: file.replace(/\\/g, '/') });
88+
if (!regex.test(fileName)) {
89+
continue;
90+
}
91+
92+
facadeObject.files.push({ name: fileName.replace(/\.sol$/, ""), origin: file.replace(/\\/g, '/') });
93+
94+
const content = await fs.readFile(file, 'utf8');
95+
try {
96+
const ast = parse(content, { tolerant: true });
97+
98+
// Extract functions from the AST
99+
traverseASTs(ast, facadeObject, fileName, facade, true);
100+
} catch (err) {
101+
console.error(`Error parsing ${file}:`, err);
102102
}
103103
}
104104

@@ -122,7 +122,42 @@ async function main() {
122122
}
123123

124124
// Generate facade contract
125-
await generateFacadeContract(facadeObject, facade, facadeConfig);
125+
const generatedCode = await generateFacadeContract(facadeObject, facade, facadeConfig);
126+
127+
const latestFacade = getLatestVersionFile(facadeFiles, facade.name);
128+
if (latestFacade === null) {
129+
writeFacadeContract(facade.name, [1, 0, 0], generatedCode);
130+
process.exit(1);
131+
}
132+
133+
const latestObject: FacadeObjects = {
134+
files: [],
135+
errors: [],
136+
events: [],
137+
functions: []
138+
};
139+
try {
140+
const generatedAst = parse(generatedCode, { tolerant: true });
141+
142+
// Extract functions from the AST
143+
traverseASTs(generatedAst, latestObject, latestFacade.file, facade);
144+
} catch (err) {
145+
console.error(`Error parsing ${latestFacade.file}:`, err);
146+
process.exit(1);
147+
}
148+
const majorDiff = getSymmetricDifference(facadeObject.functions, latestObject.functions, ["name", "parameters"]);
149+
if (majorDiff.length > 0) {
150+
latestFacade.version[0]++;
151+
}
152+
let minorDiff = getSymmetricDifference(facadeObject.errors, latestObject.errors, ["name", "parameters"]);
153+
if (minorDiff.length > 0) {
154+
latestFacade.version[1]++;
155+
}
156+
minorDiff = getSymmetricDifference(facadeObject.events, latestObject.events, ["name", "parameters"]);
157+
if (minorDiff.length > 0) {
158+
latestFacade.version[1]++;
159+
}
160+
writeFacadeContract(facade.name, latestFacade.version, generatedCode);
126161
}
127162
}
128163
}
@@ -155,6 +190,26 @@ async function loadFacadeConfig(): Promise<FacadeConfig[]> {
155190
}
156191
}
157192

193+
function validateFacadeConfig(facadeConfig: FacadeConfig) {
194+
// Ensure required fields are present
195+
if (!facadeConfig.bundleName || !facadeConfig.bundleDirName) {
196+
console.error('Error: bundleName and bundleDirName are required in facade.yaml');
197+
process.exit(1);
198+
}
199+
200+
if (!facadeConfig.facades || facadeConfig.facades.length === 0) {
201+
console.error('Error: At least one facade must be defined in facade.yaml');
202+
process.exit(1);
203+
}
204+
205+
for (const facade of facadeConfig.facades) {
206+
if (!facade.name) {
207+
console.error('Error: Each facade must have a name');
208+
process.exit(1);
209+
}
210+
}
211+
}
212+
158213
async function getSolidityFiles(dir: string): Promise<string[]> {
159214
let files: string[] = [];
160215
const items = await fs.readdir(dir);
@@ -174,57 +229,109 @@ async function getSolidityFiles(dir: string): Promise<string[]> {
174229
return files;
175230
}
176231

232+
function getLatestVersionFile(files: string[], baseName: string): { file: string; version: number[] } | null {
233+
const regex = new RegExp(`^${baseName}(?:V(\\d+)_(\\d+)_(\\d+))?\\.sol$`);
234+
235+
return files
236+
.map(file => {
237+
const match = file.match(regex);
238+
if (match) {
239+
const [_, major, minor, patch] = match.map(Number);
240+
return {
241+
file,
242+
version: [major || 1, minor || 0, patch || 0],
243+
};
244+
}
245+
return null;
246+
})
247+
.filter((item): item is { file: string; version: number[] } => item !== null)
248+
.sort((a, b) => {
249+
// バージョン配列を比較 (降順)
250+
for (let i = 0; i < 3; i++) {
251+
if (b.version[i] !== a.version[i]) {
252+
return b.version[i] - a.version[i];
253+
}
254+
}
255+
return 0;
256+
})[0] || null;
257+
}
258+
259+
function getSymmetricDifference<T>(
260+
array1: T[],
261+
array2: T[],
262+
keys: (keyof T)[]
263+
): T[] {
264+
const isMatch = (a: T, b: T) => keys.every(key => a[key] === b[key]);
265+
266+
// A ∪ B
267+
const union = [...array1, ...array2];
268+
// A ∩ B
269+
const intersection = array1.filter(item1 =>
270+
array2.some(item2 => isMatch(item1, item2))
271+
);
272+
273+
// A ∪ B - A ∩ B
274+
return union.filter(item =>
275+
!intersection.some(intersectItem => isMatch(item, intersectItem))
276+
);
277+
}
278+
177279
function traverseASTs(
178280
ast: any,
179281
facadeObjects: FacadeObjects,
180282
origin: string,
181-
facade: FacadeDefinition
283+
facade: FacadeDefinition,
284+
imported: boolean = false
182285
) {
183286
if (ast.type === 'FunctionDefinition' && ast.isConstructor === false) {
184287
extractFunctions(ast, facadeObjects.functions, origin, facade);
185288
} else if (ast.type === 'ContractDefinition') {
186289
// Traverse contract sub-nodes
187290
for (const subNode of ast.subNodes) {
188-
traverseASTs(subNode, facadeObjects, origin, facade);
291+
traverseASTs(subNode, facadeObjects, origin, facade, imported);
189292
}
190293
} else if (ast.type === 'SourceUnit') {
191294
// Traverse source unit nodes
192295
for (const child of ast.children) {
193-
traverseASTs(child, facadeObjects, origin, facade);
296+
traverseASTs(child, facadeObjects, origin, facade, imported);
194297
}
195298
} else if (ast.type === 'CustomErrorDefinition') {
196-
extractErrors(ast, facadeObjects.errors, origin);
299+
extractErrors(ast, facadeObjects.errors, origin, imported);
197300
} else if (ast.type === 'EventDefinition') {
198-
extractEvents(ast, facadeObjects.events, origin);
301+
extractEvents(ast, facadeObjects.events, origin, imported);
199302
}
200303
}
201304

202305
function extractErrors(
203306
ast: any,
204307
errors: ErrorSignature[],
205-
origin: string
308+
origin: string,
309+
imported: boolean
206310
) {
207311
const error: ErrorSignature = {
208312
name: ast.name,
209313
parameters: ast.parameters
210314
.map((param: any) => getParameter(param))
211315
.join(', '),
212-
origin: origin
316+
origin: origin,
317+
imported: imported
213318
};
214319
errors.push(error);
215320
}
216321

217322
function extractEvents(
218323
ast: any,
219324
events: EventSignature[],
220-
origin: string
325+
origin: string,
326+
imported: boolean
221327
) {
222328
const event: EventSignature = {
223329
name: ast.name,
224330
parameters: ast.parameters
225331
.map((param: any) => getParameter(param))
226332
.join(', '),
227-
origin: origin
333+
origin: origin,
334+
imported: imported
228335
};
229336
events.push(event);
230337
}
@@ -296,8 +403,8 @@ async function generateFacadeContract(
296403
objects: FacadeObjects,
297404
facade: FacadeDefinition,
298405
config: FacadeConfig
299-
) {
300-
const facadeFilePath = path.join(outputDir, `${facade.name}.sol`);
406+
): Promise<string> {
407+
const facadeFilePath = path.join(facadeDir, `${facade.name}.sol`);
301408
let code = `// SPDX-License-Identifier: MIT
302409
pragma solidity ^0.8.24;
303410
@@ -311,10 +418,16 @@ import {Schema} from "src/${config.bundleDirName}/storage/Schema.sol";
311418
code += `\ncontract ${facade.name} is Schema, ${objects.files.map((file) => file.name).join(', ')} {\n`;
312419

313420
for (const error of objects.errors) {
421+
if (error.imported) {
422+
continue;
423+
}
314424
code += generateError(error);
315425
}
316426
code += `\n`;
317427
for (const event of objects.events) {
428+
if (event.imported) {
429+
continue;
430+
}
318431
code += generateEvent(event);
319432
}
320433
code += `\n`;
@@ -324,11 +437,19 @@ import {Schema} from "src/${config.bundleDirName}/storage/Schema.sol";
324437

325438
code += `}\n`;
326439

440+
return code;
441+
327442
// Write the facade contract to the output file
328443
await fs.writeFile(facadeFilePath, code);
329444
console.log(`Facade contract generated at ${facadeFilePath}`);
330445
}
331446

447+
async function writeFacadeContract(facadeName: string, version: number[], code: string) {
448+
// Write the facade contract to the output file
449+
const facadeFilePath = path.join(facadeDir, `${facadeName}V${version[0]}_${version[1]}_${version[2]}.sol`);
450+
await fs.writeFile(facadeFilePath, code);
451+
console.log(`Facade contract generated at ${facadeFilePath}`);
452+
}
332453
function generateError(error: ErrorSignature) {
333454
return ` error ${error.name}(${error.parameters});\n`
334455
}

0 commit comments

Comments
 (0)