├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── browser.js ├── browser.spec.js ├── node.js ├── node.spec.js ├── test.mzn ├── tests.cjs └── worker.js ├── tsconfig.json ├── typedoc.json └── types └── index.d.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "npm" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | groups: 12 | production: 13 | dependency-type: "production" 14 | development: 15 | dependency-type: "development" 16 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build, test, release 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | schedule: 8 | - cron: "0 0 * * *" 9 | 10 | env: 11 | STABLE: ${{ (github.ref_name == 'stable' || startsWith(github.ref, 'refs/tags/v')) && 'yes' || 'no' }} 12 | 13 | jobs: 14 | minizinc: 15 | name: Build wasm version of MiniZinc 16 | runs-on: ubuntu-latest 17 | container: emscripten/emsdk 18 | outputs: 19 | cache-key: ${{ steps.get-cache-key.outputs.key }} 20 | cache-hit: ${{ steps.cache.outputs.cache-hit }} 21 | steps: 22 | - name: Workaround for https://github.com/actions/runner/issues/2033 23 | run: | 24 | git config --global --add safe.directory $GITHUB_WORKSPACE 25 | - name: Checkout latest MiniZinc 26 | if: ${{ env.STABLE == 'yes' }} 27 | uses: actions/checkout@v4 28 | with: 29 | repository: minizinc/libminizinc 30 | ref: master 31 | - name: Checkout edge MiniZinc 32 | if: ${{ env.STABLE == 'no' }} 33 | uses: actions/checkout@v4 34 | with: 35 | repository: minizinc/libminizinc 36 | ref: develop 37 | - name: Download vendor 38 | run: ./download_vendor 39 | env: 40 | MZNARCH: wasm 41 | - name: Get MiniZinc cache key 42 | id: get-cache-key 43 | run: echo "key=minizinc-$(git rev-parse HEAD)-${{ hashFiles('vendor/version.json') }}" >> $GITHUB_OUTPUT 44 | - name: Cache MiniZinc Build 45 | id: cache 46 | uses: actions/cache@v4 47 | with: 48 | path: minizinc 49 | key: ${{ steps.get-cache-key.outputs.key }} 50 | - name: Configure MiniZinc 51 | if: steps.cache.outputs.cache-hit != 'true' 52 | run: | 53 | emcmake cmake -S . -B build \ 54 | -DCMAKE_FIND_ROOT_PATH="/" \ 55 | -DCMAKE_BUILD_TYPE=Release \ 56 | -DBUILD_REF=$GITHUB_RUN_ID \ 57 | -DGecode_ROOT="$GITHUB_WORKSPACE/vendor/gecode" \ 58 | -DOsiCBC_ROOT="$GITHUB_WORKSPACE/vendor/cbc" \ 59 | -DCMAKE_PREFIX_PATH="$GITHUB_WORKSPACE/vendor/highs/lib/cmake/highs:$GITHUB_WORKSPACE/vendor/chuffed/lib/cmake/chuffed" \ 60 | -DCMAKE_INSTALL_PREFIX="$GITHUB_WORKSPACE/minizinc" 61 | - name: Build MiniZinc 62 | if: steps.cache.outputs.cache-hit != 'true' 63 | run: cmake --build build --config Release --target install -j 2 64 | - name: Upload package artifact 65 | uses: actions/upload-artifact@v4 66 | with: 67 | name: minizinc 68 | path: minizinc/ 69 | 70 | build: 71 | name: Build and test minizinc-js 72 | runs-on: ubuntu-latest 73 | needs: [minizinc] 74 | if: github.event_name != 'schedule' || needs.minizinc.outputs.cache-hit != 'true' 75 | env: 76 | MZN_WASM_DIR: ${{ github.workspace }}/minizinc 77 | MZN_NODE_BINARY: ${{ github.workspace }}/native/bin/minizinc 78 | steps: 79 | - uses: actions/checkout@v4 80 | - name: Cache MiniZinc 81 | id: cache 82 | uses: actions/cache@v4 83 | with: 84 | path: minizinc 85 | key: ${{ needs.minizinc.outputs.cache-key }} 86 | - name: Fetch MiniZinc 87 | if: steps.cache.outputs.cache-hit != 'true' 88 | uses: actions/download-artifact@v4 89 | with: 90 | name: minizinc 91 | path: ${{ github.workspace }}/minizinc 92 | - name: Fetch latest MiniZinc image 93 | if: ${{ env.STABLE == 'yes' }} 94 | uses: docker://minizinc/minizinc:latest 95 | with: 96 | args: sh -c "mkdir -p $GITHUB_WORKSPACE/native && cp -v -r /usr/local/* $GITHUB_WORKSPACE/native" 97 | - name: Fetch edge MiniZinc image 98 | if: ${{ env.STABLE == 'no' }} 99 | uses: docker://minizinc/minizinc:edge 100 | with: 101 | args: sh -c "mkdir -p $GITHUB_WORKSPACE/native && cp -v -r /usr/local/* $GITHUB_WORKSPACE/native" 102 | - uses: actions/setup-node@v4 103 | with: 104 | node-version: lts/* 105 | - name: Install dependencies 106 | run: npm ci 107 | - name: Bump edge version 108 | if: ${{ env.STABLE == 'no' }} 109 | run: | 110 | npm version prerelease --preid=edge --no-git-tag-version > /dev/null 111 | PACKAGE=$(jq -r .name package.json) 112 | if PUBLISHED_EDGE=$(npm view "${PACKAGE}@edge" version 2>/dev/null); then 113 | echo "Latest published edge version is ${PUBLISHED_EDGE}" 114 | P1=$(echo "${PUBLISHED_EDGE}" | cut -d - -f 1) 115 | P2=$(jq -r .version package.json | cut -d - -f 1) 116 | if [[ "$P1" == "$P2" ]]; then 117 | jq --arg new_version "${PUBLISHED_EDGE}" '.version|=$new_version' package.json > package.json.tmp 118 | rm package.json 119 | mv package.json.tmp package.json 120 | npm version prerelease --preid=edge --no-git-tag-version > /dev/null 121 | fi 122 | fi 123 | echo "Bumped edge version to $(jq -r .version package.json)" 124 | - name: Build package 125 | run: npm run build 126 | - name: Run tests 127 | run: npm test 128 | - name: Build docs 129 | run: npm run docs 130 | - name: Create package 131 | run: npm pack 132 | - name: Upload docs artifact 133 | uses: actions/upload-artifact@v4 134 | with: 135 | name: docs 136 | path: docs/ 137 | - name: Upload package artifact 138 | uses: actions/upload-artifact@v4 139 | with: 140 | name: package 141 | path: minizinc-*.tgz 142 | 143 | publish: 144 | name: Publish minizinc-js 145 | runs-on: ubuntu-latest 146 | needs: [build] 147 | if: ${{ github.event_name != 'pull_request' && ( startsWith(github.ref, 'refs/tags/v') || github.ref_name == 'develop' ) }} 148 | steps: 149 | - uses: actions/setup-node@v4 150 | with: 151 | node-version: lts/* 152 | registry-url: https://registry.npmjs.org 153 | - name: Fetch package 154 | uses: actions/download-artifact@v4 155 | with: 156 | name: package 157 | path: ${{ github.workspace }} 158 | - name: Publish edge version 159 | if: ${{ github.ref_name == 'develop' }} 160 | run: | 161 | if [ -z "$NODE_AUTH_TOKEN" ]; then 162 | npm publish minizinc-*.tgz --tag edge --dry-run 163 | else 164 | npm publish minizinc-*.tgz --tag edge 165 | fi 166 | env: 167 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 168 | - name: Publish latest version 169 | if: ${{ startsWith(github.ref, 'refs/tags/v') }} 170 | run: | 171 | if [ -z "$NODE_AUTH_TOKEN" ]; then 172 | npm publish minizinc-*.tgz --dry-run 173 | else 174 | npm publish minizinc-*.tgz 175 | fi 176 | env: 177 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 178 | 179 | pages: 180 | name: Publish documentation 181 | runs-on: ubuntu-latest 182 | needs: [build] 183 | if: ${{ github.event_name != 'pull_request' && ( github.ref_name == 'develop' || github.ref_name == 'stable' || startsWith(github.ref, 'refs/tags/v') ) }} 184 | steps: 185 | - name: Fetch documentation 186 | uses: actions/download-artifact@v4 187 | with: 188 | name: docs 189 | path: ${{ github.workspace }}/docs 190 | - name: Inject version selector script and favicon into head 191 | run: find ./docs -type f -name '*.html' -exec sh -c 'sed -i "s###" {}' \; 192 | - name: Set package usages to point to edge 193 | if: ${{ github.ref_name == 'develop' }} 194 | run: | 195 | sed -i 's#/minizinc/dist/#/minizinc@edge/dist/#g' docs/index.html 196 | sed -i 's/npm install minizinc/npm install minizinc@edge/g' docs/index.html 197 | sed -i 's#https://js.minizinc.dev/docs/stable/#https://js.minizinc.dev/docs/develop/#g' docs/index.html 198 | - name: Publish documentation 199 | uses: peaceiris/actions-gh-pages@v4 200 | with: 201 | github_token: ${{ secrets.GITHUB_TOKEN }} 202 | publish_dir: ./docs 203 | destination_dir: docs/${{ github.ref_name }} 204 | enable_jekyll: true 205 | cname: ${{ vars.PAGES_CNAME }} 206 | 207 | post_publish: 208 | name: Post-publish minizinc-js 209 | runs-on: ubuntu-latest 210 | needs: [publish] 211 | steps: 212 | - name: Purge jsDelivr edge cache 213 | if: ${{ github.ref_name == 'develop' }} 214 | uses: gacts/purge-jsdelivr-cache@v1 215 | with: 216 | url: | 217 | https://cdn.jsdelivr.net/npm/minizinc@edge/dist/minizinc.js 218 | https://cdn.jsdelivr.net/npm/minizinc@edge/dist/minizinc.mjs 219 | https://cdn.jsdelivr.net/npm/minizinc@edge/dist/minizinc-worker.js 220 | https://cdn.jsdelivr.net/npm/minizinc@edge/dist/minizinc.wasm 221 | https://cdn.jsdelivr.net/npm/minizinc@edge/dist/minizinc.data 222 | - name: Purge jsDelivr latest cache 223 | if: ${{ startsWith(github.ref, 'refs/tags/v') }} 224 | uses: gacts/purge-jsdelivr-cache@v1 225 | with: 226 | url: | 227 | https://cdn.jsdelivr.net/npm/minizinc/dist/minizinc.js 228 | https://cdn.jsdelivr.net/npm/minizinc/dist/minizinc.mjs 229 | https://cdn.jsdelivr.net/npm/minizinc/dist/minizinc-worker.js 230 | https://cdn.jsdelivr.net/npm/minizinc/dist/minizinc.wasm 231 | https://cdn.jsdelivr.net/npm/minizinc/dist/minizinc.data 232 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Serverless directories 108 | .serverless/ 109 | 110 | # FuseBox cache 111 | .fusebox/ 112 | 113 | # DynamoDB Local files 114 | .dynamodb/ 115 | 116 | # TernJS port file 117 | .tern-port 118 | 119 | # Stores VSCode versions used for testing VSCode extensions 120 | .vscode-test 121 | 122 | # yarn v2 123 | .yarn/cache 124 | .yarn/unplugged 125 | .yarn/build-state.yml 126 | .yarn/install-state.gz 127 | .pnp.* 128 | 129 | bin/ 130 | docs/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JavaScript interface for MiniZinc 2 | ================================= 3 | 4 | [![Latest documentation](https://img.shields.io/badge/docs-latest-blue)](https://js.minizinc.dev/docs/stable) 5 | [![Latest package](https://img.shields.io/npm/v/minizinc/latest?color=blue)](https://www.npmjs.com/package/minizinc) 6 | [![Edge documentation](https://img.shields.io/badge/docs-edge-orange)](https://js.minizinc.dev/docs/develop) 7 | [![Edge package](https://img.shields.io/npm/v/minizinc/edge?color=orange)](https://www.npmjs.com/package/minizinc) 8 | 9 | This package provides a JavaScript API for [MiniZinc](https://minizinc.dev) 10 | for use in web browsers using WebAssembly, or in NodeJS using a native 11 | installation of MiniZinc. 12 | 13 | This library powers the [MiniZinc Playground](https://minizinc.dev/solve). 14 | 15 | ## Getting started 16 | 17 | ### Using a CDN (recommended) 18 | 19 | Using ECMAScript modules: 20 | 21 | ```html 22 | 39 | ``` 40 | 41 | Using a traditional script: 42 | 43 | ```html 44 | 45 | 61 | ``` 62 | 63 | ### Self-hosting WebAssembly files 64 | 65 | If you're using a bundler, you can add the library to your project: 66 | 67 | ```sh 68 | npm install minizinc 69 | ``` 70 | 71 | Then import it with: 72 | 73 | ```js 74 | import * as MiniZinc from 'minizinc'; 75 | ``` 76 | 77 | These three files need to be served by your webserver (found in `node_modules/minizinc/dist`): 78 | 79 | - `minizinc-worker.js` 80 | - `minizinc.wasm` 81 | - `minizinc.data` 82 | 83 | If you place them alongside your bundled script, they should be found automatically. 84 | Otherwise, their URLs can be specified during [initialisation](#initialisation). 85 | 86 | ### In NodeJS 87 | 88 | This requires an existing [installation of MiniZinc](https://github.com/MiniZinc/MiniZincIDE/releases). 89 | 90 | Add the library with: 91 | 92 | ```sh 93 | npm install minizinc 94 | ``` 95 | 96 | Then import it with: 97 | 98 | ```js 99 | // If using ESM 100 | import * as MiniZinc from 'minizinc'; 101 | // If using CommonJS 102 | const MiniZinc = require('minizinc'); 103 | ``` 104 | 105 | If you have added MiniZinc to your `PATH`, it will be found automatically. 106 | Otherwise, you can specify the executable path during [initialisation](#initialisation). 107 | 108 | ## Usage 109 | 110 | ### Initialisation 111 | 112 | Initialisation happens automatically when the library is used, or by calling 113 | [`init(...)`](https://js.minizinc.dev/docs/stable/functions/init.html). This can be used to ensure 114 | that the WebAssembly files start loading immediately, or to specify a different URL for the worker 115 | (or path to the MiniZinc executable if using NodeJS). 116 | 117 | In the browser: 118 | 119 | ```js 120 | MiniZinc.init({ 121 | // If omitted, searches for minizinc-worker.js next to the minizinc library script 122 | workerURL: 'http://localhost:3000/path/to/my-own-worker.js', 123 | // If these are omitted, searches next to the worker script 124 | wasmURL: 'http://localhost:3000/path/to/minizinc.wasm', 125 | dataURL: 'http://localhost:3000/path/to/minizinc.data' 126 | }).then(() => { 127 | console.log('Ready'); 128 | }); 129 | ``` 130 | 131 | In NodeJS: 132 | 133 | ```js 134 | MiniZinc.init({ 135 | // Executable name 136 | minizinc: 'minizinc', 137 | // Search paths (can omit to use PATH) 138 | minizincPaths: ['/home/me/.local/bin', '/usr/local/bin'] 139 | }); 140 | ``` 141 | 142 | By default, the NodeJS version tries to find MiniZinc on your `PATH`. 143 | 144 | ### Creating Models 145 | 146 | The main entrypoint for using the library is through the 147 | [`Model`](https://js.minizinc.dev/docs/stable/classes/Model.html) class: 148 | 149 | ```js 150 | const model = new MiniZinc.Model(); 151 | // Add a file with a given name and string contents 152 | model.addFile('test.mzn', 'var 1..3: x; int: y;'); 153 | // If you're using NodeJS, you can add files from the filesystem directly 154 | model.addFile('test.mzn'); 155 | // Add model code from a string 156 | model.addString('int: z;'); 157 | // Add data in DZN format 158 | model.addDznString('y = 1;'); 159 | // Add data from a JSON object 160 | model.addJSON({z: 2}); 161 | ``` 162 | 163 | ### Solving 164 | 165 | Solving is done using the [`Model.solve(...)`](https://js.minizinc.dev/docs/stable/classes/Model.html#solve) method, 166 | which takes an object with `options` in [`.mpc`](https://minizinc.dev/doc-latest/en/command_line.html#ch-param-files) 167 | format. 168 | 169 | ```js 170 | const solve = model.solve({ 171 | options: { 172 | solver: 'gecode', 173 | 'time-limit': 10000, 174 | statistics: true 175 | } 176 | }); 177 | // You can listen for events 178 | solve.on('solution', solution => console.log(solution.output.json)); 179 | solve.on('statistics', stats => console.log(stats.statistics)); 180 | // And/or wait until complete 181 | solve.then(result => { 182 | console.log(result.solution.output.json); 183 | console.log(result.statistics); 184 | }); 185 | ``` 186 | 187 | During solving, MiniZinc emits events which can be subscribed to/unsubscribed from using the 188 | [`SolveProgress.on`](https://js.minizinc.dev/docs/stable/interfaces/SolveProgress.html#on) / 189 | [`SolveProgress.off`](https://js.minizinc.dev/docs/stable/interfaces/SolveProgress.html#off) 190 | methods. The events are those which appear in 191 | [Machine-readable JSON output format](https://minizinc.dev/doc-latest/en/json-stream.html), 192 | with the addition of the [`exit`](https://js.minizinc.dev/docs/stable/interfaces/ExitMessage.html) 193 | event, which can be used to detect when solving finishes (if you do not wish to await the 194 | [`SolveProgress`](https://js.minizinc.dev/docs/stable/interfaces/SolveProgress.html) object). 195 | 196 | By default, `--output-mode json` is used, allowing you to retrieve the model variable values 197 | directly from the solution objects. Use 198 | [`Model.solve({ jsonOutput: false, ...})`](https://js.minizinc.dev/docs/stable/classes/Model.html#solve) 199 | (and optionally specify a different `output-mode` in the `options`) to disable this behaviour. 200 | 201 | ## Documentation 202 | 203 | For more detailed documentation of all available options and functionality, visit the 204 | [API documentation](https://js.minizinc.dev/docs/stable/). 205 | 206 | ## Building 207 | 208 | ### Compiling MiniZinc for WebAssembly 209 | 210 | The WebAssembly build of MiniZinc requires [Emscripten](https://emscripten.org/). 211 | 212 | ```sh 213 | # Clone MiniZinc 214 | git clone https://github.com/MiniZinc/libminizinc minizinc 215 | 216 | # Download solvers (or you can build them yourself using emscripten) 217 | cd minizinc 218 | MZNARCH=wasm ./download_vendor 219 | 220 | # Configure MiniZinc 221 | emcmake cmake -S . -B build \ 222 | -DCMAKE_FIND_ROOT_PATH="/" \ 223 | -DCMAKE_BUILD_TYPE=Release \ 224 | -DGecode_ROOT="$PWD/vendor/gecode" \ 225 | -DOsiCBC_ROOT="$PWD/vendor/cbc" \ 226 | -DCMAKE_PREFIX_PATH="$PWD/vendor/highs/lib/cmake/highs:$PWD/vendor/chuffed/lib/cmake/chuffed" \ 227 | -DCMAKE_INSTALL_PREFIX="../minizinc-install" 228 | 229 | # Build MiniZinc 230 | cmake --build build --config Release --target install 231 | ``` 232 | 233 | The WebAssembly build of MiniZinc can also be obtained from the [build workflow](https://github.com/MiniZinc/minizinc-js/actions/workflows/build.yml) as the `minizinc` artifact. 234 | 235 | ### Building MiniZinc JS 236 | 237 | 1. Run `npm install` to install dependencies. 238 | 2. Place the `bin/` folder of the WebAssembly build of MiniZinc inside this directory. 239 | Alternatively set the `MZN_WASM_DIR` environment variable to the installation directory of the 240 | WebAssembly build of MiniZinc. 241 | 3. Run `npm run build` to build the package. The built files are in the `dist/` directory. 242 | 4. Run `npm run docs` to build the documentation. The output files are in the `docs/` directory. 243 | 244 | ## Testing 245 | 246 | When testing, the [`web-worker`](https://www.npmjs.com/package/web-worker) library is used to emulate Web Worker 247 | support in NodeJS. This allows us to test both the browser version using WebAssembly, as well as the native version. 248 | 249 | Run `npm test` to run tests using [Jest](https://jestjs.io). 250 | 251 | ## License 252 | 253 | This library is distributed under the Mozilla Public License Version 2.0. See LICENSE for more information. 254 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minizinc", 3 | "version": "4.4.3", 4 | "description": "JavaScript API for MiniZinc", 5 | "homepage": "https://js.minizinc.dev", 6 | "types": "types/index.d.ts", 7 | "main": "dist/minizinc.mjs", 8 | "type": "module", 9 | "exports": { 10 | ".": { 11 | "types": "./types/index.d.ts", 12 | "node": { 13 | "require": "./dist/minizinc-node.cjs", 14 | "default": "./dist/minizinc-node.mjs" 15 | }, 16 | "require": "./dist/minizinc.cjs", 17 | "default": "./dist/minizinc.mjs" 18 | }, 19 | "./minizinc-worker.js": "./dist/minizinc-worker.js", 20 | "./minizinc.wasm": "./dist/minizinc.wasm", 21 | "./minizinc.data": "./dist/minizinc.data" 22 | }, 23 | "scripts": { 24 | "test": "rollup -c --environment TEST && jest", 25 | "dev": "rollup -w -c", 26 | "build": "rollup -c", 27 | "docs": "typedoc" 28 | }, 29 | "files": [ 30 | "dist/minizinc*", 31 | "types/**/*.d.ts" 32 | ], 33 | "author": "Jason Nguyen ", 34 | "license": "MPL-2.0", 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/MiniZinc/minizinc-js.git" 38 | }, 39 | "devDependencies": { 40 | "@el3um4s/rollup-plugin-terser": "^1.0.2", 41 | "@rollup/plugin-alias": "^5.1.1", 42 | "@rollup/plugin-commonjs": "^28.0.3", 43 | "@rollup/plugin-replace": "^6.0.2", 44 | "@rollup/plugin-url": "^8.0.2", 45 | "jest": "^29.7.0", 46 | "rollup": "^4.41.0", 47 | "rollup-plugin-copy": "^3.5.0", 48 | "typedoc": "^0.28.4", 49 | "web-worker": "1.3.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from "@rollup/plugin-commonjs"; 2 | import copy from "rollup-plugin-copy"; 3 | import { terser } from "@el3um4s/rollup-plugin-terser"; 4 | import replace from "@rollup/plugin-replace"; 5 | import alias from "@rollup/plugin-alias"; 6 | import path from "path"; 7 | import fs from "fs"; 8 | 9 | const pkg = JSON.parse(fs.readFileSync("./package.json")); 10 | 11 | const testing = process.env.TEST; 12 | const production = !process.env.ROLLUP_WATCH && !testing; 13 | const minizincInstallDir = path.resolve(process.env.MZN_WASM_DIR || "."); 14 | 15 | const browser = (output, src) => ({ 16 | input: "src/browser.js", 17 | output: { 18 | sourcemap: !production, 19 | ...output, 20 | }, 21 | plugins: [ 22 | production && terser(), 23 | replace({ 24 | URL_BASE: src || "document.currentScript.src", 25 | PACKAGE_VERSION: JSON.stringify(pkg.version), 26 | preventAssignment: true, 27 | ...(testing && { 28 | _workerUrl: "settings.workerURL", 29 | }), 30 | }), 31 | ], 32 | }); 33 | 34 | const worker = (output) => ({ 35 | input: "src/worker.js", 36 | output: { 37 | sourcemap: !production, 38 | ...output, 39 | }, 40 | plugins: [ 41 | alias({ 42 | entries: [ 43 | { 44 | find: "minizinc-bin", 45 | replacement: path.join(minizincInstallDir, "/bin/minizinc.js"), 46 | }, 47 | ], 48 | }), 49 | copy({ 50 | targets: [ 51 | { 52 | src: [ 53 | path 54 | .join(minizincInstallDir, "/bin/minizinc.data") 55 | .replace(/\\/g, "/"), 56 | path 57 | .join(minizincInstallDir, "/bin/minizinc.wasm") 58 | .replace(/\\/g, "/"), 59 | ], 60 | dest: "dist", 61 | }, 62 | ], 63 | verbose: true, 64 | }), 65 | commonjs({ ignore: ["crypto", "fs", "path", "perf_hooks", "ws"] }), 66 | production && terser(), 67 | ], 68 | }); 69 | 70 | const node = (output) => ({ 71 | input: "src/node.js", 72 | output: { 73 | sourcemap: !production, 74 | ...output, 75 | }, 76 | plugins: [production && terser()], 77 | external: [ 78 | "node:child_process", 79 | "node:events", 80 | "node:readline", 81 | "node:fs/promises", 82 | "node:path", 83 | "node:os", 84 | ], 85 | }); 86 | 87 | const configs = testing 88 | ? [ 89 | // Bundles for running tests 90 | browser( 91 | { 92 | file: "dist/test-minizinc.cjs", 93 | format: "cjs", 94 | }, 95 | "`file:///${__dirname}`" 96 | ), 97 | worker({ 98 | file: "dist/test-minizinc-worker.cjs", 99 | format: "cjs", 100 | }), 101 | node({ 102 | file: "dist/test-minizinc-node.cjs", 103 | format: "cjs", 104 | }), 105 | ] 106 | : [ 107 | // Bundles for distribution 108 | browser({ 109 | name: "MiniZinc", 110 | file: "dist/minizinc.js", 111 | format: "iife", 112 | }), 113 | browser( 114 | { 115 | file: "dist/minizinc.mjs", 116 | format: "es", 117 | }, 118 | "import.meta.url" 119 | ), 120 | browser({ 121 | file: "dist/minizinc.cjs", 122 | format: "cjs", 123 | }), 124 | worker({ 125 | file: "dist/minizinc-worker.js", 126 | format: "iife", 127 | }), 128 | node({ 129 | file: "dist/minizinc-node.cjs", 130 | format: "cjs", 131 | }), 132 | node({ 133 | file: "dist/minizinc-node.mjs", 134 | format: "es", 135 | }), 136 | ]; 137 | 138 | export default configs; 139 | -------------------------------------------------------------------------------- /src/browser.js: -------------------------------------------------------------------------------- 1 | // Controller in charge of web worker pool in the browser 2 | 3 | const cacheBuster = encodeURIComponent(PACKAGE_VERSION); 4 | let settings = { 5 | workerURL: new URL(`./minizinc-worker.js?version=${cacheBuster}`, URL_BASE), 6 | numWorkers: 2, 7 | }; 8 | const workers = []; 9 | let workerObjectURL; 10 | 11 | function newWorker() { 12 | if (!workerObjectURL) { 13 | const importer = `importScripts(${JSON.stringify(settings.workerURL)});`; 14 | workerObjectURL = URL.createObjectURL( 15 | new Blob([importer], { type: "text/javascript" }) 16 | ); 17 | } 18 | const _workerUrl = workerObjectURL; 19 | const worker = new Worker(_workerUrl); 20 | worker.postMessage({ 21 | wasmURL: settings.wasmURL 22 | ? settings.wasmURL.toString() 23 | : new URL( 24 | `./minizinc.wasm?version=${cacheBuster}`, 25 | settings.workerURL 26 | ).toString(), 27 | dataURL: settings.dataURL 28 | ? settings.dataURL.toString() 29 | : new URL( 30 | `./minizinc.data?version=${cacheBuster}`, 31 | settings.workerURL 32 | ).toString(), 33 | }); 34 | workers.push({ worker, runCount: 0 }); 35 | } 36 | 37 | function fillWorkerPool() { 38 | while (workers.length < settings.numWorkers) { 39 | newWorker(); 40 | } 41 | } 42 | 43 | export async function init(cfg) { 44 | if (cfg) { 45 | settings = { ...settings, ...cfg }; 46 | } 47 | if (workers.length > 0) { 48 | throw new Error( 49 | "MiniZinc.init() called after library already used/initialised" 50 | ); 51 | } 52 | fillWorkerPool(); 53 | await Promise.race( 54 | workers.map( 55 | (worker) => 56 | new Promise((resolve) => { 57 | worker.worker.addEventListener( 58 | "message", 59 | (e) => { 60 | if (e.data.type === "ready") { 61 | resolve(); 62 | } 63 | }, 64 | { once: true } 65 | ); 66 | }) 67 | ) 68 | ); 69 | } 70 | 71 | export function shutdown() { 72 | for (const worker of workers) { 73 | worker.worker.terminate(); 74 | } 75 | workers.splice(0); 76 | URL.revokeObjectURL(workerObjectURL); 77 | workerObjectURL = null; 78 | } 79 | 80 | export class Model { 81 | constructor() { 82 | this.vfs = {}; 83 | this._toRun = []; 84 | this.unnamedCount = 0; 85 | } 86 | clone() { 87 | const clone = new Model(); 88 | clone.vfs = { ...this.vfs }; 89 | clone._toRun = [...this.toRun]; 90 | clone.unnamedCount = this.unnamedCount; 91 | return clone; 92 | } 93 | addString(model) { 94 | let filename = `_mzn_${this.unnamedCount++}.mzn`; 95 | while (filename in this.vfs) { 96 | filename = `_mzn_${this.unnamedCount++}.mzn`; 97 | } 98 | this.addFile(filename, model); 99 | return filename; 100 | } 101 | addDznString(dzn) { 102 | let filename = `_dzn_${this.unnamedCount++}.dzn`; 103 | while (filename in this.vfs) { 104 | filename = `_dzn_${this.unnamedCount++}.dzn`; 105 | } 106 | this.addFile(filename, dzn); 107 | return filename; 108 | } 109 | addJson(data) { 110 | let filename = `_json_${this.unnamedCount++}.json`; 111 | while (filename in this.vfs) { 112 | filename = `_json_${this.unnamedCount++}.json`; 113 | } 114 | this.addFile(filename, JSON.stringify(data)); 115 | return filename; 116 | } 117 | addFile(filename, contents, use = true) { 118 | if (typeof contents !== "string") { 119 | if (filename in this.vfs) { 120 | this._addToRun(filename, use); 121 | return; 122 | } 123 | throw new Error("Missing file contents argument"); 124 | } 125 | this.vfs[filename] = contents; 126 | this._addToRun(filename, use); 127 | } 128 | _addToRun(filename, use) { 129 | if ( 130 | use && 131 | (filename.endsWith(".mzn") || 132 | filename.endsWith(".mzc") || 133 | filename.endsWith(".dzn") || 134 | filename.endsWith(".json") || 135 | filename.endsWith(".mpc") || 136 | filename.endsWith(".fzn")) && 137 | this._toRun.indexOf(filename) === -1 138 | ) { 139 | this._toRun.push(filename); 140 | } 141 | } 142 | _run(args, options, outputFiles = null) { 143 | fillWorkerPool(); 144 | const preArgs = []; 145 | let files = this.vfs; 146 | if (options) { 147 | let mpcFile = `_mzn_${this.unnamedCount++}.mpc`; 148 | while (mpcFile in this.vfs) { 149 | mpcFile = `_mzn_${this.unnamedCount++}.mpc`; 150 | } 151 | files = { ...this.vfs, [mpcFile]: JSON.stringify(options) }; 152 | preArgs.push(mpcFile); 153 | } 154 | let { worker, runCount } = workers.pop(); 155 | worker.postMessage({ 156 | jsonStream: true, 157 | files, 158 | args: [...preArgs, ...args, ...this._toRun], 159 | outputFiles, 160 | }); 161 | return { worker, runCount: runCount + 1 }; 162 | } 163 | check(cfg) { 164 | return new Promise((resolve, _reject) => { 165 | const config = { ...cfg }; 166 | const { worker, runCount } = this._run( 167 | ["--model-check-only"], 168 | config.options 169 | ); 170 | const errors = []; 171 | worker.onmessage = (e) => { 172 | switch (e.data.type) { 173 | case "error": 174 | errors.push(e.data); 175 | break; 176 | case "exit": 177 | if (runCount < 10) { 178 | workers.push({ 179 | worker, 180 | runCount, 181 | }); 182 | } else { 183 | worker.terminate(); 184 | newWorker(); 185 | } 186 | resolve(errors); 187 | break; 188 | } 189 | }; 190 | }); 191 | } 192 | interface(cfg) { 193 | return new Promise((resolve, reject) => { 194 | const config = { ...cfg }; 195 | const { worker, runCount } = this._run( 196 | ["--model-interface-only"], 197 | config.options 198 | ); 199 | const errors = []; 200 | let iface = null; 201 | worker.onmessage = (e) => { 202 | switch (e.data.type) { 203 | case "error": 204 | errors.push(e.data); 205 | break; 206 | case "interface": 207 | iface = e.data; 208 | break; 209 | case "exit": 210 | if (runCount < 10) { 211 | workers.push({ 212 | worker, 213 | runCount, 214 | }); 215 | } else { 216 | worker.terminate(); 217 | newWorker(); 218 | } 219 | if (e.data.code === 0) { 220 | resolve(iface); 221 | } else { 222 | reject(errors); 223 | } 224 | break; 225 | } 226 | }; 227 | }); 228 | } 229 | compile(cfg) { 230 | const config = { ...cfg }; 231 | let i = 0; 232 | let out = `_fzn_${i++}.fzn`; 233 | while (out in this.vfs) { 234 | out = `_fzn_${i++}.fzn`; 235 | } 236 | const args = ["-c", "--fzn", out]; 237 | const { worker } = this._run(args, config.options, [out]); 238 | // Don't reuse this worker, always create add a new one to the pool 239 | newWorker(); 240 | let callbacks = {}; 241 | let exited = false; 242 | let error = null; 243 | worker.onmessage = (e) => { 244 | if (callbacks[e.data.type]) { 245 | for (const f of callbacks[e.data.type]) { 246 | f(e.data); 247 | } 248 | } 249 | switch (e.data.type) { 250 | case "exit": 251 | worker.terminate(); 252 | exited = true; 253 | callbacks = {}; 254 | break; 255 | case "error": 256 | if (!error) error = e.data; 257 | break; 258 | } 259 | }; 260 | return { 261 | isRunning() { 262 | return !exited; 263 | }, 264 | cancel() { 265 | if (!exited) { 266 | exited = true; 267 | worker.terminate(); 268 | if (callbacks["exit"]) { 269 | for (const f of callbacks["exit"]) { 270 | f({ type: "exit", code: null }); 271 | } 272 | } 273 | callbacks = {}; 274 | } 275 | }, 276 | on(event, callback) { 277 | if (callbacks[event]) { 278 | callbacks[event].add(callback); 279 | } else { 280 | callbacks[event] = new Set([callback]); 281 | } 282 | }, 283 | off(event, callback) { 284 | if (callbacks[event]) { 285 | callbacks[event].delete(callback); 286 | } 287 | }, 288 | then(resolve, reject) { 289 | const onExit = (e) => { 290 | if (e.code === 0) { 291 | resolve(e.outputFiles[out]); 292 | } else { 293 | const exit = error ? { message: error.message, ...e } : e; 294 | if (reject) { 295 | reject(exit); 296 | } else { 297 | throw exit; 298 | } 299 | } 300 | }; 301 | if (callbacks.exit) { 302 | callbacks.exit.add(onExit); 303 | } else { 304 | callbacks.exit = new Set([onExit]); 305 | } 306 | }, 307 | }; 308 | } 309 | solve(cfg) { 310 | const config = { jsonOutput: true, ...cfg }; 311 | const args = ["-i"]; // Always use intermediate solutions 312 | if (config.jsonOutput) { 313 | args.push("--output-mode"); 314 | args.push("json"); 315 | } 316 | const { worker } = this._run(args, config.options); 317 | // Don't reuse this worker, always create add a new one to the pool 318 | newWorker(); 319 | let error = null; 320 | let callbacks = {}; 321 | let exited = false; 322 | let solution = null; 323 | let statistics = {}; 324 | let status = "UNKNOWN"; 325 | worker.onmessage = (e) => { 326 | if (callbacks[e.data.type]) { 327 | for (const f of callbacks[e.data.type]) { 328 | f(e.data); 329 | } 330 | } 331 | switch (e.data.type) { 332 | case "exit": 333 | worker.terminate(); 334 | exited = true; 335 | callbacks = {}; 336 | break; 337 | case "error": 338 | if (!error) error = e.data; 339 | break; 340 | case "statistics": 341 | statistics = { 342 | ...statistics, 343 | ...e.data.statistics, 344 | }; 345 | break; 346 | case "solution": 347 | solution = e.data; 348 | status = "SATISFIED"; 349 | break; 350 | case "status": 351 | status = e.data.status; 352 | break; 353 | } 354 | }; 355 | return { 356 | isRunning() { 357 | return !exited; 358 | }, 359 | cancel() { 360 | if (!exited) { 361 | exited = true; 362 | worker.terminate(); 363 | if (callbacks["exit"]) { 364 | for (const f of callbacks["exit"]) { 365 | f({ type: "exit", code: null }); 366 | } 367 | } 368 | callbacks = {}; 369 | } 370 | }, 371 | on(event, callback) { 372 | if (callbacks[event]) { 373 | callbacks[event].add(callback); 374 | } else { 375 | callbacks[event] = new Set([callback]); 376 | } 377 | }, 378 | off(event, callback) { 379 | if (callbacks[event]) { 380 | callbacks[event].delete(callback); 381 | } 382 | }, 383 | then(resolve, reject) { 384 | const onExit = (e) => { 385 | if (e.code === 0) { 386 | resolve({ 387 | status, 388 | solution, 389 | statistics, 390 | }); 391 | } else { 392 | const exit = error ? { message: error.message, ...e } : e; 393 | if (reject) { 394 | reject(exit); 395 | } else { 396 | throw exit; 397 | } 398 | } 399 | }; 400 | if (callbacks.exit) { 401 | callbacks.exit.add(onExit); 402 | } else { 403 | callbacks.exit = new Set([onExit]); 404 | } 405 | }, 406 | }; 407 | } 408 | } 409 | 410 | export function version() { 411 | return new Promise((resolve, reject) => { 412 | fillWorkerPool(); 413 | let { worker, runCount } = workers.pop(); 414 | worker.postMessage({ 415 | jsonStream: false, 416 | args: ["--version"], 417 | }); 418 | worker.onmessage = (e) => { 419 | if (e.data.type === "exit") { 420 | if (runCount < 10) { 421 | workers.push({ 422 | worker, 423 | runCount: runCount + 1, 424 | }); 425 | } else { 426 | worker.terminate(); 427 | newWorker(); 428 | } 429 | if (e.data.code === 0) { 430 | resolve(e.data.stdout); 431 | } else { 432 | reject(e.data); 433 | } 434 | } 435 | }; 436 | }); 437 | } 438 | 439 | export function solvers() { 440 | return new Promise((resolve, reject) => { 441 | fillWorkerPool(); 442 | let { worker, runCount } = workers.pop(); 443 | worker.postMessage({ 444 | jsonStream: false, 445 | args: ["--solvers-json"], 446 | }); 447 | worker.onmessage = (e) => { 448 | if (e.data.type === "exit") { 449 | if (runCount < 10) { 450 | workers.push({ 451 | worker, 452 | runCount: runCount + 1, 453 | }); 454 | } else { 455 | worker.terminate(); 456 | newWorker(); 457 | } 458 | if (e.data.code === 0) { 459 | resolve(JSON.parse(e.data.stdout)); 460 | } else { 461 | reject(e.data); 462 | } 463 | } 464 | }; 465 | }); 466 | } 467 | 468 | export function readStdlibFileContents(files) { 469 | const keys = Array.isArray(files) ? files : [files]; 470 | return new Promise((resolve, reject) => { 471 | fillWorkerPool(); 472 | let { worker, runCount } = workers.pop(); 473 | worker.postMessage({ 474 | readStdlibFiles: keys, 475 | }); 476 | worker.onmessage = (e) => { 477 | if (e.data.type === "readStdlibFiles") { 478 | if (runCount < 10) { 479 | workers.push({ 480 | worker, 481 | runCount: runCount + 1, 482 | }); 483 | } else { 484 | worker.terminate(); 485 | newWorker(); 486 | } 487 | if (Array.isArray(files)) { 488 | resolve(e.data.files); 489 | } else { 490 | resolve(e.data.files[files]); 491 | } 492 | } else if (e.data.type === "error") { 493 | worker.terminate(); 494 | newWorker(); 495 | reject(e.data.message); 496 | } 497 | }; 498 | }); 499 | } 500 | -------------------------------------------------------------------------------- /src/browser.spec.js: -------------------------------------------------------------------------------- 1 | const MiniZinc = require("../dist/test-minizinc.cjs"); 2 | 3 | // Use web worker library for worker since not available on node 4 | global.Worker = require("web-worker"); 5 | global.Blob = require("buffer").Blob; 6 | 7 | const { commonTests } = require("./tests.cjs"); 8 | 9 | jest.setTimeout(30000); 10 | 11 | beforeAll(async () => { 12 | await MiniZinc.init({ 13 | workerURL: "./dist/test-minizinc-worker.cjs", 14 | wasmURL: "./dist/minizinc.wasm", 15 | dataURL: "./dist/minizinc.data", 16 | }); 17 | }); 18 | 19 | afterAll(() => { 20 | MiniZinc.shutdown(); 21 | }); 22 | 23 | test("Virtual filesystem", async () => { 24 | const model = new MiniZinc.Model(); 25 | model.addFile("test.mzn", 'include "foo.mzn";'); 26 | model.addFile( 27 | "foo.mzn", 28 | `var 1..3: x; 29 | int: y; 30 | constraint x < y;`, 31 | false 32 | ); 33 | model.addFile("data.dzn", "y = 2;"); 34 | const result = await model.solve(); 35 | const x = result.solution.output.json.x; 36 | expect(x).toBe(1); 37 | }); 38 | 39 | commonTests(MiniZinc); 40 | -------------------------------------------------------------------------------- /src/node.js: -------------------------------------------------------------------------------- 1 | // Controller used in node environments which uses child processes instead of wasm 2 | 3 | import child_process from "node:child_process"; 4 | import EventEmitter from "node:events"; 5 | import rl from "node:readline"; 6 | import fs from "node:fs/promises"; 7 | import path from "node:path"; 8 | import os from "node:os"; 9 | 10 | let settings = { minizinc: "minizinc", _executable: "minizinc" }; 11 | 12 | export async function init(cfg) { 13 | if (cfg) { 14 | settings = { ...settings, ...cfg }; 15 | } 16 | if (!/\\\//.test(settings.minizinc) && settings.minizincPaths) { 17 | for (const p of minizincPaths) { 18 | const executable = path.join(p, settings.minizinc); 19 | try { 20 | await fs.access(executable); 21 | settings._executable = executable; 22 | } catch {} 23 | } 24 | } 25 | settings._executable = settings.minizinc; 26 | } 27 | 28 | const childProcesses = new Set(); 29 | export function shutdown() { 30 | for (const proc of childProcesses) { 31 | proc.kill("SIGKILL"); 32 | } 33 | childProcesses.clear(); 34 | } 35 | 36 | export class Model { 37 | constructor() { 38 | this.vfs = {}; 39 | this._toRun = []; 40 | this.unnamedCount = 0; 41 | } 42 | clone() { 43 | const clone = new Model(); 44 | clone.vfs = { ...this.vfs }; 45 | clone._toRun = [...this.toRun]; 46 | clone.unnamedCount = this.unnamedCount; 47 | return clone; 48 | } 49 | addString(model) { 50 | let filename = `_mzn_${this.unnamedCount++}.mzn`; 51 | while (filename in this.vfs) { 52 | filename = `_mzn_${this.unnamedCount++}.mzn`; 53 | } 54 | this._addVirtual(filename, model); 55 | return filename; 56 | } 57 | addDznString(dzn) { 58 | let filename = `_dzn_${this.unnamedCount++}.dzn`; 59 | while (filename in this.vfs) { 60 | filename = `_dzn_${this.unnamedCount++}.dzn`; 61 | } 62 | this._addVirtual(filename, dzn); 63 | return filename; 64 | } 65 | addJson(data) { 66 | let filename = `_json_${this.unnamedCount++}.json`; 67 | while (filename in this.vfs) { 68 | filename = `_json_${this.unnamedCount++}.json`; 69 | } 70 | this._addVirtual(filename, JSON.stringify(data)); 71 | return filename; 72 | } 73 | _add_toRun(filename, use) { 74 | if ( 75 | use && 76 | (filename.endsWith(".mzn") || 77 | filename.endsWith(".mzc") || 78 | filename.endsWith(".dzn") || 79 | filename.endsWith(".json") || 80 | filename.endsWith(".mpc") || 81 | filename.endsWith(".fzn")) && 82 | this._toRun.indexOf(filename) === -1 83 | ) { 84 | this._toRun.push(filename); 85 | } 86 | } 87 | _addVirtual(filename, contents, use = true) { 88 | this.vfs[filename] = contents; 89 | this._add_toRun(filename, use); 90 | } 91 | addFile(filename, contents = null, use = true) { 92 | if (typeof contents === "string") { 93 | this._addVirtual(filename, contents, use); 94 | } else { 95 | this._add_toRun(filename, use); 96 | } 97 | } 98 | _run(args, options, outputFiles) { 99 | const emitter = new EventEmitter(); 100 | let proc = null; 101 | emitter.on("sigint", () => { 102 | if (proc) { 103 | proc.kill("SIGINT"); 104 | } else { 105 | proc = false; 106 | } 107 | }); 108 | (async () => { 109 | const preArgs = ["--json-stream"]; 110 | const tempdir = await fs.mkdtemp(path.join(os.tmpdir(), "mzn")); 111 | if (options) { 112 | let mpcFile = `_mzn_${this.unnamedCount++}.mpc`; 113 | while (mpcFile in this.vfs) { 114 | mpcFile = `_mzn_${this.unnamedCount++}.mpc`; 115 | } 116 | mpcFile = path.join(tempdir, mpcFile); 117 | await fs.writeFile(mpcFile, JSON.stringify(options)); 118 | preArgs.push(mpcFile); 119 | } 120 | for (const key in this.vfs) { 121 | await fs.writeFile(path.join(tempdir, key), this.vfs[key]); 122 | } 123 | if (proc === false) { 124 | emitter.emit("exit", { code: 0 }); 125 | return; 126 | } 127 | proc = child_process.spawn(settings._executable, [ 128 | ...preArgs, 129 | ...args.map((x) => 130 | outputFiles && outputFiles.indexOf(x) !== -1 131 | ? path.join(tempdir, x) 132 | : x 133 | ), 134 | ...this._toRun.map((x) => { 135 | if (x in this.vfs) { 136 | return path.join(tempdir, x); 137 | } else { 138 | return x; 139 | } 140 | }), 141 | ]); 142 | childProcesses.add(proc); 143 | const stdout = rl.createInterface(proc.stdout); 144 | stdout.on("line", async (line) => { 145 | try { 146 | const obj = JSON.parse(line); 147 | if ( 148 | "location" in obj && 149 | "filename" in obj.location && 150 | typeof obj.location.filename === "string" && 151 | obj.location.filename.indexOf(tempdir) === 0 152 | ) { 153 | // Strip prefix from filename 154 | obj.location.filename = obj.location.filename.substring( 155 | tempdir.length 156 | ); 157 | } 158 | if ("stack" in obj && Array.isArray(obj.stack)) { 159 | for (const s of obj.stack) { 160 | if ( 161 | "location" in s && 162 | "filename" in s.location && 163 | typeof s.location.filename === "string" && 164 | s.location.filename.indexOf(tempdir) === 0 165 | ) { 166 | // Strip prefix from filename 167 | s.location.filename = s.location.filename.substring( 168 | tempdir.length 169 | ); 170 | } 171 | } 172 | } 173 | emitter.emit(obj.type, obj); 174 | } catch (e) { 175 | emitter.emit("stdout", { type: "stdout", value: line }); 176 | } 177 | }); 178 | const stderr = rl.createInterface(proc.stderr); 179 | stderr.on("line", async (line) => { 180 | emitter.emit("stderr", line); 181 | }); 182 | proc.on("exit", async (c, signal) => { 183 | childProcesses.delete(proc); 184 | const exitMessage = { 185 | type: "exit", 186 | code: signal === "SIGINT" ? null : c, 187 | }; 188 | if (outputFiles) { 189 | exitMessage.outputFiles = {}; 190 | for (const key of outputFiles) { 191 | try { 192 | exitMessage.outputFiles[key] = await fs.readFile(key, { 193 | encoding: "utf8", 194 | }); 195 | } catch (e) { 196 | try { 197 | exitMessage.outputFiles[key] = await fs.readFile( 198 | path.join(tempdir, key), 199 | { encoding: "utf8" } 200 | ); 201 | } catch (e) { 202 | exitMessage.outputFiles[key] = null; 203 | } 204 | } 205 | } 206 | } 207 | emitter.emit("exit", exitMessage); 208 | fs.rm(tempdir, { recursive: true, force: true }); 209 | }); 210 | })(); 211 | return emitter; 212 | } 213 | check(cfg) { 214 | const config = { ...cfg }; 215 | const proc = this._run(["--model-check-only"], config.options); 216 | const errors = []; 217 | proc.on("error", (e) => errors.push(e)); 218 | return new Promise((resolve, _reject) => { 219 | proc.on("exit", (e) => resolve(errors)); 220 | }); 221 | } 222 | interface(cfg) { 223 | const config = { ...cfg }; 224 | const proc = this._run(["--model-interface-only"], config.options); 225 | const errors = []; 226 | let iface = null; 227 | proc.on("error", (e) => errors.push(e)); 228 | proc.on("interface", (e) => (iface = e)); 229 | return new Promise((resolve, reject) => { 230 | proc.on("exit", (e) => { 231 | if (e.code === 0) { 232 | resolve(iface); 233 | } else { 234 | reject(errors); 235 | } 236 | }); 237 | }); 238 | } 239 | compile(cfg) { 240 | const config = { ...cfg }; 241 | let i = 0; 242 | let out = `_fzn_${i++}.fzn`; 243 | while (out in this.vfs) { 244 | out = `_fzn_${i++}.fzn`; 245 | } 246 | const args = ["-c", "--fzn", out]; 247 | let running = true; 248 | let error = null; 249 | const proc = this._run(args, config.options, [out]); 250 | proc.on("exit", () => (running = false)); 251 | proc.on("error", (e) => { 252 | if (!error) error = e; 253 | }); 254 | return { 255 | isRunning() { 256 | return running; 257 | }, 258 | cancel() { 259 | proc.emit("sigint"); 260 | }, 261 | on: (event, listener) => proc.on(event, listener), 262 | off: (event, listener) => proc.off(event, listener), 263 | then(resolve, reject) { 264 | proc.on("exit", (e) => { 265 | if (e.code === 0) { 266 | resolve(e.outputFiles[out]); 267 | } else { 268 | const exit = error ? { message: error.message, ...e } : e; 269 | if (reject) { 270 | reject(exit); 271 | } else { 272 | throw exit; 273 | } 274 | } 275 | }); 276 | }, 277 | }; 278 | } 279 | solve(cfg) { 280 | const config = { jsonOutput: true, ...cfg }; 281 | const args = ["-i"]; // Always use intermediate solutions 282 | if (config.jsonOutput) { 283 | args.push("--output-mode"); 284 | args.push("json"); 285 | } 286 | let running = true; 287 | let error = null; 288 | const proc = this._run(args, config.options); 289 | proc.on("exit", () => (running = false)); 290 | let solution = null; 291 | let statistics = {}; 292 | let status = "UNKNOWN"; 293 | proc.on("statistics", (e) => { 294 | statistics = { 295 | ...statistics, 296 | ...e.statistics, 297 | }; 298 | }); 299 | proc.on("solution", (e) => { 300 | solution = e; 301 | status = "SATISFIED"; 302 | }); 303 | proc.on("status", (e) => { 304 | status = e.status; 305 | }); 306 | proc.on("error", (e) => { 307 | if (!error) error = e; 308 | }); 309 | return { 310 | isRunning() { 311 | return running; 312 | }, 313 | cancel() { 314 | proc.emit("sigint"); 315 | }, 316 | on: (event, listener) => proc.on(event, listener), 317 | off: (event, listener) => proc.off(event, listener), 318 | then(resolve, reject) { 319 | proc.on("exit", (e) => { 320 | if (e.code === 0) { 321 | resolve({ 322 | status, 323 | solution, 324 | statistics, 325 | }); 326 | } else { 327 | const exit = error ? { message: error.message, ...e } : e; 328 | if (reject) { 329 | reject(exit); 330 | } else { 331 | throw exit; 332 | } 333 | } 334 | }); 335 | }, 336 | }; 337 | } 338 | } 339 | 340 | export function version() { 341 | return new Promise((resolve, reject) => { 342 | let proc = null; 343 | proc = child_process.execFile( 344 | settings._executable, 345 | ["--version"], 346 | (error, stdout, stderr) => { 347 | childProcesses.delete(proc); 348 | if (error) { 349 | reject(error); 350 | } 351 | resolve(stdout); 352 | } 353 | ); 354 | childProcesses.add(proc); 355 | }); 356 | } 357 | 358 | export function solvers() { 359 | return new Promise((resolve, reject) => { 360 | let proc = null; 361 | proc = child_process.execFile( 362 | settings._executable, 363 | ["--solvers-json"], 364 | (error, stdout, stderr) => { 365 | childProcesses.delete(proc); 366 | if (error) { 367 | reject(error); 368 | } 369 | resolve(JSON.parse(stdout)); 370 | } 371 | ); 372 | childProcesses.add(proc); 373 | }); 374 | } 375 | 376 | export function readStdlibFileContents(files) { 377 | const keys = Array.isArray(files) ? files : [files]; 378 | return new Promise((resolve, reject) => { 379 | let proc = null; 380 | proc = child_process.execFile( 381 | settings._executable, 382 | ["--config-dirs"], 383 | async (error, stdout, stderr) => { 384 | childProcesses.delete(proc); 385 | if (error) { 386 | reject(error); 387 | } 388 | const mznStdlibDir = JSON.parse(stdout).mznStdlibDir; 389 | const result = {}; 390 | for (const key of keys) { 391 | const p = path.join(mznStdlibDir, key); 392 | const rel = path.relative(mznStdlibDir, p); 393 | if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) { 394 | reject(`Unsupported file path ${key}`); 395 | } 396 | try { 397 | result[key] = await fs.readFile(p, { 398 | encoding: "utf8", 399 | }); 400 | } catch (e) { 401 | result[key] = null; 402 | } 403 | } 404 | if (Array.isArray(files)) { 405 | resolve(result); 406 | } else { 407 | resolve(result[files]); 408 | } 409 | } 410 | ); 411 | childProcesses.add(proc); 412 | }); 413 | } 414 | -------------------------------------------------------------------------------- /src/node.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const MiniZinc = require("../dist/test-minizinc-node.cjs"); 4 | 5 | const { commonTests } = require("./tests.cjs"); 6 | 7 | beforeAll(async () => { 8 | await MiniZinc.init({ 9 | minizinc: process.env.MZN_NODE_BINARY || "minizinc", 10 | }); 11 | }); 12 | 13 | afterAll(() => { 14 | MiniZinc.shutdown(); 15 | }); 16 | 17 | commonTests(MiniZinc); 18 | 19 | test("Load from filesystem", async () => { 20 | const model = new MiniZinc.Model(); 21 | model.addFile(path.join(__dirname, "test.mzn")); 22 | const result = await model.solve(); 23 | const x = result.solution.output.json.x; 24 | expect(x).toBeGreaterThanOrEqual(1); 25 | expect(x).toBeLessThanOrEqual(3); 26 | }); 27 | -------------------------------------------------------------------------------- /src/test.mzn: -------------------------------------------------------------------------------- 1 | var 1..3: x; 2 | -------------------------------------------------------------------------------- /src/tests.cjs: -------------------------------------------------------------------------------- 1 | // Tests for common API 2 | 3 | module.exports.commonTests = (MiniZinc) => { 4 | test("Version output", async () => { 5 | const version = await MiniZinc.version(); 6 | expect(version).toMatch(/version (\d)+\.(\d)+\.(\d)+/); 7 | }); 8 | 9 | test("Solvers output", async () => { 10 | const solvers = await MiniZinc.solvers(); 11 | expect(Array.isArray(solvers)).toBe(true); 12 | }); 13 | 14 | test("Basic solve", async () => { 15 | const model = new MiniZinc.Model(); 16 | model.addString("var 1..3: x;"); 17 | const result = await model.solve(); 18 | const x = result.solution.output.json.x; 19 | expect(x).toBeGreaterThanOrEqual(1); 20 | expect(x).toBeLessThanOrEqual(3); 21 | }); 22 | 23 | test("DZN output", async () => { 24 | const model = new MiniZinc.Model(); 25 | model.addString("var 1..3: x;"); 26 | const result = await model.solve({ 27 | jsonOutput: false, 28 | options: { 29 | "output-mode": "dzn", 30 | }, 31 | }); 32 | expect(result.solution.output.dzn).toMatch(/x = [1-3];\n/); 33 | }); 34 | 35 | test("Solve with DZN data", async () => { 36 | const model = new MiniZinc.Model(); 37 | model.addString(` 38 | var 1..3: x; 39 | int: y; 40 | constraint x > y; 41 | `); 42 | model.addDznString("y = 2;"); 43 | const result = await model.solve(); 44 | const x = result.solution.output.json.x; 45 | expect(x).toBe(3); 46 | }); 47 | 48 | test("Solve with JSON data", async () => { 49 | const model = new MiniZinc.Model(); 50 | model.addString(` 51 | var 1..3: x; 52 | int: y; 53 | constraint x > y; 54 | `); 55 | model.addJson({ 56 | y: 2, 57 | }); 58 | const result = await model.solve(); 59 | const x = result.solution.output.json.x; 60 | expect(x).toBe(3); 61 | }); 62 | 63 | test("Events", async () => { 64 | const model = new MiniZinc.Model(); 65 | model.addString("var 1..3: x;"); 66 | const solve = model.solve({ 67 | options: { 68 | "all-solutions": true, 69 | statistics: true, 70 | }, 71 | }); 72 | const values = []; 73 | solve.on("solution", (e) => values.push(e.output.json.x)); 74 | solve.on("status", (e) => expect(e.status).toBe("ALL_SOLUTIONS")); 75 | const result = await solve; 76 | expect(result.statistics.nSolutions).toBe(3); 77 | expect(values).toContain(1); 78 | expect(values).toContain(2); 79 | expect(values).toContain(3); 80 | expect(values.length).toBe(3); 81 | }); 82 | 83 | test("Error events", async () => { 84 | const model = new MiniZinc.Model(); 85 | model.addString(` 86 | function int: foo(1..1: x) = x; 87 | int: v = foo(2); 88 | `); 89 | const solve = model.solve(); 90 | expect.assertions(3); 91 | solve.on("error", (e) => { 92 | expect(typeof e.message).toBe("string"); 93 | }); 94 | try { 95 | await solve; 96 | } catch (e) { 97 | expect(e.code).toBe(1); 98 | expect(typeof e.message).toBe("string"); 99 | } 100 | }); 101 | 102 | test("Basic compile", async () => { 103 | const model = new MiniZinc.Model(); 104 | model.addString("var 1..3: x;"); 105 | const fzn = await model.compile({ options: { solver: "gecode" } }); 106 | expect(fzn).toMatch( 107 | /var\s+1\s*\.\.\s*3\s*:\s*x\s*::\s*output_var\s*;\s*solve\s*satisfy;\s*/ 108 | ); 109 | }); 110 | 111 | test("Model check success", async () => { 112 | const model = new MiniZinc.Model(); 113 | model.addString("var 1..3: x;"); 114 | const errors = await model.check(); 115 | expect(errors.length).toBe(0); 116 | }); 117 | 118 | test("Model check error", async () => { 119 | const model = new MiniZinc.Model(); 120 | model.addString("var 1..3: x; var 1..3: x;"); 121 | const errors = await model.check(); 122 | expect(errors.length).toBe(1); 123 | }); 124 | 125 | test("Model interface", async () => { 126 | const model = new MiniZinc.Model(); 127 | model.addString("var 1..3: x; int: y;"); 128 | const iface = await model.interface(); 129 | expect(iface.input).toEqual({ 130 | y: { type: "int" }, 131 | }); 132 | expect(iface.output).toEqual({ x: { type: "int" } }); 133 | expect(iface.method).toBe("sat"); 134 | }); 135 | 136 | test("UTF-8 support", async () => { 137 | const model = new MiniZinc.Model(); 138 | model.addString("int: 'μ' :: output = 1; string: x :: output = \"μ\";"); 139 | const result = await model.solve(); 140 | expect(result.solution.output.json).toMatchObject({ 141 | μ: 1, 142 | x: "μ", 143 | }); 144 | }); 145 | 146 | test("Stdlib file retrieval", async () => { 147 | const contents = await MiniZinc.readStdlibFileContents("std/stdlib.mzn"); 148 | expect(contents.length).toBeGreaterThan(0); 149 | const multiple = await MiniZinc.readStdlibFileContents([ 150 | "std/stdlib.mzn", 151 | "std/redefinitions.mzn", 152 | ]); 153 | expect(multiple["std/stdlib.mzn"].length).toBeGreaterThan(0); 154 | expect(multiple["std/redefinitions.mzn"].length).toBeGreaterThan(0); 155 | await expect(MiniZinc.readStdlibFileContents("../foo")).rejects.toEqual( 156 | "Unsupported file path ../foo" 157 | ); 158 | }); 159 | }; 160 | -------------------------------------------------------------------------------- /src/worker.js: -------------------------------------------------------------------------------- 1 | // Web worker code for the browser 2 | 3 | import MINIZINC from "minizinc-bin"; 4 | 5 | let initMiniZinc = null; 6 | 7 | addEventListener("message", async (e) => { 8 | try { 9 | if (initMiniZinc) { 10 | const Module = await initMiniZinc; 11 | if (e.data.readStdlibFiles) { 12 | const files = {}; 13 | const prefix = "file:///usr/share/minizinc/"; 14 | for (const key of e.data.readStdlibFiles) { 15 | const resolved = new URL(prefix + key).href; 16 | if (resolved.indexOf(prefix) !== 0) { 17 | // Ensure path is a valid relative path 18 | console.error(`Unsupported file path ${key}`); 19 | postMessage({ 20 | type: "error", 21 | message: `Unsupported file path ${key}`, 22 | }); 23 | return; 24 | } 25 | const path = 26 | "/usr/share/minizinc/" + resolved.substring(prefix.length); 27 | if (Module.FS.analyzePath(path).exists) { 28 | files[key] = Module.FS.readFile(path, { 29 | encoding: "utf8", 30 | }); 31 | } else { 32 | files[key] = null; 33 | } 34 | } 35 | postMessage({ 36 | type: "readStdlibFiles", 37 | files, 38 | }); 39 | return; 40 | } 41 | Module.stdoutBuffer = []; 42 | Module.stderrBuffer = []; 43 | Module.jsonStream = !!e.data.jsonStream; 44 | Module.FS.mount(Module.FS.filesystems.MEMFS, null, "/minizinc"); 45 | if (e.data.files) { 46 | const prefix = "file:///minizinc/"; 47 | for (const key in e.data.files) { 48 | const resolved = new URL(prefix + key).href; 49 | if (resolved.indexOf(prefix) !== 0) { 50 | // Ensure path is a valid relative path 51 | throw new Error(`Unsupported file path ${key}`); 52 | } 53 | const dest = "/minizinc/" + resolved.substring(prefix.length); 54 | for ( 55 | let i = dest.indexOf("/", 10); 56 | i !== -1; 57 | i = dest.indexOf("/", i + 1) 58 | ) { 59 | // Create parent paths 60 | const path = dest.substring(0, i); 61 | if (!Module.FS.analyzePath(path).exists) { 62 | Module.FS.mkdir(path); 63 | } 64 | } 65 | // Write file 66 | Module.FS.writeFile(dest, e.data.files[key]); 67 | } 68 | } 69 | // Always include --json-stream 70 | const args = Module.jsonStream 71 | ? ["--json-stream", ...e.data.args] 72 | : e.data.args; 73 | const oldCwd = Module.FS.cwd(); 74 | Module.FS.chdir("/minizinc"); 75 | try { 76 | const code = Module.callMain(args); 77 | // Add exit message so the controller can tell that we're done 78 | const exitMessage = { type: "exit", code }; 79 | if (Module.stdoutBuffer.length > 0) { 80 | const decoder = new TextDecoder("utf-8"); 81 | exitMessage.stdout = decoder.decode( 82 | new Uint8Array(Module.stdoutBuffer) 83 | ); 84 | } 85 | if (Module.stderrBuffer.length > 0) { 86 | const decoder = new TextDecoder("utf-8"); 87 | exitMessage.stderr = decoder.decode( 88 | new Uint8Array(Module.stderrBuffer) 89 | ); 90 | } 91 | if (e.data.outputFiles) { 92 | exitMessage.outputFiles = {}; 93 | const prefix = "file:///minizinc/"; 94 | for (const key of e.data.outputFiles) { 95 | const resolved = new URL(prefix + key).href; 96 | if (resolved.indexOf(prefix) !== 0) { 97 | // Ensure path is a valid relative path 98 | throw new Error(`Unsupported file path ${key}`); 99 | } 100 | const path = "/minizinc/" + resolved.substring(prefix.length); 101 | if (Module.FS.analyzePath(path).exists) { 102 | exitMessage.outputFiles[key] = Module.FS.readFile(path, { 103 | encoding: "utf8", 104 | }); 105 | } else { 106 | exitMessage.outputFiles[key] = null; 107 | } 108 | } 109 | } 110 | postMessage(exitMessage); 111 | } catch (e) { 112 | console.error(e); 113 | postMessage({ 114 | type: "exit", 115 | code: -1, 116 | message: e.message, 117 | }); 118 | } 119 | Module.FS.chdir(oldCwd); 120 | Module.FS.unmount("/minizinc"); 121 | } else { 122 | initMiniZinc = MINIZINC({ 123 | locateFile(path, prefix) { 124 | if (path === "minizinc.wasm") { 125 | return e.data.wasmURL; 126 | } 127 | if (path === "minizinc.data") { 128 | return e.data.dataURL; 129 | } 130 | return prefix + path; 131 | }, 132 | preRun: [ 133 | (Module) => { 134 | const stdout = (code) => { 135 | if (code === 0x0) { 136 | return; 137 | } 138 | Module.stdoutBuffer.push(code); 139 | if (Module.jsonStream && code === 0x0a) { 140 | const decoder = new TextDecoder("utf-8"); 141 | const line = decoder.decode( 142 | new Uint8Array(Module.stdoutBuffer) 143 | ); 144 | try { 145 | // Send the JSON stream message 146 | const obj = JSON.parse(line); 147 | if ( 148 | "location" in obj && 149 | "filename" in obj.location && 150 | typeof obj.location.filename === "string" && 151 | obj.location.filename.indexOf("/minizinc/") === 0 152 | ) { 153 | // Strip prefix from filename 154 | obj.location.filename = obj.location.filename.substring(10); 155 | } 156 | if ("stack" in obj && Array.isArray(obj.stack)) { 157 | for (const s of obj.stack) { 158 | if ( 159 | "location" in s && 160 | "filename" in s.location && 161 | typeof s.location.filename === "string" && 162 | s.location.filename.indexOf("/minizinc/") === 0 163 | ) { 164 | // Strip prefix from filename 165 | s.location.filename = s.location.filename.substring(10); 166 | } 167 | } 168 | } 169 | postMessage(obj); 170 | } catch (e) { 171 | // Fall back to creating a stdout message 172 | postMessage({ 173 | type: "stdout", 174 | value: line, 175 | }); 176 | } 177 | Module.stdoutBuffer = []; 178 | } 179 | }; 180 | const stderr = (code) => { 181 | if (code === 0x0) { 182 | return; 183 | } 184 | Module.stderrBuffer.push(code); 185 | if (Module.jsonStream && code === 0x0a) { 186 | // Send as a stderr message 187 | const decoder = new TextDecoder("utf-8"); 188 | const line = decoder.decode( 189 | new Uint8Array(Module.stderrBuffer) 190 | ); 191 | postMessage({ 192 | type: "stderr", 193 | value: line, 194 | }); 195 | Module.stderrBuffer = []; 196 | } 197 | }; 198 | Module.FS.init(null, stdout, stderr); 199 | Module.FS.mkdir("/minizinc"); 200 | 201 | // Make gecode_presolver available as gecode solver 202 | Module.FS.mkdir("/home/web_user/.minizinc"); 203 | Module.FS.writeFile( 204 | "/home/web_user/.minizinc/Preferences.json", 205 | JSON.stringify({ 206 | solverDefaults: [ 207 | [ 208 | "org.minizinc.gecode_presolver", 209 | "--backend-flags", 210 | "--allow-unbounded-vars", 211 | ], 212 | ], 213 | tagDefaults: [["", "org.minizinc.gecode_presolver"]], 214 | }) 215 | ); 216 | }, 217 | ], 218 | noInitialRun: true, 219 | noExitRuntime: true, 220 | }); 221 | } 222 | 223 | await initMiniZinc; 224 | postMessage({ type: "ready" }); 225 | } catch (e) { 226 | console.error(e); 227 | } 228 | }); 229 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ES2017", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "ESNext", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 52 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | // "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 77 | 78 | /* Type Checking */ 79 | "strict": true, /* Enable all strict type-checking options. */ 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "entryPoints": ["./types/index.d.ts"], 4 | "out": "docs", 5 | "name": "MiniZinc JavaScript", 6 | "excludePrivate": true, 7 | "excludeProtected": true, 8 | "excludeExternals": true, 9 | "navigationLinks": { 10 | "MiniZinc": "https://minizinc.dev", 11 | "GitHub": "https://github.com/minizinc/minizinc-js" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Main class for solving MiniZinc instances. 3 | * 4 | * This API allows you to add `.mzn`, `.dzn`, `.json` and `.mpc` files using 5 | * the `addFile()` method, and then run MiniZinc on the files using the 6 | * `solve()` method. 7 | * 8 | * Code can also be added programmatically using the `addString()` (and similar) 9 | * methods. 10 | * 11 | * @example 12 | * ```js 13 | * const model = new MiniZinc.Model(); 14 | * // Add a file with a given name and string contents 15 | * model.addFile('test.mzn', 'var 1..3: x; int: y;'); 16 | * // Add model code from a string 17 | * model.addString('int: z;'); 18 | * // Add data in DZN format 19 | * model.addDznString('y = 1;'); 20 | * // Add data from a JSON object 21 | * model.addJSON({z: 2}); 22 | * 23 | * const solve = model.solve({ 24 | * options: { 25 | * solver: 'gecode', 26 | * 'time-limit': 10000, 27 | * statistics: true 28 | * } 29 | * }); 30 | * 31 | * // You can listen for events 32 | * solve.on('solution', solution => console.log(solution)); 33 | * solve.on('statistics', stats => console.log(stats.statistics)); 34 | * 35 | * // And/or wait until complete 36 | * solve.then(result => { 37 | * console.log(result.solution); 38 | * console.log(result.statistics); 39 | * }); 40 | * ``` 41 | */ 42 | export class Model { 43 | /** 44 | * Create a new model. 45 | * 46 | * @example 47 | * ```js 48 | * const model = new MiniZinc.Model(); 49 | * ``` 50 | */ 51 | constructor(); 52 | 53 | /** 54 | * Create a clone of this model. 55 | * 56 | * @example 57 | * ```js 58 | * const m1 = new MiniZinc.Model(); 59 | * m1.addFile('test.mzn', ` 60 | * var 1..3: x; 61 | * int: y; 62 | * `); 63 | * const m2 = m1.clone(); 64 | * // Both m1 and m2 have test.mzn 65 | * 66 | * // Add different data to each model 67 | * m1.addJson({ 68 | * y: 1 69 | * }); 70 | * m2.addJson({ 71 | * y: 2 72 | * }); 73 | * ``` 74 | */ 75 | clone(): Model; 76 | 77 | /** 78 | * Add a snippet of code to the model. 79 | * 80 | * Note that each snippet is used as a complete model file. 81 | * 82 | * @example 83 | * ```js 84 | * model.addString("var 1..3: x;"); 85 | * model.addString("float: y;"); 86 | * ``` 87 | * 88 | * @param model MiniZinc code as a string 89 | * @returns The filename of the snippet (may be useful to identify sources of errors) 90 | */ 91 | addString(model: string): string; 92 | /** 93 | * Adds a snippet of data to the model. 94 | * 95 | * Note that each snippet is used as a complete data file. 96 | * 97 | * @example 98 | * ```js 99 | * model.addDznString("x = 1;"); 100 | * ``` 101 | * 102 | * @param dzn DataZinc input as a string 103 | * @returns The filename of the snippet (may be useful to identify sources of errors) 104 | */ 105 | addDznString(dzn: string): string; 106 | 107 | /** 108 | * Adds data to the model in JSON format. 109 | * 110 | * Note that each snippet is used as a complete JSON data file. 111 | * 112 | * @example 113 | * ```js 114 | * model.addJson({ 115 | * y: 1.5 116 | * }); 117 | * ``` 118 | * 119 | * @param data The data as an object in MiniZinc JSON data input format 120 | * @returns The filename of the snippet (may be useful to identify sources of errors) 121 | */ 122 | addJson(data: object): string; 123 | 124 | /** 125 | * Makes the given string contents available to MiniZinc using the given 126 | * filename. 127 | * 128 | * @example 129 | * ```js 130 | * /// Add this file to the MiniZinc command 131 | * model.addFile("model.mzn", ` 132 | * include "foo.mzn"; 133 | * var 1..3: x; 134 | * `); 135 | * // Make this file available, but don't add it to the MiniZinc command 136 | * model.addFile("foo.mzn", "var 1..3: y;", false); 137 | * ``` 138 | * 139 | * This method is generally only used from the browser. 140 | * 141 | * @param filename The file name to use 142 | * @param contents The contents of the file 143 | * @param use Whether to add this file as an argument to the MiniZinc command 144 | */ 145 | addFile(filename: string, contents: string, use?: boolean): void; 146 | /** 147 | * Adds the given file to the model. 148 | * 149 | * @example 150 | * ```js 151 | * model.addFile("./path/to/model.mzn"); 152 | * ``` 153 | * 154 | * Only available using the native version of MiniZinc in NodeJS. 155 | * 156 | * @param filename The file name to use 157 | */ 158 | addFile(filename: string): void; 159 | 160 | /** 161 | * Check for errors in the model using `--model-check-only`. 162 | * 163 | * @example 164 | * ```js 165 | * const errors = model.check({ 166 | * options: { 167 | * solver: 'gecode' 168 | * } 169 | * }); 170 | * for (const error of errors) { 171 | * console.log(error.what, error.message); 172 | * } 173 | * ``` 174 | * 175 | * @param config Configuration options 176 | * @returns The errors in the model 177 | */ 178 | check(config: { 179 | /** Options to pass to MiniZinc in parameter configuration file format */ 180 | options?: ParamConfig; 181 | }): Promise; 182 | 183 | /** 184 | * Get the model interface using `--model-interface-only`. 185 | * 186 | * @example 187 | * ```js 188 | * model.interface({ 189 | * options: { 190 | * solver: 'gecode' 191 | * } 192 | * }).then(console.log); 193 | * ``` 194 | * 195 | * @param config Configuration options 196 | * @returns The model interface 197 | */ 198 | interface(config: { 199 | /** Options to pass to MiniZinc in parameter configuration file format */ 200 | options?: ParamConfig; 201 | }): Promise; 202 | 203 | /** 204 | * Compile this model to FlatZinc. 205 | * 206 | * @example 207 | * ```js 208 | * const compile = model.compile({ 209 | * options: { 210 | * solver: 'gecode', 211 | * statistics: true 212 | * } 213 | * }); 214 | * 215 | * // Print compilation statistics when received 216 | * compile.on('statistics', e => console.log(e.statistics)); 217 | * 218 | * // Wait for completion 219 | * compile.then(console.log); 220 | * ``` 221 | * 222 | * @param config Configuration options 223 | */ 224 | compile(config: { 225 | /** Options to pass to MiniZinc in parameter configuration file format */ 226 | options?: ParamConfig; 227 | }): CompilationProgress; 228 | 229 | /** 230 | * Solve this model and retrieve the result. 231 | * 232 | * @example 233 | * ```js 234 | * // Begin solving 235 | * const solve = model.solve({ 236 | * options: { 237 | * solver: 'gecode', 238 | * 'all-solutions': true 239 | * } 240 | * }); 241 | * 242 | * // Print each solution as it is produced 243 | * solve.on('solution', e => console.log(e.output)); 244 | * 245 | * // Wait for completion 246 | * solve.then(result => { 247 | * console.log(result.status); 248 | * }); 249 | * ``` 250 | * 251 | * @param config Configuration options 252 | */ 253 | solve(config: { 254 | /** Whether to use `--output-mode json` (`true` by default) */ 255 | jsonOutput?: boolean; 256 | /** Options to pass to MiniZinc in parameter configuration file format */ 257 | options: ParamConfig; 258 | }): SolveProgress; 259 | } 260 | 261 | /** 262 | * Initialises MiniZinc. 263 | * 264 | * Calling this function is generally optional, but may be required if the 265 | * library is unable to automatically find the `minizinc-worker.js` script in 266 | * the browser, or the MiniZinc executable on NodeJS. 267 | * 268 | * @example 269 | * ```js 270 | * // In the browser 271 | * MiniZinc.init({ workerURL: 'https://localhost:3000/minizinc-worker.js'} ); 272 | * // In NodeJS 273 | * MiniZinc.init({ minizinc: '/path/to/minizinc' }); 274 | * ``` 275 | * 276 | * It may also be useful to call in the browser to get a promise which resolves 277 | * when the WebAssembly module has been loaded. 278 | * 279 | * @example 280 | * ```js 281 | * MiniZinc.init().then(() => { 282 | * console.log('Ready to start solving'); 283 | * }); 284 | * ``` 285 | * 286 | * @param config Configuration options for initialising MiniZinc 287 | */ 288 | export function init( 289 | config?: BrowserInitConfig | NodeInitConfig 290 | ): Promise; 291 | 292 | /** 293 | * Configuration options for initialising MiniZinc in the browser 294 | */ 295 | export interface BrowserInitConfig { 296 | /** URL of the worker script */ 297 | workerURL?: string | URL; 298 | /** URL of the minizinc.wasm file */ 299 | wasmURL?: string | URL; 300 | /** URL of the minizinc.data file */ 301 | dataURL?: string | URL; 302 | /** Size of web worker pool */ 303 | numWorkers?: number; 304 | } 305 | 306 | /** 307 | * Configuration options for initialising MiniZinc in NodeJS 308 | */ 309 | export interface NodeInitConfig { 310 | /** Name of, or path to the minizinc executable */ 311 | minizinc?: string; 312 | /** Paths to search for the MiniZinc executable in */ 313 | minizincPaths?: string[]; 314 | } 315 | 316 | /** 317 | * Get the version of MiniZinc as returned by `minizinc --version`. 318 | */ 319 | export function version(): Promise; 320 | 321 | /** 322 | * Get the list of solver configurations available using `minizinc --solvers-json`. 323 | */ 324 | export function solvers(): Promise; 325 | 326 | /** 327 | * Get the contents of a text file in the MiniZinc share directory. 328 | * 329 | * Only files inside the MiniZinc share directory (or its children) may be used. 330 | * 331 | * @param file The path to the desired file relative to the MiniZinc share directory 332 | * @returns The contents of the file, or null if it can't be read 333 | */ 334 | export function readStdlibFileContents(file: string): Promise; 335 | /** 336 | * Get the contents of text files in the MiniZinc share directory. 337 | * 338 | * Only files inside the MiniZinc share directory (or its children) may be used. 339 | * 340 | * @param files An array of paths to the desired files relative to the MiniZinc share directory 341 | * @returns An object whose keys are requested file names and whose values are the file contents, or null if it can't be read 342 | */ 343 | export function readStdlibFileContents( 344 | files: string[] 345 | ): Promise<{ [key: string]: string }>; 346 | 347 | /** Terminate any running MiniZinc processes and cleanup. */ 348 | export function shutdown(): void; 349 | 350 | /** 351 | * Options configuration in parameter configuration file ([`.mpc`](https://minizinc.dev/doc-latest/en/command_line.html#ch-param-files)) 352 | * format. 353 | * 354 | * @example 355 | * ```js 356 | * model.solve({ 357 | * options: { 358 | * solver: 'gecode', // Maps to --solver gecode 359 | * 'all-solutions': true, // Maps to --all-solutions 360 | * 'output-objective': true, // maps to --output-objective 361 | * } 362 | * }); 363 | * ``` 364 | */ 365 | export interface ParamConfig { 366 | /** Solver tag to use. */ 367 | solver?: string; 368 | /** MiniZinc command line options. 369 | * 370 | * The leading `--` can be omitted, and if the value is `true` it is treated 371 | * as a flag). 372 | */ 373 | [arg: string]: any; 374 | } 375 | 376 | /** 377 | * An error message from MiniZinc. 378 | * 379 | * @category Events 380 | */ 381 | export interface ErrorMessage { 382 | /** Message type */ 383 | type: "error"; 384 | /** The kind of error which occurred */ 385 | what: string; 386 | /** The error message */ 387 | message: string; 388 | /** The file location if there is one */ 389 | location?: Location; 390 | /** The stack trace is there is one */ 391 | stack?: StackItem[]; 392 | /** Other error-specific data */ 393 | [key: string]: any; 394 | } 395 | 396 | /** 397 | * A warning message from MiniZinc. 398 | * 399 | * @category Events 400 | */ 401 | export interface WarningMessage { 402 | /** Message type */ 403 | type: "warning"; 404 | /** The kind of warning which occurred */ 405 | what: string; 406 | /** The error message */ 407 | message: string; 408 | /** The file location if there is one */ 409 | location?: Location; 410 | /** The stack trace is there is one */ 411 | stack?: StackItem[]; 412 | /** Other error-specific data */ 413 | [key: string]: any; 414 | } 415 | 416 | /** 417 | * A location in a file. 418 | */ 419 | export interface Location { 420 | /** Filename */ 421 | filename: string; 422 | /** First line (starting from 1) */ 423 | firstLine: number; 424 | /** First character (starting from 1) */ 425 | firstColumn: number; 426 | /** Last line (inclusive) */ 427 | lastLine: number; 428 | /** Last column (inclusive) */ 429 | lastColumn: number; 430 | } 431 | 432 | /** 433 | * A stack trace item. 434 | */ 435 | export interface StackItem { 436 | /** Location */ 437 | location: Location; 438 | /** Whether this is a comprehension iteration item */ 439 | isCompIter: boolean; 440 | /** String description of the stack trace item */ 441 | description: string; 442 | } 443 | 444 | /** 445 | * Model interface output. 446 | * 447 | * @category Events 448 | */ 449 | export interface ModelInterface { 450 | /** Message type */ 451 | type: "interface"; 452 | /** Model input parameters */ 453 | input: { [name: string]: VarType }; 454 | /** Model output variables */ 455 | output: { [name: string]: VarType }; 456 | /** Solve method */ 457 | method: "sat" | "min" | "max"; 458 | /** Whether there is an output item present */ 459 | has_output_item: boolean; 460 | /** Files included */ 461 | included_files: string[]; 462 | /** Global constraints used */ 463 | globals: string[]; 464 | } 465 | 466 | /** 467 | * Type definition of a variable/parameter. 468 | */ 469 | export interface VarType { 470 | /** Type of variable */ 471 | type: "int" | "float" | "bool" | "string"; 472 | /** Number of array dimensions if this is an array */ 473 | dim?: number; 474 | /** Whether or not this variable is a set */ 475 | set?: true; 476 | } 477 | 478 | /** 479 | * Model output mapping section names to their contents. 480 | * 481 | * The default mode will populate the `json` key with an object mapping 482 | * variable names to their values (see the [MiniZinc documentation](https://minizinc.dev/doc-latest/en/spec.html#json-support) 483 | * for details on the format). 484 | * 485 | * Note that sections ending with `_json` will be arrays. 486 | */ 487 | export interface Output { 488 | /** The string output to the 'default' section (where no section was 489 | * specified). */ 490 | default?: string; 491 | /** The DZN output if produced. 492 | * 493 | * @example 494 | * ```js 495 | * model.solve({ 496 | * jsonOutput: false, 497 | * options: { 498 | * solver: 'gecode', 499 | * 'output-mode': 'dzn' 500 | * } 501 | * }); 502 | * ``` 503 | * 504 | */ 505 | dzn?: string; 506 | /** The output of all sections combined */ 507 | raw?: string; 508 | /** The JSON output if produced */ 509 | json?: { [variable: string]: any }; 510 | /** Output to user-defined sections */ 511 | [section: string]: string | any[] | object | undefined; 512 | } 513 | 514 | /** 515 | * Event emitted when a solution is found by the solver. 516 | * 517 | * @category Events 518 | */ 519 | export interface SolutionMessage { 520 | /** Message type */ 521 | type: "solution"; 522 | /** Time in milliseconds (if run with `output-time: true`) */ 523 | time?: number; 524 | /** Mapping between output section name and contents */ 525 | output: Output; 526 | /** The sections output in order */ 527 | sections: string[]; 528 | } 529 | 530 | /** 531 | * Event emitted when a solution checker has been run. 532 | * 533 | * @category Events 534 | */ 535 | export interface CheckerMessage { 536 | /** Message type */ 537 | type: "checker"; 538 | /** Time in milliseconds (if run with `output-time: true`) */ 539 | time?: number; 540 | /** Messages produced by the checker */ 541 | messages: (SolutionMessage | TraceMessage | ErrorMessage | WarningMessage)[]; 542 | /** Mapping between output section name and contents 543 | * 544 | * @deprecated Use the `messages` list to retrieve the output instead. 545 | */ 546 | output: Output; 547 | /** The sections output in order 548 | * 549 | * @deprecated Use the `messages` list to retrieve the output instead. 550 | */ 551 | sections: string[]; 552 | } 553 | 554 | /** 555 | * Solve status. 556 | */ 557 | export type Status = 558 | /** All solutions found for a satisfaction problem */ 559 | | "ALL_SOLUTIONS" 560 | /** Optimal solution found for an optimisation problem */ 561 | | "OPTIMAL_SOLUTION" 562 | /** Problem is unsatisfiable */ 563 | | "UNSATISFIABLE" 564 | /** Problem is unbounded */ 565 | | "UNBOUNDED" 566 | /** Problem is unsatisfiable or unbounded */ 567 | | "UNSAT_OR_UNBOUNDED" 568 | /** Problem is satisfied */ 569 | | "SATISFIED" 570 | /** Status is unknown */ 571 | | "UNKNOWN" 572 | /** An error occurred */ 573 | | "ERROR"; 574 | 575 | /** 576 | * Event emitted when the final status is emitted by the solver. 577 | * 578 | * Note that this event may not be emitted at all. 579 | * 580 | * @category Events 581 | */ 582 | export interface StatusMessage { 583 | /** Message type */ 584 | type: "status"; 585 | /** Status */ 586 | status: Status; 587 | /** Time in milliseconds (if run with `output-time: true`) */ 588 | time?: number; 589 | } 590 | 591 | /** 592 | * Event emitted when statistics are received from the compiler or the solver. 593 | * 594 | * @category Events 595 | */ 596 | export interface StatisticsMessage { 597 | /** Message type */ 598 | type: "statistics"; 599 | /** Mapping between statistic name and value */ 600 | statistics: { [key: string]: any }; 601 | } 602 | 603 | /** 604 | * Event emitted when a timestamp message is received independently of a solution 605 | * or status. 606 | * 607 | * @category Events 608 | */ 609 | export interface TimestampMessage { 610 | /** Message type */ 611 | type: "time"; 612 | /** Time in milliseconds */ 613 | time: number; 614 | } 615 | 616 | /** 617 | * Event emitted when MiniZinc encounters a call to `trace()`. 618 | * 619 | * @category Events 620 | */ 621 | export interface TraceMessage { 622 | /** Message type */ 623 | type: "trace"; 624 | /** Output section */ 625 | section: string; 626 | /** Trace message (usually a string, but may be an array if the section ends 627 | * with `_json`). */ 628 | message: string | any[]; 629 | } 630 | 631 | /** Event emitted when MiniZinc exits. 632 | * 633 | * If solving/compilation is cancelled with the `cancel()` method, then this 634 | * event is still emitted, but with a `null` value for the `code`. 635 | * 636 | * @category Events 637 | */ 638 | export interface ExitMessage { 639 | /** Message type */ 640 | type: "exit"; 641 | /** Exit code, or null if the process was interrupted */ 642 | code: number | null; 643 | /** Possible message if there was an error */ 644 | message?: string; 645 | } 646 | 647 | /** 648 | * Thenable controller for a compilation request. 649 | * 650 | * Used to listen to events during compilation, and can be awaited to retrieve 651 | * the compiled FlatZinc. 652 | */ 653 | export interface CompilationProgress extends PromiseLike { 654 | /** 655 | * Return whether or not compilation is still in progress. 656 | * 657 | * @example 658 | * ```js 659 | * const compile = model.compile({ 660 | * options: { 661 | * solver: 'gecode' 662 | * } 663 | * }); 664 | * setInterval(() => { 665 | * if (compile.isRunning()) { 666 | * console.log('Still running'); 667 | * } 668 | * }, 1000) 669 | * ``` 670 | */ 671 | isRunning(): boolean; 672 | 673 | /** Cancel compilation. 674 | * 675 | * @example 676 | * ```js 677 | * const compile = model.compile({ 678 | * options: { 679 | * solver: 'gecode' 680 | * } 681 | * }); 682 | * setTimeout(() => { 683 | * if (compile.isRunning()) { 684 | * compile.cancel(); 685 | * } 686 | * }, 10000); 687 | * ``` 688 | */ 689 | cancel(): void; 690 | 691 | /** Listen for an event. 692 | * 693 | * @example 694 | * ```js 695 | * const compile = model.compile({ 696 | * solver: 'gecode', 697 | * statistics: true 698 | * }); 699 | * solve.on('statistics', e => { 700 | * console.log(e.statistics); 701 | * }); 702 | * ``` 703 | */ 704 | on(event: "statistics", callback: (e: StatisticsMessage) => void): void; 705 | on(event: "trace", callback: (e: TraceMessage) => void): void; 706 | on(event: "error", callback: (e: ErrorMessage) => void): void; 707 | on(event: "warning", callback: (e: WarningMessage) => void): void; 708 | on(event: "exit", callback: (e: ExitMessage) => void): void; 709 | on(event: string, callback: (e: object) => void): void; 710 | 711 | /** Stop listening for an event. 712 | * 713 | * @example 714 | * ```js 715 | * const compile = model.compile({ 716 | * solver: 'gecode', 717 | * statistics: true 718 | * }); 719 | * const onStat = e => { 720 | * console.log(e.output); 721 | * }; 722 | * // Start listening 723 | * compile.on('statistics', onStat); 724 | * setTimeout(() => { 725 | * // Stop listening 726 | * compile.off('statistics', onStat); 727 | * }, 1000); 728 | * ``` 729 | */ 730 | off(event: string, callback: (e: T) => void): void; 731 | } 732 | 733 | /** 734 | * Thenable controller for a solve request. 735 | * 736 | * Used to listen to events during solving, and can be awaited to retrieve 737 | * the final solution/statistics/status. 738 | */ 739 | export interface SolveProgress extends PromiseLike { 740 | /** 741 | * Return whether or not solving is still in progress. 742 | * 743 | * @example 744 | * ```js 745 | * const solve = model.solve({ 746 | * solver: 'gecode' 747 | * }); 748 | * setInterval(() => { 749 | * if (solve.isRunning()) { 750 | * console.log('Still running'); 751 | * } 752 | * }, 1000) 753 | * ``` 754 | */ 755 | isRunning(): boolean; 756 | 757 | /** Cancel solving. 758 | * 759 | * @example 760 | * ```js 761 | * const solve = model.solve({ 762 | * solver: 'gecode' 763 | * }); 764 | * setTimeout(() => { 765 | * if (solve.isRunning()) { 766 | * solve.cancel(); 767 | * } 768 | * }, 10000); 769 | * ``` 770 | */ 771 | cancel(): void; 772 | 773 | /** Listen for an event. 774 | * 775 | * @example 776 | * ```js 777 | * const solve = model.solve({ 778 | * solver: 'gecode' 779 | * }); 780 | * solve.on('solution', e => { 781 | * console.log(e.output); 782 | * }); 783 | * ``` 784 | */ 785 | on(event: "solution", callback: (e: SolutionMessage) => void): void; 786 | on(event: "checker", callback: (e: CheckerMessage) => void): void; 787 | on(event: "status", callback: (e: StatusMessage) => void): void; 788 | on(event: "statistics", callback: (e: StatisticsMessage) => void): void; 789 | on(event: "timestamp", callback: (e: TimestampMessage) => void): void; 790 | on(event: "trace", callback: (e: TraceMessage) => void): void; 791 | on(event: "error", callback: (e: ErrorMessage) => void): void; 792 | on(event: "warning", callback: (e: WarningMessage) => void): void; 793 | on(event: "exit", callback: (e: ExitMessage) => void): void; 794 | on(event: string, callback: (e: object) => void): void; 795 | 796 | /** Stop listening for an event. 797 | * 798 | * @example 799 | * ```js 800 | * const solve = model.solve({ 801 | * solver: 'gecode' 802 | * }); 803 | * const onSolution = e => { 804 | * console.log(e.output); 805 | * }; 806 | * // Start listening 807 | * solve.on('solution', onSolution); 808 | * setTimeout(() => { 809 | * // Stop listening 810 | * solve.off('solution', onSolution); 811 | * }, 1000); 812 | * ``` 813 | */ 814 | off(event: string, callback: (e: T) => void): void; 815 | } 816 | 817 | /** 818 | * Result of solving the model. 819 | */ 820 | export interface SolveResult { 821 | /** Solve status. */ 822 | status: Status; 823 | /** The final solution if any was found. */ 824 | solution: SolutionMessage | null; 825 | /** A combined statistics object with the latest value for each key if 826 | * statistics output was enabled during solving. 827 | */ 828 | statistics: { [key: string]: any }; 829 | } 830 | --------------------------------------------------------------------------------