parent
2c5deec864
commit
4fe15231aa
@ -1,702 +0,0 @@ |
|||||||
/* global FileReader */ |
|
||||||
/* global fetch */ |
|
||||||
const async = require('async') |
|
||||||
const Gists = require('gists') |
|
||||||
const modalDialogCustom = require('../ui/modal-dialog-custom') |
|
||||||
const tooltip = require('../ui/tooltip') |
|
||||||
const QueryParams = require('../../lib/query-params') |
|
||||||
const helper = require('../../lib/helper') |
|
||||||
const yo = require('yo-yo') |
|
||||||
const Treeview = require('../ui/TreeView') |
|
||||||
const modalDialog = require('../ui/modaldialog') |
|
||||||
const EventManager = require('events') |
|
||||||
const contextMenu = require('../ui/contextMenu') |
|
||||||
const css = require('./styles/file-explorer-styles') |
|
||||||
const globalRegistry = require('../../global/registry') |
|
||||||
const queryParams = new QueryParams() |
|
||||||
let MENU_HANDLE |
|
||||||
|
|
||||||
function fileExplorer (localRegistry, files, menuItems, plugin) { |
|
||||||
var self = this |
|
||||||
this.events = new EventManager() |
|
||||||
// file provider backend
|
|
||||||
this.files = files |
|
||||||
// element currently focused on
|
|
||||||
this.focusElement = null |
|
||||||
// path currently focused on
|
|
||||||
this.focusPath = null |
|
||||||
const allItems = |
|
||||||
[ |
|
||||||
{ |
|
||||||
action: 'createNewFile', |
|
||||||
title: 'Create New File', |
|
||||||
icon: 'fas fa-plus-circle' |
|
||||||
}, |
|
||||||
{ |
|
||||||
action: 'publishToGist', |
|
||||||
title: 'Publish all [browser] explorer files to a github gist', |
|
||||||
icon: 'fab fa-github' |
|
||||||
}, |
|
||||||
{ |
|
||||||
action: 'uploadFile', |
|
||||||
title: 'Add Local file to the Browser Storage Explorer', |
|
||||||
icon: 'far fa-folder-open' |
|
||||||
}, |
|
||||||
{ |
|
||||||
action: 'updateGist', |
|
||||||
title: 'Update the current [gist] explorer', |
|
||||||
icon: 'fab fa-github' |
|
||||||
} |
|
||||||
] |
|
||||||
// menu items
|
|
||||||
this.menuItems = allItems.filter( |
|
||||||
(item) => { |
|
||||||
if (menuItems) return menuItems.find((name) => { return name === item.action }) |
|
||||||
} |
|
||||||
) |
|
||||||
|
|
||||||
self._components = {} |
|
||||||
self._components.registry = localRegistry || globalRegistry |
|
||||||
self._deps = { |
|
||||||
config: self._components.registry.get('config').api, |
|
||||||
editor: self._components.registry.get('editor').api, |
|
||||||
fileManager: self._components.registry.get('filemanager').api |
|
||||||
} |
|
||||||
|
|
||||||
self.events.register('focus', function (path) { |
|
||||||
self._deps.fileManager.open(path) |
|
||||||
}) |
|
||||||
|
|
||||||
self._components.registry.put({ api: self, name: `fileexplorer/${self.files.type}` }) |
|
||||||
|
|
||||||
// warn if file changed outside of Remix
|
|
||||||
function remixdDialog () { |
|
||||||
return yo`<div>This file has been changed outside of Remix IDE.</div>` |
|
||||||
} |
|
||||||
|
|
||||||
this.files.event.register('fileExternallyChanged', (path, file) => { |
|
||||||
if (self._deps.config.get('currentFile') === path && self._deps.editor.currentContent() && self._deps.editor.currentContent() !== file.content) { |
|
||||||
if (this.files.isReadOnly(path)) return self._deps.editor.setText(file.content) |
|
||||||
|
|
||||||
modalDialog(path + ' changed', remixdDialog(), |
|
||||||
{ |
|
||||||
label: 'Replace by the new content', |
|
||||||
fn: () => { |
|
||||||
self._deps.editor.setText(file.content) |
|
||||||
} |
|
||||||
}, |
|
||||||
{ |
|
||||||
label: 'Keep the content displayed in Remix', |
|
||||||
fn: () => {} |
|
||||||
} |
|
||||||
) |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
// register to event of the file provider
|
|
||||||
files.event.on('fileRemoved', fileRemoved) |
|
||||||
files.event.on('fileRenamed', fileRenamed) |
|
||||||
files.event.on('fileRenamedError', fileRenamedError) |
|
||||||
files.event.on('fileAdded', fileAdded) |
|
||||||
files.event.on('folderAdded', folderAdded) |
|
||||||
|
|
||||||
function fileRenamedError (error) { |
|
||||||
modalDialogCustom.alert(error) |
|
||||||
} |
|
||||||
|
|
||||||
function fileAdded (filepath) { |
|
||||||
self.ensureRoot(() => { |
|
||||||
const folderpath = filepath.split('/').slice(0, -1).join('/') |
|
||||||
const currentTree = self.treeView.nodeAt(folderpath) |
|
||||||
if (!self.treeView.isExpanded(folderpath)) self.treeView.expand(folderpath) |
|
||||||
if (currentTree) { |
|
||||||
self.files.resolveDirectory(folderpath, (error, fileTree) => { |
|
||||||
if (error) console.error(error) |
|
||||||
if (!fileTree) return |
|
||||||
fileTree = normalize(folderpath, fileTree) |
|
||||||
self.treeView.updateNodeFromJSON(folderpath, fileTree, true) |
|
||||||
self.focusElement = self.treeView.labelAt(self.focusPath) |
|
||||||
// TODO: here we update the selected file (it applicable)
|
|
||||||
// cause we are refreshing the interface of the whole directory when there's a new file.
|
|
||||||
if (self.focusElement && !self.focusElement.classList.contains('bg-secondary')) { |
|
||||||
self.focusElement.classList.add('bg-secondary') |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
function extractNameFromKey (key) { |
|
||||||
const keyPath = key.split('/') |
|
||||||
|
|
||||||
return keyPath[keyPath.length - 1] |
|
||||||
} |
|
||||||
|
|
||||||
function folderAdded (folderpath) { |
|
||||||
self.ensureRoot(() => { |
|
||||||
folderpath = folderpath.split('/').slice(0, -1).join('/') |
|
||||||
self.files.resolveDirectory(folderpath, (error, fileTree) => { |
|
||||||
if (error) console.error(error) |
|
||||||
if (!fileTree) return |
|
||||||
fileTree = normalize(folderpath, fileTree) |
|
||||||
self.treeView.updateNodeFromJSON(folderpath, fileTree, true) |
|
||||||
if (!self.treeView.isExpanded(folderpath)) self.treeView.expand(folderpath) |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
function fileRemoved (filepath) { |
|
||||||
const label = self.treeView.labelAt(filepath) |
|
||||||
filepath = filepath.split('/').slice(0, -1).join('/') |
|
||||||
|
|
||||||
if (label && label.parentElement) { |
|
||||||
label.parentElement.removeChild(label) |
|
||||||
} |
|
||||||
|
|
||||||
self.updatePath(filepath) |
|
||||||
} |
|
||||||
|
|
||||||
function fileRenamed (oldName, newName, isFolder) { |
|
||||||
fileRemoved(oldName) |
|
||||||
fileAdded(newName) |
|
||||||
} |
|
||||||
|
|
||||||
// make interface and register to nodeClick, leafClick
|
|
||||||
self.treeView = new Treeview({ |
|
||||||
extractData: function extractData (value, tree, key) { |
|
||||||
var newValue = {} |
|
||||||
// var isReadOnly = false
|
|
||||||
var isFile = false |
|
||||||
Object.keys(value).filter(function keep (x) { |
|
||||||
if (x === '/content') isFile = true |
|
||||||
if (x[0] !== '/') return true |
|
||||||
}).forEach(function (x) { newValue[x] = value[x] }) |
|
||||||
return { |
|
||||||
path: (tree || {}).path ? tree.path + '/' + key : key, |
|
||||||
children: isFile ? undefined |
|
||||||
: value instanceof Array ? value.map((item, index) => ({ |
|
||||||
key: index, value: item |
|
||||||
})) : value instanceof Object ? Object.keys(value).map(subkey => ({ |
|
||||||
key: subkey, value: value[subkey] |
|
||||||
})) : undefined |
|
||||||
} |
|
||||||
}, |
|
||||||
formatSelf: function formatSelf (key, data, li) { |
|
||||||
const isRoot = data.path === self.files.type |
|
||||||
const isFolder = !!data.children |
|
||||||
return yo` |
|
||||||
<div class="${css.items}"> |
|
||||||
<span |
|
||||||
title="${data.path}" |
|
||||||
class="${css.label} ${!isRoot ? !isFolder ? css.leaf : css.folder : ''}" |
|
||||||
data-path="${data.path}" |
|
||||||
style="${isRoot ? 'font-weight:bold;' : ''}" |
|
||||||
onkeydown=${editModeOff} |
|
||||||
onblur=${editModeOff} |
|
||||||
> |
|
||||||
${key.split('/').pop()} |
|
||||||
</span> |
|
||||||
${isRoot ? self.renderMenuItems() : ''} |
|
||||||
</div> |
|
||||||
` |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
/** |
|
||||||
* Extracts first two folders as a subpath from the path. |
|
||||||
**/ |
|
||||||
function extractExternalFolder (path) { |
|
||||||
const firstSlIndex = path.indexOf('/', 1) |
|
||||||
if (firstSlIndex === -1) return '' |
|
||||||
const secondSlIndex = path.indexOf('/', firstSlIndex + 1) |
|
||||||
if (secondSlIndex === -1) return '' |
|
||||||
return path.substring(0, secondSlIndex) |
|
||||||
} |
|
||||||
|
|
||||||
self.treeView.event.register('nodeRightClick', function (key, data, label, event) { |
|
||||||
if (self.files.readonly) return |
|
||||||
if (key === self.files.type) return |
|
||||||
MENU_HANDLE && MENU_HANDLE.hide(null, true) |
|
||||||
const actions = {} |
|
||||||
const provider = self._deps.fileManager.fileProviderOf(key) |
|
||||||
actions['Create File'] = () => self.createNewFile(key) |
|
||||||
actions['Create Folder'] = () => self.createNewFolder(key) |
|
||||||
// @todo(#2386) not fully implemented. Readd later when fixed
|
|
||||||
if (provider.isExternalFolder(key)) { |
|
||||||
/* actions['Discard changes'] = () => { |
|
||||||
modalDialogCustom.confirm( |
|
||||||
'Discard changes', |
|
||||||
'Are you sure you want to discard all your changes?', |
|
||||||
() => { self.files.discardChanges(key) }, |
|
||||||
() => {} |
|
||||||
) |
|
||||||
} */ |
|
||||||
} else { |
|
||||||
const folderPath = extractExternalFolder(key) |
|
||||||
actions.Rename = () => { |
|
||||||
if (self.files.isReadOnly(key)) { return tooltip('cannot rename folder. ' + self.files.type + ' is a read only explorer') } |
|
||||||
var name = label.querySelector('span[data-path="' + key + '"]') |
|
||||||
if (name) editModeOn(name) |
|
||||||
} |
|
||||||
actions.Delete = () => { |
|
||||||
if (self.files.isReadOnly(key)) { return tooltip('cannot delete folder. ' + self.files.type + ' is a read only explorer') } |
|
||||||
const currentFoldername = extractNameFromKey(key) |
|
||||||
|
|
||||||
modalDialogCustom.confirm('Confirm to delete folder', `Are you sure you want to delete ${currentFoldername} folder?`, |
|
||||||
async () => { |
|
||||||
const fileManager = self._deps.fileManager |
|
||||||
const removeFolder = await fileManager.remove(key) |
|
||||||
|
|
||||||
if (!removeFolder) { |
|
||||||
tooltip(`failed to remove ${key}. Make sure the directory is empty before removing it.`) |
|
||||||
} |
|
||||||
}, () => {}) |
|
||||||
} |
|
||||||
if (folderPath === 'browser/gists') { |
|
||||||
actions['Push changes to gist'] = () => { |
|
||||||
const id = key.substr(key.lastIndexOf('/') + 1, key.length - 1) |
|
||||||
modalDialogCustom.confirm( |
|
||||||
'Push back to Gist', |
|
||||||
'Are you sure you want to push all your changes back to Gist?', |
|
||||||
() => { self.toGist(id) }, |
|
||||||
() => {} |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
MENU_HANDLE = contextMenu(event, actions) |
|
||||||
}) |
|
||||||
|
|
||||||
self.treeView.event.register('leafRightClick', function (key, data, label, event) { |
|
||||||
if (key === self.files.type) return |
|
||||||
MENU_HANDLE && MENU_HANDLE.hide(null, true) |
|
||||||
const actions = {} |
|
||||||
const provider = self._deps.fileManager.fileProviderOf(key) |
|
||||||
if (!provider.isExternalFolder(key)) { |
|
||||||
actions['Create Folder'] = () => self.createNewFolder(self._deps.fileManager.extractPathOf(key)) |
|
||||||
actions.Rename = () => { |
|
||||||
if (self.files.isReadOnly(key)) { return tooltip('cannot rename file. ' + self.files.type + ' is a read only explorer') } |
|
||||||
var name = label.querySelector('span[data-path="' + key + '"]') |
|
||||||
if (name) editModeOn(name) |
|
||||||
} |
|
||||||
actions.Delete = () => { |
|
||||||
if (self.files.isReadOnly(key)) { return tooltip('cannot delete file. ' + self.files.type + ' is a read only explorer') } |
|
||||||
const currentFilename = extractNameFromKey(key) |
|
||||||
|
|
||||||
modalDialogCustom.confirm( |
|
||||||
'Delete file', `Are you sure you want to delete ${currentFilename} file?`, |
|
||||||
async () => { |
|
||||||
const fileManager = self._deps.fileManager |
|
||||||
const removeFile = await fileManager.remove(key) |
|
||||||
|
|
||||||
if (!removeFile) { |
|
||||||
tooltip(`Failed to remove file ${key}.`) |
|
||||||
} |
|
||||||
}, |
|
||||||
() => {} |
|
||||||
) |
|
||||||
} |
|
||||||
if (key.endsWith('.js')) { |
|
||||||
actions.Run = async () => { |
|
||||||
provider.get(key, (error, content) => { |
|
||||||
if (error) return console.log(error) |
|
||||||
plugin.call('scriptRunner', 'execute', content) |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
MENU_HANDLE = contextMenu(event, actions) |
|
||||||
}) |
|
||||||
|
|
||||||
self.treeView.event.register('leafClick', function (key, data, label) { |
|
||||||
self.events.trigger('focus', [key]) |
|
||||||
}) |
|
||||||
|
|
||||||
self.treeView.event.register('nodeClick', function (path, childrenContainer) { |
|
||||||
if (!childrenContainer) return |
|
||||||
if (childrenContainer.style.display === 'none') return |
|
||||||
self.updatePath(path) |
|
||||||
}) |
|
||||||
|
|
||||||
// register to main app, trigger when the current file in the editor changed
|
|
||||||
self._deps.fileManager.events.on('currentFileChanged', (newFile) => { |
|
||||||
const provider = self._deps.fileManager.fileProviderOf(newFile) |
|
||||||
if (self.focusElement && self.focusPath !== newFile) { |
|
||||||
self.focusElement.classList.remove('bg-secondary') |
|
||||||
self.focusElement = null |
|
||||||
self.focusPath = null |
|
||||||
} |
|
||||||
if (provider && (provider.type === files.type)) { |
|
||||||
self.focusElement = self.treeView.labelAt(newFile) |
|
||||||
if (self.focusElement) { |
|
||||||
self.focusElement.classList.add('bg-secondary') |
|
||||||
self.focusPath = newFile |
|
||||||
} |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
self._deps.fileManager.events.on('noFileSelected', () => { |
|
||||||
if (self.focusElement) { |
|
||||||
self.focusElement.classList.remove('bg-secondary') |
|
||||||
self.focusElement = null |
|
||||||
self.focusPath = null |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
var textUnderEdit = null |
|
||||||
|
|
||||||
function selectElementContents (el) { |
|
||||||
var range = document.createRange() |
|
||||||
range.selectNodeContents(el) |
|
||||||
var sel = window.getSelection() |
|
||||||
sel.removeAllRanges() |
|
||||||
sel.addRange(range) |
|
||||||
} |
|
||||||
|
|
||||||
function editModeOn (label) { |
|
||||||
textUnderEdit = label.innerText |
|
||||||
label.setAttribute('contenteditable', true) |
|
||||||
label.classList.add('bg-light') |
|
||||||
label.focus() |
|
||||||
selectElementContents(label) |
|
||||||
} |
|
||||||
|
|
||||||
function editModeOff (event) { |
|
||||||
const label = this |
|
||||||
|
|
||||||
const isFolder = label.className.indexOf('folder') !== -1 |
|
||||||
function rename () { |
|
||||||
var newPath = label.dataset.path |
|
||||||
newPath = newPath.split('/') |
|
||||||
newPath[newPath.length - 1] = label.innerText |
|
||||||
newPath = newPath.join('/') |
|
||||||
if (label.innerText === '') { |
|
||||||
modalDialogCustom.alert('File name cannot be empty') |
|
||||||
label.innerText = textUnderEdit |
|
||||||
} else if (helper.checkSpecialChars(label.innerText)) { |
|
||||||
modalDialogCustom.alert('Special characters are not allowed') |
|
||||||
label.innerText = textUnderEdit |
|
||||||
} else { |
|
||||||
files.exists(newPath, (error, exist) => { |
|
||||||
if (error) return modalDialogCustom.alert('Unexpected error while renaming: ' + error) |
|
||||||
if (!exist) { |
|
||||||
files.rename(label.dataset.path, newPath, isFolder) |
|
||||||
} else { |
|
||||||
modalDialogCustom.alert('File already exists.') |
|
||||||
label.innerText = textUnderEdit |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (event.which === 13) event.preventDefault() |
|
||||||
if ((event.type === 'blur' || event.which === 13) && label.getAttribute('contenteditable')) { |
|
||||||
var save = textUnderEdit !== label.innerText |
|
||||||
if (save) { |
|
||||||
modalDialogCustom.confirm( |
|
||||||
'Confirm to rename a ' + (isFolder ? 'folder' : 'file'), |
|
||||||
'Are you sure you want to rename ' + textUnderEdit + '?', |
|
||||||
() => { rename() }, |
|
||||||
() => { label.innerText = textUnderEdit } |
|
||||||
) |
|
||||||
} |
|
||||||
label.removeAttribute('contenteditable') |
|
||||||
label.classList.remove('bg-light') |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fileExplorer.prototype.updatePath = function (path) { |
|
||||||
this.files.resolveDirectory(path, (error, fileTree) => { |
|
||||||
if (error) console.error(error) |
|
||||||
if (!fileTree) return |
|
||||||
var newTree = normalize(path, fileTree) |
|
||||||
this.treeView.updateNodeFromJSON(path, newTree, true) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
fileExplorer.prototype.hide = function () { |
|
||||||
if (this.container) this.container.style.display = 'none' |
|
||||||
} |
|
||||||
|
|
||||||
fileExplorer.prototype.show = function () { |
|
||||||
if (this.container) this.container.style.display = 'block' |
|
||||||
} |
|
||||||
|
|
||||||
fileExplorer.prototype.init = function () { |
|
||||||
this.container = yo`<div></div>` |
|
||||||
return this.container |
|
||||||
} |
|
||||||
|
|
||||||
fileExplorer.prototype.publishToGist = function () { |
|
||||||
modalDialogCustom.confirm( |
|
||||||
'Create a public gist', |
|
||||||
'Are you sure you want to publish all your files in browser directory anonymously as a public gist on github.com? Note: this will not include directories.', |
|
||||||
() => { this.toGist() } |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
fileExplorer.prototype.uploadFile = function (event) { |
|
||||||
// TODO The file explorer is merely a view on the current state of
|
|
||||||
// the files module. Please ask the user here if they want to overwrite
|
|
||||||
// a file and then just use `files.add`. The file explorer will
|
|
||||||
// pick that up via the 'fileAdded' event from the files module.
|
|
||||||
|
|
||||||
const self = this |
|
||||||
|
|
||||||
;[...event.target.files].forEach((file) => { |
|
||||||
const files = this.files |
|
||||||
function loadFile () { |
|
||||||
var fileReader = new FileReader() |
|
||||||
fileReader.onload = async function (event) { |
|
||||||
if (helper.checkSpecialChars(file.name)) { |
|
||||||
modalDialogCustom.alert('Special characters are not allowed') |
|
||||||
return |
|
||||||
} |
|
||||||
var success = await files.set(name, event.target.result) |
|
||||||
if (!success) { |
|
||||||
modalDialogCustom.alert('Failed to create file ' + name) |
|
||||||
} else { |
|
||||||
self.events.trigger('focus', [name]) |
|
||||||
} |
|
||||||
} |
|
||||||
fileReader.readAsText(file) |
|
||||||
} |
|
||||||
var name = files.type + '/' + file.name |
|
||||||
files.exists(name, (error, exist) => { |
|
||||||
if (error) console.log(error) |
|
||||||
if (!exist) { |
|
||||||
loadFile() |
|
||||||
} else { |
|
||||||
modalDialogCustom.confirm('Confirm overwrite', `The file ${name} already exists! Would you like to overwrite it?`, () => { loadFile() }) |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
fileExplorer.prototype.toGist = function (id) { |
|
||||||
const proccedResult = function (error, data) { |
|
||||||
if (error) { |
|
||||||
modalDialogCustom.alert('Failed to manage gist: ' + error) |
|
||||||
console.log('Failed to manage gist: ' + error) |
|
||||||
} else { |
|
||||||
if (data.html_url) { |
|
||||||
modalDialogCustom.confirm('Gist is ready', `The gist is at ${data.html_url}. Would you like to open it in a new window?`, () => { |
|
||||||
window.open(data.html_url, '_blank') |
|
||||||
}) |
|
||||||
} else { |
|
||||||
modalDialogCustom.alert(data.message + ' ' + data.documentation_url + ' ' + JSON.stringify(data.errors, null, '\t')) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* This function is to get the original content of given gist |
|
||||||
* @params id is the gist id to fetch |
|
||||||
*/ |
|
||||||
async function getOriginalFiles (id) { |
|
||||||
if (!id) { |
|
||||||
return [] |
|
||||||
} |
|
||||||
|
|
||||||
const url = `https://api.github.com/gists/${id}` |
|
||||||
const res = await fetch(url) |
|
||||||
const data = await res.json() |
|
||||||
return data.files || [] |
|
||||||
} |
|
||||||
|
|
||||||
// If 'id' is not defined, it is not a gist update but a creation so we have to take the files from the browser explorer.
|
|
||||||
const folder = id ? '/gists/' + id : '/' |
|
||||||
this.packageFiles(this.files, folder, (error, packaged) => { |
|
||||||
if (error) { |
|
||||||
console.log(error) |
|
||||||
modalDialogCustom.alert('Failed to create gist: ' + error.message) |
|
||||||
} else { |
|
||||||
// check for token
|
|
||||||
var tokenAccess = this._deps.config.get('settings/gist-access-token') |
|
||||||
if (!tokenAccess) { |
|
||||||
modalDialogCustom.alert( |
|
||||||
'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.' |
|
||||||
) |
|
||||||
} else { |
|
||||||
const description = 'Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. \n Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=' + |
|
||||||
queryParams.get().version + '&optimize=' + queryParams.get().optimize + '&runs=' + queryParams.get().runs + '&gist=' |
|
||||||
const gists = new Gists({ token: tokenAccess }) |
|
||||||
|
|
||||||
if (id) { |
|
||||||
const originalFileList = getOriginalFiles(id) |
|
||||||
// Telling the GIST API to remove files
|
|
||||||
const updatedFileList = Object.keys(packaged) |
|
||||||
const allItems = Object.keys(originalFileList) |
|
||||||
.filter(fileName => updatedFileList.indexOf(fileName) === -1) |
|
||||||
.reduce((acc, deleteFileName) => ({ |
|
||||||
...acc, |
|
||||||
[deleteFileName]: null |
|
||||||
}), originalFileList) |
|
||||||
// adding new files
|
|
||||||
updatedFileList.forEach((file) => { |
|
||||||
const _items = file.split('/') |
|
||||||
const _fileName = _items[_items.length - 1] |
|
||||||
allItems[_fileName] = packaged[file] |
|
||||||
}) |
|
||||||
|
|
||||||
tooltip('Saving gist (' + id + ') ...') |
|
||||||
gists.edit({ |
|
||||||
description: description, |
|
||||||
public: true, |
|
||||||
files: allItems, |
|
||||||
id: id |
|
||||||
}, (error, result) => { |
|
||||||
proccedResult(error, result) |
|
||||||
if (!error) { |
|
||||||
for (const key in allItems) { |
|
||||||
if (allItems[key] === null) delete allItems[key] |
|
||||||
} |
|
||||||
} |
|
||||||
}) |
|
||||||
} else { |
|
||||||
// id is not existing, need to create a new gist
|
|
||||||
tooltip('Creating a new gist ...') |
|
||||||
gists.create({ |
|
||||||
description: description, |
|
||||||
public: true, |
|
||||||
files: packaged |
|
||||||
}, (error, result) => { |
|
||||||
proccedResult(error, result) |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// return all the files, except the temporary/readonly ones..
|
|
||||||
fileExplorer.prototype.packageFiles = function (filesProvider, directory, callback) { |
|
||||||
const ret = {} |
|
||||||
filesProvider.resolveDirectory(directory, (error, files) => { |
|
||||||
if (error) callback(error) |
|
||||||
else { |
|
||||||
async.eachSeries(Object.keys(files), (path, cb) => { |
|
||||||
if (filesProvider.isDirectory(path)) { |
|
||||||
cb() |
|
||||||
} else { |
|
||||||
filesProvider.get(path, (error, content) => { |
|
||||||
if (error) return cb(error) |
|
||||||
if (/^\s+$/.test(content) || !content.length) { |
|
||||||
content = '// this line is added to create a gist. Empty file is not allowed.' |
|
||||||
} |
|
||||||
ret[path] = { content } |
|
||||||
cb() |
|
||||||
}) |
|
||||||
} |
|
||||||
}, (error) => { |
|
||||||
callback(error, ret) |
|
||||||
}) |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
fileExplorer.prototype.createNewFile = function (parentFolder = '/') { |
|
||||||
const self = this |
|
||||||
modalDialogCustom.prompt('Create new file', 'File Name (e.g Untitled.sol)', 'Untitled.sol', (input) => { |
|
||||||
if (!input) input = 'New file' |
|
||||||
helper.createNonClashingName(parentFolder + '/' + input, self.files, async (error, newName) => { |
|
||||||
if (error) return tooltip('Failed to create file ' + newName + ' ' + error) |
|
||||||
const fileManager = self._deps.fileManager |
|
||||||
const createFile = await fileManager.writeFile(newName, '') |
|
||||||
|
|
||||||
if (!createFile) { |
|
||||||
tooltip('Failed to create file ' + newName) |
|
||||||
} else { |
|
||||||
await fileManager.open(newName) |
|
||||||
if (newName.includes('_test.sol')) { |
|
||||||
self.events.trigger('newTestFileCreated', [newName]) |
|
||||||
} |
|
||||||
} |
|
||||||
}) |
|
||||||
}, null, true) |
|
||||||
} |
|
||||||
|
|
||||||
fileExplorer.prototype.createNewFolder = function (parentFolder) { |
|
||||||
const self = this |
|
||||||
modalDialogCustom.prompt('Create new folder', '', 'New folder', (input) => { |
|
||||||
if (!input) { |
|
||||||
return tooltip('Failed to create folder. The name can not be empty') |
|
||||||
} |
|
||||||
|
|
||||||
const currentPath = !parentFolder ? self._deps.fileManager.currentPath() : parentFolder |
|
||||||
let newName = currentPath ? currentPath + '/' + input : self.files.type + '/' + input |
|
||||||
|
|
||||||
newName = newName + '/' |
|
||||||
self.files.exists(newName, (error, exist) => { |
|
||||||
if (error) return tooltip('Unexpected error while creating folder: ' + error) |
|
||||||
if (!exist) { |
|
||||||
self.files.set(newName, '') |
|
||||||
} else { |
|
||||||
tooltip('Folder already exists.', () => {}) |
|
||||||
} |
|
||||||
}) |
|
||||||
}, null, true) |
|
||||||
} |
|
||||||
|
|
||||||
fileExplorer.prototype.renderMenuItems = function () { |
|
||||||
let items = '' |
|
||||||
if (this.menuItems) { |
|
||||||
items = this.menuItems.map(({ action, title, icon }) => { |
|
||||||
if (action === 'uploadFile') { |
|
||||||
return yo` |
|
||||||
<label |
|
||||||
id=${action} |
|
||||||
data-id="fileExplorerUploadFile${action}" |
|
||||||
class="${icon} mb-0 ${css.newFile}" |
|
||||||
title="${title}" |
|
||||||
> |
|
||||||
<input id="fileUpload" data-id="fileExplorerFileUpload" type="file" onchange=${(event) => { |
|
||||||
event.stopPropagation() |
|
||||||
this.uploadFile(event) |
|
||||||
}} multiple /> |
|
||||||
</label> |
|
||||||
` |
|
||||||
} else { |
|
||||||
return yo` |
|
||||||
<span |
|
||||||
id=${action} |
|
||||||
data-id="fileExplorerNewFile${action}" |
|
||||||
onclick=${(event) => { event.stopPropagation(); this[action]() }} |
|
||||||
class="newFile ${icon} ${css.newFile}" |
|
||||||
title=${title} |
|
||||||
> |
|
||||||
</span> |
|
||||||
` |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
return yo`<span class=" ${css.menu}">${items}</span>` |
|
||||||
} |
|
||||||
|
|
||||||
fileExplorer.prototype.ensureRoot = function (cb) { |
|
||||||
cb = cb || (() => {}) |
|
||||||
var self = this |
|
||||||
if (self.element) return cb() |
|
||||||
const root = {} |
|
||||||
root[this.files.type] = {} |
|
||||||
var element = self.treeView.render(root, false) |
|
||||||
element.classList.add(css.fileexplorer) |
|
||||||
element.events = self.events |
|
||||||
element.api = self.api |
|
||||||
self.container.appendChild(element) |
|
||||||
self.element = element |
|
||||||
if (cb) cb() |
|
||||||
self.treeView.expand(self.files.type) |
|
||||||
} |
|
||||||
|
|
||||||
function normalize (path, filesList) { |
|
||||||
var prefix = path.split('/')[0] |
|
||||||
var newList = {} |
|
||||||
Object.keys(filesList).forEach(key => { |
|
||||||
newList[prefix + '/' + key] = filesList[key].isDirectory ? {} : { '/content': true } |
|
||||||
}) |
|
||||||
return newList |
|
||||||
} |
|
||||||
|
|
||||||
module.exports = fileExplorer |
|
@ -1,580 +0,0 @@ |
|||||||
|
|
||||||
import toaster from '../../ui/tooltip' |
|
||||||
import { canUseWorker, baseURLBin, baseURLWasm, urlFromVersion, pathToURL, promisedMiniXhr } from '@remix-project/remix-solidity' |
|
||||||
const yo = require('yo-yo') |
|
||||||
const helper = require('../../../lib/helper') |
|
||||||
const addTooltip = require('../../ui/tooltip') |
|
||||||
const semver = require('semver') |
|
||||||
const modalDialogCustom = require('../../ui/modal-dialog-custom') |
|
||||||
const css = require('../styles/compile-tab-styles') |
|
||||||
const _paq = window._paq = window._paq || [] |
|
||||||
|
|
||||||
class CompilerContainer { |
|
||||||
constructor (compileTabLogic, editor, config, queryParams) { |
|
||||||
this._view = {} |
|
||||||
this.compileTabLogic = compileTabLogic |
|
||||||
this.editor = editor |
|
||||||
this.config = config |
|
||||||
this.queryParams = queryParams |
|
||||||
this.hhCompilation = false |
|
||||||
|
|
||||||
this.data = { |
|
||||||
hideWarnings: config.get('hideWarnings') || false, |
|
||||||
autoCompile: config.get('autoCompile'), |
|
||||||
compileTimeout: null, |
|
||||||
timeout: 300, |
|
||||||
allversions: null, |
|
||||||
selectedVersion: null, |
|
||||||
defaultVersion: 'soljson-v0.8.4+commit.c7e474f2.js' // this default version is defined: in makeMockCompiler (for browser test)
|
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Update the compilation button with the name of the current file |
|
||||||
*/ |
|
||||||
set currentFile (name = '') { |
|
||||||
if (name && name !== '') { |
|
||||||
this._setCompilerVersionFromPragma(name) |
|
||||||
} |
|
||||||
if (!this._view.compilationButton) return |
|
||||||
const button = this.compilationButton(name.split('/').pop()) |
|
||||||
this._disableCompileBtn(!name || (name && !this.isSolFileSelected(name))) |
|
||||||
yo.update(this._view.compilationButton, button) |
|
||||||
} |
|
||||||
|
|
||||||
isSolFileSelected (currentFile = '') { |
|
||||||
if (!currentFile) currentFile = this.config.get('currentFile') |
|
||||||
if (!currentFile) return false |
|
||||||
const extention = currentFile.substr(currentFile.length - 3, currentFile.length) |
|
||||||
return extention.toLowerCase() === 'sol' || extention.toLowerCase() === 'yul' |
|
||||||
} |
|
||||||
|
|
||||||
deactivate () { |
|
||||||
// deactivate editor listeners
|
|
||||||
this.editor.event.unregister('contentChanged') |
|
||||||
this.editor.event.unregister('sessionSwitched') |
|
||||||
} |
|
||||||
|
|
||||||
activate () { |
|
||||||
this.currentFile = this.config.get('currentFile') |
|
||||||
this.listenToEvents() |
|
||||||
} |
|
||||||
|
|
||||||
listenToEvents () { |
|
||||||
this.editor.event.register('sessionSwitched', () => { |
|
||||||
if (!this._view.compileIcon) return |
|
||||||
this.scheduleCompilation() |
|
||||||
}) |
|
||||||
|
|
||||||
this.compileTabLogic.event.on('startingCompilation', () => { |
|
||||||
if (!this._view.compileIcon) return |
|
||||||
this._view.compileIcon.setAttribute('title', 'compiling...') |
|
||||||
this._view.compileIcon.classList.remove(`${css.bouncingIcon}`) |
|
||||||
this._view.compileIcon.classList.add(`${css.spinningIcon}`) |
|
||||||
}) |
|
||||||
|
|
||||||
this.compileTabLogic.compiler.event.register('compilationDuration', (speed) => { |
|
||||||
if (!this._view.warnCompilationSlow) return |
|
||||||
if (speed > 1000) { |
|
||||||
const msg = `Last compilation took ${speed}ms. We suggest to turn off autocompilation.` |
|
||||||
this._view.warnCompilationSlow.setAttribute('title', msg) |
|
||||||
this._view.warnCompilationSlow.style.visibility = 'visible' |
|
||||||
} else { |
|
||||||
this._view.warnCompilationSlow.style.visibility = 'hidden' |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
this.editor.event.register('contentChanged', () => { |
|
||||||
if (!this._view.compileIcon) return |
|
||||||
this.scheduleCompilation() |
|
||||||
this._view.compileIcon.classList.add(`${css.bouncingIcon}`) // @TODO: compileView tab
|
|
||||||
}) |
|
||||||
|
|
||||||
this.compileTabLogic.compiler.event.register('loadingCompiler', () => { |
|
||||||
if (!this._view.compileIcon) return |
|
||||||
this._disableCompileBtn(true) |
|
||||||
this._view.compileIcon.setAttribute('title', 'compiler is loading, please wait a few moments.') |
|
||||||
this._view.compileIcon.classList.add(`${css.spinningIcon}`) |
|
||||||
this._view.warnCompilationSlow.style.visibility = 'hidden' |
|
||||||
this._updateLanguageSelector() |
|
||||||
}) |
|
||||||
|
|
||||||
this.compileTabLogic.compiler.event.register('compilerLoaded', () => { |
|
||||||
if (!this._view.compileIcon) return |
|
||||||
this._disableCompileBtn(false) |
|
||||||
this._view.compileIcon.setAttribute('title', '') |
|
||||||
this._view.compileIcon.classList.remove(`${css.spinningIcon}`) |
|
||||||
if (this.data.autoCompile) this.compileIfAutoCompileOn() |
|
||||||
}) |
|
||||||
|
|
||||||
this.compileTabLogic.compiler.event.register('compilationFinished', (success, data, source) => { |
|
||||||
if (!this._view.compileIcon) return |
|
||||||
this._view.compileIcon.setAttribute('title', 'idle') |
|
||||||
this._view.compileIcon.classList.remove(`${css.spinningIcon}`) |
|
||||||
this._view.compileIcon.classList.remove(`${css.bouncingIcon}`) |
|
||||||
_paq.push(['trackEvent', 'compiler', 'compiled_with_version', this._retrieveVersion()]) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
/************** |
|
||||||
* SUBCOMPONENT |
|
||||||
*/ |
|
||||||
compilationButton (name = '') { |
|
||||||
const displayed = name || '<no file selected>' |
|
||||||
const disabled = name && this.isSolFileSelected() ? '' : 'disabled' |
|
||||||
return yo` |
|
||||||
<button id="compileBtn" data-id="compilerContainerCompileBtn" class="btn btn-primary btn-block ${disabled} mt-3" title="Compile" onclick="${this.compile.bind(this)}"> |
|
||||||
<span>${this._view.compileIcon} Compile ${displayed}</span> |
|
||||||
</button> |
|
||||||
` |
|
||||||
} |
|
||||||
|
|
||||||
_disableCompileBtn (shouldDisable) { |
|
||||||
const btn = document.getElementById('compileBtn') |
|
||||||
if (!btn) return |
|
||||||
if (shouldDisable) { |
|
||||||
btn.classList.add('disabled') |
|
||||||
} else if (this.isSolFileSelected()) { |
|
||||||
btn.classList.remove('disabled') |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Load solc compiler version according to pragma in contract file
|
|
||||||
_setCompilerVersionFromPragma (filename) { |
|
||||||
if (!this.data.allversions) return |
|
||||||
this.compileTabLogic.fileManager.readFile(filename).then(data => { |
|
||||||
const pragmaArr = data.match(/(pragma solidity (.+?);)/g) |
|
||||||
if (pragmaArr && pragmaArr.length === 1) { |
|
||||||
const pragmaStr = pragmaArr[0].replace('pragma solidity', '').trim() |
|
||||||
const pragma = pragmaStr.substring(0, pragmaStr.length - 1) |
|
||||||
const releasedVersions = this.data.allversions.filter(obj => !obj.prerelease).map(obj => obj.version) |
|
||||||
const allVersions = this.data.allversions.map(obj => this._retrieveVersion(obj.version)) |
|
||||||
const currentCompilerName = this._retrieveVersion(this._view.versionSelector.selectedOptions[0].label) |
|
||||||
// contains only numbers part, for example '0.4.22'
|
|
||||||
const pureVersion = this._retrieveVersion() |
|
||||||
// is nightly build newer than the last release
|
|
||||||
const isNewestNightly = currentCompilerName.includes('nightly') && semver.gt(pureVersion, releasedVersions[0]) |
|
||||||
// checking if the selected version is in the pragma range
|
|
||||||
const isInRange = semver.satisfies(pureVersion, pragma) |
|
||||||
// checking if the selected version is from official compilers list(excluding custom versions) and in range or greater
|
|
||||||
const isOfficial = allVersions.includes(currentCompilerName) |
|
||||||
if (isOfficial && (!isInRange && !isNewestNightly)) { |
|
||||||
const compilerToLoad = semver.maxSatisfying(releasedVersions, pragma) |
|
||||||
const compilerPath = this.data.allversions.filter(obj => !obj.prerelease && obj.version === compilerToLoad)[0].path |
|
||||||
if (this.data.selectedVersion !== compilerPath) { |
|
||||||
this.data.selectedVersion = compilerPath |
|
||||||
this._updateVersionSelector() |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
_retrieveVersion (version) { |
|
||||||
if (!version) version = this._view.versionSelector.value |
|
||||||
if (version === 'builtin') version = this.data.defaultVersion |
|
||||||
return semver.coerce(version) ? semver.coerce(version).version : '' |
|
||||||
} |
|
||||||
|
|
||||||
render () { |
|
||||||
this.compileTabLogic.compiler.event.register('compilerLoaded', (version) => this.setVersionText(version)) |
|
||||||
this.fetchAllVersion((allversions, selectedVersion, isURL) => { |
|
||||||
this.data.allversions = allversions |
|
||||||
if (isURL) this._updateVersionSelector(selectedVersion) |
|
||||||
else { |
|
||||||
this.data.selectedVersion = selectedVersion |
|
||||||
if (this._view.versionSelector) this._updateVersionSelector() |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
this.hardhatCompilation = yo`<div class="mt-2 ${css.compilerConfig} custom-control custom-checkbox" style="display:none">
|
|
||||||
<input class="${css.autocompile} custom-control-input" onchange=${(e) => this.updatehhCompilation(e)} id="enableHardhat" type="checkbox" title="Enable Hardhat Compilation"> |
|
||||||
<label class="form-check-label custom-control-label" for="enableHardhat">Enable Hardhat Compilation</label> |
|
||||||
</div>` |
|
||||||
this._view.warnCompilationSlow = yo`<i title="Compilation Slow" style="visibility:hidden" class="${css.warnCompilationSlow} fas fa-exclamation-triangle" aria-hidden="true"></i>` |
|
||||||
this._view.compileIcon = yo`<i class="fas fa-sync ${css.icon}" aria-hidden="true"></i>` |
|
||||||
this._view.autoCompile = yo`<input class="${css.autocompile} custom-control-input" onchange=${() => this.updateAutoCompile()} data-id="compilerContainerAutoCompile" id="autoCompile" type="checkbox" title="Auto compile">` |
|
||||||
this._view.hideWarningsBox = yo`<input class="${css.autocompile} custom-control-input" onchange=${() => this.hideWarnings()} id="hideWarningsBox" type="checkbox" title="Hide warnings">` |
|
||||||
if (this.data.autoCompile) this._view.autoCompile.setAttribute('checked', '') |
|
||||||
if (this.data.hideWarnings) this._view.hideWarningsBox.setAttribute('checked', '') |
|
||||||
|
|
||||||
this._view.optimize = yo`<input onchange=${() => this.onchangeOptimize()} class="custom-control-input" id="optimize" type="checkbox">` |
|
||||||
if (this.compileTabLogic.optimize) this._view.optimize.setAttribute('checked', '') |
|
||||||
|
|
||||||
this._view.runs = yo`<input
|
|
||||||
min="1" |
|
||||||
class="custom-select ml-2 ${css.runs}" |
|
||||||
id="runs" |
|
||||||
placeholder="200" |
|
||||||
value="200" |
|
||||||
type="number" |
|
||||||
title="Estimated number of times each opcode of the deployed code will be executed across the life-time of the contract." |
|
||||||
onchange=${() => this.onchangeRuns()} |
|
||||||
>` |
|
||||||
if (this.compileTabLogic.optimize) { |
|
||||||
this._view.runs.removeAttribute('disabled') |
|
||||||
this._view.runs.value = this.compileTabLogic.runs |
|
||||||
} else { |
|
||||||
this._view.runs.setAttribute('disabled', '') |
|
||||||
} |
|
||||||
|
|
||||||
this._view.versionSelector = yo` |
|
||||||
<select onchange="${() => this.onchangeLoadVersion()}" class="custom-select" id="versionSelector" disabled> |
|
||||||
<option disabled selected>${this.data.defaultVersion}</option> |
|
||||||
<option disabled>builtin</option> |
|
||||||
</select>` |
|
||||||
this._view.languageSelector = yo` |
|
||||||
<select onchange="${() => this.onchangeLanguage()}" class="custom-select" id="compilierLanguageSelector" title="Available since v0.5.7"> |
|
||||||
<option>Solidity</option> |
|
||||||
<option>Yul</option> |
|
||||||
</select>` |
|
||||||
this._view.version = yo`<span id="version"></span>` |
|
||||||
|
|
||||||
this._view.evmVersionSelector = yo` |
|
||||||
<select onchange="${() => this.onchangeEvmVersion()}" class="custom-select" id="evmVersionSelector"> |
|
||||||
<option value="default" selected="selected">compiler default</option> |
|
||||||
<option>berlin</option> |
|
||||||
<option>muirGlacier</option> |
|
||||||
<option>istanbul</option> |
|
||||||
<option>petersburg</option> |
|
||||||
<option>constantinople</option> |
|
||||||
<option>byzantium</option> |
|
||||||
<option>spuriousDragon</option> |
|
||||||
<option>tangerineWhistle</option> |
|
||||||
<option>homestead</option> |
|
||||||
</select>` |
|
||||||
if (this.compileTabLogic.evmVersion) { |
|
||||||
const s = this._view.evmVersionSelector |
|
||||||
let i |
|
||||||
for (i = 0; i < s.options.length; i++) { |
|
||||||
if (s.options[i].value === this.compileTabLogic.evmVersion) { |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
if (i === s.options.length) { // invalid evmVersion from queryParams
|
|
||||||
s.selectedIndex = 0 // compiler default
|
|
||||||
this.onchangeEvmVersion() |
|
||||||
} else { |
|
||||||
s.selectedIndex = i |
|
||||||
this.onchangeEvmVersion() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
this._view.compilationButton = this.compilationButton() |
|
||||||
|
|
||||||
this._view.includeNightlies = yo` |
|
||||||
<input class="mr-2 custom-control-input" id="nightlies" type="checkbox" onchange=${() => this._updateVersionSelector()}> |
|
||||||
` |
|
||||||
this._view.compileContainer = yo` |
|
||||||
<section> |
|
||||||
<!-- Select Compiler Version --> |
|
||||||
<article> |
|
||||||
<header class="${css.compilerSection} border-bottom"> |
|
||||||
<div class="mb-2"> |
|
||||||
<label class="${css.compilerLabel} form-check-label" for="versionSelector"> |
|
||||||
Compiler |
|
||||||
<button class="far fa-plus-square border-0 p-0 mx-2 btn-sm" onclick="${(e) => this.promtCompiler(e)}" title="Add a custom compiler with URL"></button> |
|
||||||
</label> |
|
||||||
${this._view.versionSelector} |
|
||||||
</div> |
|
||||||
<div class="mb-2 ${css.nightlyBuilds} custom-control custom-checkbox"> |
|
||||||
${this._view.includeNightlies} |
|
||||||
<label for="nightlies" class="form-check-label custom-control-label">Include nightly builds</label> |
|
||||||
</div> |
|
||||||
<div class="mb-2"> |
|
||||||
<label class="${css.compilerLabel} form-check-label" for="compilierLanguageSelector">Language</label> |
|
||||||
${this._view.languageSelector} |
|
||||||
</div> |
|
||||||
<div class="mb-2"> |
|
||||||
<label class="${css.compilerLabel} form-check-label" for="evmVersionSelector">EVM Version</label> |
|
||||||
${this._view.evmVersionSelector} |
|
||||||
</div> |
|
||||||
<div class="mt-3"> |
|
||||||
<p class="mt-2 ${css.compilerLabel}">Compiler Configuration</p> |
|
||||||
<div class="mt-2 ${css.compilerConfig} custom-control custom-checkbox"> |
|
||||||
${this._view.autoCompile} |
|
||||||
<label class="form-check-label custom-control-label" for="autoCompile">Auto compile</label> |
|
||||||
</div> |
|
||||||
<div class="mt-2 ${css.compilerConfig} custom-control custom-checkbox"> |
|
||||||
<div class="justify-content-between align-items-center d-flex"> |
|
||||||
${this._view.optimize} |
|
||||||
<label class="form-check-label custom-control-label" for="optimize">Enable optimization</label> |
|
||||||
${this._view.runs} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
<div class="mt-2 ${css.compilerConfig} custom-control custom-checkbox"> |
|
||||||
${this._view.hideWarningsBox} |
|
||||||
<label class="form-check-label custom-control-label" for="hideWarningsBox">Hide warnings</label> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
${this.hardhatCompilation} |
|
||||||
${this._view.compilationButton} |
|
||||||
</header> |
|
||||||
</article> |
|
||||||
<!-- Config --> |
|
||||||
</section>` |
|
||||||
|
|
||||||
return this._view.compileContainer |
|
||||||
} |
|
||||||
|
|
||||||
promtCompiler () { |
|
||||||
modalDialogCustom.prompt( |
|
||||||
'Add a custom compiler', |
|
||||||
'URL', |
|
||||||
'', |
|
||||||
(url) => this.addCustomCompiler(url) |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
addCustomCompiler (url) { |
|
||||||
this.data.selectedVersion = this._view.versionSelector.value |
|
||||||
this._updateVersionSelector(url) |
|
||||||
} |
|
||||||
|
|
||||||
updateAutoCompile (event) { |
|
||||||
this.config.set('autoCompile', this._view.autoCompile.checked) |
|
||||||
} |
|
||||||
|
|
||||||
updatehhCompilation (event) { |
|
||||||
this.hhCompilation = event.target.checked |
|
||||||
} |
|
||||||
|
|
||||||
compile (event) { |
|
||||||
const currentFile = this.config.get('currentFile') |
|
||||||
if (!this.isSolFileSelected()) return |
|
||||||
|
|
||||||
this._setCompilerVersionFromPragma(currentFile) |
|
||||||
this.compileTabLogic.runCompiler(this.hhCompilation) |
|
||||||
} |
|
||||||
|
|
||||||
compileIfAutoCompileOn () { |
|
||||||
if (this.config.get('autoCompile')) { |
|
||||||
this.compile() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
hideWarnings (event) { |
|
||||||
this.config.set('hideWarnings', this._view.hideWarningsBox.checked) |
|
||||||
this.compileIfAutoCompileOn() |
|
||||||
} |
|
||||||
|
|
||||||
/* |
|
||||||
The following functions are handlers for internal events. |
|
||||||
*/ |
|
||||||
|
|
||||||
onchangeOptimize () { |
|
||||||
this.compileTabLogic.setOptimize(!!this._view.optimize.checked) |
|
||||||
if (this.compileTabLogic.optimize) { |
|
||||||
this._view.runs.removeAttribute('disabled') |
|
||||||
this.compileTabLogic.setRuns(parseInt(this._view.runs.value)) |
|
||||||
} else { |
|
||||||
this.compileTabLogic.setRuns(200) |
|
||||||
this._view.runs.setAttribute('disabled', '') |
|
||||||
} |
|
||||||
this.compileIfAutoCompileOn() |
|
||||||
} |
|
||||||
|
|
||||||
onchangeRuns () { |
|
||||||
this.compileTabLogic.setRuns(parseInt(this._view.runs.value)) |
|
||||||
this.compileIfAutoCompileOn() |
|
||||||
} |
|
||||||
|
|
||||||
onchangeLanguage () { |
|
||||||
this.compileTabLogic.setLanguage(this._view.languageSelector.value) |
|
||||||
this.compileIfAutoCompileOn() |
|
||||||
} |
|
||||||
|
|
||||||
onchangeEvmVersion () { |
|
||||||
const s = this._view.evmVersionSelector |
|
||||||
let v = s.value |
|
||||||
if (v === 'default') { |
|
||||||
v = null |
|
||||||
} |
|
||||||
this.compileTabLogic.setEvmVersion(v) |
|
||||||
for (let i = 0; i < s.options.length; i++) { |
|
||||||
if (i === s.selectedIndex) { |
|
||||||
s.options[s.selectedIndex].setAttribute('selected', 'selected') |
|
||||||
} else { |
|
||||||
s.options[i].removeAttribute('selected') |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
this.compileIfAutoCompileOn() |
|
||||||
} |
|
||||||
|
|
||||||
onchangeLoadVersion () { |
|
||||||
this.data.selectedVersion = this._view.versionSelector.value |
|
||||||
this._updateVersionSelector() |
|
||||||
this._updateLanguageSelector() |
|
||||||
} |
|
||||||
|
|
||||||
/* |
|
||||||
The following functions map with the above event handlers. |
|
||||||
They are an external API for modifying the compiler configuration. |
|
||||||
*/ |
|
||||||
setConfiguration (settings) { |
|
||||||
this.setLanguage(settings.language) |
|
||||||
this.setEvmVersion(settings.evmVersion) |
|
||||||
this.setOptimize(settings.optimize) |
|
||||||
this.setRuns(settings.runs) |
|
||||||
this.setVersion(settings.version) |
|
||||||
} |
|
||||||
|
|
||||||
setOptimize (enabled) { |
|
||||||
this._view.optimize.checked = enabled |
|
||||||
this.onchangeOptimize() |
|
||||||
} |
|
||||||
|
|
||||||
setRuns (value) { |
|
||||||
if (value) { |
|
||||||
this._view.runs.value = value |
|
||||||
this.onchangeRuns() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
setLanguage (lang) { |
|
||||||
this._view.languageSelector.value = lang |
|
||||||
this.onchangeLanguage() |
|
||||||
} |
|
||||||
|
|
||||||
setEvmVersion (version) { |
|
||||||
this._view.evmVersionSelector.value = version || 'default' |
|
||||||
this.onchangeEvmVersion() |
|
||||||
} |
|
||||||
|
|
||||||
setVersion (version) { |
|
||||||
this._view.versionSelector.value = `soljson-v${version}.js` |
|
||||||
this.onchangeLoadVersion() |
|
||||||
} |
|
||||||
|
|
||||||
_shouldBeAdded (version) { |
|
||||||
return !version.includes('nightly') || |
|
||||||
(version.includes('nightly') && this._view.includeNightlies.checked) |
|
||||||
} |
|
||||||
|
|
||||||
_updateVersionSelector (customUrl = '') { |
|
||||||
// update selectedversion if previous one got filtered out
|
|
||||||
if (!this.data.selectedVersion || !this._shouldBeAdded(this.data.selectedVersion)) { |
|
||||||
this.data.selectedVersion = this.data.defaultVersion |
|
||||||
} |
|
||||||
this._view.versionSelector.innerHTML = '' |
|
||||||
this._view.versionSelector.removeAttribute('disabled') |
|
||||||
this.queryParams.update({ version: this.data.selectedVersion }) |
|
||||||
let url |
|
||||||
if (customUrl !== '') { |
|
||||||
this.data.selectedVersion = customUrl |
|
||||||
this._view.versionSelector.appendChild(yo`<option value="${customUrl}" selected>custom</option>`) |
|
||||||
url = customUrl |
|
||||||
this.queryParams.update({ version: this.data.selectedVersion }) |
|
||||||
} else if (this.data.selectedVersion === 'builtin') { |
|
||||||
let location = window.document.location |
|
||||||
let path = location.pathname |
|
||||||
if (!path.startsWith('/')) path = '/' + path |
|
||||||
location = `${location.protocol}//${location.host}${path}assets/js` |
|
||||||
if (location.endsWith('index.html')) location = location.substring(0, location.length - 10) |
|
||||||
if (!location.endsWith('/')) location += '/' |
|
||||||
url = location + 'soljson.js' |
|
||||||
} else { |
|
||||||
if (this.data.selectedVersion.indexOf('soljson') !== 0 || helper.checkSpecialChars(this.data.selectedVersion)) { |
|
||||||
return console.log('loading ' + this.data.selectedVersion + ' not allowed') |
|
||||||
} |
|
||||||
url = `${urlFromVersion(this.data.selectedVersion)}` |
|
||||||
} |
|
||||||
|
|
||||||
this.data.allversions.forEach(build => { |
|
||||||
const option = build.path === this.data.selectedVersion |
|
||||||
? yo`<option value="${build.path}" selected>${build.longVersion}</option>` |
|
||||||
: yo`<option value="${build.path}">${build.longVersion}</option>` |
|
||||||
|
|
||||||
if (this._shouldBeAdded(option.innerText)) { |
|
||||||
this._view.versionSelector.appendChild(option) |
|
||||||
} |
|
||||||
}) |
|
||||||
if (this.data.selectedVersion !== 'builtin' && semver.lt(this._retrieveVersion(), 'v0.4.12+commit.194ff033.js')) { |
|
||||||
toaster(yo` |
|
||||||
<div> |
|
||||||
<b>Old compiler usage detected.</b> |
|
||||||
<p>You are using a compiler older than v0.4.12.</p> |
|
||||||
<p>Some functionality may not work.</p> |
|
||||||
</div>` |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
// Workers cannot load js on "file:"-URLs and we get a
|
|
||||||
// "Uncaught RangeError: Maximum call stack size exceeded" error on Chromium,
|
|
||||||
// resort to non-worker version in that case.
|
|
||||||
if (canUseWorker(this._retrieveVersion())) { |
|
||||||
this.compileTabLogic.compiler.loadVersion(true, url) |
|
||||||
this.setVersionText('(loading using worker)') |
|
||||||
} else { |
|
||||||
this.compileTabLogic.compiler.loadVersion(false, url) |
|
||||||
this.setVersionText('(loading)') |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
_updateLanguageSelector () { |
|
||||||
// This is the first version when Yul is available
|
|
||||||
if (!semver.valid(this._retrieveVersion()) || semver.lt(this._retrieveVersion(), 'v0.5.7+commit.6da8b019.js')) { |
|
||||||
this._view.languageSelector.setAttribute('disabled', '') |
|
||||||
this._view.languageSelector.value = 'Solidity' |
|
||||||
this.compileTabLogic.setLanguage('Solidity') |
|
||||||
} else { |
|
||||||
this._view.languageSelector.removeAttribute('disabled') |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
setVersionText (text) { |
|
||||||
if (this._view.version) this._view.version.innerText = text |
|
||||||
} |
|
||||||
|
|
||||||
// fetching both normal and wasm builds and creating a [version, baseUrl] map
|
|
||||||
async fetchAllVersion (callback) { |
|
||||||
let selectedVersion, allVersionsWasm, isURL |
|
||||||
let allVersions = [{ path: 'builtin', longVersion: 'Stable local version - 0.8.4' }] |
|
||||||
// fetch normal builds
|
|
||||||
const binRes = await promisedMiniXhr(`${baseURLBin}/list.json`) |
|
||||||
// fetch wasm builds
|
|
||||||
const wasmRes = await promisedMiniXhr(`${baseURLWasm}/list.json`) |
|
||||||
if (binRes.event.type === 'error' && wasmRes.event.type === 'error') { |
|
||||||
selectedVersion = 'builtin' |
|
||||||
return callback(allVersions, selectedVersion) |
|
||||||
} |
|
||||||
try { |
|
||||||
const versions = JSON.parse(binRes.json).builds.slice().reverse() |
|
||||||
allVersions = [...allVersions, ...versions] |
|
||||||
selectedVersion = this.data.defaultVersion |
|
||||||
if (this.queryParams.get().version) selectedVersion = this.queryParams.get().version |
|
||||||
// Check if version is a URL and corresponding filename starts with 'soljson'
|
|
||||||
if (selectedVersion.startsWith('https://')) { |
|
||||||
const urlArr = selectedVersion.split('/') |
|
||||||
if (urlArr[urlArr.length - 1].startsWith('soljson')) isURL = true |
|
||||||
} |
|
||||||
if (wasmRes.event.type !== 'error') { |
|
||||||
allVersionsWasm = JSON.parse(wasmRes.json).builds.slice().reverse() |
|
||||||
} |
|
||||||
} catch (e) { |
|
||||||
addTooltip('Cannot load compiler version list. It might have been blocked by an advertisement blocker. Please try deactivating any of them from this page and reload. Error: ' + e) |
|
||||||
} |
|
||||||
// replace in allVersions those compiler builds which exist in allVersionsWasm with new once
|
|
||||||
if (allVersionsWasm && allVersions) { |
|
||||||
allVersions.forEach((compiler, index) => { |
|
||||||
const wasmIndex = allVersionsWasm.findIndex(wasmCompiler => { return wasmCompiler.longVersion === compiler.longVersion }) |
|
||||||
if (wasmIndex !== -1) { |
|
||||||
allVersions[index] = allVersionsWasm[wasmIndex] |
|
||||||
pathToURL[compiler.path] = baseURLWasm |
|
||||||
} else { |
|
||||||
pathToURL[compiler.path] = baseURLBin |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
callback(allVersions, selectedVersion, isURL) |
|
||||||
} |
|
||||||
|
|
||||||
scheduleCompilation () { |
|
||||||
if (!this.config.get('autoCompile')) return |
|
||||||
if (this.data.compileTimeout) window.clearTimeout(this.data.compileTimeout) |
|
||||||
this.data.compileTimeout = window.setTimeout(() => this.compileIfAutoCompileOn(), this.data.timeout) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
module.exports = CompilerContainer |
|
@ -1,51 +0,0 @@ |
|||||||
'use strict' |
|
||||||
import * as remixLib from '@remix-project/remix-lib' |
|
||||||
|
|
||||||
const txHelper = remixLib.execution.txHelper |
|
||||||
|
|
||||||
export class CompilerAbstract { |
|
||||||
public languageversion: string |
|
||||||
public data: Record<string, any> |
|
||||||
public source: Record<string, any> |
|
||||||
|
|
||||||
constructor (languageversion, data, source) { |
|
||||||
this.languageversion = languageversion |
|
||||||
this.data = data |
|
||||||
this.source = source // source code
|
|
||||||
} |
|
||||||
|
|
||||||
getContracts () { |
|
||||||
return this.data.contracts |
|
||||||
} |
|
||||||
|
|
||||||
getContract (name) { |
|
||||||
return txHelper.getContract(name, this.data.contracts) |
|
||||||
} |
|
||||||
|
|
||||||
visitContracts (calllback) { |
|
||||||
return txHelper.visitContracts(this.data.contracts, calllback) |
|
||||||
} |
|
||||||
|
|
||||||
getData () { |
|
||||||
return this.data |
|
||||||
} |
|
||||||
|
|
||||||
getAsts () { |
|
||||||
return this.data.sources // ast
|
|
||||||
} |
|
||||||
|
|
||||||
getSourceName (fileIndex) { |
|
||||||
if (this.data && this.data.sources) { |
|
||||||
return Object.keys(this.data.sources)[fileIndex] |
|
||||||
} else if (Object.keys(this.source.sources).length === 1) { |
|
||||||
// if we don't have ast, we return the only one filename present.
|
|
||||||
const sourcesArray = Object.keys(this.source.sources) |
|
||||||
return sourcesArray[0] |
|
||||||
} |
|
||||||
return null |
|
||||||
} |
|
||||||
|
|
||||||
getSourceCode () { |
|
||||||
return this.source |
|
||||||
} |
|
||||||
} |
|
@ -1,19 +0,0 @@ |
|||||||
'use strict' |
|
||||||
import { canUseWorker, urlFromVersion } from './compiler-utils' |
|
||||||
import { Compiler } from '@remix-project/remix-solidity' |
|
||||||
import { CompilerAbstract } from './compiler-abstract' |
|
||||||
|
|
||||||
export const compile = async (compilationTargets, settings, contentResolverCallback) => { |
|
||||||
return new Promise((resolve) => { |
|
||||||
const compiler = new Compiler(contentResolverCallback) |
|
||||||
compiler.set('evmVersion', settings.evmVersion) |
|
||||||
compiler.set('optimize', settings.optimize) |
|
||||||
compiler.set('language', settings.language) |
|
||||||
compiler.set('runs', settings.runs) |
|
||||||
compiler.loadVersion(canUseWorker(settings.version), urlFromVersion(settings.version)) |
|
||||||
compiler.event.register('compilationFinished', (success, compilationData, source) => { |
|
||||||
resolve(new CompilerAbstract(settings.version, compilationData, source)) |
|
||||||
}) |
|
||||||
compiler.event.register('compilerLoaded', () => compiler.compile(compilationTargets, '')) |
|
||||||
}) |
|
||||||
} |
|
@ -1,46 +0,0 @@ |
|||||||
const semver = require('semver') |
|
||||||
const minixhr = require('minixhr') |
|
||||||
/* global Worker */ |
|
||||||
|
|
||||||
export const baseURLBin = 'https://binaries.soliditylang.org/bin' |
|
||||||
export const baseURLWasm = 'https://binaries.soliditylang.org/wasm' |
|
||||||
|
|
||||||
export const pathToURL = {} |
|
||||||
|
|
||||||
/** |
|
||||||
* Retrieves the URL of the given compiler version |
|
||||||
* @param version is the version of compiler with or without 'soljson-v' prefix and .js postfix |
|
||||||
*/ |
|
||||||
export function urlFromVersion (version) { |
|
||||||
if (!version.startsWith('soljson-v')) version = 'soljson-v' + version |
|
||||||
if (!version.endsWith('.js')) version = version + '.js' |
|
||||||
return `${pathToURL[version]}/${version}` |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Checks if the worker can be used to load a compiler. |
|
||||||
* checks a compiler whitelist, browser support and OS. |
|
||||||
*/ |
|
||||||
export function canUseWorker (selectedVersion) { |
|
||||||
const version = semver.coerce(selectedVersion) |
|
||||||
const isNightly = selectedVersion.includes('nightly') |
|
||||||
return browserSupportWorker() && ( |
|
||||||
// All compiler versions (including nightlies) after 0.6.3 are wasm compiled
|
|
||||||
semver.gt(version, '0.6.3') || |
|
||||||
// Only releases are wasm compiled starting with 0.3.6
|
|
||||||
(semver.gte(version, '0.3.6') && !isNightly) |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
function browserSupportWorker () { |
|
||||||
return document.location.protocol !== 'file:' && Worker !== undefined |
|
||||||
} |
|
||||||
|
|
||||||
// returns a promise for minixhr
|
|
||||||
export function promisedMiniXhr (url) { |
|
||||||
return new Promise((resolve) => { |
|
||||||
minixhr(url, (json, event) => { |
|
||||||
resolve({ json, event }) |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
@ -1,5 +1,2 @@ |
|||||||
export * from './compileTabLogic' |
export * from './compileTabLogic' |
||||||
export * from './compiler-abstract' |
|
||||||
export * from './compiler-helpers' |
|
||||||
export * from './compiler-utils' |
|
||||||
export * from './contract-parser' |
export * from './contract-parser' |
||||||
|
Loading…
Reference in new issue