From 0c838ee3f2101256b6a99a8041b9e723fa7e001f Mon Sep 17 00:00:00 2001 From: ci-bot Date: Thu, 2 Oct 2025 14:49:19 +0900 Subject: [PATCH 01/20] 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 c862e299277..36a8c8422b1 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 e2f4cbe8ec6..153b51c6cae 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 061338a79f6..d659b36ca9f 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 8806b2d316d2ec6036ad67b02ec26fc10e54c957 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Tue, 7 Oct 2025 15:47:35 +0900 Subject: [PATCH 02/20] 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 1e726b2d9bdf188a7ca00c85205b674d79a8d154 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Tue, 7 Oct 2025 17:13:03 +0900 Subject: [PATCH 03/20] 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 d384c142ab8774ea25a0728a47f66ae4c46fa244 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Tue, 7 Oct 2025 17:50:45 +0900 Subject: [PATCH 04/20] 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 } From 4917605f1e1a1fb23c3483f9ba0ef4d8120e7572 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Wed, 8 Oct 2025 15:38:54 +0900 Subject: [PATCH 05/20] temp: enable dynamic import and autocompletion for ethers and viem libraries --- apps/remix-ide/src/app/editor/editor.js | 109 ++++---- apps/remix-ide/src/app/editor/type-fetcher.ts | 253 +++++++----------- 2 files changed, 152 insertions(+), 210 deletions(-) diff --git a/apps/remix-ide/src/app/editor/editor.js b/apps/remix-ide/src/app/editor/editor.js index 121026d9211..b8f1ffc25b6 100644 --- a/apps/remix-ide/src/app/editor/editor.js +++ b/apps/remix-ide/src/app/editor/editor.js @@ -78,6 +78,7 @@ export default class Editor extends Plugin { this.typeLoaderDebounce = null this.tsModuleMappings = {} + this.processedPackages = new Set() } setDispatch (dispatch) { @@ -142,15 +143,21 @@ export default class Editor extends Plugin { this.activated = true this.on('editor', 'editorMounted', () => { if (!this.monaco) return - const tsDefaults = this.monaco.languages.typescript.typescriptDefaults + const ts = this.monaco.languages.typescript + const tsDefaults = ts.typescriptDefaults tsDefaults.setCompilerOptions({ - moduleResolution: this.monaco.languages.typescript.ModuleResolutionKind.NodeJs, - target: this.monaco.languages.typescript.ScriptTarget.ES2020, + moduleResolution: ts.ModuleResolutionKind.Bundler, + module: ts.ModuleKind.ESNext, + target: ts.ScriptTarget.ES2022, + lib: ['es2022', 'dom', 'dom.iterable'], allowNonTsExtensions: true, + allowSyntheticDefaultImports: true, + skipLibCheck: true, baseUrl: 'file:///node_modules/', - paths: {} + paths: this.tsModuleMappings, }) + console.log('[DIAGNOSE-SETUP] Initial CompilerOptions set.') }) this.on('sidePanel', 'focusChanged', (name) => { this.keepDecorationsFor(name, 'sourceAnnotationsPerFile') @@ -181,23 +188,32 @@ export default class Editor extends Plugin { } updateTsCompilerOptions() { - console.log('[Module Mapper] Updating TS compiler options with new paths:', this.tsModuleMappings) + if (!this.monaco) return + console.log('[DIAGNOSE-PATHS] Updating TS compiler options...') + console.log('[DIAGNOSE-PATHS] Current path mappings:', JSON.stringify(this.tsModuleMappings, null, 2)) + const tsDefaults = this.monaco.languages.typescript.typescriptDefaults - const oldOptions = tsDefaults.getCompilerOptions() + const currentOptions = 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) + tsDefaults.setCompilerOptions({ + ...currentOptions, + paths: { ...currentOptions.paths, ...this.tsModuleMappings } + }) + console.log('[DIAGNOSE-PATHS] TS compiler options updated.') + } + + addExtraLibs(libs) { + if (!this.monaco || !libs || libs.length === 0) return + console.log(`[DIAGNOSE-LIBS] Adding ${libs.length} new files to Monaco...`) - setTimeout(() => { - const allLibs = tsDefaults.getExtraLibs() - console.log('[DEBUG 4] Final check - Monaco extraLibs state:', Object.keys(allLibs).length, 'libs loaded.') - }, 2000) + const tsDefaults = this.monaco.languages.typescript.typescriptDefaults + + libs.forEach(lib => { + if (!tsDefaults.getExtraLibs()[lib.filePath]) { + tsDefaults.addExtraLib(lib.content, lib.filePath) + } + }) + console.log(`[DIAGNOSE-LIBS] Files added. Total extra libs now: ${Object.keys(tsDefaults.getExtraLibs()).length}.`) } async _onChange (file) { @@ -210,58 +226,53 @@ export default class Editor extends Plugin { const model = this.monaco.editor.getModel(this.monaco.Uri.parse(file)) if (!model) return const code = model.getValue() - - try { - const extractPackageName = (importPath) => { - if (importPath.startsWith('@')) { - const parts = importPath.split('/') - return `${parts[0]}/${parts[1]}` - } - return importPath.split('/')[0] + try { + console.log('[DIAGNOSE-ONCHANGE] Change detected, analyzing imports...') + const extractPackageName = (p) => p.startsWith('@') ? p.split('/').slice(0, 2).join('/') : p.split('/')[0] + const rawImports = [...code.matchAll(/(?:from|import)\s+['"]((?!\.).*?)['"]/g)].map(match => match[1]) + const uniquePackages = [...new Set(rawImports.map(extractPackageName))] + + const newPackages = uniquePackages.filter(p => !this.processedPackages.has(p)) + if (newPackages.length === 0) { + console.log('[DIAGNOSE-ONCHANGE] No new packages to process.') + return } - - const rawImports = [...code.matchAll(/from\s+['"]((?![./]).*?)['"]/g)].map(match => match[1]) - 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) + console.log('[DIAGNOSE-ONCHANGE] New packages to process:', newPackages) let newPathsFound = false const promises = newPackages.map(async (pkg) => { - try { - const result = await startTypeLoadingProcess(pkg, this.monaco) + this.processedPackages.add(pkg) + const result = await startTypeLoadingProcess(pkg) + + console.log(`[DIAGNOSE-ONCHANGE] Result received for "${pkg}":`, result ? { mainVirtualPath: result.mainVirtualPath, libsCount: result.libs.length } : 'null') + + if (result && result.libs && result.libs.length > 0) { + this.addExtraLibs(result.libs) - if (result && result.virtualPath) { - // this.tsModuleMappings[pkg] = [`${pkg}/`] - // this.tsModuleMappings[`${pkg}/*`] = [`${pkg}/*`] - const typeFileRelativePath = result.virtualPath.replace('file:///node_modules/', '') - - this.tsModuleMappings[pkg] = [typeFileRelativePath] + if (result.mainVirtualPath) { + this.tsModuleMappings[pkg] = [result.mainVirtualPath.replace('file:///node_modules/', '')] this.tsModuleMappings[`${pkg}/*`] = [`${pkg}/*`] - newPathsFound = true + } else { + console.warn(`[DIAGNOSE-ONCHANGE] No mainVirtualPath found for "${pkg}", path mapping will be incomplete.`) } - } catch (error) { - console.error(`[Module Mapper] Failed to process types for ${pkg}`, error) } }) - await Promise.all(promises) if (newPathsFound) { - setTimeout(() => this.updateTsCompilerOptions(), 1000) + this.updateTsCompilerOptions() + } else { + console.log('[DIAGNOSE-ONCHANGE] No new paths were mapped.') } } catch (error) { - console.error('[Type Loader] Error during type loading process:', error) + console.error('[DIAGNOSE-ONCHANGE] Error during type loading process:', error) } }, 1500) } - const currentFile = await this.call('fileManager', 'file') if (!currentFile) { return diff --git a/apps/remix-ide/src/app/editor/type-fetcher.ts b/apps/remix-ide/src/app/editor/type-fetcher.ts index 0f2e0c69fd2..1d022b6de83 100644 --- a/apps/remix-ide/src/app/editor/type-fetcher.ts +++ b/apps/remix-ide/src/app/editor/type-fetcher.ts @@ -1,184 +1,115 @@ +// type-fetcher.ts import { Monaco } from '@monaco-editor/react' -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']) - -class NoTypesError extends Error { - constructor(message: string) { - super(message) - this.name = 'NoTypesError' - } -} +type Library = { filePath: string; content: string } + +const IMPORT_RE = /from\s*['"]((?!.*\.(css|json|svg))[^'"]+)['"]/g + +async function resolveAndFetch(url: string): Promise<{ finalUrl: string; content: string }> { + const basePath = url + .replace(/\.d\.ts$/, '') + .replace(/\.ts$/, '') + .replace(/\.d\.mts$/, '') + .replace(/\.mts$/, '') + .replace(/\.d\.cts$/, '') + .replace(/\.cts$/, '') + .replace(/\.js$/, '') + .replace(/\.mjs$/, '') + .replace(/\.cjs$/, '') + + const attempts = [ + `${basePath}.d.ts`, + `${basePath}.ts`, + `${basePath}.d.mts`, + `${basePath}.mts`, + `${basePath}.d.cts`, + `${basePath}.cts`, + `${basePath}/index.d.ts`, + `${basePath}/index.ts`, + ] + + const uniqueAttempts = [...new Set(attempts)] + console.log(`[DIAGNOSE-RESOLVER] Attempting to resolve: ${url}. Trying:`, uniqueAttempts) -function getTypesPackageName(packageName: string): string { - if (packageName.startsWith('@')) { - const mangledName = packageName.substring(1).replace('/', '__'); - return `@types/${mangledName}` + for (const attemptUrl of uniqueAttempts) { + try { + const response = await fetch(attemptUrl) + if (response.ok) { + console.log(`[DIAGNOSE-RESOLVER] ✅ Success for ${url} at ${attemptUrl}`) + return { finalUrl: attemptUrl, content: await response.text() } + } + } catch (e) {} } - return `@types/${packageName}` + throw new Error(`Could not resolve type definition for ${url}`) } -export async function startTypeLoadingProcess(packageName: string, monaco: Monaco): Promise<{ virtualPath: string; hasExports: boolean } | void> { - if (NODE_BUILTINS.has(packageName)) { - packageName = '@types/node' +async function crawl( + entryUrl: string, + packageRootUrl: string, + depth: number, + maxDepth: number, + visited: Set +): Promise { + if (depth >= maxDepth || visited.has(entryUrl)) { + return [] } - - if (processedPackages.has(packageName)) return - processedPackages.add(packageName) + visited.add(entryUrl) - console.log(`[Type Fetcher] Starting type loading process for "${packageName}"...`) + const collectedLibs: Library[] = [] try { - return await loadTypesInBackground(packageName, monaco) - } catch (error) { - 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 { - console.error(`[Type Fetcher] Loading process failed for "${packageName}" and will not fallback to @types. Error:`, error.message) + const { finalUrl, content } = await resolveAndFetch(entryUrl) + const virtualPath = finalUrl.replace('https://cdn.jsdelivr.net/npm/', 'file:///node_modules/') + collectedLibs.push({ filePath: virtualPath, content }) + + const subPromises: Promise[] = [] + for (const match of content.matchAll(IMPORT_RE)) { + const importPath = match[1] + if (!importPath.startsWith('.')) continue + const nextUrl = new URL(importPath, finalUrl).href + subPromises.push(crawl(nextUrl, packageRootUrl, depth + 1, maxDepth, visited)) } - } -} - -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')) - 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) - } - for (const url of attempts) { - try { - const response = await fetch(url) - if (response.ok) { - return { finalUrl: url, content: await response.text() } - } - } catch (e) {} + const results = await Promise.all(subPromises) + results.forEach(libs => collectedLibs.push(...libs)) + } catch (e) { + console.warn(`[Crawler] Could not fetch/process ${entryUrl}, but continuing...`) } - throw new Error(`Could not resolve DTS file for ${resolvedUrl}`) + + return collectedLibs } -async function loadTypesInBackground(packageName: string, monaco: Monaco): Promise<{ virtualPath: string; hasExports: boolean } | void> { +export async function startTypeLoadingProcess(packageName: string): Promise<{ mainVirtualPath: string; libs: Library[] } | void> { + console.log(`[Type Loader] Starting JSDELIVR Limited Depth Crawl for "${packageName}"...`) 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) - - const typePathsToFetch = new Set() - - const hasExports = typeof packageJson.exports === 'object' && packageJson.exports !== null - 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] - - 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) - } + try { + const packageJsonUrl = new URL('package.json', baseUrl).href + const response = await fetch(packageJsonUrl) + if (!response.ok) throw new Error(`Failed to fetch package.json for "${packageName}"`) + + const packageJson = await response.json() + const allCollectedLibs: Library[] = [{ + filePath: `file:///node_modules/${packageName}/package.json`, + content: JSON.stringify(packageJson, null, 2), + }] + + let mainTypePath = packageJson.types || packageJson.typings + if (!mainTypePath && typeof packageJson.exports === 'object' && packageJson.exports?.['.']?.types) { + mainTypePath = packageJson.exports['.'].types } - } + mainTypePath = mainTypePath || 'index.d.ts' - const mainTypePath = packageJson.types || packageJson.typings - console.log(`[Type Fetcher DBG] Top-level 'types' field: ${mainTypePath}`) - if (typeof mainTypePath === 'string') { - typePathsToFetch.add(mainTypePath) - } - - console.log(`[Type Fetcher DBG] Total type paths found: ${typePathsToFetch.size}`) - if (typePathsToFetch.size === 0) { - 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.`) - } - } + const mainEntryUrl = new URL(mainTypePath, baseUrl).href + const visited = new Set() + const libsFromCrawl = await crawl(mainEntryUrl, baseUrl, 0, 6, visited) + allCollectedLibs.push(...libsFromCrawl) - 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.') + const mainVirtualPath = libsFromCrawl.length > 0 ? libsFromCrawl[0].filePath : '' + console.log(`[Type Loader] Finished Crawl for "${packageName}". Total files collected: ${allCollectedLibs.length}.`) - 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...`) - 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 { mainVirtualPath, libs: allCollectedLibs } + } catch (error) { + console.error(`[Type Loader] Failed to load types for "${packageName}":`, error.message) } - - return { virtualPath: mainVirtualPath, hasExports } -} - -function addLibToMonaco(filePath: string, content: string, monaco: Monaco) { - if (loadedLibs.has(filePath)) return - monaco.languages.typescript.typescriptDefaults.addExtraLib(content, filePath) - loadedLibs.add(filePath) } \ No newline at end of file From b97863293fc431940f97d8b6f1e5c4c608d84625 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Thu, 9 Oct 2025 10:29:11 +0900 Subject: [PATCH 06/20] temp: version 0.1 - viem/chains underline --- apps/remix-ide/src/app/editor/editor.js | 168 +++++++-- apps/remix-ide/src/app/editor/type-fetcher.ts | 338 ++++++++++++++---- 2 files changed, 412 insertions(+), 94 deletions(-) diff --git a/apps/remix-ide/src/app/editor/editor.js b/apps/remix-ide/src/app/editor/editor.js index b8f1ffc25b6..cebb74f3257 100644 --- a/apps/remix-ide/src/app/editor/editor.js +++ b/apps/remix-ide/src/app/editor/editor.js @@ -79,6 +79,10 @@ export default class Editor extends Plugin { this.tsModuleMappings = {} this.processedPackages = new Set() + + this.typesLoadingCount = 0 + this.shimDisposers = new Map() + this.pendingPackagesBatch = new Set() } setDispatch (dispatch) { @@ -147,8 +151,8 @@ export default class Editor extends Plugin { const tsDefaults = ts.typescriptDefaults tsDefaults.setCompilerOptions({ - moduleResolution: ts.ModuleResolutionKind.Bundler, - module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.NodeNext, + module: ts.ModuleKind.NodeNext, target: ts.ScriptTarget.ES2022, lib: ['es2022', 'dom', 'dom.iterable'], allowNonTsExtensions: true, @@ -157,7 +161,9 @@ export default class Editor extends Plugin { baseUrl: 'file:///node_modules/', paths: this.tsModuleMappings, }) - console.log('[DIAGNOSE-SETUP] Initial CompilerOptions set.') + tsDefaults.setDiagnosticsOptions({ noSemanticValidation: false, noSyntaxValidation: false }) + ts.typescriptDefaults.setEagerModelSync(true) + console.log('[DIAGNOSE-SETUP] CompilerOptions set to NodeNext and diagnostics enabled') }) this.on('sidePanel', 'focusChanged', (name) => { this.keepDecorationsFor(name, 'sourceAnnotationsPerFile') @@ -202,6 +208,67 @@ export default class Editor extends Plugin { console.log('[DIAGNOSE-PATHS] TS compiler options updated.') } + toggleTsDiagnostics(enable) { + if (!this.monaco) return + const ts = this.monaco.languages.typescript + ts.typescriptDefaults.setDiagnosticsOptions({ + noSemanticValidation: !enable, + noSyntaxValidation: false + }) + console.log(`[DIAGNOSE-DIAG] Semantic diagnostics ${enable ? 'enabled' : 'disabled'}`) + } + + addShimForPackage(pkg) { + if (!this.monaco) return + const tsDefaults = this.monaco.languages.typescript.typescriptDefaults + + const shimMainPath = `file:///__shims__/${pkg}.d.ts` + const shimWildPath = `file:///__shims__/${pkg}__wildcard.d.ts` + + if (!this.shimDisposers.has(shimMainPath)) { + const d1 = tsDefaults.addExtraLib(`declare module '${pkg}' { const _default: any\nexport = _default }`, shimMainPath) + this.shimDisposers.set(shimMainPath, d1) + } + + if (!this.shimDisposers.has(shimWildPath)) { + const d2 = tsDefaults.addExtraLib(`declare module '${pkg}/*' { const _default: any\nexport = _default }`, shimWildPath) + this.shimDisposers.set(shimWildPath, d2) + } + + this.tsModuleMappings[pkg] = [shimMainPath.replace('file:///', '')] + this.tsModuleMappings[`${pkg}/*`] = [`${pkg}/*`] + } + + removeShimsForPackage(pkg) { + const keys = [`file:///__shims__/${pkg}.d.ts`, `file:///__shims__/${pkg}__wildcard.d.ts`] + for (const k of keys) { + const disp = this.shimDisposers.get(k) + if (disp && typeof disp.dispose === 'function') { + disp.dispose() + this.shimDisposers.delete(k) + } + } + } + + beginTypesBatch() { + if (this.typesLoadingCount === 0) { + this.toggleTsDiagnostics(false) + this.triggerEvent('typesLoading', ['start']) + console.log('[DIAGNOSE-BATCH] Types batch started') + } + this.typesLoadingCount++ + } + + endTypesBatch() { + this.typesLoadingCount = Math.max(0, this.typesLoadingCount - 1) + if (this.typesLoadingCount === 0) { + this.updateTsCompilerOptions() + this.toggleTsDiagnostics(true) + this.triggerEvent('typesLoading', ['end']) + console.log('[DIAGNOSE-BATCH] Types batch ended') + } + } + addExtraLibs(libs) { if (!this.monaco || !libs || libs.length === 0) return console.log(`[DIAGNOSE-LIBS] Adding ${libs.length} new files to Monaco...`) @@ -230,7 +297,11 @@ export default class Editor extends Plugin { try { console.log('[DIAGNOSE-ONCHANGE] Change detected, analyzing imports...') const extractPackageName = (p) => p.startsWith('@') ? p.split('/').slice(0, 2).join('/') : p.split('/')[0] - const rawImports = [...code.matchAll(/(?:from|import)\s+['"]((?!\.).*?)['"]/g)].map(match => match[1]) + const IMPORT_ANY_RE = + /(?:import|export)\s+[^'"]*?from\s*['"]([^'"]+)['"]|import\s*['"]([^'"]+)['"]|require\(\s*['"]([^'"]+)['"]\s*\)/g + const rawImports = [...code.matchAll(IMPORT_ANY_RE)] + .map(m => (m[1] || m[2] || m[3] || '').trim()) + .filter(Boolean) const uniquePackages = [...new Set(rawImports.map(extractPackageName))] const newPackages = uniquePackages.filter(p => !this.processedPackages.has(p)) @@ -241,33 +312,84 @@ export default class Editor extends Plugin { console.log('[DIAGNOSE-ONCHANGE] New packages to process:', newPackages) + for (const pkg of newPackages) { + this.addShimForPackage(pkg) + } + this.updateTsCompilerOptions() + + this.beginTypesBatch() + let newPathsFound = false - const promises = newPackages.map(async (pkg) => { - this.processedPackages.add(pkg) - const result = await startTypeLoadingProcess(pkg) - - console.log(`[DIAGNOSE-ONCHANGE] Result received for "${pkg}":`, result ? { mainVirtualPath: result.mainVirtualPath, libsCount: result.libs.length } : 'null') - - if (result && result.libs && result.libs.length > 0) { - this.addExtraLibs(result.libs) - - if (result.mainVirtualPath) { - this.tsModuleMappings[pkg] = [result.mainVirtualPath.replace('file:///node_modules/', '')] + await Promise.all(newPackages.map(async (pkg) => { + try { + this.processedPackages.add(pkg) + + const result = await startTypeLoadingProcess(pkg) + console.log(`[DIAGNOSE-ONCHANGE] Result received for "${pkg}":`, result ? { mainVirtualPath: result.mainVirtualPath, libsCount: result.libs.length } : 'null') + + if (result && result.libs && result.libs.length > 0) { + this.addExtraLibs(result.libs) + + function cleanupBadPathKeys(paths, pkg) { + for (const k of Object.keys(paths)) { + const badDot = k.startsWith(`${pkg}.`) + const noSlash = k.startsWith(pkg) && !k.includes('/') && k !== pkg + if (badDot || noSlash) delete paths[k] + } + } + + cleanupBadPathKeys(this.tsModuleMappings, pkg) + + if (result.subpathMap) { + if (result.subpathMap) { + for (const [subpath, virtualPath] of Object.entries(result.subpathMap)) { + this.tsModuleMappings[subpath] = [virtualPath] + } + } + } + + if (result.mainVirtualPath) { + this.tsModuleMappings[pkg] = [result.mainVirtualPath.replace('file:///node_modules/', '')] + } else { + console.warn(`[DIAGNOSE-ONCHANGE] No mainVirtualPath found for "${pkg}", path mapping may be incomplete`) + } + this.tsModuleMappings[`${pkg}/*`] = [`${pkg}/*`] + + const libsForPkg = result.libs.filter(l => l.filePath.includes(`/node_modules/${pkg}/`)) + for (const lib of libsForPkg) { + const asPath = lib.filePath.replace('file:///node_modules/', '') + if (asPath.endsWith('/index.d.ts')) { + const dirSpec = asPath.replace('/index.d.ts', '') + if (dirSpec.startsWith(`${pkg}/`)) { + this.tsModuleMappings[dirSpec] = [asPath] + } + } + if (asPath.endsWith('.d.ts')) { + const fileSpec = asPath.replace(/\.d\.ts$/, '') + if (fileSpec.startsWith(`${pkg}/`)) { + this.tsModuleMappings[fileSpec] = [asPath] + } + } + } + + this.removeShimsForPackage(pkg) newPathsFound = true } else { - console.warn(`[DIAGNOSE-ONCHANGE] No mainVirtualPath found for "${pkg}", path mapping will be incomplete.`) + console.warn(`[DIAGNOSE-ONCHANGE] No types found for "${pkg}", keeping shim`) } + } catch (e) { + console.error('[DIAGNOSE-ONCHANGE] Type load failed for', pkg, e) } - }) - await Promise.all(promises) + })) - if (newPathsFound) { - this.updateTsCompilerOptions() - } else { - console.log('[DIAGNOSE-ONCHANGE] No new paths were mapped.') - } + if (newPathsFound) { + this.updateTsCompilerOptions() + } else { + console.log('[DIAGNOSE-ONCHANGE] No new paths were mapped.') + } + this.endTypesBatch() } catch (error) { console.error('[DIAGNOSE-ONCHANGE] 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 1d022b6de83..49d1830c84c 100644 --- a/apps/remix-ide/src/app/editor/type-fetcher.ts +++ b/apps/remix-ide/src/app/editor/type-fetcher.ts @@ -3,44 +3,152 @@ import { Monaco } from '@monaco-editor/react' type Library = { filePath: string; content: string } -const IMPORT_RE = /from\s*['"]((?!.*\.(css|json|svg))[^'"]+)['"]/g +type PackageJson = { + name?: string + version?: string + types?: string + typings?: string + main?: string + module?: string + exports?: string | Record + typesVersions?: Record> | undefined +} + +type ResolveResult = { finalUrl: string; content: string } + +const CDN_BASE = 'https://cdn.jsdelivr.net/npm/' +const VIRTUAL_BASE = 'file:///node_modules/' + +const IMPORT_ANY_RE = + /(?:import|export)\s+[^'"]*?from\s*['"]([^'"]+)['"]|import\s*['"]([^'"]+)['"]|require\(\s*['"]([^'"]+)['"]\s*\)/g + +const TRIPLE_SLASH_REF_RE = /\/\/\/\s*/g -async function resolveAndFetch(url: string): Promise<{ finalUrl: string; content: string }> { - const basePath = url +function isRelative(p: string): boolean { + return p.startsWith('./') || p.startsWith('../') || p.startsWith('/') +} + +function normalizeBareSpecifier(p: string): string { + if (!p) return p + if (p.startsWith('@')) return p.split('/').slice(0, 2).join('/') + return p.split('/')[0] +} + +function toTypesScopedName(pkg: string): string { + if (pkg.startsWith('@')) return '@types/' + pkg.slice(1).replace('/', '__') + return '@types/' + pkg +} + +function toVirtual(url: string): string { + return url.replace(CDN_BASE, VIRTUAL_BASE) +} + +function stripJsLike(url: string): string { + return url .replace(/\.d\.ts$/, '') - .replace(/\.ts$/, '') .replace(/\.d\.mts$/, '') - .replace(/\.mts$/, '') .replace(/\.d\.cts$/, '') + .replace(/\.ts$/, '') + .replace(/\.mts$/, '') .replace(/\.cts$/, '') .replace(/\.js$/, '') .replace(/\.mjs$/, '') .replace(/\.cjs$/, '') +} + +async function fetchJson(url: string): Promise { + const res = await fetch(url) + if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`) + return res.json() +} +function guessDtsFromJs(jsPath: string): string[] { + const base = stripJsLike(jsPath) const attempts = [ - `${basePath}.d.ts`, - `${basePath}.ts`, - `${basePath}.d.mts`, - `${basePath}.mts`, - `${basePath}.d.cts`, - `${basePath}.cts`, - `${basePath}/index.d.ts`, - `${basePath}/index.ts`, + `${base}.d.ts`, + `${base}.ts`, + `${base}/index.d.ts`, + `${base}/index.ts` ] - - const uniqueAttempts = [...new Set(attempts)] - console.log(`[DIAGNOSE-RESOLVER] Attempting to resolve: ${url}. Trying:`, uniqueAttempts) + return attempts +} + +type ExportTypeMap = Record + +function buildExportTypeMap(pkgName: string, pkgJson: PackageJson): ExportTypeMap { + const map: ExportTypeMap = {} + const base = `${CDN_BASE}${pkgName}/` + + const push = (subpath: string, relPath: string | undefined) => { + if (!relPath) return + const attempts = guessDtsFromJs(relPath) + map[subpath] = attempts.map(a => new URL(a, base).href) + } + + if (typeof pkgJson.exports === 'string') { + push('.', pkgJson.types || pkgJson.typings || pkgJson.exports) + } else if (pkgJson.exports && typeof pkgJson.exports === 'object') { + for (const [k, v] of Object.entries(pkgJson.exports)) { + if (typeof v === 'string') { + push(k, v) + } else if (v && typeof v === 'object') { + if (v.types) push(k, v.types) + else if (v.import) push(k, v.import) + else if (v.default) push(k, v.default) + else if (v.require) push(k, v.require) + } + } + } else { + const main = pkgJson.types || pkgJson.typings || pkgJson.module || pkgJson.main || 'index.js' + push('.', main) + } - for (const attemptUrl of uniqueAttempts) { + if (pkgJson.typesVersions && pkgJson.typesVersions['*']) { try { - const response = await fetch(attemptUrl) - if (response.ok) { - console.log(`[DIAGNOSE-RESOLVER] ✅ Success for ${url} at ${attemptUrl}`) - return { finalUrl: attemptUrl, content: await response.text() } + const rules = pkgJson.typesVersions['*'] as Record + for (const [pattern, arr] of Object.entries(rules)) { + if (!Array.isArray(arr) || arr.length === 0) continue + const sub = pattern.replace(/\/\*$/, '') + map[sub] = arr.flatMap(p => guessDtsFromJs(p).map(a => new URL(a, base).href)) } - } catch (e) {} + } catch (e) { + console.warn('[TYPE-FETCHER] Failed to parse typesVersions', e) + } } - throw new Error(`Could not resolve type definition for ${url}`) + + return map +} + +async function tryFetchOne(urls: string[]): Promise { + for (const u of [...new Set(urls)]) { + try { + const r = await fetch(u) + if (r.ok) { + const text = await r.text() + console.log('[DIAGNOSE-RESOLVER] Resolved', u) + return { finalUrl: u, content: text } + } else { + console.log('[DIAGNOSE-RESOLVER] Miss', u, 'HTTP', r.status) + } + } catch (e) { + console.log('[DIAGNOSE-RESOLVER] Error for', u) + } + } + return null +} + +async function resolveAndFetch(url: string): Promise { + const base = stripJsLike(url) + const attempts = [ + `${base}.d.ts`, + `${base}.ts`, + `${base}/index.d.ts`, + `${base}/index.ts` + ] + console.log('[DIAGNOSE-RESOLVER] Try resolve', url, 'candidates', attempts) + const hit = await tryFetchOne(attempts) + if (!hit) throw new Error(`Could not resolve type definition for ${url}`) + return hit } async function crawl( @@ -48,68 +156,156 @@ async function crawl( packageRootUrl: string, depth: number, maxDepth: number, - visited: Set + visited: Set, + enqueuePackage: (name: string) => void ): Promise { - if (depth >= maxDepth || visited.has(entryUrl)) { - return [] - } + if (depth >= maxDepth || visited.has(entryUrl)) return [] visited.add(entryUrl) - const collectedLibs: Library[] = [] - + const out: Library[] = [] try { const { finalUrl, content } = await resolveAndFetch(entryUrl) - const virtualPath = finalUrl.replace('https://cdn.jsdelivr.net/npm/', 'file:///node_modules/') - collectedLibs.push({ filePath: virtualPath, content }) - - const subPromises: Promise[] = [] - for (const match of content.matchAll(IMPORT_RE)) { - const importPath = match[1] - if (!importPath.startsWith('.')) continue - const nextUrl = new URL(importPath, finalUrl).href - subPromises.push(crawl(nextUrl, packageRootUrl, depth + 1, maxDepth, visited)) + const virtualPath = toVirtual(finalUrl) + out.push({ filePath: virtualPath, content }) + + const sub: Promise[] = [] + + for (const m of content.matchAll(TRIPLE_SLASH_REF_RE)) { + const rel = m[1] + const nextUrl = new URL(rel, finalUrl).href + if (!visited.has(nextUrl)) sub.push(crawl(nextUrl, packageRootUrl, depth + 1, maxDepth, visited, enqueuePackage)) } - const results = await Promise.all(subPromises) - results.forEach(libs => collectedLibs.push(...libs)) + for (const m of content.matchAll(IMPORT_ANY_RE)) { + const spec = (m[1] || m[2] || m[3] || '').trim() + if (!spec) continue + if (isRelative(spec)) { + const nextUrl = new URL(spec, finalUrl).href + if (!visited.has(nextUrl)) sub.push(crawl(nextUrl, packageRootUrl, depth + 1, maxDepth, visited, enqueuePackage)) + } else { + const bare = normalizeBareSpecifier(spec) + if (bare) { + enqueuePackage(bare) + console.log('[DIAGNOSE-CRAWL] Queued dependency', bare, 'from', finalUrl) + } + } + } + + const results = await Promise.all(sub) + results.forEach(arr => out.push(...arr)) } catch (e) { - console.warn(`[Crawler] Could not fetch/process ${entryUrl}, but continuing...`) + console.warn('[Crawler] Skip', entryUrl) } - - return collectedLibs + return out } -export async function startTypeLoadingProcess(packageName: string): Promise<{ mainVirtualPath: string; libs: Library[] } | void> { - console.log(`[Type Loader] Starting JSDELIVR Limited Depth Crawl for "${packageName}"...`) - const baseUrl = `https://cdn.jsdelivr.net/npm/${packageName}/` - - try { - const packageJsonUrl = new URL('package.json', baseUrl).href - const response = await fetch(packageJsonUrl) - if (!response.ok) throw new Error(`Failed to fetch package.json for "${packageName}"`) +export async function startTypeLoadingProcess(packageName: string): Promise<{ mainVirtualPath: string; libs: Library[]; subpathMap: Record } | void> { + console.log(`[Type Loader] Start for "${packageName}" via jsDelivr`) - const packageJson = await response.json() - const allCollectedLibs: Library[] = [{ - filePath: `file:///node_modules/${packageName}/package.json`, - content: JSON.stringify(packageJson, null, 2), - }] + const visitedPackages = new Set() + const pendingBare = new Set() + const collected: Library[] = [] + const subpathMap: Record = {} - let mainTypePath = packageJson.types || packageJson.typings - if (!mainTypePath && typeof packageJson.exports === 'object' && packageJson.exports?.['.']?.types) { - mainTypePath = packageJson.exports['.'].types - } - mainTypePath = mainTypePath || 'index.d.ts' + async function loadFromPackage(pkg: string): Promise<{ ok: boolean }> { + try { + const pkgJsonUrl = new URL('package.json', `${CDN_BASE}${pkg}/`).href + const pkgJson = await fetchJson(pkgJsonUrl) + console.log('[Type Loader] package.json loaded for', pkg) - const mainEntryUrl = new URL(mainTypePath, baseUrl).href - const visited = new Set() - const libsFromCrawl = await crawl(mainEntryUrl, baseUrl, 0, 6, visited) - allCollectedLibs.push(...libsFromCrawl) + const exportMap = buildExportTypeMap(pkg, pkgJson) + + function joinPkgSubpath(pkg: string, sub: string): string { + if (!sub || sub === '.') return pkg + let s = sub + if (s.startsWith('./')) s = s.slice(2) + if (s.startsWith('/')) s = s.slice(1) + return `${pkg}/${s}` + } - const mainVirtualPath = libsFromCrawl.length > 0 ? libsFromCrawl[0].filePath : '' - console.log(`[Type Loader] Finished Crawl for "${packageName}". Total files collected: ${allCollectedLibs.length}.`) + for (const [sub, urls] of Object.entries(exportMap)) { + const hit = await tryFetchOne(urls) + if (hit) { + const virtual = toVirtual(hit.finalUrl).replace(VIRTUAL_BASE, '') + const fullKey = joinPkgSubpath(pkg, sub) // ← 여기 + subpathMap[fullKey] = virtual + console.log('[Type Loader] subpath map', fullKey, '->', virtual) + } + } + + + const allEntryUrls: string[] = [] + const mainEntries = exportMap['.'] || [] + if (mainEntries.length > 0) { + allEntryUrls.push(...mainEntries) + } else if (pkgJson.types || pkgJson.typings) { + allEntryUrls.push(new URL(pkgJson.types || pkgJson.typings, `${CDN_BASE}${pkg}/`).href) + } else { + allEntryUrls.push(new URL('index.d.ts', `${CDN_BASE}${pkg}/`).href) + } - return { mainVirtualPath, libs: allCollectedLibs } - } catch (error) { - console.error(`[Type Loader] Failed to load types for "${packageName}":`, error.message) + const localVisited = new Set() + const enqueuePackage = (p: string) => { + const bare = normalizeBareSpecifier(p) + if (!bare) return + if (bare === pkg) return + if (!visitedPackages.has(bare)) { + pendingBare.add(bare) + } + } + + for (const entry of allEntryUrls) { + const libs = await crawl(entry, `${CDN_BASE}${pkg}/`, 0, 8, localVisited, enqueuePackage) + collected.push(...libs) + } + + return { ok: true } + } catch (e) { + console.warn('[Type Loader] No types in package', pkg, 'try @types fallback') + try { + const typesName = toTypesScopedName(pkg) + if (visitedPackages.has(typesName)) return { ok: false } + visitedPackages.add(typesName) + return await loadFromPackage(typesName) + } catch (ee) { + console.warn('[Type Loader] @types fallback failed for', pkg) + return { ok: false } + } + } + } + + visitedPackages.add(packageName) + const first = await loadFromPackage(packageName) + if (!first.ok) { + console.error('[Type Loader] Failed for', packageName) + return + } + + while (pendingBare.size) { + const next = Array.from(pendingBare) + pendingBare.clear() + for (const dep of next) { + if (visitedPackages.has(dep)) continue + visitedPackages.add(dep) + await loadFromPackage(dep) + } } -} \ No newline at end of file + + const mainVirtualPath = + collected.find(f => f.filePath.includes(`${VIRTUAL_BASE}${packageName}/`))?.filePath || + collected[0]?.filePath || + '' + + console.log(`[Type Loader] Done for "${packageName}" files=${collected.length}`) + + try { + const pkgJsonUrl = new URL('package.json', `${CDN_BASE}${packageName}/`).href + const pkgJson = await fetchJson(pkgJsonUrl) + collected.unshift({ + filePath: `file:///node_modules/${packageName}/package.json`, + content: JSON.stringify(pkgJson, null, 2) + }) + } catch {} + + return { mainVirtualPath, libs: collected, subpathMap } +} From 1384e8762e3e3c1c8b02c5f5b5da9aff477156b4 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Thu, 9 Oct 2025 12:22:51 +0900 Subject: [PATCH 07/20] temp: viem, ethers, lodash, axios --- apps/remix-ide/src/app/editor/editor.js | 131 +++----- apps/remix-ide/src/app/editor/type-fetcher.ts | 280 +++++------------- 2 files changed, 115 insertions(+), 296 deletions(-) diff --git a/apps/remix-ide/src/app/editor/editor.js b/apps/remix-ide/src/app/editor/editor.js index cebb74f3257..2cadf80d31c 100644 --- a/apps/remix-ide/src/app/editor/editor.js +++ b/apps/remix-ide/src/app/editor/editor.js @@ -295,125 +295,82 @@ export default class Editor extends Plugin { const code = model.getValue() try { - console.log('[DIAGNOSE-ONCHANGE] Change detected, analyzing imports...') - const extractPackageName = (p) => p.startsWith('@') ? p.split('/').slice(0, 2).join('/') : p.split('/')[0] const IMPORT_ANY_RE = /(?:import|export)\s+[^'"]*?from\s*['"]([^'"]+)['"]|import\s*['"]([^'"]+)['"]|require\(\s*['"]([^'"]+)['"]\s*\)/g + const rawImports = [...code.matchAll(IMPORT_ANY_RE)] .map(m => (m[1] || m[2] || m[3] || '').trim()) - .filter(Boolean) - const uniquePackages = [...new Set(rawImports.map(extractPackageName))] + .filter(p => p && !p.startsWith('.') && !p.startsWith('file://')) + + const uniqueImports = [...new Set(rawImports)] + const getBasePackage = (p) => p.startsWith('@') ? p.split('/').slice(0, 2).join('/') : p.split('/')[0] - const newPackages = uniquePackages.filter(p => !this.processedPackages.has(p)) - if (newPackages.length === 0) { - console.log('[DIAGNOSE-ONCHANGE] No new packages to process.') - return - } + const newBasePackages = [...new Set(uniqueImports.map(getBasePackage))] + .filter(p => !this.processedPackages.has(p)) + + if (newBasePackages.length === 0) return + + console.log('[DIAGNOSE] New base packages for analysis:', newBasePackages) - console.log('[DIAGNOSE-ONCHANGE] New packages to process:', newPackages) + this.beginTypesBatch() - for (const pkg of newPackages) { - this.addShimForPackage(pkg) - } + uniqueImports.forEach(pkgImport => { + this.addShimForPackage(pkgImport) + }) + this.updateTsCompilerOptions() + console.log('[DIAGNOSE] Shims added. Red lines should disappear.') - this.beginTypesBatch() - - let newPathsFound = false - await Promise.all(newPackages.map(async (pkg) => { + await Promise.all(newBasePackages.map(async (basePackage) => { + this.processedPackages.add(basePackage) + + console.log(`[DIAGNOSE-DEEP-PASS] Starting deep pass for "${basePackage}"`) try { - this.processedPackages.add(pkg) - - const result = await startTypeLoadingProcess(pkg) - console.log(`[DIAGNOSE-ONCHANGE] Result received for "${pkg}":`, result ? { mainVirtualPath: result.mainVirtualPath, libsCount: result.libs.length } : 'null') - + const result = await startTypeLoadingProcess(basePackage) if (result && result.libs && result.libs.length > 0) { + console.log(`[DIAGNOSE-DEEP-PASS] "${basePackage}" deep pass complete. Adding ${result.libs.length} files.`) this.addExtraLibs(result.libs) - - function cleanupBadPathKeys(paths, pkg) { - for (const k of Object.keys(paths)) { - const badDot = k.startsWith(`${pkg}.`) - const noSlash = k.startsWith(pkg) && !k.includes('/') && k !== pkg - if (badDot || noSlash) delete paths[k] - } - } - - cleanupBadPathKeys(this.tsModuleMappings, pkg) - + if (result.subpathMap) { - if (result.subpathMap) { - for (const [subpath, virtualPath] of Object.entries(result.subpathMap)) { - this.tsModuleMappings[subpath] = [virtualPath] - } + for (const [subpath, virtualPath] of Object.entries(result.subpathMap)) { + this.tsModuleMappings[subpath] = [virtualPath] } } - if (result.mainVirtualPath) { - this.tsModuleMappings[pkg] = [result.mainVirtualPath.replace('file:///node_modules/', '')] - } else { - console.warn(`[DIAGNOSE-ONCHANGE] No mainVirtualPath found for "${pkg}", path mapping may be incomplete`) + this.tsModuleMappings[basePackage] = [result.mainVirtualPath.replace('file:///node_modules/', '')] } + this.tsModuleMappings[`${basePackage}/*`] = [`${basePackage}/*`] + + uniqueImports + .filter(p => getBasePackage(p) === basePackage) + .forEach(p => this.removeShimsForPackage(p)) - this.tsModuleMappings[`${pkg}/*`] = [`${pkg}/*`] - - const libsForPkg = result.libs.filter(l => l.filePath.includes(`/node_modules/${pkg}/`)) - for (const lib of libsForPkg) { - const asPath = lib.filePath.replace('file:///node_modules/', '') - if (asPath.endsWith('/index.d.ts')) { - const dirSpec = asPath.replace('/index.d.ts', '') - if (dirSpec.startsWith(`${pkg}/`)) { - this.tsModuleMappings[dirSpec] = [asPath] - } - } - if (asPath.endsWith('.d.ts')) { - const fileSpec = asPath.replace(/\.d\.ts$/, '') - if (fileSpec.startsWith(`${pkg}/`)) { - this.tsModuleMappings[fileSpec] = [asPath] - } - } - } - - this.removeShimsForPackage(pkg) - newPathsFound = true } else { - console.warn(`[DIAGNOSE-ONCHANGE] No types found for "${pkg}", keeping shim`) + console.warn(`[DIAGNOSE-DEEP-PASS] No types found for "${basePackage}". Shim will remain.`) } } catch (e) { - console.error('[DIAGNOSE-ONCHANGE] Type load failed for', pkg, e) + console.error(`[DIAGNOSE-DEEP-PASS] Crawler failed for "${basePackage}":`, e) } })) + + console.log('[DIAGNOSE] All processes finished.') + this.endTypesBatch() - if (newPathsFound) { - this.updateTsCompilerOptions() - } else { - console.log('[DIAGNOSE-ONCHANGE] No new paths were mapped.') - } - - this.endTypesBatch() } catch (error) { - console.error('[DIAGNOSE-ONCHANGE] Error during type loading process:', error) + console.error('[DIAGNOSE-ONCHANGE] Critical error during type loading process:', error) + this.endTypesBatch() } }, 1500) } + const currentFile = await this.call('fileManager', 'file') - if (!currentFile) { - return - } - if (currentFile !== file) { - return - } + if (!currentFile || currentFile !== file) return + const input = this.get(currentFile) - if (!input) { - return - } - // if there's no change, don't do anything - if (input === this.previousInput) { - return - } + if (!input || input === this.previousInput) return + this.previousInput = input - // fire storage update - // NOTE: save at most once per 5 seconds if (this.saveTimeout) { window.clearTimeout(this.saveTimeout) } diff --git a/apps/remix-ide/src/app/editor/type-fetcher.ts b/apps/remix-ide/src/app/editor/type-fetcher.ts index 49d1830c84c..a06639648a2 100644 --- a/apps/remix-ide/src/app/editor/type-fetcher.ts +++ b/apps/remix-ide/src/app/editor/type-fetcher.ts @@ -1,27 +1,18 @@ -// type-fetcher.ts import { Monaco } from '@monaco-editor/react' type Library = { filePath: string; content: string } - type PackageJson = { name?: string version?: string types?: string typings?: string - main?: string - module?: string exports?: string | Record - typesVersions?: Record> | undefined } - type ResolveResult = { finalUrl: string; content: string } const CDN_BASE = 'https://cdn.jsdelivr.net/npm/' const VIRTUAL_BASE = 'file:///node_modules/' - -const IMPORT_ANY_RE = - /(?:import|export)\s+[^'"]*?from\s*['"]([^'"]+)['"]|import\s*['"]([^'"]+)['"]|require\(\s*['"]([^'"]+)['"]\s*\)/g - +const IMPORT_ANY_RE = /(?:import|export)\s+[^'"]*?from\s*['"]([^'"]+)['"]|import\s*['"]([^'"]+)['"]|require\(\s*['"]([^'"]+)['"]\s*\)/g const TRIPLE_SLASH_REF_RE = /\/\/\/\s*/g function isRelative(p: string): boolean { @@ -35,6 +26,7 @@ function normalizeBareSpecifier(p: string): string { } function toTypesScopedName(pkg: string): string { + if (pkg.startsWith('@types/')) return pkg if (pkg.startsWith('@')) return '@types/' + pkg.slice(1).replace('/', '__') return '@types/' + pkg } @@ -44,16 +36,7 @@ function toVirtual(url: string): string { } function stripJsLike(url: string): string { - return url - .replace(/\.d\.ts$/, '') - .replace(/\.d\.mts$/, '') - .replace(/\.d\.cts$/, '') - .replace(/\.ts$/, '') - .replace(/\.mts$/, '') - .replace(/\.cts$/, '') - .replace(/\.js$/, '') - .replace(/\.mjs$/, '') - .replace(/\.cjs$/, '') + return url.replace(/\.d\.[mc]?ts$/, '').replace(/\.[mc]?ts$/, '').replace(/\.[mc]?js$/, '') } async function fetchJson(url: string): Promise { @@ -64,248 +47,127 @@ async function fetchJson(url: string): Promise { function guessDtsFromJs(jsPath: string): string[] { const base = stripJsLike(jsPath) - const attempts = [ - `${base}.d.ts`, - `${base}.ts`, - `${base}/index.d.ts`, - `${base}/index.ts` - ] - return attempts + return [`${base}.d.ts`, `${base}.ts`, `${base}/index.d.ts`, `${base}/index.ts`] } -type ExportTypeMap = Record - -function buildExportTypeMap(pkgName: string, pkgJson: PackageJson): ExportTypeMap { - const map: ExportTypeMap = {} +function buildExportTypeMap(pkgName: string, pkgJson: PackageJson): Record { + const map: Record = {} const base = `${CDN_BASE}${pkgName}/` - const push = (subpath: string, relPath: string | undefined) => { if (!relPath) return - const attempts = guessDtsFromJs(relPath) - map[subpath] = attempts.map(a => new URL(a, base).href) + map[subpath] = guessDtsFromJs(relPath).map(a => new URL(a, base).href) } - if (typeof pkgJson.exports === 'string') { - push('.', pkgJson.types || pkgJson.typings || pkgJson.exports) - } else if (pkgJson.exports && typeof pkgJson.exports === 'object') { + if (pkgJson.exports && typeof pkgJson.exports === 'object') { for (const [k, v] of Object.entries(pkgJson.exports)) { - if (typeof v === 'string') { - push(k, v) - } else if (v && typeof v === 'object') { + if (typeof v === 'string') push(k, v) + else if (v && typeof v === 'object') { if (v.types) push(k, v.types) else if (v.import) push(k, v.import) else if (v.default) push(k, v.default) - else if (v.require) push(k, v.require) } } - } else { - const main = pkgJson.types || pkgJson.typings || pkgJson.module || pkgJson.main || 'index.js' - push('.', main) } - - if (pkgJson.typesVersions && pkgJson.typesVersions['*']) { - try { - const rules = pkgJson.typesVersions['*'] as Record - for (const [pattern, arr] of Object.entries(rules)) { - if (!Array.isArray(arr) || arr.length === 0) continue - const sub = pattern.replace(/\/\*$/, '') - map[sub] = arr.flatMap(p => guessDtsFromJs(p).map(a => new URL(a, base).href)) - } - } catch (e) { - console.warn('[TYPE-FETCHER] Failed to parse typesVersions', e) - } + if (!map['.'] && (pkgJson.types || pkgJson.typings)) { + push('.', pkgJson.types || pkgJson.typings) } - return map } async function tryFetchOne(urls: string[]): Promise { for (const u of [...new Set(urls)]) { try { - const r = await fetch(u) - if (r.ok) { - const text = await r.text() - console.log('[DIAGNOSE-RESOLVER] Resolved', u) - return { finalUrl: u, content: text } - } else { - console.log('[DIAGNOSE-RESOLVER] Miss', u, 'HTTP', r.status) - } - } catch (e) { - console.log('[DIAGNOSE-RESOLVER] Error for', u) - } + const r = await fetch(u); if (r.ok) return { finalUrl: u, content: await r.text() } + } catch (e) {} } return null } -async function resolveAndFetch(url: string): Promise { - const base = stripJsLike(url) - const attempts = [ - `${base}.d.ts`, - `${base}.ts`, - `${base}/index.d.ts`, - `${base}/index.ts` - ] - console.log('[DIAGNOSE-RESOLVER] Try resolve', url, 'candidates', attempts) - const hit = await tryFetchOne(attempts) - if (!hit) throw new Error(`Could not resolve type definition for ${url}`) - return hit -} - -async function crawl( - entryUrl: string, - packageRootUrl: string, - depth: number, - maxDepth: number, - visited: Set, - enqueuePackage: (name: string) => void -): Promise { - if (depth >= maxDepth || visited.has(entryUrl)) return [] +async function crawl(entryUrl: string, pkgName: string, visited: Set, enqueuePackage: (name: string) => void): Promise { + if (visited.has(entryUrl)) return [] visited.add(entryUrl) - const out: Library[] = [] try { - const { finalUrl, content } = await resolveAndFetch(entryUrl) - const virtualPath = toVirtual(finalUrl) - out.push({ filePath: virtualPath, content }) - - const sub: Promise[] = [] - - for (const m of content.matchAll(TRIPLE_SLASH_REF_RE)) { - const rel = m[1] - const nextUrl = new URL(rel, finalUrl).href - if (!visited.has(nextUrl)) sub.push(crawl(nextUrl, packageRootUrl, depth + 1, maxDepth, visited, enqueuePackage)) + const res = await tryFetchOne(guessDtsFromJs(entryUrl)) + if(!res) return [] + + const { finalUrl, content } = res + out.push({ filePath: toVirtual(finalUrl), content }) + const subPromises: Promise[] = [] + const crawlNext = (nextUrl: string) => { + if (!visited.has(nextUrl)) subPromises.push(crawl(nextUrl, pkgName, visited, enqueuePackage)) } - + for (const m of content.matchAll(TRIPLE_SLASH_REF_RE)) crawlNext(new URL(m[1], finalUrl).href) for (const m of content.matchAll(IMPORT_ANY_RE)) { const spec = (m[1] || m[2] || m[3] || '').trim() if (!spec) continue - if (isRelative(spec)) { - const nextUrl = new URL(spec, finalUrl).href - if (!visited.has(nextUrl)) sub.push(crawl(nextUrl, packageRootUrl, depth + 1, maxDepth, visited, enqueuePackage)) - } else { + if (isRelative(spec)) crawlNext(new URL(spec, finalUrl).href) + else { const bare = normalizeBareSpecifier(spec) - if (bare) { - enqueuePackage(bare) - console.log('[DIAGNOSE-CRAWL] Queued dependency', bare, 'from', finalUrl) - } + if (bare && !bare.startsWith('node:')) enqueuePackage(bare) } } - - const results = await Promise.all(sub) + const results = await Promise.all(subPromises) results.forEach(arr => out.push(...arr)) - } catch (e) { - console.warn('[Crawler] Skip', entryUrl) - } + } catch (e) {} return out } export async function startTypeLoadingProcess(packageName: string): Promise<{ mainVirtualPath: string; libs: Library[]; subpathMap: Record } | void> { - console.log(`[Type Loader] Start for "${packageName}" via jsDelivr`) - const visitedPackages = new Set() - const pendingBare = new Set() const collected: Library[] = [] const subpathMap: Record = {} - async function loadFromPackage(pkg: string): Promise<{ ok: boolean }> { + async function loadPackage(pkgNameToLoad: string) { + if (visitedPackages.has(pkgNameToLoad)) return + visitedPackages.add(pkgNameToLoad) + + let pkgJson: PackageJson try { - const pkgJsonUrl = new URL('package.json', `${CDN_BASE}${pkg}/`).href - const pkgJson = await fetchJson(pkgJsonUrl) - console.log('[Type Loader] package.json loaded for', pkg) - - const exportMap = buildExportTypeMap(pkg, pkgJson) - - function joinPkgSubpath(pkg: string, sub: string): string { - if (!sub || sub === '.') return pkg - let s = sub - if (s.startsWith('./')) s = s.slice(2) - if (s.startsWith('/')) s = s.slice(1) - return `${pkg}/${s}` - } - - for (const [sub, urls] of Object.entries(exportMap)) { - const hit = await tryFetchOne(urls) - if (hit) { - const virtual = toVirtual(hit.finalUrl).replace(VIRTUAL_BASE, '') - const fullKey = joinPkgSubpath(pkg, sub) // ← 여기 - subpathMap[fullKey] = virtual - console.log('[Type Loader] subpath map', fullKey, '->', virtual) - } - } - - - const allEntryUrls: string[] = [] - const mainEntries = exportMap['.'] || [] - if (mainEntries.length > 0) { - allEntryUrls.push(...mainEntries) - } else if (pkgJson.types || pkgJson.typings) { - allEntryUrls.push(new URL(pkgJson.types || pkgJson.typings, `${CDN_BASE}${pkg}/`).href) - } else { - allEntryUrls.push(new URL('index.d.ts', `${CDN_BASE}${pkg}/`).href) - } - - const localVisited = new Set() - const enqueuePackage = (p: string) => { - const bare = normalizeBareSpecifier(p) - if (!bare) return - if (bare === pkg) return - if (!visitedPackages.has(bare)) { - pendingBare.add(bare) - } - } + const pkgJsonUrl = new URL('package.json', `${CDN_BASE}${pkgNameToLoad}/`).href + pkgJson = await fetchJson(pkgJsonUrl) + } catch (e) { + console.log(`- Package '${pkgNameToLoad}' not found. Attempting @types fallback.`) + try { await loadPackage(toTypesScopedName(pkgNameToLoad)) } catch (ee) {} + return + } - for (const entry of allEntryUrls) { - const libs = await crawl(entry, `${CDN_BASE}${pkg}/`, 0, 8, localVisited, enqueuePackage) - collected.push(...libs) - } + const exportMap = buildExportTypeMap(pkgNameToLoad, pkgJson) - return { ok: true } - } catch (e) { - console.warn('[Type Loader] No types in package', pkg, 'try @types fallback') - try { - const typesName = toTypesScopedName(pkg) - if (visitedPackages.has(typesName)) return { ok: false } - visitedPackages.add(typesName) - return await loadFromPackage(typesName) - } catch (ee) { - console.warn('[Type Loader] @types fallback failed for', pkg) - return { ok: false } + if (Object.keys(exportMap).length === 0) { + console.log(`- No type declarations in '${pkgNameToLoad}'. Attempting @types fallback.`) + try { await loadPackage(toTypesScopedName(pkgNameToLoad)) } catch (ee) {} + return + } + + console.log(`[LOG 1] Starting full analysis for package: '${pkgNameToLoad}'`) + const pendingDependencies = new Set() + const enqueuePackage = (p: string) => { if (!visitedPackages.has(p)) pendingDependencies.add(p) } + + const crawlPromises: Promise[] = [] + for (const [subpath, urls] of Object.entries(exportMap)) { + const entryPointUrl = urls[0] + if (entryPointUrl) { + const virtualPathKey = subpath === '.' ? pkgNameToLoad : `${pkgNameToLoad}/${subpath.replace('./', '')}` + subpathMap[virtualPathKey] = entryPointUrl.replace(CDN_BASE, '') + crawlPromises.push(crawl(entryPointUrl, pkgNameToLoad, new Set(), enqueuePackage)) } } - } - visitedPackages.add(packageName) - const first = await loadFromPackage(packageName) - if (!first.ok) { - console.error('[Type Loader] Failed for', packageName) - return - } - - while (pendingBare.size) { - const next = Array.from(pendingBare) - pendingBare.clear() - for (const dep of next) { - if (visitedPackages.has(dep)) continue - visitedPackages.add(dep) - await loadFromPackage(dep) + const libsArrays = await Promise.all(crawlPromises) + libsArrays.forEach(libs => collected.push(...libs)) + + if (pendingDependencies.size > 0) { + console.log(`- Found dependencies for '${pkgNameToLoad}': ${Array.from(pendingDependencies).join(', ')}`) + await Promise.all(Array.from(pendingDependencies).map(loadPackage)) } } - const mainVirtualPath = - collected.find(f => f.filePath.includes(`${VIRTUAL_BASE}${packageName}/`))?.filePath || - collected[0]?.filePath || - '' + await loadPackage(packageName) - console.log(`[Type Loader] Done for "${packageName}" files=${collected.length}`) - - try { - const pkgJsonUrl = new URL('package.json', `${CDN_BASE}${packageName}/`).href - const pkgJson = await fetchJson(pkgJsonUrl) - collected.unshift({ - filePath: `file:///node_modules/${packageName}/package.json`, - content: JSON.stringify(pkgJson, null, 2) - }) - } catch {} + const mainVirtualPath = subpathMap[packageName] ? `${VIRTUAL_BASE}${subpathMap[packageName]}` : '' + const finalPackages = [...new Set(collected.map(lib => normalizeBareSpecifier(lib.filePath.replace(VIRTUAL_BASE, ''))))] + console.log(`[LOG 2] Full analysis complete. Total files: ${collected.length}. Packages loaded: ${finalPackages.join(', ')}`) return { mainVirtualPath, libs: collected, subpathMap } -} +} \ No newline at end of file From 90a6701ee21fb5f378cc9e9f9156bc355377b2b4 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Thu, 9 Oct 2025 14:14:38 +0900 Subject: [PATCH 08/20] wip: add comments for reviewer --- apps/remix-ide/src/app/editor/editor.js | 12 +++++++ apps/remix-ide/src/app/editor/type-fetcher.ts | 14 +++++++++ .../src/app/plugins/script-runner-bridge.tsx | 31 ++++++------------- package.json | 2 -- yarn.lock | 12 ------- 5 files changed, 36 insertions(+), 35 deletions(-) diff --git a/apps/remix-ide/src/app/editor/editor.js b/apps/remix-ide/src/app/editor/editor.js index 2cadf80d31c..842894399e2 100644 --- a/apps/remix-ide/src/app/editor/editor.js +++ b/apps/remix-ide/src/app/editor/editor.js @@ -283,6 +283,7 @@ export default class Editor extends Plugin { console.log(`[DIAGNOSE-LIBS] Files added. Total extra libs now: ${Object.keys(tsDefaults.getExtraLibs()).length}.`) } + // Called on every editor content change to parse import statements and trigger type loading. async _onChange (file) { this.triggerEvent('didChangeFile', [file]) @@ -312,8 +313,11 @@ export default class Editor extends Plugin { console.log('[DIAGNOSE] New base packages for analysis:', newBasePackages) + // Temporarily disable type checking during type loading to prevent error flickering. this.beginTypesBatch() + // [Phase 1: Fast Feedback] + // Add temporary type definitions (shims) first to immediately remove red underlines on import statements. uniqueImports.forEach(pkgImport => { this.addShimForPackage(pkgImport) }) @@ -321,6 +325,8 @@ export default class Editor extends Plugin { this.updateTsCompilerOptions() console.log('[DIAGNOSE] Shims added. Red lines should disappear.') + // [Phase 2: Deep Analysis] + // In the background, fetch the actual type files to enable autocompletion. await Promise.all(newBasePackages.map(async (basePackage) => { this.processedPackages.add(basePackage) @@ -329,8 +335,10 @@ export default class Editor extends Plugin { const result = await startTypeLoadingProcess(basePackage) if (result && result.libs && result.libs.length > 0) { console.log(`[DIAGNOSE-DEEP-PASS] "${basePackage}" deep pass complete. Adding ${result.libs.length} files.`) + // Add all fetched type files to Monaco. this.addExtraLibs(result.libs) + // Update path mappings so TypeScript can find the types. if (result.subpathMap) { for (const [subpath, virtualPath] of Object.entries(result.subpathMap)) { this.tsModuleMappings[subpath] = [virtualPath] @@ -341,19 +349,23 @@ export default class Editor extends Plugin { } this.tsModuleMappings[`${basePackage}/*`] = [`${basePackage}/*`] + // Remove the temporary shims now that the real types are loaded. uniqueImports .filter(p => getBasePackage(p) === basePackage) .forEach(p => this.removeShimsForPackage(p)) } else { + // Shim will remain if no types are found. console.warn(`[DIAGNOSE-DEEP-PASS] No types found for "${basePackage}". Shim will remain.`) } } catch (e) { + // Crawler can fail, but we don't want to crash the whole process. console.error(`[DIAGNOSE-DEEP-PASS] Crawler failed for "${basePackage}":`, e) } })) console.log('[DIAGNOSE] All processes finished.') + // After all type loading is complete, re-enable type checking and apply the final state. this.endTypesBatch() } 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 a06639648a2..058a5360870 100644 --- a/apps/remix-ide/src/app/editor/type-fetcher.ts +++ b/apps/remix-ide/src/app/editor/type-fetcher.ts @@ -25,6 +25,7 @@ function normalizeBareSpecifier(p: string): string { return p.split('/')[0] } +// Function to generate @types package names, includes logic to prevent infinite recursion. function toTypesScopedName(pkg: string): string { if (pkg.startsWith('@types/')) return pkg if (pkg.startsWith('@')) return '@types/' + pkg.slice(1).replace('/', '__') @@ -45,11 +46,13 @@ async function fetchJson(url: string): Promise { return res.json() } +// Guesses potential type definition file (.d.ts) paths from a JS file path. function guessDtsFromJs(jsPath: string): string[] { const base = stripJsLike(jsPath) return [`${base}.d.ts`, `${base}.ts`, `${base}/index.d.ts`, `${base}/index.ts`] } +// Analyzes the 'exports' field of package.json to create a map of subpaths to their type file URLs. function buildExportTypeMap(pkgName: string, pkgJson: PackageJson): Record { const map: Record = {} const base = `${CDN_BASE}${pkgName}/` @@ -68,6 +71,7 @@ function buildExportTypeMap(pkgName: string, pkgJson: PackageJson): Record { return null } +// A crawler that recursively follows imports/exports within a type definition file (.d.ts). async function crawl(entryUrl: string, pkgName: string, visited: Set, enqueuePackage: (name: string) => void): Promise { if (visited.has(entryUrl)) return [] visited.add(entryUrl) @@ -97,13 +102,16 @@ async function crawl(entryUrl: string, pkgName: string, visited: Set, en const crawlNext = (nextUrl: string) => { if (!visited.has(nextUrl)) subPromises.push(crawl(nextUrl, pkgName, visited, enqueuePackage)) } + // Handles triple-slash directives like '/// '. for (const m of content.matchAll(TRIPLE_SLASH_REF_RE)) crawlNext(new URL(m[1], finalUrl).href) for (const m of content.matchAll(IMPORT_ANY_RE)) { const spec = (m[1] || m[2] || m[3] || '').trim() if (!spec) continue + // Continues crawling for relative path imports, and queues up external package imports. if (isRelative(spec)) crawlNext(new URL(spec, finalUrl).href) else { const bare = normalizeBareSpecifier(spec) + // Ignores Node.js built-in modules that use the 'node:' protocol. if (bare && !bare.startsWith('node:')) enqueuePackage(bare) } } @@ -113,11 +121,13 @@ async function crawl(entryUrl: string, pkgName: string, visited: Set, en return out } +// Main function for the type loading process. Fetches type files for a package and all its dependencies. export async function startTypeLoadingProcess(packageName: string): Promise<{ mainVirtualPath: string; libs: Library[]; subpathMap: Record } | void> { const visitedPackages = new Set() const collected: Library[] = [] const subpathMap: Record = {} + // An inner function that recursively loads packages. async function loadPackage(pkgNameToLoad: string) { if (visitedPackages.has(pkgNameToLoad)) return visitedPackages.add(pkgNameToLoad) @@ -128,12 +138,14 @@ export async function startTypeLoadingProcess(packageName: string): Promise<{ ma pkgJson = await fetchJson(pkgJsonUrl) } catch (e) { console.log(`- Package '${pkgNameToLoad}' not found. Attempting @types fallback.`) + // If the package is not found, attempt to find its @types equivalent. try { await loadPackage(toTypesScopedName(pkgNameToLoad)) } catch (ee) {} return } const exportMap = buildExportTypeMap(pkgNameToLoad, pkgJson) + // If the package is found but contains no type information, attempt the @types fallback. if (Object.keys(exportMap).length === 0) { console.log(`- No type declarations in '${pkgNameToLoad}'. Attempting @types fallback.`) try { await loadPackage(toTypesScopedName(pkgNameToLoad)) } catch (ee) {} @@ -145,6 +157,7 @@ export async function startTypeLoadingProcess(packageName: string): Promise<{ ma const enqueuePackage = (p: string) => { if (!visitedPackages.has(p)) pendingDependencies.add(p) } const crawlPromises: Promise[] = [] + // Crawl all entry points of the package to gather complete type information. for (const [subpath, urls] of Object.entries(exportMap)) { const entryPointUrl = urls[0] if (entryPointUrl) { @@ -157,6 +170,7 @@ export async function startTypeLoadingProcess(packageName: string): Promise<{ ma const libsArrays = await Promise.all(crawlPromises) libsArrays.forEach(libs => collected.push(...libs)) + // Recursively load any discovered dependency packages. if (pendingDependencies.size > 0) { console.log(`- Found dependencies for '${pkgNameToLoad}': ${Array.from(pendingDependencies).join(', ')}`) await Promise.all(Array.from(pendingDependencies).map(loadPackage)) 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 55b05e168a8..361bf5533f5 100644 --- a/apps/remix-ide/src/app/plugins/script-runner-bridge.tsx +++ b/apps/remix-ide/src/app/plugins/script-runner-bridge.tsx @@ -28,34 +28,23 @@ 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`; + 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})();`; + return `${dynamicImportHelper}\n(async () => {\n try {\n${transformed}\n } catch (e) { console.error('Error executing script:', e); }\n})();` } export class ScriptRunnerBridgePlugin extends Plugin { @@ -227,12 +216,12 @@ export class ScriptRunnerBridgePlugin extends Plugin { } try { this.setIsLoading(this.activeConfig.name, true) - const transformedScript = transformScriptForRuntime(script); + const transformedScript = transformScriptForRuntime(script) - console.log('--- [ScriptRunner] Original Script ---'); - console.log(script); - console.log('--- [ScriptRunner] Transformed Script for Runtime ---'); - console.log(transformedScript); + 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) diff --git a/package.json b/package.json index 153b51c6cae..e2f4cbe8ec6 100644 --- a/package.json +++ b/package.json @@ -127,9 +127,7 @@ "@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 d659b36ca9f..061338a79f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7248,13 +7248,6 @@ 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" @@ -8725,11 +8718,6 @@ 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 71b58e7966f2d3d41436303427b1caa293a0949b Mon Sep 17 00:00:00 2001 From: ci-bot Date: Thu, 9 Oct 2025 21:45:06 +0900 Subject: [PATCH 09/20] update explaination --- apps/remix-ide/src/app/editor/editor.js | 2 +- apps/remix-ide/src/app/editor/type-fetcher.ts | 17 +++++++++++++---- .../src/app/plugins/script-runner-bridge.tsx | 8 ++++++++ .../src/lib/providers/tsCompletionProvider.ts | 7 +++++++ 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/apps/remix-ide/src/app/editor/editor.js b/apps/remix-ide/src/app/editor/editor.js index 842894399e2..078e1c2f10f 100644 --- a/apps/remix-ide/src/app/editor/editor.js +++ b/apps/remix-ide/src/app/editor/editor.js @@ -283,7 +283,7 @@ export default class Editor extends Plugin { console.log(`[DIAGNOSE-LIBS] Files added. Total extra libs now: ${Object.keys(tsDefaults.getExtraLibs()).length}.`) } - // Called on every editor content change to parse import statements and trigger type loading. + // [2/4] The conductor, called on every editor content change to parse 'import' statements and trigger the type loading process. async _onChange (file) { this.triggerEvent('didChangeFile', [file]) diff --git a/apps/remix-ide/src/app/editor/type-fetcher.ts b/apps/remix-ide/src/app/editor/type-fetcher.ts index 058a5360870..ad0bc7680ef 100644 --- a/apps/remix-ide/src/app/editor/type-fetcher.ts +++ b/apps/remix-ide/src/app/editor/type-fetcher.ts @@ -1,6 +1,9 @@ import { Monaco } from '@monaco-editor/react' +// A type representing a single type file (.d.ts) to be added to the Monaco editor, containing its virtual path and content. type Library = { filePath: string; content: string } + +// A type defining the minimum required fields from a package.json for type loading. type PackageJson = { name?: string version?: string @@ -19,6 +22,7 @@ function isRelative(p: string): boolean { return p.startsWith('./') || p.startsWith('../') || p.startsWith('/') } +// Extracts the base package name from an import path (e.g., 'viem/chains' -> 'viem'). function normalizeBareSpecifier(p: string): string { if (!p) return p if (p.startsWith('@')) return p.split('/').slice(0, 2).join('/') @@ -32,6 +36,7 @@ function toTypesScopedName(pkg: string): string { return '@types/' + pkg } +// Converts a CDN URL to a virtual file system path used by the Monaco editor. function toVirtual(url: string): string { return url.replace(CDN_BASE, VIRTUAL_BASE) } @@ -46,13 +51,15 @@ async function fetchJson(url: string): Promise { return res.json() } -// Guesses potential type definition file (.d.ts) paths from a JS file path. +// Guesses a list of potential TypeScript Definition file (.d.ts) paths from a given JS-like file path. +// For example, 'index.js' is converted to 'index.d.ts', 'index.ts', 'index/index.d.ts', etc. function guessDtsFromJs(jsPath: string): string[] { const base = stripJsLike(jsPath) return [`${base}.d.ts`, `${base}.ts`, `${base}/index.d.ts`, `${base}/index.ts`] } -// Analyzes the 'exports' field of package.json to create a map of subpaths to their type file URLs. +// Analyzes the 'exports' field of a package.json to create a map of subpath entry points to their corresponding type definition file URLs. +// This map is crucial for correctly resolving subpath imports like 'viem' vs. 'viem/chains'. function buildExportTypeMap(pkgName: string, pkgJson: PackageJson): Record { const map: Record = {} const base = `${CDN_BASE}${pkgName}/` @@ -78,6 +85,8 @@ function buildExportTypeMap(pkgName: string, pkgJson: PackageJson): Record { for (const u of [...new Set(urls)]) { try { @@ -121,13 +130,13 @@ async function crawl(entryUrl: string, pkgName: string, visited: Set, en return out } -// Main function for the type loading process. Fetches type files for a package and all its dependencies. +// [3/4] The core service that, upon request from 'editor.ts', fetches type definitions (.d.ts) for NPM packages from a CDN. export async function startTypeLoadingProcess(packageName: string): Promise<{ mainVirtualPath: string; libs: Library[]; subpathMap: Record } | void> { const visitedPackages = new Set() const collected: Library[] = [] const subpathMap: Record = {} - // An inner function that recursively loads packages. + // The core inner function that recursively loads a package and its dependencies. async function loadPackage(pkgNameToLoad: string) { if (visitedPackages.has(pkgNameToLoad)) return visitedPackages.add(pkgNameToLoad) 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 361bf5533f5..63446261fd0 100644 --- a/apps/remix-ide/src/app/plugins/script-runner-bridge.tsx +++ b/apps/remix-ide/src/app/plugins/script-runner-bridge.tsx @@ -28,22 +28,28 @@ 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 +// A helper function that transforms ESM 'import' syntax into a format executable in the browser when 'Run Script' is triggered. function transformScriptForRuntime(scriptContent: string): string { + // Injects a helper function for dynamic imports at the top of the script. const dynamicImportHelper = `const dynamicImport = (p) => new Function(\`return import('https://cdn.jsdelivr.net/npm/\${p}/+esm')\`)();\n` + // Transforms 'import { member } from "pkg"' into 'const { member } = await dynamicImport("pkg")'. let transformed = scriptContent.replace( /import\s+({[\s\S]*?})\s+from\s+['"]([^'"]+)['"]/g, 'const $1 = await dynamicImport("$2");' ) + // Transforms 'import Default from "pkg"'. transformed = transformed.replace( /import\s+([\w\d_$]+)\s+from\s+['"]([^'"]+)['"]/g, 'const $1 = (await dynamicImport("$2")).default;' ) + // Transforms 'import * as name from "pkg"'. transformed = transformed.replace( /import\s+\*\s+as\s+([\w\d_$]+)\s+from\s+['"]([^'"]+)['"]/g, 'const $1 = await dynamicImport("$2");' ) + // Wraps the entire script in an async IIFE (Immediately Invoked Function Expression) to support top-level await. return `${dynamicImportHelper}\n(async () => {\n try {\n${transformed}\n } catch (e) { console.error('Error executing script:', e); }\n})();` } @@ -216,6 +222,8 @@ export class ScriptRunnerBridgePlugin extends Plugin { } try { this.setIsLoading(this.activeConfig.name, true) + + // Transforms the script into an executable format using the function defined above. const transformedScript = transformScriptForRuntime(script) console.log('--- [ScriptRunner] Original Script ---') diff --git a/libs/remix-ui/editor/src/lib/providers/tsCompletionProvider.ts b/libs/remix-ui/editor/src/lib/providers/tsCompletionProvider.ts index 0e879da5df6..99d1f9ce1a5 100644 --- a/libs/remix-ui/editor/src/lib/providers/tsCompletionProvider.ts +++ b/libs/remix-ui/editor/src/lib/providers/tsCompletionProvider.ts @@ -7,6 +7,7 @@ interface TsCompletionInfo { }[] } +// [1/4] This class provides TypeScript/JavaScript autocompletion features to the Monaco editor. export class RemixTSCompletionProvider implements monacoTypes.languages.CompletionItemProvider { monaco: any @@ -14,8 +15,10 @@ export class RemixTSCompletionProvider implements monacoTypes.languages.Completi this.monaco = monaco } + // Defines trigger characters for autocompletion (e.g., suggesting object members after typing '.'). triggerCharacters = ['.', '"', "'", '/', '@'] + // The main function called by the Monaco editor as the user types. async provideCompletionItems(model: monacoTypes.editor.ITextModel, position: monacoTypes.Position, context: monacoTypes.languages.CompletionContext): Promise { const word = model.getWordUntilPosition(position) const range = { @@ -26,6 +29,8 @@ export class RemixTSCompletionProvider implements monacoTypes.languages.Completi } try { + // [4/4] It fetches type information loaded by the editor plugin ('editor.ts') via 'type-fetcher.ts', + // using Monaco's built-in TypeScript Worker to generate an autocompletion list. const worker = await this.monaco.languages.typescript.getTypeScriptWorker() const client = await worker(model.uri) const completions: TsCompletionInfo = await client.getCompletionsAtPosition( @@ -37,6 +42,7 @@ export class RemixTSCompletionProvider implements monacoTypes.languages.Completi return { suggestions: []} } + // Converts the suggestion list from the TypeScript Worker into a format that the Monaco editor can understand. const suggestions = completions.entries.map(entry => { return { label: entry.name, @@ -53,6 +59,7 @@ export class RemixTSCompletionProvider implements monacoTypes.languages.Completi } } + // Maps TypeScript's 'CompletionItemKind' string to Monaco's numeric Enum value. private mapTsCompletionKindToMonaco(kind: string): monacoTypes.languages.CompletionItemKind { const { CompletionItemKind } = this.monaco.languages switch (kind) { From 2596deef3ad101c2aa7ed0eabbf86feb4812c3de Mon Sep 17 00:00:00 2001 From: hsy822 Date: Tue, 14 Oct 2025 15:48:19 +0900 Subject: [PATCH 10/20] fix(script-runner): robustly transform imports for runtime execution --- apps/remix-ide/src/app/editor/editor.js | 12 ++- apps/remix-ide/src/app/editor/type-fetcher.ts | 86 ++++++++--------- .../src/app/plugins/script-runner-bridge.tsx | 92 ++++++++++++++----- 3 files changed, 122 insertions(+), 68 deletions(-) diff --git a/apps/remix-ide/src/app/editor/editor.js b/apps/remix-ide/src/app/editor/editor.js index 078e1c2f10f..7f5ca326d0d 100644 --- a/apps/remix-ide/src/app/editor/editor.js +++ b/apps/remix-ide/src/app/editor/editor.js @@ -82,9 +82,9 @@ export default class Editor extends Plugin { this.typesLoadingCount = 0 this.shimDisposers = new Map() - this.pendingPackagesBatch = new Set() } + setDispatch (dispatch) { this.dispatch = dispatch } @@ -330,9 +330,15 @@ export default class Editor extends Plugin { await Promise.all(newBasePackages.map(async (basePackage) => { this.processedPackages.add(basePackage) - console.log(`[DIAGNOSE-DEEP-PASS] Starting deep pass for "${basePackage}"`) + const activeRunnerLibs = await this.call('scriptRunnerBridge', 'getActiveRunnerLibs') + + const libInfo = activeRunnerLibs.find(lib => lib.name === basePackage) + const packageToLoad = libInfo ? `${libInfo.name}@${libInfo.version}` : basePackage + + console.log(`[DIAGNOSE] Preparing to load types for: "${packageToLoad}"`) + try { - const result = await startTypeLoadingProcess(basePackage) + const result = await startTypeLoadingProcess(packageToLoad) if (result && result.libs && result.libs.length > 0) { console.log(`[DIAGNOSE-DEEP-PASS] "${basePackage}" deep pass complete. Adding ${result.libs.length} files.`) // Add all fetched type files to Monaco. diff --git a/apps/remix-ide/src/app/editor/type-fetcher.ts b/apps/remix-ide/src/app/editor/type-fetcher.ts index ad0bc7680ef..9a5584c6f32 100644 --- a/apps/remix-ide/src/app/editor/type-fetcher.ts +++ b/apps/remix-ide/src/app/editor/type-fetcher.ts @@ -136,55 +136,55 @@ export async function startTypeLoadingProcess(packageName: string): Promise<{ ma const collected: Library[] = [] const subpathMap: Record = {} - // The core inner function that recursively loads a package and its dependencies. + // The core inner function that recursively loads a package and its dependencies. async function loadPackage(pkgNameToLoad: string) { - if (visitedPackages.has(pkgNameToLoad)) return - visitedPackages.add(pkgNameToLoad) - - let pkgJson: PackageJson - try { - const pkgJsonUrl = new URL('package.json', `${CDN_BASE}${pkgNameToLoad}/`).href - pkgJson = await fetchJson(pkgJsonUrl) - } catch (e) { - console.log(`- Package '${pkgNameToLoad}' not found. Attempting @types fallback.`) - // If the package is not found, attempt to find its @types equivalent. - try { await loadPackage(toTypesScopedName(pkgNameToLoad)) } catch (ee) {} - return - } + if (visitedPackages.has(pkgNameToLoad)) return + visitedPackages.add(pkgNameToLoad) + + let pkgJson: PackageJson + try { + const pkgJsonUrl = new URL('package.json', `${CDN_BASE}${pkgNameToLoad}/`).href + pkgJson = await fetchJson(pkgJsonUrl) + } catch (e) { + console.log(`- Package '${pkgNameToLoad}' not found. Attempting @types fallback.`) + // If the package is not found, attempt to find its @types equivalent. + try { await loadPackage(toTypesScopedName(pkgNameToLoad)) } catch (ee) {} + return + } - const exportMap = buildExportTypeMap(pkgNameToLoad, pkgJson) + const exportMap = buildExportTypeMap(pkgNameToLoad, pkgJson) - // If the package is found but contains no type information, attempt the @types fallback. - if (Object.keys(exportMap).length === 0) { - console.log(`- No type declarations in '${pkgNameToLoad}'. Attempting @types fallback.`) - try { await loadPackage(toTypesScopedName(pkgNameToLoad)) } catch (ee) {} - return - } - - console.log(`[LOG 1] Starting full analysis for package: '${pkgNameToLoad}'`) - const pendingDependencies = new Set() - const enqueuePackage = (p: string) => { if (!visitedPackages.has(p)) pendingDependencies.add(p) } - - const crawlPromises: Promise[] = [] - // Crawl all entry points of the package to gather complete type information. - for (const [subpath, urls] of Object.entries(exportMap)) { - const entryPointUrl = urls[0] - if (entryPointUrl) { - const virtualPathKey = subpath === '.' ? pkgNameToLoad : `${pkgNameToLoad}/${subpath.replace('./', '')}` - subpathMap[virtualPathKey] = entryPointUrl.replace(CDN_BASE, '') - crawlPromises.push(crawl(entryPointUrl, pkgNameToLoad, new Set(), enqueuePackage)) - } + // If the package is found but contains no type information, attempt the @types fallback. + if (Object.keys(exportMap).length === 0) { + console.log(`- No type declarations in '${pkgNameToLoad}'. Attempting @types fallback.`) + try { await loadPackage(toTypesScopedName(pkgNameToLoad)) } catch (ee) {} + return + } + + console.log(`[LOG 1] Starting full analysis for package: '${pkgNameToLoad}'`) + const pendingDependencies = new Set() + const enqueuePackage = (p: string) => { if (!visitedPackages.has(p)) pendingDependencies.add(p) } + + const crawlPromises: Promise[] = [] + // Crawl all entry points of the package to gather complete type information. + for (const [subpath, urls] of Object.entries(exportMap)) { + const entryPointUrl = urls[0] + if (entryPointUrl) { + const virtualPathKey = subpath === '.' ? pkgNameToLoad.split('@')[0] : `${pkgNameToLoad.split('@')[0]}/${subpath.replace('./', '')}` + subpathMap[virtualPathKey] = entryPointUrl.replace(CDN_BASE, '') + crawlPromises.push(crawl(entryPointUrl, pkgNameToLoad, new Set(), enqueuePackage)) } + } - const libsArrays = await Promise.all(crawlPromises) - libsArrays.forEach(libs => collected.push(...libs)) - - // Recursively load any discovered dependency packages. - if (pendingDependencies.size > 0) { - console.log(`- Found dependencies for '${pkgNameToLoad}': ${Array.from(pendingDependencies).join(', ')}`) - await Promise.all(Array.from(pendingDependencies).map(loadPackage)) - } + const libsArrays = await Promise.all(crawlPromises) + libsArrays.forEach(libs => collected.push(...libs)) + + // Recursively load any discovered dependency packages. + if (pendingDependencies.size > 0) { + console.log(`- Found dependencies for '${pkgNameToLoad}': ${Array.from(pendingDependencies).join(', ')}`) + await Promise.all(Array.from(pendingDependencies).map(loadPackage)) } +} await loadPackage(packageName) 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 63446261fd0..217d2b6f61b 100644 --- a/apps/remix-ide/src/app/plugins/script-runner-bridge.tsx +++ b/apps/remix-ide/src/app/plugins/script-runner-bridge.tsx @@ -13,7 +13,7 @@ import { ScriptRunnerUIPlugin } from '../tabs/script-runner-ui' const profile = { name: 'scriptRunnerBridge', displayName: 'Script configuration', - methods: ['execute', 'getConfigurations', 'selectScriptRunner'], + methods: ['execute', 'getConfigurations', 'selectScriptRunner', 'getActiveRunnerLibs'], events: ['log', 'info', 'warn', 'error'], icon: 'assets/img/solid-gear-circle-play.svg', description: 'Configure the dependencies for running scripts.', @@ -28,28 +28,69 @@ 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 -// A helper function that transforms ESM 'import' syntax into a format executable in the browser when 'Run Script' is triggered. -function transformScriptForRuntime(scriptContent: string): string { - // Injects a helper function for dynamic imports at the top of the script. +/** + * @description A helper function that transforms script content for runtime execution. + * It handles three types of ES module 'import' statements based on code review feedback: + * 1. Relative path imports (e.g., './utils'): Converts to `require()` to use Remix's original module system. + * 2. Pre-bundled library imports (e.g., 'ethers'): Converts to use global `window` objects to prevent version conflicts. + * 3. External/NPM package imports (e.g., 'axios'): Converts to a dynamic `import()` from a CDN. + * * @param {string} scriptContent - The original TypeScript/JavaScript content. + * @param {string[]} preBundledDeps - A list of pre-bundled dependency names. + * @returns {string} The transformed script content ready for execution. + */ +function transformScriptForRuntime(scriptContent: string, preBundledDeps: string[] = []): string { + // Helper for dynamically importing external packages from a CDN. const dynamicImportHelper = `const dynamicImport = (p) => new Function(\`return import('https://cdn.jsdelivr.net/npm/\${p}/+esm')\`)();\n` - // Transforms 'import { member } from "pkg"' into 'const { member } = await dynamicImport("pkg")'. + // Step 1: Transform 'import' statements let transformed = scriptContent.replace( - /import\s+({[\s\S]*?})\s+from\s+['"]([^'"]+)['"]/g, - 'const $1 = await dynamicImport("$2");' - ) - // Transforms 'import Default from "pkg"'. - transformed = transformed.replace( - /import\s+([\w\d_$]+)\s+from\s+['"]([^'"]+)['"]/g, - 'const $1 = (await dynamicImport("$2")).default;' - ) - // Transforms 'import * as name from "pkg"'. - transformed = transformed.replace( - /import\s+\*\s+as\s+([\w\d_$]+)\s+from\s+['"]([^'"]+)['"]/g, - 'const $1 = await dynamicImport("$2");' - ) - - // Wraps the entire script in an async IIFE (Immediately Invoked Function Expression) to support top-level await. + /import\s+(?:({[\s\S]*?})|([\w\d_$]+)|(\*\s+as\s+[\w\d_$]+))\s+from\s+['"]([^'"]+)['"]/g, + (match, namedMembers, defaultMember, namespaceMember, pkg) => { + + // Case 1: Relative path import. This was a previously working feature. + // By converting to `require()`, we let Remix's original script runner handle it. + if (pkg.startsWith('./') || pkg.startsWith('../')) { + if (namedMembers) return `const ${namedMembers} = require("${pkg}");` + if (defaultMember) return `const ${defaultMember} = require("${pkg}");` + if (namespaceMember) { + const alias = namespaceMember.split(' as ')[1] + return `const ${alias} = require("${pkg}");` + } + } + + // Case 2: Pre-bundled library import (e.g., 'ethers'). + // Uses the global `window` object to avoid version conflicts and TDZ ReferenceErrors. + if (preBundledDeps.includes(pkg)) { + const libName = pkg.split('/').pop() + const sourceObject = `window.${libName}` + if (namedMembers) return `const ${namedMembers} = ${sourceObject};` + if (defaultMember) return `const ${defaultMember} = ${sourceObject};` + if (namespaceMember) { + const alias = namespaceMember.split(' as ')[1] + return `const ${alias} = ${sourceObject};` + } + } + + // Case 3: External/NPM package import. + // This is the new dynamic import feature for user-added packages. + if (namedMembers) return `const ${namedMembers} = await dynamicImport("${pkg}");` + if (defaultMember) return `const ${defaultMember} = (await dynamicImport("${pkg}")).default;` + if (namespaceMember) { + const alias = namespaceMember.split(' as ')[1] + return `const ${alias} = await dynamicImport("${pkg}");` + } + + // Fallback for any unsupported import syntax. + return `// Unsupported import for: ${pkg}` + } + ); + + // Step 2: Remove 'export' keyword + // The script runner's execution context is not a module, so 'export' is a SyntaxError. + transformed = transformed.replace(/^export\s+/gm, '') + + // Step 3: Wrap in an async IIFE + // This enables the use of top-level 'await' for dynamic imports. return `${dynamicImportHelper}\n(async () => {\n try {\n${transformed}\n } catch (e) { console.error('Error executing script:', e); }\n})();` } @@ -86,7 +127,6 @@ export class ScriptRunnerBridgePlugin extends Plugin { await this.loadConfigurations() const ui: ScriptRunnerUIPlugin = new ScriptRunnerUIPlugin(this) this.engine.register(ui) - } setListeners() { @@ -138,6 +178,13 @@ export class ScriptRunnerBridgePlugin extends Plugin { }) } + public getActiveRunnerLibs() { + if (this.activeConfig && this.activeConfig.dependencies) { + return this.activeConfig.dependencies + } + return [] + } + public getConfigurations() { return this.configurations } @@ -224,7 +271,8 @@ export class ScriptRunnerBridgePlugin extends Plugin { this.setIsLoading(this.activeConfig.name, true) // Transforms the script into an executable format using the function defined above. - const transformedScript = transformScriptForRuntime(script) + const preBundledDeps = this.activeConfig.dependencies.map(dep => dep.name) + const transformedScript = transformScriptForRuntime(script, preBundledDeps) console.log('--- [ScriptRunner] Original Script ---') console.log(script) From 493da3ff1fe4a3786be1bb26727cba68083352c4 Mon Sep 17 00:00:00 2001 From: hsy822 Date: Wed, 15 Oct 2025 16:30:04 +0900 Subject: [PATCH 11/20] feat(editor): Implement dynamic type loading and intelligent script transformation --- apps/remix-ide/src/app/editor/type-fetcher.ts | 72 ++++++++--- .../src/app/plugins/script-runner-bridge.tsx | 120 ++++++++++-------- 2 files changed, 124 insertions(+), 68 deletions(-) diff --git a/apps/remix-ide/src/app/editor/type-fetcher.ts b/apps/remix-ide/src/app/editor/type-fetcher.ts index 9a5584c6f32..61eee9c8438 100644 --- a/apps/remix-ide/src/app/editor/type-fetcher.ts +++ b/apps/remix-ide/src/app/editor/type-fetcher.ts @@ -58,30 +58,63 @@ function guessDtsFromJs(jsPath: string): string[] { return [`${base}.d.ts`, `${base}.ts`, `${base}/index.d.ts`, `${base}/index.ts`] } -// Analyzes the 'exports' field of a package.json to create a map of subpath entry points to their corresponding type definition file URLs. -// This map is crucial for correctly resolving subpath imports like 'viem' vs. 'viem/chains'. function buildExportTypeMap(pkgName: string, pkgJson: PackageJson): Record { const map: Record = {} const base = `${CDN_BASE}${pkgName}/` const push = (subpath: string, relPath: string | undefined) => { if (!relPath) return - map[subpath] = guessDtsFromJs(relPath).map(a => new URL(a, base).href) + + // [DEBUG] Check what values are being passed to the push function. + console.log(`[DEBUG] push called with: subpath='${subpath}', relPath='${relPath}'`) + + if (/\.d\.[mc]?ts$/.test(relPath)) { + // [DEBUG] Case when using the declaration file path directly. + console.log(`[DEBUG] Direct declaration path matched for '${relPath}'. Using it as is.`) + map[subpath] = [new URL(relPath, base).href] + } else { + // [DEBUG] Case when guessing based on a JS file path. + console.log(`[DEBUG] Guessing declaration path for '${relPath}'.`) + map[subpath] = guessDtsFromJs(relPath).map(a => new URL(a, base).href) + } } - if (pkgJson.exports && typeof pkgJson.exports === 'object') { - for (const [k, v] of Object.entries(pkgJson.exports)) { - if (typeof v === 'string') push(k, v) - else if (v && typeof v === 'object') { - if (v.types) push(k, v.types) - else if (v.import) push(k, v.import) - else if (v.default) push(k, v.default) + if (pkgJson.exports) { + const exports = pkgJson.exports as Record + + // [DEBUG] Check the value of exports.types. + console.log(`[DEBUG] Checking exports.types:`, exports.types) + + if (exports.types) { + push('.', exports.types) + + // [DEBUG] Check the state of the map object after processing exports.types. + console.log(`[DEBUG] Map state after exports.types:`, JSON.stringify(map, null, 2)) + return map + } + + if (typeof exports === 'object') { + for (const [subpath, condition] of Object.entries(exports)) { + if (typeof condition === 'object' && condition !== null) { + if (condition.types) { + push(subpath, condition.types) + } else { + push(subpath, condition.import || condition.default) + } + } else if (typeof condition === 'string') { + push(subpath, condition) + } } } } - // Fallback to 'types'/'typings' field if 'exports' is missing or has no main '.' path defined. - if (!map['.'] && (pkgJson.types || pkgJson.typings)) { + + if (Object.keys(map).length === 0 && (pkgJson.types || pkgJson.typings)) { + // [DEBUG] Case when using the top-level types field. + console.log(`[DEBUG] Falling back to top-level types field:`, pkgJson.types || pkgJson.typings) push('.', pkgJson.types || pkgJson.typings) } + + // [DEBUG] Check the final state of the map object just before returning. + console.log(`[DEBUG] Final map state for '${pkgName}':`, JSON.stringify(map, null, 2)) return map } @@ -102,7 +135,12 @@ async function crawl(entryUrl: string, pkgName: string, visited: Set, en visited.add(entryUrl) const out: Library[] = [] try { - const res = await tryFetchOne(guessDtsFromJs(entryUrl)) + // Check if the entryUrl is already a type declaration file. + const urlsToTry = /\.d\.[mc]?ts$/.test(entryUrl) + ? [entryUrl] // If yes, use only that URL without guessing. + : guessDtsFromJs(entryUrl) // Otherwise, guess the paths. + + const res = await tryFetchOne(urlsToTry) if(!res) return [] const { finalUrl, content } = res @@ -126,7 +164,9 @@ async function crawl(entryUrl: string, pkgName: string, visited: Set, en } const results = await Promise.all(subPromises) results.forEach(arr => out.push(...arr)) - } catch (e) {} + } catch (e) { + // console.error(`Crawl failed for ${entryUrl}:`, e) // Optional: for deeper debugging + } return out } @@ -170,7 +210,9 @@ export async function startTypeLoadingProcess(packageName: string): Promise<{ ma for (const [subpath, urls] of Object.entries(exportMap)) { const entryPointUrl = urls[0] if (entryPointUrl) { - const virtualPathKey = subpath === '.' ? pkgNameToLoad.split('@')[0] : `${pkgNameToLoad.split('@')[0]}/${subpath.replace('./', '')}` + const pkgNameWithoutVersion = pkgNameToLoad.replace(/@[\^~]?[\d\.\w-]+$/, '') + const virtualPathKey = subpath === '.' ? pkgNameWithoutVersion : `${pkgNameWithoutVersion}/${subpath.replace('./', '')}` + subpathMap[virtualPathKey] = entryPointUrl.replace(CDN_BASE, '') crawlPromises.push(crawl(entryPointUrl, pkgNameToLoad, new Set(), enqueuePackage)) } 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 217d2b6f61b..d0fb989931b 100644 --- a/apps/remix-ide/src/app/plugins/script-runner-bridge.tsx +++ b/apps/remix-ide/src/app/plugins/script-runner-bridge.tsx @@ -30,70 +30,86 @@ const customBuildUrl = 'http://localhost:4000/build' // this will be used when t /** * @description A helper function that transforms script content for runtime execution. - * It handles three types of ES module 'import' statements based on code review feedback: - * 1. Relative path imports (e.g., './utils'): Converts to `require()` to use Remix's original module system. - * 2. Pre-bundled library imports (e.g., 'ethers'): Converts to use global `window` objects to prevent version conflicts. - * 3. External/NPM package imports (e.g., 'axios'): Converts to a dynamic `import()` from a CDN. - * * @param {string} scriptContent - The original TypeScript/JavaScript content. - * @param {string[]} preBundledDeps - A list of pre-bundled dependency names. - * @returns {string} The transformed script content ready for execution. + * It dynamically determines the transformation strategy based on whether external libraries are used. + * @param scriptContent The original script content. + * @param preBundledDeps An array of dependency objects for the active runner. + * @returns The transformed script content. */ -function transformScriptForRuntime(scriptContent: string, preBundledDeps: string[] = []): string { - // Helper for dynamically importing external packages from a CDN. - const dynamicImportHelper = `const dynamicImport = (p) => new Function(\`return import('https://cdn.jsdelivr.net/npm/\${p}/+esm')\`)();\n` +function transformScriptForRuntime(scriptContent: string, preBundledDeps: ProjectConfiguration['dependencies'] = []): string { + const depNames = preBundledDeps.map(dep => dep.name) + + // This is an exception list for pre-bundled libraries whose 'import' statements should be left as is. + const STATIC_TRANSFORM_EXCEPTIONS = [ + '@noir-lang/noir_wasm', + '@noir-lang/noir_js', + '@aztec/bb.js' + ] + + // Check for external library imports (not pre-bundled) to determine if an async IIFE wrapper is needed. + const externalImportRegex = /import\s+[\s\S]*?\s+from\s+['"]((?!\.\.?\/)[^'"]+)['"]/g + let needsAsyncWrapper = false + let match + while ((match = externalImportRegex.exec(scriptContent)) !== null) { + if (!depNames.includes(match[1])) { + needsAsyncWrapper = true + break + } + } - // Step 1: Transform 'import' statements let transformed = scriptContent.replace( /import\s+(?:({[\s\S]*?})|([\w\d_$]+)|(\*\s+as\s+[\w\d_$]+))\s+from\s+['"]([^'"]+)['"]/g, - (match, namedMembers, defaultMember, namespaceMember, pkg) => { - - // Case 1: Relative path import. This was a previously working feature. - // By converting to `require()`, we let Remix's original script runner handle it. + (fullMatch, namedMembers, defaultMember, namespaceMember, pkg) => { + // Case 1: Transform relative path imports to require(). if (pkg.startsWith('./') || pkg.startsWith('../')) { - if (namedMembers) return `const ${namedMembers} = require("${pkg}");` - if (defaultMember) return `const ${defaultMember} = require("${pkg}");` - if (namespaceMember) { - const alias = namespaceMember.split(' as ')[1] - return `const ${alias} = require("${pkg}");` - } + const members = namedMembers || defaultMember || (namespaceMember ? namespaceMember.replace('* as', '').trim() : '') + return members ? `const ${members} = require("${pkg}");` : `require("${pkg}");` } - - // Case 2: Pre-bundled library import (e.g., 'ethers'). - // Uses the global `window` object to avoid version conflicts and TDZ ReferenceErrors. - if (preBundledDeps.includes(pkg)) { - const libName = pkg.split('/').pop() - const sourceObject = `window.${libName}` - if (namedMembers) return `const ${namedMembers} = ${sourceObject};` - if (defaultMember) return `const ${defaultMember} = ${sourceObject};` - if (namespaceMember) { - const alias = namespaceMember.split(' as ')[1] - return `const ${alias} = ${sourceObject};` + + const depInfo = preBundledDeps.find(dep => dep.name === pkg) + + if (depInfo) { + // Case 2: Handling pre-bundled libraries. + // If the library is in the exception list (e.g., noir), return the original import statement. + if (STATIC_TRANSFORM_EXCEPTIONS.includes(pkg)) { + return fullMatch } + + // Only when the async wrapper is needed, transform to use the 'window' global object as an emergency measure. + if (needsAsyncWrapper) { + const members = namedMembers || defaultMember || (namespaceMember ? namespaceMember.replace('* as', '').trim() : '') + + // Since namedMembers includes braces like '{ ethers }', we can use it directly. + return members ? `const ${members} = require("${pkg}");` : `const ${pkg} = require("${pkg}");` + } + + return fullMatch } - // Case 3: External/NPM package import. - // This is the new dynamic import feature for user-added packages. - if (namedMembers) return `const ${namedMembers} = await dynamicImport("${pkg}");` - if (defaultMember) return `const ${defaultMember} = (await dynamicImport("${pkg}")).default;` - if (namespaceMember) { - const alias = namespaceMember.split(' as ')[1] - return `const ${alias} = await dynamicImport("${pkg}");` + if (defaultMember) { + return `const _${defaultMember}_module = await dynamicImport("${pkg}"); const ${defaultMember} = _${defaultMember}_module.default || _${defaultMember}_module;` } - // Fallback for any unsupported import syntax. - return `// Unsupported import for: ${pkg}` + // Case 3: Transform external libraries using dynamicImport(). + const members = namedMembers || defaultMember || (namespaceMember ? namespaceMember.replace('* as', '').trim() : null) + if (defaultMember) return `const ${defaultMember} = (await dynamicImport("${pkg}")).default` + if (members) return `const ${members} = await dynamicImport("${pkg}")` + + return fullMatch } - ); + ) - // Step 2: Remove 'export' keyword - // The script runner's execution context is not a module, so 'export' is a SyntaxError. - transformed = transformed.replace(/^export\s+/gm, '') + // Wrap the entire script in an async IIFE only when external libraries are used. + if (needsAsyncWrapper) { + const dynamicImportHelper = `const dynamicImport = (p) => new Function(\`return import('https://cdn.jsdelivr.net/npm/\${p}/+esm')\`)();\n` - // Step 3: Wrap in an async IIFE - // This enables the use of top-level 'await' for dynamic imports. - return `${dynamicImportHelper}\n(async () => {\n try {\n${transformed}\n } catch (e) { console.error('Error executing script:', e); }\n})();` + transformed = transformed.replace(/^export\s+/gm, '') + return `${dynamicImportHelper}\n(async () => {\n try {\n${transformed}\n } catch (e) { console.error('Error executing script:', e) }\n})()` + } + + return transformed } + export class ScriptRunnerBridgePlugin extends Plugin { engine: Engine dispatch: React.Dispatch = () => {} @@ -269,17 +285,15 @@ export class ScriptRunnerBridgePlugin extends Plugin { } try { this.setIsLoading(this.activeConfig.name, true) - - // Transforms the script into an executable format using the function defined above. - const preBundledDeps = this.activeConfig.dependencies.map(dep => dep.name) - const transformedScript = transformScriptForRuntime(script, preBundledDeps) + // Transforms the script into an executable format using the function defined above. + const transformedScript = transformScriptForRuntime(script, this.activeConfig.dependencies) 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) + await this.call(`${this.scriptRunnerProfileName}${this.activeConfig.name}`, 'execute', transformedScript, filePath) } catch (e) { console.error('Error executing script', e) From cdaa3d18074a38dadba2d32077d6ee7ae93bf440 Mon Sep 17 00:00:00 2001 From: hsy822 Date: Wed, 29 Oct 2025 15:10:15 +0900 Subject: [PATCH 12/20] fix(script-runner): Correctly transform imports for runtime execution --- .../src/app/plugins/script-runner-bridge.tsx | 99 +++++++------------ 1 file changed, 33 insertions(+), 66 deletions(-) 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 d0fb989931b..9f55b72b81d 100644 --- a/apps/remix-ide/src/app/plugins/script-runner-bridge.tsx +++ b/apps/remix-ide/src/app/plugins/script-runner-bridge.tsx @@ -35,81 +35,47 @@ const customBuildUrl = 'http://localhost:4000/build' // this will be used when t * @param preBundledDeps An array of dependency objects for the active runner. * @returns The transformed script content. */ -function transformScriptForRuntime(scriptContent: string, preBundledDeps: ProjectConfiguration['dependencies'] = []): string { - const depNames = preBundledDeps.map(dep => dep.name) - - // This is an exception list for pre-bundled libraries whose 'import' statements should be left as is. - const STATIC_TRANSFORM_EXCEPTIONS = [ - '@noir-lang/noir_wasm', - '@noir-lang/noir_js', - '@aztec/bb.js' - ] - - // Check for external library imports (not pre-bundled) to determine if an async IIFE wrapper is needed. - const externalImportRegex = /import\s+[\s\S]*?\s+from\s+['"]((?!\.\.?\/)[^'"]+)['"]/g - let needsAsyncWrapper = false - let match - while ((match = externalImportRegex.exec(scriptContent)) !== null) { - if (!depNames.includes(match[1])) { - needsAsyncWrapper = true - break - } - } - - let transformed = scriptContent.replace( - /import\s+(?:({[\s\S]*?})|([\w\d_$]+)|(\*\s+as\s+[\w\d_$]+))\s+from\s+['"]([^'"]+)['"]/g, - (fullMatch, namedMembers, defaultMember, namespaceMember, pkg) => { - // Case 1: Transform relative path imports to require(). - if (pkg.startsWith('./') || pkg.startsWith('../')) { - const members = namedMembers || defaultMember || (namespaceMember ? namespaceMember.replace('* as', '').trim() : '') - return members ? `const ${members} = require("${pkg}");` : `require("${pkg}");` - } +function transformScriptForRuntime(scriptContent: string, builtInDependencies: string[] = []): string { + // Helper function for dynamic import (for loading new external libraries). + const dynamicImportHelper = `const dynamicImport = (p) => new Function(\`return import('https://cdn.jsdelivr.net/npm/\${p}/+esm')\`)();\n` - const depInfo = preBundledDeps.find(dep => dep.name === pkg) + // Regex to find only NPM package imports (not relative paths starting with './', '../', '/'). + const importRegex = /import\s+(.*?)\s+from\s+['"]((?![\.\/])[\w@\/\.-]+)['"]/g - if (depInfo) { - // Case 2: Handling pre-bundled libraries. - // If the library is in the exception list (e.g., noir), return the original import statement. - if (STATIC_TRANSFORM_EXCEPTIONS.includes(pkg)) { - return fullMatch - } + // Flag to track if an async IIFE wrapper is needed. + let needsAsyncWrapper = false - // Only when the async wrapper is needed, transform to use the 'window' global object as an emergency measure. - if (needsAsyncWrapper) { - const members = namedMembers || defaultMember || (namespaceMember ? namespaceMember.replace('* as', '').trim() : '') - - // Since namedMembers includes braces like '{ ethers }', we can use it directly. - return members ? `const ${members} = require("${pkg}");` : `const ${pkg} = require("${pkg}");` - } - - return fullMatch - } + const transformed = scriptContent.replace(importRegex, (match, importClause, packageName) => { + // Case 1: If it's a built-in dependency -> Return the original import statement (no transform). + if (builtInDependencies.includes(packageName)) { + console.log(`[DIAG-TRANSFORM] Keeping built-in import: '${match}'`) + return match + } - if (defaultMember) { - return `const _${defaultMember}_module = await dynamicImport("${pkg}"); const ${defaultMember} = _${defaultMember}_module.default || _${defaultMember}_module;` - } - - // Case 3: Transform external libraries using dynamicImport(). - const members = namedMembers || defaultMember || (namespaceMember ? namespaceMember.replace('* as', '').trim() : null) - if (defaultMember) return `const ${defaultMember} = (await dynamicImport("${pkg}")).default` - if (members) return `const ${members} = await dynamicImport("${pkg}")` - - return fullMatch + // Case 2: If it's NOT a built-in dependency (new external library) -> Transform to dynamic CDN import. + console.log(`[DIAG-TRANSFORM] Transforming external import: '${match}'`) + needsAsyncWrapper = true + if (importClause.startsWith('{')) { + return `const ${importClause} = await dynamicImport("${packageName}");` + } else if (importClause.startsWith('* as')) { + const alias = importClause.split('as ')[1] + return `const ${alias} = await dynamicImport("${packageName}");` + } else { + return `const ${importClause} = (await dynamicImport("${packageName}")).default || await dynamicImport("${packageName}");` } - ) + }) - // Wrap the entire script in an async IIFE only when external libraries are used. + // If at least one dynamic import transformation occurred, wrap the entire script in an async IIFE. if (needsAsyncWrapper) { - const dynamicImportHelper = `const dynamicImport = (p) => new Function(\`return import('https://cdn.jsdelivr.net/npm/\${p}/+esm')\`)();\n` - - transformed = transformed.replace(/^export\s+/gm, '') - return `${dynamicImportHelper}\n(async () => {\n try {\n${transformed}\n } catch (e) { console.error('Error executing script:', e) }\n})()` + // Remove export statements as they are not valid inside an IIFE. + const finalTransformed = transformed.replace(/^export\s+/gm, '') + return `${dynamicImportHelper}\n(async () => {\n try {\n${finalTransformed}\n } catch (e) { console.error('Error executing script:', e); }\n})();` + } else { + // If no dynamic imports were needed, return the original script as is (no wrapper). + return transformed } - - return transformed } - export class ScriptRunnerBridgePlugin extends Plugin { engine: Engine dispatch: React.Dispatch = () => {} @@ -286,7 +252,8 @@ export class ScriptRunnerBridgePlugin extends Plugin { try { this.setIsLoading(this.activeConfig.name, true) // Transforms the script into an executable format using the function defined above. - const transformedScript = transformScriptForRuntime(script, this.activeConfig.dependencies) + const builtInDependencies = this.activeConfig.dependencies ? this.activeConfig.dependencies.map(dep => dep.name) : [] + const transformedScript = transformScriptForRuntime(script, builtInDependencies) console.log('--- [ScriptRunner] Original Script ---') console.log(script) From 6782fc931128934eebb71061bc1afd2a4b308097 Mon Sep 17 00:00:00 2001 From: hsy822 Date: Wed, 29 Oct 2025 16:22:40 +0900 Subject: [PATCH 13/20] fix(script-runner): Separate static and dynamic imports in transformation --- .../src/app/plugins/script-runner-bridge.tsx | 74 +++++++++++-------- 1 file changed, 44 insertions(+), 30 deletions(-) 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 9f55b72b81d..74ce2bc1b5d 100644 --- a/apps/remix-ide/src/app/plugins/script-runner-bridge.tsx +++ b/apps/remix-ide/src/app/plugins/script-runner-bridge.tsx @@ -36,44 +36,58 @@ const customBuildUrl = 'http://localhost:4000/build' // this will be used when t * @returns The transformed script content. */ function transformScriptForRuntime(scriptContent: string, builtInDependencies: string[] = []): string { - // Helper function for dynamic import (for loading new external libraries). const dynamicImportHelper = `const dynamicImport = (p) => new Function(\`return import('https://cdn.jsdelivr.net/npm/\${p}/+esm')\`)();\n` + const importRegex = /import\s+(.*?)\s+from\s+['"]([^'"]+)['"]/g - // Regex to find only NPM package imports (not relative paths starting with './', '../', '/'). - const importRegex = /import\s+(.*?)\s+from\s+['"]((?![\.\/])[\w@\/\.-]+)['"]/g + const staticImports = [] + const dynamicImports = [] - // Flag to track if an async IIFE wrapper is needed. - let needsAsyncWrapper = false - - const transformed = scriptContent.replace(importRegex, (match, importClause, packageName) => { - // Case 1: If it's a built-in dependency -> Return the original import statement (no transform). - if (builtInDependencies.includes(packageName)) { - console.log(`[DIAG-TRANSFORM] Keeping built-in import: '${match}'`) - return match + const scriptBody = scriptContent.replace(importRegex, (match, importClause, packageName) => { + if (packageName.startsWith('.') || packageName.startsWith('/')) { + staticImports.push(match) + return '' } - - // Case 2: If it's NOT a built-in dependency (new external library) -> Transform to dynamic CDN import. - console.log(`[DIAG-TRANSFORM] Transforming external import: '${match}'`) - needsAsyncWrapper = true - if (importClause.startsWith('{')) { - return `const ${importClause} = await dynamicImport("${packageName}");` - } else if (importClause.startsWith('* as')) { - const alias = importClause.split('as ')[1] - return `const ${alias} = await dynamicImport("${packageName}");` - } else { - return `const ${importClause} = (await dynamicImport("${packageName}")).default || await dynamicImport("${packageName}");` + + if (builtInDependencies.includes(packageName)) { + staticImports.push(match) + return '' } + + dynamicImports.push({ importClause, packageName }) + return '' }) - // If at least one dynamic import transformation occurred, wrap the entire script in an async IIFE. - if (needsAsyncWrapper) { - // Remove export statements as they are not valid inside an IIFE. - const finalTransformed = transformed.replace(/^export\s+/gm, '') - return `${dynamicImportHelper}\n(async () => {\n try {\n${finalTransformed}\n } catch (e) { console.error('Error executing script:', e); }\n})();` - } else { - // If no dynamic imports were needed, return the original script as is (no wrapper). - return transformed + let finalScript = '' + + if (staticImports.length > 0) { + finalScript += staticImports.join('\n') + '\n\n' + console.log('[DIAG-TRANSFORM] Keeping static imports:\n', staticImports.join('\n')) + } + + finalScript += `${dynamicImportHelper}\n(async () => {\n try {\n` + + if (dynamicImports.length > 0) { + const dynamicTransforms = [] + for (const info of dynamicImports) { + if (info.importClause.startsWith('{')) { + dynamicTransforms.push(` const ${info.importClause} = await dynamicImport("${info.packageName}");`) + } else if (info.importClause.startsWith('* as')) { + const alias = info.importClause.split('as ')[1] + dynamicTransforms.push(` const ${alias} = await dynamicImport("${info.packageName}");`) + } else { + dynamicTransforms.push(` const ${info.importClause} = (await dynamicImport("${info.packageName}")).default || await dynamicImport("${info.packageName}");`) + } + } + finalScript += dynamicTransforms.join('\n') + '\n\n' + console.log('[DIAG-TRANSFORM] Added dynamic imports:\n', dynamicTransforms.join('\n')) } + + const finalScriptBody = scriptBody.replace(/^export\s+/gm, '') + finalScript += finalScriptBody + + finalScript += `\n } catch (e) { console.error('Error executing script:', e); }\n})();` + + return finalScript } export class ScriptRunnerBridgePlugin extends Plugin { From 26bac4f5ea24bcd0125a2912bf5353c4554b91c3 Mon Sep 17 00:00:00 2001 From: hsy822 Date: Mon, 3 Nov 2025 17:22:13 +0900 Subject: [PATCH 14/20] Add toast notifications for JS/TS type loading --- apps/remix-ide/src/app/editor/editor.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/remix-ide/src/app/editor/editor.js b/apps/remix-ide/src/app/editor/editor.js index 7f5ca326d0d..c0fa4de274c 100644 --- a/apps/remix-ide/src/app/editor/editor.js +++ b/apps/remix-ide/src/app/editor/editor.js @@ -255,6 +255,7 @@ export default class Editor extends Plugin { this.toggleTsDiagnostics(false) this.triggerEvent('typesLoading', ['start']) console.log('[DIAGNOSE-BATCH] Types batch started') + this.call('notification', 'toast', 'Loading JS/TS type information...') } this.typesLoadingCount++ } @@ -266,6 +267,7 @@ export default class Editor extends Plugin { this.toggleTsDiagnostics(true) this.triggerEvent('typesLoading', ['end']) console.log('[DIAGNOSE-BATCH] Types batch ended') + this.call('notification', 'toast', 'JS/TS types loaded successfully.') } } From 5bab38bade32579586677a531f7f14d49334f95a Mon Sep 17 00:00:00 2001 From: hsy822 Date: Thu, 27 Nov 2025 14:39:10 +0900 Subject: [PATCH 15/20] feat: fix type loading bug --- apps/remix-ide/src/app/editor/editor.js | 97 +----- apps/remix-ide/src/app/editor/type-fetcher.ts | 322 ++++++++++++------ .../src/app/plugins/script-runner-bridge.tsx | 42 ++- .../editor/src/lib/remix-ui-editor.tsx | 39 +-- 4 files changed, 265 insertions(+), 235 deletions(-) diff --git a/apps/remix-ide/src/app/editor/editor.js b/apps/remix-ide/src/app/editor/editor.js index c023db2877b..e99f8d5273d 100644 --- a/apps/remix-ide/src/app/editor/editor.js +++ b/apps/remix-ide/src/app/editor/editor.js @@ -167,7 +167,6 @@ export default class Editor extends Plugin { }) tsDefaults.setDiagnosticsOptions({ noSemanticValidation: false, noSyntaxValidation: false }) ts.typescriptDefaults.setEagerModelSync(true) - console.log('[DIAGNOSE-SETUP] CompilerOptions set to NodeNext and diagnostics enabled') }) this.on('sidePanel', 'focusChanged', (name) => { this.keepDecorationsFor(name, 'sourceAnnotationsPerFile') @@ -184,11 +183,18 @@ export default class Editor extends Plugin { this.currentFile = null this.renderComponent() }) + this.on('scriptRunnerBridge', 'runnerChanged', async () => { + this.processedPackages.clear() + this.tsModuleMappings = {} + + if (this.currentFile) { + clearTimeout(this.typeLoaderDebounce) + await this._onChange(this.currentFile) + } + }) try { this.currentThemeType = (await this.call('theme', 'currentTheme')).quality - } catch (e) { - console.log('unable to select the theme ' + e.message) - } + } catch (e) {} this.renderComponent() } @@ -199,8 +205,6 @@ export default class Editor extends Plugin { updateTsCompilerOptions() { if (!this.monaco) return - console.log('[DIAGNOSE-PATHS] Updating TS compiler options...') - console.log('[DIAGNOSE-PATHS] Current path mappings:', JSON.stringify(this.tsModuleMappings, null, 2)) const tsDefaults = this.monaco.languages.typescript.typescriptDefaults const currentOptions = tsDefaults.getCompilerOptions() @@ -209,7 +213,6 @@ export default class Editor extends Plugin { ...currentOptions, paths: { ...currentOptions.paths, ...this.tsModuleMappings } }) - console.log('[DIAGNOSE-PATHS] TS compiler options updated.') } toggleTsDiagnostics(enable) { @@ -219,7 +222,6 @@ export default class Editor extends Plugin { noSemanticValidation: !enable, noSyntaxValidation: false }) - console.log(`[DIAGNOSE-DIAG] Semantic diagnostics ${enable ? 'enabled' : 'disabled'}`) } addShimForPackage(pkg) { @@ -239,8 +241,6 @@ export default class Editor extends Plugin { this.shimDisposers.set(shimWildPath, d2) } - this.tsModuleMappings[pkg] = [shimMainPath.replace('file:///', '')] - this.tsModuleMappings[`${pkg}/*`] = [`${pkg}/*`] } removeShimsForPackage(pkg) { @@ -258,7 +258,6 @@ export default class Editor extends Plugin { if (this.typesLoadingCount === 0) { this.toggleTsDiagnostics(false) this.triggerEvent('typesLoading', ['start']) - console.log('[DIAGNOSE-BATCH] Types batch started') this.call('notification', 'toast', 'Loading JS/TS type information...') } this.typesLoadingCount++ @@ -270,14 +269,12 @@ export default class Editor extends Plugin { this.updateTsCompilerOptions() this.toggleTsDiagnostics(true) this.triggerEvent('typesLoading', ['end']) - console.log('[DIAGNOSE-BATCH] Types batch ended') this.call('notification', 'toast', 'JS/TS types loaded successfully.') } } addExtraLibs(libs) { if (!this.monaco || !libs || libs.length === 0) return - console.log(`[DIAGNOSE-LIBS] Adding ${libs.length} new files to Monaco...`) const tsDefaults = this.monaco.languages.typescript.typescriptDefaults @@ -286,7 +283,6 @@ export default class Editor extends Plugin { tsDefaults.addExtraLib(lib.content, lib.filePath) } }) - console.log(`[DIAGNOSE-LIBS] Files added. Total extra libs now: ${Object.keys(tsDefaults.getExtraLibs()).length}.`) } // [2/4] The conductor, called on every editor content change to parse 'import' statements and trigger the type loading process. @@ -317,8 +313,6 @@ export default class Editor extends Plugin { if (newBasePackages.length === 0) return - console.log('[DIAGNOSE] New base packages for analysis:', newBasePackages) - // Temporarily disable type checking during type loading to prevent error flickering. this.beginTypesBatch() @@ -329,7 +323,6 @@ export default class Editor extends Plugin { }) this.updateTsCompilerOptions() - console.log('[DIAGNOSE] Shims added. Red lines should disappear.') // [Phase 2: Deep Analysis] // In the background, fetch the actual type files to enable autocompletion. @@ -340,13 +333,10 @@ export default class Editor extends Plugin { const libInfo = activeRunnerLibs.find(lib => lib.name === basePackage) const packageToLoad = libInfo ? `${libInfo.name}@${libInfo.version}` : basePackage - - console.log(`[DIAGNOSE] Preparing to load types for: "${packageToLoad}"`) try { const result = await startTypeLoadingProcess(packageToLoad) if (result && result.libs && result.libs.length > 0) { - console.log(`[DIAGNOSE-DEEP-PASS] "${basePackage}" deep pass complete. Adding ${result.libs.length} files.`) // Add all fetched type files to Monaco. this.addExtraLibs(result.libs) @@ -373,14 +363,15 @@ export default class Editor extends Plugin { } catch (e) { // Crawler can fail, but we don't want to crash the whole process. console.error(`[DIAGNOSE-DEEP-PASS] Crawler failed for "${basePackage}":`, e) + this.call('notification', 'toast', `Failed to load types for package: ${basePackage}.`) } })) - console.log('[DIAGNOSE] All processes finished.') // After all type loading is complete, re-enable type checking and apply the final state. this.endTypesBatch() } catch (error) { + this.processedPackages.delete(basePackage) console.error('[DIAGNOSE-ONCHANGE] Critical error during type loading process:', error) this.endTypesBatch() } @@ -426,70 +417,6 @@ export default class Editor extends Plugin { return ext && this.modes[ext] ? this.modes[ext] : this.modes.txt } - async handleTypeScriptDependenciesOf (path, content, readFile, exists) { - const isTsFile = path.endsWith('.ts') || path.endsWith('.tsx') - const isJsFile = path.endsWith('.js') || path.endsWith('.jsx') - - if (isTsFile || isJsFile) { - // extract the import, resolve their content - // and add the imported files to Monaco through the `addModel` - // so Monaco can provide auto completion - const paths = path.split('/') - paths.pop() - const fromPath = paths.join('/') // get current execution context path - const language = isTsFile ? 'typescript' : 'javascript' - - for (const match of content.matchAll(/import\s+.*\s+from\s+(?:"(.*?)"|'(.*?)')/g)) { - let pathDep = match[2] - if (pathDep.startsWith('./') || pathDep.startsWith('../')) pathDep = resolve(fromPath, pathDep) - if (pathDep.startsWith('/')) pathDep = pathDep.substring(1) - - // Try different file extensions if no extension is provided - const extensions = isTsFile ? ['.ts', '.tsx', '.d.ts'] : ['.js', '.jsx'] - let hasExtension = false - for (const ext of extensions) { - if (pathDep.endsWith(ext)) { - hasExtension = true - break - } - } - - if (!hasExtension) { - // Try to find the file with different extensions - for (const ext of extensions) { - const pathWithExt = pathDep + ext - try { - const pathExists = await exists(pathWithExt) - if (pathExists) { - pathDep = pathWithExt - break - } - } catch (e) { - // continue to next extension - } - } - } - - try { - // we can't use the fileManager plugin call directly - // because it's itself called in a plugin context, and that causes a timeout in the plugin stack - const pathExists = await exists(pathDep) - let contentDep = '' - if (pathExists) { - contentDep = await readFile(pathDep) - if (contentDep !== '') { - this.emit('addModel', contentDep, language, pathDep, this.readOnlySessions[path]) - } - } else { - // console.log("The file ", pathDep, " can't be found.") - } - } catch (e) { - console.log(e) - } - } - } - } - /** * Create an editor session * @param {string} path path of the file diff --git a/apps/remix-ide/src/app/editor/type-fetcher.ts b/apps/remix-ide/src/app/editor/type-fetcher.ts index 61eee9c8438..234cb183601 100644 --- a/apps/remix-ide/src/app/editor/type-fetcher.ts +++ b/apps/remix-ide/src/app/editor/type-fetcher.ts @@ -1,9 +1,15 @@ -import { Monaco } from '@monaco-editor/react' - -// A type representing a single type file (.d.ts) to be added to the Monaco editor, containing its virtual path and content. +/** + * [Type Definition] + * Represents a single library file (.d.ts) to be added to the Monaco editor. + * filePath: The virtual path (e.g., 'file:///node_modules/...') + * content: The actual text content of the .d.ts file. + */ type Library = { filePath: string; content: string } -// A type defining the minimum required fields from a package.json for type loading. +/** + * [Type Definition] + * Defines the minimum required fields from a package.json for type loading. + */ type PackageJson = { name?: string version?: string @@ -11,25 +17,42 @@ type PackageJson = { typings?: string exports?: string | Record } + type ResolveResult = { finalUrl: string; content: string } +/** + * [Type Definition] + * A cache map used to prevent duplicate network requests. + * Key: The Request URL. + * Value: The Promise of the request result. This allows concurrent requests + * for the same URL to share the same Promise (Deduplication). + */ +type FetchCache = Map> + const CDN_BASE = 'https://cdn.jsdelivr.net/npm/' const VIRTUAL_BASE = 'file:///node_modules/' + +// Regex to find import/export/require statements. +// Note: Currently optimized for single lines. Use [\s\S]*? if multi-line support is needed. const IMPORT_ANY_RE = /(?:import|export)\s+[^'"]*?from\s*['"]([^'"]+)['"]|import\s*['"]([^'"]+)['"]|require\(\s*['"]([^'"]+)['"]\s*\)/g + +// Regex to find triple-slash directives like /// const TRIPLE_SLASH_REF_RE = /\/\/\/\s*/g +// Checks if a path is relative ('./', '../', '/'). function isRelative(p: string): boolean { return p.startsWith('./') || p.startsWith('../') || p.startsWith('/') } -// Extracts the base package name from an import path (e.g., 'viem/chains' -> 'viem'). +// Extracts the base package name (e.g., 'viem/chains' -> 'viem', '@scope/pkg/sub' -> '@scope/pkg'). function normalizeBareSpecifier(p: string): string { if (!p) return p if (p.startsWith('@')) return p.split('/').slice(0, 2).join('/') return p.split('/')[0] } -// Function to generate @types package names, includes logic to prevent infinite recursion. +// Generates the @types scoped name (includes logic to prevent infinite recursion). +// e.g., 'react' -> '@types/react', '@scope/pkg' -> '@types/scope__pkg' function toTypesScopedName(pkg: string): string { if (pkg.startsWith('@types/')) return pkg if (pkg.startsWith('@')) return '@types/' + pkg.slice(1).replace('/', '__') @@ -41,39 +64,55 @@ function toVirtual(url: string): string { return url.replace(CDN_BASE, VIRTUAL_BASE) } +// Removes file extensions (.d.ts, .ts, .js) from a URL. function stripJsLike(url: string): string { return url.replace(/\.d\.[mc]?ts$/, '').replace(/\.[mc]?ts$/, '').replace(/\.[mc]?js$/, '') } +// Utility function to fetch JSON data. async function fetchJson(url: string): Promise { const res = await fetch(url) if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`) return res.json() } -// Guesses a list of potential TypeScript Definition file (.d.ts) paths from a given JS-like file path. -// For example, 'index.js' is converted to 'index.d.ts', 'index.ts', 'index/index.d.ts', etc. +/** + * Guesses a list of potential TypeScript Definition file (.d.ts) paths from a given JS-like file path. + * e.g., 'index.js' -> ['index.d.ts', 'index.ts', 'index/index.d.ts', 'index/index.ts'] + */ function guessDtsFromJs(jsPath: string): string[] { const base = stripJsLike(jsPath) return [`${base}.d.ts`, `${base}.ts`, `${base}/index.d.ts`, `${base}/index.ts`] } +/** + * Parses 'exports', 'types', or 'typings' fields in package.json to map subpaths + * to their corresponding entry point URLs. + */ function buildExportTypeMap(pkgName: string, pkgJson: PackageJson): Record { const map: Record = {} const base = `${CDN_BASE}${pkgName}/` + + // Helper: Validates the path and adds it to the map. const push = (subpath: string, relPath: string | undefined) => { - if (!relPath) return - // [DEBUG] Check what values are being passed to the push function. - console.log(`[DEBUG] push called with: subpath='${subpath}', relPath='${relPath}'`) + if (typeof relPath !== 'string' || !relPath) { + console.warn(`[DIAG-PUSH-ERROR] Invalid path pushed for subpath '${subpath}' in package '${pkgName}'. Type: ${typeof relPath}, Value: ${relPath}`) + return + } + + try { + new URL(relPath, base) + } catch (e) { + console.warn(`[DIAG-PUSH-SKIP] Invalid relative path skipped: ${relPath}`) + return + } if (/\.d\.[mc]?ts$/.test(relPath)) { - // [DEBUG] Case when using the declaration file path directly. - console.log(`[DEBUG] Direct declaration path matched for '${relPath}'. Using it as is.`) + // If it's already a declaration file, use as is. map[subpath] = [new URL(relPath, base).href] } else { - // [DEBUG] Case when guessing based on a JS file path. - console.log(`[DEBUG] Guessing declaration path for '${relPath}'.`) + // If it's a JS file, guess the .d.ts location. map[subpath] = guessDtsFromJs(relPath).map(a => new URL(a, base).href) } } @@ -81,14 +120,8 @@ function buildExportTypeMap(pkgName: string, pkgJson: PackageJson): Record - // [DEBUG] Check the value of exports.types. - console.log(`[DEBUG] Checking exports.types:`, exports.types) - if (exports.types) { push('.', exports.types) - - // [DEBUG] Check the state of the map object after processing exports.types. - console.log(`[DEBUG] Map state after exports.types:`, JSON.stringify(map, null, 2)) return map } @@ -98,7 +131,17 @@ function buildExportTypeMap(pkgName: string, pkgJson: PackageJson): Record { - for (const u of [...new Set(urls)]) { - try { - const r = await fetch(u); if (r.ok) return { finalUrl: u, content: await r.text() } - } catch (e) {} +/** + * [Core Logic] + * Iterates through a list of candidate URLs to fetch file content. + * - Uses 'fetchCache' to prevent duplicate network requests. + * - If a request is already in progress, it reuses the existing Promise. + * - Returns the content of the first successful (200 OK) request. + */ +async function tryFetchOne(urls: string[], fetchCache: FetchCache): Promise { + const uniqueUrls = [...new Set(urls)] + + for (const u of uniqueUrls) { + let fetchPromise = fetchCache.get(u) + + // If not in cache, start a new request + if (!fetchPromise) { + fetchPromise = (async () => { + try { + const res = await fetch(u) + if (res.ok) return await res.text() + return null + } catch (e) { + return null + } + })(); + // Store the Promise itself in the cache to handle race conditions + fetchCache.set(u, fetchPromise) + } + + // Wait for the result (reuses existing promise if available) + const content = await fetchPromise + if (content !== null) { + return { finalUrl: u, content } + } } return null } -// A crawler that recursively follows imports/exports within a type definition file (.d.ts). -async function crawl(entryUrl: string, pkgName: string, visited: Set, enqueuePackage: (name: string) => void): Promise { +/** + * [Recursive Crawler] + * Parses the content of a type definition file (.d.ts) to find imports/exports and references, + * then recursively loads them. + * - Uses 'visited' set to prevent circular dependency loops. + * - Passes 'fetchCache' down to all recursive calls to optimize network usage. + */ +async function crawl( + entryUrl: string, + pkgName: string, + visited: Set, + fetchCache: FetchCache, + enqueuePackage: (name: string) => void +): Promise { if (visited.has(entryUrl)) return [] visited.add(entryUrl) + const out: Library[] = [] try { - // Check if the entryUrl is already a type declaration file. + // If it's strictly a .d.ts, use it. Otherwise, guess the path. const urlsToTry = /\.d\.[mc]?ts$/.test(entryUrl) - ? [entryUrl] // If yes, use only that URL without guessing. - : guessDtsFromJs(entryUrl) // Otherwise, guess the paths. + ? [entryUrl] + : guessDtsFromJs(entryUrl) - const res = await tryFetchOne(urlsToTry) + // Fetch content using cache + const res = await tryFetchOne(urlsToTry, fetchCache) if(!res) return [] const { finalUrl, content } = res out.push({ filePath: toVirtual(finalUrl), content }) + const subPromises: Promise[] = [] + const crawlNext = (nextUrl: string) => { - if (!visited.has(nextUrl)) subPromises.push(crawl(nextUrl, pkgName, visited, enqueuePackage)) + // Recurse only if not visited + if (!visited.has(nextUrl)) subPromises.push(crawl(nextUrl, pkgName, visited, fetchCache, enqueuePackage)) } - // Handles triple-slash directives like '/// '. + + // 1. Parse Triple-slash references (/// ) for (const m of content.matchAll(TRIPLE_SLASH_REF_RE)) crawlNext(new URL(m[1], finalUrl).href) + + // 2. Parse Import/Export/Require statements for (const m of content.matchAll(IMPORT_ANY_RE)) { const spec = (m[1] || m[2] || m[3] || '').trim() if (!spec) continue - // Continues crawling for relative path imports, and queues up external package imports. - if (isRelative(spec)) crawlNext(new URL(spec, finalUrl).href) + if (isRelative(spec)) crawlNext(new URL(spec, finalUrl).href) // Continue crawling relative paths else { + // Enqueue external packages to be handled separately in loadPackage const bare = normalizeBareSpecifier(spec) - // Ignores Node.js built-in modules that use the 'node:' protocol. if (bare && !bare.startsWith('node:')) enqueuePackage(bare) } } const results = await Promise.all(subPromises) results.forEach(arr => out.push(...arr)) - } catch (e) { - // console.error(`Crawl failed for ${entryUrl}:`, e) // Optional: for deeper debugging - } + } catch (e) {} return out } -// [3/4] The core service that, upon request from 'editor.ts', fetches type definitions (.d.ts) for NPM packages from a CDN. +/** + * [Main Entry Point] + * The main function called by the Editor. + * Loads type definitions for a specific package and all its dependencies. + */ export async function startTypeLoadingProcess(packageName: string): Promise<{ mainVirtualPath: string; libs: Library[]; subpathMap: Record } | void> { const visitedPackages = new Set() const collected: Library[] = [] const subpathMap: Record = {} - // The core inner function that recursively loads a package and its dependencies. + // Create a shared request cache for the entire process duration (prevents duplicate 404/200 requests) + const fetchCache: FetchCache = new Map() + + // Inner function: Loads a single package and its dependencies async function loadPackage(pkgNameToLoad: string) { - if (visitedPackages.has(pkgNameToLoad)) return - visitedPackages.add(pkgNameToLoad) - - let pkgJson: PackageJson - try { - const pkgJsonUrl = new URL('package.json', `${CDN_BASE}${pkgNameToLoad}/`).href - pkgJson = await fetchJson(pkgJsonUrl) - } catch (e) { - console.log(`- Package '${pkgNameToLoad}' not found. Attempting @types fallback.`) - // If the package is not found, attempt to find its @types equivalent. - try { await loadPackage(toTypesScopedName(pkgNameToLoad)) } catch (ee) {} - return - } + if (visitedPackages.has(pkgNameToLoad)) return + visitedPackages.add(pkgNameToLoad) + + let pkgJson: PackageJson + let attemptedTypesFallback = false + + // Loop to handle the @types fallback strategy + while (true) { + let currentPkgName = pkgNameToLoad + + // If the main package failed, try the @types scoped name + if (attemptedTypesFallback) { + currentPkgName = toTypesScopedName(pkgNameToLoad) + } - const exportMap = buildExportTypeMap(pkgNameToLoad, pkgJson) + try { + const pkgJsonUrl = new URL('package.json', `${CDN_BASE}${currentPkgName}/`).href + pkgJson = await fetchJson(pkgJsonUrl) - // If the package is found but contains no type information, attempt the @types fallback. - if (Object.keys(exportMap).length === 0) { - console.log(`- No type declarations in '${pkgNameToLoad}'. Attempting @types fallback.`) - try { await loadPackage(toTypesScopedName(pkgNameToLoad)) } catch (ee) {} - return - } - - console.log(`[LOG 1] Starting full analysis for package: '${pkgNameToLoad}'`) - const pendingDependencies = new Set() - const enqueuePackage = (p: string) => { if (!visitedPackages.has(p)) pendingDependencies.add(p) } - - const crawlPromises: Promise[] = [] - // Crawl all entry points of the package to gather complete type information. - for (const [subpath, urls] of Object.entries(exportMap)) { - const entryPointUrl = urls[0] - if (entryPointUrl) { - const pkgNameWithoutVersion = pkgNameToLoad.replace(/@[\^~]?[\d\.\w-]+$/, '') - const virtualPathKey = subpath === '.' ? pkgNameWithoutVersion : `${pkgNameWithoutVersion}/${subpath.replace('./', '')}` - - subpathMap[virtualPathKey] = entryPointUrl.replace(CDN_BASE, '') - crawlPromises.push(crawl(entryPointUrl, pkgNameToLoad, new Set(), enqueuePackage)) - } - } + const exportMap = buildExportTypeMap(currentPkgName, pkgJson) - const libsArrays = await Promise.all(crawlPromises) - libsArrays.forEach(libs => collected.push(...libs)) - - // Recursively load any discovered dependency packages. - if (pendingDependencies.size > 0) { - console.log(`- Found dependencies for '${pkgNameToLoad}': ${Array.from(pendingDependencies).join(', ')}`) - await Promise.all(Array.from(pendingDependencies).map(loadPackage)) + // If no types found, attempt fallback to @types + if (Object.keys(exportMap).length === 0) { + if (!attemptedTypesFallback) { + attemptedTypesFallback = true + continue + } else { + return // Give up if @types also fails + } + } + + const pendingDependencies = new Set() + const enqueuePackage = (p: string) => { if (!visitedPackages.has(p)) pendingDependencies.add(p) } + + const crawlPromises: Promise[] = [] + for (const [subpath, urls] of Object.entries(exportMap)) { + const entryPointUrl = urls[0] + if (entryPointUrl) { + const pkgNameWithoutVersion = currentPkgName.replace(/@[\^~]?[\d\.\w-]+$/, '') + const virtualPathKey = subpath === '.' ? pkgNameWithoutVersion : `${pkgNameWithoutVersion}/${subpath.replace('./', '')}` + + subpathMap[virtualPathKey] = entryPointUrl.replace(CDN_BASE, '') + // Start crawling (passing fetchCache) + crawlPromises.push(crawl(entryPointUrl, currentPkgName, new Set(), fetchCache, enqueuePackage)) + } + } + + const libsArrays = await Promise.all(crawlPromises) + let totalCollectedFiles = 0 + libsArrays.forEach(libs => { + collected.push(...libs) + totalCollectedFiles += libs.length + }) + + // If package.json exists but no .d.ts files were found, try @types fallback + if (totalCollectedFiles === 0 && !attemptedTypesFallback) { + attemptedTypesFallback = true + continue + } + + // Load discovered dependencies + if (pendingDependencies.size > 0) { + await Promise.all(Array.from(pendingDependencies).map(loadPackage)) + } + + return + + } catch (e) { + // If 404 occurs, try @types fallback + if (e && e.message && e.message.includes('404') && !attemptedTypesFallback) { + attemptedTypesFallback = true + continue + } + console.error(`- Fatal error or already tried @types for '${currentPkgName}':`, e.message) + return + } + } } -} await loadPackage(packageName) const mainVirtualPath = subpathMap[packageName] ? `${VIRTUAL_BASE}${subpathMap[packageName]}` : '' const finalPackages = [...new Set(collected.map(lib => normalizeBareSpecifier(lib.filePath.replace(VIRTUAL_BASE, ''))))] - console.log(`[LOG 2] Full analysis complete. Total files: ${collected.length}. Packages loaded: ${finalPackages.join(', ')}`) return { mainVirtualPath, libs: collected, subpathMap } } \ No newline at end of file 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 74ce2bc1b5d..a6213765944 100644 --- a/apps/remix-ide/src/app/plugins/script-runner-bridge.tsx +++ b/apps/remix-ide/src/app/plugins/script-runner-bridge.tsx @@ -14,7 +14,7 @@ const profile = { name: 'scriptRunnerBridge', displayName: 'Script configuration', methods: ['execute', 'getConfigurations', 'selectScriptRunner', 'getActiveRunnerLibs'], - events: ['log', 'info', 'warn', 'error'], + events: ['log', 'info', 'warn', 'error', 'runnerChanged'], icon: 'assets/img/solid-gear-circle-play.svg', description: 'Configure the dependencies for running scripts.', kind: '', @@ -29,15 +29,29 @@ 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 /** - * @description A helper function that transforms script content for runtime execution. - * It dynamically determines the transformation strategy based on whether external libraries are used. - * @param scriptContent The original script content. - * @param preBundledDeps An array of dependency objects for the active runner. - * @returns The transformed script content. + * Transforms the provided script content to make it executable in a browser environment. + * * Key Transformation Logic: + * 1. Hybrid Import Handling: + * - Relative imports (starting with `.` or `/`) and libraries listed in `builtInDependencies` + * are preserved as standard static ES imports (hoisted to the top). + * - External NPM packages are converted into dynamic `await import(...)` calls fetching from `cdn.jsdelivr.net`. + * * 2. Multi-line Support: + * - Uses an enhanced Regex (`[\s\S]*?`) to correctly parse import statements that span multiple lines. + * * 3. Async Wrapper: + * - Wraps the main execution logic (excluding static imports) in an `async IIFE` + * to enable top-level await behavior for the dynamic imports. + * * 4. Syntax Adjustments: + * - Handles various import styles: Destructuring (`{ a }`), Namespace (`* as a`), and Default (`a`). + * - Removes `export` keywords to prevent syntax errors within the IIFE context. + * + * @param scriptContent - The original source code of the script to be transformed. + * @param builtInDependencies - An array of package names that are pre-bundled or available in the runtime environment + * (e.g., ['chai', 'web3']) and should not be fetched from the CDN. + * @returns The transformed script string, ready for runtime evaluation. */ function transformScriptForRuntime(scriptContent: string, builtInDependencies: string[] = []): string { const dynamicImportHelper = `const dynamicImport = (p) => new Function(\`return import('https://cdn.jsdelivr.net/npm/\${p}/+esm')\`)();\n` - const importRegex = /import\s+(.*?)\s+from\s+['"]([^'"]+)['"]/g + const importRegex = /import\s+([\s\S]*?)\s+from\s+['"]([^'"]+)['"]/g const staticImports = [] const dynamicImports = [] @@ -61,7 +75,6 @@ function transformScriptForRuntime(scriptContent: string, builtInDependencies: s if (staticImports.length > 0) { finalScript += staticImports.join('\n') + '\n\n' - console.log('[DIAG-TRANSFORM] Keeping static imports:\n', staticImports.join('\n')) } finalScript += `${dynamicImportHelper}\n(async () => {\n try {\n` @@ -79,7 +92,6 @@ function transformScriptForRuntime(scriptContent: string, builtInDependencies: s } } finalScript += dynamicTransforms.join('\n') + '\n\n' - console.log('[DIAG-TRANSFORM] Added dynamic imports:\n', dynamicTransforms.join('\n')) } const finalScriptBody = scriptBody.replace(/^export\s+/gm, '') @@ -190,7 +202,10 @@ export class ScriptRunnerBridgePlugin extends Plugin { } async selectScriptRunner(config: ProjectConfiguration) { - if (await this.loadScriptRunner(config)) await this.saveCustomConfig(this.customConfig) + if (await this.loadScriptRunner(config)) { + await this.saveCustomConfig(this.customConfig) + this.emit('runnerChanged', config) + } } async loadScriptRunner(config: ProjectConfiguration): Promise { @@ -269,11 +284,6 @@ export class ScriptRunnerBridgePlugin extends Plugin { const builtInDependencies = this.activeConfig.dependencies ? this.activeConfig.dependencies.map(dep => dep.name) : [] const transformedScript = transformScriptForRuntime(script, builtInDependencies) - 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) { @@ -313,7 +323,6 @@ export class ScriptRunnerBridgePlugin extends Plugin { } async dependencyError(data: any) { - console.log('Script runner dependency error: ', data) let message = `Error loading dependencies: ` if (isArray(data.data)) { data.data.forEach((data: any) => { @@ -449,7 +458,6 @@ export class ScriptRunnerBridgePlugin extends Plugin { console.log('Error status:', error.response.status) console.log('Error data:', error.response.data) // This should give you the output being sent console.log('Error headers:', error.response.headers) - if (error.response.data.error) { if (isArray(error.response.data.error)) { const message = `${error.response.data.error[0]}` 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 1e04bc117d2..be03018313a 100644 --- a/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx +++ b/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx @@ -1165,6 +1165,9 @@ export const EditorUI = (props: EditorUIProps) => { const editorService = editor._codeEditorService const openEditorBase = editorService.openCodeEditor.bind(editorService) editorService.openCodeEditor = async (input, source) => { + if (input && input.resource && input.resource.path.includes('__shims__')) { + return openEditorBase(input, source) + } const result = await openEditorBase(input, source) if (input && input.resource && input.resource.path) { try { @@ -1209,42 +1212,6 @@ export const EditorUI = (props: EditorUIProps) => { // 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]}) - // Configure TypeScript compiler options for JSX/TSX support - monacoRef.current.languages.typescript.typescriptDefaults.setCompilerOptions({ - jsx: monacoRef.current.languages.typescript.JsxEmit.React, - jsxFactory: 'React.createElement', - reactNamespace: 'React', - allowNonTsExtensions: true, - allowJs: true, - target: monacoRef.current.languages.typescript.ScriptTarget.Latest, - moduleResolution: monacoRef.current.languages.typescript.ModuleResolutionKind.NodeJs, - module: monacoRef.current.languages.typescript.ModuleKind.ESNext, - noEmit: true, - esModuleInterop: true, - allowSyntheticDefaultImports: true, - skipLibCheck: true, - resolveJsonModule: true, - isolatedModules: true, - }) - - // Configure JavaScript compiler options for JSX support - monacoRef.current.languages.typescript.javascriptDefaults.setCompilerOptions({ - jsx: monacoRef.current.languages.typescript.JsxEmit.React, - jsxFactory: 'React.createElement', - reactNamespace: 'React', - allowNonTsExtensions: true, - target: monacoRef.current.languages.typescript.ScriptTarget.Latest, - moduleResolution: monacoRef.current.languages.typescript.ModuleResolutionKind.NodeJs, - module: monacoRef.current.languages.typescript.ModuleKind.ESNext, - noEmit: true, - esModuleInterop: true, - allowSyntheticDefaultImports: true, - skipLibCheck: true, - resolveJsonModule: true, - isolatedModules: true, - checkJs: false, - }) - // Enable JSX diagnostics for JavaScript monacoRef.current.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ noSemanticValidation: false, From 6074f7fc760c62ad03d4051c1c960ed986b8b521 Mon Sep 17 00:00:00 2001 From: hsy822 Date: Thu, 27 Nov 2025 14:47:53 +0900 Subject: [PATCH 16/20] lint --- apps/remix-ide/src/app/editor/editor.js | 9 ++- apps/remix-ide/src/app/editor/type-fetcher.ts | 60 +++++++++---------- .../src/app/plugins/script-runner-bridge.tsx | 18 +++--- .../editor/src/lib/remix-ui-editor.tsx | 2 +- 4 files changed, 44 insertions(+), 45 deletions(-) diff --git a/apps/remix-ide/src/app/editor/editor.js b/apps/remix-ide/src/app/editor/editor.js index e99f8d5273d..1d2cfe95d42 100644 --- a/apps/remix-ide/src/app/editor/editor.js +++ b/apps/remix-ide/src/app/editor/editor.js @@ -1,6 +1,5 @@ 'use strict' import React from 'react' // eslint-disable-line -import { resolve } from 'path' import { EditorUI } from '@remix-ui/editor' // eslint-disable-line import { Plugin } from '@remixproject/engine' import * as packageJson from '../../../../../package.json' @@ -194,7 +193,7 @@ export default class Editor extends Plugin { }) try { this.currentThemeType = (await this.call('theme', 'currentTheme')).quality - } catch (e) {} + } catch (e) {} // eslint-disable-line no-empty this.renderComponent() } @@ -337,7 +336,7 @@ export default class Editor extends Plugin { try { const result = await startTypeLoadingProcess(packageToLoad) if (result && result.libs && result.libs.length > 0) { - // Add all fetched type files to Monaco. + // Add all fetched type files to Monaco. this.addExtraLibs(result.libs) // Update path mappings so TypeScript can find the types. @@ -362,16 +361,16 @@ export default class Editor extends Plugin { } } catch (e) { // Crawler can fail, but we don't want to crash the whole process. + this.processedPackages.delete(basePackage) console.error(`[DIAGNOSE-DEEP-PASS] Crawler failed for "${basePackage}":`, e) this.call('notification', 'toast', `Failed to load types for package: ${basePackage}.`) } })) - // After all type loading is complete, re-enable type checking and apply the final state. + // After all type loading is complete, re-enable type checking and apply the final state. this.endTypesBatch() } catch (error) { - this.processedPackages.delete(basePackage) console.error('[DIAGNOSE-ONCHANGE] Critical error during type loading process:', error) this.endTypesBatch() } diff --git a/apps/remix-ide/src/app/editor/type-fetcher.ts b/apps/remix-ide/src/app/editor/type-fetcher.ts index 234cb183601..e205938ea15 100644 --- a/apps/remix-ide/src/app/editor/type-fetcher.ts +++ b/apps/remix-ide/src/app/editor/type-fetcher.ts @@ -24,7 +24,7 @@ type ResolveResult = { finalUrl: string; content: string } * [Type Definition] * A cache map used to prevent duplicate network requests. * Key: The Request URL. - * Value: The Promise of the request result. This allows concurrent requests + * Value: The Promise of the request result. This allows concurrent requests * for the same URL to share the same Promise (Deduplication). */ type FetchCache = Map> @@ -86,13 +86,13 @@ function guessDtsFromJs(jsPath: string): string[] { } /** - * Parses 'exports', 'types', or 'typings' fields in package.json to map subpaths + * Parses 'exports', 'types', or 'typings' fields in package.json to map subpaths * to their corresponding entry point URLs. */ function buildExportTypeMap(pkgName: string, pkgJson: PackageJson): Record { const map: Record = {} const base = `${CDN_BASE}${pkgName}/` - + // Helper: Validates the path and adds it to the map. const push = (subpath: string, relPath: string | undefined) => { @@ -102,10 +102,10 @@ function buildExportTypeMap(pkgName: string, pkgJson: PackageJson): Record, + entryUrl: string, + pkgName: string, + visited: Set, fetchCache: FetchCache, enqueuePackage: (name: string) => void ): Promise { @@ -231,21 +231,21 @@ async function crawl( // Fetch content using cache const res = await tryFetchOne(urlsToTry, fetchCache) - if(!res) return [] - + if (!res) return [] + const { finalUrl, content } = res out.push({ filePath: toVirtual(finalUrl), content }) - + const subPromises: Promise[] = [] - + const crawlNext = (nextUrl: string) => { // Recurse only if not visited if (!visited.has(nextUrl)) subPromises.push(crawl(nextUrl, pkgName, visited, fetchCache, enqueuePackage)) } - + // 1. Parse Triple-slash references (/// ) for (const m of content.matchAll(TRIPLE_SLASH_REF_RE)) crawlNext(new URL(m[1], finalUrl).href) - + // 2. Parse Import/Export/Require statements for (const m of content.matchAll(IMPORT_ANY_RE)) { const spec = (m[1] || m[2] || m[3] || '').trim() @@ -280,14 +280,14 @@ export async function startTypeLoadingProcess(packageName: string): Promise<{ ma async function loadPackage(pkgNameToLoad: string) { if (visitedPackages.has(pkgNameToLoad)) return visitedPackages.add(pkgNameToLoad) - + let pkgJson: PackageJson let attemptedTypesFallback = false - + // Loop to handle the @types fallback strategy - while (true) { + while (true) { // eslint-disable-line no-constant-condition let currentPkgName = pkgNameToLoad - + // If the main package failed, try the @types scoped name if (attemptedTypesFallback) { currentPkgName = toTypesScopedName(pkgNameToLoad) @@ -311,12 +311,12 @@ export async function startTypeLoadingProcess(packageName: string): Promise<{ ma const pendingDependencies = new Set() const enqueuePackage = (p: string) => { if (!visitedPackages.has(p)) pendingDependencies.add(p) } - + const crawlPromises: Promise[] = [] for (const [subpath, urls] of Object.entries(exportMap)) { const entryPointUrl = urls[0] if (entryPointUrl) { - const pkgNameWithoutVersion = currentPkgName.replace(/@[\^~]?[\d\.\w-]+$/, '') + const pkgNameWithoutVersion = currentPkgName.replace(/@[\^~]?[\d.\w-]+$/, '') const virtualPathKey = subpath === '.' ? pkgNameWithoutVersion : `${pkgNameWithoutVersion}/${subpath.replace('./', '')}` subpathMap[virtualPathKey] = entryPointUrl.replace(CDN_BASE, '') @@ -328,10 +328,10 @@ export async function startTypeLoadingProcess(packageName: string): Promise<{ ma const libsArrays = await Promise.all(crawlPromises) let totalCollectedFiles = 0 libsArrays.forEach(libs => { - collected.push(...libs) - totalCollectedFiles += libs.length + collected.push(...libs) + totalCollectedFiles += libs.length }) - + // If package.json exists but no .d.ts files were found, try @types fallback if (totalCollectedFiles === 0 && !attemptedTypesFallback) { attemptedTypesFallback = true @@ -342,7 +342,7 @@ export async function startTypeLoadingProcess(packageName: string): Promise<{ ma if (pendingDependencies.size > 0) { await Promise.all(Array.from(pendingDependencies).map(loadPackage)) } - + return } catch (e) { 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 a6213765944..9067556fcdc 100644 --- a/apps/remix-ide/src/app/plugins/script-runner-bridge.tsx +++ b/apps/remix-ide/src/app/plugins/script-runner-bridge.tsx @@ -32,20 +32,20 @@ const customBuildUrl = 'http://localhost:4000/build' // this will be used when t * Transforms the provided script content to make it executable in a browser environment. * * Key Transformation Logic: * 1. Hybrid Import Handling: - * - Relative imports (starting with `.` or `/`) and libraries listed in `builtInDependencies` + * - Relative imports (starting with `.` or `/`) and libraries listed in `builtInDependencies` * are preserved as standard static ES imports (hoisted to the top). * - External NPM packages are converted into dynamic `await import(...)` calls fetching from `cdn.jsdelivr.net`. - * * 2. Multi-line Support: + * * 2. Multi-line Support: * - Uses an enhanced Regex (`[\s\S]*?`) to correctly parse import statements that span multiple lines. - * * 3. Async Wrapper: - * - Wraps the main execution logic (excluding static imports) in an `async IIFE` + * * 3. Async Wrapper: + * - Wraps the main execution logic (excluding static imports) in an `async IIFE` * to enable top-level await behavior for the dynamic imports. * * 4. Syntax Adjustments: * - Handles various import styles: Destructuring (`{ a }`), Namespace (`* as a`), and Default (`a`). * - Removes `export` keywords to prevent syntax errors within the IIFE context. * * @param scriptContent - The original source code of the script to be transformed. - * @param builtInDependencies - An array of package names that are pre-bundled or available in the runtime environment + * @param builtInDependencies - An array of package names that are pre-bundled or available in the runtime environment * (e.g., ['chai', 'web3']) and should not be fetched from the CDN. * @returns The transformed script string, ready for runtime evaluation. */ @@ -61,18 +61,18 @@ function transformScriptForRuntime(scriptContent: string, builtInDependencies: s staticImports.push(match) return '' } - + if (builtInDependencies.includes(packageName)) { staticImports.push(match) return '' } - + dynamicImports.push({ importClause, packageName }) return '' }) let finalScript = '' - + if (staticImports.length > 0) { finalScript += staticImports.join('\n') + '\n\n' } @@ -280,7 +280,7 @@ export class ScriptRunnerBridgePlugin extends Plugin { } try { this.setIsLoading(this.activeConfig.name, true) - // Transforms the script into an executable format using the function defined above. + // Transforms the script into an executable format using the function defined above. const builtInDependencies = this.activeConfig.dependencies ? this.activeConfig.dependencies.map(dep => dep.name) : [] const transformedScript = transformScriptForRuntime(script, builtInDependencies) 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 be03018313a..f6f60736236 100644 --- a/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx +++ b/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx @@ -32,7 +32,7 @@ import type { 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 || []) +const _paq = (window._paq = window._paq || []) // eslint-disable-line // Key for localStorage const HIDE_PASTE_WARNING_KEY = 'remixide.hide_paste_warning'; From d9772a30800133308a8ff73f0a66d1955ac944ea Mon Sep 17 00:00:00 2001 From: hsy822 Date: Thu, 27 Nov 2025 15:27:11 +0900 Subject: [PATCH 17/20] fix hardhat import --- apps/remix-ide/src/app/plugins/script-runner-bridge.tsx | 5 +++++ 1 file changed, 5 insertions(+) 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 9067556fcdc..74ad35a043e 100644 --- a/apps/remix-ide/src/app/plugins/script-runner-bridge.tsx +++ b/apps/remix-ide/src/app/plugins/script-runner-bridge.tsx @@ -67,6 +67,11 @@ function transformScriptForRuntime(scriptContent: string, builtInDependencies: s return '' } + if (packageName === 'hardhat') { + staticImports.push(match) + return '' + } + dynamicImports.push({ importClause, packageName }) return '' }) From 69362ed1860620cc9cce1d0df227e9584d3130ed Mon Sep 17 00:00:00 2001 From: hsy822 Date: Thu, 27 Nov 2025 15:36:10 +0900 Subject: [PATCH 18/20] lint --- apps/remix-ide/src/app/plugins/script-runner-bridge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 74ad35a043e..ceebad13461 100644 --- a/apps/remix-ide/src/app/plugins/script-runner-bridge.tsx +++ b/apps/remix-ide/src/app/plugins/script-runner-bridge.tsx @@ -71,7 +71,7 @@ function transformScriptForRuntime(scriptContent: string, builtInDependencies: s staticImports.push(match) return '' } - + dynamicImports.push({ importClause, packageName }) return '' }) From 9db69e1d142ba22f06d23e7c0ead40abc4ac1615 Mon Sep 17 00:00:00 2001 From: hsy822 Date: Thu, 27 Nov 2025 16:08:43 +0900 Subject: [PATCH 19/20] remove notification --- apps/remix-ide/src/app/editor/editor.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/remix-ide/src/app/editor/editor.js b/apps/remix-ide/src/app/editor/editor.js index 1d2cfe95d42..390125500ab 100644 --- a/apps/remix-ide/src/app/editor/editor.js +++ b/apps/remix-ide/src/app/editor/editor.js @@ -257,7 +257,6 @@ export default class Editor extends Plugin { if (this.typesLoadingCount === 0) { this.toggleTsDiagnostics(false) this.triggerEvent('typesLoading', ['start']) - this.call('notification', 'toast', 'Loading JS/TS type information...') } this.typesLoadingCount++ } @@ -268,7 +267,6 @@ export default class Editor extends Plugin { this.updateTsCompilerOptions() this.toggleTsDiagnostics(true) this.triggerEvent('typesLoading', ['end']) - this.call('notification', 'toast', 'JS/TS types loaded successfully.') } } From 1c0cd3cc0abbe46d4e784d5ecb284dac6af75756 Mon Sep 17 00:00:00 2001 From: hsy822 Date: Thu, 27 Nov 2025 16:39:39 +0900 Subject: [PATCH 20/20] rollback handleTypeScriptDependenciesOf --- apps/remix-ide/src/app/editor/editor.js | 65 +++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/apps/remix-ide/src/app/editor/editor.js b/apps/remix-ide/src/app/editor/editor.js index 390125500ab..d3475201f3e 100644 --- a/apps/remix-ide/src/app/editor/editor.js +++ b/apps/remix-ide/src/app/editor/editor.js @@ -1,5 +1,6 @@ 'use strict' import React from 'react' // eslint-disable-line +import { resolve } from 'path' import { EditorUI } from '@remix-ui/editor' // eslint-disable-line import { Plugin } from '@remixproject/engine' import * as packageJson from '../../../../../package.json' @@ -414,6 +415,70 @@ export default class Editor extends Plugin { return ext && this.modes[ext] ? this.modes[ext] : this.modes.txt } + async handleTypeScriptDependenciesOf (path, content, readFile, exists) { + const isTsFile = path.endsWith('.ts') || path.endsWith('.tsx') + const isJsFile = path.endsWith('.js') || path.endsWith('.jsx') + + if (isTsFile || isJsFile) { + // extract the import, resolve their content + // and add the imported files to Monaco through the `addModel` + // so Monaco can provide auto completion + const paths = path.split('/') + paths.pop() + const fromPath = paths.join('/') // get current execution context path + const language = isTsFile ? 'typescript' : 'javascript' + + for (const match of content.matchAll(/import\s+.*\s+from\s+(?:"(.*?)"|'(.*?)')/g)) { + let pathDep = match[2] + if (pathDep.startsWith('./') || pathDep.startsWith('../')) pathDep = resolve(fromPath, pathDep) + if (pathDep.startsWith('/')) pathDep = pathDep.substring(1) + + // Try different file extensions if no extension is provided + const extensions = isTsFile ? ['.ts', '.tsx', '.d.ts'] : ['.js', '.jsx'] + let hasExtension = false + for (const ext of extensions) { + if (pathDep.endsWith(ext)) { + hasExtension = true + break + } + } + + if (!hasExtension) { + // Try to find the file with different extensions + for (const ext of extensions) { + const pathWithExt = pathDep + ext + try { + const pathExists = await exists(pathWithExt) + if (pathExists) { + pathDep = pathWithExt + break + } + } catch (e) { + // continue to next extension + } + } + } + + try { + // we can't use the fileManager plugin call directly + // because it's itself called in a plugin context, and that causes a timeout in the plugin stack + const pathExists = await exists(pathDep) + let contentDep = '' + if (pathExists) { + contentDep = await readFile(pathDep) + if (contentDep !== '') { + this.emit('addModel', contentDep, language, pathDep, this.readOnlySessions[path]) + } + } else { + console.log("The file ", pathDep, " can't be found.") + } + } catch (e) { + console.log(e) + } + } + } + } + /** * Create an editor session * @param {string} path path of the file