Skip to content

Commit a5a232d

Browse files
authored
feat: add codegen utility (#18708)
1 parent 0501452 commit a5a232d

File tree

20 files changed

+558
-104
lines changed

20 files changed

+558
-104
lines changed

packages/app/cypress/e2e/integration/new-spec.spec.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,17 @@ describe('Button', () => {
3333
const { Primary } = composedStories
3434
mount(<Primary />)
3535
})
36-
36+
3737
it('should render Secondary', () => {
3838
const { Secondary } = composedStories
3939
mount(<Secondary />)
4040
})
41-
41+
4242
it('should render Large', () => {
4343
const { Large } = composedStories
4444
mount(<Large />)
4545
})
46-
46+
4747
it('should render Small', () => {
4848
const { Small } = composedStories
4949
mount(<Small />)
@@ -54,7 +54,7 @@ import Button from "./Button"
5454
5555
describe('<Button />', () => {
5656
it('renders', () => {
57-
see: https://reactjs.org/docs/test-utils.html
57+
// see: https://reactjs.org/docs/test-utils.html
5858
mount(<Button />)
5959
})
6060
})`,

packages/data-context/__snapshots__/data-context.spec.ts.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
exports['@packages/data-context initializeData should initialize 1'] = {
1+
exports['@packages/data-context initializeData initializes 1'] = {
22
"shellConfig": {
33
"launchOptions": {},
44
"launchArgs": {},

packages/data-context/package.json

+4
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,16 @@
2222
"cross-fetch": "^3.1.4",
2323
"dataloader": "^2.0.0",
2424
"dedent": "^0.7.0",
25+
"ejs": "^3.1.6",
2526
"electron": "14.1.0",
2627
"endent": "2.0.1",
2728
"execa": "1.0.0",
29+
"front-matter": "^4.0.2",
2830
"fs-extra": "8.1.0",
2931
"getenv": "1.0.0",
3032
"globby": "^11.0.1",
3133
"graphql": "^15.5.1",
34+
"isbinaryfile": "^4.0.8",
3235
"lodash": "4.17.21",
3336
"p-defer": "^3.0.0",
3437
"wonka": "^4.0.15"
@@ -38,6 +41,7 @@
3841
"@packages/ts": "0.0.0-development",
3942
"@packages/types": "0.0.0-development",
4043
"@types/dedent": "^0.7.0",
44+
"@types/ejs": "^3.1.0",
4145
"mocha": "7.0.1",
4246
"rimraf": "3.0.2"
4347
},

packages/data-context/src/actions/ProjectActions.ts

+17-6
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import path from 'path'
44
import type { ProjectShape } from '../data/coreDataShape'
55

66
import type { DataContext } from '..'
7-
import { SpecGenerator } from '../codegen'
7+
import { codeGenerator, SpecOptions } from '../codegen'
8+
import templates from '../codegen/templates'
89

910
export interface ProjectApiShape {
1011
getConfig(projectRoot: string): Promise<FullConfig>
@@ -290,16 +291,26 @@ export class ProjectActions {
290291
const codeGenPath = getCodeGenPath()
291292
const searchFolder = getSearchFolder()
292293

293-
const { specContent, specAbsolute } = await new SpecGenerator(this.ctx, {
294+
const newSpecCodeGenOptions = new SpecOptions(this.ctx, {
294295
codeGenPath,
295296
codeGenType,
296297
specFileExtension,
297-
}).generateSpec()
298+
})
299+
300+
const codeGenOptions = await newSpecCodeGenOptions.getCodeGenOptions()
301+
const codeGenResults = await codeGenerator(
302+
{ templateDir: templates[codeGenType], target: path.parse(codeGenPath).dir },
303+
codeGenOptions,
304+
)
305+
306+
if (!codeGenResults.files[0] || codeGenResults.failed[0]) {
307+
throw (codeGenResults.failed[0] || 'Unable to generate spec')
308+
}
298309

299-
await this.ctx.fs.outputFile(specAbsolute, specContent)
310+
const [newSpec] = codeGenResults.files
300311

301312
const spec = this.ctx.file.normalizeFileToSpec({
302-
absolute: specAbsolute,
313+
absolute: newSpec.file,
303314
searchFolder,
304315
specType: codeGenType === 'integration' ? 'integration' : 'component',
305316
projectRoot: project.projectRoot,
@@ -308,7 +319,7 @@ export class ProjectActions {
308319

309320
project.generatedSpec = {
310321
spec,
311-
content: specContent,
322+
content: newSpec.content,
312323
}
313324
}
314325
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import * as fs from 'fs-extra'
2+
import { isBinaryFile } from 'isbinaryfile'
3+
import * as path from 'path'
4+
import * as ejs from 'ejs'
5+
import fm from 'front-matter'
6+
7+
export interface Action {
8+
templateDir: string
9+
target: string
10+
overwrite?: boolean
11+
}
12+
13+
export interface CodeGenResult {
14+
status: 'add' | 'overwrite' | 'skipped'
15+
type: 'text' | 'binary'
16+
file: string
17+
content: string
18+
}
19+
20+
export interface CodeGenResults {
21+
files: Array<CodeGenResult>
22+
failed: Array<Error>
23+
}
24+
25+
/**
26+
* Utility for generating files from ejs templates or for general scaffolding purposes.
27+
* Given a templte directory, all files within will be moved to the target directory specified whilst
28+
* maintaining the folder heirarchy. It supports both text and binary files, with text files having the
29+
* additional ablity to be rendered with .ejs support meaning any arguments passed in can be interpolated
30+
* into the file. For custom file naming, front-matter can be used to specify the output fileName.
31+
*/
32+
export async function codeGenerator (
33+
action: Action,
34+
args: { [key: string]: any },
35+
): Promise<CodeGenResults> {
36+
const templateFiles = await allFilesInDir(action.templateDir)
37+
const codeGenResults: CodeGenResults = { files: [], failed: [] }
38+
39+
for (const file of templateFiles) {
40+
const isBinary = await isBinaryFile(file)
41+
const parsedFile = path.parse(file)
42+
43+
const processBinaryFile = async () => {
44+
const rawFileContent = await fs.readFile(file)
45+
const computedPath = computePath(
46+
action.templateDir,
47+
action.target,
48+
file,
49+
args,
50+
)
51+
52+
return { computedPath, content: rawFileContent, type: 'binary' } as const
53+
}
54+
55+
const processTextFile = async () => {
56+
const fileContent = (await fs.readFile(file)).toString()
57+
const { body, renderedAttributes } = frontMatter(fileContent, args)
58+
const computedPath = computePath(
59+
action.templateDir,
60+
action.target,
61+
path.join(
62+
parsedFile.dir,
63+
renderedAttributes.fileName || parsedFile.base,
64+
),
65+
args,
66+
)
67+
const renderedTemplate = ejs.render(body, args)
68+
69+
return { computedPath, content: renderedTemplate, type: 'text' } as const
70+
}
71+
72+
try {
73+
const { content, computedPath, type } = isBinary
74+
? await processBinaryFile()
75+
: await processTextFile()
76+
77+
const exists = await fileExists(computedPath)
78+
const status = !exists
79+
? 'add'
80+
: exists && action.overwrite
81+
? 'overwrite'
82+
: 'skipped'
83+
84+
if (status === 'add' || status === 'overwrite') {
85+
await fs.outputFile(computedPath, content)
86+
}
87+
88+
codeGenResults.files.push({
89+
file: computedPath,
90+
type,
91+
status,
92+
content: content.toString(),
93+
})
94+
} catch (e) {
95+
codeGenResults.failed.push(e as Error)
96+
}
97+
}
98+
99+
return codeGenResults
100+
}
101+
102+
function computePath (
103+
srcFolder: string,
104+
target: string,
105+
filePath: string,
106+
substitutions: { [k: string]: any },
107+
): string {
108+
const relativeFromSrcFolder = path.relative(srcFolder, filePath)
109+
let computedPath = path.join(target, relativeFromSrcFolder)
110+
111+
Object.entries(substitutions).forEach(([propertyName, value]) => {
112+
computedPath = computedPath.split(`{{${propertyName}}}`).join(value)
113+
})
114+
115+
return computedPath
116+
}
117+
118+
async function allFilesInDir (parent: string): Promise<string[]> {
119+
let res: string[] = []
120+
121+
for (const dir of await fs.readdir(parent)) {
122+
const child = path.join(parent, dir)
123+
const isDir = (await fs.stat(child)).isDirectory()
124+
125+
if (!isDir) {
126+
res.push(child)
127+
} else {
128+
res = [...res, ...(await allFilesInDir(child))]
129+
}
130+
}
131+
132+
return res
133+
}
134+
135+
function frontMatter (content: string, args: { [key: string]: any }) {
136+
const { attributes, body } = fm(content, { allowUnsafe: true }) as {
137+
attributes: { [key: string]: string }
138+
body: string
139+
}
140+
const renderedAttributes = Object.entries(attributes).reduce(
141+
(acc, [key, val]) => ({ ...acc, [key]: ejs.render(val, args) }),
142+
{} as { [key: string]: string },
143+
)
144+
145+
return { body, renderedAttributes }
146+
}
147+
148+
async function fileExists (absolute: string) {
149+
try {
150+
await fs.access(absolute, fs.constants.F_OK)
151+
152+
return true
153+
} catch (e) {
154+
return false
155+
}
156+
}
+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/* eslint-disable padding-line-between-statements */
22
// created by autobarrel, do not modify directly
33

4+
export * from './code-generator'
45
export * from './sample-config-files'
5-
export * from './spec-generator'
6+
export * from './spec-options'
7+
export * from './templates'

0 commit comments

Comments
 (0)