From 83f89772ef4e45d919f68e6e4f038e9e628fc370 Mon Sep 17 00:00:00 2001 From: Yannic Charlon <52761674+JustYannicc@users.noreply.github.com> Date: Fri, 3 Jul 2026 23:30:18 +0200 Subject: [PATCH] perf(extensions): quiet runtime success logs --- scripts/test-extension-runtime-logging.mjs | 144 +++++++++++++++++++++ src/renderer/src/ExtensionView.tsx | 48 ++++--- 2 files changed, 173 insertions(+), 19 deletions(-) create mode 100644 scripts/test-extension-runtime-logging.mjs diff --git a/scripts/test-extension-runtime-logging.mjs b/scripts/test-extension-runtime-logging.mjs new file mode 100644 index 00000000..6f5cbcd3 --- /dev/null +++ b/scripts/test-extension-runtime-logging.mjs @@ -0,0 +1,144 @@ +#!/usr/bin/env node + +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import test from 'node:test'; +import vm from 'node:vm'; +import { createRequire } from 'node:module'; + +const require = createRequire(import.meta.url); +const ts = require('typescript'); + +const ROOT = process.cwd(); +const EXTENSION_VIEW_PATH = path.join(ROOT, 'src/renderer/src/ExtensionView.tsx'); +const source = fs.readFileSync(EXTENSION_VIEW_PATH, 'utf8'); + +function sourceBetween(startNeedle, endNeedle) { + const start = source.indexOf(startNeedle); + assert.notEqual(start, -1, `Could not find start marker: ${startNeedle}`); + const end = source.indexOf(endNeedle, start); + assert.notEqual(end, -1, `Could not find end marker: ${endNeedle}`); + return source.slice(start, end); +} + +function runTranspiledSnippet(snippet) { + const transpiled = ts.transpileModule(snippet, { + compilerOptions: { + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES2022, + }, + fileName: EXTENSION_VIEW_PATH, + }); + const debugCalls = []; + const sandbox = { + console: { + debug: (...args) => debugCalls.push(args), + }, + __debugCalls: debugCalls, + }; + vm.runInNewContext(transpiled.outputText, sandbox, { filename: EXTENSION_VIEW_PATH }); + return sandbox.__result; +} + +test('baseline: legacy success diagnostics log and stringify exported functions', () => { + const logCalls = []; + let toStringCalls = 0; + const exported = function Command() {}; + exported.toString = () => { + toStringCalls += 1; + return 'function Command() { return "ok"; }'; + }; + + const legacyConsole = { + log: (...args) => logCalls.push(args), + }; + + legacyConsole.log('[loadExtensionExport] Extension loaded successfully'); + legacyConsole.log('[loadExtensionExport] Exported type:', typeof exported); + legacyConsole.log('[loadExtensionExport] Exported name:', exported?.name); + legacyConsole.log('[loadExtensionExport] Exported function:', exported?.toString?.().slice(0, 200)); + + console.log( + `[baseline] extension runtime success diagnostics: logs=${logCalls.length} exportedToStringCalls=${toStringCalls}` + ); + + assert.equal(logCalls.length, 4); + assert.equal(toStringCalls, 1); +}); + +test('extension runtime debug helper is quiet unless explicitly enabled', () => { + const helperSource = sourceBetween( + 'const EXTENSION_RUNTIME_DEBUG_GLOBAL', + '// ─── React Module for Extensions' + ); + const result = runTranspiledSnippet(` + ${helperSource} + const beforeEnabled = isExtensionRuntimeDebugLoggingEnabled(); + logExtensionRuntimeDebug('quiet'); + globalThis[EXTENSION_RUNTIME_DEBUG_GLOBAL] = true; + const afterEnabled = isExtensionRuntimeDebugLoggingEnabled(); + logExtensionRuntimeDebug('loud', 42); + globalThis.__result = { + beforeEnabled, + afterEnabled, + debugCalls: globalThis.__debugCalls, + }; + `); + + console.log( + `[after] extension runtime debug helper: defaultDebugCalls=0 enabledDebugCalls=${result.debugCalls.length}` + ); + + assert.equal(result.beforeEnabled, false); + assert.equal(result.afterEnabled, true); + assert.deepEqual(result.debugCalls, [['loud', 42]]); +}); + +test('success-path extension runtime diagnostics are gated behind debug logging', () => { + const moduleReactSetup = sourceBetween( + '// Create React module for extensions', + '// ─── JSX Runtime for Extensions' + ); + const reactRequireBridge = sourceBetween( + 'let reactRequireCount = 0;', + '// ── Raycast API shim' + ); + const loadSuccessPath = sourceBetween( + 'const exported =\n fakeModule.exports.default || fakeModule.exports;', + 'if (typeof exported === \'function\')' + ); + const viewRenderer = sourceBetween( + 'const ViewRenderer: React.FC<{', + 'const ScopedExtensionContext: React.FC<{' + ); + + for (const [label, snippet] of [ + ['module React setup', moduleReactSetup], + ['fakeRequire React bridge', reactRequireBridge], + ['loadExtensionExport success path', loadSuccessPath], + ['ViewRenderer render path', viewRenderer], + ]) { + assert.doesNotMatch(snippet, /console\.log/, `${label} must not emit unconditional console.log diagnostics`); + } + + assert.match(moduleReactSetup, /logExtensionRuntimeDebug/); + assert.match(reactRequireBridge, /logExtensionRuntimeDebug/); + assert.match(loadSuccessPath, /if \(isExtensionRuntimeDebugLoggingEnabled\(\)\)/); + assert.match(viewRenderer, /logExtensionRuntimeDebug/); + + const beforeDebugGate = loadSuccessPath.slice( + 0, + loadSuccessPath.indexOf('if (isExtensionRuntimeDebugLoggingEnabled())') + ); + assert.doesNotMatch(beforeDebugGate, /toString/, 'exported function toString must not run before debug is enabled'); + assert.match(loadSuccessPath, /console\.debug\('\[loadExtensionExport\] Exported function:', exported\?\.toString/); +}); + +test('actionable warnings and errors remain available', () => { + assert.match(source, /console\.warn\('Extension exported an object, not a function\. Trying to wrap it\.'\)/); + assert.match(source, /console\.warn\(`Extension tried to require unknown module:/); + assert.match(source, /console\.error\('Extension did not export a function\. Got:'/); + assert.match(source, /console\.error\('Failed to load extension:'/); + assert.match(source, /console\.error\('Stack:'/); +}); diff --git a/src/renderer/src/ExtensionView.tsx b/src/renderer/src/ExtensionView.tsx index 85643d80..2e7407a0 100644 --- a/src/renderer/src/ExtensionView.tsx +++ b/src/renderer/src/ExtensionView.tsx @@ -20,6 +20,18 @@ import { withExtensionContext } from './raycast-api/context-scope-runtime'; // Also import @raycast/utils stubs from our shim import * as RaycastUtils from './raycast-api'; +const EXTENSION_RUNTIME_DEBUG_GLOBAL = '__SUPERCMD_EXTENSION_RUNTIME_DEBUG'; + +function isExtensionRuntimeDebugLoggingEnabled(): boolean { + return (globalThis as any)[EXTENSION_RUNTIME_DEBUG_GLOBAL] === true; +} + +function logExtensionRuntimeDebug(...args: unknown[]): void { + if (isExtensionRuntimeDebugLoggingEnabled()) { + console.debug(...args); + } +} + // ─── React Module for Extensions ──────────────────────────────────── // Extensions MUST use the exact same React instance as the host app. // @@ -30,9 +42,9 @@ import * as RaycastUtils from './raycast-api'; // Create React module for extensions // We simply return the actual React import - no copying, no wrapping // This ensures extensions get the exact same React that the host uses -console.log('[React] Setting up React for extensions'); -console.log('[React] React.version:', React.version); -console.log('[React] React.useState:', typeof React.useState); +logExtensionRuntimeDebug('[React] Setting up React for extensions'); +logExtensionRuntimeDebug('[React] React.version:', React.version); +logExtensionRuntimeDebug('[React] React.useState:', typeof React.useState); // ─── JSX Runtime for Extensions ───────────────────────────────────── // We use the actual jsx-runtime import to ensure full compatibility. @@ -3607,14 +3619,11 @@ function loadExtensionExport( // This is the critical bridge between extension code and the // SuperCmd renderer environment. Every module an extension // might `require()` must be handled here. - // - // IMPORTANT: We track React requires to verify the same instance is always returned. let reactRequireCount = 0; const fakeRequire: any = (name: string): any => { - // Track all requires for debugging if (name === 'react' || name.startsWith('react/') || name === 'react-dom') { reactRequireCount++; - console.log(`[fakeRequire] #${reactRequireCount} require("${name}")`); + logExtensionRuntimeDebug(`[fakeRequire] #${reactRequireCount} require("${name}")`); } // ── React & friends ───────────────────────────────────── // CRITICAL: Extensions MUST use the same React instance as the host. @@ -3626,14 +3635,14 @@ function loadExtensionExport( switch (name) { case 'react': { // Return React directly - the exact same module the host uses - console.log('[fakeRequire] Providing React directly'); + logExtensionRuntimeDebug('[fakeRequire] Providing React directly'); (globalThis as any).__SUPERCMD_REACT = React; return React; } case 'react-dom': case 'react-dom/client': - console.log('[fakeRequire] Providing ReactDOM'); - console.log('[fakeRequire] ReactDOM.createRoot:', (ReactDOM as any).createRoot); + logExtensionRuntimeDebug('[fakeRequire] Providing ReactDOM'); + logExtensionRuntimeDebug('[fakeRequire] ReactDOM.createRoot:', (ReactDOM as any).createRoot); return ReactDOM; case 'react-dom/server': return reactDomServerStub; @@ -3641,9 +3650,9 @@ function loadExtensionExport( case 'react/jsx-dev-runtime': { // Return the actual jsx-runtime to ensure JSX creates elements // using the same React.createElement - console.log('[fakeRequire] Providing jsx-runtime'); - console.log('[fakeRequire] JsxRuntime.Fragment === React.Fragment:', JsxRuntime.Fragment === React.Fragment); - console.log('[fakeRequire] JsxRuntime.Fragment === React.Fragment:', JsxRuntime.Fragment === React.Fragment); + logExtensionRuntimeDebug('[fakeRequire] Providing jsx-runtime'); + logExtensionRuntimeDebug('[fakeRequire] JsxRuntime.Fragment === React.Fragment:', JsxRuntime.Fragment === React.Fragment); + logExtensionRuntimeDebug('[fakeRequire] JsxRuntime.Fragment === React.Fragment:', JsxRuntime.Fragment === React.Fragment); return JsxRuntime; } @@ -4011,10 +4020,12 @@ function loadExtensionExport( const exported = fakeModule.exports.default || fakeModule.exports; - console.log('[loadExtensionExport] Extension loaded successfully'); - console.log('[loadExtensionExport] Exported type:', typeof exported); - console.log('[loadExtensionExport] Exported name:', exported?.name); - console.log('[loadExtensionExport] Exported function:', exported?.toString?.().slice(0, 200)); + if (isExtensionRuntimeDebugLoggingEnabled()) { + console.debug('[loadExtensionExport] Extension loaded successfully'); + console.debug('[loadExtensionExport] Exported type:', typeof exported); + console.debug('[loadExtensionExport] Exported name:', exported?.name); + console.debug('[loadExtensionExport] Exported function:', exported?.toString?.().slice(0, 200)); + } if (typeof exported === 'function') { return exported; @@ -4165,9 +4176,8 @@ const ViewRenderer: React.FC<{ fallbackText, launchType = 'userInitiated', }) => { - // Simple test that hooks work here const [test] = useState('ok'); - console.log('[ViewRenderer] Hooks work here, rendering extension...'); + logExtensionRuntimeDebug('[ViewRenderer] Hooks work here, rendering extension...'); // Pass standard Raycast props: arguments (command arguments) and launchType return React.createElement(Component, { arguments: launchArguments,