Skip to content

Commit 27ebd4b

Browse files
lukeclaude
andcommitted
Implement build system to update runtime code from source
This commit adds the ability to build the source code and update the runtime code in the HTML file directly: 1. Added `updateRuntimeCode` method to BundleOperations to update the runtime code in HTML 2. Added markers in HTML template to identify runtime code section 3. Added WebContainer build capabilities to compile projects 4. Added UI for building and updating runtime code 5. Added comprehensive tests for the new functionality This completes another phase in the implementation plan, allowing users to make changes to source code, build the project in WebContainer, and update the runtime code in the HTML file. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent d762371 commit 27ebd4b

File tree

7 files changed

+380
-0
lines changed

7 files changed

+380
-0
lines changed

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ <h3>WebContainer Dev Environment</h3>
195195
<button id="installDepsBtn" class="button" disabled>Install Dependencies</button>
196196
<button id="startServerBtn" class="button" disabled>Start Server</button>
197197
<button id="runCommandBtn" class="button" disabled>Run Command</button>
198+
<button id="buildProjectBtn" class="button" disabled>Build & Update Runtime</button>
198199
</div>
199200
<div id="webcontainer-status">
200201
<p>WebContainer not started.</p>

src/core/bundle-generator.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,12 +213,14 @@ export function generateStandaloneBundle(
213213
</script>
214214
215215
<!-- Core Application Script -->
216+
<!-- RUNTIME_CODE_START -->
216217
<script>
217218
// Core application functionality that works in all environments
218219
document.addEventListener('DOMContentLoaded', function() {
219220
${mainScript}
220221
});
221222
</script>
223+
<!-- RUNTIME_CODE_END -->
222224
223225
${devToolsLoader}
224226
</body>

src/core/bundle-operations.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,39 @@ import { getTemplate } from './templates';
2121
* Implementation of core bundle operations
2222
*/
2323
export class BundleOperationsImpl implements BundleOperations {
24+
/**
25+
* Updates the runtime code in an HTML bundle with new built code
26+
* @param htmlContent The original HTML content
27+
* @param runtimeCode The new runtime code to inject
28+
* @param options Options for the update
29+
* @returns The updated HTML content
30+
*/
31+
async updateRuntimeCode(
32+
htmlContent: string,
33+
runtimeCode: string,
34+
options: {
35+
runtimeCodeStartMarker?: string;
36+
runtimeCodeEndMarker?: string;
37+
} = {}
38+
): Promise<string> {
39+
// Default markers for the runtime code section
40+
const startMarker = options.runtimeCodeStartMarker || '<!-- RUNTIME_CODE_START -->';
41+
const endMarker = options.runtimeCodeEndMarker || '<!-- RUNTIME_CODE_END -->';
42+
43+
// Find the runtime code section
44+
const runtimeCodeRegex = new RegExp(`${startMarker}(.*?)${endMarker}`, 's');
45+
const match = htmlContent.match(runtimeCodeRegex);
46+
47+
if (!match) {
48+
throw new Error('Runtime code section not found in HTML content');
49+
}
50+
51+
// Replace the runtime code section with the new code
52+
return htmlContent.replace(
53+
runtimeCodeRegex,
54+
`${startMarker}${runtimeCode}${endMarker}`
55+
);
56+
}
2457
/**
2558
* Creates a source bundle from a directory
2659
* @param directory The directory to bundle

src/core/types.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,20 @@ export interface BundleOperations {
6666
* @param htmlContent The HTML content
6767
*/
6868
extractBundleFromHtml(htmlContent: string): Promise<SourceBundle>;
69+
70+
/**
71+
* Updates the runtime code in an HTML bundle with new built code
72+
* @param htmlContent The original HTML content
73+
* @param runtimeCode The new runtime code to inject
74+
* @param options Options for the update
75+
* @returns The updated HTML content
76+
*/
77+
updateRuntimeCode(
78+
htmlContent: string,
79+
runtimeCode: string,
80+
options?: {
81+
runtimeCodeStartMarker?: string;
82+
runtimeCodeEndMarker?: string;
83+
}
84+
): Promise<string>;
6985
}

src/main.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,12 @@ async function handleStartWebContainer() {
798798
runCommandBtn.removeAttribute('disabled');
799799
}
800800

801+
// Enable the build button if it exists
802+
const buildProjectBtn = document.getElementById('buildProjectBtn');
803+
if (buildProjectBtn) {
804+
buildProjectBtn.removeAttribute('disabled');
805+
}
806+
801807
updateWebContainerStatus('WebContainer initialized and files mounted', 'success');
802808
} catch (error) {
803809
console.error('Error starting WebContainer:', error);
@@ -906,6 +912,67 @@ async function handleRunCommand() {
906912
}
907913
}
908914

