-
-
Save nemesisqp/2f9bca3971fb3dc9e6425ea9baec1a71 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { VirtualTypeScriptEnvironment } from "@typescript/vfs"; | |
import { CompilerOptions } from "typescript"; | |
enum ModuleResolutionKind { | |
Classic = 1, | |
NodeJs = 2, | |
} | |
importScripts("https://unpkg.com/@typescript/[email protected]/dist/vfs.globals.js"); | |
importScripts( | |
"https://cdnjs.cloudflare.com/ajax/libs/typescript/4.4.3/typescript.min.js" | |
); | |
importScripts("https://unpkg.com/@okikio/[email protected]/lib/api.js"); | |
export type VFS = typeof import("@typescript/vfs"); | |
export type EVENT_EMITTER = import("@okikio/emitter").EventEmitter; | |
export type Diagnostic = import("@codemirror/lint").Diagnostic; | |
var { | |
createDefaultMapFromCDN, | |
createSystem, | |
createVirtualTypeScriptEnvironment, | |
} = globalThis.tsvfs as VFS; | |
var ts = globalThis.ts; // as TS | |
var EventEmitter = globalThis.emitter.EventEmitter; | |
var _emitter: EVENT_EMITTER = new EventEmitter(); | |
globalThis.localStorage = globalThis.localStorage ?? ({} as Storage); | |
const BUCKET_URL = "https://prod-packager-packages.codesandbox.io/v1/typings"; | |
const TYPES_REGISTRY = "https://unpkg.com/types-registry@latest/index.json"; | |
/** | |
* Fetch dependencies types from CodeSandbox CDN | |
*/ | |
const fetchDependencyTyping = async ({ | |
name, | |
version, | |
}: { | |
name: string; | |
version: string; | |
}): Promise<Record<string, { module: { code: string } }>> => { | |
try { | |
const url = `${BUCKET_URL}/${name}/${version}.json`; | |
const { files } = await fetch(url).then((data) => data.json()); | |
return files; | |
} catch { } | |
}; | |
/** | |
* Process the TS compile options or default to ES5 | |
*/ | |
const getCompileOptions = ( | |
tsconfigFile: Record<string, any> | |
): CompilerOptions => { | |
const defaultValue = { | |
target: ts.ScriptTarget.ES2020, | |
module: ts.ScriptTarget.ESNext, | |
allowJs: true, | |
allowSyntheticDefaultImports: true, | |
allowNonTsExtensions: true, | |
esModuleInterop: true, | |
moduleResolution: ModuleResolutionKind.NodeJs, | |
}; | |
// if (tsconfigFile.compilerOptions) { | |
// const { compilerOptions } = tsconfigFile; | |
// // Hard fixes | |
// if (compilerOptions.moduleResolution === "node") { | |
// compilerOptions.moduleResolution = ModuleResolutionKind.NodeJs; | |
// } | |
// return compilerOptions; | |
// } | |
return defaultValue; | |
}; | |
const processTypescriptCacheFromStorage = ( | |
fsMapCached: Map<string, string> | |
): Map<string, string> => { | |
const cache = new Map(); | |
const matchVersion = Array.from(fsMapCached.keys()).every((file) => | |
file.startsWith(`ts-lib-${ts.version}`) | |
); | |
if (!matchVersion) cache; | |
fsMapCached.forEach((value, key) => { | |
const cleanLibName = key.replace(`ts-lib-${ts.version}-`, ""); | |
cache.set(cleanLibName, value); | |
}); | |
return cache; | |
}; | |
const isValidTypeModule = (key: string, value?: { module: { code: string } }) => | |
key.endsWith(".d.ts") || | |
(key.endsWith("/package.json") && value?.module?.code); | |
/** | |
* Main worker function | |
*/ | |
(async function lspTypescriptWorker() { | |
let env: VirtualTypeScriptEnvironment; | |
postMessage({ | |
event: "ready", | |
details: [], | |
}); | |
const createTsSystem = async ( | |
files: Record<string, { code: string }>, | |
entry: string, | |
fsMapCached: Map<string, string> | |
) => { | |
const tsFiles = new Map(); | |
const rootPaths = []; | |
const dependenciesMap = new Map(); | |
let tsconfig = null; | |
let packageJson = null; | |
let typeVersionsFromRegistry: Record<string, { latest: string }>; | |
/** | |
* Collect files | |
*/ | |
for (const filePath in files) { | |
const content = files[filePath].code; | |
// TODO: normalize path | |
if (filePath === "tsconfig.json" || filePath === "/tsconfig.json") { | |
tsconfig = content; | |
} else if (filePath === "package.json" || filePath === "/package.json") { | |
packageJson = content; | |
} else if (/^[^.]+.(tsx?|jsx?)$/.test(filePath) || filePath.endsWith(".test.tsx") || filePath.endsWith(".test.ts") || filePath.endsWith(".test.js") || filePath.endsWith(".test.jsx")) { | |
tsFiles.set(filePath, content); | |
rootPaths.push(filePath); | |
} | |
} | |
const compilerOpts = getCompileOptions(JSON.parse(tsconfig)); | |
/** | |
* Process cache or get a fresh one | |
*/ | |
let fsMap = processTypescriptCacheFromStorage(fsMapCached); | |
if (fsMap.size === 0) { | |
fsMap = await createDefaultMapFromCDN( | |
compilerOpts, | |
ts.version, | |
false, | |
ts | |
); | |
} | |
/** | |
* Post CDN payload to cache in the browser storage | |
*/ | |
postMessage({ | |
event: "cache-typescript-fsmap", | |
details: { fsMap, version: ts.version }, | |
}); | |
/** | |
* Add local files to the file-system | |
*/ | |
tsFiles.forEach((content, filePath) => { | |
fsMap.set(filePath, content); | |
}); | |
/** | |
* Get dependencies from package.json | |
*/ | |
const { dependencies, devDependencies } = JSON.parse(packageJson); | |
for (const dep in devDependencies ?? {}) { | |
dependenciesMap.set(dep, devDependencies[dep]); | |
} | |
for (const dep in dependencies ?? {}) { | |
// Avoid redundant requests | |
if (!dependenciesMap.has(`@types/${dep}`)) { | |
dependenciesMap.set(dep, dependencies[dep]); | |
} | |
} | |
/** | |
* Fetch dependencies types | |
*/ | |
dependenciesMap.forEach(async (version, name) => { | |
// 1. CodeSandbox CDN | |
const files = await fetchDependencyTyping({ name, version }); | |
const hasTypes = Object.keys(files).some( | |
(key) => key.startsWith("/" + name) && key.endsWith(".d.ts") | |
); | |
// 2. Types found | |
if (hasTypes) { | |
Object.entries(files).forEach(([key, value]) => { | |
if (isValidTypeModule(key, value)) { | |
fsMap.set(`/node_modules${key}`, value.module.code); | |
} | |
}); | |
return; | |
} | |
// 3. Types found: fetch types version from registry | |
if (!typeVersionsFromRegistry) { | |
typeVersionsFromRegistry = await fetch(TYPES_REGISTRY) | |
.then((data) => data.json()) | |
.then((data) => data.entries); | |
} | |
// 4. Types found: no Look for types in @types register | |
const typingName = `@types/${name}`; | |
if (typeVersionsFromRegistry[name]) { | |
const atTypeFiles = await fetchDependencyTyping({ | |
name: typingName, | |
version: typeVersionsFromRegistry[name].latest, | |
}); | |
Object.entries(atTypeFiles).forEach(([key, value]) => { | |
if (isValidTypeModule(key, value)) { | |
fsMap.set(`/node_modules${key}`, value.module.code); | |
} | |
}); | |
} | |
}); | |
const system = createSystem(fsMap); | |
env = createVirtualTypeScriptEnvironment( | |
system, | |
rootPaths, | |
ts, | |
compilerOpts | |
); | |
lintSystem(entry); | |
}; | |
const updateFile = (filePath: string, content: string) => { | |
if (env) { | |
env.updateFile(filePath, content); | |
} | |
}; | |
const autocompleteAtPosition = (pos: number, filePath: string) => { | |
let result = env.languageService.getCompletionsAtPosition( | |
filePath, | |
pos, | |
{} | |
); | |
postMessage({ | |
event: "autocomplete-results", | |
details: result, | |
}); | |
}; | |
const infoAtPosition = (pos: number, filePath: string) => { | |
let result = env.languageService.getQuickInfoAtPosition(filePath, pos); | |
postMessage({ | |
event: "tooltip-results", | |
details: result | |
? { | |
result, | |
tootltipText: | |
ts.displayPartsToString(result.displayParts) + | |
(result.documentation?.length | |
? "\n" + ts.displayPartsToString(result.documentation) | |
: ""), | |
} | |
: { result, tooltipText: "" }, | |
}); | |
}; | |
const lintSystem = (filePath: string) => { | |
if (!env) return; | |
let SyntacticDiagnostics = []; | |
let SemanticDiagnostic = []; | |
let SuggestionDiagnostics = []; | |
try { | |
SyntacticDiagnostics = | |
env.languageService.getSyntacticDiagnostics(filePath); | |
} catch (error) { | |
console.log(error); | |
} | |
try { | |
SemanticDiagnostic = | |
env.languageService.getSemanticDiagnostics(filePath); | |
} catch (error) { | |
console.log(error); | |
} | |
try { | |
SuggestionDiagnostics = | |
env.languageService.getSuggestionDiagnostics(filePath); | |
} catch (error) { | |
console.log(error); | |
} | |
type Diagnostics = typeof SyntacticDiagnostics & | |
typeof SemanticDiagnostic & | |
typeof SuggestionDiagnostics; | |
let result: Diagnostics = [].concat( | |
SyntacticDiagnostics, | |
SemanticDiagnostic, | |
SuggestionDiagnostics | |
); | |
postMessage({ | |
event: "lint-results", | |
details: result.reduce((acc, result) => { | |
const from = result.start; | |
const to = result.start + result.length; | |
const codeActions = env.languageService.getCodeFixesAtPosition( | |
filePath, | |
from, | |
to, | |
[result.category], | |
{}, | |
{} | |
); | |
type ErrorMessageObj = { | |
messageText: string; | |
next?: ErrorMessageObj[]; | |
}; | |
type ErrorMessage = ErrorMessageObj | string; | |
const messagesErrors = (message: ErrorMessage): string[] => { | |
if (typeof message === "string") return [message]; | |
const messageList = []; | |
const getMessage = (loop: ErrorMessageObj) => { | |
messageList.push(loop.messageText); | |
if (loop.next) { | |
loop.next.forEach((item) => { | |
getMessage(item); | |
}); | |
} | |
}; | |
getMessage(message); | |
return messageList; | |
}; | |
const severity: Diagnostic["severity"][] = [ | |
"warning", | |
"error", | |
"info", | |
"info", | |
]; | |
messagesErrors(result.messageText).forEach((message) => { | |
if (message.includes("Cannot find module")) return; // Ignore module not found | |
if (message.includes("implicitly has")) return; // ignore | |
acc.push({ | |
from, | |
to, | |
message, | |
source: result?.source, | |
severity: severity[result.category], | |
actions: codeActions as any as Diagnostic["actions"] | |
}); | |
}); | |
return acc; | |
}, [] as Diagnostic[]), | |
}); | |
}; | |
/** | |
* Listeners | |
*/ | |
_emitter.once( | |
"create-system", | |
async (payload: { | |
files: Record<string, { code: string }>; | |
entry: string; | |
fsMapCached: Map<string, string>; | |
}) => { | |
createTsSystem(payload.files, payload.entry, payload.fsMapCached); | |
} | |
); | |
_emitter.on("lint-request", (payload: { filePath: string }) => | |
lintSystem(payload.filePath) | |
); | |
_emitter.on("updateText", (payload: { filePath: string; content: string }) => | |
updateFile(payload.filePath, payload.content) | |
); | |
_emitter.on( | |
"autocomplete-request", | |
(payload: { pos: number; filePath: string }) => { | |
autocompleteAtPosition(payload.pos, payload.filePath); | |
} | |
); | |
_emitter.on( | |
"tooltip-request", | |
(payload: { pos: number; filePath: string }) => { | |
infoAtPosition(payload.pos, payload.filePath); | |
} | |
); | |
})(); | |
addEventListener( | |
"message", | |
({ data }: MessageEvent<{ event: string; details: any }>) => { | |
let { event, details } = data; | |
_emitter.emit(event, details); | |
} | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment