diff --git a/.babelrc b/.babelrc index 05581748b..1320b9a32 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,3 @@ { - "presets": ["es2015", "stage-2"] + "presets": ["@babel/preset-env"] } diff --git a/.eslintrc.json b/.eslintrc.json index 79e628819..885e082de 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -9,6 +9,11 @@ "plugin:@typescript-eslint/recommended-requiring-type-checking", "prettier" ], + "ignorePatterns": [ + "**/__tests__/**", + "**/__mocks__/**", + "jest.setup.ts" + ], "parser": "@typescript-eslint/parser", "parserOptions": { "project": "tsconfig.json", diff --git a/.gitignore b/.gitignore index 7bb67a701..914a029d2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ node_modules dist/ .idea/ coverage/ +yarn-error.log # emacs backup files *~ diff --git a/package.json b/package.json index c7d0bd67f..8fba47f4c 100644 --- a/package.json +++ b/package.json @@ -33,9 +33,9 @@ "dependencies": { "@babel/parser": "^7.19.4", "@joeychenofficial/alt-ergo-modified": "^2.4.0", - "@ts-morph/bootstrap": "^0.18.0", + "@ts-morph/bootstrap": "^0.20.0", "@types/estree": "0.0.52", - "acorn": "^8.8.2", + "acorn": "^8.10.0", "acorn-class-fields": "^1.0.0", "acorn-loose": "^8.0.0", "acorn-walk": "^8.0.0", @@ -44,7 +44,7 @@ "js-base64": "^3.7.5", "lodash": "^4.17.20", "node-getopt": "^0.3.2", - "source-map": "0.7.3", + "source-map": "0.7.4", "xmlhttprequest-ts": "^1.0.1" }, "scripts": { @@ -67,7 +67,7 @@ "@types/jest": "^29.0.0", "@types/lodash.assignin": "^4.2.6", "@types/lodash.clonedeep": "^4.5.6", - "@types/node": "^17.0.5", + "@types/node": "^20.4.5", "@types/offscreencanvas": "^2019.7.0", "@typescript-eslint/eslint-plugin": "^4.4.1", "@typescript-eslint/parser": "^4.4.1", diff --git a/sicp_publish/yarn.lock b/sicp_publish/yarn.lock index f8d7b9d7d..fe980da9d 100644 --- a/sicp_publish/yarn.lock +++ b/sicp_publish/yarn.lock @@ -2,17 +2,46 @@ # yarn lockfile v1 +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + "@joeychenofficial/alt-ergo-modified@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@joeychenofficial/alt-ergo-modified/-/alt-ergo-modified-2.4.0.tgz#27aec0cbed8ab4e2f0dad6feb4f0c9766ac3132f" integrity sha512-58b0K8pNUVZXGbua4IJQ+1K+E+jz3MkhDazZaaeKlD+sOLYR9iTHIbicV/I5K16ivYW6R9lONiT3dz8rMeFJ1w== +"@npmcli/fs@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-3.1.0.tgz#233d43a25a91d68c3a863ba0da6a3f00924a173e" + integrity sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w== + dependencies: + semver "^7.3.5" + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + "@types/estree@0.0.52": version "0.0.52" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.52.tgz#7f1f57ad5b741f3d5b210d3b1f145640d89bf8fe" integrity sha512-BZWrtCU0bMVAIliIV+HJO1f1PR41M7NKjfxrFJwwhKI1KwhwOxYw1SXg9ao+CIMt774nFuGiG6eU+udtbEI9oQ== -abbrev@1: +abbrev@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== @@ -35,74 +64,73 @@ acorn@^7.1.1: integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== acorn@^8.0.3, acorn@^8.5.0: - version "8.8.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" - integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== -ajv@^6.12.3: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== +agent-base@6, agent-base@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" + debug "4" -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== +agentkeepalive@^4.2.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.3.0.tgz#bb999ff07412653c1803b3ced35e50729830a255" + integrity sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg== + dependencies: + debug "^4.1.0" + depd "^2.0.0" + humanize-ms "^1.2.1" + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -aproba@^1.0.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== - -are-we-there-yet@~1.1.2: - version "1.1.7" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146" - integrity sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g== - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== -asn1@~0.2.3: - version "0.2.6" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" - integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== + color-convert "^2.0.1" -astring@^1.4.3: - version "1.8.3" - resolved "https://registry.yarnpkg.com/astring/-/astring-1.8.3.tgz#1a0ae738c7cc558f8e5ddc8e3120636f5cebcb85" - integrity sha512-sRpyiNrx2dEYIMmUXprS8nlpRg2Drs8m9ElX9vVEXaCB4XEAJhKfs7IcX0IwShjuOAjLR6wzIrgoptz1n19i1A== +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== +"aproba@^1.0.3 || ^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== +are-we-there-yet@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd" + integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" -aws4@^1.8.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" - integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== +astring@^1.4.3: + version "1.8.6" + resolved "https://registry.yarnpkg.com/astring/-/astring-1.8.6.tgz#2c9c157cf1739d67561c56ba896e6948f6b93731" + integrity sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg== balanced-match@^1.0.0: version "1.0.2" @@ -114,13 +142,6 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== - dependencies: - tweetnacl "^0.14.3" - bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -150,6 +171,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + buffer@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" @@ -158,10 +186,23 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== +cacache@^17.0.0: + version "17.1.3" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-17.1.3.tgz#c6ac23bec56516a7c0c52020fd48b4909d7c7044" + integrity sha512-jAdjGxmPxZh0IipMdR7fK/4sDSrHMLUV0+GvVUsjwyGNKHsh79kW/otg+GkbXwl6Uzvy9wsvHOX4nUoWldeZMg== + dependencies: + "@npmcli/fs" "^3.1.0" + fs-minipass "^3.0.0" + glob "^10.2.2" + lru-cache "^7.7.1" + minipass "^5.0.0" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + p-map "^4.0.0" + ssri "^10.0.0" + tar "^6.1.11" + unique-filename "^3.0.0" chownr@^1.1.1: version "1.1.4" @@ -173,85 +214,108 @@ chownr@^2.0.0: resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA== +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -combined-stream@^1.0.6, combined-stream@~1.0.6: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== dependencies: - delayed-stream "~1.0.0" + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-support@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -console-control-strings@^1.0.0, console-control-strings@~1.1.0: +console-control-strings@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== -core-util-is@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== - core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== +cross-spawn@^7.0.0: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== dependencies: - assert-plus "^1.0.0" + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" -decompress-response@^4.2.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" - integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== +debug@4, debug@^4.1.0, debug@^4.3.3: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: - mimic-response "^2.0.0" + ms "2.1.2" + +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== -detect-libc@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== +depd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" +detect-libc@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.2.tgz#8ccf2ba9315350e1241b88d0ac3b0e1fbd99605d" + integrity sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw== + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +encoding@^0.1.13: + version "0.1.13" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" + end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -264,54 +328,33 @@ env-paths@^2.2.0: resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== +err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + expand-template@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== -extend@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== - -extsprintf@^1.2.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" - integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== - -fast-deep-equal@^3.1.1: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== +exponential-backoff@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" + integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== - -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== +foreground-child@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" + integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" + cross-spawn "^7.0.0" + signal-exit "^4.0.1" fs-constants@^1.0.0: version "1.0.0" @@ -325,31 +368,31 @@ fs-minipass@^2.0.0: dependencies: minipass "^3.0.0" +fs-minipass@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-3.0.2.tgz#5b383858efa8c1eb8c33b39e994f7e8555b8b3a3" + integrity sha512-2GAfyfoaCDRrM6jaOS3UsBts8yJ55VioXdWcOL7dK9zdAuKT71+WBA4ifnNYqVjYv+4SsPxjK0JT4yIIn4cA/g== + dependencies: + minipass "^5.0.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg== - dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== - dependencies: - assert-plus "^1.0.0" +gauge@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce" + integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.3" + console-control-strings "^1.1.0" + has-unicode "^2.0.1" + signal-exit "^3.0.7" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.5" github-from-package@0.0.0: version "0.0.0" @@ -361,18 +404,29 @@ gl-wiretap@^0.6.2: resolved "https://registry.yarnpkg.com/gl-wiretap/-/gl-wiretap-0.6.2.tgz#e4aa19622831088fbaa7e5a18d01768f7a3fb07c" integrity sha512-fxy1XGiPkfzK+T3XKDbY7yaqMBmozCGvAFyTwaZA3imeZH83w7Hr3r3bYlMRWIyzMI/lDUvUMM/92LE2OwqFyQ== -gl@^4.5.2: - version "4.9.2" - resolved "https://registry.yarnpkg.com/gl/-/gl-4.9.2.tgz#dd31cdaec7d3c4b6761648111e55531f86137821" - integrity sha512-lLYaicQxsRPxOnKWX9pIGmtKRuw0epvI089yl9uBvemYxR9xE01eRuXJgje1U0/06Df7bdOmmcW87IPOsu52Ow== +gl@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/gl/-/gl-5.0.3.tgz#a10f37c50e48954348cc3e790b83313049bdbe1c" + integrity sha512-toWmb3Rgli5Wl9ygjZeglFBVLDYMOomy+rXlVZVDCoIRV+6mQE5nY4NgQgokYIc5oQzc1pvWY9lQJ0hGn61ZUg== dependencies: bindings "^1.5.0" bit-twiddle "^1.0.2" - glsl-tokenizer "^2.0.2" - nan "^2.15.0" - node-abi "^2.30.1" - node-gyp "^7.1.2" - prebuild-install "^5.3.6" + glsl-tokenizer "^2.1.5" + nan "^2.16.0" + node-abi "^3.22.0" + node-gyp "^9.0.0" + prebuild-install "^7.1.1" + +glob@^10.2.2: + version "10.3.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.3.tgz#8360a4ffdd6ed90df84aa8d52f21f452e86a123b" + integrity sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw== + dependencies: + foreground-child "^3.1.0" + jackspeak "^2.0.3" + minimatch "^9.0.1" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-scurry "^1.10.1" glob@^7.1.3, glob@^7.1.4: version "7.2.3" @@ -386,7 +440,7 @@ glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" -glsl-tokenizer@^2.0.2: +glsl-tokenizer@^2.1.5: version "2.1.5" resolved "https://registry.yarnpkg.com/glsl-tokenizer/-/glsl-tokenizer-2.1.5.tgz#1c2e78c16589933c274ba278d0a63b370c5fee1a" integrity sha512-XSZEJ/i4dmz3Pmbnpsy3cKh7cotvFlBiZnDOwnj/05EwNp2XrhQ4XKJxT7/pDt4kp4YcpRSKz8eTV7S+mwV6MA== @@ -399,53 +453,77 @@ gpu-mock.js@^1.3.0: integrity sha512-+lbp8rQ0p1nTa6Gk6HoLiw4yM6JTpql82U+nCF3sZbX4FJWP9PzzF1018dW8K+pbmqRmhLHbn6Bjc6i6tgUpbA== gpu.js@^2.10.4: - version "2.15.2" - resolved "https://registry.yarnpkg.com/gpu.js/-/gpu.js-2.15.2.tgz#481d389cd984fcd3f88d9dd5b2fce08c1505443c" - integrity sha512-tgTRoWEE0X2PE3OgTNub9OwadXND2K7qwOW/VwhIiedY/mbiT93jJIrgWAKBgdhQR0JmyN4ObkSu0/vjSpbuiQ== + version "2.16.0" + resolved "https://registry.yarnpkg.com/gpu.js/-/gpu.js-2.16.0.tgz#35531f8ee79292b71a454455e7bf0aaf6693182a" + integrity sha512-ZKmWdRXi3F/9nim5ew2IPXZaMlSyqtMtVC8Itobjl+qYUgUqcBHxu8WKqK0AqIyjBVBl7A5ORo4Db2hzgrPd4A== dependencies: acorn "^7.1.1" - gl "^4.5.2" + gl "^5.0.3" gl-wiretap "^0.6.2" gpu-mock.js "^1.3.0" webgpu "^0.1.16" -graceful-fs@^4.2.3: - version "4.2.10" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== +graceful-fs@^4.2.6: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q== - -har-validator@~5.1.3: - version "5.1.5" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" - integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== - dependencies: - ajv "^6.12.3" - har-schema "^2.0.0" - -has-unicode@^2.0.0: +has-unicode@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ== +http-cache-semantics@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== + +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" + ms "^2.0.0" + +iconv-lite@^0.6.2: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" ieee754@^1.1.13: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -454,7 +532,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -464,72 +542,45 @@ ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw== - dependencies: - number-is-nan "^1.0.0" +ip@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" + integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== +is-lambda@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" + integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" - integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== - -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== - -jsprim@^1.2.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" - integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.4.0" - verror "1.10.0" +jackspeak@^2.0.3: +<<<<<<< HEAD + version "2.2.1" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.2.1.tgz#655e8cf025d872c9c03d3eb63e8f0c024fef16a6" + integrity sha512-MXbxovZ/Pm42f6cDIDkl3xpwv1AGwObKwfmjs2nQePiy85tP3fatofl3FC1aBsOtP/6fq5SbtgHwWcMsLP+bDw== +======= + version "2.2.2" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.2.2.tgz#707c62733924b8dc2a0a629dc6248577788b5385" + integrity sha512-mgNtVv4vUuaKA97yxUHoA3+FkuhtxkjdXEWOyB/N76fjy0FjezEt34oy3epBtvCvS+7DyKwqCFWx/oJLV5+kCg== +>>>>>>> 545f558853f44e377aca8822074ea1730f42921e + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" lodash@^4.17.20: version "4.17.21" @@ -543,22 +594,41 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mimic-response@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" - integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== +lru-cache@^7.7.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + +"lru-cache@^9.1.1 || ^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.0.tgz#b9e2a6a72a129d81ab317202d93c7691df727e61" + integrity sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw== + +make-fetch-happen@^11.0.3: + version "11.1.1" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz#85ceb98079584a9523d4bf71d32996e7e208549f" + integrity sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w== + dependencies: + agentkeepalive "^4.2.1" + cacache "^17.0.0" + http-cache-semantics "^4.1.1" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^7.7.1" + minipass "^5.0.0" + minipass-fetch "^3.0.0" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^0.6.3" + promise-retry "^2.0.1" + socks-proxy-agent "^7.0.0" + ssri "^10.0.0" + +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== minimatch@^3.1.1: version "3.1.2" @@ -567,19 +637,75 @@ minimatch@^3.1.1: dependencies: brace-expansion "^1.1.7" +minimatch@^9.0.1: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0, minimist@^1.2.3: - version "1.2.6" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" - integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" + +minipass-fetch@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-3.0.3.tgz#d9df70085609864331b533c960fd4ffaa78d15ce" + integrity sha512-n5ITsTkDqYkYJZjcRWzZt9qnZKCT7nKCosJhHoj7S7zD+BP4jVbWs+odsniw5TA3E0sLomhTKOKjF86wf11PuQ== + dependencies: + minipass "^5.0.0" + minipass-sized "^1.0.3" + minizlib "^2.1.2" + optionalDependencies: + encoding "^0.1.13" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-pipeline@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + +minipass-sized@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" + integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== + dependencies: + minipass "^3.0.0" minipass@^3.0.0: - version "3.3.4" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.4.tgz#ca99f95dd77c43c7a76bf51e6d200025eee0ffae" - integrity sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw== + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== dependencies: yallist "^4.0.0" -minizlib@^2.1.1: +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": + version "7.0.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.2.tgz#58a82b7d81c7010da5bd4b2c0c85ac4b4ec5131e" + integrity sha512-eL79dXrE1q9dBbDCLg7xfn/vl7MS4F1gvJAgjJrQli/jbQWdUttuVawphqpffoIYfRdq78LHx6GP4bU/EQ2ATA== + +minizlib@^2.1.1, minizlib@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== @@ -597,75 +723,71 @@ mkdirp@^1.0.3: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -nan@^2.15.0: - version "2.16.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.16.0.tgz#664f43e45460fb98faf00edca0bb0d7b8dce7916" - integrity sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA== +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@^2.0.0: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nan@^2.16.0: + version "2.17.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" + integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== napi-build-utils@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== -node-abi@^2.30.1, node-abi@^2.7.0: - version "2.30.1" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.30.1.tgz#c437d4b1fe0e285aaf290d45b45d4d7afedac4cf" - integrity sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w== +negotiator@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +node-abi@^3.22.0, node-abi@^3.3.0: + version "3.45.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.45.0.tgz#f568f163a3bfca5aacfce1fbeee1fa2cc98441f5" + integrity sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ== dependencies: - semver "^5.4.1" + semver "^7.3.5" -node-gyp@^7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-7.1.2.tgz#21a810aebb187120251c3bcec979af1587b188ae" - integrity sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ== +node-gyp@^9.0.0: + version "9.4.0" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.4.0.tgz#2a7a91c7cba4eccfd95e949369f27c9ba704f369" + integrity sha512-dMXsYP6gc9rRbejLXmTbVRYjAHw7ppswsKyMxuxJxxOHzluIO1rGp9TOQgjFJ+2MCqcOcQTOPB/8Xwhr+7s4Eg== dependencies: env-paths "^2.2.0" + exponential-backoff "^3.1.1" glob "^7.1.4" - graceful-fs "^4.2.3" - nopt "^5.0.0" - npmlog "^4.1.2" - request "^2.88.2" + graceful-fs "^4.2.6" + make-fetch-happen "^11.0.3" + nopt "^6.0.0" + npmlog "^6.0.0" rimraf "^3.0.2" - semver "^7.3.2" - tar "^6.0.2" + semver "^7.3.5" + tar "^6.1.2" which "^2.0.2" -noop-logger@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2" - integrity sha512-6kM8CLXvuW5crTxsAtva2YLrRrDaiTIkIePWs9moLHqbFWT94WpNFjwS/5dfLfECg5i/lkmw3aoqVidxt23TEQ== - -nopt@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" - integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== +nopt@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-6.0.0.tgz#245801d8ebf409c6df22ab9d95b65e1309cdb16d" + integrity sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g== dependencies: - abbrev "1" + abbrev "^1.0.0" -npmlog@^4.0.1, npmlog@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== +npmlog@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830" + integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ== - -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - -object-assign@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + are-we-there-yet "^3.0.0" + console-control-strings "^1.1.0" + gauge "^4.0.3" + set-blocking "^2.0.0" once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" @@ -674,46 +796,56 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-scurry@^1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698" + integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== + dependencies: + lru-cache "^9.1.1 || ^10.0.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" -prebuild-install@^5.3.6: - version "5.3.6" - resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.3.6.tgz#7c225568d864c71d89d07f8796042733a3f54291" - integrity sha512-s8Aai8++QQGi4sSbs/M1Qku62PFK49Jm1CbgXklGz4nmHveDq0wzJkg7Na5QbnO1uNH8K7iqx2EQ/mV0MZEmOg== +prebuild-install@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" + integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== dependencies: - detect-libc "^1.0.3" + detect-libc "^2.0.0" expand-template "^2.0.3" github-from-package "0.0.0" minimist "^1.2.3" mkdirp-classic "^0.5.3" napi-build-utils "^1.0.1" - node-abi "^2.7.0" - noop-logger "^0.1.1" - npmlog "^4.0.1" + node-abi "^3.3.0" pump "^3.0.0" rc "^1.2.7" - simple-get "^3.0.3" + simple-get "^4.0.0" tar-fs "^2.0.0" tunnel-agent "^0.6.0" - which-pm-runs "^1.0.0" -process-nextick-args@~2.0.0: +promise-retry@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -psl@^1.1.28: - version "1.9.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" - integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== + dependencies: + err-code "^2.0.2" + retry "^0.12.0" pump@^3.0.0: version "3.0.0" @@ -723,16 +855,6 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@^2.1.0, punycode@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -qs@~6.5.2: - version "6.5.3" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" - integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== - rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -753,53 +875,19 @@ rc@^1.2.7: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^2.0.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.1.1, readable-stream@^3.4.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== dependencies: inherits "^2.0.3" string_decoder "^1.1.1" util-deprecate "^1.0.1" -request@^2.88.2: - version "2.88.2" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" - integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^3.3.2" +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== rimraf@^3.0.2: version "3.0.2" @@ -808,87 +896,105 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: +safe-buffer@^5.0.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -semver@^5.4.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^7.3.2: - version "7.3.7" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" - integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== +semver@^7.3.5: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" -set-blocking@~2.0.0: +set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== -signal-exit@^3.0.0: +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +signal-exit@^4.0.1: +<<<<<<< HEAD + version "4.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.0.2.tgz#ff55bb1d9ff2114c13b400688fa544ac63c36967" + integrity sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q== +======= + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== +>>>>>>> 545f558853f44e377aca8822074ea1730f42921e + simple-concat@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== -simple-get@^3.0.3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.1.tgz#cc7ba77cfbe761036fbfce3d021af25fc5584d55" - integrity sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA== +simple-get@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" + integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== dependencies: - decompress-response "^4.2.0" + decompress-response "^6.0.0" once "^1.3.1" simple-concat "^1.0.0" +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + +socks-proxy-agent@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz#dc069ecf34436621acb41e3efa66ca1b5fed15b6" + integrity sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww== + dependencies: + agent-base "^6.0.2" + debug "^4.3.3" + socks "^2.6.2" + +socks@^2.6.2: + version "2.7.1" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55" + integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ== + dependencies: + ip "^2.0.0" + smart-buffer "^4.2.0" + source-map@^0.7.3: version "0.7.4" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== -sshpk@^1.7.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" - integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" - -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw== +ssri@^10.0.0: + version "10.0.4" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-10.0.4.tgz#5a20af378be586df139ddb2dfb3bf992cf0daba6" + integrity sha512-12+IR2CB2C28MMAw0Ncqwj5QbTcs0nGIhgJzYWzDkb21vWmfNI83KS4f3Ci6GI98WreIfG7o9UXp3C0qbpA8nQ== dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" + minipass "^5.0.0" -"string-width@^1.0.2 || 2 || 3 || 4": +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -897,6 +1003,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -909,27 +1024,20 @@ string_decoder@~0.10.x: resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg== - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" @@ -956,14 +1064,14 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" -tar@^6.0.2: - version "6.1.11" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" - integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== +tar@^6.1.11, tar@^6.1.2: + version "6.1.15" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.15.tgz#c9738b0b98845a3b344d334b8fa3041aaba53a69" + integrity sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A== dependencies: chownr "^2.0.0" fs-minipass "^2.0.0" - minipass "^3.0.0" + minipass "^5.0.0" minizlib "^2.1.1" mkdirp "^1.0.3" yallist "^4.0.0" @@ -976,14 +1084,6 @@ through2@^0.6.3: readable-stream ">=1.0.33-1 <1.1.0-0" xtend ">=4.0.0 <4.1.0-0" -tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== - dependencies: - psl "^1.1.28" - punycode "^2.1.1" - tslib@^1.9.2: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" @@ -996,61 +1096,62 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== +unique-filename@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-3.0.0.tgz#48ba7a5a16849f5080d26c760c86cf5cf05770ea" + integrity sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g== + dependencies: + unique-slug "^4.0.0" -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== +unique-slug@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-4.0.0.tgz#6bae6bb16be91351badd24cdce741f892a6532e3" + integrity sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ== dependencies: - punycode "^2.1.0" + imurmurhash "^0.1.4" -util-deprecate@^1.0.1, util-deprecate@~1.0.1: +util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -uuid@^3.3.2: - version "3.4.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== - -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - webgpu@^0.1.16: version "0.1.16" resolved "https://registry.yarnpkg.com/webgpu/-/webgpu-0.1.16.tgz#dec416373e308181b28864b58c8a914461d7ceee" integrity sha512-KAXn/f8lnL8o4B718zzdfi1l0nEWQpuoWlC1L5WM/svAbeHjShCEI0l5ZcZBEEUm9FF3ZTgRjWk8iwbJfnGKTA== -which-pm-runs@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.1.0.tgz#35ccf7b1a0fce87bd8b92a478c9d045785d3bf35" - integrity sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA== - -which@^2.0.2: +which@^2.0.1, which@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" -wide-align@^1.1.0: +wide-align@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== dependencies: string-width "^1.0.2 || 2 || 3 || 4" +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" diff --git a/src/__tests__/environment.ts b/src/__tests__/environment.ts index e8d6cbe57..a86a389d2 100644 --- a/src/__tests__/environment.ts +++ b/src/__tests__/environment.ts @@ -18,7 +18,10 @@ test('Function params and body identifiers are in different environment', () => const context = mockContext(Chapter.SOURCE_4) context.prelude = null // hide the unneeded prelude const parsed = parse(code, context) - const it = evaluate(parsed as any as Program, context, false, false) + const it = evaluate(parsed as any as Program, context, { + loadTabs: false, + wrapModules: false + }) const stepsToComment = 13 // manually counted magic number for (let i = 0; i < stepsToComment; i += 1) { it.next() diff --git a/src/ec-evaluator/__tests__/__snapshots__/ec-evaluator-errors.ts.snap b/src/ec-evaluator/__tests__/__snapshots__/ec-evaluator-errors.ts.snap index 56e49bab0..8f273c24c 100644 --- a/src/ec-evaluator/__tests__/__snapshots__/ec-evaluator-errors.ts.snap +++ b/src/ec-evaluator/__tests__/__snapshots__/ec-evaluator-errors.ts.snap @@ -792,19 +792,6 @@ Object { } `; -exports[`Importing unknown variables throws UndefinedImport error: expectParsedError 1`] = ` -Object { - "alertResult": Array [], - "code": "import { foo1 } from 'one_module';", - "displayResult": Array [], - "numErrors": 1, - "parsedErrors": "'one_module' does not contain a definition for 'foo1'", - "result": undefined, - "resultStatus": "error", - "visualiseListResult": Array [], -} -`; - exports[`In a block, every going-to-be-defined variable in the block cannot be accessed until it has been defined in the block.: expectParsedError 1`] = ` Object { "alertResult": Array [], diff --git a/src/ec-evaluator/__tests__/ec-evaluator-errors.ts b/src/ec-evaluator/__tests__/ec-evaluator-errors.ts index 32695271e..87a147e45 100644 --- a/src/ec-evaluator/__tests__/ec-evaluator-errors.ts +++ b/src/ec-evaluator/__tests__/ec-evaluator-errors.ts @@ -1,6 +1,4 @@ /* tslint:disable:max-line-length */ -import * as _ from 'lodash' - import { Chapter, Variant } from '../../types' import { stripIndent } from '../../utils/formatters' import { @@ -10,20 +8,6 @@ import { expectResult } from '../../utils/testing' -jest.spyOn(_, 'memoize').mockImplementation(func => func as any) - -const mockXMLHttpRequest = (xhr: Partial = {}) => { - const xhrMock: Partial = { - open: jest.fn(() => {}), - send: jest.fn(() => {}), - status: 200, - responseText: 'Hello World!', - ...xhr - } - jest.spyOn(window, 'XMLHttpRequest').mockImplementationOnce(() => xhrMock as XMLHttpRequest) - return xhrMock -} - const undefinedVariable = stripIndent` im_undefined; ` @@ -1016,35 +1000,3 @@ test('Shadowed variables may not be assigned to until declared in the current sc optionEC3 ).toMatchInlineSnapshot(`"Line 3: Name variable not declared."`) }) - -test('Importing unknown variables throws UndefinedImport error', () => { - // for getModuleFile - mockXMLHttpRequest({ - responseText: `{ - "one_module": { - "tabs": [] - }, - "another_module": { - "tabs": [] - } - }` - }) - - // for bundle body - mockXMLHttpRequest({ - responseText: ` - require => { - return { - foo: () => 'foo', - } - } - ` - }) - - return expectParsedError( - stripIndent` - import { foo1 } from 'one_module'; - `, - optionEC - ).toMatchInlineSnapshot("\"'one_module' does not contain a definition for 'foo1'\"") -}) diff --git a/src/ec-evaluator/__tests__/ec-evaluator.ts b/src/ec-evaluator/__tests__/ec-evaluator.ts index 66ab16ea0..c4c7cfe77 100644 --- a/src/ec-evaluator/__tests__/ec-evaluator.ts +++ b/src/ec-evaluator/__tests__/ec-evaluator.ts @@ -2,22 +2,8 @@ import { Chapter, Variant } from '../../types' import { stripIndent } from '../../utils/formatters' import { expectResult } from '../../utils/testing' -// jest.mock('lodash', () => ({ -// ...jest.requireActual('lodash'), -// memoize: jest.fn(func => func) -// })) - -const mockXMLHttpRequest = (xhr: Partial = {}) => { - const xhrMock: Partial = { - open: jest.fn(() => {}), - send: jest.fn(() => {}), - status: 200, - responseText: 'Hello World!', - ...xhr - } - jest.spyOn(window, 'XMLHttpRequest').mockImplementationOnce(() => xhrMock as XMLHttpRequest) - return xhrMock -} +jest.mock('../../modules/moduleLoaderAsync') +jest.mock('../../modules/moduleLoader') const optionEC = { variant: Variant.EXPLICIT_CONTROL } const optionEC3 = { chapter: Chapter.SOURCE_3, variant: Variant.EXPLICIT_CONTROL } @@ -316,29 +302,6 @@ test('streams can be created and functions with no return statements are still e }) test('Imports are properly handled', () => { - // for getModuleFile - mockXMLHttpRequest({ - responseText: `{ - "one_module": { - "tabs": [] - }, - "another_module": { - "tabs": [] - } - }` - }) - - // for bundle body - mockXMLHttpRequest({ - responseText: ` - require => { - return { - foo: () => 'foo', - } - } - ` - }) - return expectResult( stripIndent` import { foo } from 'one_module'; diff --git a/src/ec-evaluator/interpreter.ts b/src/ec-evaluator/interpreter.ts index e10b0d7ac..472dbda2f 100644 --- a/src/ec-evaluator/interpreter.ts +++ b/src/ec-evaluator/interpreter.ts @@ -6,20 +6,23 @@ */ /* tslint:disable:max-classes-per-file */ -import * as es from 'estree' -import { partition, uniqueId } from 'lodash' +import { uniqueId } from 'lodash' import { IOptions } from '..' import { UNKNOWN_LOCATION } from '../constants' import * as errors from '../errors/errors' import { RuntimeSourceError } from '../errors/runtimeSourceError' import Closure from '../interpreter/closure' -import { UndefinedImportError } from '../modules/errors' -import { loadModuleBundle, loadModuleTabs } from '../modules/moduleLoader' -import { ModuleFunctions } from '../modules/moduleTypes' +import { loadModuleBundle } from '../modules/moduleLoader' +import type { ImportTransformOptions } from '../modules/moduleTypes' +import { initModuleContext } from '../modules/utils' import { checkEditorBreakpoints } from '../stdlib/inspector' -import { Context, ContiguousArrayElements, Result, Value } from '../types' -import * as ast from '../utils/astCreator' +import type { Context, ContiguousArrayElements, Result, Value } from '../types' +import { arrayMapFrom } from '../utils/arrayMap' +import assert from '../utils/assert' +import * as ast from '../utils/ast/astCreator' +import { isImportDeclaration } from '../utils/ast/typeGuards' +import type * as es from '../utils/ast/types' import { evaluateBinaryExpression, evaluateUnaryExpression } from '../utils/operators' import * as rttc from '../utils/rttc' import * as instr from './instrCreator' @@ -129,7 +132,11 @@ export class Stash extends Stack { * @param context The context to evaluate the program in. * @returns The result of running the ECE machine. */ -export function evaluate(program: es.Program, context: Context, options: IOptions): Value { +export async function evaluate( + program: es.Program, + context: Context, + options: IOptions +): Promise { try { context.runtime.isRunning = true context.runtime.agenda = new Agenda(program) @@ -172,47 +179,77 @@ export function resumeEvaluate(context: Context) { function evaluateImports( program: es.Program, context: Context, - loadTabs: boolean, - checkImports: boolean + { loadTabs, wrapModules }: ImportTransformOptions ) { - const [importNodes] = partition(program.body, ({ type }) => type === 'ImportDeclaration') as [ - es.ImportDeclaration[], - es.Statement[] - ] - const moduleFunctions: Record = {} + const importNodes = program.body.filter(isImportDeclaration) try { - for (const node of importNodes) { - const moduleName = node.source.value - if (typeof moduleName !== 'string') { - throw new Error(`ImportDeclarations should have string sources, got ${moduleName}`) - } + const importNodeMap = arrayMapFrom( + importNodes.map(node => { + const moduleName = node.source.value + assert( + typeof moduleName === 'string', + `ImportDeclarations should have string sources, got ${moduleName}` + ) + return [moduleName, node] + }) + ) - if (!(moduleName in moduleFunctions)) { - context.moduleContexts[moduleName] = { - state: null, - tabs: loadTabs ? loadModuleTabs(moduleName, node) : null + const environment = currentEnvironment(context) + importNodeMap.entries().forEach(([moduleName, nodes]) => { + initModuleContext(moduleName, context, loadTabs, nodes[0]) + const functions = loadModuleBundle(moduleName, context, wrapModules, nodes[0]) + for (const node of nodes) { + for (const spec of node.specifiers) { + let importedName: string + switch (spec.type) { + case 'ImportSpecifier': { + importedName = spec.imported.name + break + } + case 'ImportDefaultSpecifier': { + importedName = 'default' + break + } + case 'ImportNamespaceSpecifier': { + throw new Error('Namespace imports are not supported!') + } + } + + declareIdentifier(context, spec.local.name, node, environment) + defineVariable(context, spec.local.name, functions[importedName], true, node) } - moduleFunctions[moduleName] = loadModuleBundle(moduleName, context, node) } + }) - const functions = moduleFunctions[moduleName] - const environment = currentEnvironment(context) - for (const spec of node.specifiers) { - if (spec.type !== 'ImportSpecifier') { - throw new Error(`Only ImportSpecifiers are supported, got: ${spec.type}`) - } - - if (checkImports && !(spec.imported.name in functions)) { - throw new UndefinedImportError(spec.imported.name, moduleName, node) - } - - declareIdentifier(context, spec.local.name, node, environment) - defineVariable(context, spec.local.name, functions[spec.imported.name], true, node) - } - } + // await importNodeMap.forEachAsync(async (moduleName, nodes) => { + // await initModuleContextAsync(moduleName, context, loadTabs, nodes[0]) + // const functions = await loadModuleBundleAsync(moduleName, context, wrapModules, nodes[0]) + + // for (const node of nodes) { + // for (const spec of node.specifiers) { + // let importedName: string + // switch (spec.type) { + // case 'ImportSpecifier': { + // importedName = spec.imported.name + // break + // } + // case 'ImportDefaultSpecifier': { + // importedName = 'default' + // break + // } + // case 'ImportNamespaceSpecifier': { + // throw new Error('Namespace imports are not supported!') + // } + // } + + // declareIdentifier(context, spec.local.name, node, environment) + // defineVariable(context, spec.local.name, functions[importedName], true, node) + // } + // } + // }) } catch (error) { - // console.log(error) + // console.error(error) handleRuntimeError(context, error) } } @@ -224,16 +261,15 @@ function evaluateImports( * @param value The value of ec evaluating the program. * @returns The corresponding promise. */ -export function ECEResultPromise(context: Context, value: Value): Promise { - return new Promise((resolve, reject) => { - if (value instanceof ECEBreak) { - resolve({ status: 'suspended-ec-eval', context }) - } else if (value instanceof ECError) { - resolve({ status: 'error' }) - } else { - resolve({ status: 'finished', context, value }) - } - }) +export async function ECEResultPromise(context: Context, promise: Promise): Promise { + const value = await promise + if (value instanceof ECEBreak) { + return { status: 'suspended-ec-eval', context } + } else if (value instanceof ECError) { + return { status: 'error' } + } else { + return { status: 'finished', context, value } + } } /** @@ -335,7 +371,10 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { if (hasDeclarations(command) || hasImportDeclarations(command)) { const environment = createBlockEnvironment(context, 'programEnvironment') pushEnvironment(context, environment) - evaluateImports(command as unknown as es.Program, context, true, true) + evaluateImports(command as unknown as es.Program, context, { + loadTabs: true, + wrapModules: true + }) declareFunctionsAndVariables(context, command, environment) } diff --git a/src/ec-evaluator/utils.ts b/src/ec-evaluator/utils.ts index 44802cbdd..fd1eb3c96 100644 --- a/src/ec-evaluator/utils.ts +++ b/src/ec-evaluator/utils.ts @@ -6,7 +6,7 @@ import * as errors from '../errors/errors' import { RuntimeSourceError } from '../errors/runtimeSourceError' import Closure from '../interpreter/closure' import { Environment, Frame, Value } from '../types' -import * as ast from '../utils/astCreator' +import * as ast from '../utils/ast/astCreator' import * as instr from './instrCreator' import { Agenda } from './interpreter' import { AgendaItem, AppInstr, AssmtInstr, Instr, InstrType } from './types' diff --git a/src/editors/ace/modes/source.ts b/src/editors/ace/modes/source.ts index 92446a920..c507e581e 100644 --- a/src/editors/ace/modes/source.ts +++ b/src/editors/ace/modes/source.ts @@ -861,7 +861,7 @@ export function ModeSelector( // @ts-ignore this.$id = 'ace/mode/source' + name - }.call(Mode.prototype)) + }).call(Mode.prototype) exports.Mode = Mode } diff --git a/src/errors/localImportErrors.ts b/src/errors/localImportErrors.ts deleted file mode 100644 index 3d4a14b74..000000000 --- a/src/errors/localImportErrors.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { UNKNOWN_LOCATION } from '../constants' -import { nonAlphanumericCharEncoding } from '../localImports/filePaths' -import { ErrorSeverity, ErrorType, SourceError } from '../types' - -export abstract class InvalidFilePathError implements SourceError { - public type = ErrorType.TYPE - public severity = ErrorSeverity.ERROR - public location = UNKNOWN_LOCATION - - constructor(public filePath: string) {} - - abstract explain(): string - - abstract elaborate(): string -} - -export class IllegalCharInFilePathError extends InvalidFilePathError { - public explain() { - const validNonAlphanumericChars = Object.keys(nonAlphanumericCharEncoding) - .map(char => `'${char}'`) - .join(', ') - return `File path '${this.filePath}' must only contain alphanumeric chars and/or ${validNonAlphanumericChars}.` - } - - public elaborate() { - return 'Rename the offending file path to only use valid chars.' - } -} - -export class ConsecutiveSlashesInFilePathError extends InvalidFilePathError { - public explain() { - return `File path '${this.filePath}' cannot contain consecutive slashes '//'.` - } - - public elaborate() { - return 'Remove consecutive slashes from the offending file path.' - } -} - -export class CannotFindModuleError implements SourceError { - public type = ErrorType.TYPE - public severity = ErrorSeverity.ERROR - public location = UNKNOWN_LOCATION - - constructor(public moduleFilePath: string) {} - - public explain() { - return `Cannot find module '${this.moduleFilePath}'.` - } - - public elaborate() { - return 'Check that the module file path resolves to an existing file.' - } -} - -export class CircularImportError implements SourceError { - public type = ErrorType.TYPE - public severity = ErrorSeverity.ERROR - public location = UNKNOWN_LOCATION - - constructor(public filePathsInCycle: string[]) {} - - public explain() { - // We need to reverse the file paths in the cycle so that the - // semantics of "'/a.js' -> '/b.js'" is "'/a.js' imports '/b.js'". - const formattedCycle = this.filePathsInCycle - .map(filePath => `'${filePath}'`) - .reverse() - .join(' -> ') - return `Circular import detected: ${formattedCycle}.` - } - - public elaborate() { - return 'Break the circular import cycle by removing imports from any of the offending files.' - } -} diff --git a/src/errors/moduleErrors.ts b/src/errors/moduleErrors.ts deleted file mode 100644 index 683d6089a..000000000 --- a/src/errors/moduleErrors.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* tslint:disable: max-classes-per-file */ -import * as es from 'estree' - -import { RuntimeSourceError } from './runtimeSourceError' - -export class ModuleConnectionError extends RuntimeSourceError { - private static message: string = `Unable to get modules.` - private static elaboration: string = `You should check your Internet connection, and ensure you have used the correct module path.` - constructor(node?: es.Node) { - super(node) - } - - public explain() { - return ModuleConnectionError.message - } - - public elaborate() { - return ModuleConnectionError.elaboration - } -} - -export class ModuleNotFoundError extends RuntimeSourceError { - constructor(public moduleName: string, node?: es.Node) { - super(node) - } - - public explain() { - return `Module "${this.moduleName}" not found.` - } - - public elaborate() { - return ` - You should check your import declarations, and ensure that all are valid modules. - ` - } -} - -export class ModuleInternalError extends RuntimeSourceError { - constructor(public moduleName: string, public error?: any, node?: es.Node) { - super(node) - } - - public explain() { - return `Error(s) occured when executing the module "${this.moduleName}".` - } - - public elaborate() { - return ` - You may need to contact with the author for this module to fix this error. - ` - } -} diff --git a/src/finder.ts b/src/finder.ts index 87b56cab8..cbb000457 100644 --- a/src/finder.ts +++ b/src/finder.ts @@ -18,7 +18,7 @@ import { FullWalkerCallback, recursive, WalkerCallback -} from './utils/walkers' +} from './utils/ast/walkers' // Finds the innermost node that matches the given location export function findIdentifierNode( diff --git a/src/gpu/gpu.ts b/src/gpu/gpu.ts index 73c4f39e2..663c317d3 100644 --- a/src/gpu/gpu.ts +++ b/src/gpu/gpu.ts @@ -1,6 +1,6 @@ import * as es from 'estree' -import * as create from '../utils/astCreator' +import * as create from '../utils/ast/astCreator' import { getIdentifiersInProgram } from '../utils/uniqueIds' import GPUTransformer from './transfomer' diff --git a/src/gpu/transfomer.ts b/src/gpu/transfomer.ts index 8a9d39d2d..e9f664d19 100644 --- a/src/gpu/transfomer.ts +++ b/src/gpu/transfomer.ts @@ -1,7 +1,7 @@ import * as es from 'estree' -import * as create from '../utils/astCreator' -import { ancestor, make, simple } from '../utils/walkers' +import * as create from '../utils/ast/astCreator' +import { ancestor, make, simple } from '../utils/ast/walkers' import GPUBodyVerifier from './verification/bodyVerifier' import GPULoopVerifier from './verification/loopVerifier' diff --git a/src/gpu/verification/bodyVerifier.ts b/src/gpu/verification/bodyVerifier.ts index 706440102..c29abc3c0 100644 --- a/src/gpu/verification/bodyVerifier.ts +++ b/src/gpu/verification/bodyVerifier.ts @@ -1,6 +1,6 @@ import * as es from 'estree' -import { make, simple } from '../../utils/walkers' +import { make, simple } from '../../utils/ast/walkers' /* * GPU Body verifier helps to ensure the body is parallelizable diff --git a/src/index.ts b/src/index.ts index e0f1e8b7a..1aadb312a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,21 +16,23 @@ import { FuncDeclWithInferredTypeAnnotation, ModuleContext, NodeWithInferredType, + RecursivePartial, Result, SourceError, SVMProgram, Variant } from './types' -import { findNodeAt } from './utils/walkers' +import { findNodeAt } from './utils/ast/walkers' import { assemble } from './vm/svml-assembler' import { compileToIns } from './vm/svml-compiler' export { SourceDocumentation } from './editors/ace/docTooltip' -import * as es from 'estree' +import type * as es from 'estree' import { ECEResultPromise, resumeEvaluate } from './ec-evaluator/interpreter' -import { CannotFindModuleError } from './errors/localImportErrors' -import { validateFilePath } from './localImports/filePaths' -import preprocessFileImports from './localImports/preprocessor' +import { ModuleNotFoundError } from './modules/errors' +import type { ImportOptions } from './modules/moduleTypes' +import preprocessFileImports from './modules/preprocessor' +import { validateFilePath } from './modules/preprocessor/filePaths' import { getKeywords, getProgramNames, NameDeclaration } from './name-extractor' import { parse } from './parser/parser' import { decodeError, decodeValue } from './parser/scheme' @@ -55,6 +57,16 @@ export interface IOptions { useSubst: boolean isPrelude: boolean throwInfiniteLoops: boolean + + importOptions: ImportOptions + + /** Set to true to console log the transpiler's transpiled code */ + logTranspilerOutput: boolean + + /** Set to true to console log the preprocessor's output */ + logPreprocessorOutput: boolean + + /** Number of steps to evaluate (when using the ec-evaluator) */ envSteps: number } @@ -193,7 +205,7 @@ export async function getNames( } const cursorLoc: es.Position = { line, column: col } - const [progNames, displaySuggestions] = getProgramNames(program, comments, cursorLoc) + const [progNames, displaySuggestions] = await getProgramNames(program, comments, cursorLoc) const keywords = getKeywords(program, cursorLoc, context) return [progNames.concat(keywords), displaySuggestions] } @@ -303,7 +315,7 @@ export function getTypeInformation( export async function runInContext( code: string, context: Context, - options: Partial = {} + options: RecursivePartial = {} ): Promise { const defaultFilePath = '/default.js' const files: Partial> = {} @@ -315,7 +327,7 @@ export async function runFilesInContext( files: Partial>, entrypointFilePath: string, context: Context, - options: Partial = {} + options: RecursivePartial = {} ): Promise { for (const filePath in files) { const filePathError = validateFilePath(filePath) @@ -327,12 +339,12 @@ export async function runFilesInContext( const code = files[entrypointFilePath] if (code === undefined) { - context.errors.push(new CannotFindModuleError(entrypointFilePath)) + context.errors.push(new ModuleNotFoundError(entrypointFilePath)) return resolvedErrorPromise } if ( - context.chapter === Chapter.FULL_JS || + // context.chapter === Chapter.FULL_JS || context.chapter === Chapter.FULL_TS || context.chapter === Chapter.PYTHON_1 ) { @@ -399,19 +411,19 @@ export function compile( code: string, context: Context, vmInternalFunctions?: string[] -): SVMProgram | undefined { +): Promise { const defaultFilePath = '/default.js' const files: Partial> = {} files[defaultFilePath] = code return compileFiles(files, defaultFilePath, context, vmInternalFunctions) } -export function compileFiles( +export async function compileFiles( files: Partial>, entrypointFilePath: string, context: Context, vmInternalFunctions?: string[] -): SVMProgram | undefined { +): Promise { for (const filePath in files) { const filePathError = validateFilePath(filePath) if (filePathError !== null) { @@ -422,11 +434,11 @@ export function compileFiles( const entrypointCode = files[entrypointFilePath] if (entrypointCode === undefined) { - context.errors.push(new CannotFindModuleError(entrypointFilePath)) + context.errors.push(new ModuleNotFoundError(entrypointFilePath)) return undefined } - const preprocessedProgram = preprocessFileImports(files, entrypointFilePath, context) + const preprocessedProgram = await preprocessFileImports(files, entrypointFilePath, context) if (!preprocessedProgram) { return undefined } diff --git a/src/infiniteLoops/__tests__/instrument.ts b/src/infiniteLoops/__tests__/instrument.ts index f4f69c924..f2c92a10a 100644 --- a/src/infiniteLoops/__tests__/instrument.ts +++ b/src/infiniteLoops/__tests__/instrument.ts @@ -37,7 +37,11 @@ function mockFunctionsAndState() { * Returns the value saved in the code using the builtin 'output'. * e.g. runWithMock('output(2)') --> 2 */ -function runWithMock(main: string, codeHistory?: string[], builtins: Map = new Map()) { +async function runWithMock( + main: string, + codeHistory?: string[], + builtins: Map = new Map() +) { let output = undefined builtins.set('output', (x: any) => (output = x)) builtins.set('undefined', undefined) @@ -53,7 +57,7 @@ function runWithMock(main: string, codeHistory?: string[], builtins: Map { const main = 'output(2);' - expect(runWithMock(main, [])).toBe(2) + expect(runWithMock(main, [])).resolves.toBe(2) }) test('binary and unary expressions work', () => { - expect(runWithMock('output(1+1);', [])).toBe(2) - expect(runWithMock('output(!true);', [])).toBe(false) + expect(runWithMock('output(1+1);', [])).resolves.toBe(2) + expect(runWithMock('output(!true);', [])).resolves.toBe(false) }) test('assignment works as expected', () => { @@ -75,13 +79,13 @@ test('assignment works as expected', () => { let a = []; a[0] = 3; output(x+a[0]);` - expect(runWithMock(main)).toBe(5) + expect(runWithMock(main)).resolves.toBe(5) }) test('globals from old code accessible', () => { const main = 'output(z+1);' const prev = ['const z = w+1;', 'let w = 10;'] - expect(runWithMock(main, prev)).toBe(12) + expect(runWithMock(main, prev)).resolves.toBe(12) }) test('functions run as expected', () => { @@ -89,7 +93,7 @@ test('functions run as expected', () => { return x===0?x:f(x-1,y)+y; } output(f(5,2));` - expect(runWithMock(main)).toBe(10) + expect(runWithMock(main)).resolves.toBe(10) }) test('nested functions run as expected', () => { @@ -100,7 +104,7 @@ test('nested functions run as expected', () => { return x===0?x:f(x-1,y)+y; } output(f(5,2));` - expect(runWithMock(main)).toBe(2) + expect(runWithMock(main)).resolves.toBe(2) }) test('higher order functions run as expected', () => { @@ -108,14 +112,14 @@ test('higher order functions run as expected', () => { return f(x+1); } output(run(x=>x+1, 1));` - expect(runWithMock(main)).toBe(3) + expect(runWithMock(main)).resolves.toBe(3) }) test('loops run as expected', () => { const main = `let w = 0; for (let i = w; i < 10; i=i+1) {w = i;} output(w);` - expect(runWithMock(main)).toBe(9) + expect(runWithMock(main)).resolves.toBe(9) }) test('nested loops run as expected', () => { @@ -126,13 +130,13 @@ test('nested loops run as expected', () => { } } output(w);` - expect(runWithMock(main)).toBe(100) + expect(runWithMock(main)).resolves.toBe(100) }) test('multidimentional arrays work', () => { const main = `const x = [[1],[2]]; output(x[1] === undefined? undefined: x[1][0]);` - expect(runWithMock(main)).toBe(2) + expect(runWithMock(main)).resolves.toBe(2) }) test('if statements work as expected', () => { @@ -141,7 +145,7 @@ test('if statements work as expected', () => { x = x + 1; } else {} output(x);` - expect(runWithMock(main)).toBe(2) + expect(runWithMock(main)).resolves.toBe(2) }) test('combination of loops and functions run as expected', () => { @@ -158,5 +162,5 @@ test('combination of loops and functions run as expected', () => { w = minus(w,1); } output(z);` - expect(runWithMock(main)).toBe(100) + expect(runWithMock(main)).resolves.toBe(100) }) diff --git a/src/infiniteLoops/__tests__/runtime.ts b/src/infiniteLoops/__tests__/runtime.ts index 4eb48dae8..7b076d9d4 100644 --- a/src/infiniteLoops/__tests__/runtime.ts +++ b/src/infiniteLoops/__tests__/runtime.ts @@ -1,42 +1,20 @@ import * as es from 'estree' +import type { MockedFunction } from 'jest-mock' import { runInContext } from '../..' import createContext from '../../createContext' import { mockContext } from '../../mocks/context' -import * as moduleLoader from '../../modules/moduleLoader' +import * as moduleLoader from '../../modules/moduleLoaderAsync' import { parse } from '../../parser/parser' import { Chapter, Variant } from '../../types' -import { stripIndent } from '../../utils/formatters' import { getInfiniteLoopData, InfiniteLoopError, InfiniteLoopErrorType } from '../errors' import { testForInfiniteLoop } from '../runtime' -jest.spyOn(moduleLoader, 'memoizedGetModuleFile').mockImplementationOnce(() => { - return stripIndent` - require => { - 'use strict'; - var exports = {}; - function repeat(func, n) { - return n === 0 ? function (x) { - return x; - } : function (x) { - return func(repeat(func, n - 1)(x)); - }; - } - function twice(func) { - return repeat(func, 2); - } - function thrice(func) { - return repeat(func, 3); - } - exports.repeat = repeat; - exports.thrice = thrice; - exports.twice = twice; - Object.defineProperty(exports, '__esModule', { - value: true - }); - return exports; - } - ` +jest.mock('../../modules/moduleLoaderAsync') +jest.spyOn(moduleLoader, 'memoizedGetModuleBundleAsync') + +beforeAll(() => { + global.fetch = jest.fn() }) test('works in runInContext when throwInfiniteLoops is true', async () => { @@ -45,6 +23,7 @@ test('works in runInContext when throwInfiniteLoops is true', async () => { } fib(100000);` const context = mockContext(Chapter.SOURCE_4) + await runInContext(code, context, { throwInfiniteLoops: true }) const lastError = context.errors[context.errors.length - 1] expect(lastError instanceof InfiniteLoopError).toBe(true) @@ -77,84 +56,84 @@ const testForInfiniteLoopWithCode = (code: string, previousPrograms: es.Program[ return testForInfiniteLoop(program, previousPrograms) } -test('non-infinite recursion not detected', () => { +test('non-infinite recursion not detected', async () => { const code = `function fib(x) { return x<=1?x:fib(x-1) + fib(x-2); } fib(100000); ` - const result = testForInfiniteLoopWithCode(code, []) + const result = await testForInfiniteLoopWithCode(code, []) expect(result).toBeUndefined() }) -test('non-infinite loop not detected', () => { +test('non-infinite loop not detected', async () => { const code = `for(let i = 0;i<2000;i=i+1){i+1;} let j = 0; while(j<2000) {j=j+1;} ` - const result = testForInfiniteLoopWithCode(code, []) + const result = await testForInfiniteLoopWithCode(code, []) expect(result).toBeUndefined() }) -test('no base case function detected', () => { +test('no base case function detected', async () => { const code = `function fib(x) { return fib(x-1) + fib(x-2); } fib(100000); ` - const result = testForInfiniteLoopWithCode(code, []) + const result = await testForInfiniteLoopWithCode(code, []) expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.NoBaseCase) expect(result?.streamMode).toBe(false) }) -test('no base case loop detected', () => { +test('no base case loop detected', async () => { const code = `for(let i = 0;true;i=i+1){i+1;} ` - const result = testForInfiniteLoopWithCode(code, []) + const result = await testForInfiniteLoopWithCode(code, []) expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.NoBaseCase) expect(result?.streamMode).toBe(false) }) -test('no variables changing function detected', () => { +test('no variables changing function detected', async () => { const code = `let x = 1; function f() { return x===0?x:f(); } f(); ` - const result = testForInfiniteLoopWithCode(code, []) + const result = await testForInfiniteLoopWithCode(code, []) expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.Cycle) expect(result?.streamMode).toBe(false) expect(result?.explain()).toContain('None of the variables are being updated.') }) -test('no state change function detected', () => { +test('no state change function detected', async () => { const code = `let x = 1; function f() { return x===0?x:f(); } f(); ` - const result = testForInfiniteLoopWithCode(code, []) + const result = await testForInfiniteLoopWithCode(code, []) expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.Cycle) expect(result?.streamMode).toBe(false) expect(result?.explain()).toContain('None of the variables are being updated.') }) -test('infinite cycle detected', () => { +test('infinite cycle detected', async () => { const code = `function f(x) { return x[0] === 1? x : f(x); } f([2,3,4]); ` - const result = testForInfiniteLoopWithCode(code, []) + const result = await testForInfiniteLoopWithCode(code, []) expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.Cycle) expect(result?.streamMode).toBe(false) expect(result?.explain()).toContain('cycle') expect(result?.explain()).toContain('[2,3,4]') }) -test('infinite data structures detected', () => { +test('infinite data structures detected', async () => { const code = `function f(x) { return is_null(x)? x : f(tail(x)); } @@ -162,32 +141,32 @@ test('infinite data structures detected', () => { set_tail(tail(tail(circ)), circ); f(circ); ` - const result = testForInfiniteLoopWithCode(code, []) + const result = await testForInfiniteLoopWithCode(code, []) expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.Cycle) expect(result?.streamMode).toBe(false) expect(result?.explain()).toContain('cycle') expect(result?.explain()).toContain('(CIRCULAR)') }) -test('functions using SMT work', () => { +test('functions using SMT work', async () => { const code = `function f(x) { return x===0? x: f(x+1); } f(1); ` - const result = testForInfiniteLoopWithCode(code, []) + const result = await testForInfiniteLoopWithCode(code, []) expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.FromSmt) expect(result?.streamMode).toBe(false) }) -test('detect forcing infinite streams', () => { +test('detect forcing infinite streams', async () => { const code = `stream_to_list(integers_from(0));` - const result = testForInfiniteLoopWithCode(code, []) + const result = await testForInfiniteLoopWithCode(code, []) expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.NoBaseCase) expect(result?.streamMode).toBe(true) }) -test('detect mutual recursion', () => { +test('detect mutual recursion', async () => { const code = `function e(x){ return x===0?1:1-o(x-1); } @@ -195,23 +174,23 @@ test('detect mutual recursion', () => { return x===1?0:1-e(x-1); } e(9);` - const result = testForInfiniteLoopWithCode(code, []) + const result = await testForInfiniteLoopWithCode(code, []) expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.FromSmt) expect(result?.streamMode).toBe(false) }) -test('functions passed as arguments not checked', () => { +test('functions passed as arguments not checked', async () => { // if they are checked -> this will throw no base case const code = `const twice = f => x => f(f(x)); const thrice = f => x => f(f(f(x))); const add = x => x + 1; (thrice)(twice(twice))(twice(add))(0);` - const result = testForInfiniteLoopWithCode(code, []) + const result = await testForInfiniteLoopWithCode(code, []) expect(result).toBeUndefined() }) -test('detect complicated cycle example', () => { +test('detect complicated cycle example', async () => { const code = `function permutations(s) { return is_null(s) ? list(null) @@ -230,12 +209,12 @@ test('detect complicated cycle example', () => { remove_duplicate(list(list(1,2,3), list(1,2,3))); ` - const result = testForInfiniteLoopWithCode(code, []) + const result = await testForInfiniteLoopWithCode(code, []) expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.Cycle) expect(result?.streamMode).toBe(false) }) -test('detect complicated cycle example 2', () => { +test('detect complicated cycle example 2', async () => { const code = `function make_big_int_from_number(num){ let output = num; while(output !== 0){ @@ -246,12 +225,12 @@ test('detect complicated cycle example 2', () => { } make_big_int_from_number(1234); ` - const result = testForInfiniteLoopWithCode(code, []) + const result = await testForInfiniteLoopWithCode(code, []) expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.Cycle) expect(result?.streamMode).toBe(false) }) -test('detect complicated fromSMT example 2', () => { +test('detect complicated fromSMT example 2', async () => { const code = `function fast_power(b,n){ if (n % 2 === 0){ return b* fast_power(b, n-2); @@ -261,47 +240,54 @@ test('detect complicated fromSMT example 2', () => { } fast_power(2,3);` - const result = testForInfiniteLoopWithCode(code, []) + const result = await testForInfiniteLoopWithCode(code, []) expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.FromSmt) expect(result?.streamMode).toBe(false) }) -test('detect complicated stream example', () => { +test('detect complicated stream example', async () => { const code = `function up(a, b) { return (a > b) ? up(1, 1 + b) : pair(a, () => stream_reverse(up(a + 1, b))); } eval_stream(up(1,1), 22);` - const result = testForInfiniteLoopWithCode(code, []) + const result = await testForInfiniteLoopWithCode(code, []) expect(result).toBeDefined() expect(result?.streamMode).toBe(true) }) -test('math functions are disabled in smt solver', () => { +test('math functions are disabled in smt solver', async () => { const code = ` function f(x) { return x===0? x: f(math_floor(x+1)); } f(1);` - const result = testForInfiniteLoopWithCode(code, []) + const result = await testForInfiniteLoopWithCode(code, []) expect(result).toBeUndefined() }) -test('cycle detection ignores non deterministic functions', () => { +test('cycle detection ignores non deterministic functions', async () => { const code = ` function f(x) { return x===0?0:f(math_floor(math_random()/2) + 1); } f(1);` - const result = testForInfiniteLoopWithCode(code, []) + const result = await testForInfiniteLoopWithCode(code, []) expect(result).toBeUndefined() }) -test('handle imports properly', () => { +test('handle imports properly', async () => { + const mockedBundleLoader = moduleLoader.memoizedGetModuleBundleAsync as MockedFunction< + typeof moduleLoader.memoizedGetModuleBundleAsync + > + mockedBundleLoader.mockResolvedValueOnce(`require => ({ + thrice: f => x => f(f(f(x))) + })`) + const code = `import {thrice} from "repeat"; function f(x) { return is_number(x) ? f(x) : 42; } display(f(thrice(x=>x+1)(0)));` - const result = testForInfiniteLoopWithCode(code, []) + const result = await testForInfiniteLoopWithCode(code, []) expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.Cycle) }) diff --git a/src/infiniteLoops/detect.ts b/src/infiniteLoops/detect.ts index af9b98416..a29de175f 100644 --- a/src/infiniteLoops/detect.ts +++ b/src/infiniteLoops/detect.ts @@ -1,7 +1,7 @@ import { generate } from 'astring' import * as es from 'estree' -import { simple } from '../utils/walkers' +import { simple } from '../utils/ast/walkers' import { InfiniteLoopError, InfiniteLoopErrorType } from './errors' import { getOriginalName } from './instrument' import * as st from './state' diff --git a/src/infiniteLoops/instrument.ts b/src/infiniteLoops/instrument.ts index c8c98d0e8..fe851e97a 100644 --- a/src/infiniteLoops/instrument.ts +++ b/src/infiniteLoops/instrument.ts @@ -2,8 +2,8 @@ import { generate } from 'astring' import * as es from 'estree' import { transformImportDeclarations } from '../transpiler/transpiler' -import * as create from '../utils/astCreator' -import { recursive, simple, WalkerCallback } from '../utils/walkers' +import * as create from '../utils/ast/astCreator' +import { recursive, simple, WalkerCallback } from '../utils/ast/walkers' // transforms AST of program const globalIds = { @@ -574,15 +574,28 @@ function trackLocations(program: es.Program) { }) } -function handleImports(programs: es.Program[]): [string, string[]] { - const [prefixes, imports] = programs.reduce( - ([prefix, moduleNames], program) => { - const [prefixToAdd, importsToAdd, otherNodes] = transformImportDeclarations( +async function handleImports(programs: es.Program[]): Promise<[string, string[]]> { + const results = await Promise.all( + programs.map(async program => { + const [prefix, declNodes, otherNodes] = await transformImportDeclarations( program, - new Set(), - false + new Set(), + null, + { + loadTabs: false, + wrapModules: false + } ) - program.body = (importsToAdd as es.Program['body']).concat(otherNodes) + + program.body = (declNodes as es.Program['body']).concat(otherNodes) + return [prefix, declNodes, otherNodes] as Awaited< + ReturnType + > + }) + ) + + const [prefixes, imports] = results.reduce( + ([prefix, moduleNames], [prefixToAdd, importsToAdd]) => { prefix.push(prefixToAdd) const importedNames = importsToAdd.flatMap(node => @@ -606,11 +619,11 @@ function handleImports(programs: es.Program[]): [string, string[]] { * @param builtins Names of builtin functions. * @returns code with instrumentations. */ -function instrument( +async function instrument( previous: es.Program[], program: es.Program, builtins: Iterable -): string { +): Promise { const { builtinsId, functionsId, stateId } = globalIds const predefined = {} predefined[builtinsId] = builtinsId @@ -618,7 +631,7 @@ function instrument( predefined[stateId] = stateId const innerProgram = { ...program } - const [prefix, moduleNames] = handleImports([program].concat(previous)) + const [prefix, moduleNames] = await handleImports([program].concat(previous)) for (const name of moduleNames) { predefined[name] = name } diff --git a/src/infiniteLoops/runtime.ts b/src/infiniteLoops/runtime.ts index a9b81b95f..d85ec3406 100644 --- a/src/infiniteLoops/runtime.ts +++ b/src/infiniteLoops/runtime.ts @@ -6,7 +6,7 @@ import { getRequireProvider } from '../modules/requireProvider' import { parse } from '../parser/parser' import * as stdList from '../stdlib/list' import { Chapter, Variant } from '../types' -import * as create from '../utils/astCreator' +import * as create from '../utils/ast/astCreator' import { checkForInfiniteLoop } from './detect' import { InfiniteLoopError } from './errors' import { @@ -305,7 +305,10 @@ functions[FunctionNames.evalU] = sym.evaluateHybridUnary * @param previousProgramsStack Any code previously entered in the REPL & parsed into AST. * @returns SourceError if an infinite loop was detected, undefined otherwise. */ -export function testForInfiniteLoop(program: es.Program, previousProgramsStack: es.Program[]) { +export async function testForInfiniteLoop( + program: es.Program, + previousProgramsStack: es.Program[] +) { const context = createContext(Chapter.SOURCE_4, Variant.DEFAULT, undefined, undefined) const prelude = parse(context.prelude as string, context) as es.Program context.prelude = null @@ -313,7 +316,7 @@ export function testForInfiniteLoop(program: es.Program, previousProgramsStack: const newBuiltins = prepareBuiltins(context.nativeStorage.builtins) const { builtinsId, functionsId, stateId } = InfiniteLoopRuntimeObjectNames - const instrumentedCode = instrument(previous, program, newBuiltins.keys()) + const instrumentedCode = await instrument(previous, program, newBuiltins.keys()) const state = new st.State() const sandboxedRun = new Function( @@ -327,7 +330,7 @@ export function testForInfiniteLoop(program: es.Program, previousProgramsStack: ) try { - sandboxedRun(instrumentedCode, functions, state, newBuiltins, getRequireProvider(context)) + await sandboxedRun(instrumentedCode, functions, state, newBuiltins, getRequireProvider(context)) } catch (error) { if (error instanceof InfiniteLoopError) { if (state.lastLocation !== undefined) { diff --git a/src/infiniteLoops/state.ts b/src/infiniteLoops/state.ts index b2ff7fff6..b32918311 100644 --- a/src/infiniteLoops/state.ts +++ b/src/infiniteLoops/state.ts @@ -1,7 +1,7 @@ import { generate } from 'astring' import * as es from 'estree' -import { identifier } from '../utils/astCreator' +import { identifier } from '../utils/ast/astCreator' import * as sym from './symbolic' // Object + functions called during runtime to check for infinite loops diff --git a/src/infiniteLoops/symbolic.ts b/src/infiniteLoops/symbolic.ts index 89e3449cd..07d580b66 100644 --- a/src/infiniteLoops/symbolic.ts +++ b/src/infiniteLoops/symbolic.ts @@ -1,6 +1,6 @@ import * as es from 'estree' -import * as create from '../utils/astCreator' +import * as create from '../utils/ast/astCreator' import { evaluateBinaryExpression, evaluateUnaryExpression } from '../utils/operators' // data structure for symbolic + hybrid values diff --git a/src/interpreter/__tests__/__snapshots__/interpreter-errors.ts.snap b/src/interpreter/__tests__/__snapshots__/interpreter-errors.ts.snap index 4f232772a..f172a5554 100644 --- a/src/interpreter/__tests__/__snapshots__/interpreter-errors.ts.snap +++ b/src/interpreter/__tests__/__snapshots__/interpreter-errors.ts.snap @@ -849,6 +849,19 @@ Object { } `; +exports[`Importing unknown variables throws error: expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": "import { foo1 } from 'one_module';", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Line 1: 'one_module' does not contain a definition for 'foo1'", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + exports[`Nice errors when errors occur inside builtins: expectParsedError 1`] = ` Object { "alertResult": Array [], diff --git a/src/interpreter/__tests__/interpreter-errors.ts b/src/interpreter/__tests__/interpreter-errors.ts index 3319dabaf..615dc5de8 100644 --- a/src/interpreter/__tests__/interpreter-errors.ts +++ b/src/interpreter/__tests__/interpreter-errors.ts @@ -1,7 +1,4 @@ -// import type { FunctionLike, MockedFunction } from 'jest-mock' - /* tslint:disable:max-line-length */ -// import { memoizedGetModuleManifest } from '../../modules/moduleLoader' import { Chapter } from '../../types' import { stripIndent } from '../../utils/formatters' import { @@ -11,26 +8,7 @@ import { expectResult } from '../../utils/testing' -jest.mock('../../modules/moduleLoader', () => ({ - ...jest.requireActual('../../modules/moduleLoader'), - memoizedGetModuleFile: jest.fn().mockReturnValue(`function() { - return { - foo: () => undefined, - bar: () => undefined, - } - }`), - memoizedGetModuleManifest: jest.fn().mockReturnValue({ - one_module: { - tabs: [] - }, - another_module: { - tabs: [] - } - }) -})) - -// const asMock = (func: T) => func as MockedFunction -// const mockedModuleFile = asMock(memoizedGetModuleFile) +jest.mock('../../modules/moduleLoaderAsync') const undefinedVariable = stripIndent` im_undefined; @@ -1145,5 +1123,5 @@ test('Cascading js errors work properly', () => { test('Importing unknown variables throws error', () => { expectParsedError(stripIndent` import { foo1 } from 'one_module'; - `).toMatchInlineSnapshot("'one_module' does not contain definitions for 'foo1'") + `).toMatchInlineSnapshot(`"Line 1: 'one_module' does not contain a definition for 'foo1'"`) }) diff --git a/src/interpreter/closure.ts b/src/interpreter/closure.ts index de9a3fbaa..304666d73 100644 --- a/src/interpreter/closure.ts +++ b/src/interpreter/closure.ts @@ -11,7 +11,7 @@ import { callExpression, identifier, returnStatement -} from '../utils/astCreator' +} from '../utils/ast/astCreator' import { apply } from './interpreter' const closureToJS = (value: Closure, context: Context, klass: string) => { diff --git a/src/interpreter/interpreter-non-det.ts b/src/interpreter/interpreter-non-det.ts index 570566d3f..faf61b0f1 100644 --- a/src/interpreter/interpreter-non-det.ts +++ b/src/interpreter/interpreter-non-det.ts @@ -6,7 +6,7 @@ import { CUT, UNKNOWN_LOCATION } from '../constants' import * as errors from '../errors/errors' import { RuntimeSourceError } from '../errors/runtimeSourceError' import { Context, Environment, Frame, Value } from '../types' -import { conditionalExpression, literal, primitive } from '../utils/astCreator' +import { conditionalExpression, literal, primitive } from '../utils/ast/astCreator' import { evaluateBinaryExpression, evaluateUnaryExpression } from '../utils/operators' import * as rttc from '../utils/rttc' import Closure from './closure' diff --git a/src/interpreter/interpreter.ts b/src/interpreter/interpreter.ts index b459f08a1..1a378b0a3 100644 --- a/src/interpreter/interpreter.ts +++ b/src/interpreter/interpreter.ts @@ -1,18 +1,19 @@ /* tslint:disable:max-classes-per-file */ -import * as es from 'estree' +import type * as es from 'estree' import { isEmpty, uniqueId } from 'lodash' import { UNKNOWN_LOCATION } from '../constants' import { LazyBuiltIn } from '../createContext' import * as errors from '../errors/errors' import { RuntimeSourceError } from '../errors/runtimeSourceError' -import { UndefinedImportError } from '../modules/errors' -import { loadModuleBundle, loadModuleTabs } from '../modules/moduleLoader' -import { ModuleFunctions } from '../modules/moduleTypes' +import { loadModuleBundle } from '../modules/moduleLoader' +import type { ImportTransformOptions, ModuleFunctions } from '../modules/moduleTypes' +import { initModuleContext } from '../modules/utils' import { checkEditorBreakpoints } from '../stdlib/inspector' import { Context, ContiguousArrayElements, Environment, Frame, Value, Variant } from '../types' -import * as create from '../utils/astCreator' -import { conditionalExpression, literal, primitive } from '../utils/astCreator' +import * as create from '../utils/ast/astCreator' +import { conditionalExpression, literal, primitive } from '../utils/ast/astCreator' +import { simple } from '../utils/ast/walkers' import { evaluateBinaryExpression, evaluateUnaryExpression } from '../utils/operators' import * as rttc from '../utils/rttc' import Closure from './closure' @@ -712,8 +713,7 @@ function getNonEmptyEnv(environment: Environment): Environment { export function* evaluateProgram( program: es.Program, context: Context, - checkImports: boolean, - loadTabs: boolean + { loadTabs, wrapModules }: ImportTransformOptions ) { yield* visit(context, program) @@ -739,26 +739,26 @@ export function* evaluateProgram( } if (!(moduleName in moduleFunctions)) { - context.moduleContexts[moduleName] = { - state: null, - tabs: loadTabs ? loadModuleTabs(moduleName, node) : null - } - moduleFunctions[moduleName] = loadModuleBundle(moduleName, context, node) + initModuleContext(moduleName, context, loadTabs, node) + moduleFunctions[moduleName] = loadModuleBundle(moduleName, context, wrapModules, node) } const functions = moduleFunctions[moduleName] for (const spec of node.specifiers) { - if (spec.type !== 'ImportSpecifier') { - throw new Error(`Only Import Specifiers are supported, got ${spec.type}`) - } - - if (checkImports && !(spec.imported.name in functions)) { - throw new UndefinedImportError(spec.imported.name, moduleName, node) - } - declareIdentifier(context, spec.local.name, node) - defineVariable(context, spec.local.name, functions[spec.imported.name], true) + simple(spec, { + ImportSpecifier: () => + defineVariable( + context, + spec.local.name, + functions[(spec as es.ImportSpecifier).imported.name], + true + ), + ImportDefaultSpecifier: () => + defineVariable(context, spec.local.name, functions['default'], true), + ImportNamespaceSpecifier: () => defineVariable(context, spec.local.name, functions, true) + }) } yield* leave(context) } diff --git a/src/lazy/lazy.ts b/src/lazy/lazy.ts index 6dedcf419..a53b89aca 100644 --- a/src/lazy/lazy.ts +++ b/src/lazy/lazy.ts @@ -1,8 +1,8 @@ import * as es from 'estree' -import * as create from '../utils/astCreator' +import * as create from '../utils/ast/astCreator' +import { simple } from '../utils/ast/walkers' import { getIdentifiersInProgram } from '../utils/uniqueIds' -import { simple } from '../utils/walkers' const lazyPrimitives = new Set(['makeLazyFunction', 'wrapLazyCallee', 'forceIt', 'delayIt']) diff --git a/src/localImports/__tests__/preprocessor.ts b/src/localImports/__tests__/preprocessor.ts deleted file mode 100644 index 628d32766..000000000 --- a/src/localImports/__tests__/preprocessor.ts +++ /dev/null @@ -1,412 +0,0 @@ -import es from 'estree' - -import { parseError } from '../../index' -import { mockContext } from '../../mocks/context' -import { parse } from '../../parser/parser' -import { accessExportFunctionName, defaultExportLookupName } from '../../stdlib/localImport.prelude' -import { Chapter } from '../../types' -import preprocessFileImports, { getImportedLocalModulePaths } from '../preprocessor' -import { parseCodeError, stripLocationInfo } from './utils' - -describe('getImportedLocalModulePaths', () => { - let context = mockContext(Chapter.LIBRARY_PARSER) - - beforeEach(() => { - context = mockContext(Chapter.LIBRARY_PARSER) - }) - - const assertCorrectModulePathsAreReturned = ( - code: string, - baseFilePath: string, - expectedModulePaths: string[] - ): void => { - const program = parse(code, context) - if (program === null) { - throw parseCodeError - } - expect(getImportedLocalModulePaths(program, baseFilePath)).toEqual(new Set(expectedModulePaths)) - } - - it('throws an error if the current file path is not absolute', () => { - const code = '' - const program = parse(code, context) - if (program === null) { - throw parseCodeError - } - expect(() => getImportedLocalModulePaths(program, 'a.js')).toThrowError( - "Current file path 'a.js' is not absolute." - ) - }) - - it('returns local (relative) module imports', () => { - const code = ` - import { x } from "./dir2/b.js"; - import { y } from "../dir3/c.js"; - ` - assertCorrectModulePathsAreReturned(code, '/dir/a.js', ['/dir/dir2/b.js', '/dir3/c.js']) - }) - - it('returns local (absolute) module imports', () => { - const code = ` - import { x } from "/dir/dir2/b.js"; - import { y } from "/dir3/c.js"; - ` - assertCorrectModulePathsAreReturned(code, '/dir/a.js', ['/dir/dir2/b.js', '/dir3/c.js']) - }) - - it('does not return Source module imports', () => { - const code = ` - import { x } from "rune"; - import { y } from "sound"; - ` - assertCorrectModulePathsAreReturned(code, '/dir/a.js', []) - }) - - it('gracefully handles overly long sequences of double dots (..)', () => { - const code = `import { x } from "../../../../../../../../../b.js"; - ` - assertCorrectModulePathsAreReturned(code, '/dir/a.js', ['/b.js']) - }) - - it('returns unique module paths', () => { - const code = ` - import { a } from "./b.js"; - import { b } from "./b.js"; - import { c } from "./c.js"; - import { d } from "./c.js"; - ` - assertCorrectModulePathsAreReturned(code, '/dir/a.js', ['/dir/b.js', '/dir/c.js']) - }) -}) - -describe('preprocessFileImports', () => { - let actualContext = mockContext(Chapter.LIBRARY_PARSER) - let expectedContext = mockContext(Chapter.LIBRARY_PARSER) - - beforeEach(() => { - actualContext = mockContext(Chapter.LIBRARY_PARSER) - expectedContext = mockContext(Chapter.LIBRARY_PARSER) - }) - - const assertASTsAreEquivalent = ( - actualProgram: es.Program | undefined, - expectedCode: string - ): void => { - if (actualProgram === undefined) { - throw parseCodeError - } - - const expectedProgram = parse(expectedCode, expectedContext) - if (expectedProgram === null) { - throw parseCodeError - } - - expect(stripLocationInfo(actualProgram)).toEqual(stripLocationInfo(expectedProgram)) - } - - it('returns undefined & adds CannotFindModuleError to context if the entrypoint file does not exist', () => { - const files: Record = { - '/a.js': '1 + 2;' - } - const actualProgram = preprocessFileImports(files, '/non-existent-file.js', actualContext) - expect(actualProgram).toBeUndefined() - expect(parseError(actualContext.errors)).toMatchInlineSnapshot( - `"Cannot find module '/non-existent-file.js'."` - ) - }) - - it('returns undefined & adds CannotFindModuleError to context if an imported file does not exist', () => { - const files: Record = { - '/a.js': `import { x } from './non-existent-file.js';` - } - const actualProgram = preprocessFileImports(files, '/a.js', actualContext) - expect(actualProgram).toBeUndefined() - expect(parseError(actualContext.errors)).toMatchInlineSnapshot( - `"Cannot find module '/non-existent-file.js'."` - ) - }) - - it('returns the same AST if the entrypoint file does not contain import/export statements', () => { - const files: Record = { - '/a.js': ` - function square(x) { - return x * x; - } - square(5); - ` - } - const expectedCode = files['/a.js'] - const actualProgram = preprocessFileImports(files, '/a.js', actualContext) - assertASTsAreEquivalent(actualProgram, expectedCode) - }) - - it('removes all export-related AST nodes', () => { - const files: Record = { - '/a.js': ` - export const x = 42; - export let y = 53; - export function square(x) { - return x * x; - } - export const id = x => x; - export default function cube(x) { - return x * x * x; - } - ` - } - const expectedCode = ` - const x = 42; - let y = 53; - function square(x) { - return x * x; - } - const id = x => x; - function cube(x) { - return x * x * x; - } - ` - const actualProgram = preprocessFileImports(files, '/a.js', actualContext) - assertASTsAreEquivalent(actualProgram, expectedCode) - }) - - it('ignores Source module imports & removes all non-Source module import-related AST nodes in the preprocessed program', () => { - const files: Record = { - '/a.js': ` - import d, { a, b, c } from "source-module"; - import w, { x, y, z } from "./not-source-module.js"; - `, - '/not-source-module.js': ` - export const x = 1; - export const y = 2; - export const z = 3; - export default function square(x) { - return x * x; - } - ` - } - const expectedCode = ` - import { a, b, c } from "source-module"; - - function __$not$$dash$$source$$dash$$module$$dot$$js__() { - const x = 1; - const y = 2; - const z = 3; - function square(x) { - return x * x; - } - - return pair(square, list(pair("x", x), pair("y", y), pair("z", z))); - } - - const ___$not$$dash$$source$$dash$$module$$dot$$js___ = __$not$$dash$$source$$dash$$module$$dot$$js__(); - - const w = ${accessExportFunctionName}(___$not$$dash$$source$$dash$$module$$dot$$js___, "${defaultExportLookupName}"); - const x = ${accessExportFunctionName}(___$not$$dash$$source$$dash$$module$$dot$$js___, "x"); - const y = ${accessExportFunctionName}(___$not$$dash$$source$$dash$$module$$dot$$js___, "y"); - const z = ${accessExportFunctionName}(___$not$$dash$$source$$dash$$module$$dot$$js___, "z"); - ` - const actualProgram = preprocessFileImports(files, '/a.js', actualContext) - assertASTsAreEquivalent(actualProgram, expectedCode) - }) - - it('collates Source module imports at the start of the top-level environment of the preprocessed program', () => { - const files: Record = { - '/a.js': ` - import { b } from "./b.js"; - import { w, x } from "source-module"; - import { f, g } from "other-source-module"; - - b; - `, - '/b.js': ` - import { square } from "./c.js"; - import { x, y } from "source-module"; - import { h } from "another-source-module"; - - export const b = square(5); - `, - '/c.js': ` - import { x, y, z } from "source-module"; - - export const square = x => x * x; - ` - } - const expectedCode = ` - import { w, x, y, z } from "source-module"; - import { f, g } from "other-source-module"; - import { h } from "another-source-module"; - - function __$b$$dot$$js__(___$c$$dot$$js___) { - const square = ${accessExportFunctionName}(___$c$$dot$$js___, "square"); - - const b = square(5); - - return pair(null, list(pair("b", b))); - } - - function __$c$$dot$$js__() { - const square = x => x * x; - - return pair(null, list(pair("square", square))); - } - - const ___$c$$dot$$js___ = __$c$$dot$$js__(); - const ___$b$$dot$$js___ = __$b$$dot$$js__(___$c$$dot$$js___); - - const b = ${accessExportFunctionName}(___$b$$dot$$js___, "b"); - - b; - ` - const actualProgram = preprocessFileImports(files, '/a.js', actualContext) - assertASTsAreEquivalent(actualProgram, expectedCode) - }) - - it('returns CircularImportError if there are circular imports', () => { - const files: Record = { - '/a.js': ` - import { b } from "./b.js"; - - export const a = 1; - `, - '/b.js': ` - import { c } from "./c.js"; - - export const b = 2; - `, - '/c.js': ` - import { a } from "./a.js"; - - export const c = 3; - ` - } - preprocessFileImports(files, '/a.js', actualContext) - expect(parseError(actualContext.errors)).toMatchInlineSnapshot( - `"Circular import detected: '/a.js' -> '/b.js' -> '/c.js' -> '/a.js'."` - ) - }) - - it('returns CircularImportError if there are circular imports - verbose', () => { - const files: Record = { - '/a.js': ` - import { b } from "./b.js"; - - export const a = 1; - `, - '/b.js': ` - import { c } from "./c.js"; - - export const b = 2; - `, - '/c.js': ` - import { a } from "./a.js"; - - export const c = 3; - ` - } - preprocessFileImports(files, '/a.js', actualContext) - expect(parseError(actualContext.errors, true)).toMatchInlineSnapshot(` - "Circular import detected: '/a.js' -> '/b.js' -> '/c.js' -> '/a.js'. - Break the circular import cycle by removing imports from any of the offending files. - " - `) - }) - - it('returns CircularImportError if there are self-imports', () => { - const files: Record = { - '/a.js': ` - import { y } from "./a.js"; - const x = 1; - export { x as y }; - ` - } - preprocessFileImports(files, '/a.js', actualContext) - expect(parseError(actualContext.errors)).toMatchInlineSnapshot( - `"Circular import detected: '/a.js' -> '/a.js'."` - ) - }) - - it('returns CircularImportError if there are self-imports - verbose', () => { - const files: Record = { - '/a.js': ` - import { y } from "./a.js"; - const x = 1; - export { x as y }; - ` - } - preprocessFileImports(files, '/a.js', actualContext) - expect(parseError(actualContext.errors, true)).toMatchInlineSnapshot(` - "Circular import detected: '/a.js' -> '/a.js'. - Break the circular import cycle by removing imports from any of the offending files. - " - `) - }) - - it('returns a preprocessed program with all imports', () => { - const files: Record = { - '/a.js': ` - import { a as x, b as y } from "./b.js"; - - x + y; - `, - '/b.js': ` - import y, { square } from "./c.js"; - - const a = square(y); - const b = 3; - export { a, b }; - `, - '/c.js': ` - import { mysteryFunction } from "./d.js"; - - const x = mysteryFunction(5); - export function square(x) { - return x * x; - } - export default x; - `, - '/d.js': ` - const addTwo = x => x + 2; - export { addTwo as mysteryFunction }; - ` - } - const expectedCode = ` - function __$b$$dot$$js__(___$c$$dot$$js___) { - const y = ${accessExportFunctionName}(___$c$$dot$$js___, "${defaultExportLookupName}"); - const square = ${accessExportFunctionName}(___$c$$dot$$js___, "square"); - - const a = square(y); - const b = 3; - - return pair(null, list(pair("a", a), pair("b", b))); - } - - function __$c$$dot$$js__(___$d$$dot$$js___) { - const mysteryFunction = ${accessExportFunctionName}(___$d$$dot$$js___, "mysteryFunction"); - - const x = mysteryFunction(5); - function square(x) { - return x * x; - } - - return pair(x, list(pair("square", square))); - } - - function __$d$$dot$$js__() { - const addTwo = x => x + 2; - - return pair(null, list(pair("mysteryFunction", addTwo))); - } - - const ___$d$$dot$$js___ = __$d$$dot$$js__(); - const ___$c$$dot$$js___ = __$c$$dot$$js__(___$d$$dot$$js___); - const ___$b$$dot$$js___ = __$b$$dot$$js__(___$c$$dot$$js___); - - const x = ${accessExportFunctionName}(___$b$$dot$$js___, "a"); - const y = ${accessExportFunctionName}(___$b$$dot$$js___, "b"); - - x + y; - ` - const actualProgram = preprocessFileImports(files, '/a.js', actualContext) - assertASTsAreEquivalent(actualProgram, expectedCode) - }) -}) diff --git a/src/localImports/__tests__/transformers/hoistAndMergeImports.ts b/src/localImports/__tests__/transformers/hoistAndMergeImports.ts deleted file mode 100644 index 3a492ab24..000000000 --- a/src/localImports/__tests__/transformers/hoistAndMergeImports.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { mockContext } from '../../../mocks/context' -import { parse } from '../../../parser/parser' -import { Chapter } from '../../../types' -import { hoistAndMergeImports } from '../../transformers/hoistAndMergeImports' -import { parseCodeError, stripLocationInfo } from '../utils' - -describe('hoistAndMergeImports', () => { - let actualContext = mockContext(Chapter.LIBRARY_PARSER) - let expectedContext = mockContext(Chapter.LIBRARY_PARSER) - - beforeEach(() => { - actualContext = mockContext(Chapter.LIBRARY_PARSER) - expectedContext = mockContext(Chapter.LIBRARY_PARSER) - }) - - const assertASTsAreEquivalent = (actualCode: string, expectedCode: string): void => { - const actualProgram = parse(actualCode, actualContext) - const expectedProgram = parse(expectedCode, expectedContext) - if (actualProgram === null || expectedProgram === null) { - throw parseCodeError - } - - hoistAndMergeImports(actualProgram) - expect(stripLocationInfo(actualProgram)).toEqual(stripLocationInfo(expectedProgram)) - } - - test('hoists import declarations to the top of the program', () => { - const actualCode = ` - function square(x) { - return x * x; - } - - import { a, b, c } from "./a.js"; - - export { square }; - - import x from "source-module"; - - square(3); - ` - const expectedCode = ` - import { a, b, c } from "./a.js"; - import x from "source-module"; - - function square(x) { - return x * x; - } - - export { square }; - - square(3); - ` - assertASTsAreEquivalent(actualCode, expectedCode) - }) - - test('merges import declarations from the same module', () => { - const actualCode = ` - import { a, b, c } from "./a.js"; - import { d } from "./a.js"; - import { x } from "./b.js"; - import { e, f } from "./a.js"; - ` - const expectedCode = ` - import { a, b, c, d, e, f } from "./a.js"; - import { x } from "./b.js"; - ` - assertASTsAreEquivalent(actualCode, expectedCode) - }) -}) diff --git a/src/localImports/__tests__/transformers/removeExports.ts b/src/localImports/__tests__/transformers/removeExports.ts deleted file mode 100644 index 656969a00..000000000 --- a/src/localImports/__tests__/transformers/removeExports.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { mockContext } from '../../../mocks/context' -import { parse } from '../../../parser/parser' -import { Chapter } from '../../../types' -import { removeExports } from '../../transformers/removeExports' -import { parseCodeError, stripLocationInfo } from '../utils' - -describe('removeExports', () => { - let actualContext = mockContext(Chapter.LIBRARY_PARSER) - let expectedContext = mockContext(Chapter.LIBRARY_PARSER) - - beforeEach(() => { - actualContext = mockContext(Chapter.LIBRARY_PARSER) - expectedContext = mockContext(Chapter.LIBRARY_PARSER) - }) - - const assertASTsAreEquivalent = (actualCode: string, expectedCode: string): void => { - const actualProgram = parse(actualCode, actualContext) - const expectedProgram = parse(expectedCode, expectedContext) - if (actualProgram === null || expectedProgram === null) { - throw parseCodeError - } - - removeExports(actualProgram) - expect(stripLocationInfo(actualProgram)).toEqual(stripLocationInfo(expectedProgram)) - } - - describe('removes ExportNamedDeclaration nodes', () => { - test('when exporting variable declarations', () => { - const actualCode = ` - export const x = 42; - export let y = 53; - ` - const expectedCode = ` - const x = 42; - let y = 53; - ` - assertASTsAreEquivalent(actualCode, expectedCode) - }) - - test('when exporting function declarations', () => { - const actualCode = ` - export function square(x) { - return x * x; - } - ` - const expectedCode = ` - function square(x) { - return x * x; - } - ` - assertASTsAreEquivalent(actualCode, expectedCode) - }) - - test('when exporting arrow function declarations', () => { - const actualCode = ` - export const square = x => x * x; - ` - const expectedCode = ` - const square = x => x * x; - ` - assertASTsAreEquivalent(actualCode, expectedCode) - }) - - test('when exporting (renamed) identifiers', () => { - const actualCode = ` - const x = 42; - let y = 53; - function square(x) { - return x * x; - } - const id = x => x; - export { x, y, square as sq, id as default }; - ` - const expectedCode = ` - const x = 42; - let y = 53; - function square(x) { - return x * x; - } - const id = x => x; - ` - assertASTsAreEquivalent(actualCode, expectedCode) - }) - }) - - describe('removes ExportDefaultDeclaration nodes', () => { - // Default exports of variable declarations and arrow function declarations - // is not allowed in ES6, and will be caught by the Acorn parser. - test('when exporting function declarations', () => { - const actualCode = ` - export default function square(x) { - return x * x; - } - ` - const expectedCode = ` - function square(x) { - return x * x; - } - ` - assertASTsAreEquivalent(actualCode, expectedCode) - }) - - test('when exporting constants', () => { - const actualCode = ` - const x = 42; - export default x; - ` - const expectedCode = ` - const x = 42; - ` - assertASTsAreEquivalent(actualCode, expectedCode) - }) - - test('when exporting variables', () => { - const actualCode = ` - let y = 53; - export default y; - ` - const expectedCode = ` - let y = 53; - ` - assertASTsAreEquivalent(actualCode, expectedCode) - }) - - test('when exporting functions', () => { - const actualCode = ` - function square(x) { - return x * x; - } - export default square; - ` - const expectedCode = ` - function square(x) { - return x * x; - } - ` - assertASTsAreEquivalent(actualCode, expectedCode) - }) - - test('when exporting arrow functions', () => { - const actualCode = ` - const id = x => x; - export default id; - ` - const expectedCode = ` - const id = x => x; - ` - assertASTsAreEquivalent(actualCode, expectedCode) - }) - - test('when exporting expressions', () => { - const actualCode = ` - export default 123 + 456; - ` - const expectedCode = '' - assertASTsAreEquivalent(actualCode, expectedCode) - }) - }) -}) diff --git a/src/localImports/__tests__/transformers/removeNonSourceModuleImports.ts b/src/localImports/__tests__/transformers/removeNonSourceModuleImports.ts deleted file mode 100644 index 7f6ebd8cb..000000000 --- a/src/localImports/__tests__/transformers/removeNonSourceModuleImports.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { mockContext } from '../../../mocks/context' -import { parse } from '../../../parser/parser' -import { Chapter } from '../../../types' -import { removeNonSourceModuleImports } from '../../transformers/removeNonSourceModuleImports' -import { parseCodeError, stripLocationInfo } from '../utils' - -describe('removeNonSourceModuleImports', () => { - let actualContext = mockContext(Chapter.LIBRARY_PARSER) - let expectedContext = mockContext(Chapter.LIBRARY_PARSER) - - beforeEach(() => { - actualContext = mockContext(Chapter.LIBRARY_PARSER) - expectedContext = mockContext(Chapter.LIBRARY_PARSER) - }) - - const assertASTsAreEquivalent = (actualCode: string, expectedCode: string): void => { - const actualProgram = parse(actualCode, actualContext) - const expectedProgram = parse(expectedCode, expectedContext) - if (actualProgram === null || expectedProgram === null) { - throw parseCodeError - } - - removeNonSourceModuleImports(actualProgram) - expect(stripLocationInfo(actualProgram)).toEqual(stripLocationInfo(expectedProgram)) - } - - test('removes ImportDefaultSpecifier nodes', () => { - const actualCode = ` - import a from "./a.js"; - import x from "source-module"; - ` - const expectedCode = '' - assertASTsAreEquivalent(actualCode, expectedCode) - }) - - // While 'removeNonSourceModuleImports' will remove ImportNamespaceSpecifier nodes, we - // cannot actually test it because ImportNamespaceSpecifier nodes are banned in the parser. - // test('removes ImportNamespaceSpecifier nodes', () => { - // const actualCode = ` - // import * as a from "./a.js"; - // import * as x from "source-module"; - // ` - // const expectedCode = '' - // assertASTsAreEquivalent(actualCode, expectedCode) - // }) - - test('removes only non-Source module ImportSpecifier nodes', () => { - const actualCode = ` - import { a, b, c } from "./a.js"; - import { x, y, z } from "source-module"; - ` - const expectedCode = ` - import { x, y, z } from "source-module"; - ` - assertASTsAreEquivalent(actualCode, expectedCode) - }) -}) diff --git a/src/localImports/preprocessor.ts b/src/localImports/preprocessor.ts deleted file mode 100644 index d4f6464c1..000000000 --- a/src/localImports/preprocessor.ts +++ /dev/null @@ -1,290 +0,0 @@ -import es from 'estree' -import * as path from 'path' - -import { CannotFindModuleError, CircularImportError } from '../errors/localImportErrors' -import { parse } from '../parser/parser' -import { AcornOptions } from '../parser/types' -import { Context } from '../types' -import { isIdentifier } from '../utils/rttc' -import { createInvokedFunctionResultVariableDeclaration } from './constructors/contextSpecificConstructors' -import { DirectedGraph } from './directedGraph' -import { - transformFilePathToValidFunctionName, - transformFunctionNameToInvokedFunctionResultVariableName -} from './filePaths' -import { hoistAndMergeImports } from './transformers/hoistAndMergeImports' -import { removeExports } from './transformers/removeExports' -import { - isSourceModule, - removeNonSourceModuleImports -} from './transformers/removeNonSourceModuleImports' -import { - createAccessImportStatements, - getInvokedFunctionResultVariableNameToImportSpecifiersMap, - transformProgramToFunctionDeclaration -} from './transformers/transformProgramToFunctionDeclaration' -import { isImportDeclaration, isModuleDeclaration } from './typeGuards' - -/** - * Returns all absolute local module paths which should be imported. - * This function makes use of the file path of the current file to - * determine the absolute local module paths. - * - * Note that the current file path must be absolute. - * - * @param program The program to be operated on. - * @param currentFilePath The file path of the current file. - */ -export const getImportedLocalModulePaths = ( - program: es.Program, - currentFilePath: string -): Set => { - if (!path.isAbsolute(currentFilePath)) { - throw new Error(`Current file path '${currentFilePath}' is not absolute.`) - } - - const baseFilePath = path.resolve(currentFilePath, '..') - const importedLocalModuleNames: Set = new Set() - const importDeclarations = program.body.filter(isImportDeclaration) - importDeclarations.forEach((importDeclaration: es.ImportDeclaration): void => { - const modulePath = importDeclaration.source.value - if (typeof modulePath !== 'string') { - throw new Error('Module names must be strings.') - } - if (!isSourceModule(modulePath)) { - const absoluteModulePath = path.resolve(baseFilePath, modulePath) - importedLocalModuleNames.add(absoluteModulePath) - } - }) - return importedLocalModuleNames -} - -const parseProgramsAndConstructImportGraph = ( - files: Partial>, - entrypointFilePath: string, - context: Context -): { - programs: Record - importGraph: DirectedGraph -} => { - const programs: Record = {} - const importGraph = new DirectedGraph() - - // If there is more than one file, tag AST nodes with the source file path. - const numOfFiles = Object.keys(files).length - const shouldAddSourceFileToAST = numOfFiles > 1 - - const parseFile = (currentFilePath: string): void => { - const code = files[currentFilePath] - if (code === undefined) { - context.errors.push(new CannotFindModuleError(currentFilePath)) - return - } - - // Tag AST nodes with the source file path for use in error messages. - const parserOptions: Partial = shouldAddSourceFileToAST - ? { - sourceFile: currentFilePath - } - : {} - const program = parse(code, context, parserOptions) - if (program === null) { - return - } - - programs[currentFilePath] = program - - const importedLocalModulePaths = getImportedLocalModulePaths(program, currentFilePath) - for (const importedLocalModulePath of importedLocalModulePaths) { - // If the source & destination nodes in the import graph are the - // same, then the file is trying to import from itself. This is a - // special case of circular imports. - if (importedLocalModulePath === currentFilePath) { - context.errors.push(new CircularImportError([importedLocalModulePath, currentFilePath])) - return - } - // If we traverse the same edge in the import graph twice, it means - // that there is a cycle in the graph. We terminate early so as not - // to get into an infinite loop (and also because there is no point - // in traversing cycles when our goal is to build up the import - // graph). - if (importGraph.hasEdge(importedLocalModulePath, currentFilePath)) { - continue - } - // Since the file at 'currentFilePath' contains the import statement - // from the file at 'importedLocalModulePath', we treat the former - // as the destination node and the latter as the source node in our - // import graph. This is because when we insert the transformed - // function declarations into the resulting program, we need to start - // with the function declarations that do not depend on other - // function declarations. - importGraph.addEdge(importedLocalModulePath, currentFilePath) - // Recursively parse imported files. - parseFile(importedLocalModulePath) - } - } - - parseFile(entrypointFilePath) - - return { - programs, - importGraph - } -} - -const getSourceModuleImports = (programs: Record): es.ImportDeclaration[] => { - const sourceModuleImports: es.ImportDeclaration[] = [] - Object.values(programs).forEach((program: es.Program): void => { - const importDeclarations = program.body.filter(isImportDeclaration) - importDeclarations.forEach((importDeclaration: es.ImportDeclaration): void => { - const importSource = importDeclaration.source.value - if (typeof importSource !== 'string') { - throw new Error('Module names must be strings.') - } - if (isSourceModule(importSource)) { - sourceModuleImports.push(importDeclaration) - } - }) - }) - return sourceModuleImports -} - -/** - * Preprocesses file imports and returns a transformed Abstract Syntax Tree (AST). - * If an error is encountered at any point, returns `undefined` to signify that an - * error occurred. Details of the error can be found inside `context.errors`. - * - * The preprocessing works by transforming each imported file into a function whose - * parameters are other files (results of transformed functions) and return value - * is a pair where the head is the default export or null, and the tail is a list - * of pairs that map from exported names to identifiers. - * - * See https://github.com/source-academy/js-slang/wiki/Local-Module-Import-&-Export - * for more information. - * - * @param files An object mapping absolute file paths to file content. - * @param entrypointFilePath The absolute path of the entrypoint file. - * @param context The information associated with the program evaluation. - */ -const preprocessFileImports = ( - files: Partial>, - entrypointFilePath: string, - context: Context -): es.Program | undefined => { - // Parse all files into ASTs and build the import graph. - const { programs, importGraph } = parseProgramsAndConstructImportGraph( - files, - entrypointFilePath, - context - ) - // Return 'undefined' if there are errors while parsing. - if (context.errors.length !== 0) { - return undefined - } - - // Check for circular imports. - const topologicalOrderResult = importGraph.getTopologicalOrder() - if (!topologicalOrderResult.isValidTopologicalOrderFound) { - context.errors.push(new CircularImportError(topologicalOrderResult.firstCycleFound)) - return undefined - } - - // We want to operate on the entrypoint program to get the eventual - // preprocessed program. - const entrypointProgram = programs[entrypointFilePath] - const entrypointDirPath = path.resolve(entrypointFilePath, '..') - - // Create variables to hold the imported statements. - const entrypointProgramModuleDeclarations = entrypointProgram.body.filter(isModuleDeclaration) - const entrypointProgramInvokedFunctionResultVariableNameToImportSpecifiersMap = - getInvokedFunctionResultVariableNameToImportSpecifiersMap( - entrypointProgramModuleDeclarations, - entrypointDirPath - ) - const entrypointProgramAccessImportStatements = createAccessImportStatements( - entrypointProgramInvokedFunctionResultVariableNameToImportSpecifiersMap - ) - - // Transform all programs into their equivalent function declaration - // except for the entrypoint program. - const functionDeclarations: Record = {} - for (const [filePath, program] of Object.entries(programs)) { - // The entrypoint program does not need to be transformed into its - // function declaration equivalent as its enclosing environment is - // simply the overall program's (constructed program's) environment. - if (filePath === entrypointFilePath) { - continue - } - - const functionDeclaration = transformProgramToFunctionDeclaration(program, filePath) - const functionName = functionDeclaration.id?.name - if (functionName === undefined) { - throw new Error( - 'A transformed function declaration is missing its name. This should never happen.' - ) - } - - functionDeclarations[functionName] = functionDeclaration - } - - // Invoke each of the transformed functions and store the result in a variable. - const invokedFunctionResultVariableDeclarations: es.VariableDeclaration[] = [] - topologicalOrderResult.topologicalOrder.forEach((filePath: string): void => { - // As mentioned above, the entrypoint program does not have a function - // declaration equivalent, so there is no need to process it. - if (filePath === entrypointFilePath) { - return - } - - const functionName = transformFilePathToValidFunctionName(filePath) - const invokedFunctionResultVariableName = - transformFunctionNameToInvokedFunctionResultVariableName(functionName) - - const functionDeclaration = functionDeclarations[functionName] - const functionParams = functionDeclaration.params.filter(isIdentifier) - if (functionParams.length !== functionDeclaration.params.length) { - throw new Error( - 'Function declaration contains non-Identifier AST nodes as params. This should never happen.' - ) - } - - const invokedFunctionResultVariableDeclaration = createInvokedFunctionResultVariableDeclaration( - functionName, - invokedFunctionResultVariableName, - functionParams - ) - invokedFunctionResultVariableDeclarations.push(invokedFunctionResultVariableDeclaration) - }) - - // Get all Source module imports across the entrypoint program & all imported programs. - const sourceModuleImports = getSourceModuleImports(programs) - - // Re-assemble the program. - const preprocessedProgram: es.Program = { - ...entrypointProgram, - body: [ - ...sourceModuleImports, - ...Object.values(functionDeclarations), - ...invokedFunctionResultVariableDeclarations, - ...entrypointProgramAccessImportStatements, - ...entrypointProgram.body - ] - } - - // After this pre-processing step, all export-related nodes in the AST - // are no longer needed and are thus removed. - removeExports(preprocessedProgram) - // Likewise, all import-related nodes in the AST which are not Source - // module imports are no longer needed and are also removed. - removeNonSourceModuleImports(preprocessedProgram) - // Finally, we need to hoist all remaining imports to the top of the - // program. These imports should be source module imports since - // non-Source module imports would have already been removed. As part - // of this step, we also merge imports from the same module so as to - // import each unique name per module only once. - hoistAndMergeImports(preprocessedProgram) - - return preprocessedProgram -} - -export default preprocessFileImports diff --git a/src/localImports/transformers/hoistAndMergeImports.ts b/src/localImports/transformers/hoistAndMergeImports.ts deleted file mode 100644 index 96699587c..000000000 --- a/src/localImports/transformers/hoistAndMergeImports.ts +++ /dev/null @@ -1,81 +0,0 @@ -import es from 'estree' -import * as _ from 'lodash' - -import { createImportDeclaration, createLiteral } from '../constructors/baseConstructors' -import { cloneAndStripImportSpecifier } from '../constructors/contextSpecificConstructors' -import { isImportDeclaration } from '../typeGuards' - -/** - * Hoists import declarations to the top of the program & merges duplicate - * imports for the same module. - * - * Note that two modules are the same if and only if their import source - * is the same. This function does not resolve paths against a base - * directory. If such a functionality is required, this function will - * need to be modified. - * - * @param program The AST which should have its ImportDeclaration nodes - * hoisted & duplicate imports merged. - */ -export const hoistAndMergeImports = (program: es.Program): void => { - // Separate import declarations from non-import declarations. - const importDeclarations = program.body.filter(isImportDeclaration) - const nonImportDeclarations = program.body.filter( - (node: es.Directive | es.Statement | es.ModuleDeclaration): boolean => - !isImportDeclaration(node) - ) - - // Merge import sources & specifiers. - const importSourceToSpecifiersMap: Map< - string, - Array - > = new Map() - for (const importDeclaration of importDeclarations) { - const importSource = importDeclaration.source.value - if (typeof importSource !== 'string') { - throw new Error('Module names must be strings.') - } - const specifiers = importSourceToSpecifiersMap.get(importSource) ?? [] - for (const specifier of importDeclaration.specifiers) { - // The Acorn parser adds extra information to AST nodes that are not - // part of the ESTree types. As such, we need to clone and strip - // the import specifier AST nodes to get a canonical representation - // that we can use to keep track of whether the import specifier - // is a duplicate or not. - const strippedSpecifier = cloneAndStripImportSpecifier(specifier) - // Note that we cannot make use of JavaScript's built-in Set class - // as it compares references for objects. - const isSpecifierDuplicate = - specifiers.filter( - ( - specifier: es.ImportSpecifier | es.ImportDefaultSpecifier | es.ImportNamespaceSpecifier - ): boolean => { - return _.isEqual(strippedSpecifier, specifier) - } - ).length !== 0 - if (isSpecifierDuplicate) { - continue - } - specifiers.push(strippedSpecifier) - } - importSourceToSpecifiersMap.set(importSource, specifiers) - } - - // Convert the merged import sources & specifiers back into import declarations. - const mergedImportDeclarations: es.ImportDeclaration[] = [] - importSourceToSpecifiersMap.forEach( - ( - specifiers: Array< - es.ImportSpecifier | es.ImportDefaultSpecifier | es.ImportNamespaceSpecifier - >, - importSource: string - ): void => { - mergedImportDeclarations.push( - createImportDeclaration(specifiers, createLiteral(importSource)) - ) - } - ) - - // Hoist the merged import declarations to the top of the program body. - program.body = [...mergedImportDeclarations, ...nonImportDeclarations] -} diff --git a/src/localImports/transformers/removeExports.ts b/src/localImports/transformers/removeExports.ts deleted file mode 100644 index 7e81e9367..000000000 --- a/src/localImports/transformers/removeExports.ts +++ /dev/null @@ -1,66 +0,0 @@ -import es from 'estree' - -import { ancestor } from '../../utils/walkers' -import { isDeclaration } from '../typeGuards' - -/** - * Removes all export-related nodes from the AST. - * - * Export-related AST nodes are only needed in the local imports pre-processing - * step to determine which functions/variables/expressions should be made - * available to other files/modules. After which, they have no functional effect - * on program evaluation. - * - * @param program The AST which should be stripped of export-related nodes. - */ -export const removeExports = (program: es.Program): void => { - ancestor(program, { - // TODO: Handle other export AST nodes. - ExportNamedDeclaration( - node: es.ExportNamedDeclaration, - _state: es.Node[], - ancestors: es.Node[] - ) { - // The ancestors array contains the current node, meaning that the - // parent node is the second last node of the array. - const parent = ancestors[ancestors.length - 2] - // The parent node of an ExportNamedDeclaration node must be a Program node. - if (parent.type !== 'Program') { - return - } - const nodeIndex = parent.body.findIndex(n => n === node) - if (node.declaration) { - // If the ExportNamedDeclaration node contains a declaration, replace - // it with the declaration node in its parent node's body. - parent.body[nodeIndex] = node.declaration - } else { - // Otherwise, remove the ExportNamedDeclaration node in its parent node's body. - parent.body.splice(nodeIndex, 1) - } - }, - ExportDefaultDeclaration( - node: es.ExportDefaultDeclaration, - _state: es.Node[], - ancestors: es.Node[] - ) { - // The ancestors array contains the current node, meaning that the - // parent node is the second last node of the array. - const parent = ancestors[ancestors.length - 2] - // The parent node of an ExportNamedDeclaration node must be a Program node. - if (parent.type !== 'Program') { - return - } - const nodeIndex = parent.body.findIndex(n => n === node) - // 'node.declaration' can be either a Declaration or an Expression. - if (isDeclaration(node.declaration)) { - // If the ExportDefaultDeclaration node contains a declaration, replace - // it with the declaration node in its parent node's body. - parent.body[nodeIndex] = node.declaration - } else { - // Otherwise, the ExportDefaultDeclaration node contains a statement. - // Remove the ExportDefaultDeclaration node in its parent node's body. - parent.body.splice(nodeIndex, 1) - } - } - }) -} diff --git a/src/localImports/transformers/removeNonSourceModuleImports.ts b/src/localImports/transformers/removeNonSourceModuleImports.ts deleted file mode 100644 index 0aa3f99e9..000000000 --- a/src/localImports/transformers/removeNonSourceModuleImports.ts +++ /dev/null @@ -1,113 +0,0 @@ -import es from 'estree' - -import { ancestor } from '../../utils/walkers' -import { isFilePath } from '../filePaths' - -/** - * Returns whether a module name refers to a Source module. - * We define a Source module name to be any string that is not - * a file path. - * - * Source module import: `import { x } from "module";` - * Local (relative) module import: `import { x } from "./module";` - * Local (absolute) module import: `import { x } from "/dir/dir2/module";` - * - * @param moduleName The name of the module. - */ -export const isSourceModule = (moduleName: string): boolean => { - return !isFilePath(moduleName) -} - -/** - * Removes all non-Source module import-related nodes from the AST. - * - * All import-related nodes which are not removed in the pre-processing - * step will be treated by the Source modules loader as a Source module. - * If a Source module by the same name does not exist, the program - * evaluation will error out. As such, this function removes all - * import-related AST nodes which the Source module loader does not - * support, as well as ImportDeclaration nodes for local module imports. - * - * The definition of whether a module is a local module or a Source - * module depends on the implementation of the `isSourceModule` function. - * - * @param program The AST which should be stripped of non-Source module - * import-related nodes. - */ -export const removeNonSourceModuleImports = (program: es.Program): void => { - // First pass: remove all import AST nodes which are unused by Source modules. - ancestor(program, { - ImportSpecifier(_node: es.ImportSpecifier, _state: es.Node[], _ancestors: es.Node[]): void { - // Nothing to do here since ImportSpecifier nodes are used by Source modules. - }, - ImportDefaultSpecifier( - node: es.ImportDefaultSpecifier, - _state: es.Node[], - ancestors: es.Node[] - ): void { - // The ancestors array contains the current node, meaning that the - // parent node is the second last node of the array. - const parent = ancestors[ancestors.length - 2] - // The parent node of an ImportDefaultSpecifier node must be an ImportDeclaration node. - if (parent.type !== 'ImportDeclaration') { - return - } - const nodeIndex = parent.specifiers.findIndex(n => n === node) - // Remove the ImportDefaultSpecifier node in its parent node's array of specifiers. - // This is because Source modules do not support default imports. - parent.specifiers.splice(nodeIndex, 1) - }, - ImportNamespaceSpecifier( - node: es.ImportNamespaceSpecifier, - _state: es.Node[], - ancestors: es.Node[] - ): void { - // The ancestors array contains the current node, meaning that the - // parent node is the second last node of the array. - const parent = ancestors[ancestors.length - 2] - // The parent node of an ImportNamespaceSpecifier node must be an ImportDeclaration node. - if (parent.type !== 'ImportDeclaration') { - return - } - const nodeIndex = parent.specifiers.findIndex(n => n === node) - // Remove the ImportNamespaceSpecifier node in its parent node's array of specifiers. - // This is because Source modules do not support namespace imports. - parent.specifiers.splice(nodeIndex, 1) - } - }) - - // Operate on a copy of the Program node's body to prevent the walk from missing ImportDeclaration nodes. - const programBody = [...program.body] - const removeImportDeclaration = (node: es.ImportDeclaration, ancestors: es.Node[]): void => { - // The ancestors array contains the current node, meaning that the - // parent node is the second last node of the array. - const parent = ancestors[ancestors.length - 2] - // The parent node of an ImportDeclaration node must be a Program node. - if (parent.type !== 'Program') { - return - } - const nodeIndex = programBody.findIndex(n => n === node) - // Remove the ImportDeclaration node in its parent node's body. - programBody.splice(nodeIndex, 1) - } - // Second pass: remove all ImportDeclaration nodes for non-Source modules, or that do not - // have any specifiers (thus being functionally useless). - ancestor(program, { - ImportDeclaration(node: es.ImportDeclaration, _state: es.Node[], ancestors: es.Node[]): void { - if (typeof node.source.value !== 'string') { - throw new Error('Module names must be strings.') - } - // ImportDeclaration nodes without any specifiers are functionally useless and are thus removed. - if (node.specifiers.length === 0) { - removeImportDeclaration(node, ancestors) - return - } - // Non-Source modules should already have been handled in the pre-processing step and are no - // longer needed. They must be removed to avoid being treated as Source modules. - if (!isSourceModule(node.source.value)) { - removeImportDeclaration(node, ancestors) - } - } - }) - program.body = programBody -} diff --git a/src/localImports/typeGuards.ts b/src/localImports/typeGuards.ts deleted file mode 100644 index 6bc97715e..000000000 --- a/src/localImports/typeGuards.ts +++ /dev/null @@ -1,50 +0,0 @@ -import es from 'estree' - -// It is necessary to write this type guard like this as the 'type' of both -// 'Directive' & 'ExpressionStatement' is 'ExpressionStatement'. -// -// export interface Directive extends BaseNode { -// type: "ExpressionStatement"; -// expression: Literal; -// directive: string; -// } -// -// export interface ExpressionStatement extends BaseStatement { -// type: "ExpressionStatement"; -// expression: Expression; -// } -// -// As such, we check whether the 'directive' property exists on the object -// instead in order to differentiate between the two. -export const isDirective = (node: es.Node): node is es.Directive => { - return 'directive' in node -} - -export const isModuleDeclaration = (node: es.Node): node is es.ModuleDeclaration => { - return [ - 'ImportDeclaration', - 'ExportNamedDeclaration', - 'ExportDefaultDeclaration', - 'ExportAllDeclaration' - ].includes(node.type) -} - -export const isStatement = ( - node: es.Directive | es.Statement | es.ModuleDeclaration -): node is es.Statement => { - return !isDirective(node) && !isModuleDeclaration(node) -} - -export function isDeclaration(node: es.Node): node is es.Declaration { - // export type Declaration = - // FunctionDeclaration | VariableDeclaration | ClassDeclaration; - return ( - node.type === 'VariableDeclaration' || - node.type === 'FunctionDeclaration' || - node.type === 'ClassDeclaration' - ) -} - -export function isImportDeclaration(node: es.Node): node is es.ImportDeclaration { - return node.type === 'ImportDeclaration' -} diff --git a/src/modules/__mocks__/moduleLoader.ts b/src/modules/__mocks__/moduleLoader.ts new file mode 100644 index 000000000..a7391b8e0 --- /dev/null +++ b/src/modules/__mocks__/moduleLoader.ts @@ -0,0 +1,15 @@ +export function loadModuleBundle() { + return { + foo: () => 'foo', + bar: () => 'bar' + } +} + +export function loadModuleTabs() { + return [] +} +export const memoizedGetModuleManifest = () => ({ + one_module: { tabs: [] }, + other_module: { tabs: [] }, + another_module: { tabs: [] } +}) diff --git a/src/modules/__mocks__/moduleLoaderAsync.ts b/src/modules/__mocks__/moduleLoaderAsync.ts new file mode 100644 index 000000000..1c98c18a5 --- /dev/null +++ b/src/modules/__mocks__/moduleLoaderAsync.ts @@ -0,0 +1,32 @@ +export const memoizedGetModuleDocsAsync = jest.fn().mockResolvedValue({ + foo: 'foo', + bar: 'bar' +}) + +export const memoizedGetModuleBundleAsync = jest.fn().mockResolvedValue( + `require => ({ + foo: () => 'foo', + bar: () => 'bar', +})` +) + +export const memoizedGetModuleManifestAsync = jest.fn().mockResolvedValue({ + one_module: { tabs: [] }, + other_module: { tabs: [] }, + another_module: { tabs: [] } +}) + +export function loadModuleBundleAsync() { + return Promise.resolve({ + foo: () => 'foo', + bar: () => 'bar' + }) +} + +export function loadModuleTabsAsync() { + return Promise.resolve([]) +} + +export function checkModuleExists() { + return Promise.resolve(true) +} diff --git a/src/modules/__tests__/moduleLoader.ts b/src/modules/__tests__/moduleLoader.ts index d191d8fac..506cd35a3 100644 --- a/src/modules/__tests__/moduleLoader.ts +++ b/src/modules/__tests__/moduleLoader.ts @@ -1,11 +1,13 @@ import { createEmptyContext } from '../../createContext' -import { ModuleConnectionError, ModuleInternalError } from '../../errors/moduleErrors' import { Variant } from '../../types' import { stripIndent } from '../../utils/formatters' +import { ModuleConnectionError, ModuleInternalError } from '../errors' import * as moduleLoader from '../moduleLoader' -// Mock memoize function from lodash -jest.mock('lodash', () => ({ memoize: jest.fn(func => func) })) +jest.mock('lodash', () => ({ + ...jest.requireActual('lodash'), + memoize: jest.fn(x => x) +})) /** * Mock XMLHttpRequest from jsdom environment @@ -80,7 +82,7 @@ describe('Testing modules/moduleLoader.ts in a jsdom environment', () => { const sampleResponse = `(function () {'use strict'; function index(_params) { return { }; } return index; })();` const correctUrl = moduleLoader.MODULES_STATIC_URL + `/bundles/${validModuleBundle}.js` const mockedXMLHttpRequest = mockXMLHttpRequest({ responseText: sampleResponse }) - const response = moduleLoader.memoizedGetModuleFile(validModuleBundle, 'bundle') + const response = moduleLoader.memoizedGetBundle(validModuleBundle) expect(mockedXMLHttpRequest.open).toHaveBeenCalledTimes(1) expect(mockedXMLHttpRequest.open).toHaveBeenCalledWith('GET', correctUrl, false) expect(mockedXMLHttpRequest.send).toHaveBeenCalledTimes(1) @@ -93,7 +95,7 @@ describe('Testing modules/moduleLoader.ts in a jsdom environment', () => { const sampleResponse = `(function (React) {});` const correctUrl = moduleLoader.MODULES_STATIC_URL + `/tabs/${validModuleTab}.js` const mockedXMLHttpRequest = mockXMLHttpRequest({ responseText: sampleResponse }) - const response = moduleLoader.memoizedGetModuleFile(validModuleTab, 'tab') + const response = moduleLoader.memoizedGetTab(validModuleTab) expect(mockedXMLHttpRequest.open).toHaveBeenCalledTimes(1) expect(mockedXMLHttpRequest.open).toHaveBeenCalledWith('GET', correctUrl, false) expect(mockedXMLHttpRequest.send).toHaveBeenCalledTimes(1) @@ -112,7 +114,8 @@ describe('Testing modules/moduleLoader.ts in a jsdom environment', () => { mockXMLHttpRequest({ responseText: sampleResponse }) const loadedBundle = moduleLoader.loadModuleBundle( 'module', - createEmptyContext(1, Variant.DEFAULT, []) + createEmptyContext(1, Variant.DEFAULT, []), + false ) expect(loadedBundle.make_empty_array()).toEqual([]) }) @@ -123,7 +126,7 @@ describe('Testing modules/moduleLoader.ts in a jsdom environment', () => { const wrongModuleText = `export function es6_function(params) {};` mockXMLHttpRequest({ responseText: wrongModuleText }) expect(() => - moduleLoader.loadModuleBundle('module', createEmptyContext(1, Variant.DEFAULT, [])) + moduleLoader.loadModuleBundle('module', createEmptyContext(1, Variant.DEFAULT, []), false) ).toThrow(ModuleInternalError) }) diff --git a/src/modules/__tests__/moduleLoaderAsync.ts b/src/modules/__tests__/moduleLoaderAsync.ts new file mode 100644 index 000000000..ed2473098 --- /dev/null +++ b/src/modules/__tests__/moduleLoaderAsync.ts @@ -0,0 +1,185 @@ +import type { MockedFunction } from 'jest-mock' + +import { mockContext } from '../../mocks/context' +import { Chapter, Variant } from '../../types' +import { ModuleConnectionError, ModuleInternalError } from '../errors' +import { MODULES_STATIC_URL } from '../moduleLoader' +import * as moduleLoader from '../moduleLoaderAsync' + +jest.mock('lodash', () => ({ + ...jest.requireActual('lodash'), + memoize: jest.fn(x => x) +})) + +global.fetch = jest.fn() +const mockedFetch = fetch as MockedFunction + +function mockResponse(response: string, status: number = 200) { + mockedFetch.mockResolvedValueOnce({ + text: () => Promise.resolve(response), + json: () => Promise.resolve(JSON.parse(response)), + status + } as any) +} + +async function expectSuccess( + correctUrl: string, + expectedResp: T, + func: () => Promise, + callCount: number = 1 +) { + const response = await func() + + expect(fetch).toHaveBeenCalledTimes(callCount) + + const [calledUrl, callOpts] = mockedFetch.mock.calls[0] + expect(calledUrl).toEqual(correctUrl) + expect(callOpts).toMatchObject({ method: 'GET' }) + if (typeof expectedResp === 'string') { + expect(response).toEqual(expectedResp) + } else { + expect(response).toMatchObject(expectedResp) + } +} + +async function expectFailure(sampleUrl: string, expectedErr: any, func: () => Promise) { + await expect(() => func()).rejects.toBeInstanceOf(expectedErr) + + const [calledUrl, callOpts] = mockedFetch.mock.calls[0] + expect(calledUrl).toEqual(sampleUrl) + expect(callOpts).toMatchObject({ method: 'GET' }) +} + +beforeEach(() => { + jest.clearAllMocks() +}) + +describe('Test httpGetAsync', () => { + test('Http GET function httpGetAsync() works correctly', async () => { + const sampleResponse = `{ "repeat": { "contents": ["Repeat"] } }` + const sampleUrl = 'https://www.example.com' + + mockResponse(sampleResponse) + await expectSuccess(sampleUrl, sampleResponse, () => + moduleLoader.httpGetAsync(sampleUrl, 'text') + ) + }) + + test('Http GET function httpGetAsync() throws ModuleConnectionError', async () => { + const sampleUrl = 'https://www.example.com' + mockResponse('', 404) + + await expectFailure(sampleUrl, ModuleConnectionError, () => + moduleLoader.httpGetAsync(sampleUrl, 'text') + ) + }) + + test('Http GET modules manifest correctly', async () => { + const sampleResponse = `{ "repeat": { "contents": ["Repeat"] } }` + const correctUrl = MODULES_STATIC_URL + `/modules.json` + mockResponse(sampleResponse) + + await expectSuccess(correctUrl, JSON.parse(sampleResponse), () => + moduleLoader.memoizedGetModuleManifestAsync() + ) + }) + + test('Http GET returns objects when "json" is specified', async () => { + const sampleResponse = `{ "repeat": { "contents": ["Repeat"] } }` + const correctUrl = MODULES_STATIC_URL + `/modules.json` + mockResponse(sampleResponse) + const result = await moduleLoader.httpGetAsync(correctUrl, 'json') + expect(result).toMatchObject(JSON.parse(sampleResponse)) + }) + + test('Handles TypeErrors thrown by fetch', async () => { + mockedFetch.mockImplementationOnce(() => { + throw new TypeError() + }) + await expectFailure('anyUrl', ModuleConnectionError, () => + moduleLoader.httpGetAsync('anyUrl', 'text') + ) + }) +}) + +describe('Test bundle loading', () => { + const sampleModuleName = 'valid_module' + const sampleModuleUrl = MODULES_STATIC_URL + `/bundles/${sampleModuleName}.js` + + test('Http GET module bundle correctly', async () => { + const sampleResponse = `require => ({ foo: () => 'foo' })` + mockResponse(sampleResponse) + + const bundleText = await moduleLoader.memoizedGetModuleBundleAsync(sampleModuleName) + + expect(fetch).toHaveBeenCalledTimes(1) + + const [calledUrl, callOpts] = mockedFetch.mock.calls[0] + expect(calledUrl).toEqual(sampleModuleUrl) + expect(callOpts).toMatchObject({ method: 'GET' }) + expect(bundleText).toEqual(sampleResponse) + }) + + test('Loading a correctly implemented module bundle', async () => { + const context = mockContext(Chapter.SOURCE_4, Variant.DEFAULT) + const sampleResponse = `require => ({ foo: () => 'foo' })` + mockResponse(sampleResponse) + + const loadedModule = await moduleLoader.loadModuleBundleAsync(sampleModuleName, context, false) + + expect(loadedModule.foo()).toEqual('foo') + expect(fetch).toHaveBeenCalledTimes(1) + + const [calledUrl, callOpts] = mockedFetch.mock.calls[0] + expect(calledUrl).toEqual(sampleModuleUrl) + expect(callOpts).toMatchObject({ method: 'GET' }) + }) + + test('Loading a wrongly implemented module bundle throws ModuleInternalError', async () => { + const context = mockContext(Chapter.SOURCE_4, Variant.DEFAULT) + const wrongModuleText = `export function es6_function(params) {};` + mockResponse(wrongModuleText) + await expect(() => + moduleLoader.loadModuleBundleAsync(sampleModuleName, context, true) + ).rejects.toBeInstanceOf(ModuleInternalError) + + expect(fetch).toHaveBeenCalledTimes(1) + }) +}) + +describe('Test tab loading', () => { + const sampleTabUrl = `${MODULES_STATIC_URL}/tabs/Tab1.js` + const sampleManifest = `{ "one_module": { "tabs": ["Tab1", "Tab2"] } }` + + test('Http GET module tab correctly', async () => { + const sampleResponse = `require => ({ foo: () => 'foo' })` + mockResponse(sampleResponse) + + const bundleText = await moduleLoader.memoizedGetModuleTabAsync('Tab1') + + expect(fetch).toHaveBeenCalledTimes(1) + + const [calledUrl, callOpts] = mockedFetch.mock.calls[0] + expect(calledUrl).toEqual(sampleTabUrl) + expect(callOpts).toMatchObject({ method: 'GET' }) + expect(bundleText).toEqual(sampleResponse) + }) + + test('Loading a wrongly implemented tab throws ModuleInternalError', async () => { + mockResponse(sampleManifest) + + const wrongTabText = `export function es6_function(params) {};` + mockResponse(wrongTabText) + mockResponse(wrongTabText) + + await expect(() => moduleLoader.loadModuleTabsAsync('one_module')).rejects.toBeInstanceOf( + ModuleInternalError + ) + expect(fetch).toHaveBeenCalledTimes(3) + + const [[call0Url], [call1Url], [call2Url]] = mockedFetch.mock.calls + expect(call0Url).toEqual(`${MODULES_STATIC_URL}/modules.json`) + expect(call1Url).toEqual(`${MODULES_STATIC_URL}/tabs/Tab1.js`) + expect(call2Url).toEqual(`${MODULES_STATIC_URL}/tabs/Tab2.js`) + }) +}) diff --git a/src/modules/errors.ts b/src/modules/errors.ts index fa3ebaef0..a32e8b2f5 100644 --- a/src/modules/errors.ts +++ b/src/modules/errors.ts @@ -1,21 +1,203 @@ -import type { ImportDeclaration } from 'estree' - +import { UNKNOWN_LOCATION } from '../constants' import { RuntimeSourceError } from '../errors/runtimeSourceError' +import { ErrorSeverity, ErrorType, SourceError } from '../types' +import type * as es from '../utils/ast/types' +import { nonAlphanumericCharEncoding } from './preprocessor/filePaths' + +export class ModuleConnectionError extends RuntimeSourceError { + private static message: string = `Unable to get modules.` + private static elaboration: string = `You should check your Internet connection, and ensure you have used the correct module path.` + constructor(public readonly error?: any, node?: es.Node) { + super(node) + } + + public explain() { + return ModuleConnectionError.message + } + + public elaborate() { + return ModuleConnectionError.elaboration + } +} + +export class ModuleNotFoundError extends RuntimeSourceError { + constructor(public moduleName: string, node?: es.Node) { + super(node) + } -export class UndefinedImportError extends RuntimeSourceError { + public explain() { + return `Module '${this.moduleName}' not found.` + } + + public elaborate() { + return 'You should check your import declarations, and ensure that all are valid modules.' + } +} + +export class ModuleInternalError extends RuntimeSourceError { + constructor(public moduleName: string, public error?: any, node?: es.Node) { + super(node) + } + + public explain() { + return `Error(s) occured when executing the module "${this.moduleName}".` + } + + public elaborate() { + return ` + You may need to contact with the author for this module to fix this error. + ` + } +} + +export abstract class UndefinedImportErrorBase extends RuntimeSourceError { constructor( - public readonly symbol: string, public readonly moduleName: string, - node?: ImportDeclaration + node?: es.ModuleDeclarationWithSource | es.ExportSpecifier | es.ImportSpecifiers ) { super(node) } + public elaborate(): string { + return "Check your imports and make sure what you're trying to import exists!" + } +} + +export class UndefinedImportError extends UndefinedImportErrorBase { + constructor( + public readonly symbol: string, + moduleName: string, + node?: es.ImportSpecifier | es.ImportDefaultSpecifier | es.ExportSpecifier + ) { + super(moduleName, node) + } + public explain(): string { return `'${this.moduleName}' does not contain a definition for '${this.symbol}'` } +} + +export class UndefinedDefaultImportError extends UndefinedImportErrorBase { + constructor( + moduleName: string, + node?: es.ImportSpecifier | es.ImportDefaultSpecifier | es.ExportSpecifier + ) { + super(moduleName, node) + } + + public explain(): string { + return `'${this.moduleName}' does not contain a default export!` + } +} + +export class UndefinedNamespaceImportError extends UndefinedImportErrorBase { + constructor(moduleName: string, node?: es.ImportNamespaceSpecifier | es.ExportAllDeclaration) { + super(moduleName, node) + } + + public explain(): string { + return `'${this.moduleName}' does not export any symbols!` + } +} + +export abstract class ReexportErrorBase implements SourceError { + public severity = ErrorSeverity.ERROR + public type = ErrorType.RUNTIME + public readonly location: es.SourceLocation + public readonly sourceString: string + + constructor(public readonly modulePath: string, public readonly locations: es.SourceLocation[]) { + this.location = locations[0] ?? UNKNOWN_LOCATION + this.sourceString = locations + .map(({ start: { line, column } }) => `(${line}:${column})`) + .join(', ') + } + + public abstract explain(): string + public abstract elaborate(): string +} + +export class ReexportSymbolError extends ReexportErrorBase { + constructor(modulePath: string, public readonly symbol: string, locations: es.SourceLocation[]) { + super(modulePath, locations) + } + + public explain(): string { + return `Multiple export definitions for the symbol '${this.symbol}' at (${this.sourceString})` + } public elaborate(): string { - return "Check your imports and make sure what you're trying to import exists!" + return 'Check that you are not exporting the same symbol more than once' + } +} + +export class ReexportDefaultError extends ReexportErrorBase { + constructor(modulePath: string, locations: es.SourceLocation[]) { + super(modulePath, locations) + } + + public explain(): string { + return `Multiple default export definitions for the symbol at (${this.sourceString})` + } + + public elaborate(): string { + return 'Check that there is only a single default export' + } +} + +export class CircularImportError implements SourceError { + public type = ErrorType.TYPE + public severity = ErrorSeverity.ERROR + public location = UNKNOWN_LOCATION + + constructor(public filePathsInCycle: string[]) {} + + public explain() { + // We need to reverse the file paths in the cycle so that the + // semantics of "'/a.js' -> '/b.js'" is "'/a.js' imports '/b.js'". + const formattedCycle = this.filePathsInCycle + .map(filePath => `'${filePath}'`) + .reverse() + .join(' -> ') + return `Circular import detected: ${formattedCycle}.` + } + + public elaborate() { + return 'Break the circular import cycle by removing imports from any of the offending files.' + } +} + +export abstract class InvalidFilePathError implements SourceError { + public type = ErrorType.TYPE + public severity = ErrorSeverity.ERROR + public location = UNKNOWN_LOCATION + + constructor(public filePath: string) {} + + abstract explain(): string + + abstract elaborate(): string +} + +export class IllegalCharInFilePathError extends InvalidFilePathError { + public explain() { + const validNonAlphanumericChars = Object.keys(nonAlphanumericCharEncoding) + .map(char => `'${char}'`) + .join(', ') + return `File path '${this.filePath}' must only contain alphanumeric chars and/or ${validNonAlphanumericChars}.` + } + + public elaborate() { + return 'Rename the offending file path to only use valid chars.' + } +} + +export class ConsecutiveSlashesInFilePathError extends InvalidFilePathError { + public explain() { + return `File path '${this.filePath}' cannot contain consecutive slashes '//'.` + } + + public elaborate() { + return 'Remove consecutive slashes from the offending file path.' } } diff --git a/src/modules/moduleLoader.ts b/src/modules/moduleLoader.ts index 2801ce81a..cb73acd48 100644 --- a/src/modules/moduleLoader.ts +++ b/src/modules/moduleLoader.ts @@ -2,14 +2,15 @@ import es from 'estree' import { memoize } from 'lodash' import { XMLHttpRequest as NodeXMLHttpRequest } from 'xmlhttprequest-ts' -import { - ModuleConnectionError, - ModuleInternalError, - ModuleNotFoundError -} from '../errors/moduleErrors' import { Context } from '../types' import { wrapSourceModule } from '../utils/operators' -import { ModuleBundle, ModuleDocumentation, ModuleFunctions, Modules } from './moduleTypes' +import { ModuleConnectionError, ModuleInternalError, ModuleNotFoundError } from './errors' +import type { + ModuleBundle, + ModuleDocumentation, + ModuleFunctions, + ModuleManifest +} from './moduleTypes' import { getRequireProvider } from './requireProvider' // Supports both JSDom (Web Browser) environment and Node environment @@ -46,23 +47,37 @@ export function httpGet(url: string): string { * @return Modules */ export const memoizedGetModuleManifest = memoize(getModuleManifest) -function getModuleManifest(): Modules { - const rawManifest = httpGet(`${MODULES_STATIC_URL}/modules.json`) - return JSON.parse(rawManifest) +function getModuleManifest(): ModuleManifest { + try { + const rawManifest = httpGet(`${MODULES_STATIC_URL}/modules.json`) + return JSON.parse(rawManifest) + } catch (error) { + throw new ModuleConnectionError(error) + } } -/** - * Send a HTTP GET request to the modules endpoint to retrieve the specified file - * @return String of module file contents - */ +export const memoizedGetBundle = memoize(getModuleBundle) +function getModuleBundle(path: string) { + return httpGet(`${MODULES_STATIC_URL}/bundles/${path}.js`) +} -const memoizedGetModuleFileInternal = memoize(getModuleFile) -export const memoizedGetModuleFile = (name: string, type: 'tab' | 'bundle' | 'json') => - memoizedGetModuleFileInternal({ name, type }) -function getModuleFile({ name, type }: { name: string; type: 'tab' | 'bundle' | 'json' }): string { - return httpGet(`${MODULES_STATIC_URL}/${type}s/${name}.js${type === 'json' ? 'on' : ''}`) +export const memoizedGetTab = memoize(getModuleTab) +function getModuleTab(path: string) { + return httpGet(`${MODULES_STATIC_URL}/tabs/${path}.js`) } +// /** +// * Send a HTTP GET request to the modules endpoint to retrieve the specified file +// * @return String of module file contents +// */ + +// const memoizedGetModuleFileInternal = memoize(getModuleFile) +// export const memoizedGetModuleFile = (name: string, type: 'tab' | 'bundle' | 'json') => +// memoizedGetModuleFileInternal({ name, type }) +// function getModuleFile({ name, type }: { name: string; type: 'tab' | 'bundle' | 'json' }): string { +// return httpGet(`${MODULES_STATIC_URL}/${type}s/${name}.js${type === 'json' ? 'on' : ''}`) +// } + /** * Loads the respective module package (functions from the module) * @param path imported module name @@ -70,7 +85,12 @@ function getModuleFile({ name, type }: { name: string; type: 'tab' | 'bundle' | * @param node import declaration node * @returns the module's functions object */ -export function loadModuleBundle(path: string, context: Context, node?: es.Node): ModuleFunctions { +export function loadModuleBundle( + path: string, + context: Context, + wrapModules: boolean, + node?: es.Node +): ModuleFunctions { const modules = memoizedGetModuleManifest() // Check if the module exists @@ -78,9 +98,10 @@ export function loadModuleBundle(path: string, context: Context, node?: es.Node) if (moduleList.includes(path) === false) throw new ModuleNotFoundError(path, node) // Get module file - const moduleText = memoizedGetModuleFile(path, 'bundle') + const moduleText = memoizedGetBundle(path) try { const moduleBundle: ModuleBundle = eval(moduleText) + if (wrapModules) return moduleBundle(getRequireProvider(context)) return wrapSourceModule(path, moduleBundle, getRequireProvider(context)) } catch (error) { // console.error("bundle error: ", error) @@ -105,7 +126,7 @@ export function loadModuleTabs(path: string, node?: es.Node) { const sideContentTabPaths: string[] = modules[path].tabs // Load the tabs for the current module return sideContentTabPaths.map(path => { - const rawTabFile = memoizedGetModuleFile(path, 'tab') + const rawTabFile = memoizedGetTab(path) try { return eval(rawTabFile) } catch (error) { @@ -115,17 +136,17 @@ export function loadModuleTabs(path: string, node?: es.Node) { }) } -export const memoizedloadModuleDocs = memoize(loadModuleDocs) +export const memoizedGetModuleDocs = memoize(loadModuleDocs) export function loadModuleDocs(path: string, node?: es.Node) { try { const modules = memoizedGetModuleManifest() // Check if the module exists const moduleList = Object.keys(modules) if (!moduleList.includes(path)) throw new ModuleNotFoundError(path, node) - const result = getModuleFile({ name: path, type: 'json' }) + const result = httpGet(`${MODULES_STATIC_URL}/jsons/${path}.json`) return JSON.parse(result) as ModuleDocumentation } catch (error) { - console.warn('Failed to load module documentation') + console.warn(`Failed to load documentation for ${path}:`, error) return null } } diff --git a/src/modules/moduleLoaderAsync.ts b/src/modules/moduleLoaderAsync.ts new file mode 100644 index 000000000..314050062 --- /dev/null +++ b/src/modules/moduleLoaderAsync.ts @@ -0,0 +1,110 @@ +import type { Node } from 'estree' +import { memoize } from 'lodash' + +import type { Context } from '..' +import { TimeoutError, timeoutPromise } from '../utils/misc' +import { wrapSourceModule } from '../utils/operators' +import { ModuleConnectionError, ModuleInternalError, ModuleNotFoundError } from './errors' +import { MODULES_STATIC_URL } from './moduleLoader' +import type { ModuleBundle, ModuleDocumentation, ModuleManifest } from './moduleTypes' +import { getRequireProvider } from './requireProvider' + +export function httpGetAsync(path: string, type: 'json'): Promise +export function httpGetAsync(path: string, type: 'text'): Promise +export async function httpGetAsync(path: string, type: 'json' | 'text') { + try { + const resp = await timeoutPromise( + fetch(path, { + method: 'GET' + }), + 10000 + ) + + if (resp.status !== 200 && resp.status !== 304) { + throw new ModuleConnectionError() + } + + const promise = type === 'text' ? resp.text() : resp.json() + return timeoutPromise(promise, 10000) + } catch (error) { + if (error instanceof TypeError || error instanceof TimeoutError) { + throw new ModuleConnectionError() + } + if (!(error instanceof ModuleConnectionError)) throw new ModuleInternalError(path, error) + throw error + } +} + +/** + * Send a HTTP GET request to the modules endpoint to retrieve the manifest + * @return Modules + */ +export const memoizedGetModuleManifestAsync = memoize(getModuleManifestAsync) +function getModuleManifestAsync(): Promise { + return httpGetAsync(`${MODULES_STATIC_URL}/modules.json`, 'json') as Promise +} + +async function checkModuleExists(moduleName: string, node?: Node) { + const modules = await memoizedGetModuleManifestAsync() + // Check if the module exists + if (!(moduleName in modules)) throw new ModuleNotFoundError(moduleName, node) + + return modules[moduleName] +} + +export const memoizedGetModuleBundleAsync = memoize(getModuleBundleAsync) +async function getModuleBundleAsync(moduleName: string): Promise { + return httpGetAsync(`${MODULES_STATIC_URL}/bundles/${moduleName}.js`, 'text') +} + +export const memoizedGetModuleTabAsync = memoize(getModuleTabAsync) +function getModuleTabAsync(tabName: string): Promise { + return httpGetAsync(`${MODULES_STATIC_URL}/tabs/${tabName}.js`, 'text') +} + +export const memoizedGetModuleDocsAsync = memoize(getModuleDocsAsync) +async function getModuleDocsAsync(moduleName: string): Promise { + try { + const result = await httpGetAsync(`${MODULES_STATIC_URL}/jsons/${moduleName}.json`, 'json') + return result as ModuleDocumentation + } catch (error) { + console.warn(`Failed to load documentation for ${moduleName}:`, error) + return null + } +} + +export async function loadModuleTabsAsync(moduleName: string, node?: Node) { + const moduleInfo = await checkModuleExists(moduleName, node) + + // Load the tabs for the current module + return Promise.all( + moduleInfo.tabs.map(async path => { + const rawTabFile = await memoizedGetModuleTabAsync(path) + try { + return eval(rawTabFile) + } catch (error) { + // console.error('tab error:', error); + throw new ModuleInternalError(path, error, node) + } + }) + ) +} + +export async function loadModuleBundleAsync( + moduleName: string, + context: Context, + wrapModule: boolean, + node?: Node +) { + // await checkModuleExists(moduleName, node) + const moduleText = await memoizedGetModuleBundleAsync(moduleName) + try { + const moduleBundle: ModuleBundle = eval(moduleText) + + if (wrapModule) return wrapSourceModule(moduleName, moduleBundle, getRequireProvider(context)) + return moduleBundle(getRequireProvider(context)) + } catch (error) { + // console.error("bundle error: ", error) + throw new ModuleInternalError(moduleName, error, node) + } +} diff --git a/src/modules/moduleTypes.ts b/src/modules/moduleTypes.ts index 974f27de4..72a170c1f 100644 --- a/src/modules/moduleTypes.ts +++ b/src/modules/moduleTypes.ts @@ -1,15 +1,43 @@ import type { RequireProvider } from './requireProvider' -export type Modules = { - [module: string]: { - tabs: string[] - } +export type ModuleManifest = Record +export type ModuleBundle = (require: RequireProvider) => ModuleFunctions +export type ModuleFunctions = Record +export type ModuleDocumentation = Record + +export type ImportTransformOptions = { + /** Set to true to load module tabs */ + loadTabs: boolean + + /** + * Wrapping a Source module involves creating nice toString outputs for + * each of its functions. If this behaviour is desired, set this to true + */ + wrapModules: boolean } -export type ModuleBundle = (require: RequireProvider) => ModuleFunctions +export type ImportResolutionOptions = { + /** + * Set this to true if directories should be resolved + * @example + * ``` + * import { a } from './dir0'; // will resolve to 'dir0/index' + * ``` + */ + resolveDirectories: boolean + + /** + * Pass null to enforce strict file names: `'./dir0/file'` will resolve to exactly that path. + * Otherwise pass an array of file extensions `['js', 'ts']`. For example, if `./dir0/file` is not located, + * it will then search for that file with the given extension, e.g. `./dir0/file.js` + */ + resolveExtensions: string[] | null -export type ModuleFunctions = { - [functionName: string]: Function + /** + * Set this to true to enforce that imports from modules must be of + * defined symbols + */ + allowUndefinedImports: boolean } -export type ModuleDocumentation = Record +export type ImportOptions = ImportResolutionOptions & ImportTransformOptions diff --git a/src/modules/preprocessor/__tests__/__snapshots__/preprocessor.ts.snap b/src/modules/preprocessor/__tests__/__snapshots__/preprocessor.ts.snap new file mode 100644 index 000000000..c44e78257 --- /dev/null +++ b/src/modules/preprocessor/__tests__/__snapshots__/preprocessor.ts.snap @@ -0,0 +1,96 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`preprocessFileImports collates Source module imports at the start of the top-level environment of the preprocessed program 1`] = ` +"import {x, y, z, w} from \\"one_module\\"; +import {h} from \\"another_module\\"; +import {f, g} from \\"other_module\\"; +import {x, y, z, w} from \\"one_module\\"; +import {h} from \\"another_module\\"; +import {f, g} from \\"other_module\\"; +function __$c$$dot$$js__() { + const square = x => x * x; + return pair(null, list(pair(\\"square\\", square))); +} +function __$b$$dot$$js__(___$c$$dot$$js___) { + const square = __access_export__(___$c$$dot$$js___, \\"square\\"); + const b = square(5); + return pair(null, list(pair(\\"b\\", b))); +} +const ___$c$$dot$$js___ = __$c$$dot$$js__(); +const ___$b$$dot$$js___ = __$b$$dot$$js__(___$c$$dot$$js___); +const b = __access_export__(___$b$$dot$$js___, \\"b\\"); +b; +" +`; + +exports[`preprocessFileImports ignores Source module imports & removes all non-Source module import-related AST nodes in the preprocessed program 1`] = ` +"import {a, b, c} from \\"one_module\\"; +import d from \\"one_module\\"; +import {a, b, c} from \\"one_module\\"; +import d from \\"one_module\\"; +function __$not$$dash$$source$$dash$$module$$dot$$js__() { + const x = 1; + const y = 2; + const z = 3; + function square(x) { + return x * x; + } + return pair(square, list(pair(\\"x\\", x), pair(\\"y\\", y), pair(\\"z\\", z))); +} +const ___$not$$dash$$source$$dash$$module$$dot$$js___ = __$not$$dash$$source$$dash$$module$$dot$$js__(); +const w = __access_export__(___$not$$dash$$source$$dash$$module$$dot$$js___, \\"default\\"); +const x = __access_export__(___$not$$dash$$source$$dash$$module$$dot$$js___, \\"x\\"); +const y = __access_export__(___$not$$dash$$source$$dash$$module$$dot$$js___, \\"y\\"); +const z = __access_export__(___$not$$dash$$source$$dash$$module$$dot$$js___, \\"z\\"); +" +`; + +exports[`preprocessFileImports removes all export-related AST nodes 1`] = ` +"const x = 42; +let y = 53; +function square(x) { + return x * x; +} +const id = x => x; +function cube(x) { + return x * x * x; +} +" +`; + +exports[`preprocessFileImports returns a preprocessed program with all imports 1`] = ` +"function __$d$$dot$$js__() { + const addTwo = x => x + 2; + return pair(null, list(pair(\\"mysteryFunction\\", addTwo))); +} +function __$c$$dot$$js__(___$d$$dot$$js___) { + const mysteryFunction = __access_export__(___$d$$dot$$js___, \\"mysteryFunction\\"); + const x = mysteryFunction(5); + function square(x) { + return x * x; + } + return pair(x, list(pair(\\"square\\", square))); +} +function __$b$$dot$$js__(___$c$$dot$$js___) { + const y = __access_export__(___$c$$dot$$js___, \\"default\\"); + const square = __access_export__(___$c$$dot$$js___, \\"square\\"); + const a = square(y); + const b = 3; + return pair(null, list(pair(\\"a\\", a), pair(\\"b\\", b))); +} +const ___$d$$dot$$js___ = __$d$$dot$$js__(); +const ___$c$$dot$$js___ = __$c$$dot$$js__(___$d$$dot$$js___); +const ___$b$$dot$$js___ = __$b$$dot$$js__(___$c$$dot$$js___); +const x = __access_export__(___$b$$dot$$js___, \\"a\\"); +const y = __access_export__(___$b$$dot$$js___, \\"b\\"); +x + y; +" +`; + +exports[`preprocessFileImports returns the same AST if the entrypoint file does not contain import/export statements 1`] = ` +"function square(x) { + return x * x; +} +square(5); +" +`; diff --git a/src/modules/preprocessor/__tests__/analyzer.ts b/src/modules/preprocessor/__tests__/analyzer.ts new file mode 100644 index 000000000..722c59717 --- /dev/null +++ b/src/modules/preprocessor/__tests__/analyzer.ts @@ -0,0 +1,597 @@ +import createContext from '../../../createContext' +import { + CircularImportError, + ReexportDefaultError, + ReexportSymbolError, + UndefinedDefaultImportError, + UndefinedImportErrorBase, + UndefinedNamespaceImportError +} from '../../errors' +import { FatalSyntaxError } from '../../../parser/errors' +import { Chapter } from '../../../types' +import { stripIndent } from '../../../utils/formatters' +import validateImportAndExports from '../analyzer' +import { parseProgramsAndConstructImportGraph } from '..' + +jest.mock('../../moduleLoaderAsync') + +type ErrorInfo = { + line: number + col: number + moduleName: string + symbol?: string + namespace?: boolean +} + +async function testCode( + files: Partial>, + entrypointFilePath: string, + allowUndefinedImports: boolean +) { + const context = createContext(Chapter.FULL_JS) + const { programs, importGraph } = await parseProgramsAndConstructImportGraph( + files, + entrypointFilePath, + context + ) + + // Return 'undefined' if there are errors while parsing. + if (context.errors.length !== 0) { + throw context.errors[0] + } + + // Check for circular imports. + const topologicalOrderResult = importGraph.getTopologicalOrder() + if (!topologicalOrderResult.isValidTopologicalOrderFound) { + throw new CircularImportError(topologicalOrderResult.firstCycleFound) + } + + try { + const fullTopoOrder = topologicalOrderResult.topologicalOrder + if (!fullTopoOrder.includes(entrypointFilePath)) { + fullTopoOrder.push(entrypointFilePath) + } + await validateImportAndExports(programs, fullTopoOrder, allowUndefinedImports) + } catch (error) { + // console.log(error) + throw error + } + return true +} + +type Files = Partial> +type ImportTestCase = [Files, string, ErrorInfo] | [Files, string] +describe('Test throwing import validation errors', () => { + async function testFailure( + files: Partial>, + entrypointFilePath: string, + allowUndefinedImports: boolean, + errInfo: ErrorInfo + ) { + let err: any = null + try { + await testCode(files, entrypointFilePath, allowUndefinedImports) + } catch (error) { + err = error + } + + expect(err).toBeInstanceOf(UndefinedImportErrorBase) + expect(err.moduleName).toEqual(errInfo.moduleName) + if (errInfo.namespace) { + // Check namespace import + expect(err).toBeInstanceOf(UndefinedNamespaceImportError) + } else if (errInfo.symbol !== 'default') { + expect(err.symbol).toEqual(errInfo.symbol) + } else { + expect(err).toBeInstanceOf(UndefinedDefaultImportError) + } + + expect(err.location.start).toMatchObject({ + line: errInfo.line, + column: errInfo.col + }) + } + + function testSuccess( + files: Partial>, + entrypointFilePath: string, + allowUndefinedImports: boolean + ) { + return expect(testCode(files, entrypointFilePath, allowUndefinedImports)).resolves.toEqual(true) + } + + function testCases(desc: string, cases: ImportTestCase[]) { + describe(desc, () => { + test.each( + cases.flatMap(([files, entry, errorInfo], i) => { + return [ + [`${i}: Should not throw an error`, files, entry, true], + [`${i}: Should${errorInfo ? '' : ' not'} throw an error`, files, entry, errorInfo] + ] + }) + )('%s', async (_, files, entrypointFilePath, errorInfo) => { + if (errorInfo === true) { + await testSuccess(files, entrypointFilePath, true) + } else if (!errorInfo) { + await testSuccess(files, entrypointFilePath, false) + } else { + await testFailure(files, entrypointFilePath, false, errorInfo) + } + }) + }) + } + + describe('Test regular imports', () => { + testCases('Local imports', [ + [ + { + '/a.js': 'export const a = "a";', + '/b.js': stripIndent` + import { a } from "./a.js"; + + export function b() { + return a; + } + ` + }, + '/b.js' + ], + [ + { + '/a.js': 'export const a = "a";', + '/b.js': stripIndent` + import { a, unknown } from "./a.js"; + + export function b() { + return a; + } + ` + }, + '/b.js', + { moduleName: '/a.js', line: 1, col: 12, symbol: 'unknown' } + ] + ]) + + testCases('Source imports', [ + [ + { + '/a.js': stripIndent` + import { foo, bar } from "one_module"; + export function b() { + return foo(); + } + ` + }, + '/a.js' + ], + [ + { + '/a.js': stripIndent` + import { foo, bar } from "one_module"; + export function b() { + return foo(); + } + ` + }, + '/a.js' + ] + ]) + + testCases('Source and Local imports', [ + [ + { + '/a.js': 'export const a = "a";', + '/b.js': stripIndent` + import { a } from "./a.js"; + import { bar } from 'one_module'; + + export function b() { + bar(); + return a; + } + ` + }, + '/b.js' + ], + [ + { + '/a.js': 'export const a = "a";', + '/b.js': stripIndent` + import { a } from "./a.js"; + import { unknown } from 'one_module'; + + export function b() { + unknown(); + return a; + } + ` + }, + '/b.js', + { moduleName: 'one_module', line: 2, col: 9, symbol: 'unknown' } + ], + [ + { + '/a.js': 'export const a = "a";', + '/b.js': stripIndent` + import { a, unknown } from "./a.js"; + import { foo } from 'one_module'; + + export function b() { + foo(); + return a; + } + ` + }, + '/b.js', + { moduleName: '/a.js', line: 1, col: 12, symbol: 'unknown' } + ] + ]) + }) + + describe('Test default imports', () => { + testCases('Local imports', [ + [ + { + '/a.js': 'const a = "a"; export default a;', + '/b.js': stripIndent` + import a from "./a.js"; + + export function b() { + return a; + } + ` + }, + '/b.js' + ], + [ + { + '/a.js': 'export const a = "a";', + '/b.js': stripIndent` + import unknown, { a } from "./a.js"; + + export function b() { + return a; + } + ` + }, + '/b.js', + { moduleName: '/a.js', line: 1, col: 7, symbol: 'default' } + ], + [ + { + '/a.js': 'export const a = "a";', + '/b.js': stripIndent` + import unknown from "./a.js"; + + export function b() { + return a; + } + ` + }, + '/b.js', + { moduleName: '/a.js', line: 1, col: 7, symbol: 'default' } + ], + [ + { + '/a.js': 'export const a = "a";', + '/b.js': stripIndent` + import { default as unknown } from "./a.js"; + + export function b() { + return a; + } + ` + }, + '/b.js', + { moduleName: '/a.js', line: 1, col: 9, symbol: 'default' } + ] + ]) + + testCases('Source imports', [ + [ + { + '/a.js': stripIndent` + import foo from "one_module"; + export function b() { + return foo(); + } + ` + }, + '/a.js', + { moduleName: 'one_module', line: 1, col: 7, symbol: 'default' } + ], + [ + { + '/a.js': stripIndent` + import { default as foo } from "one_module"; + export function b() { + return foo(); + } + ` + }, + '/a.js', + { moduleName: 'one_module', line: 1, col: 9, symbol: 'default' } + ] + ]) + + testCases('Source and Local imports', [ + [ + { + '/a.js': 'const a = "a"; export default a', + '/b.js': stripIndent` + import a from "./a.js"; + import { bar } from 'one_module'; + + export function b() { + bar(); + return a; + } + ` + }, + '/b.js' + ], + [ + { + '/a.js': 'export const a = "a";', + '/b.js': stripIndent` + import { a } from "./a.js"; + import unknown from 'one_module'; + + export function b() { + unknown(); + return a; + } + ` + }, + '/b.js', + { moduleName: 'one_module', line: 2, col: 7, symbol: 'default' } + ], + [ + { + '/a.js': 'export const a = "a";', + '/b.js': stripIndent` + import unknown, { a } from "./a.js"; + import { default as foo } from 'one_module'; + + export function b() { + foo(); + return a; + } + ` + }, + '/b.js', + { moduleName: '/a.js', line: 1, col: 7, symbol: 'default' } + ] + ]) + }) + + // Re-enable this when namespace imports become supported + // describe('Test namespace imports', () => { + // testCases('Local imports', [ + // [ + // { + // '/a.js': 'export const a = 0;', + // '/b.js': 'import * as a from "./a.js"' + // }, + // '/b.js' + // ], + // [ + // { + // '/a.js': 'const a = 0;', + // '/b.js': 'import * as a from "./a.js"' + // }, + // '/b.js', + // { line: 1, col: 7, moduleName: '/a.js', namespace: true } + // ] + // ]) + + // testCases('Source imports', [ + // [ + // { + // '/a.js': 'import * as bar from "one_module";' + // }, + // '/a.js' + // ] + // ]) + // }) + + describe('Test named exports', () => { + testCases('Exporting from another local module', [ + [ + { + '/a.js': 'export const a = 0;', + '/b.js': 'export { a } from "./a.js"' + }, + '/b.js' + ], + [ + { + '/a.js': 'export const a = 0;', + '/b.js': 'export { b } from "./a.js"' + }, + '/b.js', + { line: 1, col: 9, moduleName: '/a.js', symbol: 'b' } + ], + [ + { + '/a.js': 'export const a = 0;', + '/b.js': 'export { b as a } from "./a.js"' + }, + '/b.js', + { line: 1, col: 9, moduleName: '/a.js', symbol: 'b' } + ], + [ + { + '/a.js': 'export const a = "a"', + '/b.js': 'export * from "./a.js"', + '/c.js': 'export { a } from "./b.js"' + }, + '/c.js' + ] + ]) + }) + + describe('Test export all declarations', () => { + testCases('Exporting from another local module', [ + [ + { + '/a.js': 'export const a = "a"', + '/b.js': 'export * from "./a.js"' + }, + '/b.js' + ], + [ + { + '/a.js': 'const a = "a"', + '/b.js': 'export * from "./a.js"' + }, + '/b.js', + { line: 1, col: 0, moduleName: '/a.js', namespace: true } + ], + [ + { + '/a.js': 'export const a = "a"', + '/b.js': 'export * from "./a.js"', + '/c.js': 'export * from "./b.js"' + }, + '/c.js' + ] + ]) + }) +}) + +describe('Test reexport symbol errors', () => { + function expectFailure( + files: Partial>, + entrypointFilePath: string, + obj: any + ) { + return expect(testCode(files, entrypointFilePath, false)).rejects.toBeInstanceOf(obj) + } + + describe('Duplicate named exports should be handled by FatalSyntaxErrors', () => + test.each([ + [ + 'Duplicate named exports within the same file', + `export function a() {}; export { a } from '/b.js'` + ], + [ + 'Duplicate named exports within the same file with aliasing', + `export function a() {}; export { b as a } from '/b.js'` + ], + [ + 'Duplicate default local exports within the same file', + `export default function a() {}; export { b as default } from '/b.js'` + ], + [ + 'Duplicate named local exports across multiple files', + { + '/a.js': 'export const a = 5; export { a } from "./b.js";', + '/b.js': 'export const a = 6;' + } + ], + [ + 'Duplicate named local and source exports', + 'export const a = 5; export { foo as a } from "one_module";' + ], + [ + 'Duplicate default local and source exports', + 'export default function a() {}; export { foo as default } from "one_module";' + ] + ])('%#. %s', (_, code) => { + const files = typeof code === 'string' ? { '/a.js': code } : code + return expectFailure(files, '/a.js', FatalSyntaxError) + })) + + describe('Duplicate ExportAll declarations', () => + test.each([ + [ + 'Duplicate named local exports', + { + '/a.js': 'export const foo_a = 5; export * from "/b.js";', + '/b.js': 'export const foo_a = 5;' + } + ], + [ + 'Duplicate named local and source exports', + { + '/a.js': 'export const foo = 5; export * from "one_module";' + } + ], + [ + 'Multiple ExportAllDeclarations are checked', + { + '/a.js': 'export * from "/b.js"; export * from "/c.js"', + '/b.js': 'export const foo_a = 5;', + '/c.js': 'export const foo_a = 5;' + } + ], + [ + 'Exports are checked transitively', + { + '/a.js': 'export const foo_a = 5; export * from "/b.js"', + '/b.js': 'export * from "/c.js"', + '/c.js': 'export const foo_a = 5;' + } + ] + // Re-enable when exportalldeclarations with exported names are supported + // [ + // 'Named ExportAllDeclarations have their exported name accounted for', + // { + // '/a.js': 'export const foo_a = 5; export * from "/b.js"', + // '/b.js': 'export * as foo_a from "/c.js"', + // '/c.js': 'export const foo_c = 5;' + // } + // ] + ])('%#. %s', (_, files) => expectFailure(files, '/a.js', ReexportSymbolError))) + + describe('Test default exports', () => + test.each([ + [ + 'Duplicate default local exports', + { + '/a.js': 'export default function a() {}; export * from "/b.js";', + '/b.js': 'export default function b() {};' + } + ], + [ + 'Duplicate unnamed default local exports', + { + '/a.js': 'export default () => {}; export * from "/b.js";', + '/b.js': 'export default 123;' + } + ], + [ + 'Duplicate default exports using ExportNamedDeclarations', + { + '/a.js': 'const a = 1; export { a as default }; export * from "./b.js";', + '/b.js': 'const b = 5; export default b' + } + ], + [ + 'Multiple ExportAllDeclarations are checked', + { + '/a.js': 'export * from "/b.js"; export * from "/c.js"', + '/b.js': 'export default "b";', + '/c.js': 'export default "c";' + } + ], + [ + 'Exports are checked transitively', + { + '/a.js': 'export default "a"; export * from "/b.js"', + '/b.js': 'export * from "/c.js"', + '/c.js': 'export default "c"' + } + ] + // Re-enable when exportalldeclarations with exported names are supported + // [ + // 'Named ExportAllDeclarations have their exported name accounted for', + // { + // '/a.js': 'export default "a"; export * from "/b.js"', + // '/b.js': 'export * as default from "/c.js"', + // '/c.js': 'export const foo_c = 5;' + // } + // ] + ])('%#. %s', (_, files) => expectFailure(files, '/a.js', ReexportDefaultError))) +}) diff --git a/src/localImports/__tests__/directedGraph.ts b/src/modules/preprocessor/__tests__/directedGraph.ts similarity index 100% rename from src/localImports/__tests__/directedGraph.ts rename to src/modules/preprocessor/__tests__/directedGraph.ts diff --git a/src/localImports/__tests__/errorMessages.ts b/src/modules/preprocessor/__tests__/errorMessages.ts similarity index 97% rename from src/localImports/__tests__/errorMessages.ts rename to src/modules/preprocessor/__tests__/errorMessages.ts index 31c912c4b..d636c9ed0 100644 --- a/src/localImports/__tests__/errorMessages.ts +++ b/src/modules/preprocessor/__tests__/errorMessages.ts @@ -1,6 +1,6 @@ -import { parseError, runFilesInContext } from '../../index' -import { mockContext } from '../../mocks/context' -import { Chapter, Variant } from '../../types' +import { parseError, runFilesInContext } from '../../../index' +import { mockContext } from '../../../mocks/context' +import { Chapter, Variant } from '../../../types' describe('syntax errors', () => { let context = mockContext(Chapter.SOURCE_4) diff --git a/src/modules/preprocessor/__tests__/preprocessor.ts b/src/modules/preprocessor/__tests__/preprocessor.ts new file mode 100644 index 000000000..ff707b28e --- /dev/null +++ b/src/modules/preprocessor/__tests__/preprocessor.ts @@ -0,0 +1,463 @@ +import { generate } from 'astring' +import type { Program } from 'estree' +import type { MockedFunction } from 'jest-mock' + +import { parseError } from '../../..' +import { mockContext } from '../../../mocks/context' +import { Chapter } from '../../../types' +import { memoizedGetModuleDocsAsync } from '../../moduleLoaderAsync' +import preprocessFileImports from '..' +import hoistAndMergeImports from '../transformers/hoistAndMergeImports' +import { parseCodeError } from './utils' + +jest.mock('../../moduleLoaderAsync') + +// The preprocessor now checks for the existence of source modules +// so this is here to solve that issue + +/* +describe('getImportedLocalModulePaths', () => { + let context = mockContext(Chapter.LIBRARY_PARSER) + + beforeEach(() => { + context = mockContext(Chapter.LIBRARY_PARSER) + }) + + const assertCorrectModulePathsAreReturned = ( + code: string, + baseFilePath: string, + expectedModulePaths: string[] + ): void => { + const program = parse(code, context) + if (program === null) { + throw parseCodeError + } + expect(getImportedLocalModulePaths(program, baseFilePath)).toEqual(new Set(expectedModulePaths)) + } + + it('throws an error if the current file path is not absolute', () => { + const code = '' + const program = parse(code, context) + if (program === null) { + throw parseCodeError + } + expect(() => getImportedLocalModulePaths(program, 'a.js')).toThrowError( + "Current file path 'a.js' is not absolute." + ) + }) + + it('returns local (relative) module imports', () => { + const code = ` + import { x } from "./dir2/b.js"; + import { y } from "../dir3/c.js"; + ` + assertCorrectModulePathsAreReturned(code, '/dir/a.js', ['/dir/dir2/b.js', '/dir3/c.js']) + }) + + it('returns local (absolute) module imports', () => { + const code = ` + import { x } from "/dir/dir2/b.js"; + import { y } from "/dir3/c.js"; + ` + assertCorrectModulePathsAreReturned(code, '/dir/a.js', ['/dir/dir2/b.js', '/dir3/c.js']) + }) + + it('does not return Source module imports', () => { + const code = ` + import { x } from "rune"; + import { y } from "sound"; + ` + assertCorrectModulePathsAreReturned(code, '/dir/a.js', []) + }) + + it('gracefully handles overly long sequences of double dots (..)', () => { + const code = `import { x } from "../../../../../../../../../b.js"; + ` + assertCorrectModulePathsAreReturned(code, '/dir/a.js', ['/b.js']) + }) + + it('returns unique module paths', () => { + const code = ` + import { a } from "./b.js"; + import { b } from "./b.js"; + import { c } from "./c.js"; + import { d } from "./c.js"; + ` + assertCorrectModulePathsAreReturned(code, '/dir/a.js', ['/dir/b.js', '/dir/c.js']) + }) +}) +*/ + +describe('preprocessFileImports', () => { + let actualContext = mockContext(Chapter.LIBRARY_PARSER) + // let expectedContext = mockContext(Chapter.LIBRARY_PARSER) + + beforeEach(() => { + actualContext = mockContext(Chapter.LIBRARY_PARSER) + // expectedContext = mockContext(Chapter.LIBRARY_PARSER) + }) + + // const assertASTsAreEquivalent = ( + // actualProgram: Program | undefined, + // expectedCode: string + // ): void => { + // // assert(actualProgram !== undefined, 'Actual program should not be undefined') + // if (!actualProgram) { + // // console.log(actualContext.errors[0], 'occurred at:', actualContext.errors[0].location.start) + // throw new Error('Actual program should not be undefined!') + // } + + // const expectedProgram = parse(expectedCode, expectedContext) + // if (expectedProgram === null) { + // throw parseCodeError + // } + + // expect(stripLocationInfo(actualProgram)).toEqual(stripLocationInfo(expectedProgram)) + // } + + const testAgainstSnapshot = (program: Program | undefined | null) => { + if (!program) { + throw parseCodeError + } + + hoistAndMergeImports(program, { '': program }) + + expect(generate(program)).toMatchSnapshot() + } + + it('returns undefined & adds ModuleNotFoundError to context if the entrypoint file does not exist', async () => { + const files: Record = { + '/a.js': '1 + 2;' + } + const actualProgram = await preprocessFileImports(files, '/non-existent-file.js', actualContext) + expect(actualProgram).toBeUndefined() + expect(parseError(actualContext.errors)).toMatchInlineSnapshot( + `"Module '/non-existent-file.js' not found."` + ) + }) + + it('returns undefined & adds ModuleNotFoundError to context if an imported file does not exist', async () => { + const files: Record = { + '/a.js': `import { x } from './non-existent-file.js';` + } + const actualProgram = await preprocessFileImports(files, '/a.js', actualContext) + expect(actualProgram).toBeUndefined() + expect(parseError(actualContext.errors)).toMatchInlineSnapshot( + `"Line 1: Module '/non-existent-file.js' not found."` + ) + }) + + it('returns the same AST if the entrypoint file does not contain import/export statements', async () => { + const files: Record = { + '/a.js': ` + function square(x) { + return x * x; + } + square(5); + ` + } + // const expectedCode = files['/a.js'] + const actualProgram = await preprocessFileImports(files, '/a.js', actualContext) + testAgainstSnapshot(actualProgram) + }) + + it('removes all export-related AST nodes', async () => { + const files: Record = { + '/a.js': ` + export const x = 42; + export let y = 53; + export function square(x) { + return x * x; + } + export const id = x => x; + export default function cube(x) { + return x * x * x; + } + ` + } + // const expectedCode = ` + // const x = 42; + // let y = 53; + // function square(x) { + // return x * x; + // } + // const id = x => x; + // function cube(x) { + // return x * x * x; + // } + // ` + const actualProgram = await preprocessFileImports(files, '/a.js', actualContext) + testAgainstSnapshot(actualProgram) + }) + + it('ignores Source module imports & removes all non-Source module import-related AST nodes in the preprocessed program', async () => { + const docsMocked = memoizedGetModuleDocsAsync as MockedFunction< + typeof memoizedGetModuleDocsAsync + > + docsMocked.mockResolvedValueOnce({ + default: '', + a: '', + b: '', + c: '' + }) + + const files: Record = { + '/a.js': ` + import d, { a, b, c } from "one_module"; + import w, { x, y, z } from "./not-source-module.js"; + `, + '/not-source-module.js': ` + export const x = 1; + export const y = 2; + export const z = 3; + export default function square(x) { + return x * x; + } + ` + } + // const expectedCode = ` + // import { a, b, c } from "one_module"; + + // function __$not$$dash$$source$$dash$$module$$dot$$js__() { + // const x = 1; + // const y = 2; + // const z = 3; + // function square(x) { + // return x * x; + // } + + // return pair(square, list(pair("x", x), pair("y", y), pair("z", z))); + // } + + // const ___$not$$dash$$source$$dash$$module$$dot$$js___ = __$not$$dash$$source$$dash$$module$$dot$$js__(); + + // const w = ${accessExportFunctionName}(___$not$$dash$$source$$dash$$module$$dot$$js___, "${defaultExportLookupName}"); + // const x = ${accessExportFunctionName}(___$not$$dash$$source$$dash$$module$$dot$$js___, "x"); + // const y = ${accessExportFunctionName}(___$not$$dash$$source$$dash$$module$$dot$$js___, "y"); + // const z = ${accessExportFunctionName}(___$not$$dash$$source$$dash$$module$$dot$$js___, "z"); + // ` + const actualProgram = await preprocessFileImports(files, '/a.js', actualContext, { + allowUndefinedImports: true + }) + testAgainstSnapshot(actualProgram) + // assertASTsAreEquivalent(actualProgram, expectedCode) + }) + + it('collates Source module imports at the start of the top-level environment of the preprocessed program', async () => { + const docsMocked = memoizedGetModuleDocsAsync as MockedFunction< + typeof memoizedGetModuleDocsAsync + > + docsMocked.mockResolvedValue({ + f: '', + g: '', + h: '', + w: '', + x: '', + y: '', + z: '' + }) + const files: Record = { + '/a.js': ` + import { b } from "./b.js"; + import { w, x } from "one_module"; + import { f, g } from "other_module"; + + b; + `, + '/b.js': ` + import { square } from "./c.js"; + import { x, y } from "one_module"; + import { h } from "another_module"; + + export const b = square(5); + `, + '/c.js': ` + import { x, y, z } from "one_module"; + + export const square = x => x * x; + ` + } + // const expectedCode = ` + // import { w, x, y, z } from "one_module"; + // import { f, g } from "other_module"; + // import { h } from "another_module"; + + // function __$b$$dot$$js__(___$c$$dot$$js___) { + // const square = ${accessExportFunctionName}(___$c$$dot$$js___, "square"); + + // const b = square(5); + + // return pair(null, list(pair("b", b))); + // } + + // function __$c$$dot$$js__() { + // const square = x => x * x; + + // return pair(null, list(pair("square", square))); + // } + + // const ___$c$$dot$$js___ = __$c$$dot$$js__(); + // const ___$b$$dot$$js___ = __$b$$dot$$js__(___$c$$dot$$js___); + + // const b = ${accessExportFunctionName}(___$b$$dot$$js___, "b"); + + // b; + // ` + const actualProgram = await preprocessFileImports(files, '/a.js', actualContext, { + allowUndefinedImports: true + }) + testAgainstSnapshot(actualProgram) + // assertASTsAreEquivalent(actualProgram, expectedCode) + }) + + it('returns CircularImportError if there are circular imports', async () => { + const files: Record = { + '/a.js': ` + import { b } from "./b.js"; + + export const a = 1; + `, + '/b.js': ` + import { c } from "./c.js"; + + export const b = 2; + `, + '/c.js': ` + import { a } from "./a.js"; + + export const c = 3; + ` + } + await preprocessFileImports(files, '/a.js', actualContext) + expect(parseError(actualContext.errors)).toMatchInlineSnapshot( + `"Circular import detected: '/c.js' -> '/a.js' -> '/b.js' -> '/c.js'."` + ) + }) + + it('returns CircularImportError if there are circular imports - verbose', async () => { + const files: Record = { + '/a.js': ` + import { b } from "./b.js"; + + export const a = 1; + `, + '/b.js': ` + import { c } from "./c.js"; + + export const b = 2; + `, + '/c.js': ` + import { a } from "./a.js"; + + export const c = 3; + ` + } + await preprocessFileImports(files, '/a.js', actualContext) + expect(parseError(actualContext.errors, true)).toMatchInlineSnapshot(` + "Circular import detected: '/c.js' -> '/a.js' -> '/b.js' -> '/c.js'. + Break the circular import cycle by removing imports from any of the offending files. + " + `) + }) + + it('returns CircularImportError if there are self-imports', async () => { + const files: Record = { + '/a.js': ` + import { y } from "./a.js"; + const x = 1; + export { x as y }; + ` + } + await preprocessFileImports(files, '/a.js', actualContext) + expect(parseError(actualContext.errors)).toMatchInlineSnapshot( + `"Circular import detected: '/a.js' -> '/a.js'."` + ) + }) + + it('returns CircularImportError if there are self-imports - verbose', async () => { + const files: Record = { + '/a.js': ` + import { y } from "./a.js"; + const x = 1; + export { x as y }; + ` + } + await preprocessFileImports(files, '/a.js', actualContext) + expect(parseError(actualContext.errors, true)).toMatchInlineSnapshot(` + "Circular import detected: '/a.js' -> '/a.js'. + Break the circular import cycle by removing imports from any of the offending files. + " + `) + }) + + it('returns a preprocessed program with all imports', async () => { + const files: Record = { + '/a.js': ` + import { a as x, b as y } from "./b.js"; + + x + y; + `, + '/b.js': ` + import y, { square } from "./c.js"; + + const a = square(y); + const b = 3; + export { a, b }; + `, + '/c.js': ` + import { mysteryFunction } from "./d.js"; + + const x = mysteryFunction(5); + export function square(x) { + return x * x; + } + export default x; + `, + '/d.js': ` + const addTwo = x => x + 2; + export { addTwo as mysteryFunction }; + ` + } + // const expectedCode = ` + // function __$b$$dot$$js__(___$c$$dot$$js___) { + // const y = ${accessExportFunctionName}(___$c$$dot$$js___, "${defaultExportLookupName}"); + // const square = ${accessExportFunctionName}(___$c$$dot$$js___, "square"); + + // const a = square(y); + // const b = 3; + + // return pair(null, list(pair("a", a), pair("b", b))); + // } + + // function __$c$$dot$$js__(___$d$$dot$$js___) { + // const mysteryFunction = ${accessExportFunctionName}(___$d$$dot$$js___, "mysteryFunction"); + + // const x = mysteryFunction(5); + // function square(x) { + // return x * x; + // } + + // return pair(x, list(pair("square", square))); + // } + + // function __$d$$dot$$js__() { + // const addTwo = x => x + 2; + + // return pair(null, list(pair("mysteryFunction", addTwo))); + // } + + // const ___$d$$dot$$js___ = __$d$$dot$$js__(); + // const ___$c$$dot$$js___ = __$c$$dot$$js__(___$d$$dot$$js___); + // const ___$b$$dot$$js___ = __$b$$dot$$js__(___$c$$dot$$js___); + + // const x = ${accessExportFunctionName}(___$b$$dot$$js___, "a"); + // const y = ${accessExportFunctionName}(___$b$$dot$$js___, "b"); + + // x + y; + // ` + const actualProgram = await preprocessFileImports(files, '/a.js', actualContext, { + allowUndefinedImports: true + }) + testAgainstSnapshot(actualProgram) + }) +}) diff --git a/src/modules/preprocessor/__tests__/resolver.ts b/src/modules/preprocessor/__tests__/resolver.ts new file mode 100644 index 000000000..50dfe3272 --- /dev/null +++ b/src/modules/preprocessor/__tests__/resolver.ts @@ -0,0 +1,101 @@ +import { memoizedGetModuleManifestAsync } from '../../moduleLoaderAsync' +import resolveModule from '../resolver' + +jest.mock('../../moduleLoaderAsync') + +beforeEach(() => { + jest.clearAllMocks() +}) + +test('If only local imports are used, the module manifest is not loaded', async () => { + await resolveModule('/a.js', '/b.js', () => true, { + resolveDirectories: false, + resolveExtensions: null + }) + + expect(memoizedGetModuleManifestAsync).toHaveBeenCalledTimes(0) +}) + +test('Returns false and resolved path of source file when resolution fails', () => { + return expect( + resolveModule('/', './a', () => false, { + resolveDirectories: true, + resolveExtensions: ['js'] + }) + ).resolves.toEqual([false, '/a']) +}) + +test('Will resolve directory imports', () => { + const mockResolver = (p: string) => p === '/a/index' + + return expect( + resolveModule('/', '/a', mockResolver, { + resolveDirectories: true, + resolveExtensions: null + }) + ).resolves.toEqual([true, '/a/index']) +}) + +test('Will resolve extensions', () => { + const mockResolver = (p: string) => p === '/a.ts' + + return expect( + resolveModule('/', '/a', mockResolver, { + resolveDirectories: false, + resolveExtensions: ['js', 'ts'] + }) + ).resolves.toEqual([true, '/a.ts']) +}) + +test('Will resolve directory import with extensions', () => { + const mockResolver = (p: string) => p === '/a/index.ts' + + return expect( + resolveModule('/', '/a', mockResolver, { + resolveDirectories: true, + resolveExtensions: ['js', 'ts'] + }) + ).resolves.toEqual([true, '/a/index.ts']) +}) + +test('Will not resolve if the corresponding options are given as false', () => { + const mockResolver = (p: string) => { + return p === '/a.js' || p === '/a/index' + } + return expect( + resolveModule('/', './a', mockResolver, { + resolveDirectories: false, + resolveExtensions: null + }) + ).resolves.toEqual([false, '/a']) +}) + +test('Will not resolve directories with extensions if resolveExtensions is false', () => { + const mockResolver = (p: string) => p === '/a/index.js' + return expect( + resolveModule('/', './a', mockResolver, { + resolveDirectories: true, + resolveExtensions: null + }) + ).resolves.toEqual([false, '/a']) +}) + +test('Checks the module manifest when importing source modules', async () => { + const result = await resolveModule('/', 'one_module', () => false, { + resolveDirectories: true, + resolveExtensions: ['js'] + }) + + expect(memoizedGetModuleManifestAsync).toHaveBeenCalledTimes(1) + expect(result).toEqual([true, 'one_module']) +}) + +test('Returns false on failing to resolve a source module', async () => { + const result = await resolveModule('/', 'unknown_module', () => true, { + resolveDirectories: true, + resolveExtensions: ['js'] + }) + + expect(memoizedGetModuleManifestAsync).toHaveBeenCalledTimes(1) + expect(result).toEqual([false, 'unknown_module']) +}) diff --git a/src/modules/preprocessor/__tests__/transformers/__snapshots__/hoistAndMergeImports.ts.snap b/src/modules/preprocessor/__tests__/transformers/__snapshots__/hoistAndMergeImports.ts.snap new file mode 100644 index 000000000..0e387b4ca --- /dev/null +++ b/src/modules/preprocessor/__tests__/transformers/__snapshots__/hoistAndMergeImports.ts.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`hoistAndMergeImports hoists import declarations to the top of the program 1`] = ` +"import x from \\"source-module\\"; +function square(x) { + return x * x; +} +import {a, b, c} from \\"./a.js\\"; +export {square}; +import x from \\"source-module\\"; +square(3); +" +`; + +exports[`hoistAndMergeImports merges import declarations from the same module 1`] = ` +"import {a, b, c} from \\"./a.js\\"; +import {d} from \\"./a.js\\"; +import {x} from \\"./b.js\\"; +import {e, f} from \\"./a.js\\"; +" +`; diff --git a/src/modules/preprocessor/__tests__/transformers/hoistAndMergeImports.ts b/src/modules/preprocessor/__tests__/transformers/hoistAndMergeImports.ts new file mode 100644 index 000000000..4e92660db --- /dev/null +++ b/src/modules/preprocessor/__tests__/transformers/hoistAndMergeImports.ts @@ -0,0 +1,84 @@ +import { generate } from 'astring' + +import { mockContext } from '../../../../mocks/context' +import { parse } from '../../../../parser/parser' +import { Chapter } from '../../../../types' +import hoistAndMergeImports from '../../transformers/hoistAndMergeImports' +import { parseCodeError } from '../utils' + +describe('hoistAndMergeImports', () => { + let actualContext = mockContext(Chapter.LIBRARY_PARSER) + // let expectedContext = mockContext(Chapter.LIBRARY_PARSER) + + beforeEach(() => { + actualContext = mockContext(Chapter.LIBRARY_PARSER) + // expectedContext = mockContext(Chapter.LIBRARY_PARSER) + }) + + // const assertASTsAreEquivalent = (actualCode: string, expectedCode: string): void => { + // const actualProgram = parse(actualCode, actualContext) + // const expectedProgram = parse(expectedCode, expectedContext) + // if (actualProgram === null || expectedProgram === null) { + // throw parseCodeError + // } + + // hoistAndMergeImports(actualProgram, [actualProgram]) + // expect(stripLocationInfo(actualProgram)).toEqual(stripLocationInfo(expectedProgram)) + // } + + const testAgainstSnapshot = (code: string) => { + const program = parse(code, actualContext) + if (program === null) { + throw parseCodeError + } + + hoistAndMergeImports(program, { '': program }) + expect(generate(program)).toMatchSnapshot() + } + + test('hoists import declarations to the top of the program', () => { + const actualCode = ` + function square(x) { + return x * x; + } + + import { a, b, c } from "./a.js"; + + export { square }; + + import x from "source-module"; + + square(3); + ` + testAgainstSnapshot(actualCode) + // const expectedCode = ` + // import { a, b, c } from "./a.js"; + // import x from "source-module"; + + // function square(x) { + // return x * x; + // } + + // export { square }; + + // square(3); + // ` + // assertASTsAreEquivalent(actualCode, expectedCode) + }) + + test('merges import declarations from the same module', () => { + const actualCode = ` + import { a, b, c } from "./a.js"; + import { d } from "./a.js"; + import { x } from "./b.js"; + import { e, f } from "./a.js"; + ` + + testAgainstSnapshot(actualCode) + // const expectedCode = ` + // import { a, b, c, d, e, f } from "./a.js"; + // import { x } from "./b.js"; + // ` + // assertASTsAreEquivalent(actualCode, expectedCode) + }) +}) diff --git a/src/localImports/__tests__/transformers/transformProgramToFunctionDeclaration.ts b/src/modules/preprocessor/__tests__/transformers/transformProgramToFunctionDeclaration.ts similarity index 98% rename from src/localImports/__tests__/transformers/transformProgramToFunctionDeclaration.ts rename to src/modules/preprocessor/__tests__/transformers/transformProgramToFunctionDeclaration.ts index b3221ec50..50df9c4b0 100644 --- a/src/localImports/__tests__/transformers/transformProgramToFunctionDeclaration.ts +++ b/src/modules/preprocessor/__tests__/transformers/transformProgramToFunctionDeclaration.ts @@ -1,7 +1,7 @@ -import { mockContext } from '../../../mocks/context' -import { parse } from '../../../parser/parser' -import { defaultExportLookupName } from '../../../stdlib/localImport.prelude' -import { Chapter } from '../../../types' +import { mockContext } from '../../../../mocks/context' +import { parse } from '../../../../parser/parser' +import { defaultExportLookupName } from '../../../../stdlib/localImport.prelude' +import { Chapter } from '../../../../types' import { transformProgramToFunctionDeclaration } from '../../transformers/transformProgramToFunctionDeclaration' import { parseCodeError, stripLocationInfo } from '../utils' diff --git a/src/localImports/__tests__/utils.ts b/src/modules/preprocessor/__tests__/utils.ts similarity index 97% rename from src/localImports/__tests__/utils.ts rename to src/modules/preprocessor/__tests__/utils.ts index d2ad6fbc4..85b9d798b 100644 --- a/src/localImports/__tests__/utils.ts +++ b/src/modules/preprocessor/__tests__/utils.ts @@ -1,6 +1,6 @@ import es from 'estree' -import { full, simple } from '../../utils/walkers' +import { full, simple } from '../../../utils/ast/walkers' export const parseCodeError = new Error('Unable to parse code') diff --git a/src/modules/preprocessor/analyzer.ts b/src/modules/preprocessor/analyzer.ts new file mode 100644 index 000000000..a2bd5d16a --- /dev/null +++ b/src/modules/preprocessor/analyzer.ts @@ -0,0 +1,384 @@ +import { UNKNOWN_LOCATION } from '../../constants' +import { + ModuleInternalError, + ReexportDefaultError, + ReexportSymbolError, + UndefinedDefaultImportError, + UndefinedImportError, + UndefinedNamespaceImportError +} from '../../modules/errors' +import ArrayMap from '../../utils/arrayMap' +import assert from '../../utils/assert' +import * as create from '../../utils/ast/astCreator' +import { extractIdsFromPattern, processExportNamedDeclaration } from '../../utils/ast/astUtils' +import { isSourceImport } from '../../utils/ast/typeGuards' +import type * as es from '../../utils/ast/types' +import { reduceAsync } from '../../utils/misc' +import { memoizedGetModuleDocsAsync } from '../moduleLoaderAsync' + +type ExportRecord = { + /** + * The name of the symbol defined by its source + */ + symbolName: string + + /** + * The actual source in which the symbol is defined + */ + source: string + + loc: es.SourceLocation +} + +/** + * An abstraction of the `Set` type. When `allowUndefinedImports` is true, + * the set is replaced with an object that will never throw an error for any kind + * of imported symbol + */ +type ExportSymbolsRecord = { + has: (symbol: string) => boolean + readonly size: number + [Symbol.iterator]: () => Iterator +} + +/** + * An abstraction of the `Map` type. When `allowUndefinedImports` is true, + * the set is replaced with an object that will ensure no errors are thrown for any kind + * of imported symbol + */ +type ExportSourceMap = { + get: (symbol: string) => ExportRecord | undefined + set: (symbol: string, value: ExportRecord) => void + keys: () => Iterable +} + +const validateDefaultImport = ( + spec: es.ImportDefaultSpecifier | es.ExportSpecifier | es.ImportSpecifier, + sourcePath: string, + modExported: ExportSymbolsRecord +) => { + if (!modExported.has('default')) { + throw new UndefinedDefaultImportError(sourcePath, spec) + } +} + +const validateImport = ( + spec: es.ImportSpecifier | es.ImportDefaultSpecifier | es.ExportSpecifier, + sourcePath: string, + modExported: ExportSymbolsRecord +) => { + let symbol: string + switch (spec.type) { + case 'ExportSpecifier': { + symbol = spec.local.name + break + } + case 'ImportSpecifier': { + symbol = spec.imported.name + break + } + case 'ImportDefaultSpecifier': { + symbol = 'default' + break + } + } + + if (symbol === 'default') { + validateDefaultImport(spec, sourcePath, modExported) + } else if (!modExported.has(symbol)) { + throw new UndefinedImportError(symbol, sourcePath, spec) + } +} + +const validateNamespaceImport = ( + spec: es.ImportNamespaceSpecifier | es.ExportAllDeclaration, + sourcePath: string, + modExported: ExportSymbolsRecord +) => { + if (modExported.size === 0) { + throw new UndefinedNamespaceImportError(sourcePath, spec) + } +} + +/** + * Check for undefined imports, and also for symbols that have multiple export + * definitions, and also resolve export and import directives to their sources + */ +export default async function analyzeImportsAndExports( + programs: Record, + topoOrder: string[], + allowUndefinedImports: boolean +) { + const exportMap: Record = {} + + /** + The idea behind this function is to resolve indirect exports + For example + ``` + // a.js + export const a = "a"; + // b.js + export { a as b } from './a.js' + ``` + + We want to change the following import statement `import { b } from './b.js'` to + `import { a } from './a.js', since the `export` declaration in `b.js` just serves + as a redirection and doesn't affect code behaviour + */ + function resolveSymbol(source: string, desiredSymbol: string): [string, string] { + const mapResult = exportMap[source].get(desiredSymbol) + + // The export map must already contain the desired symbol. For example: + // the declaration `export { a } from './a.js'` would cause `./a.js` to be processed before the + // current file due to the topological ordering + assert(!!mapResult, 'This should never be null if the topological order provided is correct') + + // eslint-disable-next-line prefer-const + let { source: newSource, symbolName, loc } = mapResult + + // So for each exported symbol, we return the path to the file where it is actually + // defined and the name it was defined with (since exports can have aliases) + // Kind of like a UFDS, where the roots of each set are symbols that are defined within + // its own file, or imports from Source modules + if (isSourceImport(source) || newSource === source) return [newSource, symbolName] + ;[newSource, symbolName] = resolveSymbol(newSource, symbolName) + exportMap[source].set(desiredSymbol, { source: newSource, symbolName, loc }) + return [newSource, symbolName] + } + + const getDocs = async ( + node: es.ModuleDeclarationWithSource + ): Promise<[ExportSymbolsRecord, string]> => { + const path = node.source!.value as string + + if (allowUndefinedImports) { + exportMap[path] = { + get: (symbol: string) => ({ + source: path, + symbolName: symbol, + loc: UNKNOWN_LOCATION + }), + set: () => {}, + keys: () => [''] + } + + // When undefined imports are allowed, we substitute the list of exported + // symbols for an object that behaves like a set but always returns true when + // `has` is queried + return [ + { + has: () => true, + [Symbol.iterator]: () => ({ next: () => ({ done: true, value: null }) }), + size: 9999 + }, + path + ] + } + + if (!(path in exportMap)) { + // Because modules are loaded in topological order, the exported symbols for a local + // module should be loaded by the time they are needed + // So we can assume that it is the documentation for a Source module that needs to be + // loaded here + assert( + isSourceImport(path), + `Trying to load: ${path}, local modules should already have been loaded in topological order` + ) + + const docs = await memoizedGetModuleDocsAsync(path) + if (!docs) { + throw new ModuleInternalError(path, `Failed to load documentation for ${path}`) + } + exportMap[path] = new Map( + Object.keys(docs).map(symbol => [ + symbol, + { source: path, symbolName: symbol, loc: UNKNOWN_LOCATION } + ]) + ) + } + return [new Set(exportMap[path].keys()), path] + } + + const newImportDeclaration = ( + source: string, + local: es.Identifier, + imported: string + ): es.ImportDeclaration => ({ + type: 'ImportDeclaration', + source: create.literal(source), + specifiers: [ + imported === 'default' + ? { + type: 'ImportDefaultSpecifier', + local + } + : { + type: 'ImportSpecifier', + local, + imported: create.identifier(imported) + } + ] + }) + + const newPrograms: Record = {} + for (const moduleName of topoOrder) { + const program = programs[moduleName] + const exportedSymbols = new ArrayMap() + + const newBody = await reduceAsync( + program.body, + async (body, node) => { + switch (node.type) { + case 'ImportDeclaration': { + const [exports, source] = await getDocs(node) + const newDecls = node.specifiers.map(spec => { + switch (spec.type) { + case 'ImportDefaultSpecifier': + case 'ImportSpecifier': { + if (!allowUndefinedImports) validateImport(spec, source, exports) + + const desiredSymbol = + spec.type === 'ImportSpecifier' ? spec.imported.name : 'default' + const [newSource, symbolName] = resolveSymbol(source, desiredSymbol) + return newImportDeclaration(newSource, spec.local, symbolName) + } + case 'ImportNamespaceSpecifier': { + throw new Error('Namespace imports are not supported!') + // validateNamespaceImport(spec, source, exports) + // return { + // ...node, + // specifiers: [spec] + // } + } + } + }) + return [...body, ...newDecls] + } + case 'ExportDefaultDeclaration': { + exportedSymbols.add('default', { + source: moduleName, + symbolName: 'default', + loc: node.loc! + }) + return [...body, node] + } + case 'ExportNamedDeclaration': { + return await processExportNamedDeclaration(node, { + withVarDecl: async ({ declarations }) => { + for (const { id } of declarations) { + extractIdsFromPattern(id).forEach(({ name }) => { + exportedSymbols.add(name, { + source: moduleName, + symbolName: name, + loc: id.loc! + }) + }) + } + return [...body, node] + }, + withFunction: async ({ id: { name } }) => { + exportedSymbols.add(name, { + source: moduleName, + symbolName: name, + loc: node.loc! + }) + return [...body, node] + }, + withClass: async ({ id: { name } }) => { + exportedSymbols.add(name, { + source: moduleName, + symbolName: name, + loc: node.loc! + }) + return [...body, node] + }, + localExports: async ({ specifiers }) => { + specifiers.forEach(spec => + exportedSymbols.add(spec.exported.name, { + source: moduleName, + symbolName: spec.local.name, + loc: spec.loc! + }) + ) + return [...body, node] + }, + withSource: async node => { + const [exports, source] = await getDocs(node) + const newDecls = node.specifiers.map(spec => { + if (!allowUndefinedImports) validateImport(spec, source, exports) + + const [newSource, symbolName] = resolveSymbol(source, spec.local.name) + exportedSymbols.add(spec.exported.name, { + source: newSource, + symbolName, + loc: spec.loc! + }) + + const newDecl: es.ExportNamedDeclarationWithSource = { + type: 'ExportNamedDeclaration', + declaration: null, + source: create.literal(newSource), + specifiers: [ + { + type: 'ExportSpecifier', + exported: spec.exported, + local: create.identifier(symbolName) + } + ] + } + return newDecl + }) + return [...body, ...newDecls] + } + }) + } + case 'ExportAllDeclaration': { + if (node.exported) { + throw new Error('ExportAllDeclarations with exported name are not supported') + // exportedSymbols.add(node.exported.name, { + // source, + // symbolName: node.exported.name, + // loc: node.loc!, + // }) + } else { + const [exports, source] = await getDocs(node) + if (!allowUndefinedImports) validateNamespaceImport(node, source, exports) + + for (const symbol of exports) { + const [newSource, newSymbol] = resolveSymbol(source, symbol) + exportedSymbols.add(symbol, { + source: newSource, + symbolName: newSymbol, + loc: node.loc! + }) + } + } + } + default: + return [...body, node] + } + }, + [] as es.Program['body'] + ) + + exportMap[moduleName] = new Map( + exportedSymbols.entries().map(([symbol, records]) => { + if (records.length === 1) return [symbol, records[0]] + assert(records.length > 0, 'An exported symbol cannot have zero nodes associated with it') + const locations = records.map(({ loc }) => loc) + if (symbol === 'default') { + throw new ReexportDefaultError(moduleName, locations) + } else { + throw new ReexportSymbolError(moduleName, symbol, locations) + } + }) + ) + + newPrograms[moduleName] = { + ...program, + body: newBody + } + } + + return newPrograms +} diff --git a/src/localImports/constructors/baseConstructors.ts b/src/modules/preprocessor/constructors/baseConstructors.ts similarity index 96% rename from src/localImports/constructors/baseConstructors.ts rename to src/modules/preprocessor/constructors/baseConstructors.ts index f67fb5c6c..eea13ac83 100644 --- a/src/localImports/constructors/baseConstructors.ts +++ b/src/modules/preprocessor/constructors/baseConstructors.ts @@ -1,4 +1,4 @@ -import es from 'estree' +import type * as es from '../../../utils/ast/types' // Note that typecasting is done on some of the constructed AST nodes because // the ESTree AST node types are not fully aligned with the actual AST that @@ -74,7 +74,7 @@ export const createFunctionDeclaration = ( name: string, params: es.Pattern[], body: es.Statement[] -): es.FunctionDeclaration => { +): es.FunctionDeclarationWithId => { return { type: 'FunctionDeclaration', expression: false, @@ -90,7 +90,7 @@ export const createFunctionDeclaration = ( } // The 'expression' property is not typed in ESTree, but it exists // on FunctionDeclaration nodes in the AST generated by acorn parser. - } as es.FunctionDeclaration + } as es.FunctionDeclarationWithId } export const createImportDeclaration = ( diff --git a/src/localImports/constructors/contextSpecificConstructors.ts b/src/modules/preprocessor/constructors/contextSpecificConstructors.ts similarity index 97% rename from src/localImports/constructors/contextSpecificConstructors.ts rename to src/modules/preprocessor/constructors/contextSpecificConstructors.ts index 4b8c9d8e4..aa16ec4b5 100644 --- a/src/localImports/constructors/contextSpecificConstructors.ts +++ b/src/modules/preprocessor/constructors/contextSpecificConstructors.ts @@ -1,6 +1,6 @@ import es from 'estree' -import { accessExportFunctionName } from '../../stdlib/localImport.prelude' +import { accessExportFunctionName } from '../../../stdlib/localImport.prelude' import { createCallExpression, createIdentifier, diff --git a/src/localImports/directedGraph.ts b/src/modules/preprocessor/directedGraph.ts similarity index 100% rename from src/localImports/directedGraph.ts rename to src/modules/preprocessor/directedGraph.ts diff --git a/src/localImports/filePaths.ts b/src/modules/preprocessor/filePaths.ts similarity index 99% rename from src/localImports/filePaths.ts rename to src/modules/preprocessor/filePaths.ts index 50cd1c4f0..8c12af17a 100644 --- a/src/localImports/filePaths.ts +++ b/src/modules/preprocessor/filePaths.ts @@ -2,7 +2,7 @@ import { ConsecutiveSlashesInFilePathError, IllegalCharInFilePathError, InvalidFilePathError -} from '../errors/localImportErrors' +} from '../../modules/errors' /** * Maps non-alphanumeric characters that are legal in file paths diff --git a/src/modules/preprocessor/index.ts b/src/modules/preprocessor/index.ts new file mode 100644 index 000000000..e56140cd0 --- /dev/null +++ b/src/modules/preprocessor/index.ts @@ -0,0 +1,332 @@ +import * as pathlib from 'path' + +import { parse } from '../../parser/parser' +import type { AcornOptions } from '../../parser/types' +import type { Context } from '../../types' +import assert from '../../utils/assert' +import { + isIdentifier, + isModuleDeclaration, + isModuleDeclarationWithSource, + isSourceImport +} from '../../utils/ast/typeGuards' +import type * as es from '../../utils/ast/types' +import { CircularImportError, ModuleNotFoundError } from '../errors' +import type { ImportResolutionOptions } from '../moduleTypes' +import analyzeImportsAndExports from './analyzer' +import { createInvokedFunctionResultVariableDeclaration } from './constructors/contextSpecificConstructors' +import { DirectedGraph } from './directedGraph' +import { transformFunctionNameToInvokedFunctionResultVariableName } from './filePaths' +import resolveModule from './resolver' +import hoistAndMergeImports from './transformers/hoistAndMergeImports' +import removeImportsAndExports from './transformers/removeImportsAndExports' +import { + createAccessImportStatements, + getInvokedFunctionResultVariableNameToImportSpecifiersMap, + transformProgramToFunctionDeclaration +} from './transformers/transformProgramToFunctionDeclaration' + +/** + * Error type to indicate that preprocessing has failed but that the context + * contains the underlying errors + */ +class PreprocessError extends Error {} + +const defaultResolutionOptions: Required = { + allowUndefinedImports: false, + resolveDirectories: false, + resolveExtensions: null +} + +/** + * Parse all of the provided files and figure out which modules + * are dependent on which, returning that result in the form + * of a DAG + */ +export const parseProgramsAndConstructImportGraph = async ( + files: Partial>, + entrypointFilePath: string, + context: Context, + rawResolutionOptions: Partial = {} +): Promise<{ + programs: Record + importGraph: DirectedGraph +}> => { + const resolutionOptions = { + ...defaultResolutionOptions, + ...rawResolutionOptions + } + const programs: Record = {} + const importGraph = new DirectedGraph() + + // If there is more than one file, tag AST nodes with the source file path. + const numOfFiles = Object.keys(files).length + const shouldAddSourceFileToAST = numOfFiles > 1 + + async function resolve(path: string, node?: es.ModuleDeclarationWithSource) { + let source: string + if (node) { + assert( + typeof node.source.value === 'string', + `${node.type} should have a source of type string, got ${node.source}` + ) + source = node.source.value + } else { + source = path + } + + const [resolved, modAbsPath] = await resolveModule( + node ? path : '.', + source, + p => files[p] !== undefined, + resolutionOptions + ) + + if (!resolved) throw new ModuleNotFoundError(modAbsPath, node) + return modAbsPath + } + + /** + * Process each file (as a module) and determine which other (local and source) + * modules are required. This function should always be called with absolute + * paths + * + * @param currentFilePath Current absolute file path of the module + */ + async function parseFile(currentFilePath: string): Promise { + if (currentFilePath in programs) { + return + } + + const code = files[currentFilePath] + assert( + code !== undefined, + "Module resolver should've thrown an error if the file path did not resolve" + ) + + // Tag AST nodes with the source file path for use in error messages. + const parserOptions: Partial = shouldAddSourceFileToAST + ? { + sourceFile: currentFilePath + } + : {} + const program = parse(code, context, parserOptions, false) + if (!program) { + // Due to a bug in the typed parser where throwOnError isn't respected, + // we need to throw a quick exit error here instead + throw new PreprocessError() + } + + const dependencies = new Set() + programs[currentFilePath] = program + + for (const node of program.body) { + if (!isModuleDeclarationWithSource(node)) continue + + const modAbsPath = await resolve(currentFilePath, node) + if (modAbsPath === currentFilePath) { + throw new CircularImportError([modAbsPath, currentFilePath]) + } + + dependencies.add(modAbsPath) + + // Replace the source of the node with the resolved path + node.source.value = modAbsPath + } + + await Promise.all( + Array.from(dependencies).map(async dependency => { + // There is no need to track Source modules as dependencies, as it can be assumed + // that they will always come first in the topological order + if (!isSourceImport(dependency)) { + await parseFile(dependency) + // If the edge has already been traversed before, the import graph + // must contain a cycle. Then we can exit early and proceed to find the cycle + if (importGraph.hasEdge(dependency, currentFilePath)) { + throw new PreprocessError() + } + + importGraph.addEdge(dependency, currentFilePath) + } + }) + ) + } + + try { + // Remember to resolve the entrypoint file too! + const entrypointAbsPath = await resolve(entrypointFilePath) + await parseFile(entrypointAbsPath) + } catch (error) { + if (!(error instanceof PreprocessError)) { + context.errors.push(error) + } + } + + return { + programs, + importGraph + } +} + +export type PreprocessOptions = { + allowUndefinedImports?: boolean +} & ImportResolutionOptions + +const defaultOptions: Required = { + ...defaultResolutionOptions, + allowUndefinedImports: false +} + +/** + * Preprocesses file imports and returns a transformed Abstract Syntax Tree (AST). + * If an error is encountered at any point, returns `undefined` to signify that an + * error occurred. Details of the error can be found inside `context.errors`. + * + * The preprocessing works by transforming each imported file into a function whose + * parameters are other files (results of transformed functions) and return value + * is a pair where the head is the default export or null, and the tail is a list + * of pairs that map from exported names to identifiers. + * + * See https://github.com/source-academy/js-slang/wiki/Local-Module-Import-&-Export + * for more information. + * + * @param files An object mapping absolute file paths to file content. + * @param entrypointFilePath The absolute path of the entrypoint file. + * @param context The information associated with the program evaluation. + */ +const preprocessFileImports = async ( + files: Partial>, + entrypointFilePath: string, + context: Context, + rawOptions: Partial = {} +): Promise => { + const { allowUndefinedImports, ...resolutionOptions } = { + ...defaultOptions, + ...rawOptions + } + + // Parse all files into ASTs and build the import graph. + const { programs, importGraph } = await parseProgramsAndConstructImportGraph( + files, + entrypointFilePath, + context, + resolutionOptions + ) + + // Return 'undefined' if there are errors while parsing. + if (context.errors.length !== 0) { + return undefined + } + + // Check for circular imports. + const topologicalOrderResult = importGraph.getTopologicalOrder() + if (!topologicalOrderResult.isValidTopologicalOrderFound) { + context.errors.push(new CircularImportError(topologicalOrderResult.firstCycleFound)) + return undefined + } + + let newPrograms: Record = {} + try { + // Based on how the import graph is constructed, it could be the case that the entrypoint + // file is never included in the topo order. This is only an issue for the import export + // analyzer, hence the following code + const fullTopoOrder = topologicalOrderResult.topologicalOrder + if (!fullTopoOrder.includes(entrypointFilePath)) { + // Since it's the entrypoint, it must be loaded last + fullTopoOrder.push(entrypointFilePath) + } + + // This check is performed after cycle detection because if we tried to resolve export symbols + // and there is a cycle in the import graph the constructImportGraph function may end up in an + // infinite loop + // For example, a.js: export * from './b.js' and b.js: export * from './a.js' + // Then trying to discover what symbols are exported by a.js will require determining what symbols + // b.js exports, which would in turn require the symbols exported by a.js + // If the topological order exists, then this is guaranteed not to occur + newPrograms = await analyzeImportsAndExports(programs, fullTopoOrder, allowUndefinedImports) + } catch (error) { + context.errors.push(error) + return undefined + } + + // We want to operate on the entrypoint program to get the eventual + // preprocessed program. + const entrypointProgram = newPrograms[entrypointFilePath] + const entrypointDirPath = pathlib.resolve(entrypointFilePath, '..') + + // Create variables to hold the imported statements. + const entrypointProgramModuleDeclarations = entrypointProgram.body.filter(isModuleDeclaration) + const entrypointProgramInvokedFunctionResultVariableNameToImportSpecifiersMap = + getInvokedFunctionResultVariableNameToImportSpecifiersMap( + entrypointProgramModuleDeclarations, + entrypointDirPath + ) + const entrypointProgramAccessImportStatements = createAccessImportStatements( + entrypointProgramInvokedFunctionResultVariableNameToImportSpecifiersMap + ) + + // Transform all programs into their equivalent function declaration + // except for the entrypoint program. + const [functionDeclarations, invokedFunctionResultVariableDeclarations] = + topologicalOrderResult.topologicalOrder + // The entrypoint program does not need to be transformed into its + // function declaration equivalent as its enclosing environment is + // simply the overall program's (constructed program's) environment. + .filter(path => path !== entrypointFilePath) + .reduce( + ([funcDecls, invokeDecls], filePath) => { + const program = newPrograms[filePath] + const functionDeclaration = transformProgramToFunctionDeclaration(program, filePath) + + const functionName = functionDeclaration.id.name + const invokedFunctionResultVariableName = + transformFunctionNameToInvokedFunctionResultVariableName(functionName) + + const functionParams = functionDeclaration.params.filter(isIdentifier) + assert( + functionParams.length === functionDeclaration.params.length, + 'Function declaration contains non-Identifier AST nodes as params. This should never happen.' + ) + + // Invoke each of the transformed functions and store the result in a variable. + const invokedFunctionResultVariableDeclaration = + createInvokedFunctionResultVariableDeclaration( + functionName, + invokedFunctionResultVariableName, + functionParams + ) + + return [ + [...funcDecls, functionDeclaration], + [...invokeDecls, invokedFunctionResultVariableDeclaration] + ] + }, + [[], []] as [es.FunctionDeclaration[], es.VariableDeclaration[]] + ) + + // Re-assemble the program. + const preprocessedProgram: es.Program = { + ...entrypointProgram, + body: [ + ...functionDeclarations, + ...invokedFunctionResultVariableDeclarations, + ...entrypointProgramAccessImportStatements, + ...entrypointProgram.body + ] + } + + // console.log(generate(preprocessedProgram)) + + // Import and Export related nodes are no longer necessary, so we can remove them from the program entirely + removeImportsAndExports(preprocessedProgram) + + // Finally, we need to hoist all remaining imports to the top of the + // program. These imports should be source module imports since + // non-Source module imports would have already been removed. As part + // of this step, we also merge imports from the same module so as to + // import each unique name per module only once. + hoistAndMergeImports(preprocessedProgram, newPrograms) + return preprocessedProgram +} + +export default preprocessFileImports diff --git a/src/modules/preprocessor/resolver.ts b/src/modules/preprocessor/resolver.ts new file mode 100644 index 000000000..08a42c737 --- /dev/null +++ b/src/modules/preprocessor/resolver.ts @@ -0,0 +1,42 @@ +import * as pathlib from 'path' + +import { isSourceImport } from '../../utils/ast/typeGuards' +import { memoizedGetModuleManifestAsync } from '../moduleLoaderAsync' +import type { ImportResolutionOptions } from '../moduleTypes' + +/** + * Function that returns the full, absolute path to the module being imported + * @param ourPath Path of the current module + * @param source Path to the module being imported + * @param modulePredicate Predicate for checking if the given module exists + * @param options Import resolution options + */ +export default async function resolveModule( + ourPath: string, + source: string, + modulePredicate: (p: string) => boolean, + options: Omit +): Promise<[resolved: boolean, modAbsPath: string]> { + if (isSourceImport(source)) { + const moduleManifest = await memoizedGetModuleManifestAsync() + return [source in moduleManifest, source] + } else { + const modAbsPath = pathlib.resolve(ourPath, '..', source) + if (modulePredicate(modAbsPath)) return [true, modAbsPath] + + if (options.resolveDirectories && modulePredicate(`${modAbsPath}/index`)) { + return [true, `${modAbsPath}/index`] + } + + if (options.resolveExtensions) { + for (const ext of options.resolveExtensions) { + if (modulePredicate(`${modAbsPath}.${ext}`)) return [true, `${modAbsPath}.${ext}`] + + if (options.resolveDirectories && modulePredicate(`${modAbsPath}/index.${ext}`)) { + return [true, `${modAbsPath}/index.${ext}`] + } + } + } + return [false, modAbsPath] + } +} diff --git a/src/modules/preprocessor/transformers/hoistAndMergeImports.ts b/src/modules/preprocessor/transformers/hoistAndMergeImports.ts new file mode 100644 index 000000000..5f0968fa3 --- /dev/null +++ b/src/modules/preprocessor/transformers/hoistAndMergeImports.ts @@ -0,0 +1,123 @@ +import { isImportDeclaration, isSourceImport } from '../../../utils/ast/typeGuards' +import type * as es from '../../../utils/ast/types' +import { + createIdentifier, + createImportDeclaration, + createImportDefaultSpecifier, + createImportNamespaceSpecifier, + createImportSpecifier, + createLiteral +} from '../constructors/baseConstructors' + +/** + * Hoists import declarations to the top of the program & merges duplicate + * imports for the same module. + * + * Note that two modules are the same if and only if their import source + * is the same. This function does not resolve paths against a base + * directory. If such a functionality is required, this function will + * need to be modified. + * + * @param outputProgram The AST which should have its ImportDeclaration nodes + * hoisted & duplicate imports merged. + */ +export default function hoistAndMergeImports( + outputProgram: es.Program, + programs: Record +) { + const importsToSpecifiers = new Map< + string, + { namespaceSymbols: Set; imports: Map> } + >() + + // Now we go over the programs again + Object.values(programs).forEach(program => { + program.body.forEach(node => { + if (!isImportDeclaration(node)) return + + const source = node.source!.value as string + // We no longer need imports from non-source modules, so we can just ignore them + if (!isSourceImport(source)) return + + if (!importsToSpecifiers.has(source)) { + importsToSpecifiers.set(source, { + namespaceSymbols: new Set(), + imports: new Map() + }) + } + const specifierMap = importsToSpecifiers.get(source)! + node.specifiers.forEach(spec => { + let importingName: string + switch (spec.type) { + case 'ImportSpecifier': { + importingName = spec.imported.name + break + } + case 'ImportDefaultSpecifier': { + importingName = 'default' + break + } + case 'ImportNamespaceSpecifier': { + specifierMap.namespaceSymbols.add(spec.local.name) + return + } + } + + if (!specifierMap.imports.has(importingName)) { + specifierMap.imports.set(importingName, new Set()) + } + specifierMap.imports.get(importingName)!.add(spec.local.name) + }) + }) + }) + + // Every distinct source module being imported is given its own ImportDeclaration node + const importDeclarations = Array.from(importsToSpecifiers.entries()).flatMap( + ([moduleName, { imports, namespaceSymbols }]) => { + // Across different modules, the user may choose to alias some of the declarations, so we keep track, + // of all the different aliases used for each unique imported symbol + const specifiers = Array.from(imports.entries()).flatMap(([importedName, aliases]) => { + if (importedName !== 'default') { + return Array.from(aliases).map(alias => + createImportSpecifier(createIdentifier(alias), createIdentifier(importedName)) + ) + } else { + return [] + } + }) + + let output = + specifiers.length > 0 + ? [createImportDeclaration(specifiers, createLiteral(moduleName))] + : [] + if (imports.has('default')) { + // You can't have multiple default specifiers per node, so we need to create + // a new node for each + output = output.concat( + Array.from(imports.get('default')!.values()).map(alias => + createImportDeclaration( + [createImportDefaultSpecifier(createIdentifier(alias))], + createLiteral(moduleName) + ) + ) + ) + } + + if (namespaceSymbols.size > 0) { + // You can't have multiple namespace specifiers per node, so we need to create + // a new node for each + output = output.concat( + Array.from(namespaceSymbols).map(alias => + createImportDeclaration( + [createImportNamespaceSpecifier(createIdentifier(alias))], + createLiteral(moduleName) + ) + ) + ) + } + + return output + } + ) + outputProgram.body = [...importDeclarations, ...outputProgram.body] +} diff --git a/src/modules/preprocessor/transformers/removeImportsAndExports.ts b/src/modules/preprocessor/transformers/removeImportsAndExports.ts new file mode 100644 index 000000000..072304926 --- /dev/null +++ b/src/modules/preprocessor/transformers/removeImportsAndExports.ts @@ -0,0 +1,25 @@ +import type { Program, Statement } from 'estree' + +import { processExportDefaultDeclaration } from '../../../utils/ast/astUtils' + +export default function removeImportsAndExports(program: Program) { + const newBody = program.body.reduce((res, node) => { + switch (node.type) { + case 'ExportDefaultDeclaration': + return processExportDefaultDeclaration(node, { + ClassDeclaration: decl => [...res, decl], + FunctionDeclaration: decl => [...res, decl], + Expression: () => res + }) + case 'ExportNamedDeclaration': + return node.declaration ? [...res, node.declaration] : res + case 'ImportDeclaration': + case 'ExportAllDeclaration': + return res + default: + return [...res, node] + } + }, [] as Statement[]) + + program.body = newBody +} diff --git a/src/localImports/transformers/transformProgramToFunctionDeclaration.ts b/src/modules/preprocessor/transformers/transformProgramToFunctionDeclaration.ts similarity index 58% rename from src/localImports/transformers/transformProgramToFunctionDeclaration.ts rename to src/modules/preprocessor/transformers/transformProgramToFunctionDeclaration.ts index 98a179106..4e542d81e 100644 --- a/src/localImports/transformers/transformProgramToFunctionDeclaration.ts +++ b/src/modules/preprocessor/transformers/transformProgramToFunctionDeclaration.ts @@ -1,8 +1,21 @@ -import es from 'estree' -import * as path from 'path' +import * as pathlib from 'path' -import { defaultExportLookupName } from '../../stdlib/localImport.prelude' import { + accessExportFunctionName, + defaultExportLookupName +} from '../../../stdlib/localImport.prelude' +import assert from '../../../utils/assert' +import { processExportDefaultDeclaration } from '../../../utils/ast/astUtils' +import { + isDeclaration, + isDirective, + isModuleDeclaration, + isSourceImport, + isStatement +} from '../../../utils/ast/typeGuards' +import type * as es from '../../../utils/ast/types' +import { + createCallExpression, createFunctionDeclaration, createIdentifier, createLiteral, @@ -17,32 +30,38 @@ import { transformFilePathToValidFunctionName, transformFunctionNameToInvokedFunctionResultVariableName } from '../filePaths' -import { isDeclaration, isDirective, isModuleDeclaration, isStatement } from '../typeGuards' -import { isSourceModule } from './removeNonSourceModuleImports' - -type ImportSpecifier = es.ImportSpecifier | es.ImportDefaultSpecifier | es.ImportNamespaceSpecifier export const getInvokedFunctionResultVariableNameToImportSpecifiersMap = ( nodes: es.ModuleDeclaration[], currentDirPath: string -): Record => { - const invokedFunctionResultVariableNameToImportSpecifierMap: Record = - {} +): Record => { + const invokedFunctionResultVariableNameToImportSpecifierMap: Record< + string, + (es.ImportSpecifiers | es.ExportSpecifier)[] + > = {} nodes.forEach((node: es.ModuleDeclaration): void => { - // Only ImportDeclaration nodes specify imported names. - if (node.type !== 'ImportDeclaration') { - return - } - const importSource = node.source.value - if (typeof importSource !== 'string') { - throw new Error( - 'Encountered an ImportDeclaration node with a non-string source. This should never occur.' - ) + switch (node.type) { + case 'ExportNamedDeclaration': { + if (!node.source) return + break + } + case 'ImportDeclaration': + break + default: + return } + + const importSource = node.source!.value + assert( + typeof importSource === 'string', + `Encountered an ${node.type} node with a non-string source. This should never occur.` + ) + // Only handle import declarations for non-Source modules. - if (isSourceModule(importSource)) { + if (isSourceImport(importSource)) { return } + // Different import sources can refer to the same file. For example, // both './b.js' & '../dir/b.js' can refer to the same file if the // current file path is '/dir/a.js'. To ensure that every file is @@ -50,13 +69,15 @@ export const getInvokedFunctionResultVariableNameToImportSpecifiersMap = ( // current file path to get the absolute file path of the file to // be imported. Since the absolute file path is guaranteed to be // unique, it is also the canonical file path. - const importFilePath = path.resolve(currentDirPath, importSource) + const importFilePath = pathlib.resolve(currentDirPath, importSource) + // Even though we limit the chars that can appear in Source file // paths, some chars in file paths (such as '/') cannot be used // in function names. As such, we substitute illegal chars with // legal ones in a manner that gives us a bijective mapping from // file paths to function names. const importFunctionName = transformFilePathToValidFunctionName(importFilePath) + // In the top-level environment of the resulting program, for every // imported file, we will end up with two different names; one for // the function declaration, and another for the variable holding @@ -71,6 +92,7 @@ export const getInvokedFunctionResultVariableNameToImportSpecifiersMap = ( // Having the two different names helps us to achieve this objective. const invokedFunctionResultVariableName = transformFunctionNameToInvokedFunctionResultVariableName(importFunctionName) + // If this is the file ImportDeclaration node for the canonical // file path, instantiate the entry in the map. if ( @@ -83,116 +105,82 @@ export const getInvokedFunctionResultVariableNameToImportSpecifiersMap = ( ...node.specifiers ) }) + return invokedFunctionResultVariableNameToImportSpecifierMap } -const getIdentifier = (node: es.Declaration): es.Identifier | null => { - switch (node.type) { - case 'FunctionDeclaration': - if (node.id === null) { - throw new Error( - 'Encountered a FunctionDeclaration node without an identifier. This should have been caught when parsing.' - ) - } - return node.id - case 'VariableDeclaration': - const id = node.declarations[0].id - // In Source, variable names are Identifiers. - if (id.type !== 'Identifier') { - throw new Error(`Expected variable name to be an Identifier, but was ${id.type} instead.`) - } - return id - case 'ClassDeclaration': - throw new Error('Exporting of class is not supported.') - } -} +const getExportExpressions = ( + nodes: es.ModuleDeclaration[], + invokedFunctionResultVariableNameToImportSpecifierMap: Record< + string, + (es.ImportSpecifiers | es.ExportSpecifier)[] + > +) => { + const exportExpressions: Record = {} -const getExportedNameToIdentifierMap = ( - nodes: es.ModuleDeclaration[] -): Record => { - const exportedNameToIdentifierMap: Record = {} - nodes.forEach((node: es.ModuleDeclaration): void => { - // Only ExportNamedDeclaration nodes specify exported names. - if (node.type !== 'ExportNamedDeclaration') { - return - } - if (node.declaration) { - const identifier = getIdentifier(node.declaration) - if (identifier === null) { - return + for (const node of nodes) { + switch (node.type) { + case 'ExportNamedDeclaration': { + if (node.declaration) { + let identifier: es.Identifier + if (node.declaration.type === 'VariableDeclaration') { + const { + declarations: [{ id }] + } = node.declaration + identifier = id as es.Identifier + } else { + identifier = node.declaration.id! + } + exportExpressions[identifier.name] = identifier + } else if (!node.source) { + node.specifiers.forEach(({ exported: { name }, local }) => { + exportExpressions[name] = local + }) + } + break + } + case 'ExportDefaultDeclaration': { + exportExpressions[defaultExportLookupName] = processExportDefaultDeclaration(node, { + ClassDeclaration: ({ id }) => id, + FunctionDeclaration: ({ id }) => id, + Expression: expr => expr + }) + break } - // When an ExportNamedDeclaration node has a declaration, the - // identifier is the same as the exported name (i.e., no renaming). - const exportedName = identifier.name - exportedNameToIdentifierMap[exportedName] = identifier - } else { - // When an ExportNamedDeclaration node does not have a declaration, - // it contains a list of names to export, i.e., export { a, b as c, d };. - // Exported names can be renamed using the 'as' keyword. As such, the - // exported names and their corresponding identifiers might be different. - node.specifiers.forEach((node: es.ExportSpecifier): void => { - const exportedName = node.exported.name - const identifier = node.local - exportedNameToIdentifierMap[exportedName] = identifier - }) } - }) - return exportedNameToIdentifierMap -} + } -const getDefaultExportExpression = ( - nodes: es.ModuleDeclaration[], - exportedNameToIdentifierMap: Partial> -): es.Expression | null => { - let defaultExport: es.Expression | null = null + for (const [source, nodes] of Object.entries( + invokedFunctionResultVariableNameToImportSpecifierMap + )) { + for (const node of nodes) { + if (node.type !== 'ExportSpecifier') continue - // Handle default exports which are parsed as ExportNamedDeclaration AST nodes. - // 'export { name as default };' is equivalent to 'export default name;' but - // is represented by an ExportNamedDeclaration node instead of an - // ExportedDefaultDeclaration node. - // - // NOTE: If there is a named export representing the default export, its entry - // in the map must be removed to prevent it from being treated as a named export. - if (exportedNameToIdentifierMap['default'] !== undefined) { - defaultExport = exportedNameToIdentifierMap['default'] - delete exportedNameToIdentifierMap['default'] + const { + exported: { name: exportName }, + local: { name: localName } + } = node + exportExpressions[exportName] = createCallExpression(accessExportFunctionName, [ + createIdentifier(source), + createLiteral(localName) + ]) + } } - nodes.forEach((node: es.ModuleDeclaration): void => { - // Only ExportDefaultDeclaration nodes specify the default export. - if (node.type !== 'ExportDefaultDeclaration') { - return - } - if (defaultExport !== null) { - // This should never occur because multiple default exports should have - // been caught by the Acorn parser when parsing into an AST. - throw new Error('Encountered multiple default exports!') - } - if (isDeclaration(node.declaration)) { - const identifier = getIdentifier(node.declaration) - if (identifier === null) { - return - } - // When an ExportDefaultDeclaration node has a declaration, the - // identifier is the same as the exported name (i.e., no renaming). - defaultExport = identifier - } else { - // When an ExportDefaultDeclaration node does not have a declaration, - // it has an expression. - defaultExport = node.declaration - } - }) - return defaultExport + return exportExpressions } export const createAccessImportStatements = ( - invokedFunctionResultVariableNameToImportSpecifiersMap: Record + invokedFunctionResultVariableNameToImportSpecifiersMap: Record< + string, + (es.ImportSpecifiers | es.ExportSpecifier)[] + > ): es.VariableDeclaration[] => { const importDeclarations: es.VariableDeclaration[] = [] for (const [invokedFunctionResultVariableName, importSpecifiers] of Object.entries( invokedFunctionResultVariableNameToImportSpecifiersMap )) { - importSpecifiers.forEach((importSpecifier: ImportSpecifier): void => { + importSpecifiers.forEach(importSpecifier => { let importDeclaration switch (importSpecifier.type) { case 'ImportSpecifier': @@ -212,6 +200,8 @@ export const createAccessImportStatements = ( case 'ImportNamespaceSpecifier': // In order to support namespace imports, Source would need to first support objects. throw new Error('Namespace imports are not supported.') + case 'ExportSpecifier': + return } importDeclarations.push(importDeclaration) }) @@ -220,12 +210,12 @@ export const createAccessImportStatements = ( } const createReturnListArguments = ( - exportedNameToIdentifierMap: Record + exportedNameToIdentifierMap: Record ): Array => { return Object.entries(exportedNameToIdentifierMap).map( - ([exportedName, identifier]: [string, es.Identifier]): es.SimpleCallExpression => { + ([exportedName, expr]: [string, es.Identifier]): es.SimpleCallExpression => { const head = createLiteral(exportedName) - const tail = identifier + const tail = expr return createPairCallExpression(head, tail) } ) @@ -285,29 +275,26 @@ const removeModuleDeclarations = ( export const transformProgramToFunctionDeclaration = ( program: es.Program, currentFilePath: string -): es.FunctionDeclaration => { +): es.FunctionDeclarationWithId => { const moduleDeclarations = program.body.filter(isModuleDeclaration) - const currentDirPath = path.resolve(currentFilePath, '..') + const currentDirPath = pathlib.resolve(currentFilePath, '..') // Create variables to hold the imported statements. const invokedFunctionResultVariableNameToImportSpecifiersMap = getInvokedFunctionResultVariableNameToImportSpecifiersMap(moduleDeclarations, currentDirPath) + const accessImportStatements = createAccessImportStatements( invokedFunctionResultVariableNameToImportSpecifiersMap ) // Create the return value of all exports for the function. - const exportedNameToIdentifierMap = getExportedNameToIdentifierMap(moduleDeclarations) - const defaultExportExpression = getDefaultExportExpression( + const { [defaultExportLookupName]: defaultExport, ...exportExpressions } = getExportExpressions( moduleDeclarations, - exportedNameToIdentifierMap - ) - const defaultExport = defaultExportExpression ?? createLiteral(null) - const namedExports = createListCallExpression( - createReturnListArguments(exportedNameToIdentifierMap) + invokedFunctionResultVariableNameToImportSpecifiersMap ) + const namedExports = createListCallExpression(createReturnListArguments(exportExpressions)) const returnStatement = createReturnStatement( - createPairCallExpression(defaultExport, namedExports) + createPairCallExpression(defaultExport ?? createLiteral(null), namedExports) ) // Assemble the function body. diff --git a/src/modules/utils.ts b/src/modules/utils.ts new file mode 100644 index 000000000..0da46ebf9 --- /dev/null +++ b/src/modules/utils.ts @@ -0,0 +1,183 @@ +import type { ImportDeclaration, Node } from 'estree' + +import type { Context } from '..' +import assert from '../utils/assert' +import { getUniqueId } from '../utils/uniqueIds' +import { loadModuleTabs } from './moduleLoader' +import { loadModuleTabsAsync } from './moduleLoaderAsync' + +/** + * Create the module's context and load its tabs (if `loadTabs` is true) + */ +export async function initModuleContext( + moduleName: string, + context: Context, + loadTabs: boolean, + node?: Node +) { + if (!(moduleName in context.moduleContexts)) { + context.moduleContexts[moduleName] = { + state: null, + tabs: loadTabs ? loadModuleTabs(moduleName, node) : null + } + } else if (context.moduleContexts[moduleName].tabs === null && loadTabs) { + context.moduleContexts[moduleName].tabs = loadModuleTabs(moduleName, node) + } +} + +/** + * Create the module's context and load its tabs (if `loadTabs` is true) + */ +export async function initModuleContextAsync( + moduleName: string, + context: Context, + loadTabs: boolean, + node?: Node +) { + if (!(moduleName in context.moduleContexts)) { + context.moduleContexts[moduleName] = { + state: null, + tabs: loadTabs ? await loadModuleTabsAsync(moduleName, node) : null + } + } else if (context.moduleContexts[moduleName].tabs === null && loadTabs) { + context.moduleContexts[moduleName].tabs = await loadModuleTabsAsync(moduleName, node) + } +} + +/** + * Represents a loaded Source module + */ +export type ModuleInfo = { + /** + * `ImportDeclarations` that import from this module. + */ + nodes: ImportDeclaration[] + + /** + * Represents the loaded module. It can be the module's functions itself (see the ec-evaluator), + * or just the module text (see the transpiler), or any other type. + * + * This field should not be null when the function returns. + */ + content: T + + /** + * The unique name given to this module. If `usedIdentifiers` is not provided, this field will be `null`. + */ + namespaced: string | null +} + +/** + * Function that converts an `ImportSpecifier` into the given Transformed type. + * It can be used as a `void` returning function as well, in case the specifiers + * don't need to be transformed, just acted upon. + * @example + * ImportSpecifier(specifier, node, info) => { + * return create.constantDeclaration( + * spec.local.name, + * create.memberExpression( + * create.identifier(info.namespaced), + * spec.imported.name + * ), + * ) + * } + */ +export type SpecifierProcessor = ( + spec: ImportDeclaration['specifiers'][0], + moduleInfo: ModuleInfo, + node: ImportDeclaration +) => Transformed + +export type ImportSpecifierType = + | 'ImportSpecifier' + | 'ImportDefaultSpecifier' + | 'ImportNamespaceSpecifier' + +/** + * This function is intended to unify how each of the different Source runners load imports. It handles + * namespacing (if `usedIdentifiers` is provided), loading the module's context (if `context` is not `null`), + * loading the module's tabs (if `loadTabs` is given as `true`) and the conversion + * of import specifiers to the relevant type used by the runner. + * @param nodes Nodes to transform + * @param context Context to transform with, or `null`. Setting this to null prevents module contexts and tabs from being loaded. + * @param loadTabs Set this to false to prevent tabs from being loaded even if a context is provided. + * @param moduleLoader Function that takes the name of the module and returns its loaded representation. + * @param processors Functions for working with each type of import specifier. + * @param usedIdentifiers Set containing identifiers already used in code. If null, namespacing is not conducted. + * @returns The loaded modules, along with the transformed versions of the given nodes + */ +export async function transformImportNodesAsync( + nodes: ImportDeclaration[], + context: Context | null, + loadTabs: boolean, + moduleLoader: (name: string, node?: Node) => Promise, + processors: Record>, + usedIdentifiers?: Set +) { + const internalLoader = async (name: string, node?: Node) => { + // Make sure that module contexts are initialized before + // loading the bundles + if (context) { + await initModuleContextAsync(name, context, loadTabs, node) + } + + return moduleLoader(name, node) + } + + const promises: Promise[] = [] + const moduleInfos = nodes.reduce((res, node) => { + const moduleName = node.source.value + assert( + typeof moduleName === 'string', + `Expected ImportDeclaration to have a source of type string, got ${moduleName}` + ) + + if (!(moduleName in res)) { + // First time we are loading this module + res[moduleName] = { + nodes: [], + content: null as any, + namespaced: null + } + const loadPromise = internalLoader(moduleName, node).then(content => { + res[moduleName].content = content + }) + + promises.push(loadPromise) + } + + res[moduleName].nodes.push(node) + + // Collate all the identifiers introduced by specifiers to prevent collisions when + // the import declaration has aliases, e.g import { show as __MODULE__ } from 'rune'; + if (usedIdentifiers) { + node.specifiers.forEach(spec => usedIdentifiers.add(spec.local.name)) + } + return res + }, {} as Record>) + + // Wait for all module and symbol loading to finish + await Promise.all(promises) + + return Object.entries(moduleInfos).reduce((res, [moduleName, info]) => { + // Now for each module, we give it a unique namespaced id + const namespaced = usedIdentifiers ? getUniqueId(usedIdentifiers, '__MODULE__') : null + info.namespaced = namespaced + + assert(info.content !== null, `${moduleName} was not loaded properly. This should never happen`) + + return { + ...res, + [moduleName]: { + content: info.nodes.flatMap(node => + node.specifiers.flatMap(spec => { + // Finally, transform that specifier into the form needed + // by the runner + return processors[spec.type](spec, info, node) + }) + ), + info + } + } + }, {} as Record; content: Transformed[] }>) +} diff --git a/src/name-extractor/__tests__/autocomplete.ts b/src/name-extractor/__tests__/autocomplete.ts index f0b8eaf00..17170bf96 100644 --- a/src/name-extractor/__tests__/autocomplete.ts +++ b/src/name-extractor/__tests__/autocomplete.ts @@ -14,7 +14,8 @@ test('Test empty program does not generate names', async () => { }) test('Test simple extraction of constant and variable names', async () => { - const code: string = '\ + const code: string = + '\ const foo1 = 1;\n\ let foo2 = 2;\n\ f\ @@ -176,7 +177,8 @@ test('Test accessing parameter names inside function', async () => { // For-loops test('Test accessing local block in for-loop parameter', async () => { - const code: string = '\ + const code: string = + '\ let bar = 1;\n\ let baz = 2;\n\ for (b) {\ @@ -192,7 +194,8 @@ test('Test accessing local block in for-loop parameter', async () => { }) test('Test accessing for-loop parameter in for-loop body', async () => { - const code: string = '\ + const code: string = + '\ for (let foo=10;) {\n\ f\n\ }\ @@ -205,7 +208,8 @@ test('Test accessing for-loop parameter in for-loop body', async () => { }) test('Test that for-loop local variable cannot be accessed outside loop', async () => { - const code: string = '\ + const code: string = + '\ for (let x=1; x<10; x=x+1) {\n\ let foo = x;\n\ }\n\ @@ -221,7 +225,8 @@ test('Test that for-loop local variable cannot be accessed outside loop', async // While-loops test('Test accessing local block in while-loop parameter', async () => { - const code: string = '\ + const code: string = + '\ let bar = 1;\n\ let baz = 2;\n\ while (b) {\ @@ -237,7 +242,8 @@ test('Test accessing local block in while-loop parameter', async () => { }) test('Test that while-loop local variable cannot be accessed outside loop', async () => { - const code: string = '\ + const code: string = + '\ while (let x=1; x<10; x=x+1) {\n\ let foo = x;\n\ }\n\ @@ -253,7 +259,8 @@ test('Test that while-loop local variable cannot be accessed outside loop', asyn // Conditionals test('Test accessing local block in if-else parameter', async () => { - const code: string = '\ + const code: string = + '\ let bar = 1;\n\ let baz = 2;\n\ if (b) {\ @@ -269,7 +276,8 @@ test('Test accessing local block in if-else parameter', async () => { }) test('Test that local variable in if-block cannot be accessed in else-block', async () => { - const code: string = '\ + const code: string = + '\ if (true) {\n\ let foo = x;\n\ } else {\n\ @@ -323,7 +331,8 @@ test('Test that variable in if cannot be accessed outside if-statement', async ( // Blocks test('Test that declaration in blocks cannot be accessed outside block', async () => { - const code: string = '\ + const code: string = + '\ {\n\ let foo = 1;\n\ }\n\ @@ -337,7 +346,8 @@ test('Test that declaration in blocks cannot be accessed outside block', async ( }) test('Test that declaration outside blocks can be accessed inside block', async () => { - const code: string = '\ + const code: string = + '\ let bar = 1;\n\ {\n\ let baz = 1;\n\ @@ -397,7 +407,8 @@ test('Test that declaration inside anonymous functions can be accessed in body', }) test('Test that declaration inside anonymous functions cannot be accessed outside', async () => { - const code: string = '\ + const code: string = + '\ let foo = (bar1, bar2) => { \n\ let baz = 1;\n\ }\n\ @@ -434,7 +445,8 @@ test('Test that local and global variables are available in return statements', // Declarations test('Test that no prompts are returned when user is declaring variable', async () => { - const code: string = '\ + const code: string = + '\ let bar = 1;\n\ let b\n\ ' diff --git a/src/name-extractor/index.ts b/src/name-extractor/index.ts index 14656b8ae..f48338907 100644 --- a/src/name-extractor/index.ts +++ b/src/name-extractor/index.ts @@ -1,11 +1,18 @@ -import * as es from 'estree' +import type * as es from 'estree' -import { Context } from '../' +import type { Context } from '../' import { UNKNOWN_LOCATION } from '../constants' -import { ModuleConnectionError, ModuleNotFoundError } from '../errors/moduleErrors' import { findAncestors, findIdentifierNode } from '../finder' -import { memoizedloadModuleDocs } from '../modules/moduleLoader' +import { ModuleConnectionError, ModuleNotFoundError } from '../modules/errors' +import { memoizedGetModuleDocsAsync } from '../modules/moduleLoaderAsync' import syntaxBlacklist from '../parser/source/syntax' +import { + isDeclaration, + isFunctionNode, + isImportDeclaration, + isLoop, + isSourceImport +} from '../utils/ast/typeGuards' export interface NameDeclaration { name: string @@ -19,26 +26,6 @@ const KIND_FUNCTION = 'func' const KIND_PARAM = 'param' const KIND_CONST = 'const' -function isImportDeclaration(node: es.Node): boolean { - return node.type === 'ImportDeclaration' -} - -function isDeclaration(node: es.Node): boolean { - return node.type === 'VariableDeclaration' || node.type === 'FunctionDeclaration' -} - -function isFunction(node: es.Node): boolean { - return ( - node.type === 'FunctionDeclaration' || - node.type === 'FunctionExpression' || - node.type === 'ArrowFunctionExpression' - ) -} - -function isLoop(node: es.Node): boolean { - return node.type === 'WhileStatement' || node.type === 'ForStatement' -} - // Update this to use exported check from "acorn-loose" package when it is released function isDummyName(name: string): boolean { return name === '✖' @@ -116,7 +103,7 @@ export function getKeywords( ) { addAllowedKeywords(keywordsInBlock) // Keywords only allowed in functions - if (ancestors.some(node => isFunction(node))) { + if (ancestors.some(node => isFunctionNode(node))) { addAllowedKeywords(keywordsInFunction) } @@ -139,11 +126,11 @@ export function getKeywords( * @returns Tuple consisting of the list of suggestions, and a boolean value indicating if * suggestions should be displayed, i.e. `[suggestions, shouldPrompt]` */ -export function getProgramNames( +export async function getProgramNames( prog: es.Node, comments: acorn.Comment[], cursorLoc: es.Position -): [NameDeclaration[], boolean] { +): Promise<[NameDeclaration[], boolean]> { function before(first: es.Position, second: es.Position) { return first.line < second.line || (first.line === second.line && first.column <= second.column) } @@ -170,7 +157,7 @@ export function getProgramNames( // Workaround due to minification problem // tslint:disable-next-line const node = queue.shift()! - if (isFunction(node)) { + if (isFunctionNode(node)) { // This is the only time we want raw identifiers nameQueue.push(...(node as any).params) } @@ -198,13 +185,22 @@ export function getProgramNames( } } - const res: any = {} - nameQueue - .map(node => getNames(node, n => cursorInLoc(n.loc))) - .reduce((prev, cur) => prev.concat(cur), []) // no flatmap feelsbad - .forEach((decl, idx) => { - res[decl.name] = { ...decl, score: idx } - }) // Deduplicate, ensure deeper declarations overwrite + const names = await Promise.all(nameQueue.map(node => getNames(node, n => cursorInLoc(n.loc)))) + const res = names.flat().reduce( + (prev, each, idx) => ({ + ...prev, + [each.name]: { ...each, score: idx } // Deduplicate, ensure deeper declarations overwrite + }), + {} as Record + ) + + // const res: any = {} + // nameQueue + // .map(node => getNames(node, n => cursorInLoc(n.loc))) + // .reduce((prev, cur) => prev.concat(cur), []) // no flatmap feelsbad + // .forEach((decl, idx) => { + // res[decl.name] = { ...decl, score: idx } + // }) return [Object.values(res), true] } @@ -305,44 +301,79 @@ function cursorInIdentifier(node: es.Node, locTest: (node: es.Node) => boolean): * is located within the node, false otherwise * @returns List of found names */ -function getNames(node: es.Node, locTest: (node: es.Node) => boolean): NameDeclaration[] { +async function getNames( + node: es.Node, + locTest: (node: es.Node) => boolean +): Promise { + const createDocHtml = (header: string, desc: string) => + `

${header}

${desc}
` + switch (node.type) { case 'ImportDeclaration': + const source = node.source.value as string + if (!isSourceImport(source)) { + return node.specifiers.map(({ local: { name } }) => ({ + name, + meta: KIND_IMPORT, + docHTML: createDocHtml( + `Imported symbol ${name} from ${source}`, + `No documentation available for ${name} from ${source}` + ) + })) + } + const specs = node.specifiers.filter(x => !isDummyName(x.local.name)) try { - const docs = memoizedloadModuleDocs(node.source.value as string, node) + const docs = await memoizedGetModuleDocsAsync(source) if (!docs) { - return specs.map(spec => ({ - name: spec.local.name, + return specs.map(({ local: { name } }) => ({ + name, meta: KIND_IMPORT, - docHTML: `Unable to retrieve documentation for ${spec.local.name} from ${node.source.value} module` + docHTML: `Unable to retrieve documentation for ${name} from ${source} module` })) } return specs.map(spec => { - if (spec.type !== 'ImportSpecifier' || docs[spec.local.name] === undefined) { - return { - name: spec.local.name, - meta: KIND_IMPORT, - docHTML: `No documentation available for ${spec.local.name} from ${node.source.value} module` - } - } else { + const localName = spec.local.name + + if (docs[spec.local.name] === undefined) { return { name: spec.local.name, meta: KIND_IMPORT, - docHTML: docs[spec.local.name] + docHTML: `No documentation available for ${localName} from ${source} module` } } + + switch (spec.type) { + case 'ImportSpecifier': + return { + name: localName, + meta: KIND_IMPORT, + docHTML: docs[spec.imported.name] + } + case 'ImportDefaultSpecifier': + return { + name: localName, + meta: KIND_IMPORT, + docHTML: docs['default'] + } + case 'ImportNamespaceSpecifier': + return { + name: localName, + meta: KIND_IMPORT, + docHTML: `${source} module namespace import` + } + } }) } catch (err) { if (!(err instanceof ModuleNotFoundError || err instanceof ModuleConnectionError)) throw err - return specs.map(spec => ({ - name: spec.local.name, + return specs.map(({ local: { name } }) => ({ + name, meta: KIND_IMPORT, - docHTML: `Unable to retrieve documentation for ${spec.local.name} from ${node.source.value} module` + docHTML: `Unable to retrieve documentation for ${name} from ${source} module` })) } case 'VariableDeclaration': @@ -353,12 +384,12 @@ function getNames(node: es.Node, locTest: (node: es.Node) => boolean): NameDecla if ( !name || isDummyName(name) || - (decl.init && !isFunction(decl.init) && locTest(decl.init)) // Avoid suggesting `let foo = foo`, but suggest recursion with arrow functions + (decl.init && !isFunctionNode(decl.init) && locTest(decl.init)) // Avoid suggesting `let foo = foo`, but suggest recursion with arrow functions ) { continue } - if (node.kind === KIND_CONST && decl.init && isFunction(decl.init)) { + if (node.kind === KIND_CONST && decl.init && isFunctionNode(decl.init)) { // constant initialized with arrow function will always be a function declarations.push({ name, meta: KIND_FUNCTION }) } else { diff --git a/src/parser/__tests__/__snapshots__/allowed-syntax.ts.snap b/src/parser/__tests__/__snapshots__/allowed-syntax.ts.snap index bfc5a3d5e..eaadcf2ea 100644 --- a/src/parser/__tests__/__snapshots__/allowed-syntax.ts.snap +++ b/src/parser/__tests__/__snapshots__/allowed-syntax.ts.snap @@ -4290,7 +4290,7 @@ x[key] = 3;", exports[`Syntaxes are allowed in the chapter they are introduced 35: fails a chapter below 1`] = ` Object { "alertResult": Array [], - "code": "import defaultExport from \\"module-name\\";", + "code": "import defaultExport from \\"one_module\\";", "displayResult": Array [], "numErrors": 1, "parsedErrors": "Line 1: Import default specifiers are not allowed", @@ -4303,7 +4303,7 @@ Object { exports[`Syntaxes are allowed in the chapter they are introduced 35: parse passes 1`] = ` Object { "alertResult": Array [], - "code": "parse(\\"import defaultExport from \\\\\\"module-name\\\\\\";\\");", + "code": "parse(\\"import defaultExport from \\\\\\"one_module\\\\\\";\\");", "displayResult": Array [], "numErrors": 0, "parsedErrors": "", @@ -4318,7 +4318,7 @@ Object { null, ], Array [ - "module-name", + "one_module", null, ], ], @@ -4331,7 +4331,7 @@ Object { exports[`Syntaxes are allowed in the chapter they are introduced 35: passes 1`] = ` Object { "alertResult": Array [], - "code": "import defaultExport from \\"module-name\\";", + "code": "import defaultExport from \\"one_module\\";", "displayResult": Array [], "numErrors": 0, "parsedErrors": "", @@ -4451,6 +4451,100 @@ Object { } `; +exports[`Syntaxes are allowed in the chapter they are introduced 36: parse passes 2`] = ` +Object { + "alertResult": Array [], + "code": "parse(\\"export default function f(x) {\\\\n return x;\\\\n}\\\\nf(5);\\");", + "displayResult": Array [], + "numErrors": 0, + "parsedErrors": "", + "result": Array [ + "sequence", + Array [ + Array [ + Array [ + "export_default_declaration", + Array [ + Array [ + "function_declaration", + Array [ + Array [ + "name", + Array [ + "f", + null, + ], + ], + Array [ + Array [ + Array [ + "name", + Array [ + "x", + null, + ], + ], + null, + ], + Array [ + Array [ + "return_statement", + Array [ + Array [ + "name", + Array [ + "x", + null, + ], + ], + null, + ], + ], + null, + ], + ], + ], + ], + null, + ], + ], + Array [ + Array [ + "application", + Array [ + Array [ + "name", + Array [ + "f", + null, + ], + ], + Array [ + Array [ + Array [ + "literal", + Array [ + 5, + null, + ], + ], + null, + ], + null, + ], + ], + ], + null, + ], + ], + null, + ], + ], + "resultStatus": "finished", + "visualiseListResult": Array [], +} +`; + exports[`Syntaxes are allowed in the chapter they are introduced 36: passes 1`] = ` Object { "alertResult": Array [], diff --git a/src/parser/__tests__/allowed-syntax.ts b/src/parser/__tests__/allowed-syntax.ts index b94cc193d..5b585300c 100644 --- a/src/parser/__tests__/allowed-syntax.ts +++ b/src/parser/__tests__/allowed-syntax.ts @@ -2,6 +2,9 @@ import { Chapter } from '../../types' import { stripIndent } from '../../utils/formatters' import { snapshotFailure, snapshotSuccess } from '../../utils/testing' +jest.mock('../../modules/moduleLoaderAsync') +jest.mock('../../modules/moduleLoader') + test.each([ [Chapter.SOURCE_1, ''], @@ -294,7 +297,7 @@ test.each([ [ Chapter.LIBRARY_PARSER, ` - import defaultExport from "module-name"; + import defaultExport from "one_module"; ` ], @@ -340,15 +343,20 @@ test.each([ (chapter: Chapter, snippet: string, skipSuccessTests: boolean = false) => { snippet = stripIndent(snippet) const parseSnippet = `parse(${JSON.stringify(snippet)});` - const tests = [] + + const tests: Promise[] = [] if (!skipSuccessTests) { tests.push( - snapshotSuccess(snippet, { chapter, native: chapter !== Chapter.LIBRARY_PARSER }, 'passes') + snapshotSuccess( + snippet, + { chapter, native: chapter !== Chapter.LIBRARY_PARSER, allowUndefinedImports: true }, + 'passes' + ) ) tests.push( snapshotSuccess( parseSnippet, - { chapter: Math.max(4, chapter), native: true }, + { chapter: Math.max(4, chapter), native: true, allowUndefinedImports: true }, 'parse passes' ) ) diff --git a/src/parser/__tests__/scheme-encode-decode.ts b/src/parser/__tests__/scheme-encode-decode.ts index a458f0bac..b26b1e70e 100644 --- a/src/parser/__tests__/scheme-encode-decode.ts +++ b/src/parser/__tests__/scheme-encode-decode.ts @@ -2,7 +2,7 @@ import { Node } from 'estree' import { UnassignedVariable } from '../../errors/errors' import { decode, encode } from '../../scm-slang/src' -import { dummyExpression } from '../../utils/dummyAstCreator' +import { dummyExpression } from '../../utils/ast/dummyAstCreator' import { decodeError, decodeValue } from '../scheme' describe('Scheme encoder and decoder', () => { diff --git a/src/parser/source/index.ts b/src/parser/source/index.ts index 5ccf8c1d8..e8e0d6fa7 100644 --- a/src/parser/source/index.ts +++ b/src/parser/source/index.ts @@ -2,8 +2,8 @@ import { parse as acornParse, Token, tokenizer } from 'acorn' import { Node as ESNode, Program } from 'estree' import { DEFAULT_ECMA_VERSION } from '../../constants' -import { Chapter, Context, Rule, SourceError, Variant } from '../../types' -import { ancestor, AncestorWalkerFn } from '../../utils/walkers' +import { Chapter, Context, Rule, Variant } from '../../types' +import { ancestor, AncestorWalkerFn } from '../../utils/ast/walkers' import { DisallowedConstructError, FatalSyntaxError } from '../errors' import { AcornOptions, Parser } from '../types' import { createAcornParserOptions, positionToSourceLocation } from '../utils' @@ -82,7 +82,7 @@ export class SourceParser implements Parser { _state: any, ancestors: ESNode[] ) => { - const errors: SourceError[] = checker(node, ancestors) + const errors = checker(node, ancestors) if (throwOnError && errors.length > 0) throw errors[0] errors.forEach(e => context.errors.push(e)) @@ -98,7 +98,7 @@ export class SourceParser implements Parser { }) ancestor(ast as ESNode, mapToObj(validationWalkers), undefined, undefined) - return context.errors.length == 0 + return context.errors.length === 0 } toString(): string { diff --git a/src/parser/source/rules/index.ts b/src/parser/source/rules/index.ts index ab04cd6f1..cc392bc43 100644 --- a/src/parser/source/rules/index.ts +++ b/src/parser/source/rules/index.ts @@ -16,6 +16,7 @@ import noImplicitDeclareUndefined from './noImplicitDeclareUndefined' import noImplicitReturnUndefined from './noImplicitReturnUndefined' import noImportSpecifierWithDefault from './noImportSpecifierWithDefault' import noNull from './noNull' +import noReexportDeclaration from './noReexportDeclaration' import noSpreadInArray from './noSpreadInArray' import noTemplateExpression from './noTemplateExpression' import noTypeofOperator from './noTypeofOperator' @@ -33,6 +34,7 @@ const rules: Rule[] = [ noDeclareMutable, noDotAbbreviation, noExportNamedDeclarationWithDefault, + noReexportDeclaration, noFunctionDeclarationWithoutIdentifier, noIfWithoutElse, noImportSpecifierWithDefault, diff --git a/src/parser/source/rules/noReexportDeclaration.ts b/src/parser/source/rules/noReexportDeclaration.ts new file mode 100644 index 000000000..014c2203d --- /dev/null +++ b/src/parser/source/rules/noReexportDeclaration.ts @@ -0,0 +1,42 @@ +import { UNKNOWN_LOCATION } from '../../../constants' +import { Chapter, ErrorSeverity, ErrorType, Rule, SourceError } from '../../../types' +import { isExportNamedDeclarationWithSource } from '../../../utils/ast/typeGuards' +import type { + ExportNamedDeclaration, + ExportNamedDeclarationWithSource +} from '../../../utils/ast/types' + +export class NoReexportDeclaration implements SourceError { + public type = ErrorType.SYNTAX + public severity: ErrorSeverity.ERROR + + constructor(public readonly node?: ExportNamedDeclarationWithSource) {} + + get location() { + return this.node?.loc ?? UNKNOWN_LOCATION + } + + public explain(): string { + return "Export statements of the form export { x } from 'module' are not allowed" + } + + public elaborate(): string { + return this.explain() + } +} + +const noReexportDeclaration: Rule = { + name: 'no-reexport-declaration', + disableFromChapter: Chapter.FULL_JS, + checkers: { + ExportNamedDeclaration(node: ExportNamedDeclaration) { + if (isExportNamedDeclarationWithSource(node)) { + return [new NoReexportDeclaration(node)] + } + + return [] + } + } +} + +export default noReexportDeclaration diff --git a/src/repl/transpiler.ts b/src/repl/transpiler.ts index db7d3906b..bd441a00e 100644 --- a/src/repl/transpiler.ts +++ b/src/repl/transpiler.ts @@ -11,7 +11,7 @@ import { transpile } from '../transpiler/transpiler' import { Chapter, Variant } from '../types' import { validateAndAnnotate } from '../validator/validator' -function transpileCode( +async function transpileCode( chapter: Chapter = Chapter.SOURCE_1, variant: Variant = Variant.DEFAULT, code = '', @@ -35,7 +35,7 @@ function transpileCode( if (pretranspile) { return generate(program) } else { - return transpile(program as Program, context).transpiled + return (await transpile(program as Program, context)).transpiled } } @@ -90,8 +90,8 @@ function main() { }) process.stdin.on('end', () => { const code = Buffer.concat(chunks).toString('utf-8') - const transpiled = transpileCode(chapter, variant, code, pretranspile) - process.stdout.write(transpiled) + transpileCode(chapter, variant, code, pretranspile).then(data => process.stdout.write(data)) + // process.stdout.write(transpiled) }) } diff --git a/src/runner/__tests__/files.ts b/src/runner/__tests__/files.ts index df213f7d0..9a64d7b86 100644 --- a/src/runner/__tests__/files.ts +++ b/src/runner/__tests__/files.ts @@ -60,15 +60,15 @@ describe('runFilesInContext', () => { it('returns CannotFindModuleError if entrypoint file does not exist', () => { const files: Record = {} runFilesInContext(files, '/a.js', context) - expect(parseError(context.errors)).toMatchInlineSnapshot(`"Cannot find module '/a.js'."`) + expect(parseError(context.errors)).toMatchInlineSnapshot(`"Module '/a.js' not found."`) }) it('returns CannotFindModuleError if entrypoint file does not exist - verbose', () => { const files: Record = {} runFilesInContext(files, '/a.js', context) expect(parseError(context.errors, true)).toMatchInlineSnapshot(` - "Cannot find module '/a.js'. - Check that the module file path resolves to an existing file. + "Module '/a.js' not found. + You should check your import declarations, and ensure that all are valid modules. " `) }) @@ -132,15 +132,15 @@ describe('compileFiles', () => { it('returns CannotFindModuleError if entrypoint file does not exist', () => { const files: Record = {} compileFiles(files, '/a.js', context) - expect(parseError(context.errors)).toMatchInlineSnapshot(`"Cannot find module '/a.js'."`) + expect(parseError(context.errors)).toMatchInlineSnapshot(`"Module '/a.js' not found."`) }) it('returns CannotFindModuleError if entrypoint file does not exist - verbose', () => { const files: Record = {} compileFiles(files, '/a.js', context) expect(parseError(context.errors, true)).toMatchInlineSnapshot(` - "Cannot find module '/a.js'. - Check that the module file path resolves to an existing file. + "Module '/a.js' not found. + You should check your import declarations, and ensure that all are valid modules. " `) }) diff --git a/src/runner/__tests__/runners.ts b/src/runner/__tests__/runners.ts index 50f1fa3d4..785c900c2 100644 --- a/src/runner/__tests__/runners.ts +++ b/src/runner/__tests__/runners.ts @@ -3,10 +3,13 @@ import { UndefinedVariable } from '../../errors/errors' import { mockContext } from '../../mocks/context' import { FatalSyntaxError } from '../../parser/errors' import { Chapter, Finished, Variant } from '../../types' -import { locationDummyNode } from '../../utils/astCreator' +import { locationDummyNode } from '../../utils/ast/astCreator' import { CodeSnippetTestCase } from '../../utils/testing' import { htmlErrorHandlingScript } from '../htmlRunner' +jest.mock('../../modules/moduleLoader') +jest.mock('../../modules/moduleLoaderAsync') + const JAVASCRIPT_CODE_SNIPPETS_NO_ERRORS: CodeSnippetTestCase[] = [ { name: 'LITERAL OBJECT', diff --git a/src/runner/errors.ts b/src/runner/errors.ts index 4f5b2308c..6c1844582 100644 --- a/src/runner/errors.ts +++ b/src/runner/errors.ts @@ -3,7 +3,7 @@ import { NullableMappedPosition, RawSourceMap, SourceMapConsumer } from 'source- import { UNKNOWN_LOCATION } from '../constants' import { ConstAssignment, ExceptionError, UndefinedVariable } from '../errors/errors' import { SourceError } from '../types' -import { locationDummyNode } from '../utils/astCreator' +import { locationDummyNode } from '../utils/ast/astCreator' enum BrowserType { Chrome = 'Chrome', diff --git a/src/runner/fullJSRunner.ts b/src/runner/fullJSRunner.ts index ea9ad0d16..670037360 100644 --- a/src/runner/fullJSRunner.ts +++ b/src/runner/fullJSRunner.ts @@ -1,20 +1,19 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { generate } from 'astring' -import * as es from 'estree' +import type * as es from 'estree' import { RawSourceMap } from 'source-map' import { IOptions, Result } from '..' import { NATIVE_STORAGE_ID } from '../constants' import { RuntimeSourceError } from '../errors/runtimeSourceError' -import { hoistAndMergeImports } from '../localImports/transformers/hoistAndMergeImports' import { getRequireProvider, RequireProvider } from '../modules/requireProvider' import { parse } from '../parser/parser' import { evallerReplacer, getBuiltins, transpile } from '../transpiler/transpiler' -import type { Context, NativeStorage } from '../types' -import * as create from '../utils/astCreator' +import type { Context, NativeStorage, RecursivePartial } from '../types' +import * as create from '../utils/ast/astCreator' import { getIdentifiersInProgram } from '../utils/uniqueIds' import { toSourceError } from './errors' -import { appendModulesToContext, resolvedErrorPromise } from './utils' +import { resolvedErrorPromise } from './utils' function fullJSEval( code: string, @@ -49,7 +48,7 @@ function containsPrevEval(context: Context): boolean { export async function fullJSRunner( program: es.Program, context: Context, - options: Partial = {} + options: RecursivePartial = {} ): Promise { // prelude & builtins // only process builtins and preludes if it is a fresh eval context @@ -61,10 +60,6 @@ export async function fullJSRunner( ? [] : [...getBuiltins(context.nativeStorage), ...prelude] - // modules - hoistAndMergeImports(program) - appendModulesToContext(program, context) - // evaluate and create a separate block for preludes and builtins const preEvalProgram: es.Program = create.program([ ...preludeAndBuiltins, @@ -80,13 +75,15 @@ export async function fullJSRunner( let transpiled let sourceMapJson: RawSourceMap | undefined try { - ;({ transpiled, sourceMapJson } = transpile(program, context)) - return Promise.resolve({ + ;({ transpiled, sourceMapJson } = await transpile(program, context, options.importOptions)) + if (options.logTranspilerOutput) console.log(transpiled) + return { status: 'finished', context, value: await fullJSEval(transpiled, requireProvider, context.nativeStorage) - }) + } } catch (error) { + // console.log(error) context.errors.push( error instanceof RuntimeSourceError ? error : await toSourceError(error, sourceMapJson) ) diff --git a/src/runner/htmlRunner.ts b/src/runner/htmlRunner.ts index f52c688f8..76d00cbde 100644 --- a/src/runner/htmlRunner.ts +++ b/src/runner/htmlRunner.ts @@ -1,5 +1,5 @@ -import { IOptions, Result } from '..' -import { Context } from '../types' +import type { IOptions, Result } from '..' +import type { Context, RecursivePartial } from '../types' const HTML_ERROR_HANDLING_SCRIPT_TEMPLATE = `