Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions docs/todos/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,7 @@ This document provides a comprehensive index of all TODO items in the 8f4e proje
| 154 | Split compiler utils and use syntax helpers | 🟡 | 1-2d | 2025-12-30 | Split `packages/compiler/src/utils.ts` into per-function modules with in-source tests and replace syntax checks with syntax helpers |
| 155 | Add Framebuffer Memory Accounting in glugglug | 🟡 | 2-4h | 2025-12-30 | Track estimated render target and cache framebuffer memory usage for debugging and profiling |
| 156 | Add GLSL Shader Code Blocks for Post-Process Effects | 🟡 | 2-4d | 2026-01-02 | Replace project `postProcessEffects` with vertex/fragment shader code blocks and derive effects from block pairs |
<<<<<<< Updated upstream
| 157 | Disable Compilation for Runtime-Ready Projects | 🟡 | 3-5h | 2026-01-02 | Add a `disableCompilation` config flag to hard-block compilation and skip config/module compilation paths |
=======
| 157 | Add Comment Code Blocks | 🟡 | 1-2d | 2026-01-02 | Add a non-compiled comment block type for editor notes and documentation |
| 158 | Add Background Effects | 🟡 | 2-4d | 2026-01-02 | Add background-only effects analogous to post-process effects without impacting UI |
>>>>>>> Stashed changes
| 002 | Enable Strict TypeScript in Editor Package | 🟡 | 2-3d | 2025-08-23 | Currently has 52 type errors when strict settings enabled, causing missing null checks and implicit any types that reduce type safety and developer experience |
| 025 | Separate Editor View Layer into Standalone Package | 🟡 | 3-5d | 2025-08-26 | Extract Canvas-based rendering and sprite management into `@8f4e/browser-view` package to make core editor a pure state machine compatible with any renderer |
| 026 | Separate Editor User Interactions into Standalone Package | 🟡 | 2-3d | 2025-08-26 | Extract DOM event handling and input logic into `@8f4e/browser-input` package to enable alternative input systems (touch, joystick, terminal) |
Expand Down Expand Up @@ -79,6 +74,7 @@ This document provides a comprehensive index of all TODO items in the 8f4e proje

| ID | Title | Priority | Effort | Completed | Summary |
|----|-------|----------|--------|-----------|---------|
| 157 | Disable Compilation for Runtime-Ready Projects | 🟡 | 3-5h | 2026-01-02 | Added `disableCompilation` config flag to hard-block compilation and skip config/module compilation paths; comprehensive tests added covering compiler, config effects, and runtime-ready export |
| 147 | Split Memory Instruction Into int/float With Shared Helpers | 🟡 | 4-6h | 2025-12-25 | Split `memory.ts` into `int.ts`/`float.ts`, add shared helpers for argument parsing, pointer depth, and memory flags, split `memory.test.ts` into `int.test.ts`/`float.test.ts`; all tests pass, typecheck passes, lint passes |
| 148 | Consolidate syntax-related logic into syntax-rules package | 🟡 | 2-3d | 2025-12-25 | Archived - syntax logic consolidated into `@8f4e/compiler/syntax` subpath; superseded by TODO 152 |
| 127 | Update Deprecated npm Dependencies | 🟡 | 2-4h | 2025-12-20 | Upgraded ESLint from v8.57.0 to v9.39.2, added @eslint/[email protected] and [email protected], updated typescript-eslint packages to 8.50.0, removed .eslintignore file; eliminated deprecation warnings for eslint, rimraf, and @humanwhocodes packages |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ title: 'TODO: Disable Compilation for Runtime-Ready Projects'
priority: Medium
effort: 3-5h
created: 2026-01-02
status: Open
completed: null
status: Completed
completed: 2026-01-02
---

# TODO: Disable Compilation for Runtime-Ready Projects
Expand Down Expand Up @@ -45,10 +45,10 @@ Alternative approaches considered:

## Success Criteria

- [ ] Projects with `disableCompilation: true` do not call any compile callbacks.
- [ ] Runtime-ready exports with the flag set do not attempt to compile config or modules.
- [ ] A clear log or error message is emitted when compilation is skipped.
- [ ] Relevant tests cover the new behavior.
- [x] Projects with `disableCompilation: true` do not call any compile callbacks.
- [x] Runtime-ready exports with the flag set do not attempt to compile config or modules.
- [x] A clear log or error message is emitted when compilation is skipped.
- [x] Relevant tests cover the new behavior.

## Affected Components

