diff --git a/docs/todos/_index.md b/docs/todos/_index.md index 7dfe5403f..f31855155 100644 --- a/docs/todos/_index.md +++ b/docs/todos/_index.md @@ -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) | @@ -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/js@9.39.2 and globals@16.5.0, updated typescript-eslint packages to 8.50.0, removed .eslintignore file; eliminated deprecation warnings for eslint, rimraf, and @humanwhocodes packages | diff --git a/docs/todos/157-disable-compilation-when-runtime-ready.md b/docs/todos/archived/157-disable-compilation-when-runtime-ready.md similarity index 95% rename from docs/todos/157-disable-compilation-when-runtime-ready.md rename to docs/todos/archived/157-disable-compilation-when-runtime-ready.md index 8f191b521..6df844f61 100644 --- a/docs/todos/157-disable-compilation-when-runtime-ready.md +++ b/docs/todos/archived/157-disable-compilation-when-runtime-ready.md @@ -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 @@ -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 diff --git a/packages/editor/packages/editor-state/src/configSchema.ts b/packages/editor/packages/editor-state/src/configSchema.ts index 4a016bb6a..cae1a0a6a 100644 --- a/packages/editor/packages/editor-state/src/configSchema.ts +++ b/packages/editor/packages/editor-state/src/configSchema.ts @@ -101,6 +101,7 @@ const configSchema: ConfigSchema = { type: 'array', items: runtimeSettingsSchema, }, + disableCompilation: { type: 'boolean' }, }, additionalProperties: false, }; diff --git a/packages/editor/packages/editor-state/src/effects/compiler.ts b/packages/editor/packages/editor-state/src/effects/compiler.ts index eb113dd19..c67eca0f9 100644 --- a/packages/editor/packages/editor-state/src/effects/compiler.ts +++ b/packages/editor/packages/editor-state/src/effects/compiler.ts @@ -28,6 +28,14 @@ export function flattenProjectForCompiler(codeBlocks: Set) export default async function compiler(store: StateManager, 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) { diff --git a/packages/editor/packages/editor-state/src/effects/config.ts b/packages/editor/packages/editor-state/src/effects/config.ts index d02a6b4ff..64090e664 100644 --- a/packages/editor/packages/editor-state/src/effects/config.ts +++ b/packages/editor/packages/editor-state/src/effects/config.ts @@ -71,6 +71,12 @@ export default function configEffect(store: StateManager, 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) { @@ -111,6 +117,11 @@ export default function configEffect(store: StateManager, events: EventDi * @returns Promise resolving to the merged config object */ export async function compileConfigForExport(state: State): Promise> { + // 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) { diff --git a/packages/editor/packages/editor-state/src/effects/disableCompilation.test.ts b/packages/editor/packages/editor-state/src/effects/disableCompilation.test.ts new file mode 100644 index 000000000..e4e5d0fdc --- /dev/null +++ b/packages/editor/packages/editor-state/src/effects/disableCompilation.test.ts @@ -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>; + let mockEvents: ReturnType; + 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); + }); + }); +}); diff --git a/packages/editor/packages/editor-state/src/effects/projectImport.test.ts b/packages/editor/packages/editor-state/src/effects/projectImport.test.ts index 8e853103d..306ee27ff 100644 --- a/packages/editor/packages/editor-state/src/effects/projectImport.test.ts +++ b/packages/editor/packages/editor-state/src/effects/projectImport.test.ts @@ -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(); + }); + }); }); diff --git a/packages/editor/packages/editor-state/src/effects/projectImport.ts b/packages/editor/packages/editor-state/src/effects/projectImport.ts index 3ba9f6ca3..2a67dcdac 100644 --- a/packages/editor/packages/editor-state/src/effects/projectImport.ts +++ b/packages/editor/packages/editor-state/src/effects/projectImport.ts @@ -76,6 +76,11 @@ export default function projectImport(store: StateManager, events: EventD state.compiler.compiledModules = newProject.compiledModules; } + // Store compiled config from runtime-ready projects + if (newProject.compiledConfig) { + state.compiler.compiledConfig = newProject.compiledConfig; + } + state.graphicHelper.outputsByWordAddress.clear(); state.graphicHelper.selectedCodeBlock = undefined; state.graphicHelper.draggedCodeBlock = undefined; diff --git a/packages/editor/packages/editor-state/src/impureHelpers/config/applyConfigToState.test.ts b/packages/editor/packages/editor-state/src/impureHelpers/config/applyConfigToState.test.ts index 788ea055d..32e9ee6e5 100644 --- a/packages/editor/packages/editor-state/src/impureHelpers/config/applyConfigToState.test.ts +++ b/packages/editor/packages/editor-state/src/impureHelpers/config/applyConfigToState.test.ts @@ -70,4 +70,20 @@ describe('applyConfigToState', () => { expect(state.runtime.runtimeSettings).toEqual(originalSettings); expect(state.runtime.selectedRuntime).toBe(originalSelectedRuntime); }); + + it('should apply disableCompilation flag', () => { + const state = createMockState(); + const store = createStateManager(state); + expect(state.compiler.disableCompilation).toBe(false); + applyConfigToState(store, { disableCompilation: true }); + expect(state.compiler.disableCompilation).toBe(true); + }); + + it('should not change disableCompilation when not in config', () => { + const state = createMockState(); + const store = createStateManager(state); + state.compiler.disableCompilation = true; + applyConfigToState(store, { memorySizeBytes: 65536 }); + expect(state.compiler.disableCompilation).toBe(true); + }); }); diff --git a/packages/editor/packages/editor-state/src/impureHelpers/config/applyConfigToState.ts b/packages/editor/packages/editor-state/src/impureHelpers/config/applyConfigToState.ts index e41cbc019..8d6c901db 100644 --- a/packages/editor/packages/editor-state/src/impureHelpers/config/applyConfigToState.ts +++ b/packages/editor/packages/editor-state/src/impureHelpers/config/applyConfigToState.ts @@ -11,6 +11,7 @@ export interface ConfigObject { memorySizeBytes?: number; selectedRuntime?: number; runtimeSettings?: Runtimes[]; + disableCompilation?: boolean; } /** @@ -52,4 +53,8 @@ export function applyConfigToState(store: StateManager, config: ConfigObj if (typeof config.memorySizeBytes === 'number') { store.set('compiler.compilerOptions.memorySizeBytes', config.memorySizeBytes); } + + if (typeof config.disableCompilation === 'boolean') { + store.set('compiler.disableCompilation', config.disableCompilation); + } } diff --git a/packages/editor/packages/editor-state/src/pureHelpers/state/createDefaultState.ts b/packages/editor/packages/editor-state/src/pureHelpers/state/createDefaultState.ts index 51ff83492..6da170e20 100644 --- a/packages/editor/packages/editor-state/src/pureHelpers/state/createDefaultState.ts +++ b/packages/editor/packages/editor-state/src/pureHelpers/state/createDefaultState.ts @@ -23,6 +23,7 @@ export default function createDefaultState() { ignoredKeywords: ['debug', 'button', 'switch', 'offset', 'plot', 'piano'], }, }, + disableCompilation: false, }, midi: { inputs: [], diff --git a/packages/editor/packages/editor-state/src/pureHelpers/testingUtils/testUtils.ts b/packages/editor/packages/editor-state/src/pureHelpers/testingUtils/testUtils.ts index bdcae65db..674eeb49c 100644 --- a/packages/editor/packages/editor-state/src/pureHelpers/testingUtils/testUtils.ts +++ b/packages/editor/packages/editor-state/src/pureHelpers/testingUtils/testUtils.ts @@ -226,6 +226,7 @@ export function createMockState(overrides: DeepPartial = {}): State { ignoredKeywords: [], }, }, + disableCompilation: false, }, callbacks: { requestRuntime: createMockAsyncFunction(() => () => {}), diff --git a/packages/editor/packages/editor-state/src/types.ts b/packages/editor/packages/editor-state/src/types.ts index ab57a2335..88ff33352 100644 --- a/packages/editor/packages/editor-state/src/types.ts +++ b/packages/editor/packages/editor-state/src/types.ts @@ -174,6 +174,8 @@ export interface Compiler { compilerOptions: CompileOptions; allocatedMemorySize: number; compiledFunctions?: CompiledFunctionLookup; + disableCompilation: boolean; + compiledConfig?: Record; } export interface Midi {