website/_webpack/js/tryFlow.js (489 lines of code) (raw):
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// defines window.requirejs
import './require_2_3_3';
import * as LZString from 'lz-string';
import {INITIAL, Registry, parseRawGrammar} from 'vscode-textmate';
import {createOnigScanner, createOnigString, loadWASM} from 'vscode-oniguruma';
import {load as initFlowLocally} from './flow-loader';
import THEME from './light_vs';
requirejs.config({
baseUrl: '/assets',
waitSeconds: 30,
paths: {
'vs': 'https://unpkg.com/monaco-editor@0.26.1/min/vs',
}
});
function appendMsg(container, msg, editor) {
const clickHandler = (msg) => {
editor.getDoc().setSelection(
{line: msg.loc.start.line - 1, ch: msg.loc.start.column - 1},
{line: msg.loc.end.line - 1, ch: msg.loc.end.column}
);
editor.focus();
};
if (msg.loc && msg.context != null) {
const div = document.createElement('div');
const basename = msg.loc.source.replace(/.*\//, '');
const filename = basename !== '-' ? `${msg.loc.source}:` : '';
const prefix = `${filename}${msg.loc.start.line}: `;
const before = msg.context.slice(0, msg.loc.start.column - 1);
const highlight = (msg.loc.start.line === msg.loc.end.line) ?
msg.context.slice(msg.loc.start.column - 1, msg.loc.end.column) :
msg.context.slice(msg.loc.start.column - 1);
const after = (msg.loc.start.line === msg.loc.end.line) ?
msg.context.slice(msg.loc.end.column) :
'';
div.appendChild(document.createTextNode(prefix + before));
const bold = document.createElement('strong');
bold.className = "msgHighlight";
bold.appendChild(document.createTextNode(highlight));
div.appendChild(bold);
div.appendChild(document.createTextNode(after));
container.appendChild(div);
const offset = msg.loc.start.column + prefix.length - 1;
const arrow = `${(prefix + before).replace(/[^ ]/g, ' ')}^ `;
container.appendChild(document.createTextNode(arrow));
const span = document.createElement('span');
span.className = "msgType";
span.appendChild(document.createTextNode(msg.descr));
container.appendChild(span);
const handler = clickHandler.bind(null, msg);
bold.addEventListener('click', handler);
span.addEventListener('click', handler);
} else if (msg.type === "Comment") {
const descr = `. ${msg.descr}\n`;
container.appendChild(document.createTextNode(descr));
} else {
const descr = `${msg.descr}\n`;
container.appendChild(document.createTextNode(descr));
}
};
function printExtra(info, editor) {
const list = document.createElement('ul');
if (info.message) {
const li = document.createElement('li');
info.message.forEach(msg => appendMsg(li, msg, editor));
list.appendChild(li);
}
if (info.children) {
const li = document.createElement('li');
info.children.forEach(info => {
li.appendChild(printExtra(info, editor));
});
list.appendChild(li);
}
return list;
}
function printError(err, editor) {
const li = document.createElement('li');
err.message.forEach(msg => appendMsg(li, msg, editor));
if (err.extra) {
err.extra.forEach(info => {
li.appendChild(printExtra(info, editor));
});
}
return li;
}
function printErrors(errors, editor) {
const list = document.createElement('ul');
errors.forEach(err => {
list.appendChild(printError(err, editor));
});
return list;
}
function removeChildren(node) {
while (node.lastChild) node.removeChild(node.lastChild);
}
function asSeverity(severity) {
switch (severity) {
case 'error':
return monaco.MarkerSeverity.Error;
case 'warning':
return monaco.MarkerSeverity.Warning;
default:
return monaco.MarkerSeverity.Hint;
}
}
function validate(flowProxy, model, callback) {
Promise.resolve(flowProxy)
.then(flowProxy => flowProxy.checkContent(model.uri.fsPath, model.getValue()))
.then(errors => {
const markers = errors.map(err => {
var messages = err.message;
var firstLoc = messages[0].loc;
var message = messages.map(msg => msg.descr).join("\n");
return {
// the code is also in the message, so don't also include it here.
// but if we fixed the message, we'd do this:
// code: Array.isArray(err.error_codes) ? err.error_codes[0] : undefined,
severity: asSeverity(err.level),
message: message,
source: firstLoc.source,
startLineNumber: firstLoc.start.line,
startColumn: firstLoc.start.column,
endLineNumber: firstLoc.end.line,
endColumn: firstLoc.end.column + 1,
// TODO: show references
// relatedInformation: ...
};
});
monaco.editor.setModelMarkers(model, 'default', markers);
if (callback != null) {
callback(errors);
}
});
}
const lastEditorValue = localStorage.getItem('tryFlowLastContent');
const defaultValue = (lastEditorValue && getHashedValue(lastEditorValue)) || `/* @flow */
function foo(x: ?number): string {
if (x) {
return x;
}
return "default string";
}
`;
function getHashedValue(hash) {
if (hash[0] !== '#' || hash.length < 2) return null;
const version = hash.slice(1, 2);
const encoded = hash.slice(2);
if (version === '0' && encoded.match(/^[a-zA-Z0-9+/=_-]+$/)) {
return LZString.decompressFromEncodedURIComponent(encoded);
}
return null;
}
function removeClass(elem, className) {
elem.className = elem.className.split(/\s+/).filter(function(name) {
return name !== className;
}).join(' ');
}
class Deferred {
constructor() {
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
}
const workerRegistry = {}
class FlowWorker {
constructor(version) {
this._version = version;
this._pending = {};
this._index = 0;
const worker = this._worker = new Worker(window.tryFlowWorker);
worker.onmessage = ({data}) => {
if (data.id && this._pending[data.id]) {
if (data.err) {
this._pending[data.id].reject(data.err);
} else {
this._pending[data.id].resolve(data.result);
}
delete this._pending[data.id];
}
};
worker.onerror = function() {
console.log('There is an error with your worker!');
};
// keep a reference to the worker, so that it doesn't get GC'd and killed.
workerRegistry[version] = worker;
}
send(data) {
const id = ++this._index;
const version = this._version;
this._pending[id] = new Deferred();
this._worker.postMessage({ id, version, ...data });
return this._pending[id].promise;
}
}
function initFlowWorker(version) {
const worker = new FlowWorker(version);
return worker.send({ type: 'init' }).then(() => worker);
}
class AsyncLocalFlow {
constructor(flow) {
this._flow = flow;
}
checkContent(filename, body) {
return Promise.resolve(this._flow.checkContent(filename, body));
}
typeAtPos(filename, body, line, col) {
return Promise.resolve(this._flow.typeAtPos(filename, body, line, col));
}
supportsParse() {
return Promise.resolve(this._flow.parse != null);
}
parse(body, options) {
return Promise.resolve(this._flow.parse(body, options));
}
}
class AsyncWorkerFlow {
constructor(worker) {
this._worker = worker;
}
checkContent(filename, body) {
return this._worker.send({ type: 'checkContent', filename, body });
}
typeAtPos(filename, body, line, col) {
return this._worker.send({ type: 'typeAtPos', filename, body, line, col });
}
supportsParse() {
return this._worker.send({ type: 'supportsParse' });
}
parse(body, options) {
return this._worker.send({ type: 'parse', body, options });
}
}
function initFlow(version) {
const useWorker = localStorage.getItem('tryFlowUseWorker');
if (useWorker === 'true') {
return initFlowWorker(version).then((flow) => new AsyncWorkerFlow(flow));
} else {
return initFlowLocally(version).then((flow) => new AsyncLocalFlow(flow));
}
}
const grammars = {
'source.js': '/static/syntaxes/flow-grammar.json',
'source.regexp.flow': '/static/syntaxes/flow-regex-grammar.json',
};
const registry =
import('vscode-oniguruma/release/onig.wasm')
.then(wasmModule => fetch(wasmModule.default))
// manually convert to an ArrayBuffer because Jekyll 3.x doesn't
// support serving .wasm as application/wasm via `jekyll serve`.
// Fixed in Jekyll 4
.then(response => response.arrayBuffer())
.then(data => {
loadWASM(data);
}).then(() => {
return new Registry({
onigLib: Promise.resolve({
createOnigScanner,
createOnigString,
}),
loadGrammar: (scopeName) => {
if (grammars.hasOwnProperty(scopeName)) {
const url = grammars[scopeName];
return fetch(url).then(response => response.json());
}
console.error(`Unknown scope name: ${scopeName}`);
return null;
},
theme: THEME
});
});
function createTokensProvider(languageId) {
return (
registry
.then(registry => registry.loadGrammarWithConfiguration('source.js', languageId, {}))
.then(grammar => {
if (grammar == null) {
throw Error(`no grammar for ${scopeName}`);
}
return {
getInitialState() {
return INITIAL;
},
tokenizeEncoded(line, state) {
const tokenizeLineResult2 = grammar.tokenizeLine2(line, state);
const endState = tokenizeLineResult2.ruleStack;
const {tokens} = tokenizeLineResult2;
return {tokens, endState};
},
};
})
);
}
export function createEditor(
flowVersion,
domNode,
resultsNode,
flowVersions
) {
const state = {flow: initFlow(flowVersion)};
requirejs(["vs/editor/editor.main"], function() {
const location = window.location;
state.flow.then(function() {
removeClass(resultsNode, 'show-loading');
});
const headNode = document.getElementsByTagName('head')[0];
const styles = document.createElement('style');
styles.type = 'text/css';
styles.media = 'screen';
headNode.appendChild(styles);
const errorsTabNode = document.createElement('li');
errorsTabNode.className = "tab errors-tab";
errorsTabNode.appendChild(document.createTextNode('Errors'));
errorsTabNode.addEventListener('click', function(evt) {
removeClass(resultsNode, 'show-json');
removeClass(resultsNode, 'show-ast');
resultsNode.className += ' show-errors';
evt.preventDefault();
});
const jsonTabNode = document.createElement('li');
jsonTabNode.className = "tab json-tab";
jsonTabNode.appendChild(document.createTextNode('JSON'));
jsonTabNode.addEventListener('click', function(evt) {
removeClass(resultsNode, 'show-errors');
removeClass(resultsNode, 'show-ast');
resultsNode.className += ' show-json';
evt.preventDefault();
});
const astTabNode = document.createElement('li');
astTabNode.className = "tab ast-tab";
astTabNode.appendChild(document.createTextNode('AST'));
astTabNode.addEventListener('click', function(evt) {
removeClass(resultsNode, 'show-errors');
removeClass(resultsNode, 'show-json');
resultsNode.className += ' show-ast';
evt.preventDefault();
});
const versionSelector = document.createElement('select');
flowVersions.forEach(
function(version) {
const option = document.createElement('option');
option.value = version;
option.text = version;
option.selected = version == flowVersion;
versionSelector.add(option, null);
}
);
const versionTabNode = document.createElement('li');
versionTabNode.className = "version";
versionTabNode.appendChild(versionSelector);
const toolbarNode = document.createElement('ul');
toolbarNode.className = "toolbar";
toolbarNode.appendChild(errorsTabNode);
toolbarNode.appendChild(jsonTabNode);
toolbarNode.appendChild(astTabNode);
toolbarNode.appendChild(versionTabNode);
const errorsNode = document.createElement('pre');
errorsNode.className = "errors";
const jsonNode = document.createElement('pre');
jsonNode.className = "json";
const astNode = document.createElement('pre');
astNode.className = "ast";
resultsNode.appendChild(toolbarNode);
resultsNode.appendChild(errorsNode);
resultsNode.appendChild(jsonNode);
resultsNode.appendChild(astNode);
resultsNode.className += " show-errors";
const cursorPositionNode = document.querySelector('footer .cursor-position');
const typeAtPosNode = document.querySelector('footer .type-at-pos');
function onFlowErrors(errors) {
if (errorsNode) {
if (errors.length) {
removeChildren(errorsNode);
errorsNode.appendChild(printErrors(errors, editor));
} else {
errorsNode.innerText = 'No errors!';
}
}
if (jsonNode) {
removeChildren(jsonNode);
jsonNode.appendChild(
document.createTextNode(JSON.stringify(errors, null, 2))
);
}
if (astNode) {
state.flow
.then((flowProxy) => {
flowProxy.supportsParse()
.then(supportsParse => {
if (supportsParse) {
const options = {
esproposal_class_instance_fields: true,
esproposal_class_static_fields: true,
esproposal_decorators: true,
esproposal_export_star_as: true,
esproposal_optional_chaining: true,
esproposal_nullish_coalescing: true,
types: true,
};
flowProxy.parse(editor.getValue(), options).then(ast => {
removeChildren(astNode);
astNode.appendChild(
document.createTextNode(JSON.stringify(ast, null, 2))
);
astNode.dataset.disabled = "false";
});
} else if (astNode.dataset.disabled !== "true") {
astNode.dataset.disabled = "true";
removeChildren(astNode);
astNode.appendChild(
document.createTextNode(
"AST output is not supported in this version of Flow."
)
);
}
})
});
}
}
monaco.languages.register({
id: 'flow',
extensions: ['.js', '.flow'],
aliases: ['Flow'],
});
fetch('/static/syntaxes/flow-configuration.json')
.then(response => response.json())
.then(config => monaco.languages.setLanguageConfiguration('flow', config));
const languageId = monaco.languages.getEncodedLanguageId('flow');
monaco.languages.setTokensProvider('flow', createTokensProvider(languageId));
registry.then(registry => {
const colors = registry.getColorMap();
styles.innerHTML = generateTokensCSSForColorMap(colors);
});
const model = monaco.editor.createModel(
getHashedValue(location.hash) || defaultValue,
'flow',
monaco.Uri.file('-'),
);
const editor = monaco.editor.create(domNode, {
model: model,
minimap: { enabled: false },
scrollBeyondLastLine: false,
overviewRulerBorder: false,
theme: 'vs-light',
});
// typecheck on load
validate(state.flow, model, onFlowErrors);
model.onDidChangeContent(() => {
const value = model.getValue();
// typecheck on edit
validate(state.flow, model, onFlowErrors);
// update the URL
const encoded = LZString.compressToEncodedURIComponent(value);
history.replaceState(undefined, undefined, `#0${encoded}`);
localStorage.setItem('tryFlowLastContent', location.hash);
});
editor.onDidChangeCursorPosition((event) => {
const cursor = event.position;
const value = editor.getModel().getValue();
cursorPositionNode.innerHTML = `${cursor.lineNumber}:${cursor.column}`;
state.flow
.then(flowProxy => flowProxy.typeAtPos(
'-', value, cursor.lineNumber, cursor.column - 1
))
.then(result => {
// flow.js <= 0.125 incorrectly returned an ocaml string
// instead of a JS string, where the string value is hidden in a
// `c` property.
var typeAtPos = typeof result === "string"
? result
: result.c;
typeAtPosNode.title = typeAtPos;
typeAtPosNode.textContent = typeAtPos;
})
.catch(() => {
typeAtPosNode.title = '';
typeAtPosNode.textContent = '';
});
});
versionTabNode.addEventListener('change', function(evt) {
const version = evt.target.value;
resultsNode.className += ' show-loading';
state.flow = initFlow(version);
state.flow.then(function() {
removeClass(resultsNode, 'show-loading');
});
validate(state.flow, model, onFlowErrors);
});
});
}
// from https://github.com/microsoft/vscode/blob/013501950e78b9dde5c2e6ec3f2ddfb9201156b7/src/vs/editor/common/modes/supports/tokenization.ts#L398
function generateTokensCSSForColorMap(colorMap) {
let rules = [];
for (let i = 1, len = colorMap.length; i < len; i++) {
let color = colorMap[i];
// CUSTOM: .code is Try Flow's parent component. we make it more specific to override Monaco
rules[i] = `.code .mtk${i} { color: ${color}; }`;
}
rules.push('.code .mtki { font-style: italic; }');
rules.push('.code .mtkb { font-weight: bold; }');
rules.push('.code .mtku { text-decoration: underline; text-underline-position: under; }');
return rules.join('\n');
}