Initial tests set up (#10)

* Begin debugging

* Initial tests set up

This adds tests using uvu (we can switch if people want) and restructures things a bit so that it's easier to test.

Like in snowpack you set up a little project. In our tests you can say:

```js
const result = await runtime.load('/blog/hello-world')
```

And analyze the result. I included a `test-helpers.js` which has a function that will turn HTML into a cheerio instance, for inspecting the result HTML.

* Add CI

* Remove extra console logs

* Formatting
This commit is contained in:
Matthew Phillips 2021-03-19 17:07:45 -04:00 committed by GitHub
parent 8ebc077cb0
commit 17c3c98f07
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 4199 additions and 4026 deletions

26
.github/workflows/nodejs.yml vendored Normal file
View file

@ -0,0 +1,26 @@
name: Node CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: npm install, build, and test
run: |
npm ci
npm run build
npm test
env:
CI: true

283
package-lock.json generated
View file

@ -536,7 +536,8 @@
"boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
"dev": true
},
"boxen": {
"version": "4.2.0",
@ -684,67 +685,31 @@
"integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg=="
},
"cheerio": {
"version": "0.22.0",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz",
"integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=",
"version": "1.0.0-rc.5",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.5.tgz",
"integrity": "sha512-yoqps/VCaZgN4pfXtenwHROTp8NG6/Hlt4Jpz2FEP0ZJQ+ZUkVDd0hAPDNKhj3nakpfPt/CNs57yEtxD1bXQiw==",
"dev": true,
"requires": {
"css-select": "~1.2.0",
"dom-serializer": "~0.1.0",
"entities": "~1.1.1",
"htmlparser2": "^3.9.1",
"lodash.assignin": "^4.0.9",
"lodash.bind": "^4.1.4",
"lodash.defaults": "^4.0.1",
"lodash.filter": "^4.4.0",
"lodash.flatten": "^4.2.0",
"lodash.foreach": "^4.3.0",
"lodash.map": "^4.4.0",
"lodash.merge": "^4.4.0",
"lodash.pick": "^4.2.1",
"lodash.reduce": "^4.4.0",
"lodash.reject": "^4.4.0",
"lodash.some": "^4.4.0"
},
"dependencies": {
"domhandler": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
"integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
"requires": {
"domelementtype": "1"
"cheerio-select-tmp": "^0.1.0",
"dom-serializer": "~1.2.0",
"domhandler": "^4.0.0",
"entities": "~2.1.0",
"htmlparser2": "^6.0.0",
"parse5": "^6.0.0",
"parse5-htmlparser2-tree-adapter": "^6.0.0"
}
},
"htmlparser2": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
"integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
"cheerio-select-tmp": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/cheerio-select-tmp/-/cheerio-select-tmp-0.1.1.tgz",
"integrity": "sha512-YYs5JvbpU19VYJyj+F7oYrIE2BOll1/hRU7rEy/5+v9BzkSo3bK81iAeeQEMI92vRIxz677m72UmJUiVwwgjfQ==",
"dev": true,
"requires": {
"domelementtype": "^1.3.1",
"domhandler": "^2.3.0",
"domutils": "^1.5.1",
"entities": "^1.1.1",
"inherits": "^2.0.1",
"readable-stream": "^3.1.1"
}
},
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
},
"string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"requires": {
"safe-buffer": "~5.2.0"
}
}
"css-select": "^3.1.2",
"css-what": "^4.0.0",
"domelementtype": "^2.1.0",
"domhandler": "^4.0.0",
"domutils": "^2.4.4"
}
},
"chokidar": {
@ -950,14 +915,16 @@
"dev": true
},
"css-select": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
"integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-3.1.2.tgz",
"integrity": "sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA==",
"dev": true,
"requires": {
"boolbase": "~1.0.0",
"css-what": "2.1",
"domutils": "1.5.1",
"nth-check": "~1.0.1"
"boolbase": "^1.0.0",
"css-what": "^4.0.0",
"domhandler": "^4.0.0",
"domutils": "^2.4.3",
"nth-check": "^2.0.0"
}
},
"css-tree": {
@ -970,9 +937,10 @@
}
},
"css-what": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz",
"integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg=="
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-4.0.0.tgz",
"integrity": "sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A==",
"dev": true
},
"cssesc": {
"version": "3.0.0",
@ -1019,6 +987,11 @@
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
"dev": true
},
"deepmerge": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
},
"default-browser-id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-2.0.0.tgz",
@ -1045,6 +1018,18 @@
"integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==",
"dev": true
},
"dequal": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz",
"integrity": "sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==",
"dev": true
},
"diff": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
"integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
"dev": true
},
"dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@ -1064,18 +1049,21 @@
}
},
"dom-serializer": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz",
"integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.2.0.tgz",
"integrity": "sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA==",
"dev": true,
"requires": {
"domelementtype": "^1.3.0",
"entities": "^1.1.1"
"domelementtype": "^2.0.1",
"domhandler": "^4.0.0",
"entities": "^2.0.0"
}
},
"domelementtype": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
"integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w=="
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz",
"integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==",
"dev": true
},
"domhandler": {
"version": "4.0.0",
@ -1093,12 +1081,14 @@
}
},
"domutils": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
"integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.5.0.tgz",
"integrity": "sha512-Ho16rzNMOFk2fPwChGh3D2D9OEHAfG19HgmRR2l+WLSsIstNsAYBzePH412bL0y5T44ejABIVfTHQ8nqi/tBCg==",
"dev": true,
"requires": {
"dom-serializer": "0",
"domelementtype": "1"
"dom-serializer": "^1.0.1",
"domelementtype": "^2.0.1",
"domhandler": "^4.0.0"
}
},
"dot-prop": {
@ -1151,9 +1141,10 @@
}
},
"entities": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w=="
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==",
"dev": true
},
"error-ex": {
"version": "1.3.2",
@ -1806,7 +1797,8 @@
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"ini": {
"version": "1.3.7",
@ -2073,71 +2065,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash.assignin": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz",
"integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI="
},
"lodash.bind": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz",
"integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU="
},
"lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY="
},
"lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
},
"lodash.filter": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz",
"integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4="
},
"lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
},
"lodash.foreach": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
"integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM="
},
"lodash.map": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz",
"integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM="
},
"lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
},
"lodash.pick": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz",
"integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM="
},
"lodash.reduce": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz",
"integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs="
},
"lodash.reject": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz",
"integrity": "sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU="
},
"lodash.some": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz",
"integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0="
},
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -2292,6 +2224,12 @@
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true
},
"mri": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.1.6.tgz",
"integrity": "sha512-oi1b3MfbyGa7FJMP9GmLTttni5JoICpYBRlq+x5V16fZbLsnL9N3wFqqIm/nIG43FjUFkFh9Epzp/kzUGUnJxQ==",
"dev": true
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -2390,11 +2328,12 @@
"dev": true
},
"nth-check": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz",
"integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz",
"integrity": "sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==",
"dev": true,
"requires": {
"boolbase": "~1.0.0"
"boolbase": "^1.0.0"
}
},
"object-assign": {
@ -2499,6 +2438,21 @@
"lines-and-columns": "^1.1.6"
}
},
"parse5": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
"dev": true
},
"parse5-htmlparser2-tree-adapter": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz",
"integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==",
"dev": true,
"requires": {
"parse5": "^6.0.1"
}
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@ -2873,10 +2827,14 @@
"tslib": "^1.9.0"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
"sade": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.7.4.tgz",
"integrity": "sha512-y5yauMD93rX840MwUJr7C1ysLFBgMspsdTo4UVrDg3fXDvtwOyIqykhVAAm6fk/3au77773itJStObgK+LKaiA==",
"dev": true,
"requires": {
"mri": "^1.1.0"
}
},
"sass": {
"version": "1.32.8",
@ -3216,6 +3174,12 @@
"is-number": "^7.0.0"
}
},
"totalist": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-2.0.0.tgz",
"integrity": "sha512-+Y17F0YzxfACxTyjfhnJQEe7afPA0GSpYlFkl2VFMxYP7jshQf9gXV7cH47EfToBumFThfKBvfAcoUn6fdNeRQ==",
"dev": true
},
"touch": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
@ -3366,6 +3330,19 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"uvu": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.1.tgz",
"integrity": "sha512-JGxttnOGDFs77FaZ0yMUHIzczzQ5R1IlDeNW6Wymw6gAscwMdAffVOP6TlxLIfReZyK8tahoGwWZaTCJzNFDkg==",
"dev": true,
"requires": {
"dequal": "^2.0.0",
"diff": "^5.0.0",
"kleur": "^4.0.3",
"sade": "^1.7.3",
"totalist": "^2.0.0"
}
},
"v8-compile-cache": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",