915+
// Handle building the project and updating runtime code
916+
async function handleBuildProject() {
917+
updateWebContainerStatus('Building project and updating runtime code...', 'info');
918+
919+
try {
920+
// 1. Build the project in WebContainer
921+
const buildResult = await webContainerManager.buildProject();
922+
923+
if (!buildResult.success) {
924+
updateWebContainerStatus('Build failed, cannot update runtime code', 'error');
925+
return;
926+
}
927+
928+
// 2. Extract the built code from WebContainer
929+
const distDir = buildResult.distDir || 'dist';
930+
const runtimeCode = await webContainerManager.getBuiltRuntimeCode(distDir);
931+
932+
if (!runtimeCode) {
933+
updateWebContainerStatus('Failed to extract built code from WebContainer', 'error');
934+
return;
935+
}
936+
937+
// 3. Use File System API to get current HTML content
938+
const currentHtml = await fileSystemManager.readFile();
939+
940+
if (!currentHtml) {
941+
updateWebContainerStatus('Failed to read current HTML file', 'error');
942+
return;
943+
}
944+
945+
// 4. Use the BundleOperations to update the runtime code
946+
try {
947+
// Import at runtime to avoid circular dependency
948+
const { BundleOperationsImpl } = await import('./core/bundle-operations');
949+
const bundleOps = new BundleOperationsImpl();
950+
951+
// Update the runtime code in the HTML
952+
const updatedHtml = await bundleOps.updateRuntimeCode(currentHtml, runtimeCode);
953+
954+
// 5. Write the updated HTML back to the file
955+
const success = await fileSystemManager.writeFile(updatedHtml);
956+
957+
if (success) {
958+
updateWebContainerStatus(
959+
'Successfully built project and updated runtime code. ' +
960+
'<a href="#" onclick="location.reload()">Reload the page</a> to see changes.',
961+
'success'
962+
);
963+
} else {
964+
updateWebContainerStatus('Failed to save updated HTML file', 'error');
965+
}
966+
} catch (error) {
967+
console.error('Error updating runtime code:', error);
968+
updateWebContainerStatus(`Error updating runtime code: ${error instanceof Error ? error.message : String(error)}`, 'error');
969+
}
970+
} catch (error) {
971+
console.error('Error in build process:', error);
972+
updateWebContainerStatus(`Error in build process: ${error instanceof Error ? error.message : String(error)}`, 'error');
973+
}
974+
}
975+
909976
// Handle refreshing the preview
910977
function handleRefreshPreview() {
911978
const previewFrame = document.getElementById('preview-frame') as HTMLIFrameElement;
@@ -1000,6 +1067,9 @@ function setupDevToolsEventListeners() {
10001067
case 'openPreviewBtn':
10011068
handleOpenPreview();
10021069
break;
1070+
case 'buildProjectBtn':
1071+
handleBuildProject();
1072+
break;
10031073
}
10041074
}
10051075

src/webcontainer.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,169 @@ export class WebContainerManager {
870870
return this.state.isServerRunning;
871871
}
872872