Expand Down
1 change: 1 addition & 0 deletions packages/editor/packages/editor-state/src/configSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ const configSchema: ConfigSchema = {
type: 'array',
items: runtimeSettingsSchema,
},
disableCompilation: { type: 'boolean' },
},
additionalProperties: false,
};
Expand Down
8 changes: 8 additions & 0 deletions packages/editor/packages/editor-state/src/effects/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ export function flattenProjectForCompiler(codeBlocks: Set<CodeBlockGraphicData>)
export default async function compiler(store: StateManager<State>, events: EventDispatcher) {
const state = store.getState();
async function onRecompile() {
// Check if compilation is disabled by config
if (state.compiler.disableCompilation) {
log(state, 'Compilation skipped: disableCompilation flag is set', 'Compiler');
store.set('compiler.isCompiling', false);
store.set('codeErrors.compilationErrors', []);
return;
}

// Check if project has pre-compiled WASM already loaded (runtime-ready project)
// If codeBuffer is populated and we don't have a compiler, skip compilation
if (state.compiler.codeBuffer.length > 0 && !state.callbacks.compileProject) {
Expand Down
11 changes: 11 additions & 0 deletions packages/editor/packages/editor-state/src/effects/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ export default function configEffect(store: StateManager<State>, events: EventDi
return;
}

// Check if compilation is disabled by config
if (state.compiler.disableCompilation) {
log(state, 'Config compilation skipped: disableCompilation flag is set', 'Config');
return;
}

const configBlocks = collectConfigBlocks(state.graphicHelper.codeBlocks);

if (configBlocks.length === 0) {
Expand Down Expand Up @@ -111,6 +117,11 @@ export default function configEffect(store: StateManager<State>, events: EventDi
* @returns Promise resolving to the merged config object
*/
export async function compileConfigForExport(state: State): Promise<Record<string, unknown>> {
// If compilation is disabled, return the stored compiled config if available
if (state.compiler.disableCompilation) {
return state.compiler.compiledConfig || {};
}

// If no compileConfig callback, return empty object
const compileConfig = state.callbacks.compileConfig;
if (!compileConfig) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import { describe, it, expect, beforeEach, vi, type MockInstance } from 'vitest';
import createStateManager from '@8f4e/state-manager';

import compiler from './compiler';
import configEffect, { compileConfigForExport } from './config';

import { createMockState, createMockCodeBlock } from '../pureHelpers/testingUtils/testUtils';
import { createMockEventDispatcherWithVitest } from '../pureHelpers/testingUtils/vitestTestUtils';

import type { State } from '../types';

describe('disableCompilation feature', () => {
let mockState: State;
let store: ReturnType<typeof createStateManager<State>>;
let mockEvents: ReturnType<typeof createMockEventDispatcherWithVitest>;
let mockCompileProject: MockInstance;
let mockCompileConfig: MockInstance;

beforeEach(() => {
mockCompileProject = vi.fn().mockResolvedValue({
compiledModules: {},
codeBuffer: new Uint8Array([1, 2, 3]),
allocatedMemorySize: 1024,
memoryBuffer: new Int32Array(256),
memoryBufferFloat: new Float32Array(256),
memoryAction: { action: 'reused' },
});

mockCompileConfig = vi.fn().mockResolvedValue({
config: { memorySizeBytes: 1048576 },
errors: [],
});

const moduleBlock = createMockCodeBlock({
id: 'test-module',
code: ['module testModule', 'moduleEnd'],
creationIndex: 0,
blockType: 'module',
});

const configBlock = createMockCodeBlock({
id: 'config-block',
code: ['config', 'memorySizeBytes 1048576', 'configEnd'],
creationIndex: 1,
blockType: 'config',
});

mockState = createMockState({
compiler: {
disableCompilation: false,
},
callbacks: {
compileProject: mockCompileProject,
compileConfig: mockCompileConfig,
},
});

mockState.graphicHelper.codeBlocks.add(moduleBlock);
mockState.graphicHelper.codeBlocks.add(configBlock);

mockEvents = createMockEventDispatcherWithVitest();
store = createStateManager(mockState);
});

describe('Project compilation', () => {
it('should skip compilation when disableCompilation is true', async () => {
store.set('compiler.disableCompilation', true);

compiler(store, mockEvents);

const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls;
const compileCall = onCalls.find(call => call[0] === 'codeBlockAdded');
expect(compileCall).toBeDefined();

const onRecompileCallback = compileCall![1];
await onRecompileCallback();

expect(mockCompileProject).not.toHaveBeenCalled();
expect(mockState.compiler.isCompiling).toBe(false);
expect(mockState.codeErrors.compilationErrors).toEqual([]);
expect(
mockState.console.logs.some(
log =>
log.message.includes('Compilation skipped: disableCompilation flag is set') && log.category === '[Compiler]'
)
).toBe(true);
});

it('should compile normally when disableCompilation is false', async () => {
store.set('compiler.disableCompilation', false);

compiler(store, mockEvents);

const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls;
const compileCall = onCalls.find(call => call[0] === 'codeBlockAdded');
const onRecompileCallback = compileCall![1];

await onRecompileCallback();

expect(mockCompileProject).toHaveBeenCalled();
});

it('should prioritize disableCompilation check over pre-compiled WASM check', async () => {
store.set('compiler.disableCompilation', true);
store.set('compiler.codeBuffer', new Uint8Array([1, 2, 3, 4, 5]));
mockState.callbacks.compileProject = undefined;

compiler(store, mockEvents);

const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls;
const compileCall = onCalls.find(call => call[0] === 'codeBlockAdded');
const onRecompileCallback = compileCall![1];

await onRecompileCallback();

expect(
mockState.console.logs.some(log => log.message.includes('Compilation skipped: disableCompilation flag is set'))
).toBe(true);
expect(mockState.console.logs.some(log => log.message.includes('Using pre-compiled WASM'))).toBe(false);
});
});

describe('Config compilation', () => {
it('should skip config compilation when disableCompilation is true', async () => {
store.set('compiler.disableCompilation', true);

configEffect(store, mockEvents);

const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls;
const configCall = onCalls.find(call => call[0] === 'codeBlockAdded');
expect(configCall).toBeDefined();

const rebuildConfigCallback = configCall![1];
await rebuildConfigCallback();

expect(mockCompileConfig).not.toHaveBeenCalled();
expect(
mockState.console.logs.some(
log =>
log.message.includes('Config compilation skipped: disableCompilation flag is set') &&
log.category === '[Config]'
)
).toBe(true);
});

it('should compile config normally when disableCompilation is false', async () => {
store.set('compiler.disableCompilation', false);

configEffect(store, mockEvents);

const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls;
const configCall = onCalls.find(call => call[0] === 'codeBlockAdded');
const rebuildConfigCallback = configCall![1];

await rebuildConfigCallback();

expect(mockCompileConfig).toHaveBeenCalled();
});
});

describe('Runtime-ready export', () => {
it('should skip config compilation for export when disableCompilation is true', async () => {
mockState.compiler.disableCompilation = true;

const result = await compileConfigForExport(mockState);

expect(mockCompileConfig).not.toHaveBeenCalled();
expect(result).toEqual({});
});

it('should return stored compiledConfig when disableCompilation is true and config exists', async () => {
mockState.compiler.disableCompilation = true;
mockState.compiler.compiledConfig = {
memorySizeBytes: 2097152,
selectedRuntime: 1,
};

const result = await compileConfigForExport(mockState);

expect(mockCompileConfig).not.toHaveBeenCalled();
expect(result).toEqual({
memorySizeBytes: 2097152,
selectedRuntime: 1,
});
});

it('should compile config for export when disableCompilation is false', async () => {
mockState.compiler.disableCompilation = false;

const result = await compileConfigForExport(mockState);

expect(mockCompileConfig).toHaveBeenCalled();
expect(result).toEqual({ memorySizeBytes: 1048576 });
});

it('should return empty config when no compileConfig callback is provided', async () => {
mockState.compiler.disableCompilation = false;
mockState.callbacks.compileConfig = undefined;

const result = await compileConfigForExport(mockState);

expect(result).toEqual({});
});
});

describe('applyConfigToState integration', () => {
it('should set disableCompilation flag from config', async () => {
mockCompileConfig.mockResolvedValue({
config: { disableCompilation: true },
errors: [],
});

configEffect(store, mockEvents);

const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls;
const configCall = onCalls.find(call => call[0] === 'codeBlockAdded');
const rebuildConfigCallback = configCall![1];

await rebuildConfigCallback();

expect(mockState.compiler.disableCompilation).toBe(true);
});

it('should not change disableCompilation flag when not in config', async () => {
store.set('compiler.disableCompilation', false);

mockCompileConfig.mockResolvedValue({
config: { memorySizeBytes: 2097152 },
errors: [],
});

configEffect(store, mockEvents);

const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls;
const configCall = onCalls.find(call => call[0] === 'codeBlockAdded');
const rebuildConfigCallback = configCall![1];

await rebuildConfigCallback();

expect(mockState.compiler.disableCompilation).toBe(false);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -309,4 +309,49 @@ describe('projectImport', () => {
consoleWarnSpy.mockRestore();
});
});

describe('Runtime-ready project loading', () => {
it('should store compiledConfig when loading a runtime-ready project', () => {
const runtimeReadyProject: Project = {
...EMPTY_DEFAULT_PROJECT,
compiledConfig: {
memorySizeBytes: 2097152,
selectedRuntime: 1,
disableCompilation: true,
},
compiledWasm: 'base64encodedwasm',
memorySnapshot: 'base64encodedmemory',
};

projectImport(store, mockEvents, mockState);

const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls;
const loadProjectCall = onCalls.find(call => call[0] === 'loadProject');
const loadProjectCallback = loadProjectCall![1];

loadProjectCallback({ project: runtimeReadyProject });

expect(mockState.compiler.compiledConfig).toEqual({
memorySizeBytes: 2097152,
selectedRuntime: 1,
disableCompilation: true,
});
});

it('should not set compiledConfig when not present in project', () => {
const regularProject: Project = {
...EMPTY_DEFAULT_PROJECT,
};

projectImport(store, mockEvents, mockState);

const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls;
const loadProjectCall = onCalls.find(call => call[0] === 'loadProject');
const loadProjectCallback = loadProjectCall![1];

loadProjectCallback({ project: regularProject });

expect(mockState.compiler.compiledConfig).toBeUndefined();
});
});
});
Loading