View file

@ -21,7 +21,8 @@
"dev": "concurrently 'tsc --watch' 'npm run copy-js:watch'",
"format": "prettier -w 'src/**/*.{js,ts}'",
"copy-js": "copyfiles -u 1 src/*.js lib/",
"copy-js:watch": "nodemon -w src --ext js --exec 'npm run copy-js'"
"copy-js:watch": "nodemon -w src --ext js --exec 'npm run copy-js'",
"test": "uvu test -i fixtures -i test-utils.js"
},
"dependencies": {
"@types/estree": "0.0.46",
@ -31,8 +32,8 @@
"acorn-jsx": "^5.3.1",
"astring": "^1.7.0",
"autoprefixer": "^10.2.5",
"cheerio": "^0.22.0",
"css-tree": "^1.1.2",
"deepmerge": "^4.2.2",
"domhandler": "^4.0.0",
"es-module-lexer": "^0.4.1",
"gray-matter": "^4.0.2",
@ -57,6 +58,7 @@
"@types/yargs-parser": "^20.2.0",
"@typescript-eslint/eslint-plugin": "^4.18.0",
"@typescript-eslint/parser": "^4.18.0",
"cheerio": "^1.0.0-rc.5",
"concurrently": "^6.0.0",
"copyfiles": "^2.4.1",
"eslint": "^7.22.0",
@ -67,6 +69,7 @@
"preact": "^10.5.12",
"preact-render-to-string": "^5.1.14",
"prettier": "^2.2.1",
"typescript": "^4.2.3"
"typescript": "^4.2.3",
"uvu": "^0.5.1"
}
}

View file