873+
/**
874+
* Builds the project in the WebContainer
875+
* @param buildScript - The name of the build script in package.json (e.g., "build")
876+
* @returns Promise resolving to a success indicator and output
877+
*/
878+
public async buildProject(buildScript: string = 'build'): Promise<{
879+
success: boolean;
880+
output: string;
881+
distDir?: string;
882+
}> {
883+
if (!this.webcontainerInstance) {
884+
return {
885+
success: false,
886+
output: 'WebContainer not initialized'
887+
};
888+
}
889+
890+
this.writeToTerminal(`\n🔨 Building project with: ${this.serverConfig.command} run ${buildScript}\n\n`);
891+
892+
try {
893+
// Run the build command
894+
const buildProcess = await this.webcontainerInstance.spawn(
895+
this.serverConfig.command,
896+
['run', buildScript]
897+
);
898+
899+
let output = '';
900+
901+
// Collect output
902+
buildProcess.output.pipeTo(
903+
new WritableStream({
904+
write: (chunk) => {
905+
output += chunk;
906+
this.writeToTerminal(chunk);
907+
}
908+
})
909+
);
910+
911+
// Wait for build to complete
912+
const exitCode = await buildProcess.exit;
913+
914+
if (exitCode !== 0) {
915+
this.writeToTerminal(`\n❌ Build failed with exit code ${exitCode}\n`);
916+
return {
917+
success: false,
918+
output
919+
};
920+
}
921+
922+
this.writeToTerminal(`\n✅ Build completed successfully\n`);
923+
924+
// Determine output directory - usually dist/ or build/
925+
let distDir = 'dist';
926+
927+
try {
928+
// Try to find the dist directory
929+
const fsApi = this.webcontainerInstance.fs as any;
930+
const hasDistDir = await this.hasDirectory('dist');
931+
932+
if (!hasDistDir) {
933+
const hasBuildDir = await this.hasDirectory('build');
934+
if (hasBuildDir) {
935+
distDir = 'build';
936+
} else {
937+
this.writeToTerminal(`\n⚠️ Could not find dist/ or build/ directory after build\n`);
938+
}
939+
}
940+
} catch (error) {
941+
this.writeToTerminal(`\n⚠️ Error checking for build directory: ${error instanceof Error ? error.message : String(error)}\n`);
942+
}
943+
944+
return {
945+
success: true,
946+
output,
947+
distDir
948+
};
949+
} catch (error) {
950+
const errorMessage = error instanceof Error ? error.message : String(error);
951+
this.writeToTerminal(`\n❌ Error during build: ${errorMessage}\n`);
952+
return {
953+
success: false,
954+
output: `Build error: ${errorMessage}`
955+
};
956+
}
957+
}
958+
959+
/**
960+
* Gets the built runtime code from the WebContainer
961+
* @param distDir - The distribution directory (usually 'dist' or 'build')
962+
* @param mainFile - The main JavaScript file (default: 'index.js' or 'main.js')
963+
* @returns Promise resolving to the built code as a string or null if not found
964+
*/
965+
public async getBuiltRuntimeCode(
966+
distDir: string = 'dist',
967+
mainFile: string | string[] = ['index.js', 'main.js', 'bundle.js']
968+
): Promise<string | null> {
969+
if (!this.webcontainerInstance) {
970+
return null;
971+
}
972+
973+
try {
974+
// Use type assertion for the fs API
975+
const fsApi = this.webcontainerInstance.fs as any;
976+
977+
// Check if the dist directory exists
978+
if (!await this.hasDirectory(distDir)) {
979+
this.writeToTerminal(`\n⚠️ Distribution directory '${distDir}' not found\n`);
980+
return null;
981+
}
982+
983+
// Get files in the dist directory
984+
const entries = await fsApi.readdir(distDir);
985+
986+
// If mainFile is a string, convert to array
987+
const mainFileOptions = typeof mainFile === 'string' ? [mainFile] : mainFile;
988+
989+
// Find the main file
990+
let foundMainFile = null;
991+
for (const option of mainFileOptions) {
992+
if (entries.includes(option)) {
993+
foundMainFile = option;
994+
break;
995+
}
996+
}
997+
998+
if (!foundMainFile) {
999+
this.writeToTerminal(`\n⚠️ Main file not found in ${distDir}/ directory\n`);
1000+
this.writeToTerminal(`\n Available files: ${entries.join(', ')}\n`);
1001+
1002+
// Try to find any JavaScript file
1003+
foundMainFile = entries.find(entry => entry.endsWith('.js'));
1004+
1005+
if (!foundMainFile) {
1006+
this.writeToTerminal(`\n❌ No JavaScript files found in ${distDir}/ directory\n`);
1007+
return null;
1008+
}
1009+
1010+
this.writeToTerminal(`\n Using ${foundMainFile} as the main file\n`);
1011+
}
1012+
1013+
// Read the main file
1014+
const filePath = `${distDir}/${foundMainFile}`;
1015+
const content = await fsApi.readFile(filePath, 'utf-8');
1016+
1017+
if (!content) {
1018+
this.writeToTerminal(`\n❌ Main file is empty: ${filePath}\n`);
1019+
return null;
1020+
}
1021+
1022+
this.writeToTerminal(`\n✅ Successfully extracted built code from ${filePath}\n`);
1023+
1024+
// Wrap in a script tag
1025+
return `<script>
1026+
// Built runtime code from ${filePath} - generated on ${new Date().toISOString()}
1027+
${content}
1028+
</script>`;
1029+
} catch (error) {
1030+
const errorMessage = error instanceof Error ? error.message : String(error);
1031+
this.writeToTerminal(`\n❌ Error getting built runtime code: ${errorMessage}\n`);
1032+
return null;
1033+
}
1034+
}
1035+
8731036
/**
8741037
* Checks if the WebContainer is initialized
8751038
*/

0 commit comments

Comments
 (0)