From d44da42fd4199dc2d440401026957c171502c883 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Thu, 2 Oct 2025 14:49:19 +0900 Subject: [PATCH 1/4] feat(editor): Implement dynamic type loading for TS/JS autocompletion --- apps/remix-ide/src/app/editor/editor.js | 47 +++++++++- apps/remix-ide/src/app/editor/type-fetcher.ts | 81 ++++++++++++++++++ apps/remix-ide/src/app/editor/type-parser.ts | 21 +++++ .../src/lib/providers/tsCompletionProvider.ts | 85 +++++++++++++++++++ .../editor/src/lib/remix-ui-editor.tsx | 7 +- package.json | 2 + yarn.lock | 12 +++ 7 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 apps/remix-ide/src/app/editor/type-fetcher.ts create mode 100644 apps/remix-ide/src/app/editor/type-parser.ts create mode 100644 libs/remix-ui/editor/src/lib/providers/tsCompletionProvider.ts diff --git a/apps/remix-ide/src/app/editor/editor.js b/apps/remix-ide/src/app/editor/editor.js index c89fd8a1d07..8b3e980a0c0 100644 --- a/apps/remix-ide/src/app/editor/editor.js +++ b/apps/remix-ide/src/app/editor/editor.js @@ -6,6 +6,8 @@ import { Plugin } from '@remixproject/engine' import * as packageJson from '../../../../../package.json' import { PluginViewWrapper } from '@remix-ui/helper' +import { fetchAndLoadTypes } from './type-fetcher' + const EventManager = require('../../lib/events') const profile = { @@ -71,12 +73,19 @@ export default class Editor extends Plugin { this.api = {} this.dispatch = null this.ref = null + + this.monaco = null + this.typeLoaderDebounce = null } setDispatch (dispatch) { this.dispatch = dispatch } + setMonaco (monaco) { + this.monaco = monaco + } + updateComponent(state) { return this.setMonaco(monaco)} /> } @@ -128,6 +138,17 @@ export default class Editor extends Plugin { async onActivation () { this.activated = true + this.on('editor', 'editorMounted', () => { + if (!this.monaco) return + const tsDefaults = this.monaco.languages.typescript.typescriptDefaults + + tsDefaults.setCompilerOptions({ + moduleResolution: this.monaco.languages.typescript.ModuleResolutionKind.NodeJs, + typeRoots: ["file:///node_modules/@types", "file:///node_modules"], + target: this.monaco.languages.typescript.ScriptTarget.ES2020, + allowNonTsExtensions: true, + }) + }) this.on('sidePanel', 'focusChanged', (name) => { this.keepDecorationsFor(name, 'sourceAnnotationsPerFile') this.keepDecorationsFor(name, 'markerPerFile') @@ -158,6 +179,30 @@ export default class Editor extends Plugin { async _onChange (file) { this.triggerEvent('didChangeFile', [file]) + + if (this.monaco && (file.endsWith('.ts') || file.endsWith('.js'))) { + clearTimeout(this.typeLoaderDebounce) + this.typeLoaderDebounce = setTimeout(async () => { + if (!this.monaco) return + const model = this.monaco.editor.getModel(this.monaco.Uri.parse(file)) + if (!model) return + const code = model.getValue() + + try { + const npmImports = [...code.matchAll(/from\s+['"]((?![./]).+)['"]/g)].map(match => match[1]) + const uniquePackages = [...new Set(npmImports)] + + if (uniquePackages.length > 0) { + await Promise.all(uniquePackages.map(pkg => fetchAndLoadTypes(pkg, this.monaco))) + const tsDefaults = this.monaco.languages.typescript.typescriptDefaults + tsDefaults.setCompilerOptions(tsDefaults.getCompilerOptions()) + } + } catch (error) { + console.error('[Type Loader] Error during type loading process:', error) + } + }, 1500) + } + const currentFile = await this.call('fileManager', 'file') if (!currentFile) { return @@ -232,7 +277,7 @@ export default class Editor extends Plugin { this.emit('addModel', contentDep, 'typescript', pathDep, this.readOnlySessions[path]) } } else { - console.log("The file ", pathDep, " can't be found.") + // console.log("The file ", pathDep, " can't be found.") } } catch (e) { console.log(e) diff --git a/apps/remix-ide/src/app/editor/type-fetcher.ts b/apps/remix-ide/src/app/editor/type-fetcher.ts new file mode 100644 index 00000000000..d75715c7f5e --- /dev/null +++ b/apps/remix-ide/src/app/editor/type-fetcher.ts @@ -0,0 +1,81 @@ +import { Monaco } from '@monaco-editor/react' + +const loadedFiles = new Set() + +function resolvePath(baseFilePath: string, relativePath: string): string { + const newUrl = new URL(relativePath, baseFilePath) + return newUrl.href +} + +export async function fetchAndLoadTypes(packageName: string, monaco: Monaco) { + const initialPackageJsonPath = `file:///node_modules/${packageName}/package.json` + if (loadedFiles.has(initialPackageJsonPath)) return + + try { + const response = await fetch(`https://cdn.jsdelivr.net/npm/${packageName}/package.json`) + if (!response.ok) { + if (!packageName.startsWith('@types/')) { + console.warn(`[Type Fetcher] Failed to get package.json for "${packageName}". Trying @types...`) + return fetchAndLoadTypes(`@types/${packageName}`, monaco) + } + console.error(`[Type Fetcher] Failed to get package.json for "${packageName}".`) + return + } + + const packageJson = await response.json() + const filesToProcess: string[] = [] + + addFileToMonaco(initialPackageJsonPath, JSON.stringify(packageJson), monaco) + + const mainTypeFile = packageJson.types || packageJson.typings || 'index.d.ts' + const mainTypeFilePath = resolvePath(initialPackageJsonPath, mainTypeFile) + filesToProcess.push(mainTypeFilePath) + + if (packageJson.dependencies) { + for (const depName of Object.keys(packageJson.dependencies)) { + fetchAndLoadTypes(depName, monaco) + } + } + + while (filesToProcess.length > 0) { + const currentFilePath = filesToProcess.shift() + if (!currentFilePath || loadedFiles.has(currentFilePath)) continue + + try { + const cdnUrl = currentFilePath.replace('file:///node_modules/', 'https://cdn.jsdelivr.net/npm/') + const fileResponse = await fetch(cdnUrl) + + if (fileResponse.ok) { + const content = await fileResponse.text() + addFileToMonaco(currentFilePath, content, monaco) + + const relativeImports = [...content.matchAll(/(from\s+['"](\.\.?\/.*?)['"])|(import\s+['"](\.\.?\/.*?)['"])/g)] + for (const match of relativeImports) { + const relativePath = match[2] || match[4] + if (relativePath) { + const newPath = resolvePath(currentFilePath, relativePath) + const finalPath = newPath.endsWith('.d.ts') ? newPath : `${newPath}.d.ts` + if (!loadedFiles.has(finalPath)) { + filesToProcess.push(finalPath) + } + } + } + } else { + console.warn(`[Type Fetcher] 404 - Could not fetch ${cdnUrl}`) + } + } catch (e) { + console.error(`[Type Fetcher] Error fetching or processing ${currentFilePath}`, e) + loadedFiles.add(currentFilePath) + } + } + } catch (error) { + console.error(`[Type Fetcher] Critical error processing ${packageName}:`, error) + } +} + +function addFileToMonaco(filePath: string, content: string, monaco: Monaco) { + if (loadedFiles.has(filePath)) return + + monaco.languages.typescript.typescriptDefaults.addExtraLib(content, filePath) + loadedFiles.add(filePath) +} \ No newline at end of file diff --git a/apps/remix-ide/src/app/editor/type-parser.ts b/apps/remix-ide/src/app/editor/type-parser.ts new file mode 100644 index 00000000000..39e9b5961f5 --- /dev/null +++ b/apps/remix-ide/src/app/editor/type-parser.ts @@ -0,0 +1,21 @@ +import * as acorn from 'acorn' + +export function parseImports(code: string): string[] { + const packages: string[] = [] + + try { + const ast = acorn.parse(code, { sourceType: 'module', ecmaVersion: 'latest' }) + + for (const node of ast.body) { + if (node.type === 'ImportDeclaration') { + if (node.source && typeof node.source.value === 'string') { + packages.push(node.source.value) + } + } + } + } catch (error) { + console.error('[Type Parser] Code parsing error:', error.message) + } + + return [...new Set(packages)] +} \ No newline at end of file diff --git a/libs/remix-ui/editor/src/lib/providers/tsCompletionProvider.ts b/libs/remix-ui/editor/src/lib/providers/tsCompletionProvider.ts new file mode 100644 index 00000000000..0e879da5df6 --- /dev/null +++ b/libs/remix-ui/editor/src/lib/providers/tsCompletionProvider.ts @@ -0,0 +1,85 @@ +import { monacoTypes } from '@remix-ui/editor' + +interface TsCompletionInfo { + entries: { + name: string + kind: string + }[] +} + +export class RemixTSCompletionProvider implements monacoTypes.languages.CompletionItemProvider { + monaco: any + + constructor(monaco: any) { + this.monaco = monaco + } + + triggerCharacters = ['.', '"', "'", '/', '@'] + + async provideCompletionItems(model: monacoTypes.editor.ITextModel, position: monacoTypes.Position, context: monacoTypes.languages.CompletionContext): Promise { + const word = model.getWordUntilPosition(position) + const range = { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: word.startColumn, + endColumn: word.endColumn + } + + try { + const worker = await this.monaco.languages.typescript.getTypeScriptWorker() + const client = await worker(model.uri) + const completions: TsCompletionInfo = await client.getCompletionsAtPosition( + model.uri.toString(), + model.getOffsetAt(position) + ) + + if (!completions || !completions.entries) { + return { suggestions: []} + } + + const suggestions = completions.entries.map(entry => { + return { + label: entry.name, + kind: this.mapTsCompletionKindToMonaco(entry.kind), + insertText: entry.name, + range: range + } + }) + + return { suggestions } + } catch (error) { + console.error('[TSCompletionProvider] Error fetching completions:', error) + return { suggestions: []} + } + } + + private mapTsCompletionKindToMonaco(kind: string): monacoTypes.languages.CompletionItemKind { + const { CompletionItemKind } = this.monaco.languages + switch (kind) { + case 'method': + case 'memberFunction': + return CompletionItemKind.Method + case 'function': + return CompletionItemKind.Function + case 'property': + case 'memberVariable': + return CompletionItemKind.Property + case 'class': + return CompletionItemKind.Class + case 'interface': + return CompletionItemKind.Interface + case 'keyword': + return CompletionItemKind.Keyword + case 'variable': + return CompletionItemKind.Variable + case 'constructor': + return CompletionItemKind.Constructor + case 'enum': + return CompletionItemKind.Enum + case 'module': + return CompletionItemKind.Module + default: + return CompletionItemKind.Text + } + } +} \ No newline at end of file diff --git a/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx b/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx index aa913e823d4..036c037325e 100644 --- a/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx +++ b/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx @@ -28,6 +28,7 @@ import { noirLanguageConfig, noirTokensProvider } from './syntaxes/noir' import { IPosition, IRange } from 'monaco-editor' import { GenerationParams } from '@remix/remix-ai-core'; import { RemixInLineCompletionProvider } from './providers/inlineCompletionProvider' +import { RemixTSCompletionProvider } from './providers/tsCompletionProvider' const _paq = (window._paq = window._paq || []) // Key for localStorage @@ -154,6 +155,7 @@ export interface EditorUIProps { } plugin: PluginType editorAPI: EditorAPIType + setMonaco: (monaco: Monaco) => void } const contextMenuEvent = new EventManager() export const EditorUI = (props: EditorUIProps) => { @@ -1152,6 +1154,7 @@ export const EditorUI = (props: EditorUIProps) => { function handleEditorWillMount(monaco) { monacoRef.current = monaco + props.setMonaco(monaco) // Register a new language monacoRef.current.languages.register({ id: 'remix-solidity' }) monacoRef.current.languages.register({ id: 'remix-cairo' }) @@ -1164,9 +1167,11 @@ export const EditorUI = (props: EditorUIProps) => { // Allow JSON schema requests monacoRef.current.languages.json.jsonDefaults.setDiagnosticsOptions({ enableSchemaRequest: true }) + monacoRef.current.languages.registerCompletionItemProvider('typescript', new RemixTSCompletionProvider(monaco)) + monacoRef.current.languages.registerCompletionItemProvider('javascript', new RemixTSCompletionProvider(monaco)) + // hide the module resolution error. We have to remove this when we know how to properly resolve imports. monacoRef.current.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ diagnosticCodesToIgnore: [2792]}) - // Register a tokens provider for the language monacoRef.current.languages.setMonarchTokensProvider('remix-solidity', solidityTokensProvider as any) monacoRef.current.languages.setLanguageConfiguration('remix-solidity', solidityLanguageConfig as any) diff --git a/package.json b/package.json index ecf828ee24e..e27e40afe3d 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,9 @@ "@reown/appkit": "^1.7.4", "@reown/appkit-adapter-ethers": "^1.7.4", "@ricarso/react-image-magnifiers": "^1.9.0", + "@types/acorn": "^6.0.4", "@types/nightwatch": "^2.3.1", + "acorn": "^8.15.0", "ansi-gray": "^0.1.1", "assert": "^2.1.0", "async": "^2.6.2", diff --git a/yarn.lock b/yarn.lock index 4974ad25681..4003c39cffb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7248,6 +7248,13 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== +"@types/acorn@^6.0.4": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@types/acorn/-/acorn-6.0.4.tgz#b1a652a373d0cace52dace608fced14f58e9c4a9" + integrity sha512-DafqcBAjbOOmgqIx3EF9EAdBKAKgspv00aQVIW3fVQ0TXo5ZPBeSRey1SboVAUzjw8Ucm7cd1gtTSlosYoEQLA== + dependencies: + acorn "*" + "@types/aria-query@^4.2.0": version "4.2.2" resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc" @@ -8718,6 +8725,11 @@ acorn-walk@^8.0.0, acorn-walk@^8.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== +acorn@*, acorn@^8.15.0: + version "8.15.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" + integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== + "acorn@>= 2.5.2 <= 5.7.5": version "5.7.4" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" From e35cc8b9bc0c94627a66c1be23832a0d918d53ed Mon Sep 17 00:00:00 2001 From: ci-bot Date: Tue, 7 Oct 2025 15:47:35 +0900 Subject: [PATCH 2/4] ethers autocompletion --- apps/remix-ide/src/app/editor/editor.js | 73 ++++++- apps/remix-ide/src/app/editor/type-fetcher.ts | 189 ++++++++++++------ apps/remix-ide/src/app/editor/type-parser.ts | 21 -- 3 files changed, 196 insertions(+), 87 deletions(-) delete mode 100644 apps/remix-ide/src/app/editor/type-parser.ts diff --git a/apps/remix-ide/src/app/editor/editor.js b/apps/remix-ide/src/app/editor/editor.js index 8b3e980a0c0..98348e14f0f 100644 --- a/apps/remix-ide/src/app/editor/editor.js +++ b/apps/remix-ide/src/app/editor/editor.js @@ -6,7 +6,7 @@ import { Plugin } from '@remixproject/engine' import * as packageJson from '../../../../../package.json' import { PluginViewWrapper } from '@remix-ui/helper' -import { fetchAndLoadTypes } from './type-fetcher' +import { startTypeLoadingProcess } from './type-fetcher' const EventManager = require('../../lib/events') @@ -76,6 +76,8 @@ export default class Editor extends Plugin { this.monaco = null this.typeLoaderDebounce = null + + this.tsModuleMappings = {} } setDispatch (dispatch) { @@ -144,9 +146,10 @@ export default class Editor extends Plugin { tsDefaults.setCompilerOptions({ moduleResolution: this.monaco.languages.typescript.ModuleResolutionKind.NodeJs, - typeRoots: ["file:///node_modules/@types", "file:///node_modules"], target: this.monaco.languages.typescript.ScriptTarget.ES2020, allowNonTsExtensions: true, + baseUrl: 'file:///node_modules/', + paths: {} }) }) this.on('sidePanel', 'focusChanged', (name) => { @@ -177,6 +180,26 @@ export default class Editor extends Plugin { this.off('sidePanel', 'pluginDisabled') } + updateTsCompilerOptions() { + console.log('[Module Mapper] Updating TS compiler options with new paths:', this.tsModuleMappings) + const tsDefaults = this.monaco.languages.typescript.typescriptDefaults + const oldOptions = tsDefaults.getCompilerOptions() + + const newOptions = { + ...oldOptions, + baseUrl: 'file:///node_modules/', + paths: this.tsModuleMappings + } + + console.log('[DEBUG 3] Updating TS compiler with new options:', JSON.stringify(newOptions, null, 2)) + tsDefaults.setCompilerOptions(newOptions) + + setTimeout(() => { + const allLibs = tsDefaults.getExtraLibs() + console.log('[DEBUG 4] Final check - Monaco extraLibs state:', Object.keys(allLibs).length, 'libs loaded.') + }, 2000) + } + async _onChange (file) { this.triggerEvent('didChangeFile', [file]) @@ -189,14 +212,48 @@ export default class Editor extends Plugin { const code = model.getValue() try { - const npmImports = [...code.matchAll(/from\s+['"]((?![./]).+)['"]/g)].map(match => match[1]) - const uniquePackages = [...new Set(npmImports)] + + const extractPackageName = (importPath) => { + if (importPath.startsWith('@')) { + const parts = importPath.split('/') + return `${parts[0]}/${parts[1]}` + } + return importPath.split('/')[0] + } + + const rawImports = [...code.matchAll(/from\s+['"]((?![./]).*?)['"]/g)].map(match => match[1]) - if (uniquePackages.length > 0) { - await Promise.all(uniquePackages.map(pkg => fetchAndLoadTypes(pkg, this.monaco))) - const tsDefaults = this.monaco.languages.typescript.typescriptDefaults - tsDefaults.setCompilerOptions(tsDefaults.getCompilerOptions()) + const uniquePackages = [...new Set(rawImports.map(extractPackageName))] + console.log('[DEBUG 1] Extracted Package Names:', uniquePackages) + const newPackages = uniquePackages.filter(p => !this.tsModuleMappings[p]) + if (newPackages.length === 0) return + + console.log('[Module Mapper] New packages detected:', newPackages) + + let newPathsFound = false + const promises = newPackages.map(async (pkg) => { + try { + const path = await startTypeLoadingProcess(pkg, this.monaco) + if (path && typeof path === 'string') { + const relativePath = path.replace('file:///node_modules/', '') + const dirPath = relativePath.substring(0, relativePath.lastIndexOf('/') + 1) + + this.tsModuleMappings[pkg] = [dirPath] + this.tsModuleMappings[`${pkg}/*`] = [`${pkg}/*`] + + newPathsFound = true + } + } catch (error) { + console.error(`[Module Mapper] Failed to process types for ${pkg}`, error) + } + }) + + await Promise.all(promises) + + if (newPathsFound) { + setTimeout(() => this.updateTsCompilerOptions(), 1000) } + } catch (error) { console.error('[Type Loader] Error during type loading process:', error) } diff --git a/apps/remix-ide/src/app/editor/type-fetcher.ts b/apps/remix-ide/src/app/editor/type-fetcher.ts index d75715c7f5e..50055a75d1e 100644 --- a/apps/remix-ide/src/app/editor/type-fetcher.ts +++ b/apps/remix-ide/src/app/editor/type-fetcher.ts @@ -1,81 +1,154 @@ import { Monaco } from '@monaco-editor/react' -const loadedFiles = new Set() +const processedPackages = new Set() +const loadedLibs = new Set() +const NODE_BUILTINS = new Set(['util', 'events', 'buffer', 'stream', 'path', 'fs', 'os', 'crypto', 'http', 'https', 'url', 'zlib']) -function resolvePath(baseFilePath: string, relativePath: string): string { - const newUrl = new URL(relativePath, baseFilePath) - return newUrl.href +class NoTypesError extends Error { + constructor(message: string) { + super(message) + this.name = 'NoTypesError' + } +} + +function getTypesPackageName(packageName: string): string { + if (packageName.startsWith('@')) { + const mangledName = packageName.substring(1).replace('/', '__'); + return `@types/${mangledName}` + } + return `@types/${packageName}` } -export async function fetchAndLoadTypes(packageName: string, monaco: Monaco) { - const initialPackageJsonPath = `file:///node_modules/${packageName}/package.json` - if (loadedFiles.has(initialPackageJsonPath)) return +export async function startTypeLoadingProcess(packageName: string, monaco: Monaco): Promise { + if (NODE_BUILTINS.has(packageName)) { + packageName = '@types/node' + } + + if (processedPackages.has(packageName)) return + processedPackages.add(packageName) + console.log(`[Type Fetcher] Starting type loading process for "${packageName}"...`) + try { - const response = await fetch(`https://cdn.jsdelivr.net/npm/${packageName}/package.json`) - if (!response.ok) { - if (!packageName.startsWith('@types/')) { - console.warn(`[Type Fetcher] Failed to get package.json for "${packageName}". Trying @types...`) - return fetchAndLoadTypes(`@types/${packageName}`, monaco) + return await loadTypesInBackground(packageName, monaco) + } catch (error) { + if (error instanceof NoTypesError) { + console.warn(`[Type Fetcher] No types found for "${packageName}". Reason:`, error.message) + const typesPackageName = getTypesPackageName(packageName) + console.log(`[Type Fetcher] Trying ${typesPackageName} as a fallback...`) + return startTypeLoadingProcess(typesPackageName, monaco) + } else { + console.error(`[Type Fetcher] Loading process failed for "${packageName}" and will not fallback to @types. Error:`, error.message) + } + } +} + +async function resolveAndFetchDts(resolvedUrl: string): Promise<{ finalUrl: string; content: string }> { + const urlWithoutTrailingSlash = resolvedUrl.endsWith('/') ? resolvedUrl.slice(0, -1) : resolvedUrl + const attempts: string[] = [] + + if (/\.(m|c)?js$/.test(urlWithoutTrailingSlash)) { + attempts.push(urlWithoutTrailingSlash.replace(/\.(m|c)?js$/, '.d.ts')) + } else if (!urlWithoutTrailingSlash.endsWith('.d.ts')) { + attempts.push(`${urlWithoutTrailingSlash}.d.ts`) + attempts.push(`${urlWithoutTrailingSlash}/index.d.ts`) + } else { + attempts.push(urlWithoutTrailingSlash) + } + + for (const url of attempts) { + try { + const response = await fetch(url) + if (response.ok) { + return { finalUrl: url, content: await response.text() } } - console.error(`[Type Fetcher] Failed to get package.json for "${packageName}".`) - return + } catch (e) {} + } + throw new Error(`Could not resolve DTS file for ${resolvedUrl}`) +} + +async function loadTypesInBackground(packageName: string, monaco: Monaco): Promise { + const baseUrl = `https://cdn.jsdelivr.net/npm/${packageName}/` + const packageJsonUrl = `${baseUrl}package.json` + const response = await fetch(packageJsonUrl) + + if (!response.ok) throw new Error(`Failed to fetch package.json for "${packageName}"`) + + const packageJson = await response.json() + + console.log(`[Type Fetcher] Fetched package.json for "${packageName}", version: ${packageJson.version}`) + + addLibToMonaco(`file:///node_modules/${packageName}/package.json`, JSON.stringify(packageJson), monaco) + + let mainTypeFileRelativePath: string | undefined = undefined + const exports = packageJson.exports + + if (typeof exports === 'object' && exports !== null) { + const mainExport = exports['.'] + if (typeof mainExport === 'object' && mainExport !== null) { + if (typeof mainExport.types === 'string') mainTypeFileRelativePath = mainExport.types + else if (typeof mainExport.import === 'string') mainTypeFileRelativePath = mainExport.import + else if (typeof mainExport.default === 'string') mainTypeFileRelativePath = mainExport.default + } else if (typeof mainExport === 'string') { + mainTypeFileRelativePath = mainExport } + } + + if (!mainTypeFileRelativePath) { + mainTypeFileRelativePath = packageJson.types || packageJson.typings + } - const packageJson = await response.json() - const filesToProcess: string[] = [] + if (!mainTypeFileRelativePath) { + throw new NoTypesError(`No 'types', 'typings', or 'exports' field found in package.json.`) + } - addFileToMonaco(initialPackageJsonPath, JSON.stringify(packageJson), monaco) + if (!mainTypeFileRelativePath.startsWith('./')) mainTypeFileRelativePath = './' + mainTypeFileRelativePath - const mainTypeFile = packageJson.types || packageJson.typings || 'index.d.ts' - const mainTypeFilePath = resolvePath(initialPackageJsonPath, mainTypeFile) - filesToProcess.push(mainTypeFilePath) + const rootTypeFileUrl = new URL(mainTypeFileRelativePath, baseUrl).href + console.log('[DEBUG 2-1] Attempting to fetch main type file from URL:', rootTypeFileUrl) + const { finalUrl: finalRootUrl, content: rootContent } = await resolveAndFetchDts(rootTypeFileUrl) + const virtualPath = finalRootUrl.replace('https://cdn.jsdelivr.net/npm', 'file:///node_modules') + addLibToMonaco(virtualPath, rootContent, monaco) + console.log(`[Type Fetcher] Immediate load complete for ${packageName}'s main file.`); + + (async () => { if (packageJson.dependencies) { - for (const depName of Object.keys(packageJson.dependencies)) { - fetchAndLoadTypes(depName, monaco) - } + console.log(`[Type Fetcher] Found ${Object.keys(packageJson.dependencies).length} dependencies for ${packageName}. Fetching them...`) + Object.keys(packageJson.dependencies).forEach(dep => startTypeLoadingProcess(dep, monaco)) } - while (filesToProcess.length > 0) { - const currentFilePath = filesToProcess.shift() - if (!currentFilePath || loadedFiles.has(currentFilePath)) continue - - try { - const cdnUrl = currentFilePath.replace('file:///node_modules/', 'https://cdn.jsdelivr.net/npm/') - const fileResponse = await fetch(cdnUrl) - - if (fileResponse.ok) { - const content = await fileResponse.text() - addFileToMonaco(currentFilePath, content, monaco) - - const relativeImports = [...content.matchAll(/(from\s+['"](\.\.?\/.*?)['"])|(import\s+['"](\.\.?\/.*?)['"])/g)] - for (const match of relativeImports) { - const relativePath = match[2] || match[4] - if (relativePath) { - const newPath = resolvePath(currentFilePath, relativePath) - const finalPath = newPath.endsWith('.d.ts') ? newPath : `${newPath}.d.ts` - if (!loadedFiles.has(finalPath)) { - filesToProcess.push(finalPath) - } - } + const queue = [{ url: finalRootUrl, content: rootContent }] + const processedUrls = new Set([finalRootUrl]) + + while (queue.length > 0) { + const { url: currentFileUrl, content: currentFileContent } = queue.shift()! + const relativeImports = [...currentFileContent.matchAll(/(?:from|import)\s+['"]((?:\.\.?\/)[^'"]+)['"]/g)] + + for (const match of relativeImports) { + const relativePath = match[1] + const resolvedUrl = new URL(relativePath, currentFileUrl).href + + if (processedUrls.has(resolvedUrl)) continue + processedUrls.add(resolvedUrl) + + try { + const { finalUrl, content } = await resolveAndFetchDts(resolvedUrl) + const newVirtualPath = finalUrl.replace('https://cdn.jsdelivr.net/npm', 'file:///node_modules') + if (!loadedLibs.has(newVirtualPath)) { + addLibToMonaco(newVirtualPath, content, monaco) + queue.push({ url: finalUrl, content }) } - } else { - console.warn(`[Type Fetcher] 404 - Could not fetch ${cdnUrl}`) - } - } catch (e) { - console.error(`[Type Fetcher] Error fetching or processing ${currentFilePath}`, e) - loadedFiles.add(currentFilePath) + } catch (error) {} } } - } catch (error) { - console.error(`[Type Fetcher] Critical error processing ${packageName}:`, error) - } -} + })() -function addFileToMonaco(filePath: string, content: string, monaco: Monaco) { - if (loadedFiles.has(filePath)) return + return virtualPath +} +function addLibToMonaco(filePath: string, content: string, monaco: Monaco) { + if (loadedLibs.has(filePath)) return monaco.languages.typescript.typescriptDefaults.addExtraLib(content, filePath) - loadedFiles.add(filePath) + loadedLibs.add(filePath) } \ No newline at end of file diff --git a/apps/remix-ide/src/app/editor/type-parser.ts b/apps/remix-ide/src/app/editor/type-parser.ts deleted file mode 100644 index 39e9b5961f5..00000000000 --- a/apps/remix-ide/src/app/editor/type-parser.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as acorn from 'acorn' - -export function parseImports(code: string): string[] { - const packages: string[] = [] - - try { - const ast = acorn.parse(code, { sourceType: 'module', ecmaVersion: 'latest' }) - - for (const node of ast.body) { - if (node.type === 'ImportDeclaration') { - if (node.source && typeof node.source.value === 'string') { - packages.push(node.source.value) - } - } - } - } catch (error) { - console.error('[Type Parser] Code parsing error:', error.message) - } - - return [...new Set(packages)] -} \ No newline at end of file From ff2d3f231212e656dfc3dcad8b85dc4c39f2c6b6 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Tue, 7 Oct 2025 17:13:03 +0900 Subject: [PATCH 3/4] wip: dynamic loading from cdn --- apps/remix-ide/src/app/editor/editor.js | 10 +- apps/remix-ide/src/app/editor/type-fetcher.ts | 106 ++++++++---------- .../src/app/plugins/script-runner-bridge.tsx | 40 ++++++- 3 files changed, 89 insertions(+), 67 deletions(-) diff --git a/apps/remix-ide/src/app/editor/editor.js b/apps/remix-ide/src/app/editor/editor.js index 98348e14f0f..96e19d608b1 100644 --- a/apps/remix-ide/src/app/editor/editor.js +++ b/apps/remix-ide/src/app/editor/editor.js @@ -233,12 +233,10 @@ export default class Editor extends Plugin { let newPathsFound = false const promises = newPackages.map(async (pkg) => { try { - const path = await startTypeLoadingProcess(pkg, this.monaco) - if (path && typeof path === 'string') { - const relativePath = path.replace('file:///node_modules/', '') - const dirPath = relativePath.substring(0, relativePath.lastIndexOf('/') + 1) - - this.tsModuleMappings[pkg] = [dirPath] + const result = await startTypeLoadingProcess(pkg, this.monaco) + + if (result && result.virtualPath) { + this.tsModuleMappings[pkg] = [`${pkg}/`] this.tsModuleMappings[`${pkg}/*`] = [`${pkg}/*`] newPathsFound = true diff --git a/apps/remix-ide/src/app/editor/type-fetcher.ts b/apps/remix-ide/src/app/editor/type-fetcher.ts index 50055a75d1e..37866944af6 100644 --- a/apps/remix-ide/src/app/editor/type-fetcher.ts +++ b/apps/remix-ide/src/app/editor/type-fetcher.ts @@ -19,7 +19,7 @@ function getTypesPackageName(packageName: string): string { return `@types/${packageName}` } -export async function startTypeLoadingProcess(packageName: string, monaco: Monaco): Promise { +export async function startTypeLoadingProcess(packageName: string, monaco: Monaco): Promise<{ virtualPath: string; hasExports: boolean } | void> { if (NODE_BUILTINS.has(packageName)) { packageName = '@types/node' } @@ -67,7 +67,7 @@ async function resolveAndFetchDts(resolvedUrl: string): Promise<{ finalUrl: stri throw new Error(`Could not resolve DTS file for ${resolvedUrl}`) } -async function loadTypesInBackground(packageName: string, monaco: Monaco): Promise { +async function loadTypesInBackground(packageName: string, monaco: Monaco): Promise<{ virtualPath: string; hasExports: boolean } | void> { const baseUrl = `https://cdn.jsdelivr.net/npm/${packageName}/` const packageJsonUrl = `${baseUrl}package.json` const response = await fetch(packageJsonUrl) @@ -77,74 +77,60 @@ async function loadTypesInBackground(packageName: string, monaco: Monaco): Promi const packageJson = await response.json() console.log(`[Type Fetcher] Fetched package.json for "${packageName}", version: ${packageJson.version}`) - addLibToMonaco(`file:///node_modules/${packageName}/package.json`, JSON.stringify(packageJson), monaco) - let mainTypeFileRelativePath: string | undefined = undefined - const exports = packageJson.exports - - if (typeof exports === 'object' && exports !== null) { - const mainExport = exports['.'] - if (typeof mainExport === 'object' && mainExport !== null) { - if (typeof mainExport.types === 'string') mainTypeFileRelativePath = mainExport.types - else if (typeof mainExport.import === 'string') mainTypeFileRelativePath = mainExport.import - else if (typeof mainExport.default === 'string') mainTypeFileRelativePath = mainExport.default - } else if (typeof mainExport === 'string') { - mainTypeFileRelativePath = mainExport - } - } - - if (!mainTypeFileRelativePath) { - mainTypeFileRelativePath = packageJson.types || packageJson.typings - } - - if (!mainTypeFileRelativePath) { - throw new NoTypesError(`No 'types', 'typings', or 'exports' field found in package.json.`) - } - - if (!mainTypeFileRelativePath.startsWith('./')) mainTypeFileRelativePath = './' + mainTypeFileRelativePath + const typePathsToFetch = new Set() - const rootTypeFileUrl = new URL(mainTypeFileRelativePath, baseUrl).href - console.log('[DEBUG 2-1] Attempting to fetch main type file from URL:', rootTypeFileUrl) + const hasExports = typeof packageJson.exports === 'object' && packageJson.exports !== null + console.log(`[Type Fetcher DBG] 'hasExports' field detected: ${hasExports}`) - const { finalUrl: finalRootUrl, content: rootContent } = await resolveAndFetchDts(rootTypeFileUrl) - const virtualPath = finalRootUrl.replace('https://cdn.jsdelivr.net/npm', 'file:///node_modules') - addLibToMonaco(virtualPath, rootContent, monaco) - console.log(`[Type Fetcher] Immediate load complete for ${packageName}'s main file.`); - - (async () => { - if (packageJson.dependencies) { - console.log(`[Type Fetcher] Found ${Object.keys(packageJson.dependencies).length} dependencies for ${packageName}. Fetching them...`) - Object.keys(packageJson.dependencies).forEach(dep => startTypeLoadingProcess(dep, monaco)) + if (hasExports) { + for (const key in packageJson.exports) { + const entry = packageJson.exports[key] + if (typeof entry === 'object' && entry !== null && typeof entry.types === 'string') { + console.log(`[Type Fetcher DBG] Found types in exports['${key}']: ${entry.types}`) + typePathsToFetch.add(entry.types) + } } + } - const queue = [{ url: finalRootUrl, content: rootContent }] - const processedUrls = new Set([finalRootUrl]) - - while (queue.length > 0) { - const { url: currentFileUrl, content: currentFileContent } = queue.shift()! - const relativeImports = [...currentFileContent.matchAll(/(?:from|import)\s+['"]((?:\.\.?\/)[^'"]+)['"]/g)] - - for (const match of relativeImports) { - const relativePath = match[1] - const resolvedUrl = new URL(relativePath, currentFileUrl).href + const mainTypePath = packageJson.types || packageJson.typings + console.log(`[Type Fetcher DBG] Top-level 'types' field: ${mainTypePath}`) + if (typeof mainTypePath === 'string') { + typePathsToFetch.add(mainTypePath) + } - if (processedUrls.has(resolvedUrl)) continue - processedUrls.add(resolvedUrl) + console.log(`[Type Fetcher DBG] Total type paths found: ${typePathsToFetch.size}`) + if (typePathsToFetch.size === 0) { + throw new NoTypesError(`No type definition entry found in package.json.`) + } - try { - const { finalUrl, content } = await resolveAndFetchDts(resolvedUrl) - const newVirtualPath = finalUrl.replace('https://cdn.jsdelivr.net/npm', 'file:///node_modules') - if (!loadedLibs.has(newVirtualPath)) { - addLibToMonaco(newVirtualPath, content, monaco) - queue.push({ url: finalUrl, content }) - } - } catch (error) {} - } + let mainVirtualPath = '' + for (const relativePath of typePathsToFetch) { + let cleanPath = relativePath + if (!cleanPath.startsWith('./')) cleanPath = './' + cleanPath + + const fileUrl = new URL(cleanPath, baseUrl).href + try { + const { finalUrl, content } = await resolveAndFetchDts(fileUrl) + const virtualPath = finalUrl.replace('https://cdn.jsdelivr.net/npm', 'file:///node_modules') + addLibToMonaco(virtualPath, content, monaco) + if (!mainVirtualPath) mainVirtualPath = virtualPath + } catch (error) { + console.warn(`[Type Fetcher] Could not fetch sub-type file: ${fileUrl}`, error) } - })() + } + + if (!mainVirtualPath) throw new Error('Failed to fetch any type definition files.') - return virtualPath + console.log(`[Type Fetcher] Completed fetching all type definitions for ${packageName}.`) + + if (packageJson.dependencies) { + console.log(`[Type Fetcher] Found ${Object.keys(packageJson.dependencies).length} dependencies for ${packageName}. Fetching them...`) + Object.keys(packageJson.dependencies).forEach(dep => startTypeLoadingProcess(dep, monaco)) + } + + return { virtualPath: mainVirtualPath, hasExports } } function addLibToMonaco(filePath: string, content: string, monaco: Monaco) { diff --git a/apps/remix-ide/src/app/plugins/script-runner-bridge.tsx b/apps/remix-ide/src/app/plugins/script-runner-bridge.tsx index 961afb16fc8..55b05e168a8 100644 --- a/apps/remix-ide/src/app/plugins/script-runner-bridge.tsx +++ b/apps/remix-ide/src/app/plugins/script-runner-bridge.tsx @@ -28,6 +28,36 @@ const configFileName = 'remix.config.json' let baseUrl = 'https://remix-project-org.github.io/script-runner-generator' const customBuildUrl = 'http://localhost:4000/build' // this will be used when the server is ready +/** + * Transforms standard import statements into dynamic import statements for runtime execution. + * @param {string} scriptContent The original script content. + * @returns {string} The transformed script content. + */ +function transformScriptForRuntime(scriptContent: string): string { + // 1. dynamicImport 헬퍼 함수를 스크립트 맨 위에 주입 + const dynamicImportHelper = `const dynamicImport = (p) => new Function(\`return import('https://cdn.jsdelivr.net/npm/\${p}/+esm')\`)();\n`; + + // 2. 다양한 import 구문을 변환 + // 'import { ... } from "package"' 구문 + let transformed = scriptContent.replace( + /import\s+({[\s\S]*?})\s+from\s+['"]([^'"]+)['"]/g, + 'const $1 = await dynamicImport("$2");' + ); + // 'import Default from "package"' 구문 + transformed = transformed.replace( + /import\s+([\w\d_$]+)\s+from\s+['"]([^'"]+)['"]/g, + 'const $1 = (await dynamicImport("$2")).default;' + ); + // 'import * as name from "package"' 구문 + transformed = transformed.replace( + /import\s+\*\s+as\s+([\w\d_$]+)\s+from\s+['"]([^'"]+)['"]/g, + 'const $1 = await dynamicImport("$2");' + ); + + // 3. 모든 코드를 async IIFE로 감싸서 top-level await 문제 해결 + return `${dynamicImportHelper}\n(async () => {\n try {\n${transformed}\n } catch (e) { console.error('Error executing script:', e); }\n})();`; +} + export class ScriptRunnerBridgePlugin extends Plugin { engine: Engine dispatch: React.Dispatch = () => {} @@ -197,7 +227,15 @@ export class ScriptRunnerBridgePlugin extends Plugin { } try { this.setIsLoading(this.activeConfig.name, true) - await this.call(`${this.scriptRunnerProfileName}${this.activeConfig.name}`, 'execute', script, filePath) + const transformedScript = transformScriptForRuntime(script); + + console.log('--- [ScriptRunner] Original Script ---'); + console.log(script); + console.log('--- [ScriptRunner] Transformed Script for Runtime ---'); + console.log(transformedScript); + + await this.call(`${this.scriptRunnerProfileName}${this.activeConfig.name}`, 'execute',transformedScript, filePath) + } catch (e) { console.error('Error executing script', e) } From adb74ac1045942af0a4a51141e7c990e8b78497f Mon Sep 17 00:00:00 2001 From: ci-bot Date: Tue, 7 Oct 2025 17:50:45 +0900 Subject: [PATCH 4/4] bug fix --- apps/remix-ide/src/app/editor/editor.js | 8 ++- apps/remix-ide/src/app/editor/type-fetcher.ts | 60 ++++++++++++++++--- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/apps/remix-ide/src/app/editor/editor.js b/apps/remix-ide/src/app/editor/editor.js index 96e19d608b1..121026d9211 100644 --- a/apps/remix-ide/src/app/editor/editor.js +++ b/apps/remix-ide/src/app/editor/editor.js @@ -236,9 +236,13 @@ export default class Editor extends Plugin { const result = await startTypeLoadingProcess(pkg, this.monaco) if (result && result.virtualPath) { - this.tsModuleMappings[pkg] = [`${pkg}/`] - this.tsModuleMappings[`${pkg}/*`] = [`${pkg}/*`] + // this.tsModuleMappings[pkg] = [`${pkg}/`] + // this.tsModuleMappings[`${pkg}/*`] = [`${pkg}/*`] + const typeFileRelativePath = result.virtualPath.replace('file:///node_modules/', '') + this.tsModuleMappings[pkg] = [typeFileRelativePath] + this.tsModuleMappings[`${pkg}/*`] = [`${pkg}/*`] + newPathsFound = true } } catch (error) { diff --git a/apps/remix-ide/src/app/editor/type-fetcher.ts b/apps/remix-ide/src/app/editor/type-fetcher.ts index 37866944af6..0f2e0c69fd2 100644 --- a/apps/remix-ide/src/app/editor/type-fetcher.ts +++ b/apps/remix-ide/src/app/editor/type-fetcher.ts @@ -32,9 +32,11 @@ export async function startTypeLoadingProcess(packageName: string, monaco: Monac try { return await loadTypesInBackground(packageName, monaco) } catch (error) { - if (error instanceof NoTypesError) { + if (error.message.includes('No type definition') || error.message.includes('Failed to fetch any type definition')) { console.warn(`[Type Fetcher] No types found for "${packageName}". Reason:`, error.message) const typesPackageName = getTypesPackageName(packageName) + if (packageName === typesPackageName) return + console.log(`[Type Fetcher] Trying ${typesPackageName} as a fallback...`) return startTypeLoadingProcess(typesPackageName, monaco) } else { @@ -49,9 +51,15 @@ async function resolveAndFetchDts(resolvedUrl: string): Promise<{ finalUrl: stri if (/\.(m|c)?js$/.test(urlWithoutTrailingSlash)) { attempts.push(urlWithoutTrailingSlash.replace(/\.(m|c)?js$/, '.d.ts')) - } else if (!urlWithoutTrailingSlash.endsWith('.d.ts')) { + attempts.push(urlWithoutTrailingSlash.replace(/\.(m|c)?js$/, '.d.mts')) + attempts.push(urlWithoutTrailingSlash.replace(/\.(m|c)?js$/, '.d.cts')) + } else if (!/\.d\.(m|c)?ts$/.test(urlWithoutTrailingSlash)) { attempts.push(`${urlWithoutTrailingSlash}.d.ts`) + attempts.push(`${urlWithoutTrailingSlash}.d.mts`) + attempts.push(`${urlWithoutTrailingSlash}.d.cts`) attempts.push(`${urlWithoutTrailingSlash}/index.d.ts`) + attempts.push(`${urlWithoutTrailingSlash}/index.d.mts`) + attempts.push(`${urlWithoutTrailingSlash}/index.d.cts`) } else { attempts.push(urlWithoutTrailingSlash) } @@ -82,14 +90,34 @@ async function loadTypesInBackground(packageName: string, monaco: Monaco): Promi const typePathsToFetch = new Set() const hasExports = typeof packageJson.exports === 'object' && packageJson.exports !== null - console.log(`[Type Fetcher DBG] 'hasExports' field detected: ${hasExports}`) + console.log(`[Type Fetcher DBG] 'exports' field detected: ${hasExports}`) if (hasExports) { for (const key in packageJson.exports) { + if (key.includes('*') || key.endsWith('package.json')) continue + const entry = packageJson.exports[key] - if (typeof entry === 'object' && entry !== null && typeof entry.types === 'string') { - console.log(`[Type Fetcher DBG] Found types in exports['${key}']: ${entry.types}`) - typePathsToFetch.add(entry.types) + + let typePath: string | null = null + + if (typeof entry === 'string') { + if (!entry.endsWith('.json')) { + typePath = entry.replace(/\.(m|c)?js$/, '.d.ts') + } + } else if (typeof entry === 'object' && entry !== null) { + if (typeof entry.types === 'string') { + typePath = entry.types + } + else if (typeof entry.import === 'string') { + typePath = entry.import.replace(/\.(m|c)?js$/, '.d.ts') + } else if (typeof entry.default === 'string') { + typePath = entry.default.replace(/\.(m|c)?js$/, '.d.ts') + } + } + + if (typePath) { + console.log(`[Type Fetcher DBG] Found type path for exports['${key}']: ${typePath}`) + typePathsToFetch.add(typePath) } } } @@ -102,7 +130,15 @@ async function loadTypesInBackground(packageName: string, monaco: Monaco): Promi console.log(`[Type Fetcher DBG] Total type paths found: ${typePathsToFetch.size}`) if (typePathsToFetch.size === 0) { - throw new NoTypesError(`No type definition entry found in package.json.`) + const mainField = packageJson.main + if (typeof mainField === 'string') { + console.log(`[Type Fetcher DBG] Inferring from 'main' field: ${mainField}`) + typePathsToFetch.add(mainField.replace(/\.(m|c)?js$/, '.d.ts')) + } + + if (typePathsToFetch.size === 0) { + throw new NoTypesError(`No type definition entry found in package.json.`) + } } let mainVirtualPath = '' @@ -127,7 +163,15 @@ async function loadTypesInBackground(packageName: string, monaco: Monaco): Promi if (packageJson.dependencies) { console.log(`[Type Fetcher] Found ${Object.keys(packageJson.dependencies).length} dependencies for ${packageName}. Fetching them...`) - Object.keys(packageJson.dependencies).forEach(dep => startTypeLoadingProcess(dep, monaco)) + const depPromises = Object.keys(packageJson.dependencies).map(dep => { + try { + return startTypeLoadingProcess(dep, monaco) + } catch(e) { + console.warn(`[Type Fetcher] Failed to start loading types for dependency: ${dep}`, e.message) + return Promise.resolve() + } + }) + await Promise.all(depPromises) } return { virtualPath: mainVirtualPath, hasExports }