@ -1,10 +1,10 @@
import type { AstroConfig } from './@types/astro';
import * as colors from 'kleur/colors';
import { join as pathJoin, resolve as pathResolve } from 'path';
import { existsSync, promises as fsPromises } from 'fs';
import { promises as fsPromises } from 'fs';
import yargs from 'yargs-parser';
import { loadConfig } from './config.js';
import generate from './generate.js';
import devServer from './dev.js';
@ -49,25 +49,6 @@ async function printVersion() {
console.error(pkg.version);
}
async function loadConfig(rawRoot: string | undefined): Promise<AstroConfig | undefined> {
if (typeof rawRoot === 'undefined') {
rawRoot = process.cwd();
}
const root = pathResolve(rawRoot);
const fileProtocolRoot = `file://${root}/`;
const astroConfigPath = pathJoin(root, 'astro.config.mjs');
if (!existsSync(astroConfigPath)) {
return undefined;
}
const astroConfig: AstroConfig = (await import(astroConfigPath)).default;
astroConfig.projectRoot = new URL(astroConfig.projectRoot + '/', fileProtocolRoot);
astroConfig.hmxRoot = new URL(astroConfig.hmxRoot + '/', fileProtocolRoot);
return astroConfig;
}
async function runCommand(rawRoot: string, cmd: (a: AstroConfig) => Promise<void>) {
const astroConfig = await loadConfig(rawRoot);
if (typeof astroConfig === 'undefined') {

View file

@ -1,6 +1,6 @@
import type { CompileOptions } from '../@types/compiler';
import type { Ast, TemplateNode } from '../compiler/interfaces';
import type { JsxItem, TransformResult } from '../@types/astro.js';
import type { JsxItem, TransformResult } from '../@types/astro';
import eslexer from 'es-module-lexer';
import esbuild from 'esbuild';
@ -61,7 +61,6 @@ function getAttributes(attrs: Attribute[]): Record<string, string> {
result[attr.name] = JSON.stringify(getTextFromAttribute(val));
continue;
default:
console.log(val);
throw new Error('UNKNOWN V');
}
}
@ -75,7 +74,6 @@ function getTextFromAttribute(attr: any): string {
if (attr.data !== undefined) {
return attr.data;
}
console.log(attr);
throw new Error('UNKNOWN attr');
}
@ -169,12 +167,11 @@ function compileScriptSafe(raw: string, loader: 'jsx' | 'tsx'): string {
export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Promise<TransformResult> {
await eslexer.init;
const script = compileScriptSafe(ast.instance ? ast.instance.content : '', 'tsx');
// Compile scripts as TypeScript, always
const script = compileScriptSafe(ast.instance ? ast.instance.content : '', 'tsx');
// Todo: Validate that `h` and `Fragment` aren't defined in the script
const [scriptImports] = eslexer.parse(script, 'optional-sourcename');
const components = Object.fromEntries(
scriptImports.map((imp) => {
@ -193,7 +190,6 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
walk(ast.html, {
enter(node: TemplateNode) {
// console.log("enter", node.type);
switch (node.type) {
case 'MustacheTag':
let code = compileScriptSafe(node.expression, 'jsx');
@ -238,7 +234,6 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
case 'Element':
const name: string = node.name;
if (!name) {
console.log(node);
throw new Error('AHHHH');
}
const attributes = getAttributes(node.attributes);
@ -298,12 +293,10 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
return;
}
default:
console.log(node);
throw new Error('Unexpected node type: ' + node.type);
}
},
leave(node, parent, prop, index) {
// console.log("leave", node.type);
switch (node.type) {
case 'Text':
case 'MustacheTag':

View file

@ -1,6 +1,7 @@
// @ts-nocheck
const now = (typeof process !== 'undefined' && process.hrtime)
const now =
typeof process !== 'undefined' && process.hrtime
? () => {
const t = process.hrtime();
return t[0] * 1e3 + t[1] / 1e6;
@ -16,10 +17,13 @@ interface Timing {
function collapse_timings(timings) {
const result = {};
timings.forEach(timing => {
result[timing.label] = Object.assign({
total: timing.end - timing.start
}, timing.children && collapse_timings(timing.children));
timings.forEach((timing) => {
result[timing.label] = Object.assign(
{
total: timing.end - timing.start,
},
timing.children && collapse_timings(timing.children)
);
});
return result;
}
@ -42,7 +46,7 @@ export default class Stats {
label,
start: now(),
end: null,
children: []
children: [],
};
this.current_children.push(timing);
@ -64,12 +68,15 @@ export default class Stats {
}
render() {
const timings = Object.assign({
total: now() - this.start_time
}, collapse_timings(this.timings));
const timings = Object.assign(
{
total: now() - this.start_time,
},
collapse_timings(this.timings)
);
return {
timings
timings,
};
}
}

View file

@ -5,14 +5,16 @@ import jsx from 'acorn-jsx';
const acornJsx = acorn.Parser.extend(jsx());
export const parse = (source: string): Node => acorn.parse(source, {
export const parse = (source: string): Node =>
acorn.parse(source, {
sourceType: 'module',
ecmaVersion: 2020,
locations: true
});
locations: true,
});
export const parse_expression_at = (source: string, index: number): Node => acornJsx.parseExpressionAt(source, index, {
export const parse_expression_at = (source: string, index: number): Node =>
acornJsx.parseExpressionAt(source, index, {
sourceType: 'module',
ecmaVersion: 2020,
locations: true
});
locations: true,
});

View file

@ -8,7 +8,7 @@ import full_char_code_at from '../utils/full_char_code_at.js';
import { TemplateNode, Ast, ParserOptions, Fragment, Style, Script } from '../interfaces.js';
import error from '../utils/error.js';
type ParserState = (parser: Parser) => (ParserState | void);
type ParserState = (parser: Parser) => ParserState | void;
interface LastAutoClosedTag {
tag: string;
@ -43,7 +43,7 @@ export class Parser {
start: null,
end: null,
type: 'Fragment',
children: []
children: [],
};
this.stack.push(this.html);
@ -60,16 +60,19 @@ export class Parser {
const type = current.type === 'Element' ? `<${current.name}>` : 'Block';
const slug = current.type === 'Element' ? 'element' : 'block';
this.error({
this.error(
{
code: `unclosed-${slug}`,
message: `${type} was left open`
}, current.start);
message: `${type} was left open`,
},
current.start
);
}
if (state !== fragment) {
this.error({
code: 'unexpected-eof',
message: 'Unexpected end of input'
message: 'Unexpected end of input',
});
}
@ -92,10 +95,13 @@ export class Parser {
}
acorn_error(err: any) {
this.error({
this.error(
{
code: 'parse-error',
message: err.message.replace(/ \(\d+:\d+\)$/, '')
}, err.pos);
message: err.message.replace(/ \(\d+:\d+\)$/, ''),
},
err.pos
);
}
error({ code, message }: { code: string; message: string }, index = this.index) {
@ -104,7 +110,7 @@ export class Parser {
code,
source: this.template,
start: index,
filename: this.filename
filename: this.filename,
});
}
@ -117,7 +123,7 @@ export class Parser {
if (required) {
this.error({
code: `unexpected-${this.index === this.template.length ? 'eof' : 'token'}`,
message: message || `Expected ${str}`
message: message || `Expected ${str}`,
});
}
@ -136,10 +142,7 @@ export class Parser {
}
allow_whitespace() {
while (
this.index < this.template.length &&
whitespace.test(this.template[this.index])
) {
while (this.index < this.template.length && whitespace.test(this.template[this.index])) {
this.index++;
}
}
@ -167,13 +170,16 @@ export class Parser {
i += code <= 0xffff ? 1 : 2;
}
const identifier = this.template.slice(this.index, this.index = i);
const identifier = this.template.slice(this.index, (this.index = i));
if (!allow_reserved && reserved.has(identifier)) {
this.error({
this.error(
{
code: 'unexpected-reserved-word',
message: `'${identifier}' is a reserved word in JavaScript and cannot be used here`
}, start);
message: `'${identifier}' is a reserved word in JavaScript and cannot be used here`,
},
start
);
}
return identifier;
@ -183,7 +189,7 @@ export class Parser {
if (this.index >= this.template.length) {
this.error({
code: 'unexpected-eof',
message: 'Unexpected end of input'
message: 'Unexpected end of input',
});
}
@ -203,7 +209,7 @@ export class Parser {
if (!whitespace.test(this.template[this.index])) {
this.error({
code: 'missing-whitespace',
message: 'Expected whitespace'
message: 'Expected whitespace',
});
}
@ -211,42 +217,48 @@ export class Parser {
}
}
export default function parse(
template: string,
options: ParserOptions = {}
): Ast {
export default function parse(template: string, options: ParserOptions = {}): Ast {
const parser = new Parser(template, options);
// TODO we may want to allow multiple <style> tags —
// one scoped, one global. for now, only allow one
if (parser.css.length > 1) {
parser.error({
parser.error(
{
code: 'duplicate-style',
message: 'You can only have one top-level <style> tag per component'
}, parser.css[1].start);
message: 'You can only have one top-level <style> tag per component',
},
parser.css[1].start
);
}
const instance_scripts = parser.js.filter(script => script.context === 'default');
const module_scripts = parser.js.filter(script => script.context === 'module');
const instance_scripts = parser.js.filter((script) => script.context === 'default');
const module_scripts = parser.js.filter((script) => script.context === 'module');
if (instance_scripts.length > 1) {
parser.error({
parser.error(
{
code: 'invalid-script',
message: 'A component can only have one instance-level <script> element'
}, instance_scripts[1].start);
message: 'A component can only have one instance-level <script> element',
},
instance_scripts[1].start
);
}
if (module_scripts.length > 1) {
parser.error({
parser.error(
{
code: 'invalid-script',
message: 'A component can only have one <script context="module"> element'
}, module_scripts[1].start);
message: 'A component can only have one <script context="module"> element',
},
module_scripts[1].start
);
}
return {
html: parser.html,
css: parser.css[0],
instance: instance_scripts[0],
module: module_scripts[0]
module: module_scripts[0],
};
}

View file

@ -3,18 +3,11 @@
import { Parser } from '../index.js';
import { isIdentifierStart } from 'acorn';
import full_char_code_at from '../../utils/full_char_code_at.js';
import {
is_bracket_open,
is_bracket_close,
is_bracket_pair,
get_bracket_close
} from '../utils/bracket.js';
import { is_bracket_open, is_bracket_close, is_bracket_pair, get_bracket_close } from '../utils/bracket.js';
import { parse_expression_at } from '../acorn.js';
import { Pattern } from 'estree';
export default function read_context(
parser: Parser
): Pattern & { start: number; end: number } {
export default function read_context(parser: Parser): Pattern & { start: number; end: number } {
const start = parser.index;
let i = parser.index;
@ -24,14 +17,14 @@ export default function read_context(
type: 'Identifier',
name: parser.read_identifier(),
start,
end: parser.index
end: parser.index,
};
}
if (!is_bracket_open(code)) {
parser.error({
code: 'unexpected-token',
message: 'Expected identifier or destructure pattern'
message: 'Expected identifier or destructure pattern',
});
}
@ -46,9 +39,7 @@ export default function read_context(
if (!is_bracket_pair(bracket_stack[bracket_stack.length - 1], code)) {
parser.error({
code: 'unexpected-token',
message: `Expected ${String.fromCharCode(
get_bracket_close(bracket_stack[bracket_stack.length - 1])
)}`
message: `Expected ${String.fromCharCode(get_bracket_close(bracket_stack[bracket_stack.length - 1]))}`,
});
}
bracket_stack.pop();
@ -74,10 +65,7 @@ export default function read_context(
const first_space = space_with_newline.indexOf(' ');
space_with_newline = space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
return (parse_expression_at(
`${space_with_newline}(${pattern_string} = 1)`,
start - 1
) as any).left;
return (parse_expression_at(`${space_with_newline}(${pattern_string} = 1)`, start - 1) as any).left;
} catch (error) {
parser.acorn_error(error);
}

View file

@ -22,10 +22,13 @@ export default function read_expression(parser: Parser): string {
if (char === ')') {
num_parens -= 1;
} else if (!whitespace.test(char)) {
parser.error({
parser.error(
{
code: 'unexpected-token',
message: 'Expected )'
}, index);
message: 'Expected )',
},
index
);
}
index += 1;

View file

@ -8,23 +8,29 @@ import { Node, Program } from 'estree';
const script_closing_tag = '</script>';
function get_context(parser: Parser, attributes: any[], start: number): string {
const context = attributes.find(attribute => attribute.name === 'context');
const context = attributes.find((attribute) => attribute.name === 'context');
if (!context) return 'default';
if (context.value.length !== 1 || context.value[0].type !== 'Text') {
parser.error({
parser.error(
{
code: 'invalid-script',
message: 'context attribute must be static'
}, start);
message: 'context attribute must be static',
},
start
);
}
const value = context.value[0].data;
if (value !== 'module') {
parser.error({
parser.error(
{
code: 'invalid-script',
message: 'If the context attribute is supplied, its value must be "module"'
}, context.start);
message: 'If the context attribute is supplied, its value must be "module"',
},
context.start
);
}
return value;
@ -37,12 +43,11 @@ export default function read_script(parser: Parser, start: number, attributes: N
if (script_end === -1) {
parser.error({
code: 'unclosed-script',
message: '<script> must have a closing tag'
message: '<script> must have a closing tag',
});
}
const source = parser.template.slice(0, script_start).replace(/[^\n]/g, ' ') +
parser.template.slice(script_start, script_end);
const source = parser.template.slice(0, script_start).replace(/[^\n]/g, ' ') + parser.template.slice(script_start, script_end);
parser.index = script_end + script_closing_tag.length;
return {
@ -50,6 +55,6 @@ export default function read_script(parser: Parser, start: number, attributes: N
start,
end: parser.index,
context: get_context(parser, attributes, start),
content: source
content: source,
};
}

View file

@ -70,7 +70,7 @@ export default function mustache(parser: Parser) {
} else {
parser.error({
code: 'unexpected-block-close',
message: 'Unexpected block closing tag'
message: 'Unexpected block closing tag',
});
}
@ -102,7 +102,7 @@ export default function mustache(parser: Parser) {
if (parser.eat('if')) {
parser.error({
code: 'invalid-elseif',
message: "'elseif' should be 'else if'"
message: "'elseif' should be 'else if'",
});
}
@ -114,9 +114,9 @@ export default function mustache(parser: Parser) {
if (block.type !== 'IfBlock') {
parser.error({
code: 'invalid-elseif-placement',
message: parser.stack.some(block => block.type === 'IfBlock')
message: parser.stack.some((block) => block.type === 'IfBlock')
? `Expected to close ${to_string(block)} before seeing {:else if ...} block`
: 'Cannot have an {:else if ...} block outside an {#if ...} block'
: 'Cannot have an {:else if ...} block outside an {#if ...} block',
});
}
@ -138,9 +138,9 @@ export default function mustache(parser: Parser) {
type: 'IfBlock',
elseif: true,
expression,
children: []
}
]
children: [],
},
],
};
parser.stack.push(block.else.children[0]);
@ -150,9 +150,9 @@ export default function mustache(parser: Parser) {
if (block.type !== 'IfBlock' && block.type !== 'EachBlock') {
parser.error({
code: 'invalid-else-placement',
message: parser.stack.some(block => block.type === 'IfBlock' || block.type === 'EachBlock')
message: parser.stack.some((block) => block.type === 'IfBlock' || block.type === 'EachBlock')
? `Expected to close ${to_string(block)} before seeing {:else} block`
: 'Cannot have an {:else} block outside an {#if ...} or {#each ...} block'
: 'Cannot have an {:else} block outside an {#if ...} or {#each ...} block',
});
}
@ -163,7 +163,7 @@ export default function mustache(parser: Parser) {
start: parser.index,
end: null,
type: 'ElseBlock',
children: []
children: [],
};
parser.stack.push(block.else);
@ -176,18 +176,18 @@ export default function mustache(parser: Parser) {
if (block.type !== 'PendingBlock') {
parser.error({
code: 'invalid-then-placement',
message: parser.stack.some(block => block.type === 'PendingBlock')
message: parser.stack.some((block) => block.type === 'PendingBlock')
? `Expected to close ${to_string(block)} before seeing {:then} block`
: 'Cannot have an {:then} block outside an {#await ...} block'
: 'Cannot have an {:then} block outside an {#await ...} block',
});
}
} else {
if (block.type !== 'ThenBlock' && block.type !== 'PendingBlock') {
parser.error({
code: 'invalid-catch-placement',
message: parser.stack.some(block => block.type === 'ThenBlock' || block.type === 'PendingBlock')
message: parser.stack.some((block) => block.type === 'ThenBlock' || block.type === 'PendingBlock')
? `Expected to close ${to_string(block)} before seeing {:catch} block`
: 'Cannot have an {:catch} block outside an {#await ...} block'
: 'Cannot have an {:catch} block outside an {#await ...} block',
});
}
}
@ -209,7 +209,7 @@ export default function mustache(parser: Parser) {
end: null,
type: is_then ? 'ThenBlock' : 'CatchBlock',
children: [],
skip: false
skip: false,
};
await_block[is_then ? 'then' : 'catch'] = new_block;
@ -229,7 +229,7 @@ export default function mustache(parser: Parser) {
} else {
parser.error({
code: 'expected-block-type',
message: 'Expected if, each, await or key'
message: 'Expected if, each, await or key',
});
}
@ -238,8 +238,9 @@ export default function mustache(parser: Parser) {
const expression = read_expression(parser);
// @ts-ignore
const block: TemplateNode = type === 'AwaitBlock' ?
{
const block: TemplateNode =
type === 'AwaitBlock'
? {
start,
end: null,
type,
@ -251,29 +252,29 @@ export default function mustache(parser: Parser) {
end: null,
type: 'PendingBlock',
children: [],
skip: true
skip: true,
},
then: {
start: null,
end: null,
type: 'ThenBlock',
children: [],
skip: true
skip: true,
},
catch: {
start: null,
end: null,
type: 'CatchBlock',
children: [],
skip: true
skip: true,
},
}
} :
{
: {
start,
end: null,
type,
expression,
children: []
children: [],
};
parser.allow_whitespace();
@ -293,7 +294,7 @@ export default function mustache(parser: Parser) {
if (!block.index) {
parser.error({
code: 'expected-name',
message: 'Expected name'
message: 'Expected name',
});
}
@ -360,7 +361,7 @@ export default function mustache(parser: Parser) {
start,
end: parser.index,
type: 'RawMustacheTag',
expression
expression,
});
} else if (parser.eat('@debug')) {
// let identifiers;
@ -406,7 +407,7 @@ export default function mustache(parser: Parser) {
start,
end: parser.index,
type: 'MustacheTag',
expression
expression,
});
}
}

View file

@ -17,7 +17,7 @@ const meta_tags = new Map([
['svelte:head', 'Head'],
['svelte:options', 'Options'],
['svelte:window', 'Window'],
['svelte:body', 'Body']
['svelte:body', 'Body'],
]);
const valid_meta_tags = Array.from(meta_tags.keys()).concat('svelte:self', 'svelte:component', 'svelte:fragment');
@ -27,16 +27,16 @@ const specials = new Map([
'script',
{
read: read_script,
property: 'js'
}
property: 'js',
},
],
[
'style',
{
read: read_style,
property: 'css'
}
]
property: 'css',
},
],
]);
const SELF = /^svelte:self(?=[\s/>])/;
@ -66,7 +66,7 @@ export default function tag(parser: Parser) {
start,
end: parser.index,
type: 'Comment',
data
data,
});
return;
@ -79,28 +79,34 @@ export default function tag(parser: Parser) {
if (meta_tags.has(name)) {
const slug = meta_tags.get(name).toLowerCase();
if (is_closing_tag) {
if (
(name === 'svelte:window' || name === 'svelte:body') &&
parser.current().children.length
) {
parser.error({
if ((name === 'svelte:window' || name === 'svelte:body') && parser.current().children.length) {
parser.error(
{
code: `invalid-${slug}-content`,
message: `<${name}> cannot have children`
}, parser.current().children[0].start);
message: `<${name}> cannot have children`,
},
parser.current().children[0].start
);
}
} else {
if (name in parser.meta_tags) {
parser.error({
parser.error(
{
code: `duplicate-${slug}`,
message: `A component can only have one <${name}> tag`
}, start);
message: `A component can only have one <${name}> tag`,
},
start
);
}
if (parser.stack.length > 1) {
parser.error({
parser.error(
{
code: `invalid-${slug}-placement`,
message: `<${name}> tags cannot be inside elements or blocks`
}, start);
message: `<${name}> tags cannot be inside elements or blocks`,
},
start
);
}
parser.meta_tags[name] = true;
@ -109,10 +115,15 @@ export default function tag(parser: Parser) {
const type = meta_tags.has(name)
? meta_tags.get(name)
: (/[A-Z]/.test(name[0]) || name === 'svelte:self' || name === 'svelte:component') ? 'InlineComponent'
: name === 'svelte:fragment' ? 'SlotTemplate'
: name === 'title' && parent_is_head(parser.stack) ? 'Title'
: name === 'slot' && !parser.customElement ? 'Slot' : 'Element';
: /[A-Z]/.test(name[0]) || name === 'svelte:self' || name === 'svelte:component'
? 'InlineComponent'
: name === 'svelte:fragment'
? 'SlotTemplate'
: name === 'title' && parent_is_head(parser.stack)
? 'Title'
: name === 'slot' && !parser.customElement
? 'Slot'
: 'Element';
const element: TemplateNode = {
start,
@ -120,17 +131,20 @@ export default function tag(parser: Parser) {
type,
name,
attributes: [],
children: []
children: [],
};
parser.allow_whitespace();
if (is_closing_tag) {
if (is_void(name)) {
parser.error({
parser.error(
{
code: 'invalid-void-content',
message: `<${name}> is a void element and cannot have children, or a closing tag`
}, start);
message: `<${name}> is a void element and cannot have children, or a closing tag`,
},
start
);
}
parser.eat('>', true);
@ -138,13 +152,17 @@ export default function tag(parser: Parser) {
// close any elements that don't have their own closing tags, e.g. <div><p></div>
while (parent.name !== name) {
if (parent.type !== 'Element') {
const message = parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name
const message =
parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name
? `</${name}> attempted to close <${name}> that was already automatically closed by <${parser.last_auto_closed_tag.reason}>`
: `</${name}> attempted to close an element that was not open`;
parser.error({
parser.error(
{
code: 'invalid-closing-tag',
message
}, start);
message,
},
start
);
}
parent.end = start;
@ -167,7 +185,7 @@ export default function tag(parser: Parser) {
parser.last_auto_closed_tag = {
tag: parent.name,
reason: name,
depth: parser.stack.length
depth: parser.stack.length,
};
}
@ -180,20 +198,26 @@ export default function tag(parser: Parser) {
}
if (name === 'svelte:component') {
const index = element.attributes.findIndex(attr => attr.type === 'Attribute' && attr.name === 'this');
const index = element.attributes.findIndex((attr) => attr.type === 'Attribute' && attr.name === 'this');
if (!~index) {
parser.error({
parser.error(
{
code: 'missing-component-definition',
message: "<svelte:component> must have a 'this' attribute"
}, start);
message: "<svelte:component> must have a 'this' attribute",
},
start
);
}
const definition = element.attributes.splice(index, 1)[0];
if (definition.value === true || definition.value.length !== 1 || definition.value[0].type === 'Text') {
parser.error({
parser.error(
{
code: 'invalid-component-definition',
message: 'invalid component definition'
}, definition.start);
message: 'invalid component definition',
},
definition.start
);
}
element.expression = definition.value[0].expression;
@ -220,11 +244,7 @@ export default function tag(parser: Parser) {
element.end = parser.index;
} else if (name === 'textarea') {
// special case
element.children = read_sequence(
parser,
() =>
parser.template.slice(parser.index, parser.index + 11) === '</textarea>'
);
element.children = read_sequence(parser, () => parser.template.slice(parser.index, parser.index + 11) === '</textarea>');
parser.read(/<\/textarea>/);
element.end = parser.index;
} else if (name === 'script' || name === 'style') {
@ -258,10 +278,13 @@ function read_tag_name(parser: Parser) {
}
if (!legal) {
parser.error({
parser.error(
{
code: 'invalid-self-placement',
message: '<svelte:self> components can only exist inside {#if} blocks, {#each} blocks, or slots passed to components'
}, start);
message: '<svelte:self> components can only exist inside {#if} blocks, {#each} blocks, or slots passed to components',
},
start
);
}
return 'svelte:self';
@ -281,17 +304,23 @@ function read_tag_name(parser: Parser) {
let message = `Valid <svelte:...> tag names are ${list(valid_meta_tags)}`;
if (match) message += ` (did you mean '${match}'?)`;
parser.error({
parser.error(
{
code: 'invalid-tag-name',
message
}, start);
message,
},
start
);
}
if (!valid_tag_name.test(name)) {
parser.error({
parser.error(
{
code: 'invalid-tag-name',
message: 'Expected valid tag name'
}, start);
message: 'Expected valid tag name',
},
start
);
}
return name;
@ -302,10 +331,13 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
function check_unique(name: string) {
if (unique_names.has(name)) {
parser.error({
parser.error(
{
code: 'duplicate-attribute',
message: 'Attributes need to be unique'
}, start);
message: 'Attributes need to be unique',
},
start
);
}
unique_names.add(name);
}
@ -323,7 +355,7 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
start,
end: parser.index,
type: 'Spread',
expression
expression,
};
} else {
const value_start = parser.index;
@ -339,7 +371,8 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
end: parser.index,
type: 'Attribute',
name,
value: [{
value: [
{
start: value_start,
end: value_start + name.length,
type: 'AttributeShorthand',
@ -347,9 +380,10 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
start: value_start,
end: value_start + name.length,
type: 'Identifier',
name
}
}]
name,
},
},
],
};
}
}
@ -371,10 +405,13 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
value = read_attribute_value(parser);
end = parser.index;
} else if (parser.match_regex(/["']/)) {
parser.error({
parser.error(
{
code: 'unexpected-token',
message: 'Expected ='
}, parser.index);
message: 'Expected =',
},
parser.index
);
}
if (type) {
@ -387,25 +424,34 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
}
if (type === 'Ref') {
parser.error({
parser.error(
{
code: 'invalid-ref-directive',
message: `The ref directive is no longer supported — use \`bind:this={${directive_name}}\` instead`
}, start);
message: `The ref directive is no longer supported — use \`bind:this={${directive_name}}\` instead`,
},
start
);
}
if (type === 'Class' && directive_name === '') {
parser.error({
parser.error(
{
code: 'invalid-class-directive',
message: 'Class binding name cannot be empty'
}, start + colon_index + 1);
message: 'Class binding name cannot be empty',
},
start + colon_index + 1
);
}
if (value[0]) {
if ((value as any[]).length > 1 || value[0].type === 'Text') {
parser.error({
parser.error(
{
code: 'invalid-directive-value',
message: 'Directive value must be a JavaScript expression enclosed in curly braces'
}, value[0].start);
message: 'Directive value must be a JavaScript expression enclosed in curly braces',
},
value[0].start
);
}
}
@ -415,7 +461,7 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
type,
name: directive_name,
modifiers,
expression: (value[0] && value[0].expression) || null
expression: (value[0] && value[0].expression) || null,
};
if (type === 'Transition') {
@ -429,7 +475,7 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
start: directive.start + colon_index + 1,
end: directive.end,
type: 'Identifier',
name: directive.name
name: directive.name,
} as any;
}
@ -443,7 +489,7 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
end,
type: 'Attribute',
name,
value
value,
};
}
@ -461,11 +507,7 @@ function get_directive_type(name: string): DirectiveType {
function read_attribute_value(parser: Parser) {
const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null;
const regex = (
quote_mark === "'" ? /'/ :
quote_mark === '"' ? /"/ :
/(\/>|[\s"'=<>`])/
);
const regex = quote_mark === "'" ? /'/ : quote_mark === '"' ? /"/ : /(\/>|[\s"'=<>`])/;
const value = read_sequence(parser, () => !!parser.match_regex(regex));
@ -479,7 +521,7 @@ function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] {
end: null,
type: 'Text',
raw: '',
data: null
data: null,
};
function flush() {
@ -510,7 +552,7 @@ function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] {
start: index,
end: parser.index,
type: 'MustacheTag',
expression
expression,
});
current_chunk = {
@ -518,7 +560,7 @@ function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] {
end: null,
type: 'Text',
raw: '',
data: null
data: null,
};
} else {
current_chunk.raw += parser.template[parser.index++];
@ -527,6 +569,6 @@ function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] {
parser.error({
code: 'unexpected-eof',
message: 'Unexpected end of input'
message: 'Unexpected end of input',
});
}

View file

@ -8,11 +8,7 @@ export default function text(parser: Parser) {
let data = '';
while (
parser.index < parser.template.length &&
!parser.match('<') &&
!parser.match('{')
) {
while (parser.index < parser.template.length && !parser.match('<') && !parser.match('{')) {
data += parser.template[parser.index++];
}
@ -21,7 +17,7 @@ export default function text(parser: Parser) {
end: parser.index,
type: 'Text',
raw: data,
data: decode_character_references(data)
data: decode_character_references(data),
};
parser.current().children.push(node);

View file

@ -14,10 +14,7 @@ export function is_bracket_close(code) {
}
export function is_bracket_pair(open, close) {
return (
(open === SQUARE_BRACKET_OPEN && close === SQUARE_BRACKET_CLOSE) ||
(open === CURLY_BRACKET_OPEN && close === CURLY_BRACKET_CLOSE)
);
return (open === SQUARE_BRACKET_OPEN && close === SQUARE_BRACKET_CLOSE) || (open === CURLY_BRACKET_OPEN && close === CURLY_BRACKET_CLOSE);
}
export function get_bracket_close(open) {

View file

@ -2030,5 +2030,5 @@ export default {
sc: 8827,
wp: 8472,
wr: 8768,
xi: 958
xi: 958,
};

View file

@ -34,13 +34,10 @@ const windows_1252 = [
339,
157,
382,
376
376,
];
const entity_pattern = new RegExp(
`&(#?(?:x[\\w\\d]+|\\d+|${Object.keys(entities).join('|')}))(?:;|\\b)`,
'g'
);
const entity_pattern = new RegExp(`&(#?(?:x[\\w\\d]+|\\d+|${Object.keys(entities).join('|')}))(?:;|\\b)`, 'g');
export function decode_character_references(html: string) {
return html.replace(entity_pattern, (match, entity) => {
@ -120,14 +117,7 @@ const disallowed_contents = new Map([
['li', new Set(['li'])],
['dt', new Set(['dt', 'dd'])],
['dd', new Set(['dt', 'dd'])],
[
'p',
new Set(
'address article aside blockquote div dl fieldset footer form h1 h2 h3 h4 h5 h6 header hgroup hr main menu nav ol p pre section table ul'.split(
' '
)
)
],
['p', new Set('address article aside blockquote div dl fieldset footer form h1 h2 h3 h4 h5 h6 header hgroup hr main menu nav ol p pre section table ul'.split(' '))],
['rt', new Set(['rt', 'rp'])],
['rp', new Set(['rt', 'rp'])],
['optgroup', new Set(['optgroup'])],
@ -137,7 +127,7 @@ const disallowed_contents = new Map([
['tfoot', new Set(['tbody'])],
['tr', new Set(['tr', 'tbody'])],
['td', new Set(['td', 'th', 'tr'])],
['th', new Set(['td', 'th', 'tr'])]
['th', new Set(['td', 'th', 'tr'])],
]);
// can this be a child of the parent element, or does it implicitly

View file

@ -16,14 +16,17 @@ export class CompileError extends Error {
}
}
export default function error(message: string, props: {
export default function error(
message: string,
props: {
name: string;
code: string;
source: string;
filename: string;
start: number;
end?: number;
}): never {
}
): never {
const error = new CompileError(message);
error.name = props.name;

View file

@ -159,11 +159,7 @@ class FuzzySet {
let results = [];
// start with high gram size and if there are no results, go to lower gram sizes
for (
let gram_size = GRAM_SIZE_UPPER;
gram_size >= GRAM_SIZE_LOWER;
--gram_size
) {
for (let gram_size = GRAM_SIZE_UPPER; gram_size >= GRAM_SIZE_LOWER; --gram_size) {
results = this.__get(value, gram_size);
if (results) {
return results;
@ -207,10 +203,7 @@ class FuzzySet {
// build a results list of [score, str]
for (const match_index in matches) {
match_score = matches[match_index];
results.push([
match_score / (vector_normal * items[match_index][0]),
items[match_index][1]
]);
results.push([match_score / (vector_normal * items[match_index][0]), items[match_index][1]]);
}
results.sort(sort_descending);
@ -219,10 +212,7 @@ class FuzzySet {
const end_index = Math.min(50, results.length);
// truncate somewhat arbitrarily to 50
for (let i = 0; i < end_index; ++i) {
new_results.push([
_distance(results[i][1], normalized_value),
results[i][1]
]);
new_results.push([_distance(results[i][1], normalized_value), results[i][1]]);
}
results = new_results;
results.sort(sort_descending);

View file

@ -1,12 +1,8 @@
function tabs_to_spaces(str: string) {
return str.replace(/^\t+/, match => match.split('\t').join(' '));
return str.replace(/^\t+/, (match) => match.split('\t').join(' '));
}
export default function get_code_frame(
source: string,
line: number,
column: number
) {
export default function get_code_frame(source: string, line: number, column: number) {
const lines = source.split('\n');
const frame_start = Math.max(0, line - 2);

View file

@ -1,6 +1,4 @@
export default function list(items: string[], conjunction = 'or') {
if (items.length === 1) return items[0];
return `${items.slice(0, -1).join(', ')} ${conjunction} ${items[
items.length - 1
]}`;
return `${items.slice(0, -1).join(', ')} ${conjunction} ${items[items.length - 1]}`;
}

View file

@ -57,7 +57,7 @@ export const globals = new Set([
'undefined',
'URIError',
'URL',
'window'
'window',
]);
export const reserved = new Set([
@ -108,7 +108,7 @@ export const reserved = new Set([
'void',
'while',
'with',
'yield'
'yield',
]);
const void_element_names = /^(?:area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/;

View file

@ -8,21 +8,6 @@ export const xlink = 'http://www.w3.org/1999/xlink';
export const xml = 'http://www.w3.org/XML/1998/namespace';
export const xmlns = 'http://www.w3.org/2000/xmlns';
export const valid_namespaces = [
'foreign',
'html',
'mathml',
'svg',
'xlink',
'xml',
'xmlns',
foreign,
html,
mathml,
svg,
xlink,
xml,
xmlns
];
export const valid_namespaces = ['foreign', 'html', 'mathml', 'svg', 'xlink', 'xml', 'xmlns', foreign, html, mathml, svg, xlink, xml, xmlns];
export const namespaces: Record<string, string> = { foreign, html, mathml, svg, xlink, xml, xmlns };

22
src/config.ts Normal file
View file

@ -0,0 +1,22 @@
import type { AstroConfig } from './@types/astro';
import { join as pathJoin, resolve as pathResolve } from 'path';
import { existsSync } from 'fs';
export async function loadConfig(rawRoot: string | undefined): Promise<AstroConfig | undefined> {
if (typeof rawRoot === 'undefined') {
rawRoot = process.cwd();
}
const root = pathResolve(rawRoot);
const fileProtocolRoot = `file://${root}/`;
const astroConfigPath = pathJoin(root, 'astro.config.mjs');
if (!existsSync(astroConfigPath)) {
return undefined;
}
const astroConfig: AstroConfig = (await import(astroConfigPath)).default;
astroConfig.projectRoot = new URL(astroConfig.projectRoot + '/', fileProtocolRoot);
astroConfig.hmxRoot = new URL(astroConfig.hmxRoot + '/', fileProtocolRoot);
return astroConfig;
}

View file

@ -1,12 +1,11 @@
import type { AstroConfig } from './@types/astro';
import type { LogOptions } from './logger.js';
import { loadConfiguration, startServer as startSnowpackServer, logger as snowpackLogger } from 'snowpack';
import { existsSync, promises as fsPromises } from 'fs';
import { logger as snowpackLogger } from 'snowpack';
import http from 'http';
import { relative as pathRelative } from 'path';
import { defaultLogDestination, info, error, parseError } from './logger.js';
const { readFile } = fsPromises;
import { defaultLogDestination, error, parseError } from './logger.js';
import { createRuntime } from './runtime.js';
const hostname = '127.0.0.1';
const port = 3000;
@ -20,96 +19,46 @@ const logging: LogOptions = {
};
export default async function (astroConfig: AstroConfig) {
const { projectRoot, hmxRoot } = astroConfig;
const { projectRoot } = astroConfig;
const internalPath = new URL('./frontend/', import.meta.url);
const snowpackConfigPath = new URL('./snowpack.config.js', projectRoot);
// Workaround for SKY-251
const hmxPlugOptions: { resolve?: (s: string) => string } = {};
if (existsSync(new URL('./package-lock.json', projectRoot))) {
const pkgLockStr = await readFile(new URL('./package-lock.json', projectRoot), 'utf-8');
const pkgLock = JSON.parse(pkgLockStr);
hmxPlugOptions.resolve = (pkgName: string) => {
const ver = pkgLock.dependencies[pkgName].version;
return `/_snowpack/pkg/${pkgName}.v${ver}.js`;
};
}
const snowpackConfig = await loadConfiguration(
{
root: projectRoot.pathname,
mount: {
[hmxRoot.pathname]: '/_hmx',
[internalPath.pathname]: '/__hmx_internal__',
},
plugins: [['astro/snowpack-plugin', hmxPlugOptions]],
devOptions: {
open: 'none',
output: 'stream',
port: 0,
},
packageOptions: {
knownEntrypoints: ['preact-render-to-string'],
external: ['@vue/server-renderer'],
},
},
snowpackConfigPath.pathname
);
const snowpack = await startSnowpackServer({
config: snowpackConfig,
lockfile: null,
});
const runtime = snowpack.getServerRuntime();
const runtime = await createRuntime(astroConfig, logging);
const server = http.createServer(async (req, res) => {
const fullurl = new URL(req.url || '/', 'https://example.org/');
const reqPath = decodeURI(fullurl.pathname);
const selectedPage = reqPath.substr(1) || 'index';
info(logging, 'access', reqPath);
const result = await runtime.load(req.url);
const selectedPageLoc = new URL(`./pages/${selectedPage}.hmx`, hmxRoot);
const selectedPageMdLoc = new URL(`./pages/${selectedPage}.md`, hmxRoot);
const selectedPageUrl = `/_hmx/pages/${selectedPage}.js`;
// Non-hmx pages
if (!existsSync(selectedPageLoc) && !existsSync(selectedPageMdLoc)) {
try {
const result = await snowpack.loadUrl(reqPath);
switch (result.statusCode) {
case 200: {
if (result.contentType) {
res.setHeader('Content-Type', result.contentType);
}
res.write(result.contents);
res.end();
} catch (err) {
break;
}
case 404: {
const fullurl = new URL(req.url || '/', 'https://example.org/');
const reqPath = decodeURI(fullurl.pathname);
error(logging, 'static', 'Not found', reqPath);
res.statusCode = 404;
res.setHeader('Content-Type', 'text/plain');
res.end('Not Found');
break;
}
return;
}
try {
const mod = await runtime.importModule(selectedPageUrl);
const html = await mod.exports.default();
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end(html);
} catch (err) {
switch (err.code) {
case 500: {
switch (result.type) {
case 'parse-error': {
const err = result.error;
err.filename = pathRelative(projectRoot.pathname, err.filename);
debugger;
parseError(logging, err);
break;
}
default: {
console.error(err.code, err);
error(logging, 'running hmx', err);
error(logging, 'executing hmx', result.error);
break;
}
}
break;
}
}
});

View file

@ -1,31 +1,32 @@
import type { CompileError } from './compiler/utils/error.js';
import { bold, blue, red, grey, underline } from 'kleur/colors';
import { Writable } from 'stream';
import { format as utilFormat } from 'util';
type ConsoleStream = Writable & {
fd: 1 | 2
fd: 1 | 2;
};
export const defaultLogDestination = new Writable({
objectMode: true,
write(event: LogMessage, _, callback) {
let dest: ConsoleStream = process.stderr;
if(levels[event.level] < levels['error']) {
if (levels[event.level] < levels['error']) {
dest = process.stdout;
}
let type = event.type;
if(event.level === 'info') {
if (event.level === 'info') {
type = bold(blue(type));
} else if(event.level === 'error') {
} else if (event.level === 'error') {
type = bold(red(type));
}
dest.write(`[${type}] `);
dest.write(event.message);
dest.write(utilFormat(...event.args));
dest.write('\n');
callback();
}
},
});
interface LogWritable<T> extends Writable {
@ -37,18 +38,19 @@ export type LoggerEvent = 'debug' | 'info' | 'warn' | 'error';
export interface LogOptions {
dest: LogWritable<LogMessage>;
level: LoggerLevel
level: LoggerLevel;
}
export const defaultLogOptions: LogOptions = {
dest: defaultLogDestination,
level: 'info'
level: 'info',
};
export interface LogMessage {
type: string;
level: LoggerLevel,
level: LoggerLevel;
message: string;
args: Array<any>;
}
const levels: Record<LoggerLevel, number> = {
@ -59,19 +61,14 @@ const levels: Record<LoggerLevel, number> = {
silent: 90,
};
export function log(opts: LogOptions = defaultLogOptions, level: LoggerLevel, type: string, ...messages: Array<any>) {
let event: LogMessage = {
export function log(opts: LogOptions = defaultLogOptions, level: LoggerLevel, type: string, ...args: Array<any>) {
const event: LogMessage = {
type,
level,
message: ''
args,
message: '',
};
if(messages.length === 1 && typeof messages[0] === 'object') {
Object.assign(event, messages[0]);
} else {
event.message = messages.join(' ');
}
// test if this level is enabled or not
if (levels[opts.level] > levels[level]) {
return; // do nothing
@ -99,20 +96,24 @@ export function error(opts: LogOptions, type: string, ...messages: Array<any>) {
export function parseError(opts: LogOptions, err: CompileError) {
let frame = err.frame
// Switch colons for pipes
.replace(/^([0-9]+)(:)/mg, `${bold('$1')}`)
.replace(/^([0-9]+)(:)/gm, `${bold('$1')}`)
// Make the caret red.
.replace(/(?<=^\s+)(\^)/mg, bold(red(' ^')))
.replace(/(?<=^\s+)(\^)/gm, bold(red(' ^')))
// Add identation
.replace(/^/mg, ' ');
.replace(/^/gm, ' ');
error(opts, 'parse-error', `
error(
opts,
'parse-error',
`
${underline(bold(grey(`${err.filename}:${err.start.line}:${err.start.column}`)))}
${bold(red(`𝘅 ${err.message}`))}
${frame}
`);
`
);
}
// A default logger for when too lazy to pass LogOptions around.
@ -120,5 +121,5 @@ export const logger = {
debug: debug.bind(null, defaultLogOptions),
info: info.bind(null, defaultLogOptions),
warn: warn.bind(null, defaultLogOptions),
error: error.bind(null, defaultLogOptions)
error: error.bind(null, defaultLogOptions),
};

137
src/runtime.ts Normal file
View file

@ -0,0 +1,137 @@
import type { SnowpackDevServer, ServerRuntime as SnowpackServerRuntime, LoadResult as SnowpackLoadResult } from 'snowpack';
import type { AstroConfig } from './@types/astro';
import type { LogOptions } from './logger';
import type { CompileError } from './compiler/utils/error.js';
import { info, error, parseError } from './logger.js';
import { existsSync, promises as fsPromises } from 'fs';
import { loadConfiguration, startServer as startSnowpackServer } from 'snowpack';
const { readFile } = fsPromises;
interface RuntimeConfig {
astroConfig: AstroConfig;
logging: LogOptions;
snowpack: SnowpackDevServer;
snowpackRuntime: SnowpackServerRuntime;
}
type LoadResultSuccess = {
statusCode: 200;
contents: string | Buffer;
contentType?: string | false;
};
type LoadResultNotFound = { statusCode: 404; error: Error };
type LoadResultError = { statusCode: 500 } & ({ type: 'parse-error'; error: CompileError } | { type: 'unknown'; error: Error });
export type LoadResult = LoadResultSuccess | LoadResultNotFound | LoadResultError;
async function load(config: RuntimeConfig, rawPathname: string | undefined): Promise<LoadResult> {
const { logging, snowpack, snowpackRuntime } = config;
const { hmxRoot } = config.astroConfig;
const fullurl = new URL(rawPathname || '/', 'https://example.org/');
const reqPath = decodeURI(fullurl.pathname);
const selectedPage = reqPath.substr(1) || 'index';
info(logging, 'access', reqPath);
const selectedPageLoc = new URL(`./pages/${selectedPage}.hmx`, hmxRoot);
const selectedPageMdLoc = new URL(`./pages/${selectedPage}.md`, hmxRoot);
const selectedPageUrl = `/_hmx/pages/${selectedPage}.js`;
// Non-hmx pages
if (!existsSync(selectedPageLoc) && !existsSync(selectedPageMdLoc)) {
try {
const result = await snowpack.loadUrl(reqPath);
return {
statusCode: 200,
...result,
};
} catch (err) {
return {
statusCode: 404,
error: err,
};
}
}
try {
const mod = await snowpackRuntime.importModule(selectedPageUrl);
const html = (await mod.exports.default()) as string;
return {
statusCode: 200,
contents: html,
};
} catch (err) {
switch (err.code) {
case 'parse-error': {
return {
statusCode: 500,
type: 'parse-error',
error: err,
};
}
default: {
return {
statusCode: 500,
type: 'unknown',
error: err,
};
}
}
}
}
export async function createRuntime(astroConfig: AstroConfig, logging: LogOptions) {
const { projectRoot, hmxRoot } = astroConfig;
const internalPath = new URL('./frontend/', import.meta.url);
// Workaround for SKY-251
const hmxPlugOptions: { resolve?: (s: string) => string } = {};
if (existsSync(new URL('./package-lock.json', projectRoot))) {
const pkgLockStr = await readFile(new URL('./package-lock.json', projectRoot), 'utf-8');
const pkgLock = JSON.parse(pkgLockStr);
hmxPlugOptions.resolve = (pkgName: string) => {
const ver = pkgLock.dependencies[pkgName].version;
return `/_snowpack/pkg/${pkgName}.v${ver}.js`;
};
}
const snowpackConfig = await loadConfiguration({
root: projectRoot.pathname,
mount: {
[hmxRoot.pathname]: '/_hmx',
[internalPath.pathname]: '/__hmx_internal__',
},
plugins: [[new URL('../snowpack-plugin.cjs', import.meta.url).pathname, hmxPlugOptions]],
devOptions: {
open: 'none',
output: 'stream',
port: 0,
},
packageOptions: {
knownEntrypoints: ['preact-render-to-string'],
external: ['@vue/server-renderer'],
},
});
const snowpack = await startSnowpackServer({
config: snowpackConfig,
lockfile: null,
});
const snowpackRuntime = snowpack.getServerRuntime();
const runtimeConfig: RuntimeConfig = {
astroConfig,
logging,
snowpack,
snowpackRuntime,
};
return {
load: load.bind(null, runtimeConfig),
shutdown: () => snowpack.shutdown(),
};
}

0
src/style-stuff.ts Normal file
View file

View file

@ -0,0 +1,6 @@
export default {
projectRoot: '.',
hmxRoot: './astro',
dist: './_site'
}

View file

@ -0,0 +1,15 @@
<script hmx="setup">
export function setup() {
return {
props: {}
}
}
</script>
<head>
<!-- Head Stuff -->
</head>
<body>
<h1>Hello world!</h1>
</body>

View file

@ -0,0 +1,5 @@
export default {
mount: {
}
};

38
test/hmx-basic.test.js Normal file
View file

@ -0,0 +1,38 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { createRuntime } from '../lib/runtime.js';
import { doc } from './test-utils.js';
const Basics = suite('HMX Basics');
let runtime;
Basics.before(async () => {
const astroConfig = {
projectRoot: new URL('./fixtures/hmx-basic/', import.meta.url),
hmxRoot: new URL('./fixtures/hmx-basic/astro/', import.meta.url),
dist: './_site'
};
const logging = {
level: 'error',
dest: process.stderr
};
runtime = await createRuntime(astroConfig, logging);
});
Basics.after(async () => {
await runtime.shutdown();
});
Basics('Can load hmx page', async () => {
const result = await runtime.load('/');
assert.equal(result.statusCode, 200);
const $ = doc(result.contents);
assert.equal($('h1').text(), 'Hello world!');
});
Basics.run();

5
test/test-utils.js Normal file
View file

@ -0,0 +1,5 @@
import cheerio from 'cheerio';
export function doc(html) {
return cheerio.load(html);
}