Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 222 additions & 16 deletions apps/remix-ide/src/app/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { Plugin } from '@remixproject/engine'
import * as packageJson from '../../../../../package.json'
import { PluginViewWrapper } from '@remix-ui/helper'

import { startTypeLoadingProcess } from './type-fetcher'

const EventManager = require('../../lib/events')

const profile = {
Expand Down Expand Up @@ -71,12 +73,26 @@ export default class Editor extends Plugin {
this.api = {}
this.dispatch = null
this.ref = null

this.monaco = null
this.typeLoaderDebounce = null

this.tsModuleMappings = {}
this.processedPackages = new Set()

this.typesLoadingCount = 0
this.shimDisposers = new Map()
this.pendingPackagesBatch = new Set()
}

setDispatch (dispatch) {
this.dispatch = dispatch
}

setMonaco (monaco) {
this.monaco = monaco
}

updateComponent(state) {
return <EditorUI
editorAPI={state.api}
Expand All @@ -86,6 +102,7 @@ export default class Editor extends Plugin {
events={state.events}
plugin={state.plugin}
isDiff={state.isDiff}
setMonaco={(monaco) => this.setMonaco(monaco)}
/>
}

Expand Down Expand Up @@ -128,6 +145,26 @@ export default class Editor extends Plugin {

async onActivation () {
this.activated = true
this.on('editor', 'editorMounted', () => {
if (!this.monaco) return
const ts = this.monaco.languages.typescript
const tsDefaults = ts.typescriptDefaults

tsDefaults.setCompilerOptions({
moduleResolution: ts.ModuleResolutionKind.NodeNext,
module: ts.ModuleKind.NodeNext,
target: ts.ScriptTarget.ES2022,
lib: ['es2022', 'dom', 'dom.iterable'],
allowNonTsExtensions: true,
allowSyntheticDefaultImports: true,
skipLibCheck: true,
baseUrl: 'file:///node_modules/',
paths: this.tsModuleMappings,
})
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')
this.keepDecorationsFor(name, 'markerPerFile')
Expand Down Expand Up @@ -156,27 +193,196 @@ export default class Editor extends Plugin {
this.off('sidePanel', 'pluginDisabled')
}

async _onChange (file) {
this.triggerEvent('didChangeFile', [file])
const currentFile = await this.call('fileManager', 'file')
if (!currentFile) {
return
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()

tsDefaults.setCompilerOptions({
...currentOptions,
paths: { ...currentOptions.paths, ...this.tsModuleMappings }
})
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 (currentFile !== file) {
return

if (!this.shimDisposers.has(shimWildPath)) {
const d2 = tsDefaults.addExtraLib(`declare module '${pkg}/*' { const _default: any\nexport = _default }`, shimWildPath)
this.shimDisposers.set(shimWildPath, d2)
}
const input = this.get(currentFile)
if (!input) {
return

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)
}
}
// if there's no change, don't do anything
if (input === this.previousInput) {
return
}

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...`)

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}.`)
}

// Called on every editor content change to parse import statements and trigger type loading.
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 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(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 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)

// 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)
})

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)

console.log(`[DIAGNOSE-DEEP-PASS] Starting deep pass for "${basePackage}"`)
try {
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]
}
}
if (result.mainVirtualPath) {
this.tsModuleMappings[basePackage] = [result.mainVirtualPath.replace('file:///node_modules/', '')]
}
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) {
console.error('[DIAGNOSE-ONCHANGE] Critical error during type loading process:', error)
this.endTypesBatch()
}
}, 1500)
}

const currentFile = await this.call('fileManager', 'file')
if (!currentFile || currentFile !== file) return

const input = this.get(currentFile)
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)
}
Expand Down Expand Up @@ -232,7 +438,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)
Expand Down
Loading