diff --git a/apps/remix-ide-e2e/src/commands/verifyContracts.ts b/apps/remix-ide-e2e/src/commands/verifyContracts.ts index aef1b06217..f29c3e42c6 100644 --- a/apps/remix-ide-e2e/src/commands/verifyContracts.ts +++ b/apps/remix-ide-e2e/src/commands/verifyContracts.ts @@ -2,7 +2,7 @@ import { NightwatchBrowser } from 'nightwatch' import EventEmitter from 'events' class VerifyContracts extends EventEmitter { - command (this: NightwatchBrowser, compiledContractNames: string[], opts = { wait: 1000, version: null }): NightwatchBrowser { + command (this: NightwatchBrowser, compiledContractNames: string[], opts = { wait: 1000, version: null, runs: '200' }): NightwatchBrowser { this.api.perform((done) => { verifyContracts(this.api, compiledContractNames, opts, () => { done() @@ -13,13 +13,13 @@ class VerifyContracts extends EventEmitter { } } -function verifyContracts (browser: NightwatchBrowser, compiledContractNames: string[], opts: { wait: number, version?: string }, callback: VoidFunction) { +function verifyContracts (browser: NightwatchBrowser, compiledContractNames: string[], opts: { wait: number, version?: string, runs?: string }, callback: VoidFunction) { browser .clickLaunchIcon('solidity') .pause(opts.wait) .pause(5000) .waitForElementPresent('*[data-id="compiledContracts"] option', 60000) - .perform((done) => { + .perform(async (done) => { if (opts.version) { browser .click('*[data-id="compilation-details"]') @@ -36,10 +36,28 @@ function verifyContracts (browser: NightwatchBrowser, compiledContractNames: str done() callback() }) - } else { - compiledContractNames.forEach((name) => { - browser.waitForElementContainsText('[data-id="compiledContracts"]', name, 60000) + } if (opts.runs) { + browser + .click('*[data-id="compilation-details"]') + .waitForElementVisible('*[data-id="remixui_treeviewitem_metadata"]') + .pause(2000) + .click('*[data-id="remixui_treeviewitem_metadata"]') + .waitForElementVisible('*[data-id="treeViewDivtreeViewItemsettings"]') + .pause(2000) + .click('*[data-id="treeViewDivtreeViewItemsettings"]') + .waitForElementVisible('*[data-id="treeViewDivtreeViewItemoptimizer"]') + .click('*[data-id="treeViewDivtreeViewItemoptimizer"]') + .waitForElementVisible('*[data-id="treeViewDivruns"]') + .assert.containsText('*[data-id="treeViewDivruns"]', `${opts.runs}`) + .click('[data-id="workspacesModalDialog-modal-footer-ok-react"]') + .perform(() => { + done() + callback() }) + } else { + for (const index in compiledContractNames) { + await browser.waitForElementContainsText('[data-id="compiledContracts"]', compiledContractNames[index], 60000) + } done() callback() } diff --git a/apps/remix-ide-e2e/src/tests/ballot.test.ts b/apps/remix-ide-e2e/src/tests/ballot.test.ts index 794dee9775..5244241e89 100644 --- a/apps/remix-ide-e2e/src/tests/ballot.test.ts +++ b/apps/remix-ide-e2e/src/tests/ballot.test.ts @@ -122,6 +122,24 @@ module.exports = { }) // Test in Udapp UI , treeViewDiv0 shows returned value on method click .assert.containsText('*[data-id="treeViewDiv0"]', 'bytes32: winnerName_ 0x48656c6c6f20576f726c64210000000000000000000000000000000000000000') + }, + + 'Compile Ballot using config file': function (browser: NightwatchBrowser) { + browser + .addFile('cf.json', {content: configFile}) + .clickLaunchIcon('solidity') + .waitForElementVisible('*[data-id="scConfigExpander"]') + .click('*[data-id="scConfigExpander"]') + .waitForElementVisible('*[data-id="scFileConfiguration"]', 10000) + .click('*[data-id="scFileConfiguration"]') + .waitForElementVisible('*[data-id="scConfigChangeFilePath"]', 10000) + .click('*[data-id="scConfigChangeFilePath"]') + .waitForElementVisible('*[data-id="scConfigFilePathInput"]', 10000) + .clearValue('*[data-id="scConfigFilePathInput"]') + .setValue('*[data-id="scConfigFilePathInput"]', 'cf.json') + .sendKeys('*[data-id$="scConfigFilePathInput"]', browser.Keys.ENTER) + .openFile('Untitled.sol') + .verifyContracts(['Ballot'], {wait: 2000, runs: '300'}) .end() } } @@ -190,6 +208,7 @@ const stateCheck = { immutable: false } } + const ballotABI = `[ { "inputs": [ @@ -356,3 +375,22 @@ const ballotABI = `[ "type": "function" } ]` + +const configFile = ` +{ + "language": "Solidity", + "settings": { + "optimizer": { + "enabled": true, + "runs": 300 + }, + "outputSelection": { + "*": { + "": ["ast"], + "*": ["abi", "metadata", "devdoc", "userdoc", "storageLayout", "evm.legacyAssembly", "evm.bytecode", "evm.deployedBytecode", "evm.methodIdentifiers", "evm.gasEstimates", "evm.assembly"] + } + }, + "evmVersion": "byzantium" + } +} +` \ No newline at end of file diff --git a/apps/remix-ide-e2e/src/tests/compiler_api.test.ts b/apps/remix-ide-e2e/src/tests/compiler_api.test.ts index 2adc5a4c62..0646b83f80 100644 --- a/apps/remix-ide-e2e/src/tests/compiler_api.test.ts +++ b/apps/remix-ide-e2e/src/tests/compiler_api.test.ts @@ -168,3 +168,4 @@ contract DoesNotCompile { function fStackLimit(uint u1, uint u2, uint u3, uint u4, uint u5, uint u6, uint u7, uint u8, uint u9, uint u10, uint u11, uint u12) public { } }` + diff --git a/apps/remix-ide-e2e/src/tests/plugin_api.ts b/apps/remix-ide-e2e/src/tests/plugin_api.ts index cf696f2bec..faabf03247 100644 --- a/apps/remix-ide-e2e/src/tests/plugin_api.ts +++ b/apps/remix-ide-e2e/src/tests/plugin_api.ts @@ -231,7 +231,6 @@ module.exports = { 'Should get current files #group7': async function (browser: NightwatchBrowser) { await clickAndCheckLog(browser, 'fileManager:readdir', { - 'compiler_config.json': { isDirectory: false }, contracts: { isDirectory: true }, scripts: { isDirectory: true }, tests: { isDirectory: true }, @@ -286,15 +285,12 @@ module.exports = { 'Should create empty workspace #group2': async function (browser: NightwatchBrowser) { await clickAndCheckLog(browser, 'filePanel:createWorkspace', null, null, ['emptyworkspace', true]) await clickAndCheckLog(browser, 'filePanel:getCurrentWorkspace', { name: 'emptyworkspace', isLocalhost: false, absolutePath: '.workspaces/emptyworkspace' }, null, null) - await clickAndCheckLog(browser, 'fileManager:readdir', { - 'compiler_config.json': { isDirectory: false } - }, null, '/') + await clickAndCheckLog(browser, 'fileManager:readdir', {}, null, '/') }, 'Should create workspace #group2': async function (browser: NightwatchBrowser) { await clickAndCheckLog(browser, 'filePanel:createWorkspace', null, null, 'testspace') await clickAndCheckLog(browser, 'filePanel:getCurrentWorkspace', { name: 'testspace', isLocalhost: false, absolutePath: '.workspaces/testspace' }, null, null) await clickAndCheckLog(browser, 'fileManager:readdir', { - 'compiler_config.json': { isDirectory: false }, contracts: { isDirectory: true }, scripts: { isDirectory: true }, tests: { isDirectory: true }, diff --git a/apps/remix-ide-e2e/src/tests/workspace.test.ts b/apps/remix-ide-e2e/src/tests/workspace.test.ts index f5fea02e36..0c36951a76 100644 --- a/apps/remix-ide-e2e/src/tests/workspace.test.ts +++ b/apps/remix-ide-e2e/src/tests/workspace.test.ts @@ -107,8 +107,7 @@ module.exports = { const fileList = document.querySelector('*[data-id="treeViewUltreeViewMenu"]') return fileList.getElementsByTagName('li').length; }, [], function(result){ - // check there are no files in FE except config file - browser.assert.equal(result.value, 1, 'Incorrect number of files'); + browser.assert.equal(result.value, 0, 'Incorrect number of files'); }); }, diff --git a/apps/remix-ide-e2e/src/types/index.d.ts b/apps/remix-ide-e2e/src/types/index.d.ts index e8698d4407..7fbcb174d5 100644 --- a/apps/remix-ide-e2e/src/types/index.d.ts +++ b/apps/remix-ide-e2e/src/types/index.d.ts @@ -11,7 +11,7 @@ declare module 'nightwatch' { testContracts(fileName: string, contractCode: NightwatchContractContent, compiledContractNames: string[]): NightwatchBrowser, setEditorValue(value: string, callback?: () => void): NightwatchBrowser, addFile(name: string, content: NightwatchContractContent): NightwatchBrowser, - verifyContracts(compiledContractNames: string[], opts?: { wait: number, version?: string }): NightwatchBrowser, + verifyContracts(compiledContractNames: string[], opts?: { wait: number, version?: string, runs?: string }): NightwatchBrowser, selectAccount(account?: string): NightwatchBrowser, clickFunction(fnFullName: string, expectedInput?: NightwatchClickFunctionExpectedInput): NightwatchBrowser, testFunction(txHash: string, expectedInput: NightwatchTestFunctionExpectedInput): NightwatchBrowser, diff --git a/apps/remix-ide/src/app/editor/editor.js b/apps/remix-ide/src/app/editor/editor.js index 50f7f42d76..97dc63f988 100644 --- a/apps/remix-ide/src/app/editor/editor.js +++ b/apps/remix-ide/src/app/editor/editor.js @@ -167,6 +167,32 @@ class Editor extends Plugin { } }) + this.on('fileManager', 'noFileSelected', async () => { + this.currentFile = null + this.renderComponent() + }) + this.on('fileManager', 'currentFileChanged', async (name) => { + if (name.endsWith('.ts')) { + // extract the import, resolve their content + // and add the imported files to Monaco through the `addModel` + // so Monaco can provide auto completion + let content = await this.call('fileManager', 'readFile', name) + const paths = name.split('/') + paths.pop() + const fromPath = paths.join('/') // get current execution context path + for (const match of content.matchAll(/import\s+.*\s+from\s+(?:"(.*?)"|'(.*?)')/g)) { + let path = match[2] + if (path.startsWith('./') || path.startsWith('../')) path = resolve(fromPath, path) + if (path.startsWith('/')) path = path.substring(1) + if (!path.endsWith('.ts')) path = path + '.ts' + if (await this.call('fileManager', 'exists', path)) { + content = await this.call('fileManager', 'readFile', path) + this.emit('addModel', content, 'typescript', path, false) + } + } + } + }) + this.on('fileManager', 'noFileSelected', async () => { this.currentFile = null this.renderComponent() diff --git a/apps/remix-ide/src/app/tabs/compile-tab.js b/apps/remix-ide/src/app/tabs/compile-tab.js index 8e3d17e366..23a53846f7 100644 --- a/apps/remix-ide/src/app/tabs/compile-tab.js +++ b/apps/remix-ide/src/app/tabs/compile-tab.js @@ -56,6 +56,10 @@ class CompileTab extends CompilerApiMixin(ViewPlugin) { // implements ICompilerA this.renderComponent() } + onFileRemoved () { + this.renderComponent() + } + onNoFileSelected () { this.renderComponent() } diff --git a/apps/solidity-compiler/src/app/compiler-api.ts b/apps/solidity-compiler/src/app/compiler-api.ts index fb8d848039..3065af630a 100644 --- a/apps/solidity-compiler/src/app/compiler-api.ts +++ b/apps/solidity-compiler/src/app/compiler-api.ts @@ -19,6 +19,7 @@ export const CompilerApiMixin = (Base) => class extends Base { onCurrentFileChanged: (fileName: string) => void // onResetResults: () => void onSetWorkspace: (isLocalhost: boolean, workspaceName: string) => void + onFileRemoved: (path: string) => void onNoFileSelected: () => void onCompilationFinished: (compilationDetails: { contractMap: { file: string } | Record, contractsDetails: Record }) => void onSessionSwitched: () => void @@ -240,6 +241,10 @@ export const CompilerApiMixin = (Base) => class extends Base { if (this.onSetWorkspace) this.onSetWorkspace(workspace.isLocalhost, workspace.name) }) + this.on('fileManager', 'fileRemoved', (path) => { + if (this.onFileRemoved) this.onFileRemoved(path) + }) + this.on('remixd', 'rootFolderChanged', () => { this.resetResults() if (this.onSetWorkspace) this.onSetWorkspace(true, 'localhost') @@ -282,23 +287,20 @@ export const CompilerApiMixin = (Base) => class extends Base { type: 'warning' }) } else this.statusChanged({ key: 'succeed', title: 'compilation successful', type: 'success' }) - // Store the contracts - this.compilationDetails.contractsDetails = {} - this.compiler.visitContracts((contract) => { - this.compilationDetails.contractsDetails[contract.name] = parseContracts( - contract.name, - contract.object, - this.compiler.getSource(contract.file) - ) - }) } else { const count = (data.errors ? data.errors.filter(error => error.severity === 'error').length : 0 + (data.error ? 1 : 0)) this.statusChanged({ key: count, title: `compilation failed with ${count} error${count > 1 ? 's' : ''}`, type: 'error' }) } - // Update contract Selection - this.compilationDetails.contractMap = {} - if (success) this.compiler.visitContracts((contract) => { this.compilationDetails.contractMap[contract.name] = contract }) - this.compilationDetails.target = source.target + // Store the contracts and Update contract Selection + if (success) { + this.compilationDetails = await this.visitsContractApi(source, data) + } else { + this.compilationDetails = { + contractMap: {}, + contractsDetails: {}, + target: source.target + } + } if (this.onCompilationFinished) this.onCompilationFinished(this.compilationDetails) // set annotations if (data.errors) { @@ -342,4 +344,31 @@ export const CompilerApiMixin = (Base) => class extends Base { } window.document.addEventListener('keydown', this.data.eventHandlers.onKeyDown) } + + async visitsContractApi (source, data): Promise<{ contractMap: { file: string } | Record, contractsDetails: Record, target?: string }> { + return new Promise((resolve) => { + if (!data.contracts || (data.contracts && Object.keys(data.contracts).length === 0)) { + return resolve({ + contractMap: {}, + contractsDetails: {}, + target: source.target + }) + } + const contractMap = {} + const contractsDetails = {} + this.compiler.visitContracts((contract) => { + contractMap[contract.name] = contract + contractsDetails[contract.name] = parseContracts( + contract.name, + contract.object, + this.compiler.getSource(contract.file) + ) + }) + return resolve({ + contractMap, + contractsDetails, + target: source.target + }) + }) + } } diff --git a/libs/remix-lib/src/types/ICompilerApi.ts b/libs/remix-lib/src/types/ICompilerApi.ts index f34d20f420..259aa2547a 100644 --- a/libs/remix-lib/src/types/ICompilerApi.ts +++ b/libs/remix-lib/src/types/ICompilerApi.ts @@ -25,6 +25,7 @@ export interface ICompilerApi { onCurrentFileChanged: (fileName: string) => void // onResetResults: () => void, onSetWorkspace: (isLocalhost: boolean, workspaceName: string) => void + onFileRemoved: (path: string) => void onNoFileSelected: () => void onCompilationFinished: (contractsDetails: any, contractMap: any) => void onSessionSwitched: () => void diff --git a/libs/remix-ui/editor/src/lib/remix-plugin-types.ts b/libs/remix-ui/editor/src/lib/remix-plugin-types.ts index ef781936c0..ceb49f5399 100644 --- a/libs/remix-ui/editor/src/lib/remix-plugin-types.ts +++ b/libs/remix-ui/editor/src/lib/remix-plugin-types.ts @@ -226,7 +226,7 @@ declare interface CondensedCompilationInput { optimize: boolean /** e.g: 0.6.8+commit.0bbfe453 */ version: string - evmVersion?: 'istanbul' | 'petersburg' | 'constantinople' | 'byzantium' | 'spuriousDragon' | 'tangerineWhistle' | 'homestead' + evmVersion?: 'berlin' | 'istanbul' | 'petersburg' | 'constantinople' | 'byzantium' | 'spuriousDragon' | 'tangerineWhistle' | 'homestead' } declare interface ContentImport { diff --git a/libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx b/libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx index 17d3baa2d2..bc06aae822 100644 --- a/libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx +++ b/libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx @@ -12,6 +12,7 @@ import { CopyToClipboard } from '@remix-ui/clipboard' import { configFileContent } from './compilerConfiguration' import './css/style.css' +const defaultPath = "compiler_config.json" declare global { interface Window { @@ -22,11 +23,23 @@ declare global { const _paq = window._paq = window._paq || [] //eslint-disable-line export const CompilerContainer = (props: CompilerContainerProps) => { - const { api, compileTabLogic, tooltip, modal, compiledFileName, updateCurrentVersion, configurationSettings, isHardhatProject, isTruffleProject, workspaceName } = props // eslint-disable-line + const { + api, + compileTabLogic, + tooltip, + modal, + compiledFileName, + updateCurrentVersion, + configurationSettings, + isHardhatProject, + isTruffleProject, + workspaceName, + configFilePath, + setConfigFilePath, + } = props // eslint-disable-line const [state, setState] = useState({ hideWarnings: false, autoCompile: false, - configFilePath: "compiler_config.json", useFileConfiguration: false, matomoAutocompileOnce: true, optimize: false, @@ -40,7 +53,8 @@ export const CompilerContainer = (props: CompilerContainerProps) => { compiledFileName: '', includeNightlies: false, language: 'Solidity', - evmVersion: '' + evmVersion: '', + createFileOnce: true }) const [showFilePathInput, setShowFilePathInput] = useState(false) const [toggleExpander, setToggleExpander] = useState(false) @@ -53,17 +67,27 @@ export const CompilerContainer = (props: CompilerContainerProps) => { const [compilerContainer, dispatch] = useReducer(compilerReducer, compilerInitialState) useEffect(() => { - api.setAppParameter('configFilePath', "/compiler_config.json") - api.fileExists("/compiler_config.json").then((exists) => { - if (!exists) createNewConfigFile() - else { - // what to do? discuss + if (workspaceName) { + api.setAppParameter('configFilePath', defaultPath) + if (state.useFileConfiguration) { + api.fileExists(defaultPath).then((exists) => { + if (!exists && state.useFileConfiguration) createNewConfigFile() + }) } - }) - api.setAppParameter('configFilePath', "/compiler_config.json") - setShowFilePathInput(false) + setShowFilePathInput(false) + } }, [workspaceName]) + useEffect(() => { + if (state.useFileConfiguration) { + api.fileExists(defaultPath).then((exists) => { + if (!exists) createNewConfigFile() + }) + setToggleExpander(true) + } + }, [state.useFileConfiguration]) + + useEffect(() => { const listener = (event) => { if (configFilePathInput.current !== event.target) { @@ -106,8 +130,10 @@ export const CompilerContainer = (props: CompilerContainerProps) => { const hideWarnings = await api.getAppParameter('hideWarnings') as boolean || false const includeNightlies = await api.getAppParameter('includeNightlies') as boolean || false const useFileConfiguration = await api.getAppParameter('useFileConfiguration') as boolean || false - let configFilePath = await api.getAppParameter('configFilePath') - if (!configFilePath || configFilePath == '') configFilePath = "/compiler_config.json" + let configFilePathSaved = await api.getAppParameter('configFilePath') + if (!configFilePathSaved || configFilePathSaved == '') configFilePathSaved = defaultPath + + setConfigFilePath(configFilePathSaved) setState(prevState => { const params = api.getCompilerParameters() @@ -122,7 +148,6 @@ export const CompilerContainer = (props: CompilerContainerProps) => { autoCompile: autocompile, includeNightlies: includeNightlies, useFileConfiguration: useFileConfiguration, - configFilePath: configFilePath, optimize: optimize, runs: runs, evmVersion: (evmVersion !== null) && (evmVersion !== 'null') && (evmVersion !== undefined) && (evmVersion !== 'undefined') ? evmVersion : 'default', @@ -181,7 +206,7 @@ export const CompilerContainer = (props: CompilerContainerProps) => { useEffect(() => { compileTabLogic.setUseFileConfiguration(state.useFileConfiguration) - if (state.useFileConfiguration) compileTabLogic.setConfigFilePath(state.configFilePath) + if (state.useFileConfiguration) compileTabLogic.setConfigFilePath(configFilePath) }, [state.useFileConfiguration]) useEffect(() => { @@ -191,6 +216,16 @@ export const CompilerContainer = (props: CompilerContainerProps) => { }, [configurationSettings]) const toggleConfigType = () => { + if (state.useFileConfiguration) + if (state.createFileOnce) { + api.fileExists(defaultPath).then((exists) => { + if (!exists || state.useFileConfiguration ) createNewConfigFile() + }) + setState(prevState => { + return { ...prevState, createFileOnce: false } + }) + } + setState(prevState => { api.setAppParameter('useFileConfiguration', !state.useFileConfiguration) return { ...prevState, useFileConfiguration: !state.useFileConfiguration } @@ -198,18 +233,17 @@ export const CompilerContainer = (props: CompilerContainerProps) => { } const openFile = async () => { - api.open(state.configFilePath) + api.open(configFilePath) } const createNewConfigFile = async () => { - let filePath = configFilePathInput.current && configFilePathInput.current.value !== '' ? configFilePathInput.current.value : state.configFilePath + let filePath = configFilePathInput.current && configFilePathInput.current.value !== '' ? configFilePathInput.current.value : configFilePath + if (filePath === '') filePath = defaultPath if (!filePath.endsWith('.json')) filePath = filePath + '.json' await api.writeFile(filePath, configFileContent) api.setAppParameter('configFilePath', filePath) - setState(prevState => { - return { ...prevState, configFilePath: filePath } - }) + setConfigFilePath(filePath) compileTabLogic.setConfigFilePath(filePath) setShowFilePathInput(false) } @@ -220,9 +254,7 @@ export const CompilerContainer = (props: CompilerContainerProps) => { if (await api.fileExists(configFilePathInput.current.value)) { api.setAppParameter('configFilePath', configFilePathInput.current.value) - setState(prevState => { - return { ...prevState, configFilePath: configFilePathInput.current.value } - }) + setConfigFilePath(configFilePathInput.current.value) compileTabLogic.setConfigFilePath(configFilePathInput.current.value) setShowFilePathInput(false) @@ -394,6 +426,9 @@ export const CompilerContainer = (props: CompilerContainerProps) => { compileIcon.current.classList.remove('remixui_spinningIcon') compileIcon.current.classList.remove('remixui_bouncingIcon') if (!state.autoCompile || (state.autoCompile && state.matomoAutocompileOnce)) { + if (state.useFileConfiguration) + _paq.push(['trackEvent', 'compiler', 'compiled_with_config_file']) + _paq.push(['trackEvent', 'compiler', 'compiled_with_version', _retrieveVersion()]) if (state.autoCompile && state.matomoAutocompileOnce) { setState(prevState => { @@ -750,36 +785,38 @@ export const CompilerContainer = (props: CompilerContainerProps) => {
- +
{ (!showFilePathInput && state.useFileConfiguration) && {state.configFilePath} } - { (!showFilePathInput&& !state.useFileConfiguration) && {state.configFilePath} } + onClick={configFilePath === '' ? () => {} : openFile} + className="py-2 remixui_compilerConfigPath" + >{configFilePath === '' ? 'No file selected.' : configFilePath} } + { (!showFilePathInput && !state.useFileConfiguration) && {configFilePath} } { if (event.key === 'Enter') { handleConfigPathChange() } }} /> - { !showFilePathInput && } + { !showFilePathInput && }
-
-
-
-
-
+
+
+
{ (global.fs.mode === 'browser') && (currentWorkspace !== NO_WORKSPACE) &&