diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 4a7d76ea6..eb40544de 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -269,7 +269,7 @@ jobs: local-path: ./build/src/bindings/javascript/build/src/wasm remote-path: /home/www/opencor.ws/libopencor/downloads/wasm sync: full - - name: Publish libOpenCOR + - name: Publish libOpenCOR and its types # To publish using OIDC implies automatic provenance generation, so no need to specify --provenance (see # https://docs.npmjs.com/trusted-publishers). Provenance requires "repository.url" in package.json to match the # repository from which we are publishing. In our case, it means that we can only publish to npm from the @@ -277,3 +277,5 @@ jobs: run: | cd build/src/bindings/javascript/build/src npm publish --access public --no-git-checks + cd types + npm publish --access public --no-git-checks diff --git a/VERSION.txt b/VERSION.txt index 20169d42c..334205bc2 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -1.20260713.0 +1.20260716.0 diff --git a/cmake/common.cmake b/cmake/common.cmake index 64d411f17..463b49cfd 100644 --- a/cmake/common.cmake +++ b/cmake/common.cmake @@ -363,12 +363,14 @@ function(configure_target TARGET) # Mark as advanced the CMake variables set by our various packages. mark_as_advanced(CURL_DIR + CURL_USE_CMAKECONFIG + CURL_USE_PKGCONFIG Clang_DIR LLVM_DIR - Libssh2_DIR SUNDIALS_DIR combine-static_DIR libCellML_DIR + libssh2_DIR numl-static_DIR sbml-static_DIR sedml-static_DIR) diff --git a/cmake/environmentchecks.cmake b/cmake/environmentchecks.cmake index 387027b34..b0ab50156 100644 --- a/cmake/environmentchecks.cmake +++ b/cmake/environmentchecks.cmake @@ -127,10 +127,14 @@ if(Python_EXECUTABLE) mark_as_advanced(NB_CREATE_INSTALL_RULES NB_TEST - NB_TEST_SANITZE + NB_TEST_CUDA + NB_TEST_FREE_THREADED + NB_TEST_SANITIZERS_ASAN + NB_TEST_SANITIZERS_TSAN + NB_TEST_SANITIZERS_UBSAN NB_TEST_SHARED_BUILD NB_TEST_STABLE_ABI - nanobind_DIR) + NB_USE_SUBMODULE_DEPS) endif() # Look for various programs. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5c439f616..ceac4bbf9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -328,11 +328,14 @@ if(EMSCRIPTEN) # -sINITIAL_TABLE=65536 to have enough space for our WebAssembly table, -sINLINING_LIMIT to increase the # inlining limit, -sMODULARIZE=1 to generate a module factory function, -sSHARED_MEMORY=1 to enable shared # memory support, -sSTACK_SIZE=8MB to reserve a reasonable amount of memory for the stack at startup (which - # is needed for "big" models), and --bind to generate the necessary Embind bindings. + # is needed for "big" models), --bind to generate the necessary Embind bindings, and --emit-tsd to generate + # a TypeScript type definitions file. # Note #3: for linking, we used to have -sALLOW_MEMORY_GROWTH=1, -sINITIAL_MEMORY=256MB, and -sMAXIMUM_MEMORY=2GB, # but we now support threading and to allow memory growth might non-WASM code to run slowly. So, now, we # use -sINITIAL_MEMORY=2GB and don't allow memory growth. + set(DTS_FILE ${CMAKE_PROJECT_NAME}.d.ts) + target_compile_options(${CMAKE_PROJECT_NAME} PRIVATE -O3 -matomics @@ -355,25 +358,48 @@ if(EMSCRIPTEN) -sMODULARIZE=1 -sSHARED_MEMORY=1 -sSTACK_SIZE=8MB - --bind) + --bind + --emit-tsd ${DTS_FILE}) # Package our JavaScript bindings, if possible. + find_program(NODE_EXE NAMES node) find_program(NPM_EXE NAMES ${PREFERRED_NPM_NAMES} npm) - if(NPM_EXE) + if(NODE_EXE AND NPM_EXE) + # Post-process the generated .d.ts to match our EM_ASM runtime patches. + + add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD + COMMAND ${NODE_EXE} ${CMAKE_SOURCE_DIR}/src/bindings/javascript/postprocess.dts.mjs ${DTS_FILE}) + + # Package our JavaScript bindings into a .tgz file that can be published to npm. + set(TGZ_FILENAME ${REAL_CMAKE_PROJECT_NAME}-${PROJECT_VERSION}.tgz) set(TGZ_FILE ${CMAKE_BINARY_DIR}/../../../../${TGZ_FILENAME}) add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory dist + COMMAND ${CMAKE_COMMAND} -E copy ${DTS_FILE} dist COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_PROJECT_NAME}.js dist COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_PROJECT_NAME}.wasm dist - COMMAND ${CMAKE_COMMAND} -E copy package.json dist COMMAND ${NPM_EXE} pack COMMAND ${CMAKE_COMMAND} -E copy opencor-${TGZ_FILENAME} ${TGZ_FILE}) + + # Also package our .d.ts file as a standalone TypeScript type definitions package. + + set(TYPES_TGZ_FILENAME ${REAL_CMAKE_PROJECT_NAME}-types-${PROJECT_VERSION}.tgz) + set(TYPES_TGZ_FILE ${CMAKE_BINARY_DIR}/../../../../${TYPES_TGZ_FILENAME}) + + add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory types/dist + COMMAND ${CMAKE_COMMAND} -E copy package.types.json types/package.json + COMMAND ${CMAKE_COMMAND} -E copy README.md types + COMMAND ${CMAKE_COMMAND} -E copy LICENSE.txt types + COMMAND ${CMAKE_COMMAND} -E copy ${DTS_FILE} types/dist + COMMAND ${CMAKE_COMMAND} -E chdir types ${NPM_EXE} pack + COMMAND ${CMAKE_COMMAND} -E copy types/opencor-${TYPES_TGZ_FILENAME} ${TYPES_TGZ_FILE}) else() - message(WARNING "npm could not be found, so libOpenCOR's JavaScript bindings could not be packaged.") + message(FATAL_ERROR "The node and/or npm tools could not be found, so libOpenCOR's JavaScript bindings could not be finalised and packaged.") endif() # Copy our generated files over so that they can be deployed. @@ -381,15 +407,17 @@ if(EMSCRIPTEN) foreach(DEPLOY_DIR ${CMAKE_PROJECT_VERSION} latest) add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory wasm/${DEPLOY_DIR} - COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_PROJECT_NAME}.js wasm/${DEPLOY_DIR}/${CMAKE_PROJECT_NAME}.js - COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_PROJECT_NAME}.wasm wasm/${DEPLOY_DIR}/${CMAKE_PROJECT_NAME}.wasm) + COMMAND ${CMAKE_COMMAND} -E copy ${DTS_FILE} wasm/${DEPLOY_DIR} + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_PROJECT_NAME}.js wasm/${DEPLOY_DIR} + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_PROJECT_NAME}.wasm wasm/${DEPLOY_DIR}) endforeach() # Copy our generated files over so that they can be used by our test Web page. add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_PROJECT_NAME}.js ${JAVASCRIPT_BINDINGS_DIR}/${CMAKE_PROJECT_NAME}.js - COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_PROJECT_NAME}.wasm ${JAVASCRIPT_BINDINGS_DIR}/${CMAKE_PROJECT_NAME}.wasm) + COMMAND ${CMAKE_COMMAND} -E copy ${DTS_FILE} ${JAVASCRIPT_BINDINGS_DIR}/${DTS_FILE} + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_PROJECT_NAME}.js ${JAVASCRIPT_BINDINGS_DIR} + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_PROJECT_NAME}.wasm ${JAVASCRIPT_BINDINGS_DIR}) # Let people know how to run our test Web page. diff --git a/src/bindings/javascript/CMakeLists.txt b/src/bindings/javascript/CMakeLists.txt index e8184aba3..adcdc6f3d 100644 --- a/src/bindings/javascript/CMakeLists.txt +++ b/src/bindings/javascript/CMakeLists.txt @@ -55,6 +55,8 @@ if(EMSCRIPTEN OR LIBOPENCOR_JAVASCRIPT_BINDINGS) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/package.in.json ${CMAKE_CURRENT_BINARY_DIR}/../../package.json @ONLY) configure_file(${CMAKE_SOURCE_DIR}/README.md ${CMAKE_CURRENT_BINARY_DIR}/../../README.md) configure_file(${CMAKE_SOURCE_DIR}/LICENSE.txt ${CMAKE_CURRENT_BINARY_DIR}/../../LICENSE.txt) + + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/package.types.in.json ${CMAKE_CURRENT_BINARY_DIR}/../../package.types.json @ONLY) else() # Build our JavaScript bindings. diff --git a/src/bindings/javascript/file.cpp b/src/bindings/javascript/file.cpp index d216c3e3a..05cb17c65 100644 --- a/src/bindings/javascript/file.cpp +++ b/src/bindings/javascript/file.cpp @@ -83,9 +83,11 @@ void fileApi() // clang-format off EM_ASM({ - Module["File"]["Type"] = Module["File.Type"]; + if (Module["File"]) { + Module["File"]["Type"] = Module["File.Type"]; - delete Module["File.Type"]; + delete Module["File.Type"]; + } }); // clang-format on // FileManager API. diff --git a/src/bindings/javascript/logger.cpp b/src/bindings/javascript/logger.cpp index 1bf2f698b..a85dccade 100644 --- a/src/bindings/javascript/logger.cpp +++ b/src/bindings/javascript/logger.cpp @@ -48,12 +48,14 @@ void loggerApi() // clang-format off EM_ASM({ - Module["Issue"]["Type"] = Module["Issue.Type"]; + if (Module["Issue"]) { + Module["Issue"]["Type"] = Module["Issue.Type"]; - delete Module["Issue.Type"]; + delete Module["Issue.Type"]; - Module["Issue"].prototype.toString = function() { - return this.description; - }; + Module["Issue"].prototype.toString = function() { + return this.description; + }; + } }); // clang-format on } diff --git a/src/bindings/javascript/main.cpp b/src/bindings/javascript/main.cpp index 802bbf51f..552927022 100644 --- a/src/bindings/javascript/main.cpp +++ b/src/bindings/javascript/main.cpp @@ -60,7 +60,13 @@ EMSCRIPTEN_BINDINGS(libOpenCOR) let vectorNames = 'Doubles|FilePtrs|IssuePtrs|SedAbstractTaskPtrs|SedChangePtrs|SedDataDescriptionPtrs|SedDataGeneratorPtrs|SedInstanceTaskPtrs|SedModelPtrs|SedOutputPtrs|SedSimulationPtrs|SedStylePtrs|Strings'.split('|'); vectorNames.forEach((name) => { - let prototype = Module[name].prototype; + let vectorClass = Module[name]; + + if (!vectorClass) { + return; + } + + let prototype = vectorClass.prototype; Object.defineProperty(prototype, 'length', { get: function() { diff --git a/src/bindings/javascript/package.in.json b/src/bindings/javascript/package.in.json index 766920fdc..078be4ff0 100644 --- a/src/bindings/javascript/package.in.json +++ b/src/bindings/javascript/package.in.json @@ -6,22 +6,27 @@ "bugs": { "url": "https://github.com/opencor/libopencor/issues" }, - "description": "libOpenCOR is is a library that can be used to organise, edit, simulate, and analyse CellML files.", + "description": "libOpenCOR is a library that can be used to organise, edit, simulate, and analyse CellML files.", "exports": { - ".": "./dist/libopencor.js", - "./libopencor.wasm": "./dist/libopencor.wasm" + ".": { + "types": "./dist/libopencor.d.ts", + "import": "./dist/libopencor.js" + } }, - "files": ["dist/libopencor.js", "dist/libopencor.wasm", "README.md", "LICENSE.txt"], + "files": ["dist/libopencor.d.ts", "dist/libopencor.js", "dist/libopencor.wasm", "README.md", "LICENSE.txt"], "homepage": "https://opencor.ws/libopencor/", "keywords": ["cellml", "combine", "omex", "opencor", "sed-ml"], "license": "Apache-2.0", + "main": "dist/libopencor.js", "module": "dist/libopencor.js", "name": "@opencor/libopencor", "productName": "libOpenCOR", + "sideEffects": false, "repository": { "type": "git", "url": "git+https://github.com/opencor/libopencor.git" }, "type": "module", + "types": "dist/libopencor.d.ts", "version": "@PROJECT_VERSION@" } diff --git a/src/bindings/javascript/package.types.in.json b/src/bindings/javascript/package.types.in.json new file mode 100644 index 000000000..175f16cb4 --- /dev/null +++ b/src/bindings/javascript/package.types.in.json @@ -0,0 +1,25 @@ +{ + "author": { + "name": "Alan Garny", + "email": "a.garny@auckland.ac.nz" + }, + "bugs": { + "url": "https://github.com/opencor/libopencor/issues" + }, + "description": "libOpenCOR TypeScript type definitions.", + "files": ["dist/libopencor.d.ts", "README.md", "LICENSE.txt"], + "homepage": "https://opencor.ws/libopencor/", + "keywords": ["cellml", "combine", "omex", "opencor", "sed-ml", "types"], + "license": "Apache-2.0", + "main": "dist/libopencor.d.ts", + "name": "@opencor/libopencor-types", + "productName": "libOpenCOR types", + "sideEffects": false, + "repository": { + "type": "git", + "url": "git+https://github.com/opencor/libopencor.git" + }, + "type": "module", + "types": "dist/libopencor.d.ts", + "version": "@PROJECT_VERSION@" +} diff --git a/src/bindings/javascript/postprocess.dts.mjs b/src/bindings/javascript/postprocess.dts.mjs new file mode 100644 index 000000000..60b6694c0 --- /dev/null +++ b/src/bindings/javascript/postprocess.dts.mjs @@ -0,0 +1,207 @@ +/* +Copyright libOpenCOR contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { readFileSync, writeFileSync } from 'node:fs'; +import { basename } from 'node:path'; + +// Find the matching closing brace for a given opening brace. + +function findMatchingClosingBrace(text, start) { + let depth = 0; + + for (let i = start; i < text.length; ++i) { + if (text[i] === '{') { + ++depth; + } else if (text[i] === '}') { + if (--depth === 0) { + return i + 1; + } + } + } + + return -1; +} + +// Normalise an interface declaration by adding 2 extra spaces of indentation to every non-empty line in the body +// and re-indenting the closing brace by 2 spaces. + +function normaliseInterfaceDeclaration(interfaceDeclaration) { + const openingBraceIndex = interfaceDeclaration.indexOf('{'); + const rest = interfaceDeclaration.slice(openingBraceIndex + 1); + const closingBraceIndex = rest.lastIndexOf('}'); + + return `${interfaceDeclaration.slice(0, openingBraceIndex + 1)}${rest.slice(0, closingBraceIndex).replace(/^(?!$)/gm, ' ')} ${rest.slice(closingBraceIndex)}`; +} + +// Collect dotted interface and type declarations into a map of namespace -> name -> +// { interfaceDeclaration, typeDeclaration }. + +const enumValueRegEx = /export interface (\w+\.\w+)Value\s*\{/g; +const filePath = process.argv[2]; +let content = readFileSync(filePath, 'utf8'); +const namespaceData = new Map(); + +for (let match = enumValueRegEx.exec(content); match !== null; match = enumValueRegEx.exec(content)) { + const fullName = match[1]; + const dotIndex = fullName.indexOf('.'); + const namespace = fullName.slice(0, dotIndex); + + if (!namespaceData.has(namespace)) { + namespaceData.set(namespace, new Map()); + } + + const members = namespaceData.get(namespace); + const memberName = fullName.slice(dotIndex + 1); + const baseName = memberName.endsWith('Value') ? memberName.slice(0, -5) : memberName; + + if (!members.has(baseName)) { + members.set(baseName, {}); + } + + const closingBraceIndex = findMatchingClosingBrace(content, content.indexOf('{', match.index)); + + members.get(baseName).interfaceDeclaration = content.slice(match.index, closingBraceIndex); +} + +// Collect dotted type declarations into the same map. + +const enumTypeRegEx = /export type (\w+\.\w+) = ([\w.]+Value<\d+>(?:\|[\w.]+Value<\d+>)*);/g; + +for (let match = enumTypeRegEx.exec(content); match !== null; match = enumTypeRegEx.exec(content)) { + const fullName = match[1]; + const dotIndex = fullName.indexOf('.'); + const namespace = fullName.slice(0, dotIndex); + + if (!namespaceData.has(namespace)) { + namespaceData.set(namespace, new Map()); + } + + const members = namespaceData.get(namespace); + const name = fullName.slice(dotIndex + 1); + + if (!members.has(name)) { + members.set(name, {}); + } + + members.get(name).typeDeclaration = match[0]; +} + +// Generate namespace blocks for each namespace and remove the old dotted declarations from the content. + +const namespaceBlocks = []; + +for (const [namespace, members] of namespaceData) { + const lines = [`export namespace ${namespace} {`]; + + for (const [_name, data] of members) { + // Remove the namespace prefix from the interface and type declarations. + + const namespaceReferenceRegEx = new RegExp(`${namespace}\\.`, 'g'); + + if (data.interfaceDeclaration) { + const stripped = data.interfaceDeclaration.replace(namespaceReferenceRegEx, ''); + + lines.push(` ${normaliseInterfaceDeclaration(stripped)}`); + + // Remove the old dotted declaration from content. + + content = content.replace(data.interfaceDeclaration, ''); + } + + // Add the type declaration (e.g., export type Type = TypeValue<1> | TypeValue<2>;). + + if (data.typeDeclaration) { + const stripped = data.typeDeclaration.replace(namespaceReferenceRegEx, '').replace(/\|/g, ' | '); + + lines.push(` ${stripped}`); + + // Remove the old dotted declaration from content. + + content = content.replace(data.typeDeclaration, ''); + } + } + + lines.push('}'); + + namespaceBlocks.push(lines.join('\n')); +} + +// Remove any remaining dotted property declarations from the EmbindModule interface. + +for (const [namespace] of namespaceData) { + const propertyRegEx = new RegExp(`\\n ${namespace}\\.\\w+: \\{[^}]*\\};`, 'g'); + + content = content.replace(propertyRegEx, ''); +} + +// Remove any excessive blank lines that may have been introduced by the above operations. + +content = content.replace(/\n{4,}/g, '\n\n\n'); + +// Add any missing properties to the EmbindModule interface. + +for (const [namespace, members] of namespaceData) { + const entryRegEx = new RegExp(`(\\n ${namespace}: \\{[^}]*)(\\};)`); + + if (entryRegEx.test(content)) { + const existingEntry = content.match(entryRegEx)[1]; + let newProperties = ''; + + for (const [name] of members) { + const propertyLine = ` readonly ${name}: typeof ${namespace}.${name};`; + + if (!existingEntry.includes(propertyLine)) { + newProperties += `\n${propertyLine}`; + } + } + + if (newProperties) { + content = content.replace(entryRegEx, `${existingEntry.replace(/\s+$/, '')}${newProperties}\n $2`); + } + } +} + +// Insert the namespace blocks into the EmbindModule interface. + +if (namespaceBlocks.length) { + const embindIndex = content.search(/\ninterface EmbindModule/); + + if (embindIndex !== -1) { + const insertPos = embindIndex + 1; + + content = `${content.slice(0, insertPos)}${namespaceBlocks.join('\n\n')}\n\n${content.slice(insertPos)}`; + } +} + +// Add a toString() method to the Issue interface if it doesn't already exist. + +const issueInterfaceStart = content.search(/export interface Issue\s+(extends\s+\w+\s+)?\{/); +const issueInterfaceEnd = findMatchingClosingBrace( + content, + issueInterfaceStart + content.slice(issueInterfaceStart).indexOf('{') +); + +const issueBody = content.slice(issueInterfaceStart, issueInterfaceEnd); + +if (!issueBody.includes('toString():')) { + content = `${content.slice(0, issueInterfaceEnd - 1)} toString(): string;\n${content.slice(issueInterfaceEnd - 1)}`; +} + +// Update the file in place. + +writeFileSync(filePath, content, 'utf8'); + +console.log(`-- Patched ${basename(filePath)}`); diff --git a/src/bindings/javascript/sed.cpp b/src/bindings/javascript/sed.cpp index 5d271890f..e8292b529 100644 --- a/src/bindings/javascript/sed.cpp +++ b/src/bindings/javascript/sed.cpp @@ -45,11 +45,13 @@ void sedApi() // SedDataDescription API. - emscripten::class_>("SedDataDescription"); + emscripten::class_>("SedDataDescription") + .smart_ptr("SedDataDescription"); // SedDataGenerator API. - emscripten::class_>("SedDataGenerator"); + emscripten::class_>("SedDataGenerator") + .smart_ptr("SedDataGenerator"); // SedDocument API. @@ -143,7 +145,8 @@ void sedApi() // SedOutput API. - emscripten::class_>("SedOutput"); + emscripten::class_>("SedOutput") + .smart_ptr("SedOutput"); // SedRepeatedTask API. @@ -186,7 +189,8 @@ void sedApi() // SedStyle API. - emscripten::class_>("SedStyle"); + emscripten::class_>("SedStyle") + .smart_ptr("SedStyle"); // SedTask API. diff --git a/src/bindings/javascript/solver.cpp b/src/bindings/javascript/solver.cpp index 2ba0d50c3..41cce5298 100644 --- a/src/bindings/javascript/solver.cpp +++ b/src/bindings/javascript/solver.cpp @@ -33,13 +33,15 @@ void solverApi() // clang-format off EM_ASM({ - Module["Solver"]["Type"] = Module["Solver.Type"]; + if (Module["Solver"]) { + Module["Solver"]["Type"] = Module["Solver.Type"]; - delete Module["Solver.Type"]; + delete Module["Solver.Type"]; - Module["Solver"].prototype.toString = function() { - return this.name; - }; + Module["Solver"].prototype.toString = function() { + return this.name; + }; + } }); // clang-format on // SolverOde API. @@ -50,6 +52,7 @@ void solverApi() // SolverOdeFixedStep API. emscripten::class_>("SolverOdeFixedStep") + .smart_ptr("SolverOdeFixedStep") .property("step", &libOpenCOR::SolverOdeFixedStep::step, &libOpenCOR::SolverOdeFixedStep::setStep); // SolverNla API. @@ -103,15 +106,17 @@ void solverApi() .property("interpolateSolution", &libOpenCOR::SolverCvode::interpolateSolution, &libOpenCOR::SolverCvode::setInterpolateSolution); EM_ASM({ - Module["SolverCvode"]["IntegrationMethod"] = Module["SolverCvode.IntegrationMethod"]; - Module["SolverCvode"]["IterationType"] = Module["SolverCvode.IterationType"]; - Module["SolverCvode"]["LinearSolver"] = Module["SolverCvode.LinearSolver"]; - Module["SolverCvode"]["Preconditioner"] = Module["SolverCvode.Preconditioner"]; - - delete Module["SolverCvode.IntegrationMethod"]; - delete Module["SolverCvode.IterationType"]; - delete Module["SolverCvode.LinearSolver"]; - delete Module["SolverCvode.Preconditioner"]; + if (Module["SolverCvode"]) { + Module["SolverCvode"]["IntegrationMethod"] = Module["SolverCvode.IntegrationMethod"]; + Module["SolverCvode"]["IterationType"] = Module["SolverCvode.IterationType"]; + Module["SolverCvode"]["LinearSolver"] = Module["SolverCvode.LinearSolver"]; + Module["SolverCvode"]["Preconditioner"] = Module["SolverCvode.Preconditioner"]; + + delete Module["SolverCvode.IntegrationMethod"]; + delete Module["SolverCvode.IterationType"]; + delete Module["SolverCvode.LinearSolver"]; + delete Module["SolverCvode.Preconditioner"]; + } }); // SolverForwardEuler API. @@ -146,9 +151,11 @@ void solverApi() .property("lowerHalfBandwidth", &libOpenCOR::SolverKinsol::lowerHalfBandwidth, &libOpenCOR::SolverKinsol::setLowerHalfBandwidth); EM_ASM({ - Module["SolverKinsol"]["LinearSolver"] = Module["SolverKinsol.LinearSolver"]; + if (Module["SolverKinsol"]) { + Module["SolverKinsol"]["LinearSolver"] = Module["SolverKinsol.LinearSolver"]; - delete Module["SolverKinsol.LinearSolver"]; + delete Module["SolverKinsol.LinearSolver"]; + } }); // SolverSecondOrderRungeKutta API.