Move @parity/resolc from js-revive (#296)

- Move npm package from paritytech/js-revive 
- Rename package to `@parity/resolc`
This commit is contained in:
PG Herveou
2025-04-30 17:24:52 +02:00
committed by GitHub
parent f6a412eef4
commit e07d0f0cb7
49 changed files with 940 additions and 75 deletions
+54
View File
@@ -0,0 +1,54 @@
const fs = require("fs");
const path = require("path");
const { minify } = require("terser");
const SOLJSON_URI =
"https://binaries.soliditylang.org/wasm/soljson-v0.8.29+commit.ab55807c.js";
const RESOLC_WASM_URI =
process.env.RELEASE_RESOLC_WASM_URI || "http://127.0.0.1:8080/resolc.wasm";
const RESOLC_WASM_TARGET_DIR = path.join(
__dirname,
"../../target/wasm32-unknown-emscripten/release",
);
const RESOLC_JS = path.join(RESOLC_WASM_TARGET_DIR, "resolc.js");
const RESOLC_WEB_JS = path.join(RESOLC_WASM_TARGET_DIR, "resolc_web.js");
const resolcJs = fs.readFileSync(RESOLC_JS, "utf-8");
const packedJsContent = `
if (typeof importScripts === "function") {
importScripts("${SOLJSON_URI}");
var moduleArgs = {
wasmBinary: (function () {
var xhr = new XMLHttpRequest();
xhr.open("GET", "${RESOLC_WASM_URI}", false);
xhr.responseType = "arraybuffer";
xhr.send(null);
return new Uint8Array(xhr.response);
})(),
soljson: Module
};
} else {
console.log("Not a WebWorker, skipping Soljson and WASM loading.");
}
${resolcJs}
createRevive = createRevive.bind(null, moduleArgs);
`;
minify(packedJsContent)
.then((minifiedJs) => {
if (minifiedJs.error) {
console.error("Error during minification:", minifiedJs.error);
process.exit(1);
}
fs.writeFileSync(RESOLC_WEB_JS, minifiedJs.code, "utf-8");
console.log(`Combined script written to ${RESOLC_WEB_JS}`);
})
.catch((err) => {
console.error("Minification failed:", err);
process.exit(1);
});
+152
View File
@@ -0,0 +1,152 @@
const { test, expect } = require("@playwright/test");
const fs = require("fs");
const path = require("path");
function loadFixture(fixture) {
const fixturePath = path.resolve(__dirname, `../fixtures/${fixture}`);
return JSON.parse(fs.readFileSync(fixturePath, "utf-8"));
}
async function loadTestPage(page) {
await page.goto("http://127.0.0.1:8080");
const outputElement = page.locator("#output");
await outputElement.waitFor({ state: "visible" });
await page.setContent("");
}
async function runWorker(page, input) {
return await page.evaluate((input) => {
return new Promise((resolve, reject) => {
const worker = new Worker("worker.js");
worker.postMessage(JSON.stringify(input));
worker.onmessage = (event) => {
resolve(event.data.output);
worker.terminate();
};
worker.onerror = (error) => {
reject(error.message || error);
worker.terminate();
};
});
}, input);
}
test("should successfully compile valid Solidity code in browser", async ({
page,
}) => {
await loadTestPage(page);
const standardInput = loadFixture("storage.json");
const result = await runWorker(page, standardInput);
expect(typeof result).toBe("string");
let output = JSON.parse(result);
expect(output).toHaveProperty("contracts");
expect(output.contracts["fixtures/storage.sol"]).toHaveProperty("Storage");
expect(output.contracts["fixtures/storage.sol"].Storage).toHaveProperty(
"abi",
);
expect(output.contracts["fixtures/storage.sol"].Storage).toHaveProperty(
"evm",
);
expect(output.contracts["fixtures/storage.sol"].Storage.evm).toHaveProperty(
"bytecode",
);
});
test("should successfully compile large valid Solidity code in browser", async ({
page,
browserName,
}) => {
if (browserName === "firefox") {
// Skipping tests with large contracts on Firefox due to out-of-memory issues.
test.skip();
}
await loadTestPage(page);
const standardInput = loadFixture("token.json");
const result = await runWorker(page, standardInput);
expect(typeof result).toBe("string");
let output = JSON.parse(result);
expect(output).toHaveProperty("contracts");
expect(output.contracts["fixtures/token.sol"]).toHaveProperty("MyToken");
expect(output.contracts["fixtures/token.sol"].MyToken).toHaveProperty("abi");
expect(output.contracts["fixtures/token.sol"].MyToken).toHaveProperty("evm");
expect(output.contracts["fixtures/token.sol"].MyToken.evm).toHaveProperty(
"bytecode",
);
});
test("should throw an error for invalid Solidity code in browser", async ({
page,
}) => {
await loadTestPage(page);
const standardInput = loadFixture("invalid_contract_content.json");
const result = await runWorker(page, standardInput);
expect(typeof result).toBe("string");
let output = JSON.parse(result);
expect(output).toHaveProperty("errors");
expect(Array.isArray(output.errors)).toBeTruthy(); // Check if it's an array
expect(output.errors.length).toBeGreaterThan(0);
expect(output.errors[0]).toHaveProperty("type");
expect(output.errors[0].type).toContain("ParserError");
});
test("should return not found error for missing imports in browser", async ({
page,
}) => {
await loadTestPage(page);
const standardInput = loadFixture("missing_import.json");
const result = await runWorker(page, standardInput);
expect(typeof result).toBe("string");
let output = JSON.parse(result);
expect(output).toHaveProperty("errors");
expect(Array.isArray(output.errors)).toBeTruthy(); // Check if it's an array
expect(output.errors.length).toBeGreaterThan(0);
expect(output.errors[0]).toHaveProperty("message");
expect(output.errors[0].message).toContain(
'Source "nonexistent/console.sol" not found',
);
});
test('should successfully compile a valid Solidity contract that instantiates another contract in the browser', async ({ page }) => {
await loadTestPage(page);
const standardInput = loadFixture('instantiate.json')
const result = await runWorker(page, standardInput);
expect(typeof result).toBe('string');
let output = JSON.parse(result);
expect(output).toHaveProperty('contracts');
expect(output.contracts['fixtures/instantiate.sol']).toHaveProperty('ChildContract');
expect(output.contracts['fixtures/instantiate.sol'].ChildContract).toHaveProperty('abi');
expect(output.contracts['fixtures/instantiate.sol'].ChildContract).toHaveProperty('evm');
expect(output.contracts['fixtures/instantiate.sol'].ChildContract.evm).toHaveProperty('bytecode');
expect(output.contracts['fixtures/instantiate.sol']).toHaveProperty('MainContract');
expect(output.contracts['fixtures/instantiate.sol'].MainContract).toHaveProperty('abi');
expect(output.contracts['fixtures/instantiate.sol'].MainContract).toHaveProperty('evm');
expect(output.contracts['fixtures/instantiate.sol'].MainContract.evm).toHaveProperty('bytecode');
});
test('should successfully compile a valid Solidity contract that instantiates the token contracts in the browser', async ({
page,
browserName,
}) => {
if (browserName === "firefox") {
// Skipping tests with large contracts on Firefox due to out-of-memory issues.
test.skip();
}
await loadTestPage(page);
const standardInput = loadFixture('instantiate_tokens.json')
const result = await runWorker(page, standardInput);
expect(typeof result).toBe('string');
let output = JSON.parse(result);
expect(output).toHaveProperty('contracts');
expect(output.contracts['fixtures/instantiate_tokens.sol']).toHaveProperty('TokensFactory');
expect(output.contracts['fixtures/instantiate_tokens.sol'].TokensFactory).toHaveProperty('abi');
expect(output.contracts['fixtures/instantiate_tokens.sol'].TokensFactory).toHaveProperty('evm');
expect(output.contracts['fixtures/instantiate_tokens.sol'].TokensFactory.evm).toHaveProperty('bytecode');
});
+57
View File
@@ -0,0 +1,57 @@
Module.stdinData = null;
Module.stdinDataPosition = 0;
Module.stdoutData = [];
Module.stderrData = [];
// Method to read all collected stdout data
Module.readFromStdout = function () {
if (!Module.stdoutData.length) return "";
const decoder = new TextDecoder("utf-8");
const data = decoder.decode(new Uint8Array(Module.stdoutData));
Module.stdoutData = [];
return data;
};
// Method to read all collected stderr data
Module.readFromStderr = function () {
if (!Module.stderrData.length) return "";
const decoder = new TextDecoder("utf-8");
const data = decoder.decode(new Uint8Array(Module.stderrData));
Module.stderrData = [];
return data;
};
// Method to write data to stdin
Module.writeToStdin = function (data) {
const encoder = new TextEncoder();
Module.stdinData = encoder.encode(data);
Module.stdinDataPosition = 0;
};
// Override the `preRun` method to customize file system initialization
Module.preRun = Module.preRun || [];
Module.preRun.push(function () {
// Custom stdin function
function customStdin() {
if (
!Module.stdinData ||
Module.stdinDataPosition >= Module.stdinData.length
) {
return null; // End of input (EOF)
}
return Module.stdinData[Module.stdinDataPosition++];
}
// Custom stdout function
function customStdout(char) {
Module.stdoutData.push(char);
}
// Custom stderr function
function customStderr(char) {
Module.stderrData.push(char);
}
// Initialize the FS (File System) with custom handlers
FS.init(customStdin, customStdout, customStderr);
});
+34
View File
@@ -0,0 +1,34 @@
mergeInto(LibraryManager.library, {
soljson_compile: function (inputPtr, inputLen) {
const inputJson = UTF8ToString(inputPtr, inputLen);
const output = Module.soljson.cwrap("solidity_compile", "string", [
"string",
])(inputJson);
return stringToNewUTF8(output);
},
soljson_version: function () {
const version = Module.soljson.cwrap("solidity_version", "string", [])();
return stringToNewUTF8(version);
},
resolc_compile: function (inputPtr, inputLen) {
const inputJson = UTF8ToString(inputPtr, inputLen);
var revive = createRevive();
revive.writeToStdin(inputJson);
// Call main on the new instance
const result = revive.callMain(["--recursive-process"]);
if (result) {
const stderrString = revive.readFromStderr();
const error = JSON.stringify({
type: "error",
message: stderrString || "Unknown error",
});
return stringToNewUTF8(error);
} else {
const stdoutString = revive.readFromStdout();
const json = JSON.stringify({ type: "success", data: stdoutString });
return stringToNewUTF8(json);
}
},
});
+1
View File
@@ -0,0 +1 @@
../../../../target/wasm32-unknown-emscripten/release/resolc.js
+1
View File
@@ -0,0 +1 @@
../../../../target/wasm32-unknown-emscripten/release/resolc.wasm
+32
View File
@@ -0,0 +1,32 @@
const soljson = require("solc/soljson");
const createRevive = require("./resolc.js");
async function compile(standardJsonInput) {
if (!standardJsonInput) {
throw new Error("Input JSON for the Solidity compiler is required.");
}
// Initialize the compiler
const compiler = createRevive();
compiler.soljson = soljson;
// Provide input to the compiler
compiler.writeToStdin(JSON.stringify(standardJsonInput));
// Run the compiler
compiler.callMain(["--standard-json"]);
// Collect output
const stdout = compiler.readFromStdout();
const stderr = compiler.readFromStderr();
// Check for errors and throw if stderr exists
if (stderr) {
throw new Error(`Compilation failed: ${stderr}`);
}
// Return the output if no errors
return stdout;
}
module.exports = { compile };
+38
View File
@@ -0,0 +1,38 @@
const { compile } = require("./revive.js");
const compilerStandardJsonInput = {
language: "Solidity",
sources: {
"MyContract.sol": {
content: `
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract MyContract {
function greet() public pure returns (string memory) {
return "Hello";
}
}
`,
},
},
settings: {
optimizer: {
enabled: true,
runs: 200,
},
outputSelection: {
"*": {
"*": ["abi"],
},
},
},
};
async function runCompiler() {
let output = await compile(compilerStandardJsonInput);
console.log("Output: " + output);
}
runCompiler().catch((err) => {
console.error("Error:", err);
});
+53
View File
@@ -0,0 +1,53 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Web Worker Example</title>
<style>
/* Ensure the pre tag wraps long lines */
pre {
white-space: pre-wrap; /* Wrap long lines */
word-wrap: break-word; /* Break long words */
max-width: 100%; /* Optional: Ensures it doesn't overflow container */
overflow-wrap: break-word; /* Another method for wrapping */
}
</style>
</head>
<body>
<h1>Revive Compilation Output</h1>
<pre id="output"></pre>
<script>
var outputElement = document.getElementById("output");
var worker = new Worker("./worker.js");
const standardJsonInput = {
language: "Solidity",
sources: {
contract: {
content: "contract MyContract { function f() public { } }",
},
},
settings: {
optimizer: {
enabled: true,
runs: 200,
},
outputSelection: {
"*": {
"*": ["abi"],
},
},
},
};
worker.addEventListener(
"message",
function (e) {
outputElement.textContent = e.data.output;
},
false,
);
worker.postMessage(JSON.stringify(standardJsonInput));
</script>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
../../../../target/wasm32-unknown-emscripten/release/resolc.wasm
+1
View File
@@ -0,0 +1 @@
../../../../target/wasm32-unknown-emscripten/release/resolc_web.js
+14
View File
@@ -0,0 +1,14 @@
importScripts("./resolc_web.js");
// Handle messages from the main thread
onmessage = async function (e) {
const m = createRevive();
// Set input data for stdin
m.writeToStdin(e.data);
// Compile the Solidity source code
m.callMain(["--standard-json"]);
postMessage({ output: m.readFromStdout() || m.readFromStderr() });
};
+19
View File
@@ -0,0 +1,19 @@
{
"language": "Solidity",
"sources": {
"fixtures/instantiate.sol": {
"content": "// SPDX-License-Identifier: GPL-3.0\npragma solidity >=0.8.2 <0.9.0;\ncontract ChildContract {\n constructor() {\n }\n}\ncontract MainContract {\n constructor() {\n ChildContract newContract = new ChildContract();\n }\n}"
}
},
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"outputSelection": {
"*": {
"*": ["abi"]
}
}
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,19 @@
{
"language": "Solidity",
"sources": {
"fixtures/storage.sol": {
"content": "// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.0;\nimport \"nonexistent/console.sol\";\ncontract MyContract { function greet() public pure returns (string memory) { return \"Hello\" // Missing semicolon }}"
}
},
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"outputSelection": {
"*": {
"*": ["abi"]
}
}
}
}
@@ -0,0 +1,19 @@
{
"language": "Solidity",
"sources": {
"fixtures/storage.sol": {
"content": "// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.0;\nimport \"nonexistent/console.sol\";\ncontract MyContract { function f() public { } }"
}
},
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"outputSelection": {
"*": {
"*": ["abi"]
}
}
}
}
+19
View File
@@ -0,0 +1,19 @@
{
"language": "Solidity",
"sources": {
"fixtures/storage.sol": {
"content": "// SPDX-License-Identifier: GPL-3.0\npragma solidity >=0.8.2 <0.9.0;\ncontract Storage {\n uint256 number;\n function store(uint256 num) public { number = num; }\n function retrieve() public view returns (uint256){ return number; }\n}"
}
},
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"outputSelection": {
"*": {
"*": ["abi"]
}
}
}
}
File diff suppressed because one or more lines are too long
+24
View File
@@ -0,0 +1,24 @@
{
"name": "revive",
"private": true,
"dependencies": {
"solc": "^0.8.29"
},
"scripts": {
"example:web": "http-server ./examples/web/",
"example:node": "node ./examples/node/run_revive.js",
"test:node": "mocha --timeout 60000 ./tests",
"test:bun": "bun test --timeout 60000 node.test",
"test:all": "npm run test:node && npm run test:bun",
"format": "prettier --write .",
"build:package": "node ./build.js"
},
"devDependencies": {
"@playwright/test": "^1.49.1",
"chai": "^5.1.2",
"http-server": "^14.1.1",
"mocha": "^11.0.1",
"prettier": "^3.4.2",
"terser": "^5.37.0"
}
}
+46
View File
@@ -0,0 +1,46 @@
const { defineConfig, devices } = require("@playwright/test");
/**
* @see https://playwright.dev/docs/test-configuration
*/
module.exports = defineConfig({
testDir: "./e2e",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "list",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://127.0.0.1:8080",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
timeout: 480000,
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
],
/* Run your local dev server before starting the tests */
webServer: {
command: "npm run example:web",
url: "http://127.0.0.1:8080",
reuseExistingServer: !process.env.CI,
},
});
+142
View File
@@ -0,0 +1,142 @@
import { expect } from "chai";
import { compile } from "../examples/node/revive.js";
import { fileURLToPath } from "url";
import path from "path";
import fs from "fs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
function loadFixture(fixture) {
const fixturePath = path.resolve(__dirname, `../fixtures/${fixture}`);
return JSON.parse(fs.readFileSync(fixturePath, "utf-8"));
}
describe("Compile Function Tests", function () {
it("should successfully compile valid Solidity code", async function () {
const standardInput = loadFixture("storage.json");
const result = await compile(standardInput);
expect(result).to.be.a("string");
const output = JSON.parse(result);
expect(output).to.have.property("contracts");
expect(output.contracts["fixtures/storage.sol"]).to.have.property(
"Storage",
);
expect(output.contracts["fixtures/storage.sol"].Storage).to.have.property(
"abi",
);
expect(output.contracts["fixtures/storage.sol"].Storage).to.have.property(
"evm",
);
expect(
output.contracts["fixtures/storage.sol"].Storage.evm,
).to.have.property("bytecode");
});
if (typeof globalThis.Bun == "undefined") {
// Running this test with Bun on a Linux host causes:
// RuntimeError: Out of bounds memory access (evaluating 'getWasmTableEntry(index)(a1, a2, a3, a4, a5)')
// Once this issue is resolved, the test will be re-enabled.
it("should successfully compile large Solidity code", async function () {
const standardInput = loadFixture("token.json");
const result = await compile(standardInput);
expect(result).to.be.a("string");
const output = JSON.parse(result);
expect(output).to.have.property("contracts");
expect(output.contracts["fixtures/token.sol"]).to.have.property(
"MyToken",
);
expect(output.contracts["fixtures/token.sol"].MyToken).to.have.property(
"abi",
);
expect(output.contracts["fixtures/token.sol"].MyToken).to.have.property(
"evm",
);
expect(
output.contracts["fixtures/token.sol"].MyToken.evm,
).to.have.property("bytecode");
});
it("should successfully compile a valid Solidity contract that instantiates the token contracts", async function () {
const standardInput = loadFixture("instantiate_tokens.json");
const result = await compile(standardInput);
expect(result).to.be.a("string");
const output = JSON.parse(result);
expect(output).to.have.property("contracts");
expect(
output.contracts["fixtures/instantiate_tokens.sol"],
).to.have.property("TokensFactory");
expect(
output.contracts["fixtures/instantiate_tokens.sol"].TokensFactory,
).to.have.property("abi");
expect(
output.contracts["fixtures/instantiate_tokens.sol"].TokensFactory,
).to.have.property("evm");
expect(
output.contracts["fixtures/instantiate_tokens.sol"].TokensFactory.evm,
).to.have.property("bytecode");
});
}
it("should throw an error for invalid Solidity code", async function () {
const standardInput = loadFixture("invalid_contract_content.json");
const result = await compile(standardInput);
expect(result).to.be.a("string");
const output = JSON.parse(result);
expect(output).to.have.property("errors");
expect(output.errors).to.be.an("array");
expect(output.errors.length).to.be.greaterThan(0);
expect(output.errors[0].type).to.exist;
expect(output.errors[0].type).to.contain("ParserError");
});
it("should return not found error for missing imports", async function () {
const standardInput = loadFixture("missing_import.json");
const result = await compile(standardInput);
const output = JSON.parse(result);
expect(output).to.have.property("errors");
expect(output.errors).to.be.an("array");
expect(output.errors.length).to.be.greaterThan(0);
expect(output.errors[0].message).to.exist;
expect(output.errors[0].message).to.include(
'Source "nonexistent/console.sol" not found',
);
});
it("should successfully compile a valid Solidity contract that instantiates another contract", async function () {
const standardInput = loadFixture("instantiate.json");
const result = await compile(standardInput);
expect(result).to.be.a("string");
const output = JSON.parse(result);
expect(output).to.have.property("contracts");
expect(output.contracts["fixtures/instantiate.sol"]).to.have.property(
"ChildContract",
);
expect(
output.contracts["fixtures/instantiate.sol"].ChildContract,
).to.have.property("abi");
expect(
output.contracts["fixtures/instantiate.sol"].ChildContract,
).to.have.property("evm");
expect(
output.contracts["fixtures/instantiate.sol"].ChildContract.evm,
).to.have.property("bytecode");
expect(output.contracts["fixtures/instantiate.sol"]).to.have.property(
"MainContract",
);
expect(
output.contracts["fixtures/instantiate.sol"].MainContract,
).to.have.property("abi");
expect(
output.contracts["fixtures/instantiate.sol"].MainContract,
).to.have.property("evm");
expect(
output.contracts["fixtures/instantiate.sol"].MainContract.evm,
).to.have.property("bytecode");
});
});