Skip to content

Commit df1d4af

Browse files
committed
migrate to handlebars, handle multiple env vars
1 parent dacf3b8 commit df1d4af

File tree

16 files changed

+285
-239
lines changed

16 files changed

+285
-239
lines changed

packages/cli/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
},
4949
"devDependencies": {
5050
"@types/command-exists": "^1.2.3",
51+
"@types/handlebars": "^4.1.0",
5152
"@types/inquirer": "^9.0.7",
5253
"@types/json2md": "^1.5.4",
5354
"@types/node": "^18.18.6",
@@ -77,12 +78,12 @@
7778
"boxen": "^7.1.1",
7879
"chalk": "^5.3.0",
7980
"cli-highlight": "^2.1.11",
80-
"code-block-writer": "^13.0.3",
8181
"command-exists": "^1.2.9",
8282
"commander": "^11.1.0",
8383
"console-table-printer": "^2.11.2",
8484
"dockerfile-ast": "^0.6.1",
8585
"e2b": "^2.0.0",
86+
"handlebars": "^4.7.8",
8687
"inquirer": "^9.2.12",
8788
"open": "^9.1.0",
8889
"statuses": "^2.0.1",

packages/cli/src/commands/template/migrate.ts

Lines changed: 105 additions & 204 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { select } from '@inquirer/prompts'
2-
import CodeBlockWriter from 'code-block-writer'
32
import * as commander from 'commander'
43
import { Template, TemplateBuilder, TemplateFinal } from 'e2b'
54
import * as fs from 'fs'
5+
import Handlebars from 'handlebars'
66
import * as path from 'path'
77
import { E2BConfig, getConfigPath, loadConfig } from 'src/config'
88
import { defaultDockerfileName } from 'src/docker/constants'
@@ -43,116 +43,114 @@ interface TemplateJSON {
4343
}>
4444
}
4545

