Skip to content

Commit 03faeb9

Browse files
committed
update template init for new templates SDK
1 parent 17317b7 commit 03faeb9

File tree

10 files changed

+1040
-329
lines changed

10 files changed

+1040
-329
lines changed

packages/cli/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"@types/inquirer": "^9.0.7",
5353
"@types/json2md": "^1.5.4",
5454
"@types/node": "^18.18.6",
55+
"@types/npmcli__package-json": "^4.0.4",
5556
"@types/statuses": "^2.0.5",
5657
"@types/update-notifier": "6.0.5",
5758
"@vitest/coverage-v8": "^3.2.4",
@@ -74,6 +75,7 @@
7475
"dependencies": {
7576
"@iarna/toml": "^2.2.5",
7677
"@inquirer/prompts": "^5.5.0",
78+
"@npmcli/package-json": "^5.2.1",
7779
"async-listen": "^3.0.1",
7880
"boxen": "^7.1.1",
7981
"chalk": "^5.3.0",
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import * as fs from 'fs'
2+
import * as path from 'path'
3+
4+
/**
5+
* Generate unique file names to avoid overwriting existing files
6+
*/
7+
export function getUniqueFileName(
8+
directory: string,
9+
baseName: string,
10+
extension: string
11+
): string {
12+
let fileName = `${baseName}${extension}`
13+
let counter = 1
14+
15+
while (fs.existsSync(path.join(directory, fileName))) {
16+
fileName = `${baseName}-${counter}${extension}`
17+
counter++
18+
}
19+
20+
return fileName
21+
}
22+
23+
/**
24+
* Write content to a file, creating directories if needed
25+
*/
26+
export async function writeFileContent(
27+
filePath: string,
28+
content: string
29+
): Promise<void> {
30+
const dir = path.dirname(filePath)
31+
32+
// Ensure directory exists
33+
if (!fs.existsSync(dir)) {
34+
await fs.promises.mkdir(dir, { recursive: true })
35+
}
36+
37+
await fs.promises.writeFile(filePath, content)
38+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { Template } from 'e2b'
2+
import * as fs from 'fs'
3+
import Handlebars from 'handlebars'
4+
import * as path from 'path'
5+
import { TemplateJSON, TemplateType } from './types'
6+
7+
// Track if helpers are registered to avoid duplicate registration
8+
let helpersRegistered = false
9+
10+
export function registerHandlebarsHelpers() {
11+
if (helpersRegistered) return
12+
13+
Handlebars.registerHelper('eq', function (a: any, b: any, options: any) {
14+
if (a === b) {
15+
// @ts-ignore - this context is provided by Handlebars
16+
return options.fn(this)
17+
}
18+
return ''
19+
})
20+
21+
Handlebars.registerHelper('escapeQuotes', function (str) {
22+
return str ? str.replace(/'/g, "\\'") : str
23+
})
24+
25+
Handlebars.registerHelper('escapeDoubleQuotes', function (str) {
26+
return str ? str.replace(/"/g, '\\"') : str
27+
})
28+
29+
helpersRegistered = true
30+
}
31+
32+
/**
33+
* Transform template data for Handlebars
34+
*/
35+
export function transformTemplateData(template: TemplateType) {
36+
// Extract JSON structure from parsed template
37+
const json = JSON.parse(Template.toJSON(template, false)) as TemplateJSON
38+
39+
const transformedSteps: any[] = []
40+
41+
for (const step of json.steps) {
42+
switch (step.type) {
43+
case 'ENV': {
44+
// Keep all environment variables from one ENV instruction together
45+
const envVars: Record<string, string> = {}
46+
for (let i = 0; i < step.args.length; i += 2) {
47+
if (i + 1 < step.args.length) {
48+
envVars[step.args[i]] = step.args[i + 1]
49+
}
50+
}
51+
transformedSteps.push({
52+
type: 'ENV',
53+
envVars,
54+
})
55+
break
56+
}
57+
case 'COPY': {
58+
if (step.args.length >= 2) {
59+
const src = step.args[0]
60+
let dest = step.args[step.args.length - 1]
61+
if (!dest || dest === '') {
62+
dest = '.'
63+
}
64+
transformedSteps.push({
65+
type: 'COPY',
66+
src,
67+
dest,
68+
})
69+
}
70+
break
71+
}
72+
default:
73+
transformedSteps.push({
74+
type: step.type,
75+
args: step.args,
76+
})
77+
}
78+
}
79+
80+
return {
81+
...json,
82+
steps: transformedSteps,
83+
}
84+
}
85+
86+
/**
87+
* Convert the template to TypeScript code using Handlebars
88+
*/
89+
export function generateTypeScriptCode(
90+
template: TemplateType,
91+
alias: string,
92+
cpuCount?: number,
93+
memoryMB?: number
94+
): { templateContent: string; buildContent: string } {
95+
registerHandlebarsHelpers()
96+
const transformedData = transformTemplateData(template)
97+
98+
// Load and compile templates
99+
// In dist, templates are at dist/templates/, __dirname is dist/
100+
const templatesDir = path.join(__dirname, 'templates')
101+
const templateSource = fs.readFileSync(
102+
path.join(templatesDir, 'typescript-template.hbs'),
103+
'utf8'
104+
)
105+
const buildSource = fs.readFileSync(
106+
path.join(templatesDir, 'typescript-build.hbs'),
107+
'utf8'
108+
)
109+
110+
const templateTemplate = Handlebars.compile(templateSource)
111+
const buildTemplate = Handlebars.compile(buildSource)
112+
113+
// Generate content
114+
const templateData = {
115+
...transformedData,
116+
}
117+
118+
const templateContent = templateTemplate(templateData)
119+
120+
const buildContent = buildTemplate({
121+
alias,
122+
cpuCount,
123+
memoryMB,
124+
})
125+
126+
return {
127+
templateContent: templateContent.trim(),
128+
buildContent: buildContent.trim(),
129+
}
130+
}
131+
132+
/**
133+
* Convert the template to Python code using Handlebars
134+
*/
135+
export function generatePythonCode(
136+
template: TemplateType,
137+
alias: string,
138+
cpuCount?: number,
139+
memoryMB?: number,
140+
isAsync: boolean = false
141+
): { templateContent: string; buildContent: string } {
142+
registerHandlebarsHelpers()
143+
const transformedData = transformTemplateData(template)
144+
145+
// Load and compile templates
146+
// In dist, templates are at dist/templates/, __dirname is dist/
147+
const templatesDir = path.join(__dirname, 'templates')
148+
const templateSource = fs.readFileSync(
149+
path.join(templatesDir, 'python-template.hbs'),
150+
'utf8'
151+
)
152+
const buildSource = fs.readFileSync(
153+
path.join(templatesDir, `python-build-${isAsync ? 'async' : 'sync'}.hbs`),
154+
'utf8'
155+
)
156+
157+
const templateTemplate = Handlebars.compile(templateSource)
158+
const buildTemplate = Handlebars.compile(buildSource)
159+
160+
// Generate content
161+
const templateContent = templateTemplate({
162+
...transformedData,
163+
isAsync,
164+
})
165+
166+
const buildContent = buildTemplate({
167+
alias,
168+
cpuCount,
169+
memoryMB,
170+
})
171+
172+
return {
173+
templateContent: templateContent.trim(),
174+
buildContent: buildContent.trim(),
175+
}
176+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Re-export all the public APIs from the lib modules
2+
export * from './types'
3+
export * from './template-generator'
4+
export * from './file-utils'
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import * as path from 'path'
2+
import { asLocalRelative, asPrimary } from 'src/utils/format'
3+
import {
4+
Language,
5+
TemplateType,
6+
GeneratedFiles,
7+
languageDisplay,
8+
} from './types'
9+
import { generateTypeScriptCode, generatePythonCode } from './handlebars'
10+
import { getUniqueFileName, writeFileContent } from './file-utils'
11+
12+
/**
13+
* Generate and write template files for a given language
14+
*/
15+
export async function generateAndWriteTemplateFiles(
16+
root: string,
17+
alias: string,
18+
language: Language,
19+
template: TemplateType,
20+
cpuCount?: number,
21+
memoryMB?: number
22+
): Promise<GeneratedFiles> {
23+
if (language === Language.TypeScript) {
24+
const { templateContent, buildContent: buildDevContent } =
25+
generateTypeScriptCode(template, `${alias}-dev`, cpuCount, memoryMB)
26+
const { buildContent: buildProdContent } = generateTypeScriptCode(
27+
template,
28+
alias,
29+
cpuCount,
30+
memoryMB
31+
)
32+
33+
const extension = '.ts'
34+
const templateFile = getUniqueFileName(root, 'template', extension)
35+
const buildDevFile = getUniqueFileName(root, 'build.dev', extension)
36+
const buildProdFile = getUniqueFileName(root, 'build.prod', extension)
37+
38+
await writeFileContent(path.join(root, templateFile), templateContent)
39+
await writeFileContent(path.join(root, buildDevFile), buildDevContent)
40+
await writeFileContent(path.join(root, buildProdFile), buildProdContent)
41+
42+
console.log(
43+
`\n✅ Generated ${asPrimary(
44+
languageDisplay[Language.TypeScript]
45+
)} template files:`
46+
)
47+
console.log(` ${asLocalRelative(templateFile)}`)
48+
console.log(` ${asLocalRelative(buildDevFile)}`)
49+
console.log(` ${asLocalRelative(buildProdFile)}`)
50+
51+
return { templateFile, buildDevFile, buildProdFile, language }
52+
} else {
53+
const isAsync = language === Language.PythonAsync
54+
const { templateContent, buildContent: buildDevContent } =
55+
generatePythonCode(template, `${alias}-dev`, cpuCount, memoryMB, isAsync)
56+
const { buildContent: buildProdContent } = generatePythonCode(
57+
template,
58+
alias,
59+
cpuCount,
60+
memoryMB,
61+
isAsync
62+
)
63+
64+
const extension = '.py'
65+
const templateFile = getUniqueFileName(root, 'template', extension)
66+
const buildDevFile = getUniqueFileName(root, 'build_dev', extension)
67+
const buildProdFile = getUniqueFileName(root, 'build_prod', extension)
68+
69+
await writeFileContent(path.join(root, templateFile), templateContent)
70+
await writeFileContent(path.join(root, buildDevFile), buildDevContent)
71+
await writeFileContent(path.join(root, buildProdFile), buildProdContent)
72+
73+
console.log(
74+
`\n✅ Generated ${asPrimary(languageDisplay[language])} template files:`
75+
)
76+
console.log(` ${asLocalRelative(templateFile)}`)
77+
console.log(` ${asLocalRelative(buildDevFile)}`)
78+
console.log(` ${asLocalRelative(buildProdFile)}`)
79+
80+
return { templateFile, buildDevFile, buildProdFile, language }
81+
}
82+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { TemplateBuilder, TemplateFinal } from 'e2b'
2+
3+
export enum Language {
4+
TypeScript = 'typescript',
5+
PythonSync = 'python-sync',
6+
PythonAsync = 'python-async',
7+
}
8+
9+
export const validLanguages: Language[] = [
10+
Language.TypeScript,
11+
Language.PythonSync,
12+
Language.PythonAsync,
13+
]
14+
15+
export const languageDisplay = {
16+
[Language.TypeScript]: 'TypeScript',
17+
[Language.PythonSync]: 'Python (sync)',
18+
[Language.PythonAsync]: 'Python (async)',
19+
}
20+
21+
export type TemplateType = TemplateBuilder | TemplateFinal
22+
23+
export interface TemplateJSON {
24+
fromImage?: string
25+
fromTemplate?: string
26+
startCmd?: string
27+
readyCmd?: string
28+
force: boolean
29+
steps: Array<{
30+
type: string
31+
args: string[]
32+
filesHash?: string
33+
force?: boolean
34+
}>
35+
}
36+
37+
export interface GeneratedFiles {
38+
templateFile: string
39+
buildDevFile: string
40+
buildProdFile: string
41+
language: Language
42+
}

0 commit comments

Comments
 (0)