@triflare/fx 2.1.0-beta.1 → 2.1.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +15 -6
- package/product.yml +1 -1
- package/src/build.js +23 -6
- package/src/cli/commands/check.js +3 -1
- package/src/cli/commands/feature.js +54 -5
- package/src/cli/commands/index.js +2 -0
- package/src/cli/commands/lint.js +4 -0
- package/src/cli/commands/test.js +186 -0
- package/src/cli/context.js +3 -1
- package/src/config-edit.js +78 -0
- package/src/config.js +77 -0
- package/src/features.js +89 -3
- package/src/formatter.js +21 -1
- package/src/index.js +21 -0
- package/src/lint/analyze.js +25 -5
- package/src/lint/config-file.js +14 -0
- package/src/merge.js +7 -0
- package/src/parser.js +78 -3
- package/src/project.js +14 -3
- package/src/test/discover.js +82 -0
- package/src/test/parse.js +72 -0
- package/src/test/pattern.js +84 -0
- package/src/test/reporters.js +332 -0
- package/src/test/runner.js +1457 -0
- package/src/vm.js +353 -0
- package/vendor/pm-vm-dist/.fx-build.json +7 -0
- package/vendor/pm-vm-dist/LICENSE +373 -0
- package/vendor/pm-vm-dist/NOTICE +6 -0
- package/vendor/pm-vm-dist/dist/iframe-extension-worker.js +1 -0
- package/vendor/pm-vm-dist/dist/js/extension-worker/extension-worker.7aa98c98b43d49535069.js +1 -0
- package/vendor/pm-vm-dist/dist/media/music/00765be80e1ec0073c7d855eac123b96.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/040abc33bc843ccea91acc82b370a2a5.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/059f32937ff4e15834ad94972c058386.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/0ae12e3be02439395202850a9c469930.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/0ae841a4034e4847205ff93589fe38b7.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/101c38be253a79f22164915a7b41a485.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/1c0f11f5e3bc3a290e3011a863acbf19.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/1e35fc4bf222acbb373be13bef7b494c.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/27470aa23024c67716d745d09d1eeb0d.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/2a2c19ccfb24fd1f611133f46859f697.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/2f42c08774845c95c5afe6aa2011660d.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/317cfa5845ae8684414e36059aa217cf.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/32ab039554155f1cc982d06b17c1b268.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/368b67706d2601ccdb29ba0ae4a06c7d.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/418e78ff0f0202a85f8e51ddceb6383b.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/44fede772a621f62516dafeec7ba6e76.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/45b502e33be78aca1c76962d8f0c9799.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/4efb38b83c696fde2043ad3770fd6cf5.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/555849284aa06b4716e47d7e12341ab4.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/6072de860d1aed016b4b82379510efc7.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/612b52d7a8538850a26cc9a6adda0451.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/6469f82ff2d352bc85e7528635ca6b98.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/66f88d71bbebdff3c79c315946b77516.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/70ce80e25dd12856990938c55f576862.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/79fb30eddc4dd8362cabe7dc80b59da6.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/7f0e67fcb804175c4169eabf81d21040.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/832a2a264fdeb69057ad7208584a49b5.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/83f40654a5eb9dc81f854c2b6f83422c.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/8414e157e2b98d7d4462330c8c4b1cc2.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/8d02b8a9547b47c8506d6c15c913acc1.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/912efc3e11a27b50b879a7ad3bad2eab.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/92771972e8ea5e5e76566218bd193471.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/92e9bc4ec854ae2c3157169fc5ca47a4.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/9b36fd7513f2eeab9876bc75c98262c6.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/9c571ca77e0ff0d1071c39941677febc.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/9d3f26ebd92e02a921b031785dafd6d6.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/9f769e9f239b0a1c048486b24cd9e15b.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/a5a68e9168e8e677cc15494e8680538b.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/a6a3f99085f00a94f2fb5aebfeee2879.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/a6caebbce7ac630fa45cd12fedc920c0.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/b5e23b90e61aaf37f5de98e9ca81fdbb.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/bc4d077743d9bc06d58065fd764f4019.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/bef29ef069663beb6280b70aac6705de.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/c2ba808baa06dac52c1787790396e54c.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/ca1b5393c07468dc2be541c24c3cc687.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/cad1541317d9ef4443a8c6325c7df3be.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/cde6740cc2c1d45a800e1237cc86f9a1.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/d448a74cb6caebc801e921ecdfd0c5d8.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/d5e7e578ff2d4a9b14c0d51f33a24299.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/df6c9e67211654be18df433951b021b0.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/dfe095471b15f0935cde612b52c91a3c.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/e64996bbc174611c2ad3d04a1434f7a0.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/e9e0e78519e67c9a17563711949e4684.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/eb290db5208d65da146b41d34f22f008.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/ebe739a8e09e50ea00c5f1afacc15d6a.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/ed88e2831000ca0e9dceabb95a346597.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/f059a67cea01cb6e49ba358c05d658c9.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/f3a9a9ae967dd5a330f29aa94d5c4680.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/f7e43cdb9928ae9999bc623f2087ee1a.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/fc4058e26c5069cbf08e741771d1e210.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/media/music/fd598784d15d0a4673327e341eb40d3b.mp3 +1 -0
- package/vendor/pm-vm-dist/dist/scratch-vm.js +187 -0
- package/vendor/pm-vm-dist/dist/static/assets/0043f23189fb3cbf2021a88959961ece.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/00d2dff9d5ece15a67074374b2027590.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/02996322134b61a0344527cddc61f9bd.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/06e9863f95061349f2ba2e0fb0b58ddc.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/0966e5defb1ae50e569fbbc7ebe7f22a.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/09995475180591a6c64c8744fbfdf4d8.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/0b18f02669375ad2ee80bef8d07ea7ec.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/0dcd27b60c185811b19f59d56eaa12f6.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/0e8ed599d9875c77d52db36de2b2b33f.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/103fe904046c35630c600cb122492d78.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/10af80546707afae5507edb828dd35a0.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/11c35683a60415bf62ee1a7324a25848.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/129b99ac19024c35228d855f42fb852b.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/1980303ffc6f5c17c601a9e5992aea77.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/1a064f9a752824e781ccf79ab66850ed.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/1a87a73db1ba0193283680e37b35754e.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/1c4f445589ea1ce7d954fc601f787f95.svg +58 -0
- package/vendor/pm-vm-dist/dist/static/assets/1c8f54c61adf1cee25299833093395f3.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/1c9630b745711a0b829841e8555d76e4.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/1d1e22600289d252ebda0b9924bf7d3a.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/1e7ed7b14b056ebd80f434143846fb4e.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/2b0cfb89b2c8d1badb03136fcf911c3b.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/2b7b80de8884cc3cf584aa0deb86a59b.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/3682b35e452a7f4a5a61167d1632e24a.ttf +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/368c582b289e75a3c5299e954c2ad735.ttf +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/36ed01ff8cd7eccf19f2606b56f6cd9f.svg +18 -0
- package/vendor/pm-vm-dist/dist/static/assets/39563f3b38b1fdb805323b1f69dace54.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/3fbd690bdae84637e34b8b933a164e3d.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/4240a79d13ecf2792f825457194c96d9.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/44c364962f4cb85aef2cdec4f0e01fd4.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/48d66af328a669a1c84c5dc87a4e9a3b.ttf +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/4c5757837d14e9fca439064c62617ba1.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/4c768843c9fa6593074231cd762b83f8.ttf +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/4ec6148233578e86736548eb8b9952e7.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/51a75dd50afd0bc6022dfc86455be19c.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/57f0f65052d9dac78ee66b11cc696b6a.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/5888c4ba6b8a0767f8be5bbc81994397.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/58d62765bbaa69620eb5bcd2c1cbbebb.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/5c6533a34175f61a9cab627f671111e1.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/5e0704b815761d38242f1b961ce9ef69.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/5e80f6caeb6323606a203fbb248fe18a.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/5f46c8dc3b8cc2296eb73f013dfce3db.ttf +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/5fe2f38d9b66e0a55b49e717fcd5c001.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/61edf73198617b2326d37f232b8abd6f.otf +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/638bc6bde253241454993ae12273970b.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/64bcfe1f13fce38e2ea26280157ff2ce.ttf +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/7337f194b0a727f4be98902487dfb615.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/74496d9086d97aaeeafb3085e9957668.ttf +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/76e8f03f3f2ac00ab16dd4f2afee63c4.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/78a74345ee778afbd6abe637b4f69efc.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/7a7fdb40997804d4778e2a12bd7599f7.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/7c5a16e0627c64fd62ec597bb72b879d.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/827c57667d965940792a82d82345e075.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/83857346b7c2e6a351c69377e82a3606.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/83f2d48d0c7d1e6be8a509d4d2c6d0e5.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/87a3a62c0627e925b92d3ede2f62b04f.ttf +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/8996930d8e83f63cc3341172205df460.otf +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/8bad9bd9d9a0d53b4a0f4e69bf02764a.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/8c0c02f16572b730d422f96a3ce53e7f.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/8ca9997e9ed7534a8d0f349a6125bd24.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/8d83d454d2c42614a203322393c23a5b.ttf +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/8f3259bb74d811177e8e65dcbea39ed4.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/8fdeee6df0b53509b7f9d623cab482b3.svg +1 -0
- package/vendor/pm-vm-dist/dist/static/assets/90eaf6469dbf3a808a3d9739cc77358d.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/942d5ca1a892e7dc5156d0ae7b438c1b.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/94b3a496d095dd3b3cfbaabb84a190fd.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/961bfad11616b6f5c951c58499cf267a.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/97aa37df83e2c5c2effa09d6622cd3a7.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/9ad12a9de73f49fb645902987027eb2b.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/9ec3449723269a806ec6eaba0f8f6cef.ttf +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/9f8e3a1db63c7a8fd976beca7e3546c3.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/a02821bc5d19d616d43d55ac10779101.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/a05bbf6359407ad7950e115edab178d9.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/a0b130dbafdc7958b73d162239053173.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/a61fc8393abec3c0d1e813fc01f252ee.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/a7c08a472cc8b1c4e0c45ae9b030b414.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/b52b83f63e8482255d46f40ea454fa01.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/b6a8e4a96ffcd83154ad2c5cbcb52751.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/b700cf6f9c368ce49851d287ddd3b63c.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/bacc13779ca01580b89f39dd2118c22a.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/bb59f04e7f0aabe8e142ac1607a03dd6.otf +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/bd16b79cacee7f23835efba88ed4aa67.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/c36dd943607003a3229aca77b49d32de.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/c44ce3bfa465281e765f197d6c3ee7ea.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/c60f3f06c5fb4d49df7236aeb2f31a9d.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/d434438e5d193118e4aad3a846e1c7ba.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/d4b3ce804680eb7d58f6accc7436e807.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/d6214ab9abf22f5d5dee060cb72f637f.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/d72b98474fc83d6268886779339e242e.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/da1a6ebcd442e9d867b31fb21ce6a289.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/da3126c6a70c4ef84192c33f99996354.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/e27f7cdf42df8b768345ae83fecffe4b.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/e6180253665b8248f685c6d96881f75b.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/eb64e706eb7a3b324ba50d3279a980b1.ttf +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/ecb1cbda3b9c4f29ab971734d97e3faf.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/ed2a96fbdb476efe419246982e357121.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/edd84717633475a8d3db34087123314a.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/f04f7b0f673ced51710157ce16e49276.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/f28b1ecd6fccd6218a837c14b1109da7.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/f382f43d667b784d1bcd8cfe7aa6fff0.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/f8d8b9d9f1aa45900b786980418219b1.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/f974293a11768662b53444715e3439ed.png +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/f9868658d8483a6dbcd600e0226dfeef.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/fad59dd220562492e6722aa8a97f9369.mp3 +0 -0
- package/vendor/pm-vm-dist/dist/static/assets/fc383956e315b7ee2bd8dd09de311701.mp3 +0 -0
- package/vendor/pm-vm-dist/package.json +3 -0
package/src/features.js
CHANGED
|
@@ -12,7 +12,12 @@ import fs from "node:fs";
|
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
import { parseConfig } from "./config.js";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
addFeatureEntry,
|
|
17
|
+
readFxValue,
|
|
18
|
+
removeFeatureEntry,
|
|
19
|
+
setFxScalar,
|
|
20
|
+
} from "./config-edit.js";
|
|
16
21
|
import { loadExtensions } from "./extensions.js";
|
|
17
22
|
import { formatDiff } from "./vendor.js";
|
|
18
23
|
|
|
@@ -40,15 +45,29 @@ export const FEATURES = {
|
|
|
40
45
|
},
|
|
41
46
|
};
|
|
42
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Built-in language/compiler features. These have no extension source and no
|
|
50
|
+
* vendored copy: they live behind a single `fx.<name>` setting, so they are
|
|
51
|
+
* enabled and disabled without touching `features:`.
|
|
52
|
+
*/
|
|
53
|
+
export const CORE_FEATURES = {
|
|
54
|
+
tests: {
|
|
55
|
+
name: "tests",
|
|
56
|
+
title: "Testing",
|
|
57
|
+
summary: "Write project tests and run them with `fx test`.",
|
|
58
|
+
core: true,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
43
62
|
/** List feature names for help and error messages. */
|
|
44
63
|
export function featureNames() {
|
|
45
|
-
return Object.keys(FEATURES).sort();
|
|
64
|
+
return [...Object.keys(FEATURES), ...Object.keys(CORE_FEATURES)].sort();
|
|
46
65
|
}
|
|
47
66
|
|
|
48
67
|
/** Resolve one feature by name. */
|
|
49
68
|
export function getFeature(name) {
|
|
50
69
|
const key = String(name ?? "");
|
|
51
|
-
const feature = FEATURES[key];
|
|
70
|
+
const feature = FEATURES[key] ?? CORE_FEATURES[key];
|
|
52
71
|
if (!feature)
|
|
53
72
|
throw new FeatureError(
|
|
54
73
|
`Unknown feature ${JSON.stringify(key)}. Available features: ${featureNames().join(", ")}.`,
|
|
@@ -158,6 +177,39 @@ function readFeatureEntries(configPath) {
|
|
|
158
177
|
* @param {boolean} [options.force] replace a modified copy
|
|
159
178
|
* @returns {{feature: object, name: string, file: string, path: string, changed: boolean, configChanged: boolean}}
|
|
160
179
|
*/
|
|
180
|
+
/** True when a feature's setting is enabled in the project config. */
|
|
181
|
+
function coreFeatureEnabled(configPath, feature) {
|
|
182
|
+
if (!configPath || !fs.existsSync(configPath)) return false;
|
|
183
|
+
return Boolean(readFxValue(configPath, feature.name));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Whether a feature is currently enabled in a project.
|
|
188
|
+
* @param {{configPath?: string, name: string}} options
|
|
189
|
+
*/
|
|
190
|
+
export function featureEnabled({ configPath, name } = {}) {
|
|
191
|
+
const feature = getFeature(name);
|
|
192
|
+
if (feature.core) return coreFeatureEnabled(configPath, feature);
|
|
193
|
+
if (!configPath || !fs.existsSync(configPath)) return false;
|
|
194
|
+
return Boolean(
|
|
195
|
+
readFeatureEntries(configPath).find((entry) => entry.name === feature.name),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Enable a feature in a project.
|
|
201
|
+
*
|
|
202
|
+
* Extension features copy their source into the project; core features only
|
|
203
|
+
* record `fx.<name>: true` in `fx.yml`.
|
|
204
|
+
*
|
|
205
|
+
* @param {object} options
|
|
206
|
+
* @param {string} options.configPath
|
|
207
|
+
* @param {string} options.configDir
|
|
208
|
+
* @param {string} options.name
|
|
209
|
+
* @param {string} [options.vendorDir]
|
|
210
|
+
* @param {boolean} [options.force] replace a modified copy
|
|
211
|
+
* @returns {{feature: object, name: string, file: string|null, path: string|null, changed: boolean, configChanged: boolean}}
|
|
212
|
+
*/
|
|
161
213
|
export function enableFeature({
|
|
162
214
|
configPath,
|
|
163
215
|
configDir,
|
|
@@ -166,6 +218,18 @@ export function enableFeature({
|
|
|
166
218
|
force = false,
|
|
167
219
|
} = {}) {
|
|
168
220
|
const feature = getFeature(name);
|
|
221
|
+
if (feature.core) {
|
|
222
|
+
const changed = !coreFeatureEnabled(configPath, feature);
|
|
223
|
+
if (changed) setFxScalar(configPath, feature.name, true);
|
|
224
|
+
return {
|
|
225
|
+
feature,
|
|
226
|
+
name: feature.name,
|
|
227
|
+
file: null,
|
|
228
|
+
path: null,
|
|
229
|
+
changed,
|
|
230
|
+
configChanged: changed,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
169
233
|
const dir = assertInside(
|
|
170
234
|
configDir,
|
|
171
235
|
path.resolve(configDir, String(vendorDir || "extensions")),
|
|
@@ -212,6 +276,24 @@ export function enableFeature({
|
|
|
212
276
|
*/
|
|
213
277
|
export function disableFeature({ configPath, configDir, name } = {}) {
|
|
214
278
|
const feature = getFeature(name);
|
|
279
|
+
if (feature.core) {
|
|
280
|
+
if (!coreFeatureEnabled(configPath, feature))
|
|
281
|
+
return {
|
|
282
|
+
feature,
|
|
283
|
+
name: feature.name,
|
|
284
|
+
file: null,
|
|
285
|
+
changed: false,
|
|
286
|
+
warning: null,
|
|
287
|
+
};
|
|
288
|
+
setFxScalar(configPath, feature.name, false);
|
|
289
|
+
return {
|
|
290
|
+
feature,
|
|
291
|
+
name: feature.name,
|
|
292
|
+
file: null,
|
|
293
|
+
changed: true,
|
|
294
|
+
warning: `${feature.title} will become a core feature in a future version.`,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
215
297
|
const entry = readFeatureEntries(configPath).find(
|
|
216
298
|
(candidate) => candidate.name === feature.name,
|
|
217
299
|
);
|
|
@@ -259,6 +341,10 @@ export function updateFeature({
|
|
|
259
341
|
dryRun = false,
|
|
260
342
|
} = {}) {
|
|
261
343
|
const feature = getFeature(name);
|
|
344
|
+
if (feature.core)
|
|
345
|
+
throw new FeatureError(
|
|
346
|
+
`${feature.title} is built into Fx, so there is no vendored source or lock entry to update.`,
|
|
347
|
+
);
|
|
262
348
|
const entry = readFeatureEntries(configPath).find(
|
|
263
349
|
(candidate) => candidate.name === feature.name,
|
|
264
350
|
);
|
package/src/formatter.js
CHANGED
|
@@ -81,6 +81,10 @@ function renderExpression(node, minimum = 0, parentOperator) {
|
|
|
81
81
|
return `${node.name}(${node.args
|
|
82
82
|
.map((arg) => renderExpression(arg))
|
|
83
83
|
.join(", ")})`;
|
|
84
|
+
case "Member":
|
|
85
|
+
// Member access binds tighter than every operator, so its object is
|
|
86
|
+
// rendered without parentheses, as in `state.list("items").length`.
|
|
87
|
+
return `${renderExpression(node.object)}.${node.property}`;
|
|
84
88
|
case "ArrayLiteral":
|
|
85
89
|
return `[${node.items.map((item) => renderExpression(item)).join(", ")}]`;
|
|
86
90
|
case "Unary": {
|
|
@@ -131,6 +135,7 @@ function category(node) {
|
|
|
131
135
|
case "FunctionDecl":
|
|
132
136
|
return "function";
|
|
133
137
|
case "HatDecl":
|
|
138
|
+
case "TestDecl":
|
|
134
139
|
return "script";
|
|
135
140
|
default:
|
|
136
141
|
return "statement";
|
|
@@ -353,6 +358,17 @@ class Formatter {
|
|
|
353
358
|
this.renderNodeBody(node, indent);
|
|
354
359
|
this.out.push(`${pad}}`);
|
|
355
360
|
return;
|
|
361
|
+
case "TestDecl":
|
|
362
|
+
this.out.push(`${pad}test ${stringLiteral(node.name)} {`);
|
|
363
|
+
this.renderNodeBody(node, indent);
|
|
364
|
+
this.out.push(`${pad}}`);
|
|
365
|
+
return;
|
|
366
|
+
case "Expect":
|
|
367
|
+
this.out.push(
|
|
368
|
+
`${pad}expect ${renderExpression(node.value)}` +
|
|
369
|
+
`${node.message ? `, ${stringLiteral(node.message)}` : ""};`,
|
|
370
|
+
);
|
|
371
|
+
return;
|
|
356
372
|
case "If":
|
|
357
373
|
this.renderIf(node, indent);
|
|
358
374
|
return;
|
|
@@ -475,7 +491,11 @@ class Formatter {
|
|
|
475
491
|
format() {
|
|
476
492
|
const units = [];
|
|
477
493
|
if (this.program.stage) units.push(this.program.stage);
|
|
478
|
-
units.push(
|
|
494
|
+
units.push(
|
|
495
|
+
...this.program.sprites,
|
|
496
|
+
...this.program.topItems,
|
|
497
|
+
...this.program.tests,
|
|
498
|
+
);
|
|
479
499
|
// Preserve the order declarations had in this file; only separate them.
|
|
480
500
|
units.sort((a, b) => this.tokenIndexOf(a.loc) - this.tokenIndexOf(b.loc));
|
|
481
501
|
this.renderBody(units, 0, this.tokens.length - 1);
|
package/src/index.js
CHANGED
|
@@ -61,6 +61,27 @@ export {
|
|
|
61
61
|
writeLock,
|
|
62
62
|
} from "./vendor.js";
|
|
63
63
|
export { formatDevUrls, injectDevClient, startDevServer } from "./dev.js";
|
|
64
|
+
export {
|
|
65
|
+
DEFAULT_TEST_PATTERN,
|
|
66
|
+
discoverTests,
|
|
67
|
+
globToRegExp,
|
|
68
|
+
isTestFile,
|
|
69
|
+
matchesTestPattern,
|
|
70
|
+
} from "./test/discover.js";
|
|
71
|
+
export { parseTestFile, parseTests } from "./test/parse.js";
|
|
72
|
+
export {
|
|
73
|
+
TEST_EXIT,
|
|
74
|
+
TestError,
|
|
75
|
+
formatFailure,
|
|
76
|
+
runTests,
|
|
77
|
+
} from "./test/runner.js";
|
|
78
|
+
export {
|
|
79
|
+
REPORTER_NAMES,
|
|
80
|
+
createTestReporter,
|
|
81
|
+
jsonReport,
|
|
82
|
+
junitReport,
|
|
83
|
+
} from "./test/reporters.js";
|
|
84
|
+
export { createVm, loadProject, loadVmModules } from "./vm.js";
|
|
64
85
|
export * from "./catalog.js";
|
|
65
86
|
export * from "./config.js";
|
|
66
87
|
export { FxError, LexError, ParseError, CompileError } from "./errors.js";
|
package/src/lint/analyze.js
CHANGED
|
@@ -14,6 +14,7 @@ import { ParseError } from "../errors.js";
|
|
|
14
14
|
import { tokenizeWithComments } from "../lexer.js";
|
|
15
15
|
import { mergePrograms } from "../merge.js";
|
|
16
16
|
import { parse } from "../parser.js";
|
|
17
|
+
import { DEFAULT_TEST_PATTERN, isTestPath } from "../test/pattern.js";
|
|
17
18
|
import { DEFAULT_LINT, resolveRuleSeverities } from "./config.js";
|
|
18
19
|
import { RULES, ruleById } from "./rules.js";
|
|
19
20
|
|
|
@@ -1193,6 +1194,7 @@ export function sortDiagnostics(diagnostics) {
|
|
|
1193
1194
|
* configPath?: string|null,
|
|
1194
1195
|
* configDir?: string|null,
|
|
1195
1196
|
* rootDir?: string|null,
|
|
1197
|
+
* testPattern?: string|null,
|
|
1196
1198
|
* extensionCatalog?: Record<string, object>,
|
|
1197
1199
|
* declaredExtensions?: string[],
|
|
1198
1200
|
* lintConfig?: object,
|
|
@@ -1211,6 +1213,9 @@ export function lintSources({
|
|
|
1211
1213
|
sources = [],
|
|
1212
1214
|
config = null,
|
|
1213
1215
|
configPath = null,
|
|
1216
|
+
configDir = null,
|
|
1217
|
+
rootDir = null,
|
|
1218
|
+
testPattern = DEFAULT_TEST_PATTERN,
|
|
1214
1219
|
extensionCatalog = {},
|
|
1215
1220
|
declaredExtensions = [],
|
|
1216
1221
|
lintConfig = DEFAULT_LINT,
|
|
@@ -1252,12 +1257,27 @@ export function lintSources({
|
|
|
1252
1257
|
}
|
|
1253
1258
|
}
|
|
1254
1259
|
|
|
1260
|
+
// A source that declares tests, or whose path matches the configured
|
|
1261
|
+
// test-file pattern, is a test file. Test files are never part of the
|
|
1262
|
+
// project program — `mergePrograms` rejects them by design — so they are
|
|
1263
|
+
// left out of the cross-file merge and the project rules. Editors lint every
|
|
1264
|
+
// `.tfx` file in a project, test files included, and must not report the
|
|
1265
|
+
// merge error on each test declaration. Pattern matching also keeps a
|
|
1266
|
+
// helper-only test file (no `test` declarations) from leaking its top-level
|
|
1267
|
+
// variables into project scope.
|
|
1268
|
+
const testRoot = rootDir ?? configDir ?? null;
|
|
1269
|
+
const projectParsed = parsed.filter(
|
|
1270
|
+
({ file, ast }) =>
|
|
1271
|
+
(ast.tests ?? []).length === 0 &&
|
|
1272
|
+
!isTestPath(file, testRoot, testPattern),
|
|
1273
|
+
);
|
|
1274
|
+
|
|
1255
1275
|
// Partial sprites are resolved by the project merge, which throws when the
|
|
1256
1276
|
// feature is off or a base declaration is missing. Report those first.
|
|
1257
1277
|
const partialSprites = Boolean(config?.fx?.partialSprites);
|
|
1258
1278
|
let partialBlocked = false;
|
|
1259
1279
|
if (!partialSprites) {
|
|
1260
|
-
for (const { ast } of
|
|
1280
|
+
for (const { ast } of projectParsed) {
|
|
1261
1281
|
for (const sprite of ast.sprites) {
|
|
1262
1282
|
if (!sprite.partial) continue;
|
|
1263
1283
|
partialBlocked = true;
|
|
@@ -1271,7 +1291,7 @@ export function lintSources({
|
|
|
1271
1291
|
}
|
|
1272
1292
|
}
|
|
1273
1293
|
const groups = new Map();
|
|
1274
|
-
for (const { ast } of
|
|
1294
|
+
for (const { ast } of projectParsed) {
|
|
1275
1295
|
for (const sprite of ast.sprites) {
|
|
1276
1296
|
const group = groups.get(sprite.name) ?? { base: false, fragment: null };
|
|
1277
1297
|
if (sprite.partial) group.fragment ??= sprite;
|
|
@@ -1290,10 +1310,10 @@ export function lintSources({
|
|
|
1290
1310
|
}
|
|
1291
1311
|
|
|
1292
1312
|
let program = null;
|
|
1293
|
-
if (!partialBlocked) {
|
|
1313
|
+
if (!partialBlocked && projectParsed.length > 0) {
|
|
1294
1314
|
try {
|
|
1295
1315
|
program = mergePrograms(
|
|
1296
|
-
|
|
1316
|
+
projectParsed.map(({ file, ast }) => ({ ast, file })),
|
|
1297
1317
|
{ partialSprites },
|
|
1298
1318
|
);
|
|
1299
1319
|
} catch (error) {
|
|
@@ -1305,7 +1325,7 @@ export function lintSources({
|
|
|
1305
1325
|
}
|
|
1306
1326
|
}
|
|
1307
1327
|
if (program) analyzer.analyzeProgram(program);
|
|
1308
|
-
else for (const { ast } of
|
|
1328
|
+
else for (const { ast } of projectParsed) analyzer.analyzeProgram(ast);
|
|
1309
1329
|
|
|
1310
1330
|
// Diagnostics from filesystem-backed project checks (assets, fx.lock).
|
|
1311
1331
|
for (const diagnostic of extraDiagnostics) {
|
package/src/lint/config-file.js
CHANGED
|
@@ -22,6 +22,7 @@ function extensionName(source) {
|
|
|
22
22
|
* config: {fx: {partialSprites: boolean}},
|
|
23
23
|
* lintConfig: object,
|
|
24
24
|
* declaredExtensions: string[],
|
|
25
|
+
* testPattern: string|null,
|
|
25
26
|
* projectName: string|null,
|
|
26
27
|
* error?: string,
|
|
27
28
|
* }}
|
|
@@ -35,6 +36,7 @@ export function parseProjectConfigText(text) {
|
|
|
35
36
|
config: null,
|
|
36
37
|
lintConfig: null,
|
|
37
38
|
declaredExtensions: [],
|
|
39
|
+
testPattern: null,
|
|
38
40
|
projectName: null,
|
|
39
41
|
error: error instanceof Error ? error.message : String(error),
|
|
40
42
|
};
|
|
@@ -44,6 +46,7 @@ export function parseProjectConfigText(text) {
|
|
|
44
46
|
config: null,
|
|
45
47
|
lintConfig: null,
|
|
46
48
|
declaredExtensions: [],
|
|
49
|
+
testPattern: null,
|
|
47
50
|
projectName: null,
|
|
48
51
|
error: "fx.yml must contain a mapping at the top level",
|
|
49
52
|
};
|
|
@@ -68,6 +71,7 @@ export function parseProjectConfigText(text) {
|
|
|
68
71
|
config: null,
|
|
69
72
|
lintConfig: null,
|
|
70
73
|
declaredExtensions: [],
|
|
74
|
+
testPattern: null,
|
|
71
75
|
projectName: typeof project.name === "string" ? project.name : null,
|
|
72
76
|
error: error instanceof Error ? error.message : String(error),
|
|
73
77
|
};
|
|
@@ -88,10 +92,20 @@ export function parseProjectConfigText(text) {
|
|
|
88
92
|
}
|
|
89
93
|
}
|
|
90
94
|
|
|
95
|
+
const testPattern =
|
|
96
|
+
fx.tests &&
|
|
97
|
+
typeof fx.tests === "object" &&
|
|
98
|
+
!Array.isArray(fx.tests) &&
|
|
99
|
+
typeof fx.tests.pattern === "string" &&
|
|
100
|
+
fx.tests.pattern.trim()
|
|
101
|
+
? fx.tests.pattern.trim()
|
|
102
|
+
: null;
|
|
103
|
+
|
|
91
104
|
return {
|
|
92
105
|
config: { fx: { partialSprites: Boolean(fx.partialSprites) } },
|
|
93
106
|
lintConfig,
|
|
94
107
|
declaredExtensions: [...declaredExtensions],
|
|
108
|
+
testPattern,
|
|
95
109
|
projectName: typeof project.name === "string" ? project.name : null,
|
|
96
110
|
};
|
|
97
111
|
}
|
package/src/merge.js
CHANGED
|
@@ -43,6 +43,13 @@ export function mergePrograms(
|
|
|
43
43
|
const groups = new Map();
|
|
44
44
|
|
|
45
45
|
for (const { ast, file } of entries) {
|
|
46
|
+
if (ast.tests && ast.tests.length > 0)
|
|
47
|
+
throw new ParseError(
|
|
48
|
+
`Test ${JSON.stringify(ast.tests[0].name)} is declared in a project source. ` +
|
|
49
|
+
`Move test declarations to a test file (for example ` +
|
|
50
|
+
`test/score.test.tfx); ordinary builds never compile test files.`,
|
|
51
|
+
errorInfo(ast.tests[0].loc),
|
|
52
|
+
);
|
|
46
53
|
if (ast.stage) {
|
|
47
54
|
if (stage) {
|
|
48
55
|
throw new ParseError(
|
package/src/parser.js
CHANGED
|
@@ -97,6 +97,7 @@ class Parser {
|
|
|
97
97
|
const sprites = [];
|
|
98
98
|
let stage = null;
|
|
99
99
|
const topItems = [];
|
|
100
|
+
const tests = [];
|
|
100
101
|
|
|
101
102
|
while (this.peek().type !== "eof") {
|
|
102
103
|
if (
|
|
@@ -116,12 +117,38 @@ class Parser {
|
|
|
116
117
|
) {
|
|
117
118
|
if (stage) this.fail("A project may only declare one stage");
|
|
118
119
|
stage = this.parseStage();
|
|
120
|
+
} else if (
|
|
121
|
+
this.atIdent("test") &&
|
|
122
|
+
this.peek(1).type === "string" &&
|
|
123
|
+
this.peek(2).type === "punct" &&
|
|
124
|
+
this.peek(2).value === "{"
|
|
125
|
+
) {
|
|
126
|
+
tests.push(this.parseTestDecl());
|
|
119
127
|
} else {
|
|
120
128
|
topItems.push(this.parseBodyItem());
|
|
121
129
|
}
|
|
122
130
|
}
|
|
123
131
|
|
|
124
|
-
return { type: "Program", stage, sprites, topItems };
|
|
132
|
+
return { type: "Program", stage, sprites, topItems, tests };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** `test "name" { ... }` — only valid in test files. */
|
|
136
|
+
parseTestDecl() {
|
|
137
|
+
const tok = this.expect("test");
|
|
138
|
+
const name = this.expectType("string", "a test name string").value;
|
|
139
|
+
const body = this.parseBlockBody();
|
|
140
|
+
return { type: "TestDecl", name, body, loc: tok };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** `expect <expression>[, "message"];` */
|
|
144
|
+
parseExpect() {
|
|
145
|
+
const tok = this.expect("expect");
|
|
146
|
+
const value = this.parseExpression();
|
|
147
|
+
let message = null;
|
|
148
|
+
if (this.eat(","))
|
|
149
|
+
message = this.expectType("string", "an assertion message string").value;
|
|
150
|
+
this.expect(";");
|
|
151
|
+
return { type: "Expect", value, message, loc: tok };
|
|
125
152
|
}
|
|
126
153
|
|
|
127
154
|
parseSprite(partial = false, startTok = null) {
|
|
@@ -207,6 +234,18 @@ class Parser {
|
|
|
207
234
|
}
|
|
208
235
|
case "when":
|
|
209
236
|
return this.parseHat();
|
|
237
|
+
case "expect":
|
|
238
|
+
// A complete call or assignment stays a normal statement. An
|
|
239
|
+
// expression continuing after the closing parenthesis is an assertion.
|
|
240
|
+
if (
|
|
241
|
+
!(
|
|
242
|
+
this.peek(1).type === "punct" &&
|
|
243
|
+
ASSIGN_OPS.has(this.peek(1).value)
|
|
244
|
+
) &&
|
|
245
|
+
!this.isExpectCall()
|
|
246
|
+
)
|
|
247
|
+
return this.parseExpect();
|
|
248
|
+
break;
|
|
210
249
|
case "if":
|
|
211
250
|
return this.parseIf();
|
|
212
251
|
case "repeat":
|
|
@@ -233,6 +272,22 @@ class Parser {
|
|
|
233
272
|
return this.parseSimpleStatement();
|
|
234
273
|
}
|
|
235
274
|
|
|
275
|
+
isExpectCall() {
|
|
276
|
+
if (this.peek(1).type !== "punct" || this.peek(1).value !== "(")
|
|
277
|
+
return false;
|
|
278
|
+
let depth = 0;
|
|
279
|
+
for (let offset = 1; this.peek(offset).type !== "eof"; offset++) {
|
|
280
|
+
const token = this.peek(offset);
|
|
281
|
+
if (token.type !== "punct") continue;
|
|
282
|
+
if (token.value === "(") depth++;
|
|
283
|
+
if (token.value === ")" && --depth === 0) {
|
|
284
|
+
const next = this.peek(offset + 1);
|
|
285
|
+
return next.type === "punct" && [";", "{"].includes(next.value);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
236
291
|
parseVarDecl(cloud) {
|
|
237
292
|
const tok = cloud ? this.expect("cloud") : this.expect("let");
|
|
238
293
|
if (cloud) this.expect("let");
|
|
@@ -474,6 +529,26 @@ class Parser {
|
|
|
474
529
|
return { name: segments.join("."), segments, loc: tok };
|
|
475
530
|
}
|
|
476
531
|
|
|
532
|
+
/**
|
|
533
|
+
* Postfix member access on a call result, as in
|
|
534
|
+
* `state.list("items").length`. Qualified identifiers already absorb their
|
|
535
|
+
* segments while they are parsed.
|
|
536
|
+
*/
|
|
537
|
+
parseMemberAccess(expression) {
|
|
538
|
+
let node = expression;
|
|
539
|
+
while (this.atPunct(".") && this.peek(1).type === "ident") {
|
|
540
|
+
this.next();
|
|
541
|
+
const property = this.next();
|
|
542
|
+
node = {
|
|
543
|
+
type: "Member",
|
|
544
|
+
object: node,
|
|
545
|
+
property: property.value,
|
|
546
|
+
loc: property,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
return node;
|
|
550
|
+
}
|
|
551
|
+
|
|
477
552
|
parseExpressionList(closing) {
|
|
478
553
|
const list = [];
|
|
479
554
|
if (this.atPunct(closing)) return list;
|
|
@@ -607,13 +682,13 @@ class Parser {
|
|
|
607
682
|
if (this.eat("(")) {
|
|
608
683
|
const args = this.parseExpressionList(")");
|
|
609
684
|
this.expect(")");
|
|
610
|
-
return {
|
|
685
|
+
return this.parseMemberAccess({
|
|
611
686
|
type: "Call",
|
|
612
687
|
name: name.name,
|
|
613
688
|
segments: name.segments,
|
|
614
689
|
args,
|
|
615
690
|
loc: tok,
|
|
616
|
-
};
|
|
691
|
+
});
|
|
617
692
|
}
|
|
618
693
|
return {
|
|
619
694
|
type: "Identifier",
|
package/src/project.js
CHANGED
|
@@ -9,6 +9,7 @@ import fs from "node:fs";
|
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { configFilename, sourceExtension } from "./brand.js";
|
|
11
11
|
import { mergePrograms as mergeParsed } from "./merge.js";
|
|
12
|
+
import { DEFAULT_TEST_PATTERN, matchesTestPattern } from "./test/discover.js";
|
|
12
13
|
|
|
13
14
|
/** Directories never scanned for `.tfx` sources. */
|
|
14
15
|
const IGNORED_DIRS = new Set([
|
|
@@ -21,12 +22,18 @@ const IGNORED_DIRS = new Set([
|
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Recursively find every `.tfx` file under `dir`, skipping hidden entries and
|
|
24
|
-
* well-known build/dependency directories.
|
|
25
|
-
*
|
|
25
|
+
* well-known build/dependency directories. Test files are skipped so they
|
|
26
|
+
* never become part of the project target.
|
|
27
|
+
*
|
|
28
|
+
* Results are deterministic (sorted by path) so builds are reproducible.
|
|
26
29
|
* @param {string} dir
|
|
30
|
+
* @param {{testPattern?: string|null}} [options] `null` keeps test files
|
|
27
31
|
* @returns {string[]} absolute file paths
|
|
28
32
|
*/
|
|
29
|
-
export function discoverSources(
|
|
33
|
+
export function discoverSources(
|
|
34
|
+
dir,
|
|
35
|
+
{ testPattern = DEFAULT_TEST_PATTERN } = {},
|
|
36
|
+
) {
|
|
30
37
|
const root = path.resolve(dir);
|
|
31
38
|
const found = [];
|
|
32
39
|
const extension = sourceExtension().toLowerCase();
|
|
@@ -49,6 +56,10 @@ export function discoverSources(dir) {
|
|
|
49
56
|
entry.isFile() &&
|
|
50
57
|
entry.name.toLowerCase().endsWith(extension)
|
|
51
58
|
) {
|
|
59
|
+
if (testPattern) {
|
|
60
|
+
const relative = path.relative(root, full).split(path.sep).join("/");
|
|
61
|
+
if (matchesTestPattern(relative, testPattern)) continue;
|
|
62
|
+
}
|
|
52
63
|
found.push(full);
|
|
53
64
|
}
|
|
54
65
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test-file discovery.
|
|
3
|
+
*
|
|
4
|
+
* Test files are ordinary `.tfx` sources that match a project glob. They are
|
|
5
|
+
* never part of the project target, so both `fx test` and the normal build
|
|
6
|
+
* path use the same matcher.
|
|
7
|
+
*/
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { DEFAULT_TEST_PATTERN, matchesTestPattern } from "./pattern.js";
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
DEFAULT_TEST_PATTERN,
|
|
14
|
+
globToRegExp,
|
|
15
|
+
matchesTestPattern,
|
|
16
|
+
} from "./pattern.js";
|
|
17
|
+
|
|
18
|
+
const IGNORED_DIRS = new Set([
|
|
19
|
+
"node_modules",
|
|
20
|
+
"build",
|
|
21
|
+
"dist",
|
|
22
|
+
"coverage",
|
|
23
|
+
".cache",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/** Recursively collect test files below `current`, skipping ignored dirs. */
|
|
27
|
+
function walk(root, current, found, pattern, extension) {
|
|
28
|
+
let entries;
|
|
29
|
+
try {
|
|
30
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
31
|
+
} catch {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (entry.name.startsWith(".")) continue;
|
|
37
|
+
const full = path.join(current, entry.name);
|
|
38
|
+
if (entry.isDirectory()) {
|
|
39
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
40
|
+
walk(root, full, found, pattern, extension);
|
|
41
|
+
} else if (entry.isFile() && entry.name.toLowerCase().endsWith(extension)) {
|
|
42
|
+
const relative = path.relative(root, full).split(path.sep).join("/");
|
|
43
|
+
if (matchesTestPattern(relative, pattern)) found.push(full);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Find every test file under a directory.
|
|
50
|
+
*
|
|
51
|
+
* @param {{dir: string, pattern?: string, extension?: string}} options
|
|
52
|
+
* @returns {Array<{file: string, relative: string}>} sorted by path
|
|
53
|
+
*/
|
|
54
|
+
export function discoverTests({
|
|
55
|
+
dir,
|
|
56
|
+
pattern = DEFAULT_TEST_PATTERN,
|
|
57
|
+
extension = ".tfx",
|
|
58
|
+
} = {}) {
|
|
59
|
+
if (!dir) throw new Error("discoverTests needs a directory");
|
|
60
|
+
const root = path.resolve(dir);
|
|
61
|
+
const found = [];
|
|
62
|
+
walk(root, root, found, pattern, String(extension).toLowerCase());
|
|
63
|
+
found.sort();
|
|
64
|
+
return found.map((file) => ({
|
|
65
|
+
file,
|
|
66
|
+
relative: path.relative(root, file).split(path.sep).join("/"),
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Whether one explicit path is a test file for a project directory.
|
|
72
|
+
* @param {string} file
|
|
73
|
+
* @param {string} dir
|
|
74
|
+
* @param {string} [pattern]
|
|
75
|
+
*/
|
|
76
|
+
export function isTestFile(file, dir, pattern = DEFAULT_TEST_PATTERN) {
|
|
77
|
+
const relative = path
|
|
78
|
+
.relative(path.resolve(dir), path.resolve(file))
|
|
79
|
+
.split(path.sep)
|
|
80
|
+
.join("/");
|
|
81
|
+
return matchesTestPattern(relative, pattern);
|
|
82
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test-file parsing.
|
|
3
|
+
*
|
|
4
|
+
* A test file is a normal `.tfx` source with `test` declarations. Project
|
|
5
|
+
* declarations (sprites, the stage, hats, project variables) are rejected:
|
|
6
|
+
* tests reuse the language, not the project structure.
|
|
7
|
+
*/
|
|
8
|
+
import { parse } from "../parser.js";
|
|
9
|
+
import { ParseError } from "../errors.js";
|
|
10
|
+
|
|
11
|
+
const ALLOWED_TOP_LEVEL = new Set(["VarDecl", "FunctionDecl"]);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} source
|
|
15
|
+
* @param {string} file
|
|
16
|
+
* @returns {{file: string, tests: object[], helpers: object[]}}
|
|
17
|
+
*/
|
|
18
|
+
export function parseTestFile(source, file = "<test>") {
|
|
19
|
+
const ast = parse(source, file);
|
|
20
|
+
|
|
21
|
+
if (ast.stage)
|
|
22
|
+
throw new ParseError(
|
|
23
|
+
"Test files cannot declare a stage; tests run against the project build",
|
|
24
|
+
locationOf(ast.stage),
|
|
25
|
+
);
|
|
26
|
+
if (ast.sprites.length > 0)
|
|
27
|
+
throw new ParseError(
|
|
28
|
+
`Test files cannot declare sprites (found ${JSON.stringify(ast.sprites[0].name)})`,
|
|
29
|
+
locationOf(ast.sprites[0]),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
for (const item of ast.topItems) {
|
|
33
|
+
if (!ALLOWED_TOP_LEVEL.has(item.type))
|
|
34
|
+
throw new ParseError(
|
|
35
|
+
"Test files may only declare tests, shared variables and shared functions",
|
|
36
|
+
locationOf(item),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { file, tests: ast.tests, helpers: ast.topItems };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Source location of an AST node, defaulting unknown fields to null. */
|
|
44
|
+
function locationOf(node) {
|
|
45
|
+
const loc = node?.loc ?? {};
|
|
46
|
+
return { file: loc.file, line: loc.line, column: loc.column };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse several test files. Errors are collected so a run can report every
|
|
51
|
+
* broken file before failing with the language-error exit code.
|
|
52
|
+
*
|
|
53
|
+
* @param {Array<{file: string, source: string}>} entries
|
|
54
|
+
* @returns {{files: Array<{file: string, tests: object[], helpers: object[]}>, diagnostics: object[]}}
|
|
55
|
+
*/
|
|
56
|
+
export function parseTests(entries) {
|
|
57
|
+
const files = [];
|
|
58
|
+
const diagnostics = [];
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
try {
|
|
61
|
+
files.push(parseTestFile(entry.source, entry.file));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
diagnostics.push({
|
|
64
|
+
file: entry.file,
|
|
65
|
+
message: error.message,
|
|
66
|
+
line: error.line ?? null,
|
|
67
|
+
column: error.column ?? null,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { files, diagnostics };
|
|
72
|
+
}
|