46-
/**
47-
* Convert the JSON representation from Template.toJSON() to TypeScript code
48-
*/
49-
function jsonToTypeScript(
50-
json: TemplateJSON,
51-
alias: string,
52-
cpuCount?: number,
53-
memoryMB?: number
54-
): { templateContent: string; buildContent: string } {
55-
const templateWriter = new CodeBlockWriter({ indentNumberOfSpaces: 2 })
46+
// Register Handlebars helpers
47+
Handlebars.registerHelper('eq', function (a: any, b: any, options: any) {
48+
if (a === b) {
49+
// @ts-ignore - this context is provided by Handlebars
50+
return options.fn(this)
51+
}
52+
return ''
53+
})
5654

57-
// Template file
58-
templateWriter.writeLine("import { Template } from 'e2b'")
59-
templateWriter.blankLine()
60-
templateWriter.write('export const template = Template()')
55+
Handlebars.registerHelper('escapeQuotes', function (str) {
56+
return str ? str.replace(/'/g, "\\'") : str
57+
})
6158

62-
// Handle base image or template
63-
if (json.fromImage) {
64-
templateWriter.newLine().indent().write(`.fromImage('${json.fromImage}')`)
65-
} else {
66-
throw new Error('Unsupported template Dockerfile')
67-
}
59+
Handlebars.registerHelper('escapeDoubleQuotes', function (str) {
60+
return str ? str.replace(/"/g, '\\"') : str
61+
})
62+
63+
// Transform template JSON data for Handlebars
64+
function transformTemplateData(json: TemplateJSON) {
65+
const transformedSteps: any[] = []
6866

69-
// Process steps
7067
for (const step of json.steps) {
7168
switch (step.type) {
72-
case 'WORKDIR':
73-
templateWriter
74-
.newLine()
75-
.indent()
76-
.write(`.setWorkdir('${step.args[0]}')`)
77-
break
78-
case 'USER':
79-
templateWriter.newLine().indent().write(`.setUser('${step.args[0]}')`)
80-
break
8169
case 'ENV': {
82-
const envs: Record<string, string> = {}
70+
// Keep all environment variables from one ENV instruction together
71+
const envVars: Record<string, string> = {}
8372
for (let i = 0; i < step.args.length; i += 2) {
8473
if (i + 1 < step.args.length) {
85-
envs[step.args[i]] = step.args[i + 1]
74+
envVars[step.args[i]] = step.args[i + 1]
8675
}
8776
}
88-
if (Object.keys(envs).length > 0) {
89-
templateWriter
90-
.newLine()
91-
.setIndentationLevel(1)
92-
.write('.setEnvs(')
93-
.inlineBlock(() => {
94-
for (const [key, value] of Object.entries(envs)) {
95-
templateWriter.write(`'${key}': '${value}',`)
96-
}
97-
})
98-
.write(')')
99-
.setIndentationLevel(0)
100-
}
101-
break
102-
}
103-
case 'RUN': {
104-
templateWriter.newLine().indent().write(`.runCmd('${step.args[0]}')`)
77+
transformedSteps.push({
78+
type: 'ENV',
79+
envVars,
80+
})
10581
break
10682
}
107-
case 'COPY':
83+
case 'COPY': {
10884
if (step.args.length >= 2) {
10985
const src = step.args[0]
11086
let dest = step.args[step.args.length - 1]
111-
// Normalize empty or . destinations
11287
if (!dest || dest === '') {
11388
dest = '.'
11489
}
115-
templateWriter.newLine().indent().write(`.copy('${src}', '${dest}')`)
90+
transformedSteps.push({
91+
type: 'COPY',
92+
src,
93+
dest,
94+
})
11695
}
11796
break
97+
}
11898
default:
119-
// For unsupported instructions, add a comment
120-
templateWriter
121-
.newLine()
122-
.indent()
123-
.write(`// UNSUPPORTED: ${step.type} ${step.args.join(' ')}`)
99+
transformedSteps.push({
100+
type: step.type,
101+
args: step.args,
102+
})
124103
}
125104
}
126105

127-
// Handle start and ready commands from config
128-
if (json.startCmd && json.readyCmd) {
129-
const startCmd = json.startCmd.replace(/'/g, "\\'")
130-
const readyCmd = json.readyCmd.replace(/'/g, "\\'")
131-
templateWriter
132-
.newLine()
133-
.indent()
134-
.write(`.setStartCmd('${startCmd}', '${readyCmd}')`)
135-
} else if (json.readyCmd) {
136-
const readyCmd = json.readyCmd.replace(/'/g, "\\'")
137-
templateWriter.newLine().indent().write(`.setReadyCmd('${readyCmd}')`)
106+
return {
107+
...json,
108+
steps: transformedSteps,
109+
}
110+
}
111+
112+
/**
113+
* Convert the JSON representation from Template.toJSON() to TypeScript code
114+
*/
115+
function jsonToTypeScript(
116+
json: TemplateJSON,
117+
alias: string,
118+
cpuCount?: number,
119+
memoryMB?: number
120+
): { templateContent: string; buildContent: string } {
121+
const transformedData = transformTemplateData(json)
122+
123+
// Load and compile templates
124+
// When running from dist/index.js, templates are in dist/templates/
125+
const templatesDir = path.join(__dirname, 'templates')
126+
const templateSource = fs.readFileSync(
127+
path.join(templatesDir, 'typescript-template.hbs'),
128+
'utf8'
129+
)
130+
const buildSource = fs.readFileSync(
131+
path.join(templatesDir, 'typescript-build.hbs'),
132+
'utf8'
133+
)
134+
135+
const templateTemplate = Handlebars.compile(templateSource)
136+
const buildTemplate = Handlebars.compile(buildSource)
137+
138+
// Generate content
139+
const templateData = {
140+
...transformedData,
138141
}
139142

140-
// Generate build script
141-
const buildWriter = new CodeBlockWriter({ indentNumberOfSpaces: 2 })
143+
const templateContent = templateTemplate(templateData)
142144

143-
buildWriter.writeLine("import { Template } from 'e2b'")
144-
buildWriter.writeLine("import { template } from './template'")
145-
buildWriter.blankLine()
146-
buildWriter.write('await Template.build(template, ').inlineBlock(() => {
147-
buildWriter.writeLine(`alias: '${alias}',`)
148-
if (cpuCount) buildWriter.writeLine(`cpuCount: ${cpuCount},`)
149-
if (memoryMB) buildWriter.writeLine(`memoryMB: ${memoryMB},`)
145+
const buildContent = buildTemplate({
146+
alias,
147+
cpuCount,
148+
memoryMB,
150149
})
151-
buildWriter.write(')')
152150

153151
return {
154-
templateContent: templateWriter.toString(),
155-
buildContent: buildWriter.toString(),
152+
templateContent: templateContent.trim(),
153+
buildContent: buildContent.trim(),
156154
}
157155
}
158156

@@ -166,135 +164,38 @@ function jsonToPython(
166164
memoryMB?: number,
167165
isAsync: boolean = false
168166
): { templateContent: string; buildContent: string } {
169-
const templateWriter = new CodeBlockWriter({
170-
indentNumberOfSpaces: 4,
171-
useTabs: false,
172-
})
173-
const templateImportClass = isAsync ? 'AsyncTemplate' : 'Template'
174-
175-
// Template file
176-
templateWriter.writeLine(`from e2b import ${templateImportClass}`)
177-
templateWriter.blankLine()
178-
templateWriter.write('template = (')
179-
templateWriter.newLine().indent().write(`${templateImportClass}()`)
180-
181-
// Handle base image or template
182-
if (json.fromImage) {
183-
templateWriter.newLine().indent().write(`.from_image("${json.fromImage}")`)
184-
} else {
185-
throw new Error('Unsupported template Dockerfile')
186-
}
187-
188-
// Process steps
189-
for (const step of json.steps) {
190-
switch (step.type) {
191-
case 'WORKDIR':
192-
templateWriter
193-
.newLine()
194-
.indent()
195-
.write(`.set_workdir("${step.args[0]}")`)
196-
break
197-
case 'USER':
198-
templateWriter.newLine().indent().write(`.set_user("${step.args[0]}")`)
199-
break
200-
case 'ENV': {
201-
templateWriter.newLine().indent().write('.set_envs({')
202-
for (let i = 0; i < step.args.length; i += 2) {
203-
if (i + 1 < step.args.length) {
204-
templateWriter
205-
.newLine()
206-
.indent(2)
207-
.write(`"${step.args[i]}": "${step.args[i + 1]}",`)
208-
}
209-
}
210-
templateWriter.newLine().indent().write('})')
211-
break
212-
}
213-
case 'RUN': {
214-
templateWriter.newLine().indent().write(`.run_cmd("${step.args[0]}")`)
215-
break
216-
}
217-
case 'COPY':
218-
if (step.args.length >= 2) {
219-
const src = step.args[0]
220-
let dest = step.args[step.args.length - 1]
221-
// Normalize empty or . destinations
222-
if (!dest || dest === '') {
223-
dest = '.'
224-
}
225-
templateWriter.newLine().indent().write(`.copy("${src}", "${dest}")`)
226-
}
227-
break
228-
default:
229-
// For unsupported instructions, add a comment
230-
templateWriter
231-
.newLine()
232-
.indent()
233-
.write(`// UNSUPPORTED: ${step.type} ${step.args.join(' ')}`)
234-
}
235-
}
236-
237-
// Handle start and ready commands from config
238-
if (json.startCmd && json.readyCmd) {
239-
const startCmd = json.startCmd.replace(/"/g, '\\"')
240-
const readyCmd = json.readyCmd.replace(/"/g, '\\"')
241-
templateWriter
242-
.newLine()
243-
.indent()
244-
.write(`.set_start_cmd("${startCmd}", "${readyCmd}")`)
245-
} else if (json.readyCmd) {
246-
const readyCmd = json.readyCmd.replace(/"/g, '\\"')
247-
templateWriter.newLine().indent().write(`.set_ready_cmd("${readyCmd}")`)
248-
}
167+
const transformedData = transformTemplateData(json)
168+
169+
// Load and compile templates
170+
// When running from dist/index.js, templates are in dist/templates/
171+
const templatesDir = path.join(__dirname, 'templates')
172+
const templateSource = fs.readFileSync(
173+
path.join(templatesDir, 'python-template.hbs'),
174+
'utf8'
175+
)
176+
const buildSource = fs.readFileSync(
177+
path.join(templatesDir, `python-build-${isAsync ? 'async' : 'sync'}.hbs`),
178+
'utf8'
179+
)
249180

250-
templateWriter.writeLine(')')
181+
const templateTemplate = Handlebars.compile(templateSource)
182+
const buildTemplate = Handlebars.compile(buildSource)
251183

252-
// Generate build script
253-
const buildWriter = new CodeBlockWriter({
254-
indentNumberOfSpaces: 4,
255-
useTabs: false,
184+
// Generate content
185+
const templateContent = templateTemplate({
186+
...transformedData,
187+
isAsync,
256188
})
257189

258-
if (isAsync) {
259-
buildWriter.writeLine('import asyncio')
260-
buildWriter.writeLine(`from e2b import ${templateImportClass}`)
261-
buildWriter.writeLine('from template import template')
262-
buildWriter.blankLine()
263-
buildWriter.blankLine()
264-
buildWriter.writeLine('async def main():')
265-
buildWriter.setIndentationLevel(1)
266-
buildWriter.writeLine(`await ${templateImportClass}.build(`)
267-
buildWriter.setIndentationLevel(2)
268-
buildWriter.writeLine('template,')
269-
buildWriter.writeLine(`alias="${alias}",`)
270-
if (cpuCount) buildWriter.writeLine(`cpu_count=${cpuCount},`)
271-
if (memoryMB) buildWriter.writeLine(`memory_mb=${memoryMB},`)
272-
buildWriter.setIndentationLevel(1)
273-
buildWriter.writeLine(')')
274-
buildWriter.blankLine()
275-
buildWriter.blankLine()
276-
buildWriter.setIndentationLevel(0)
277-
buildWriter.writeLine('if __name__ == "__main__":')
278-
buildWriter.setIndentationLevel(1)
279-
buildWriter.writeLine('asyncio.run(main())')
280-
} else {
281-
buildWriter.writeLine(`from e2b import ${templateImportClass}`)
282-
buildWriter.writeLine('from template import template')
283-
buildWriter.blankLine()
284-
buildWriter.blankLine()
285-
buildWriter.writeLine(`${templateImportClass}.build(`)
286-
buildWriter.setIndentationLevel(1)
287-
buildWriter.writeLine('template,')
288-
buildWriter.writeLine(`alias="${alias}",`)
289-
if (cpuCount) buildWriter.writeLine(`cpu_count=${cpuCount},`)
290-
if (memoryMB) buildWriter.writeLine(`memory_mb=${memoryMB},`)
291-
buildWriter.setIndentationLevel(0)
292-
buildWriter.writeLine(')')
293-
}
190+
const buildContent = buildTemplate({
191+
alias,
192+
cpuCount,
193+
memoryMB,
194+
})
294195

295196
return {
296-
templateContent: templateWriter.toString(),
297-
buildContent: buildWriter.toString(),
197+
templateContent: templateContent.trim(),
198+
buildContent: buildContent.trim(),
298199
}
299200
}
300201

0 commit comments

Comments
 (0)