diff --git a/package-lock.json b/package-lock.json index 76e03066..3e8fe476 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "oc", - "version": "0.50.24", + "version": "0.50.27", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "oc", - "version": "0.50.24", + "version": "0.50.27", "license": "MIT", "dependencies": { "@kitajs/html": "^4.2.9", @@ -33,7 +33,7 @@ "multer": "^1.4.3", "nice-cache": "^0.0.5", "oc-client": "^4.0.2", - "oc-client-browser": "^2.1.4", + "oc-client-browser": "^2.1.5", "oc-empty-response-handler": "^1.0.2", "oc-get-unix-utc-timestamp": "^1.0.6", "oc-s3-storage-adapter": "^2.2.0", @@ -52,6 +52,7 @@ "semver": "^7.7.1", "semver-extra": "^3.0.0", "serialize-error": "^8.1.0", + "source-map": "^0.7.6", "targz": "^1.0.1", "try-require": "^1.2.1", "undici": "^6.21.1", @@ -6537,6 +6538,15 @@ "node": ">=0.10.0" } }, + "node_modules/jstransformer-uglify-js/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/jstransformer-uglify-js/node_modules/uglify-js": { "version": "2.8.29", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", @@ -7396,9 +7406,9 @@ } }, "node_modules/oc-client-browser": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/oc-client-browser/-/oc-client-browser-2.1.4.tgz", - "integrity": "sha512-IqOTt0OgbACGS7f0yWKQ0xMxefSxCMDldmIe12gkmTLMsFxcmtvSUAH+6Asob7apSaJE7gPTB4tjINLS2EZb8A==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/oc-client-browser/-/oc-client-browser-2.1.5.tgz", + "integrity": "sha512-hytsviyN0wwLmJcMTNCQHTMi//jn6wGTyyEaktfWO6Pg4h1NuDyXEJ9OEc0YWJeQCiUU4nMt0oyVALLpEvGjNQ==", "license": "MIT", "dependencies": { "@rdevis/turbo-stream": "^2.4.1", @@ -7996,6 +8006,15 @@ "node": ">=0.10.0" } }, + "node_modules/oc-jade-legacy/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/oc-jade-legacy/node_modules/uglify-js": { "version": "2.8.29", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", @@ -9534,11 +9553,12 @@ } }, "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", "engines": { - "node": ">=0.10.0" + "node": ">= 12" } }, "node_modules/source-map-js": { diff --git a/package.json b/package.json index d68ecf87..0df43c0a 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "multer": "^1.4.3", "nice-cache": "^0.0.5", "oc-client": "^4.0.2", - "oc-client-browser": "^2.1.4", + "oc-client-browser": "^2.1.5", "oc-empty-response-handler": "^1.0.2", "oc-get-unix-utc-timestamp": "^1.0.6", "oc-s3-storage-adapter": "^2.2.0", @@ -117,6 +117,7 @@ "semver": "^7.7.1", "semver-extra": "^3.0.0", "serialize-error": "^8.1.0", + "source-map": "^0.7.6", "targz": "^1.0.1", "try-require": "^1.2.1", "undici": "^6.21.1", diff --git a/src/registry/routes/helpers/format-error-stack.ts b/src/registry/routes/helpers/format-error-stack.ts new file mode 100644 index 00000000..d30d1d96 --- /dev/null +++ b/src/registry/routes/helpers/format-error-stack.ts @@ -0,0 +1,206 @@ +// import fs from 'fs'; +import path from 'node:path'; +import source from 'source-map'; + +function extractInlineSourceMap(code: string) { + try { + const map = code.match( + /\/\/# sourceMappingURL=data:application\/json;charset=utf-8;base64,(.*)/ + )?.[1]; + if (map) { + return atob(map); + } + return null; + } catch { + return null; + } +} + +export async function processStackTrace({ + stackTrace, + code +}: { + stackTrace: string; + code: string; +}) { + const rawSourceMap = extractInlineSourceMap(code); + if (!rawSourceMap) return null; + const consumer = await new source.SourceMapConsumer(rawSourceMap); + const lines = stackTrace.split('\n').filter((l) => l.trim().startsWith('at')); + + const result = { + stack: [] as string[], + codeFrame: [] as string[] + }; + + for (const line of lines) { + // More flexible regex to handle different stack trace formats + let match = line.match(/at (.+) \((.+):(\d+):(\d+)\)/); + if (!match) { + // Handle lines without function names like "at /path/file.js:line:col" + match = line.match(/at (.+):(\d+):(\d+)/); + if (match) { + const [, file, lineStr, colStr] = match; + match = [null, null, file, lineStr, colStr] as any; + } + } + + if (!match) { + result.stack.push(`${line.trim()} (could not parse)`); + continue; + } + + const [, functionName, file, lineStr, colStr] = match; + const lineNum = parseInt(lineStr, 10); + const colNum = parseInt(colStr, 10); + + // Check if this line is from the file we have a source map for + if (!file.includes('server.js')) { + result.stack.push(`${line.trim()} (no source map)`); + continue; + } + + const original = consumer.originalPositionFor({ + line: lineNum, + column: colNum + }); + + if (original.source && original.line !== null) { + // Filter out frames that map to external libraries or oc-server internals AFTER mapping + if ( + original.source.includes('node_modules') || + original.source.includes('oc-server') || + original.source.includes('__oc_higherOrderServer') + ) { + // Don't show filtered frames + continue; + } + + // Try to get the function name from multiple sources + let displayName = original.name || functionName || ''; + + // Clean up the function name if it includes object/class info + if (functionName && !original.name) { + displayName = functionName; + } + + // Make file paths relative to current directory for better readability + let relativePath = original.source; + try { + if (path.isAbsolute(original.source)) { + relativePath = path.relative(process.cwd(), original.source); + } + } catch { + // Keep original path if relative conversion fails + } + + const stackLine = `at ${displayName} (${relativePath}:${original.line}:${original.column})`; + result.stack.push(stackLine); + + // Show source code context if available + const sourceContent = consumer.sourceContentFor(original.source, true); + if (sourceContent && original.line) { + const codeFrame = getCodeFrame( + sourceContent, + original.line, + original.column || 0 + ); + if (codeFrame) { + result.codeFrame.push(codeFrame); + } + } + } else { + // Fallback to original line if source mapping fails + result.stack.push(`${line.trim()} (source map failed)`); + } + } + + // Don't forget to destroy the consumer + try { + consumer.destroy(); + } catch {} + + return { + stack: result.stack.join('\n'), + frame: [ + // For some reason, the first block lacks some indentation + ...result.codeFrame.slice(0, 1).map((x) => ` ${x}`), + ...result.codeFrame.slice(1) + ].join('\n') + }; +} + +// Helper function to show code context around the error +function getCodeFrame( + sourceContent: string, + line: number, + column: number, + contextLines = 2 +) { + try { + const lines = sourceContent.split('\n'); + const targetLine = line - 1; // Convert to 0-based + + if (targetLine < 0 || targetLine >= lines.length) { + return null; + } + + const start = Math.max(0, targetLine - contextLines); + const end = Math.min(lines.length, targetLine + contextLines + 1); + + let result = ''; + for (let i = start; i < end; i++) { + const lineNumber = i + 1; + const isTarget = i === targetLine; + const prefix = isTarget ? '> ' : ' '; + const lineNumberStr = String(lineNumber).padStart(3, ' '); + + result += `${prefix}${lineNumberStr} | ${lines[i]}\n`; + + // Add pointer to exact column for target line + if (isTarget && column > 0) { + const pointer = ' '.repeat(column); // Account for prefix and line number + result += `${' '.repeat(5)} | ${pointer}^\n`; + } + } + + return result.trim(); + } catch { + return null; + } +} + +// async function main() { +// const stackTrace = ` +// TypeError: Cannot read properties of undefined (reading 'name') +// at HandledServer.initial (/Users/ricardo.agullo/Dev/octests/helpai/_package/server.js:196:38) +// at async ocServerWrapper (/Users/ricardo.agullo/Dev/octests/helpai/_package/server.js:83:19) +// `; +// const rawSourceMap = fs.readFileSync( +// './helpai/_package/server.js.map', +// 'utf8' +// ); +// const { stack, codeFrame } = await processStackTrace({ +// stackTrace, +// rawSourceMap +// }); + +// // Log the stack trace +// for (const line of stack) { +// console.log(` ${line}`); +// } + +// // Log the code frames +// // for (const frame of codeFrame) { +// // console.log(`\n${frame}\n`); +// // } +// for (let i = 0; i < codeFrame.length; i++) { +// if (i === 0) { +// console.log(`\n ${codeFrame[i]}\n`); +// } else { +// console.log(`\n${codeFrame[i]}\n`); +// } +// } +// } + +// main().catch(console.error); diff --git a/src/registry/routes/helpers/get-component.ts b/src/registry/routes/helpers/get-component.ts index 1805fee9..f13a1d10 100644 --- a/src/registry/routes/helpers/get-component.ts +++ b/src/registry/routes/helpers/get-component.ts @@ -26,6 +26,7 @@ import * as urlBuilder from '../../domain/url-builder'; import * as validator from '../../domain/validators'; import { validateTemplateOcVersion } from '../../domain/validators'; import applyDefaultValues from './apply-default-values'; +import { processStackTrace } from './format-error-stack'; import * as getComponentFallback from './get-component-fallback'; import GetComponentRetrievingInfo from './get-component-retrieving-info'; @@ -136,7 +137,7 @@ export default function getComponent(conf: Config, repository: Repository) { return env; }; - const renderer = ( + const renderer = async ( options: RendererOptions, cb: (result: GetComponentResult) => void ) => { @@ -311,7 +312,7 @@ export default function getComponent(conf: Config, repository: Repository) { ); }; - const returnComponent = (err: any, data: any) => { + const returnComponent = async (err: any, data: any) => { if (componentCallbackDone) { return; } @@ -381,7 +382,7 @@ export default function getComponent(conf: Config, repository: Repository) { error: err }); - return callback({ + const response = { status, response: { code: 'GENERIC_ERROR', @@ -391,10 +392,29 @@ export default function getComponent(conf: Config, repository: Repository) { details: { message: err.message, stack: err.stack, - originalError: err + originalError: err, + frame: undefined as string | undefined } } - }); + }; + + if (conf.local && err.stack) { + const { content } = await repository + .getDataProvider(component.name, component.version) + .catch(() => ({ content: null })); + if (content) { + const processedStack = await processStackTrace({ + stackTrace: err.stack, + code: content + }).catch(() => null); + if (processedStack) { + response.response.details.stack = processedStack.stack; + response.response.details.frame = processedStack.frame; + } + } + } + + return callback(response); } const response: { diff --git a/src/registry/views/info.tsx b/src/registry/views/info.tsx index 73856fbd..9ea65df5 100644 --- a/src/registry/views/info.tsx +++ b/src/registry/views/info.tsx @@ -197,13 +197,13 @@ export default function Info(vm: Vm) {

Component's href:

- + value={componentHref} + />

Accept-Language header:

diff --git a/src/registry/views/preview.ts b/src/registry/views/preview.ts index 53a560b1..a881e7f1 100644 --- a/src/registry/views/preview.ts +++ b/src/registry/views/preview.ts @@ -13,6 +13,7 @@ export default function preview(vm: { const imports = vm.component.oc.files.imports; const componentHref = `${baseUrl}${name}/${version}/${vm.qs}`; const clientHref = vm.fallbackClient || `${baseUrl}oc-client/client.js`; + const id = `oc-${name}@${version}`; return ` @@ -22,7 +23,144 @@ export default function preview(vm: { width: 100%; height: 100%; margin: 0; - }; + } + + /* OC preview error overlay */ + .oc-error-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.92); + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + z-index: 2147483647; + } + + .oc-error-card { + background: #0b0b0c; + border: 1px solid #222; + border-radius: 12px; + color: #f5f5f5; + max-width: 860px; + width: 100%; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6); + overflow: hidden; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Inter, Ubuntu, 'Helvetica Neue', Arial, sans-serif; + } + + .oc-error-card__header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-bottom: 1px solid #1e1e1e; + background: linear-gradient(180deg, rgba(255,255,255,.04), rgba(255,255,255,0)); + } + + .oc-error-title { + font-size: 16px; + font-weight: 600; + color: #ffffff; + } + + .oc-error-badge { + font-size: 12px; + background: #2b2b2d; + color: #f9f9f9; + border: 1px solid #3a3a3c; + border-radius: 999px; + padding: 4px 10px; + } + + .oc-error-card__body { + padding: 16px 20px; + display: flex; + flex-direction: column; + gap: 8px; + } + + .oc-error-row { + display: flex; + gap: 12px; + align-items: center; + flex-wrap: wrap; + } + + .oc-error-key { + min-width: 160px; + color: #b3b3b3; + font-size: 12px; + text-transform: uppercase; + letter-spacing: .04em; + } + + .oc-error-value { + color: #e8e8e8; + font-weight: 500; + } + + .oc-error-message { + margin-top: 4px; + background: #151516; + border: 1px solid #272729; + border-radius: 8px; + padding: 12px 14px; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 13px; + color: #f1d6d6; + } + + .oc-error-hint { + margin-top: 6px; + color: #d0d0d0; + } + + .oc-error-link { + color: #8ab4ff; + text-decoration: none; + } + .oc-error-link:hover { text-decoration: underline; } + + .oc-error-actions { + display: flex; + gap: 10px; + padding: 14px 20px; + border-top: 1px solid #1e1e1e; + background: rgba(255,255,255,.02); + } + + .oc-btn { + appearance: none; + border: 1px solid #2f2f31; + background: #1c1c1f; + color: #ffffff; + font-weight: 600; + border-radius: 8px; + padding: 8px 12px; + font-size: 13px; + cursor: pointer; + } + .oc-btn:hover { background: #232327; } + .oc-btn--secondary { background: transparent; } + .oc-btn--secondary:hover { background: #18181b; } + .oc-btn--ghost { background: transparent; border-color: transparent; color: #bdbdbd; } + .oc-btn--ghost:hover { background: #18181b; color: #ffffff; } + + .oc-error-details summary { + cursor: pointer; + color: #bdbdbd; + margin: 6px 0; + } + .oc-error-pre { + max-height: 280px; + overflow: auto; + background: #0f0f10; + border: 1px solid #242426; + border-radius: 8px; + padding: 12px; + color: #cfe3ff; + } @@ -36,11 +174,122 @@ export default function preview(vm: { } - + + + + + - ${vm.liveReload} `; diff --git a/src/registry/views/static/info.ts b/src/registry/views/static/info.ts index a0bc8b0a..397a41de 100644 --- a/src/registry/views/static/info.ts +++ b/src/registry/views/static/info.ts @@ -31,6 +31,14 @@ oc.cmd.push(function() { }; $('.refresh-preview').click(refreshPreview); + // Add Enter key handler for the href input field + $('#href').keypress(function(e) { + if (e.which === 13) { // Enter key + refreshPreview(); + return false; + } + }); + $('.open-preview').click(function() { refreshPreview(); var url = $('.preview').attr('src'); diff --git a/src/registry/views/static/style.ts b/src/registry/views/static/style.ts index 79ff4bfd..11908569 100644 --- a/src/registry/views/static/style.ts +++ b/src/registry/views/static/style.ts @@ -101,7 +101,7 @@ p { } .preview { - height: 300px; + height: 600px; border: 1px solid var(--color-border-default); }