-
Notifications
You must be signed in to change notification settings - Fork 53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Codegen 643 #646
Codegen 643 #646
Changes from all commits
4cced73
80068ee
deb09f8
dfa7ca7
4972e2d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,44 +1,6 @@ | ||
#!/usr/bin/env node | ||
import { codegen } from "./codegen"; | ||
import * as fs from "node:fs"; | ||
import { initConfig } from "./config"; | ||
import { loopCodegen } from "./resolve"; | ||
|
||
const DEFAULT_CONFIG_FILE_NAME = "lumos-molecule-codegen.json"; | ||
|
||
function camelcase(str: string): string { | ||
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); | ||
} | ||
|
||
type Config = { | ||
objectKeyFormat: "camelcase" | "keep"; | ||
prepend: string; | ||
schemaFile: string; | ||
}; | ||
|
||
const fileConfig: Partial<Config> = (() => { | ||
if (fs.existsSync(DEFAULT_CONFIG_FILE_NAME)) { | ||
return JSON.parse(fs.readFileSync(DEFAULT_CONFIG_FILE_NAME, "utf8")); | ||
} | ||
return {}; | ||
})(); | ||
|
||
const config: Config = { | ||
objectKeyFormat: fileConfig.objectKeyFormat || "keep", | ||
prepend: fileConfig.prepend || "", | ||
schemaFile: fileConfig.schemaFile || "schema.mol", | ||
}; | ||
|
||
// 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"), { | ||
prepend: config.prepend, | ||
formatObjectKeys: | ||
config.objectKeyFormat === "camelcase" ? camelcase : undefined, | ||
}); | ||
|
||
console.log(generated); | ||
const config = initConfig(); | ||
loopCodegen(config); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import * as fs from "node:fs"; | ||
|
||
const DEFAULT_CONFIG_FILE_NAME = | ||
process.env.CONF_PATH || "lumos-molecule-codegen.json"; | ||
|
||
export type Config = { | ||
objectKeyFormat: "camelcase" | "keep"; | ||
prepend: string; | ||
schemaFile: string; | ||
output: number; // 0: Default out console, 1: Write file, 2. Just return | ||
dir: string; // | ||
}; | ||
|
||
export function initConfig(): Config { | ||
const fileConfig: Partial<Config> = (() => { | ||
if (fs.existsSync(DEFAULT_CONFIG_FILE_NAME)) { | ||
return JSON.parse(fs.readFileSync(DEFAULT_CONFIG_FILE_NAME, "utf8")); | ||
} | ||
return {}; | ||
})(); | ||
|
||
const config: Config = { | ||
objectKeyFormat: fileConfig.objectKeyFormat || "keep", | ||
prepend: fileConfig.prepend || "", | ||
schemaFile: fileConfig.schemaFile || "schema.mol", | ||
output: fileConfig.output || 0, | ||
dir: fileConfig.dir || __dirname, | ||
}; | ||
|
||
return config; | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,176 @@ | ||||||||||||||||||||||||
#!/usr/bin/env node | ||||||||||||||||||||||||
import { ParseResult } from "./type"; | ||||||||||||||||||||||||
import { codegenReturnWithElements } from "./codegen"; | ||||||||||||||||||||||||
import { Config } from "./config"; | ||||||||||||||||||||||||
import * as fs from "node:fs"; | ||||||||||||||||||||||||
import * as path from "path"; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
function camelcase(str: string): string { | ||||||||||||||||||||||||
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
export type RelativePath = string; | ||||||||||||||||||||||||
export type FileWithDependence = { | ||||||||||||||||||||||||
relativePath: string; | ||||||||||||||||||||||||
dependencies: string[]; | ||||||||||||||||||||||||
}; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
export function resolveDependencies( | ||||||||||||||||||||||||
importPath: RelativePath, | ||||||||||||||||||||||||
baseDir: string, | ||||||||||||||||||||||||
resolved: Set<RelativePath> | ||||||||||||||||||||||||
): FileWithDependence[] { | ||||||||||||||||||||||||
const dependencies: FileWithDependence[] = []; | ||||||||||||||||||||||||
// check if the file exist | ||||||||||||||||||||||||
const realPath = path.join(baseDir, importPath); | ||||||||||||||||||||||||
if (!fs.existsSync(realPath)) { | ||||||||||||||||||||||||
console.error(`Schema file ${realPath} does not exist.`); | ||||||||||||||||||||||||
process.exit(1); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
const cur: FileWithDependence = { | ||||||||||||||||||||||||
relativePath: importPath, | ||||||||||||||||||||||||
dependencies: [], | ||||||||||||||||||||||||
}; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
const schema = fs.readFileSync(realPath, "utf-8"); | ||||||||||||||||||||||||
if (!schema) { | ||||||||||||||||||||||||
return [cur]; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
const matched = schema.match(/.*import\s+"(.*)".*;/g); | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I found that the import statement definition should exclude the
maybe a capturing group could be helpful here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a detail I overlooked, it really has a big impact |
||||||||||||||||||||||||
if (!matched) { | ||||||||||||||||||||||||
return [cur]; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// collect all import filenames | ||||||||||||||||||||||||
const importFileNames = matched | ||||||||||||||||||||||||
.map((item: string) => { | ||||||||||||||||||||||||
// if is comment statement, continue | ||||||||||||||||||||||||
if (item.trim().startsWith("//")) { | ||||||||||||||||||||||||
return ""; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
const m = item.match(/.*"(.*)".*/); | ||||||||||||||||||||||||
return m ? m[1] : ""; | ||||||||||||||||||||||||
}) | ||||||||||||||||||||||||
.filter(Boolean); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// loop all import files | ||||||||||||||||||||||||
for (const importFileName of importFileNames) { | ||||||||||||||||||||||||
const mFilePath = path.join(baseDir, importFileName + ".mol"); | ||||||||||||||||||||||||
const mRelativePath = path.relative(baseDir, mFilePath); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
cur.dependencies.push(importFileName); | ||||||||||||||||||||||||
if (!resolved.has(mFilePath)) { | ||||||||||||||||||||||||
// mask this file has resolved | ||||||||||||||||||||||||
resolved.add(mFilePath); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
const _dependencies = resolveDependencies( | ||||||||||||||||||||||||
mRelativePath, | ||||||||||||||||||||||||
baseDir, | ||||||||||||||||||||||||
resolved | ||||||||||||||||||||||||
); | ||||||||||||||||||||||||
dependencies.push(..._dependencies); | ||||||||||||||||||||||||
Comment on lines
+68
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
It's better to avoid using variables that start with an underline as they often imply that the variable is private and mutable in a scope. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will avoid this way of writing in the future |
||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
dependencies.push(cur); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
return dependencies; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
export function extractAndEraseImportClauses(code: string): string { | ||||||||||||||||||||||||
const lines = code.split("\n"); | ||||||||||||||||||||||||
const delImportLines = lines.filter((line: string) => { | ||||||||||||||||||||||||
return !line.trim().startsWith("import"); | ||||||||||||||||||||||||
}); | ||||||||||||||||||||||||
return delImportLines.join("\n"); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
function printOrWrite(resultMap: Map<string, ParseResult>, config: Config) { | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is suggested to divide the function based on distinct output targets to make it more testable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that's better,agree with you. |
||||||||||||||||||||||||
for (const name of resultMap.keys()) { | ||||||||||||||||||||||||
if (config.output < 2) { | ||||||||||||||||||||||||
console.log(`// ${String("-").repeat(66)} //`); | ||||||||||||||||||||||||
console.log(`// generate from ${name}`); | ||||||||||||||||||||||||
console.log(`// ${String("-").repeat(66)} //`); | ||||||||||||||||||||||||
console.log(resultMap.get(name)?.code); | ||||||||||||||||||||||||
if (config.output === 1) { | ||||||||||||||||||||||||
const dir = path.join(config.dir, "mols"); | ||||||||||||||||||||||||
if (!fs.existsSync(dir)) { | ||||||||||||||||||||||||
console.log(`mkdir mols`); | ||||||||||||||||||||||||
fs.mkdirSync(dir); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
const tsName = name.replace(".mol", ".ts"); | ||||||||||||||||||||||||
const targetDir = path.dirname(path.join(dir, tsName)); | ||||||||||||||||||||||||
if (!fs.existsSync(targetDir)) { | ||||||||||||||||||||||||
console.log(`mkdir ${targetDir}`); | ||||||||||||||||||||||||
fs.mkdirSync(targetDir, { recursive: true }); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
console.log(`writing file ${tsName}`); | ||||||||||||||||||||||||
fs.writeFileSync( | ||||||||||||||||||||||||
path.join(dir, tsName), | ||||||||||||||||||||||||
resultMap.get(name)?.code || "" | ||||||||||||||||||||||||
); | ||||||||||||||||||||||||
console.log(`write file ${tsName} finish`); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
export function loopCodegen(config: Config): Map<string, ParseResult> { | ||||||||||||||||||||||||
const result: Map<string, ParseResult> = new Map(); | ||||||||||||||||||||||||
const baseDir = path.dirname(config.schemaFile); | ||||||||||||||||||||||||
const relativePath = path.basename(config.schemaFile); | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I found that it's a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, indeed. |
||||||||||||||||||||||||
const dependencies = resolveDependencies(relativePath, baseDir, new Set()); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
if (dependencies.length === 0) { | ||||||||||||||||||||||||
return result; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
const parsed: Set<string> = new Set(); | ||||||||||||||||||||||||
dependencies.forEach((cur) => { | ||||||||||||||||||||||||
// has generated, continue | ||||||||||||||||||||||||
if (parsed.has(cur.relativePath)) { | ||||||||||||||||||||||||
return; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// erase the import clause from the schema when calling the codegen method | ||||||||||||||||||||||||
const realPath = path.join(baseDir, cur.relativePath); | ||||||||||||||||||||||||
const schema = extractAndEraseImportClauses( | ||||||||||||||||||||||||
fs.readFileSync(realPath, "utf-8") | ||||||||||||||||||||||||
); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
let optionPrepend = config.prepend; | ||||||||||||||||||||||||
// append all ESM import to config.prepend | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
for (const importName of cur.dependencies) { | ||||||||||||||||||||||||
const importAbsolutePath = path.join( | ||||||||||||||||||||||||
path.dirname(realPath), | ||||||||||||||||||||||||
importName + ".mol" | ||||||||||||||||||||||||
); | ||||||||||||||||||||||||
const importRelativePath = path.relative(baseDir, importAbsolutePath); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
if (result.has(importRelativePath)) { | ||||||||||||||||||||||||
const imptDesc = `\nimport { ${result | ||||||||||||||||||||||||
.get(importRelativePath) | ||||||||||||||||||||||||
?.elements.join(", ")} } from './${importName}'`; | ||||||||||||||||||||||||
optionPrepend += imptDesc; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
const codegenReturn = codegenReturnWithElements(schema, { | ||||||||||||||||||||||||
prepend: optionPrepend, | ||||||||||||||||||||||||
formatObjectKeys: | ||||||||||||||||||||||||
String(config.objectKeyFormat).toLowerCase() === "camelcase" | ||||||||||||||||||||||||
? camelcase | ||||||||||||||||||||||||
: undefined, | ||||||||||||||||||||||||
}); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
parsed.add(cur.relativePath); | ||||||||||||||||||||||||
result.set(cur.relativePath, codegenReturn); | ||||||||||||||||||||||||
}); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
printOrWrite(result, config); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
return result; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
// base.mol | ||
|
||
array RGB [byte;3]; | ||
vector UTF8String <byte>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
// character.mol | ||
|
||
import "base"; | ||
import "submodule/base"; | ||
|
||
// array RGB [byte;3]; | ||
// vector UTF8String <byte>; | ||
|
||
table Character { | ||
hair_color: RGB, | ||
hair_color_c: RGB4, | ||
name: UTF8String, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
// base.mol | ||
|
||
array RGB4 [byte;3]; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import test from "ava"; | ||
import { | ||
resolveDependencies, | ||
extractAndEraseImportClauses, | ||
} from "../src/resolve"; | ||
|
||
function expectResolveDependencies() { | ||
const generated = resolveDependencies( | ||
"character.mol", | ||
"./tests/mol", | ||
new Set() | ||
); | ||
|
||
return generated.length === 3 && generated[2].dependencies.length === 2; | ||
} | ||
|
||
test("dependencies length right", (t) => { | ||
t.true(expectResolveDependencies()); | ||
}); | ||
|
||
test("erase import base", (t) => { | ||
const result = extractAndEraseImportClauses(` | ||
import "base"; | ||
// import "submodule/base"; | ||
|
||
table Character { | ||
hair_color: RGB, | ||
} | ||
`); | ||
t.true(!result.includes(`import "base"`)); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When defining a function with similar parameters, like
importPath
,baseDir
, andresolvedRelativePath
, it is recommended to comment themThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good suggestion!