-
Notifications
You must be signed in to change notification settings - Fork 62
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add streaming type definitions #121
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3055,4 +3055,4 @@ | |
}, | ||
"jsx-tag-attributes-illegal": { "name": "invalid.illegal.attribute.tsx", "match": "\\S+" } | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1075,4 +1075,4 @@ | |
] | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
import { editor, languages, Uri } from 'monaco-editor'; | ||
import { resolve } from 'resolve.exports'; | ||
|
||
const UNPKG = 'https://unpkg.com'; | ||
|
||
interface PackageJSON { | ||
types?: string; | ||
typings?: string; | ||
} | ||
|
||
const GLOBAL_CACHE = new Set(); | ||
|
||
function matchURLS(str: string): string[] { | ||
// Find all "from" expression | ||
const fromMatches = str.match(/from ((".*")|('.*'))/g) ?? []; | ||
// Find all "dynamic import" expression | ||
const importMatches = str.match(/import\(((".*")|('.*'))\)/g) ?? []; | ||
|
||
const matches = [ | ||
...fromMatches.map((item) => item.replace('from ', '')), | ||
...importMatches | ||
.map((item) => item.replace('import', '')) | ||
.map((item) => item.substring(1, item.length - 1)), | ||
].map((item) => item.substring(1, item.length - 1)); | ||
|
||
return matches; | ||
} | ||
|
||
function getPackageName(source: string) { | ||
const pathname = source.split('/'); | ||
if (source.startsWith('@')) { | ||
return `${pathname[0]}/${pathname[1]}`; | ||
} | ||
return pathname[0]; | ||
} | ||
|
||
function getTypes(packageName: string) { | ||
// TODO consider namespaced packages | ||
return `@types/${packageName}`; | ||
} | ||
|
||
function resolveTypings(pkg: PackageJSON, entry: string, isSubpackage = false) { | ||
if ('exports' in pkg) { | ||
const result = resolve(pkg, entry, { | ||
unsafe: true, | ||
conditions: ['types'], | ||
}) ?? resolve(pkg, entry, { | ||
unsafe: true, | ||
conditions: ['typings'], | ||
}); | ||
if (result) { | ||
return result; | ||
} | ||
} | ||
if (!isSubpackage) { | ||
return pkg.types ?? pkg.typings | ||
} | ||
return undefined; | ||
} | ||
|
||
function addDefinition( | ||
// Content of the file | ||
content: string, | ||
// Path to file | ||
uri: string, | ||
// File type | ||
type: string, | ||
) { | ||
languages.typescript.typescriptDefaults.addExtraLib( | ||
content, | ||
uri, | ||
); | ||
|
||
editor.createModel( | ||
content, | ||
type, | ||
Uri.parse(uri), | ||
); | ||
} | ||
|
||
const DTS_CACHE = new Set(); | ||
|
||
class DefLoader { | ||
static async loadTSFile(source: string) { | ||
// this.loadDTS(`${source}.ts`); | ||
this.loadDTS(`${source}.d.ts`); | ||
} | ||
|
||
static async loadDTS( | ||
source: string, | ||
) { | ||
if (DTS_CACHE.has(source)) { | ||
return; | ||
} | ||
DTS_CACHE.add(source); | ||
const targetPath = new URL(source, UNPKG); | ||
const response = await fetch(targetPath); | ||
if (response.ok) { | ||
const dts = await response.text(); | ||
|
||
addDefinition(dts, `file:///node_modules/${source}`, 'typescript'); | ||
|
||
const imports = matchURLS(dts) ?? []; | ||
|
||
const splitPath = source.split('/'); | ||
const directory = splitPath.slice(0, -1).join('/'); | ||
|
||
await Promise.all(imports.map((item) => { | ||
if (item) { | ||
if (item.startsWith('./') || item.startsWith('../')) { | ||
const clean = item.endsWith('.js') ? item.substring(0, item.length - 3) : item; | ||
const resolved = new URL(`${directory}/${clean}`, 'file://').pathname.substring(1); | ||
this.loadTSFile(resolved); | ||
this.loadTSFile(`${resolved}/index`); | ||
} else { | ||
this.loadPackage(item); | ||
} | ||
} | ||
})); | ||
} | ||
} | ||
|
||
static async loadPackage( | ||
// The import URL | ||
source: string, | ||
// referral URL | ||
original = source, | ||
) { | ||
if (GLOBAL_CACHE.has(source)) { | ||
return; | ||
} | ||
GLOBAL_CACHE.add(source); | ||
const packageName = getPackageName(source); | ||
// Get the package.json | ||
const targetUnpkg = new URL(packageName, UNPKG); | ||
const response = await fetch(`${targetUnpkg}/package.json`); | ||
const pkg = await response.json() as PackageJSON; | ||
if (packageName !== source) { | ||
// Attempt to resolve types | ||
const typeDeclarations = resolveTypings(pkg, source, true); | ||
if (typeDeclarations) { | ||
await this.loadDTS(`${packageName}/${typeDeclarations}`); | ||
} else { | ||
this.loadPackage(packageName); | ||
} | ||
} else { | ||
// Check for `types` or `typings` | ||
const typeDeclarations = resolveTypings(pkg, packageName); | ||
if (typeDeclarations) { | ||
addDefinition(JSON.stringify(pkg), `file:///node_modules/${original}/package.json`, 'json'); | ||
await this.loadDTS(`${packageName}/${typeDeclarations}`); | ||
return; | ||
} | ||
await this.loadPackage(getTypes(packageName), original); | ||
} | ||
} | ||
} | ||
|
||
export default async function loadDefinitions( | ||
source: string, | ||
): Promise<void> { | ||
const imports = matchURLS(source) ?? []; | ||
|
||
await Promise.all(imports.map((item) => { | ||
if (item && !item.startsWith('.')) { | ||
DefLoader.loadPackage(item); | ||
} | ||
})); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { Registry } from 'monaco-textmate'; | ||
import { wireTmGrammars } from 'monaco-editor-textmate'; | ||
import * as monaco from 'monaco-editor'; | ||
import cssDefinition from './languages/css.tmLanguage.json?url'; | ||
import tsxDefinition from './languages/TypeScriptReact.tmLanguage.json?url'; | ||
|
||
const grammars = new Map(); | ||
grammars.set('css', 'source.css'); | ||
// grammars.set('javascript', 'source.js'); | ||
grammars.set('javascript', 'source.js.jsx'); | ||
// grammars.set('jsx', 'source.js.jsx'); | ||
// grammars.set('tsx', 'source.tsx'); | ||
// grammars.set('typescript', 'source.ts'); | ||
grammars.set('typescript', 'source.tsx'); | ||
|
||
const inverseGrammars: Record<string, string> = { | ||
'source.css': 'css', | ||
'source.js': 'jsx', | ||
// 'source.js': 'javascript', | ||
'source.js.jsx': 'jsx', | ||
'source.tsx': 'tsx', | ||
// 'source.ts': 'typescript', | ||
}; | ||
|
||
|
||
function createRegistry(): Registry { | ||
return new Registry({ | ||
getGrammarDefinition: async (scopeName) => { | ||
console.log(scopeName); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah I forgot to remove this 🤣 |
||
switch (inverseGrammars[scopeName]) { | ||
case 'css': | ||
return { | ||
format: 'json', | ||
content: await (await fetch(cssDefinition)).text(), | ||
}; | ||
case 'jsx': | ||
case 'typescript': | ||
case 'javascript': | ||
case 'tsx': | ||
default: | ||
return { | ||
format: 'json', | ||
content: await (await fetch(tsxDefinition)).text(), | ||
}; | ||
} | ||
}, | ||
}); | ||
} | ||
|
||
let LOADED = false; | ||
|
||
export default async function setupLanguages( | ||
editor: monaco.editor.ICodeEditor, | ||
): Promise<void> { | ||
if (LOADED) { | ||
return; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this changes semantics... I thought we needed to wireTmGrammars to every new editor instance (i.e. toggling output code tab) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't recall having to wire it all the time, but if it does cause issues then we can remove the guard |
||
} | ||
LOADED = true; | ||
await wireTmGrammars( | ||
monaco, | ||
createRegistry(), | ||
grammars, | ||
editor, | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
|
||
import { loadWASM } from 'onigasm'; | ||
import onigasm from 'onigasm/lib/onigasm.wasm?url'; | ||
import './setupThemes'; | ||
import './setupTypescript'; | ||
|
||
let LOADED = false; | ||
|
||
export default async function setupMonaco(): Promise<void> { | ||
if (!LOADED) { | ||
LOADED = true; | ||
await loadWASM(onigasm); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this used? Edit: nvm, I see above, but why not just make 'javascript' also map to source.tsx?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TBF we can just remove it. jsx and tsx has different grammars.