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

289
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"
}
},
"htmlparser2": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
"integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
"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"
}
}
"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"
}
},
"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": {
"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,75 +1,82 @@
// @ts-nocheck
const now = (typeof process !== 'undefined' && process.hrtime)
? () => {
const t = process.hrtime();
return t[0] * 1e3 + t[1] / 1e6;
}
: () => self.performance.now();
const now =
typeof process !== 'undefined' && process.hrtime
? () => {
const t = process.hrtime();
return t[0] * 1e3 + t[1] / 1e6;
}
: () => self.performance.now();
interface Timing {
label: string;
start: number;
end: number;
children: Timing[];
label: string;
start: number;
end: number;
children: 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));
});
return result;
const result = {};
timings.forEach((timing) => {
result[timing.label] = Object.assign(
{
total: timing.end - timing.start,
},
timing.children && collapse_timings(timing.children)
);
});
return result;
}
export default class Stats {
start_time: number;
current_timing: Timing;
current_children: Timing[];
timings: Timing[];
stack: Timing[];
start_time: number;
current_timing: Timing;
current_children: Timing[];
timings: Timing[];
stack: Timing[];
constructor() {
this.start_time = now();
this.stack = [];
this.current_children = this.timings = [];
}
constructor() {
this.start_time = now();
this.stack = [];
this.current_children = this.timings = [];
}
start(label) {
const timing = {
label,
start: now(),
end: null,
children: []
};
start(label) {
const timing = {
label,
start: now(),
end: null,
children: [],
};
this.current_children.push(timing);
this.stack.push(timing);
this.current_children.push(timing);
this.stack.push(timing);
this.current_timing = timing;
this.current_children = timing.children;
}
this.current_timing = timing;
this.current_children = timing.children;
}
stop(label) {
if (label !== this.current_timing.label) {
throw new Error(`Mismatched timing labels (expected ${this.current_timing.label}, got ${label})`);
}
stop(label) {
if (label !== this.current_timing.label) {
throw new Error(`Mismatched timing labels (expected ${this.current_timing.label}, got ${label})`);
}
this.current_timing.end = now();
this.stack.pop();
this.current_timing = this.stack[this.stack.length - 1];
this.current_children = this.current_timing ? this.current_timing.children : this.timings;
}
this.current_timing.end = now();
this.stack.pop();
this.current_timing = this.stack[this.stack.length - 1];
this.current_children = this.current_timing ? this.current_timing.children : this.timings;
}
render() {
const timings = Object.assign({
total: now() - this.start_time
}, collapse_timings(this.timings));
render() {
const timings = Object.assign(
{
total: now() - this.start_time,
},
collapse_timings(this.timings)
);
return {
timings
};
}
return {
timings,
};
}
}

View file

@ -1 +1 @@
export { default as parse } from './parse/index.js';
export { default as parse } from './parse/index.js';

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, {
sourceType: 'module',
ecmaVersion: 2020,
locations: true
});
export const parse = (source: string): Node =>
acorn.parse(source, {
sourceType: 'module',
ecmaVersion: 2020,
locations: true,
});
export const parse_expression_at = (source: string, index: number): Node => acornJsx.parseExpressionAt(source, index, {
sourceType: 'module',
ecmaVersion: 2020,
locations: true
});
export const parse_expression_at = (source: string, index: number): Node =>
acornJsx.parseExpressionAt(source, index, {
sourceType: 'module',
ecmaVersion: 2020,
locations: true,
});

View file

@ -8,245 +8,257 @@ 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;
reason: string;
depth: number;
tag: string;
reason: string;
depth: number;
}
export class Parser {
readonly template: string;
readonly filename?: string;
readonly customElement: boolean;
readonly template: string;
readonly filename?: string;
readonly customElement: boolean;
index = 0;
stack: TemplateNode[] = [];
index = 0;
stack: TemplateNode[] = [];
html: Fragment;
css: Style[] = [];
js: Script[] = [];
meta_tags = {};
last_auto_closed_tag?: LastAutoClosedTag;
html: Fragment;
css: Style[] = [];
js: Script[] = [];
meta_tags = {};
last_auto_closed_tag?: LastAutoClosedTag;
constructor(template: string, options: ParserOptions) {
if (typeof template !== 'string') {
throw new TypeError('Template must be a string');
}
constructor(template: string, options: ParserOptions) {
if (typeof template !== 'string') {
throw new TypeError('Template must be a string');
}
this.template = template.replace(/\s+$/, '');
this.filename = options.filename;
this.customElement = options.customElement;
this.template = template.replace(/\s+$/, '');
this.filename = options.filename;
this.customElement = options.customElement;
this.html = {
start: null,
end: null,
type: 'Fragment',
children: []
};
this.html = {
start: null,
end: null,
type: 'Fragment',
children: [],
};
this.stack.push(this.html);
this.stack.push(this.html);
let state: ParserState = fragment;
let state: ParserState = fragment;
while (this.index < this.template.length) {
state = state(this) || fragment;
}
while (this.index < this.template.length) {
state = state(this) || fragment;
}
if (this.stack.length > 1) {
const current = this.current();
if (this.stack.length > 1) {
const current = this.current();
const type = current.type === 'Element' ? `<${current.name}>` : 'Block';
const slug = current.type === 'Element' ? 'element' : 'block';
const type = current.type === 'Element' ? `<${current.name}>` : 'Block';
const slug = current.type === 'Element' ? 'element' : 'block';
this.error({
code: `unclosed-${slug}`,
message: `${type} was left open`
}, current.start);
}
this.error(
{
code: `unclosed-${slug}`,
message: `${type} was left open`,
},
current.start
);
}
if (state !== fragment) {
this.error({
code: 'unexpected-eof',
message: 'Unexpected end of input'
});
}
if (state !== fragment) {
this.error({
code: 'unexpected-eof',
message: 'Unexpected end of input',
});
}
if (this.html.children.length) {
let start = this.html.children[0].start;
while (whitespace.test(template[start])) start += 1;
if (this.html.children.length) {
let start = this.html.children[0].start;
while (whitespace.test(template[start])) start += 1;
let end = this.html.children[this.html.children.length - 1].end;
while (whitespace.test(template[end - 1])) end -= 1;
let end = this.html.children[this.html.children.length - 1].end;
while (whitespace.test(template[end - 1])) end -= 1;
this.html.start = start;
this.html.end = end;
} else {
this.html.start = this.html.end = null;
}
}
this.html.start = start;
this.html.end = end;
} else {
this.html.start = this.html.end = null;
}
}
current() {
return this.stack[this.stack.length - 1];
}
current() {
return this.stack[this.stack.length - 1];
}
acorn_error(err: any) {
this.error({
code: 'parse-error',
message: err.message.replace(/ \(\d+:\d+\)$/, '')
}, err.pos);
}
acorn_error(err: any) {
this.error(
{
code: 'parse-error',
message: err.message.replace(/ \(\d+:\d+\)$/, ''),
},
err.pos
);
}
error({ code, message }: { code: string; message: string }, index = this.index) {
error(message, {
name: 'ParseError',
code,
source: this.template,
start: index,
filename: this.filename
});
}
error({ code, message }: { code: string; message: string }, index = this.index) {
error(message, {
name: 'ParseError',
code,
source: this.template,
start: index,
filename: this.filename,
});
}
eat(str: string, required?: boolean, message?: string) {
if (this.match(str)) {
this.index += str.length;
return true;
}
eat(str: string, required?: boolean, message?: string) {
if (this.match(str)) {
this.index += str.length;
return true;
}
if (required) {
this.error({
code: `unexpected-${this.index === this.template.length ? 'eof' : 'token'}`,
message: message || `Expected ${str}`
});
}
if (required) {
this.error({
code: `unexpected-${this.index === this.template.length ? 'eof' : 'token'}`,
message: message || `Expected ${str}`,
});
}
return false;
}
return false;
}
match(str: string) {
return this.template.slice(this.index, this.index + str.length) === str;
}
match(str: string) {
return this.template.slice(this.index, this.index + str.length) === str;
}
match_regex(pattern: RegExp) {
const match = pattern.exec(this.template.slice(this.index));
if (!match || match.index !== 0) return null;
match_regex(pattern: RegExp) {
const match = pattern.exec(this.template.slice(this.index));
if (!match || match.index !== 0) return null;
return match[0];
}
return match[0];
}
allow_whitespace() {
while (
this.index < this.template.length &&
whitespace.test(this.template[this.index])
) {
this.index++;
}
}
allow_whitespace() {
while (this.index < this.template.length && whitespace.test(this.template[this.index])) {
this.index++;
}
}
read(pattern: RegExp) {
const result = this.match_regex(pattern);
if (result) this.index += result.length;
return result;
}
read(pattern: RegExp) {
const result = this.match_regex(pattern);
if (result) this.index += result.length;
return result;
}
read_identifier(allow_reserved = false) {
const start = this.index;
read_identifier(allow_reserved = false) {
const start = this.index;
let i = this.index;
let i = this.index;
const code = full_char_code_at(this.template, i);
if (!isIdentifierStart(code, true)) return null;
const code = full_char_code_at(this.template, i);
if (!isIdentifierStart(code, true)) return null;
i += code <= 0xffff ? 1 : 2;
i += code <= 0xffff ? 1 : 2;
while (i < this.template.length) {
const code = full_char_code_at(this.template, i);
while (i < this.template.length) {
const code = full_char_code_at(this.template, i);
if (!isIdentifierChar(code, true)) break;
i += code <= 0xffff ? 1 : 2;
}
if (!isIdentifierChar(code, true)) break;
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({
code: 'unexpected-reserved-word',
message: `'${identifier}' is a reserved word in JavaScript and cannot be used here`
}, start);
}
if (!allow_reserved && reserved.has(identifier)) {
this.error(
{
code: 'unexpected-reserved-word',
message: `'${identifier}' is a reserved word in JavaScript and cannot be used here`,
},
start
);
}
return identifier;
}
return identifier;
}
read_until(pattern: RegExp) {
if (this.index >= this.template.length) {
this.error({
code: 'unexpected-eof',
message: 'Unexpected end of input'
});
}
read_until(pattern: RegExp) {
if (this.index >= this.template.length) {
this.error({
code: 'unexpected-eof',
message: 'Unexpected end of input',
});
}
const start = this.index;
const match = pattern.exec(this.template.slice(start));
const start = this.index;
const match = pattern.exec(this.template.slice(start));
if (match) {
this.index = start + match.index;
return this.template.slice(start, this.index);
}
if (match) {
this.index = start + match.index;
return this.template.slice(start, this.index);
}
this.index = this.template.length;
return this.template.slice(start);
}
this.index = this.template.length;
return this.template.slice(start);
}
require_whitespace() {
if (!whitespace.test(this.template[this.index])) {
this.error({
code: 'missing-whitespace',
message: 'Expected whitespace'
});
}
require_whitespace() {
if (!whitespace.test(this.template[this.index])) {
this.error({
code: 'missing-whitespace',
message: 'Expected whitespace',
});
}
this.allow_whitespace();
}
this.allow_whitespace();
}
}
export default function parse(
template: string,
options: ParserOptions = {}
): Ast {
const parser = new Parser(template, options);
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({
code: 'duplicate-style',
message: 'You can only have one top-level <style> tag per component'
}, parser.css[1].start);
}
// 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(
{
code: 'duplicate-style',
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({
code: 'invalid-script',
message: 'A component can only have one instance-level <script> element'
}, instance_scripts[1].start);
}
if (instance_scripts.length > 1) {
parser.error(
{
code: 'invalid-script',
message: 'A component can only have one instance-level <script> element',
},
instance_scripts[1].start
);
}
if (module_scripts.length > 1) {
parser.error({
code: 'invalid-script',
message: 'A component can only have one <script context="module"> element'
}, module_scripts[1].start);
}
if (module_scripts.length > 1) {
parser.error(
{
code: 'invalid-script',
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]
};
return {
html: parser.html,
css: parser.css[0],
instance: instance_scripts[0],
module: module_scripts[0],
};
}

View file

@ -3,82 +3,70 @@
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 } {
const start = parser.index;
let i = parser.index;
export default function read_context(parser: Parser): Pattern & { start: number; end: number } {
const start = parser.index;
let i = parser.index;
const code = full_char_code_at(parser.template, i);
if (isIdentifierStart(code, true)) {
return {
type: 'Identifier',
name: parser.read_identifier(),
start,
end: parser.index
};
}
const code = full_char_code_at(parser.template, i);
if (isIdentifierStart(code, true)) {
return {
type: 'Identifier',
name: parser.read_identifier(),
start,
end: parser.index,
};
}
if (!is_bracket_open(code)) {
parser.error({
code: 'unexpected-token',
message: 'Expected identifier or destructure pattern'
});
}
if (!is_bracket_open(code)) {
parser.error({
code: 'unexpected-token',
message: 'Expected identifier or destructure pattern',
});
}
const bracket_stack = [code];
i += code <= 0xffff ? 1 : 2;
const bracket_stack = [code];
i += code <= 0xffff ? 1 : 2;
while (i < parser.template.length) {
const code = full_char_code_at(parser.template, i);
if (is_bracket_open(code)) {
bracket_stack.push(code);
} else if (is_bracket_close(code)) {
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])
)}`
});
}
bracket_stack.pop();
if (bracket_stack.length === 0) {
i += code <= 0xffff ? 1 : 2;
break;
}
}
i += code <= 0xffff ? 1 : 2;
}
while (i < parser.template.length) {
const code = full_char_code_at(parser.template, i);
if (is_bracket_open(code)) {
bracket_stack.push(code);
} else if (is_bracket_close(code)) {
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]))}`,
});
}
bracket_stack.pop();
if (bracket_stack.length === 0) {
i += code <= 0xffff ? 1 : 2;
break;
}
}
i += code <= 0xffff ? 1 : 2;
}
parser.index = i;
parser.index = i;
const pattern_string = parser.template.slice(start, i);
try {
// the length of the `space_with_newline` has to be start - 1
// because we added a `(` in front of the pattern_string,
// which shifted the entire string to right by 1
// so we offset it by removing 1 character in the `space_with_newline`
// to achieve that, we remove the 1st space encountered,
// so it will not affect the `column` of the node
let space_with_newline = parser.template.slice(0, start).replace(/[^\n]/g, ' ');
const first_space = space_with_newline.indexOf(' ');
space_with_newline = space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
const pattern_string = parser.template.slice(start, i);
try {
// the length of the `space_with_newline` has to be start - 1
// because we added a `(` in front of the pattern_string,
// which shifted the entire string to right by 1
// so we offset it by removing 1 character in the `space_with_newline`
// to achieve that, we remove the 1st space encountered,
// so it will not affect the `column` of the node
let space_with_newline = parser.template.slice(0, start).replace(/[^\n]/g, ' ');
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;
} catch (error) {
parser.acorn_error(error);
}
return (parse_expression_at(`${space_with_newline}(${pattern_string} = 1)`, start - 1) as any).left;
} catch (error) {
parser.acorn_error(error);
}
}

View file

@ -6,36 +6,39 @@ import { whitespace } from '../../utils/patterns.js';
// import { Node } from 'estree';
export default function read_expression(parser: Parser): string {
try {
const node = parse_expression_at(parser.template, parser.index);
try {
const node = parse_expression_at(parser.template, parser.index);
let num_parens = 0;
let num_parens = 0;
for (let i = parser.index; i < node.start; i += 1) {
if (parser.template[i] === '(') num_parens += 1;
}
for (let i = parser.index; i < node.start; i += 1) {
if (parser.template[i] === '(') num_parens += 1;
}
let index = node.end;
while (num_parens > 0) {
const char = parser.template[index];
let index = node.end;
while (num_parens > 0) {
const char = parser.template[index];
if (char === ')') {
num_parens -= 1;
} else if (!whitespace.test(char)) {
parser.error({
code: 'unexpected-token',
message: 'Expected )'
}, index);
}
if (char === ')') {
num_parens -= 1;
} else if (!whitespace.test(char)) {
parser.error(
{
code: 'unexpected-token',
message: 'Expected )',
},
index
);
}
index += 1;
}
index += 1;
}
parser.index = index;
return parser.template.substring(node.start, node.end);
// return node as Node;
} catch (err) {
parser.acorn_error(err);
}
parser.index = index;
return parser.template.substring(node.start, node.end);
// return node as Node;
} catch (err) {
parser.acorn_error(err);
}
}

View file

@ -8,48 +8,53 @@ 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');
if (!context) return 'default';
const context = attributes.find((attribute) => attribute.name === 'context');
if (!context) return 'default';
if (context.value.length !== 1 || context.value[0].type !== 'Text') {
parser.error({
code: 'invalid-script',
message: 'context attribute must be static'
}, start);
}
if (context.value.length !== 1 || context.value[0].type !== 'Text') {
parser.error(
{
code: 'invalid-script',
message: 'context attribute must be static',
},
start
);
}
const value = context.value[0].data;
const value = context.value[0].data;
if (value !== 'module') {
parser.error({
code: 'invalid-script',
message: 'If the context attribute is supplied, its value must be "module"'
}, context.start);
}
if (value !== 'module') {
parser.error(
{
code: 'invalid-script',
message: 'If the context attribute is supplied, its value must be "module"',
},
context.start
);
}
return value;
return value;
}
export default function read_script(parser: Parser, start: number, attributes: Node[]): Script {
const script_start = parser.index;
const script_end = parser.template.indexOf(script_closing_tag, script_start);
const script_start = parser.index;
const script_end = parser.template.indexOf(script_closing_tag, script_start);
if (script_end === -1) {
parser.error({
code: 'unclosed-script',
message: '<script> must have a closing tag'
});
}
if (script_end === -1) {
parser.error({
code: 'unclosed-script',
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);
parser.index = script_end + script_closing_tag.length;
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 {
type: 'Script',
start,
end: parser.index,
context: get_context(parser, attributes, start),
content: source
};
return {
type: 'Script',
start,
end: parser.index,
context: get_context(parser, attributes, start),
content: source,
};
}

View file

@ -4,13 +4,13 @@ import text from './text.js';
import { Parser } from '../index.js';
export default function fragment(parser: Parser) {
if (parser.match('<')) {
return tag;
}
if (parser.match('<')) {
return tag;
}
if (parser.match('{')) {
return mustache;
}
if (parser.match('{')) {
return mustache;
}
return text;
return text;
}

View file

@ -10,403 +10,404 @@ import { TemplateNode } from '../../interfaces.js';
type TODO = any;
function trim_whitespace(block: TemplateNode, trim_before: boolean, trim_after: boolean) {
if (!block.children || block.children.length === 0) return; // AwaitBlock
if (!block.children || block.children.length === 0) return; // AwaitBlock
const first_child = block.children[0];
const last_child = block.children[block.children.length - 1];
const first_child = block.children[0];
const last_child = block.children[block.children.length - 1];
if (first_child.type === 'Text' && trim_before) {
first_child.data = trim_start(first_child.data);
if (!first_child.data) block.children.shift();
}
if (first_child.type === 'Text' && trim_before) {
first_child.data = trim_start(first_child.data);
if (!first_child.data) block.children.shift();
}
if (last_child.type === 'Text' && trim_after) {
last_child.data = trim_end(last_child.data);
if (!last_child.data) block.children.pop();
}
if (last_child.type === 'Text' && trim_after) {
last_child.data = trim_end(last_child.data);
if (!last_child.data) block.children.pop();
}
if (block.else) {
trim_whitespace(block.else, trim_before, trim_after);
}
if (block.else) {
trim_whitespace(block.else, trim_before, trim_after);
}
if (first_child.elseif) {
trim_whitespace(first_child, trim_before, trim_after);
}
if (first_child.elseif) {
trim_whitespace(first_child, trim_before, trim_after);
}
}
export default function mustache(parser: Parser) {
const start = parser.index;
parser.index += 1;
const start = parser.index;
parser.index += 1;
parser.allow_whitespace();
parser.allow_whitespace();
// {/if}, {/each}, {/await} or {/key}
if (parser.eat('/')) {
let block = parser.current();
let expected: TODO;
// {/if}, {/each}, {/await} or {/key}
if (parser.eat('/')) {
let block = parser.current();
let expected: TODO;
if (closing_tag_omitted(block.name)) {
block.end = start;
parser.stack.pop();
block = parser.current();
}
if (closing_tag_omitted(block.name)) {
block.end = start;
parser.stack.pop();
block = parser.current();
}
if (block.type === 'ElseBlock' || block.type === 'PendingBlock' || block.type === 'ThenBlock' || block.type === 'CatchBlock') {
block.end = start;
parser.stack.pop();
block = parser.current();
if (block.type === 'ElseBlock' || block.type === 'PendingBlock' || block.type === 'ThenBlock' || block.type === 'CatchBlock') {
block.end = start;
parser.stack.pop();
block = parser.current();
expected = 'await';
}
expected = 'await';
}
if (block.type === 'IfBlock') {
expected = 'if';
} else if (block.type === 'EachBlock') {
expected = 'each';
} else if (block.type === 'AwaitBlock') {
expected = 'await';
} else if (block.type === 'KeyBlock') {
expected = 'key';
} else {
parser.error({
code: 'unexpected-block-close',
message: 'Unexpected block closing tag'
});
}
if (block.type === 'IfBlock') {
expected = 'if';
} else if (block.type === 'EachBlock') {
expected = 'each';
} else if (block.type === 'AwaitBlock') {
expected = 'await';
} else if (block.type === 'KeyBlock') {
expected = 'key';
} else {
parser.error({
code: 'unexpected-block-close',
message: 'Unexpected block closing tag',
});
}
parser.eat(expected, true);
parser.allow_whitespace();
parser.eat('}', true);
parser.eat(expected, true);
parser.allow_whitespace();
parser.eat('}', true);
while (block.elseif) {
block.end = parser.index;
parser.stack.pop();
block = parser.current();
while (block.elseif) {
block.end = parser.index;
parser.stack.pop();
block = parser.current();
if (block.else) {
block.else.end = start;
}
}
if (block.else) {
block.else.end = start;
}
}
// strip leading/trailing whitespace as necessary
const char_before = parser.template[block.start - 1];
const char_after = parser.template[parser.index];
const trim_before = !char_before || whitespace.test(char_before);
const trim_after = !char_after || whitespace.test(char_after);
// strip leading/trailing whitespace as necessary
const char_before = parser.template[block.start - 1];
const char_after = parser.template[parser.index];
const trim_before = !char_before || whitespace.test(char_before);
const trim_after = !char_after || whitespace.test(char_after);
trim_whitespace(block, trim_before, trim_after);
trim_whitespace(block, trim_before, trim_after);
block.end = parser.index;
parser.stack.pop();
} else if (parser.eat(':else')) {
if (parser.eat('if')) {
parser.error({
code: 'invalid-elseif',
message: "'elseif' should be 'else if'"
});
}
block.end = parser.index;
parser.stack.pop();
} else if (parser.eat(':else')) {
if (parser.eat('if')) {
parser.error({
code: 'invalid-elseif',
message: "'elseif' should be 'else if'",
});
}
parser.allow_whitespace();
parser.allow_whitespace();
// :else if
if (parser.eat('if')) {
const block = parser.current();
if (block.type !== 'IfBlock') {
parser.error({
code: 'invalid-elseif-placement',
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'
});
}
// :else if
if (parser.eat('if')) {
const block = parser.current();
if (block.type !== 'IfBlock') {
parser.error({
code: 'invalid-elseif-placement',
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',
});
}
parser.require_whitespace();
parser.require_whitespace();
const expression = read_expression(parser);
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
parser.allow_whitespace();
parser.eat('}', true);
block.else = {
start: parser.index,
end: null,
type: 'ElseBlock',
children: [
{
start: parser.index,
end: null,
type: 'IfBlock',
elseif: true,
expression,
children: []
}
]
};
block.else = {
start: parser.index,
end: null,
type: 'ElseBlock',
children: [
{
start: parser.index,
end: null,
type: 'IfBlock',
elseif: true,
expression,
children: [],
},
],
};
parser.stack.push(block.else.children[0]);
} else {
// :else
const block = parser.current();
if (block.type !== 'IfBlock' && block.type !== 'EachBlock') {
parser.error({
code: 'invalid-else-placement',
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'
});
}
parser.stack.push(block.else.children[0]);
} else {
// :else
const block = parser.current();
if (block.type !== 'IfBlock' && block.type !== 'EachBlock') {
parser.error({
code: 'invalid-else-placement',
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',
});
}
parser.allow_whitespace();
parser.eat('}', true);
parser.allow_whitespace();
parser.eat('}', true);
block.else = {
start: parser.index,
end: null,
type: 'ElseBlock',
children: []
};
block.else = {
start: parser.index,
end: null,
type: 'ElseBlock',
children: [],
};
parser.stack.push(block.else);
}
} else if (parser.match(':then') || parser.match(':catch')) {
const block = parser.current();
const is_then = parser.eat(':then') || !parser.eat(':catch');
parser.stack.push(block.else);
}
} else if (parser.match(':then') || parser.match(':catch')) {
const block = parser.current();
const is_then = parser.eat(':then') || !parser.eat(':catch');
if (is_then) {
if (block.type !== 'PendingBlock') {
parser.error({
code: 'invalid-then-placement',
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'
});
}
} 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')
? `Expected to close ${to_string(block)} before seeing {:catch} block`
: 'Cannot have an {:catch} block outside an {#await ...} block'
});
}
}
if (is_then) {
if (block.type !== 'PendingBlock') {
parser.error({
code: 'invalid-then-placement',
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',
});
}
} 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')
? `Expected to close ${to_string(block)} before seeing {:catch} block`
: 'Cannot have an {:catch} block outside an {#await ...} block',
});
}
}
block.end = start;
parser.stack.pop();
const await_block = parser.current();
block.end = start;
parser.stack.pop();
const await_block = parser.current();
if (!parser.eat('}')) {
parser.require_whitespace();
await_block[is_then ? 'value' : 'error'] = read_context(parser);
parser.allow_whitespace();
parser.eat('}', true);
}
if (!parser.eat('}')) {
parser.require_whitespace();
await_block[is_then ? 'value' : 'error'] = read_context(parser);
parser.allow_whitespace();
parser.eat('}', true);
}
const new_block: TemplateNode = {
start,
// @ts-ignore
end: null,
type: is_then ? 'ThenBlock' : 'CatchBlock',
children: [],
skip: false
};
const new_block: TemplateNode = {
start,
// @ts-ignore
end: null,
type: is_then ? 'ThenBlock' : 'CatchBlock',
children: [],
skip: false,
};
await_block[is_then ? 'then' : 'catch'] = new_block;
parser.stack.push(new_block);
} else if (parser.eat('#')) {
// {#if foo}, {#each foo} or {#await foo}
let type;
await_block[is_then ? 'then' : 'catch'] = new_block;
parser.stack.push(new_block);
} else if (parser.eat('#')) {
// {#if foo}, {#each foo} or {#await foo}
let type;
if (parser.eat('if')) {
type = 'IfBlock';
} else if (parser.eat('each')) {
type = 'EachBlock';
} else if (parser.eat('await')) {
type = 'AwaitBlock';
} else if (parser.eat('key')) {
type = 'KeyBlock';
} else {
parser.error({
code: 'expected-block-type',
message: 'Expected if, each, await or key'
});
}
if (parser.eat('if')) {
type = 'IfBlock';
} else if (parser.eat('each')) {
type = 'EachBlock';
} else if (parser.eat('await')) {
type = 'AwaitBlock';
} else if (parser.eat('key')) {
type = 'KeyBlock';
} else {
parser.error({
code: 'expected-block-type',
message: 'Expected if, each, await or key',
});
}
parser.require_whitespace();
parser.require_whitespace();
const expression = read_expression(parser);
const expression = read_expression(parser);
// @ts-ignore
const block: TemplateNode = type === 'AwaitBlock' ?
{
start,
end: null,
type,
expression,
value: null,
error: null,
pending: {
start: null,
end: null,
type: 'PendingBlock',
children: [],
skip: true
},
then: {
start: null,
end: null,
type: 'ThenBlock',
children: [],
skip: true
},
catch: {
start: null,
end: null,
type: 'CatchBlock',
children: [],
skip: true
}
} :
{
start,
end: null,
type,
expression,
children: []
};
// @ts-ignore
const block: TemplateNode =
type === 'AwaitBlock'
? {
start,
end: null,
type,
expression,
value: null,
error: null,
pending: {
start: null,
end: null,
type: 'PendingBlock',
children: [],
skip: true,
},
then: {
start: null,
end: null,
type: 'ThenBlock',
children: [],
skip: true,
},
catch: {
start: null,
end: null,
type: 'CatchBlock',
children: [],
skip: true,
},
}
: {
start,
end: null,
type,
expression,
children: [],
};
parser.allow_whitespace();
parser.allow_whitespace();
// {#each} blocks must declare a context {#each list as item}
if (type === 'EachBlock') {
parser.eat('as', true);
parser.require_whitespace();
// {#each} blocks must declare a context {#each list as item}
if (type === 'EachBlock') {
parser.eat('as', true);
parser.require_whitespace();
block.context = read_context(parser);
block.context = read_context(parser);
parser.allow_whitespace();
parser.allow_whitespace();
if (parser.eat(',')) {
parser.allow_whitespace();
block.index = parser.read_identifier();
if (!block.index) {
parser.error({
code: 'expected-name',
message: 'Expected name'
});
}
if (parser.eat(',')) {
parser.allow_whitespace();
block.index = parser.read_identifier();
if (!block.index) {
parser.error({
code: 'expected-name',
message: 'Expected name',
});
}
parser.allow_whitespace();
}
parser.allow_whitespace();
}
if (parser.eat('(')) {
parser.allow_whitespace();
if (parser.eat('(')) {
parser.allow_whitespace();
block.key = read_expression(parser);
parser.allow_whitespace();
parser.eat(')', true);
parser.allow_whitespace();
}
}
block.key = read_expression(parser);
parser.allow_whitespace();
parser.eat(')', true);
parser.allow_whitespace();
}
}
const await_block_shorthand = type === 'AwaitBlock' && parser.eat('then');
if (await_block_shorthand) {
parser.require_whitespace();
block.value = read_context(parser);
parser.allow_whitespace();
}
const await_block_shorthand = type === 'AwaitBlock' && parser.eat('then');
if (await_block_shorthand) {
parser.require_whitespace();
block.value = read_context(parser);
parser.allow_whitespace();
}
const await_block_catch_shorthand = !await_block_shorthand && type === 'AwaitBlock' && parser.eat('catch');
if (await_block_catch_shorthand) {
parser.require_whitespace();
block.error = read_context(parser);
parser.allow_whitespace();
}
const await_block_catch_shorthand = !await_block_shorthand && type === 'AwaitBlock' && parser.eat('catch');
if (await_block_catch_shorthand) {
parser.require_whitespace();
block.error = read_context(parser);
parser.allow_whitespace();
}
parser.eat('}', true);
parser.eat('}', true);
// @ts-ignore
parser.current().children.push(block);
parser.stack.push(block);
// @ts-ignore
parser.current().children.push(block);
parser.stack.push(block);
if (type === 'AwaitBlock') {
let child_block;
if (await_block_shorthand) {
block.then.skip = false;
child_block = block.then;
} else if (await_block_catch_shorthand) {
block.catch.skip = false;
child_block = block.catch;
} else {
block.pending.skip = false;
child_block = block.pending;
}
if (type === 'AwaitBlock') {
let child_block;
if (await_block_shorthand) {
block.then.skip = false;
child_block = block.then;
} else if (await_block_catch_shorthand) {
block.catch.skip = false;
child_block = block.catch;
} else {
block.pending.skip = false;
child_block = block.pending;
}
child_block.start = parser.index;
parser.stack.push(child_block);
}
} else if (parser.eat('@html')) {
// {@html content} tag
parser.require_whitespace();
child_block.start = parser.index;
parser.stack.push(child_block);
}
} else if (parser.eat('@html')) {
// {@html content} tag
parser.require_whitespace();
const expression = read_expression(parser);
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
parser.allow_whitespace();
parser.eat('}', true);
// @ts-ignore
parser.current().children.push({
start,
end: parser.index,
type: 'RawMustacheTag',
expression
});
} else if (parser.eat('@debug')) {
// let identifiers;
// @ts-ignore
parser.current().children.push({
start,
end: parser.index,
type: 'RawMustacheTag',
expression,
});
} else if (parser.eat('@debug')) {
// let identifiers;
// // Implies {@debug} which indicates "debug all"
// if (parser.read(/\s*}/)) {
// identifiers = [];
// } else {
// const expression = read_expression(parser);
// // Implies {@debug} which indicates "debug all"
// if (parser.read(/\s*}/)) {
// identifiers = [];
// } else {
// const expression = read_expression(parser);
// identifiers = expression.type === 'SequenceExpression'
// ? expression.expressions
// : [expression];
// identifiers = expression.type === 'SequenceExpression'
// ? expression.expressions
// : [expression];
// identifiers.forEach(node => {
// if (node.type !== 'Identifier') {
// parser.error({
// code: 'invalid-debug-args',
// message: '{@debug ...} arguments must be identifiers, not arbitrary expressions'
// }, node.start);
// }
// });
// identifiers.forEach(node => {
// if (node.type !== 'Identifier') {
// parser.error({
// code: 'invalid-debug-args',
// message: '{@debug ...} arguments must be identifiers, not arbitrary expressions'
// }, node.start);
// }
// });
// parser.allow_whitespace();
// parser.eat('}', true);
// }
// parser.allow_whitespace();
// parser.eat('}', true);
// }
// parser.current().children.push({
// start,
// end: parser.index,
// type: 'DebugTag',
// identifiers
// });
throw new Error('@debug not yet supported');
} else {
const expression = read_expression(parser);
// parser.current().children.push({
// start,
// end: parser.index,
// type: 'DebugTag',
// identifiers
// });
throw new Error('@debug not yet supported');
} else {
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
parser.allow_whitespace();
parser.eat('}', true);
// @ts-ignore
parser.current().children.push({
start,
end: parser.index,
type: 'MustacheTag',
expression
});
}
// @ts-ignore
parser.current().children.push({
start,
end: parser.index,
type: 'MustacheTag',
expression,
});
}
}

View file

@ -14,29 +14,29 @@ import list from '../../utils/list.js';
const valid_tag_name = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
const meta_tags = new Map([
['svelte:head', 'Head'],
['svelte:options', 'Options'],
['svelte:window', 'Window'],
['svelte:body', 'Body']
['svelte:head', 'Head'],
['svelte:options', 'Options'],
['svelte:window', 'Window'],
['svelte:body', 'Body'],
]);
const valid_meta_tags = Array.from(meta_tags.keys()).concat('svelte:self', 'svelte:component', 'svelte:fragment');
const specials = new Map([
[
'script',
{
read: read_script,
property: 'js'
}
],
[
'style',
{
read: read_style,
property: 'css'
}
]
[
'script',
{
read: read_script,
property: 'js',
},
],
[
'style',
{
read: read_style,
property: 'css',
},
],
]);
const SELF = /^svelte:self(?=[\s/>])/;
@ -44,489 +44,531 @@ const COMPONENT = /^svelte:component(?=[\s/>])/;
const SLOT = /^svelte:fragment(?=[\s/>])/;
function parent_is_head(stack) {
let i = stack.length;
while (i--) {
const { type } = stack[i];
if (type === 'Head') return true;
if (type === 'Element' || type === 'InlineComponent') return false;
}
return false;
let i = stack.length;
while (i--) {
const { type } = stack[i];
if (type === 'Head') return true;
if (type === 'Element' || type === 'InlineComponent') return false;
}
return false;
}
export default function tag(parser: Parser) {
const start = parser.index++;
const start = parser.index++;
let parent = parser.current();
let parent = parser.current();
if (parser.eat('!--')) {
const data = parser.read_until(/-->/);
parser.eat('-->', true, 'comment was left open, expected -->');
if (parser.eat('!--')) {
const data = parser.read_until(/-->/);
parser.eat('-->', true, 'comment was left open, expected -->');
parser.current().children.push({
start,
end: parser.index,
type: 'Comment',
data
});
parser.current().children.push({
start,
end: parser.index,
type: 'Comment',
data,
});
return;
}
return;
}
const is_closing_tag = parser.eat('/');
const is_closing_tag = parser.eat('/');
const name = read_tag_name(parser);
const name = read_tag_name(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({
code: `invalid-${slug}-content`,
message: `<${name}> cannot have children`
}, parser.current().children[0].start);
}
} else {
if (name in parser.meta_tags) {
parser.error({
code: `duplicate-${slug}`,
message: `A component can only have one <${name}> tag`
}, start);
}
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(
{
code: `invalid-${slug}-content`,
message: `<${name}> cannot have children`,
},
parser.current().children[0].start
);
}
} else {
if (name in parser.meta_tags) {
parser.error(
{
code: `duplicate-${slug}`,
message: `A component can only have one <${name}> tag`,
},
start
);
}
if (parser.stack.length > 1) {
parser.error({
code: `invalid-${slug}-placement`,
message: `<${name}> tags cannot be inside elements or blocks`
}, start);
}
if (parser.stack.length > 1) {
parser.error(
{
code: `invalid-${slug}-placement`,
message: `<${name}> tags cannot be inside elements or blocks`,
},
start
);
}
parser.meta_tags[name] = true;
}
}
parser.meta_tags[name] = true;
}
}
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';
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';
const element: TemplateNode = {
start,
end: null, // filled in later
type,
name,
attributes: [],
children: []
};
const element: TemplateNode = {
start,
end: null, // filled in later
type,
name,
attributes: [],
children: [],
};
parser.allow_whitespace();
parser.allow_whitespace();
if (is_closing_tag) {
if (is_void(name)) {
parser.error({
code: 'invalid-void-content',
message: `<${name}> is a void element and cannot have children, or a closing tag`
}, start);
}
if (is_closing_tag) {
if (is_void(name)) {
parser.error(
{
code: 'invalid-void-content',
message: `<${name}> is a void element and cannot have children, or a closing tag`,
},
start
);
}
parser.eat('>', true);
parser.eat('>', true);
// 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
? `</${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({
code: 'invalid-closing-tag',
message
}, start);
}
// 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
? `</${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(
{
code: 'invalid-closing-tag',
message,
},
start
);
}
parent.end = start;
parser.stack.pop();
parent.end = start;
parser.stack.pop();
parent = parser.current();
}
parent = parser.current();
}
parent.end = parser.index;
parser.stack.pop();
parent.end = parser.index;
parser.stack.pop();
if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) {
parser.last_auto_closed_tag = null;
}
if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) {
parser.last_auto_closed_tag = null;
}
return;
} else if (closing_tag_omitted(parent.name, name)) {
parent.end = start;
parser.stack.pop();
parser.last_auto_closed_tag = {
tag: parent.name,
reason: name,
depth: parser.stack.length
};
}
return;
} else if (closing_tag_omitted(parent.name, name)) {
parent.end = start;
parser.stack.pop();
parser.last_auto_closed_tag = {
tag: parent.name,
reason: name,
depth: parser.stack.length,
};
}
const unique_names: Set<string> = new Set();
const unique_names: Set<string> = new Set();
let attribute;
while ((attribute = read_attribute(parser, unique_names))) {
element.attributes.push(attribute);
parser.allow_whitespace();
}
let attribute;
while ((attribute = read_attribute(parser, unique_names))) {
element.attributes.push(attribute);
parser.allow_whitespace();
}
if (name === 'svelte:component') {
const index = element.attributes.findIndex(attr => attr.type === 'Attribute' && attr.name === 'this');
if (!~index) {
parser.error({
code: 'missing-component-definition',
message: "<svelte:component> must have a 'this' attribute"
}, start);
}
if (name === 'svelte:component') {
const index = element.attributes.findIndex((attr) => attr.type === 'Attribute' && attr.name === 'this');
if (!~index) {
parser.error(
{
code: 'missing-component-definition',
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({
code: 'invalid-component-definition',
message: 'invalid component definition'
}, definition.start);
}
const definition = element.attributes.splice(index, 1)[0];
if (definition.value === true || definition.value.length !== 1 || definition.value[0].type === 'Text') {
parser.error(
{
code: 'invalid-component-definition',
message: 'invalid component definition',
},
definition.start
);
}
element.expression = definition.value[0].expression;
}
element.expression = definition.value[0].expression;
}
// special cases top-level <script> and <style>
if (specials.has(name) && parser.stack.length === 1) {
const special = specials.get(name);
// special cases top-level <script> and <style>
if (specials.has(name) && parser.stack.length === 1) {
const special = specials.get(name);
parser.eat('>', true);
const content = special.read(parser, start, element.attributes);
if (content) parser[special.property].push(content);
return;
}
parser.eat('>', true);
const content = special.read(parser, start, element.attributes);
if (content) parser[special.property].push(content);
return;
}
parser.current().children.push(element);
parser.current().children.push(element);
const self_closing = parser.eat('/') || is_void(name);
const self_closing = parser.eat('/') || is_void(name);
parser.eat('>', true);
parser.eat('>', true);
if (self_closing) {
// don't push self-closing elements onto the stack
element.end = parser.index;
} else if (name === 'textarea') {
// special case
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') {
// special case
const start = parser.index;
const data = parser.read_until(new RegExp(`</${name}>`));
const end = parser.index;
element.children.push({ start, end, type: 'Text', data });
parser.eat(`</${name}>`, true);
element.end = parser.index;
} else {
parser.stack.push(element);
}
if (self_closing) {
// don't push self-closing elements onto the stack
element.end = parser.index;
} else if (name === 'textarea') {
// special case
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') {
// special case
const start = parser.index;
const data = parser.read_until(new RegExp(`</${name}>`));
const end = parser.index;
element.children.push({ start, end, type: 'Text', data });
parser.eat(`</${name}>`, true);
element.end = parser.index;
} else {
parser.stack.push(element);
}
}
function read_tag_name(parser: Parser) {
const start = parser.index;
const start = parser.index;
if (parser.read(SELF)) {
// check we're inside a block, otherwise this
// will cause infinite recursion
let i = parser.stack.length;
let legal = false;
if (parser.read(SELF)) {
// check we're inside a block, otherwise this
// will cause infinite recursion
let i = parser.stack.length;
let legal = false;
while (i--) {
const fragment = parser.stack[i];
if (fragment.type === 'IfBlock' || fragment.type === 'EachBlock' || fragment.type === 'InlineComponent') {
legal = true;
break;
}
}
while (i--) {
const fragment = parser.stack[i];
if (fragment.type === 'IfBlock' || fragment.type === 'EachBlock' || fragment.type === 'InlineComponent') {
legal = true;
break;
}
}
if (!legal) {
parser.error({
code: 'invalid-self-placement',
message: '<svelte:self> components can only exist inside {#if} blocks, {#each} blocks, or slots passed to components'
}, start);
}
if (!legal) {
parser.error(
{
code: 'invalid-self-placement',
message: '<svelte:self> components can only exist inside {#if} blocks, {#each} blocks, or slots passed to components',
},
start
);
}
return 'svelte:self';
}
return 'svelte:self';
}
if (parser.read(COMPONENT)) return 'svelte:component';
if (parser.read(COMPONENT)) return 'svelte:component';
if (parser.read(SLOT)) return 'svelte:fragment';
if (parser.read(SLOT)) return 'svelte:fragment';
const name = parser.read_until(/(\s|\/|>)/);
const name = parser.read_until(/(\s|\/|>)/);
if (meta_tags.has(name)) return name;
if (meta_tags.has(name)) return name;
if (name.startsWith('svelte:')) {
const match = fuzzymatch(name.slice(7), valid_meta_tags);
if (name.startsWith('svelte:')) {
const match = fuzzymatch(name.slice(7), valid_meta_tags);
let message = `Valid <svelte:...> tag names are ${list(valid_meta_tags)}`;
if (match) message += ` (did you mean '${match}'?)`;
let message = `Valid <svelte:...> tag names are ${list(valid_meta_tags)}`;
if (match) message += ` (did you mean '${match}'?)`;
parser.error({
code: 'invalid-tag-name',
message
}, start);
}
parser.error(
{
code: 'invalid-tag-name',
message,
},
start
);
}
if (!valid_tag_name.test(name)) {
parser.error({
code: 'invalid-tag-name',
message: 'Expected valid tag name'
}, start);
}
if (!valid_tag_name.test(name)) {
parser.error(
{
code: 'invalid-tag-name',
message: 'Expected valid tag name',
},
start
);
}
return name;
return name;
}
function read_attribute(parser: Parser, unique_names: Set<string>) {
const start = parser.index;
const start = parser.index;
function check_unique(name: string) {
if (unique_names.has(name)) {
parser.error({
code: 'duplicate-attribute',
message: 'Attributes need to be unique'
}, start);
}
unique_names.add(name);
}
function check_unique(name: string) {
if (unique_names.has(name)) {
parser.error(
{
code: 'duplicate-attribute',
message: 'Attributes need to be unique',
},
start
);
}
unique_names.add(name);
}
if (parser.eat('{')) {
parser.allow_whitespace();
if (parser.eat('{')) {
parser.allow_whitespace();
if (parser.eat('...')) {
const expression = read_expression(parser);
if (parser.eat('...')) {
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
parser.allow_whitespace();
parser.eat('}', true);
return {
start,
end: parser.index,
type: 'Spread',
expression
};
} else {
const value_start = parser.index;
return {
start,
end: parser.index,
type: 'Spread',
expression,
};
} else {
const value_start = parser.index;
const name = parser.read_identifier();
parser.allow_whitespace();
parser.eat('}', true);
const name = parser.read_identifier();
parser.allow_whitespace();
parser.eat('}', true);
check_unique(name);
check_unique(name);
return {
start,
end: parser.index,
type: 'Attribute',
name,
value: [{
start: value_start,
end: value_start + name.length,
type: 'AttributeShorthand',
expression: {
start: value_start,
end: value_start + name.length,
type: 'Identifier',
name
}
}]
};
}
}
return {
start,
end: parser.index,
type: 'Attribute',
name,
value: [
{
start: value_start,
end: value_start + name.length,
type: 'AttributeShorthand',
expression: {
start: value_start,
end: value_start + name.length,
type: 'Identifier',
name,
},
},
],
};
}
}
// eslint-disable-next-line no-useless-escape
const name = parser.read_until(/[\s=\/>"']/);
if (!name) return null;
// eslint-disable-next-line no-useless-escape
const name = parser.read_until(/[\s=\/>"']/);
if (!name) return null;
let end = parser.index;
let end = parser.index;
parser.allow_whitespace();
parser.allow_whitespace();
const colon_index = name.indexOf(':');
const type = colon_index !== -1 && get_directive_type(name.slice(0, colon_index));
const colon_index = name.indexOf(':');
const type = colon_index !== -1 && get_directive_type(name.slice(0, colon_index));
let value: any[] | true = true;
if (parser.eat('=')) {
parser.allow_whitespace();
value = read_attribute_value(parser);
end = parser.index;
} else if (parser.match_regex(/["']/)) {
parser.error({
code: 'unexpected-token',
message: 'Expected ='
}, parser.index);
}
let value: any[] | true = true;
if (parser.eat('=')) {
parser.allow_whitespace();
value = read_attribute_value(parser);
end = parser.index;
} else if (parser.match_regex(/["']/)) {
parser.error(
{
code: 'unexpected-token',
message: 'Expected =',
},
parser.index
);
}
if (type) {
const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|');
if (type) {
const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|');
if (type === 'Binding' && directive_name !== 'this') {
check_unique(directive_name);
} else if (type !== 'EventHandler' && type !== 'Action') {
check_unique(name);
}
if (type === 'Binding' && directive_name !== 'this') {
check_unique(directive_name);
} else if (type !== 'EventHandler' && type !== 'Action') {
check_unique(name);
}
if (type === 'Ref') {
parser.error({
code: 'invalid-ref-directive',
message: `The ref directive is no longer supported — use \`bind:this={${directive_name}}\` instead`
}, start);
}
if (type === 'Ref') {
parser.error(
{
code: 'invalid-ref-directive',
message: `The ref directive is no longer supported — use \`bind:this={${directive_name}}\` instead`,
},
start
);
}
if (type === 'Class' && directive_name === '') {
parser.error({
code: 'invalid-class-directive',
message: 'Class binding name cannot be empty'
}, start + colon_index + 1);
}
if (type === 'Class' && directive_name === '') {
parser.error(
{
code: 'invalid-class-directive',
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({
code: 'invalid-directive-value',
message: 'Directive value must be a JavaScript expression enclosed in curly braces'
}, value[0].start);
}
}
if (value[0]) {
if ((value as any[]).length > 1 || value[0].type === 'Text') {
parser.error(
{
code: 'invalid-directive-value',
message: 'Directive value must be a JavaScript expression enclosed in curly braces',
},
value[0].start
);
}
}
const directive: Directive = {
start,
end,
type,
name: directive_name,
modifiers,
expression: (value[0] && value[0].expression) || null
};
const directive: Directive = {
start,
end,
type,
name: directive_name,
modifiers,
expression: (value[0] && value[0].expression) || null,
};
if (type === 'Transition') {
const direction = name.slice(0, colon_index);
directive.intro = direction === 'in' || direction === 'transition';
directive.outro = direction === 'out' || direction === 'transition';
}
if (type === 'Transition') {
const direction = name.slice(0, colon_index);
directive.intro = direction === 'in' || direction === 'transition';
directive.outro = direction === 'out' || direction === 'transition';
}
if (!directive.expression && (type === 'Binding' || type === 'Class')) {
directive.expression = {
start: directive.start + colon_index + 1,
end: directive.end,
type: 'Identifier',
name: directive.name
} as any;
}
if (!directive.expression && (type === 'Binding' || type === 'Class')) {
directive.expression = {
start: directive.start + colon_index + 1,
end: directive.end,
type: 'Identifier',
name: directive.name,
} as any;
}
return directive;
}
return directive;
}
check_unique(name);
check_unique(name);
return {
start,
end,
type: 'Attribute',
name,
value
};
return {
start,
end,
type: 'Attribute',
name,
value,
};
}
function get_directive_type(name: string): DirectiveType {
if (name === 'use') return 'Action';
if (name === 'animate') return 'Animation';
if (name === 'bind') return 'Binding';
if (name === 'class') return 'Class';
if (name === 'on') return 'EventHandler';
if (name === 'let') return 'Let';
if (name === 'ref') return 'Ref';
if (name === 'in' || name === 'out' || name === 'transition') return 'Transition';
if (name === 'use') return 'Action';
if (name === 'animate') return 'Animation';
if (name === 'bind') return 'Binding';
if (name === 'class') return 'Class';
if (name === 'on') return 'EventHandler';
if (name === 'let') return 'Let';
if (name === 'ref') return 'Ref';
if (name === 'in' || name === 'out' || name === 'transition') return 'Transition';
}
function read_attribute_value(parser: Parser) {
const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null;
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));
const value = read_sequence(parser, () => !!parser.match_regex(regex));
if (quote_mark) parser.index += 1;
return value;
if (quote_mark) parser.index += 1;
return value;
}
function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] {
let current_chunk: Text = {
start: parser.index,
end: null,
type: 'Text',
raw: '',
data: null
};
let current_chunk: Text = {
start: parser.index,
end: null,
type: 'Text',
raw: '',
data: null,
};
function flush() {
if (current_chunk.raw) {
current_chunk.data = decode_character_references(current_chunk.raw);
current_chunk.end = parser.index;
chunks.push(current_chunk);
}
}
function flush() {
if (current_chunk.raw) {
current_chunk.data = decode_character_references(current_chunk.raw);
current_chunk.end = parser.index;
chunks.push(current_chunk);
}
}
const chunks: TemplateNode[] = [];
const chunks: TemplateNode[] = [];
while (parser.index < parser.template.length) {
const index = parser.index;
while (parser.index < parser.template.length) {
const index = parser.index;
if (done()) {
flush();
return chunks;
} else if (parser.eat('{')) {
flush();
if (done()) {
flush();
return chunks;
} else if (parser.eat('{')) {
flush();
parser.allow_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
parser.allow_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
chunks.push({
start: index,
end: parser.index,
type: 'MustacheTag',
expression
});
chunks.push({
start: index,
end: parser.index,
type: 'MustacheTag',
expression,
});
current_chunk = {
start: parser.index,
end: null,
type: 'Text',
raw: '',
data: null
};
} else {
current_chunk.raw += parser.template[parser.index++];
}
}
current_chunk = {
start: parser.index,
end: null,
type: 'Text',
raw: '',
data: null,
};
} else {
current_chunk.raw += parser.template[parser.index++];
}
}
parser.error({
code: 'unexpected-eof',
message: 'Unexpected end of input'
});
parser.error({
code: 'unexpected-eof',
message: 'Unexpected end of input',
});
}

View file

@ -4,25 +4,21 @@ import { decode_character_references } from '../utils/html.js';
import { Parser } from '../index.js';
export default function text(parser: Parser) {
const start = parser.index;
const start = parser.index;
let data = '';
let data = '';
while (
parser.index < parser.template.length &&
!parser.match('<') &&
!parser.match('{')
) {
data += parser.template[parser.index++];
}
while (parser.index < parser.template.length && !parser.match('<') && !parser.match('{')) {
data += parser.template[parser.index++];
}
const node = {
start,
end: parser.index,
type: 'Text',
raw: data,
data: decode_character_references(data)
};
const node = {
start,
end: parser.index,
type: 'Text',
raw: data,
data: decode_character_references(data),
};
parser.current().children.push(node);
parser.current().children.push(node);
}

View file

@ -6,25 +6,22 @@ const CURLY_BRACKET_OPEN = '{'.charCodeAt(0);
const CURLY_BRACKET_CLOSE = '}'.charCodeAt(0);
export function is_bracket_open(code) {
return code === SQUARE_BRACKET_OPEN || code === CURLY_BRACKET_OPEN;
return code === SQUARE_BRACKET_OPEN || code === CURLY_BRACKET_OPEN;
}
export function is_bracket_close(code) {
return code === SQUARE_BRACKET_CLOSE || code === CURLY_BRACKET_CLOSE;
return code === SQUARE_BRACKET_CLOSE || code === CURLY_BRACKET_CLOSE;
}
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) {
if (open === SQUARE_BRACKET_OPEN) {
return SQUARE_BRACKET_CLOSE;
}
if (open === CURLY_BRACKET_OPEN) {
return CURLY_BRACKET_CLOSE;
}
if (open === SQUARE_BRACKET_OPEN) {
return SQUARE_BRACKET_CLOSE;
}
if (open === CURLY_BRACKET_OPEN) {
return CURLY_BRACKET_CLOSE;
}
}

File diff suppressed because it is too large Load diff

View file

@ -3,64 +3,61 @@
import entities from './entities.js';
const windows_1252 = [
8364,
129,
8218,
402,
8222,
8230,
8224,
8225,
710,
8240,
352,
8249,
338,
141,
381,
143,
144,
8216,
8217,
8220,
8221,
8226,
8211,
8212,
732,
8482,
353,
8250,
339,
157,
382,
376
8364,
129,
8218,
402,
8222,
8230,
8224,
8225,
710,
8240,
352,
8249,
338,
141,
381,
143,
144,
8216,
8217,
8220,
8221,
8226,
8211,
8212,
732,
8482,
353,
8250,
339,
157,
382,
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) => {
let code;
return html.replace(entity_pattern, (match, entity) => {
let code;
// Handle named entities
if (entity[0] !== '#') {
code = entities[entity];
} else if (entity[1] === 'x') {
code = parseInt(entity.substring(2), 16);
} else {
code = parseInt(entity.substring(1), 10);
}
// Handle named entities
if (entity[0] !== '#') {
code = entities[entity];
} else if (entity[1] === 'x') {
code = parseInt(entity.substring(2), 16);
} else {
code = parseInt(entity.substring(1), 10);
}
if (!code) {
return match;
}
if (!code) {
return match;
}
return String.fromCodePoint(validate_code(code));
});
return String.fromCodePoint(validate_code(code));
});
}
const NUL = 0;
@ -71,83 +68,76 @@ const NUL = 0;
//
// Source: http://en.wikipedia.org/wiki/Character_encodings_in_HTML#Illegal_characters
function validate_code(code: number) {
// line feed becomes generic whitespace
if (code === 10) {
return 32;
}
// line feed becomes generic whitespace
if (code === 10) {
return 32;
}
// ASCII range. (Why someone would use HTML entities for ASCII characters I don't know, but...)
if (code < 128) {
return code;
}
// ASCII range. (Why someone would use HTML entities for ASCII characters I don't know, but...)
if (code < 128) {
return code;
}
// code points 128-159 are dealt with leniently by browsers, but they're incorrect. We need
// to correct the mistake or we'll end up with missing € signs and so on
if (code <= 159) {
return windows_1252[code - 128];
}
// code points 128-159 are dealt with leniently by browsers, but they're incorrect. We need
// to correct the mistake or we'll end up with missing € signs and so on
if (code <= 159) {
return windows_1252[code - 128];
}
// basic multilingual plane
if (code < 55296) {
return code;
}
// basic multilingual plane
if (code < 55296) {
return code;
}
// UTF-16 surrogate halves
if (code <= 57343) {
return NUL;
}
// UTF-16 surrogate halves
if (code <= 57343) {
return NUL;
}
// rest of the basic multilingual plane
if (code <= 65535) {
return code;
}
// rest of the basic multilingual plane
if (code <= 65535) {
return code;
}
// supplementary multilingual plane 0x10000 - 0x1ffff
if (code >= 65536 && code <= 131071) {
return code;
}
// supplementary multilingual plane 0x10000 - 0x1ffff
if (code >= 65536 && code <= 131071) {
return code;
}
// supplementary ideographic plane 0x20000 - 0x2ffff
if (code >= 131072 && code <= 196607) {
return code;
}
// supplementary ideographic plane 0x20000 - 0x2ffff
if (code >= 131072 && code <= 196607) {
return code;
}
return NUL;
return NUL;
}
// based on http://developers.whatwg.org/syntax.html#syntax-tag-omission
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(
' '
)
)
],
['rt', new Set(['rt', 'rp'])],
['rp', new Set(['rt', 'rp'])],
['optgroup', new Set(['optgroup'])],
['option', new Set(['option', 'optgroup'])],
['thead', new Set(['tbody', 'tfoot'])],
['tbody', new Set(['tbody', 'tfoot'])],
['tfoot', new Set(['tbody'])],
['tr', new Set(['tr', 'tbody'])],
['td', new Set(['td', 'th', 'tr'])],
['th', new Set(['td', 'th', 'tr'])]
['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(' '))],
['rt', new Set(['rt', 'rp'])],
['rp', new Set(['rt', 'rp'])],
['optgroup', new Set(['optgroup'])],
['option', new Set(['option', 'optgroup'])],
['thead', new Set(['tbody', 'tfoot'])],
['tbody', new Set(['tbody', 'tfoot'])],
['tfoot', new Set(['tbody'])],
['tr', new Set(['tr', 'tbody'])],
['td', new Set(['td', 'th', 'tr'])],
['th', new Set(['td', 'th', 'tr'])],
]);
// can this be a child of the parent element, or does it implicitly
// close it, like `<li>one<li>two`?
export function closing_tag_omitted(current: string, next?: string) {
if (disallowed_contents.has(current)) {
if (!next || disallowed_contents.get(current).has(next)) {
return true;
}
}
if (disallowed_contents.has(current)) {
if (!next || disallowed_contents.get(current).has(next)) {
return true;
}
}
return false;
return false;
}

View file

@ -1,30 +1,30 @@
import { TemplateNode } from '../../interfaces.js';
export function to_string(node: TemplateNode) {
switch (node.type) {
case 'IfBlock':
return '{#if} block';
case 'ThenBlock':
return '{:then} block';
case 'ElseBlock':
return '{:else} block';
case 'PendingBlock':
case 'AwaitBlock':
return '{#await} block';
case 'CatchBlock':
return '{:catch} block';
case 'EachBlock':
return '{#each} block';
case 'RawMustacheTag':
return '{@html} block';
case 'DebugTag':
return '{@debug} block';
case 'Element':
case 'InlineComponent':
case 'Slot':
case 'Title':
return `<${node.name}> tag`;
default:
return node.type;
}
switch (node.type) {
case 'IfBlock':
return '{#if} block';
case 'ThenBlock':
return '{:then} block';
case 'ElseBlock':
return '{:else} block';
case 'PendingBlock':
case 'AwaitBlock':
return '{#await} block';
case 'CatchBlock':
return '{:catch} block';
case 'EachBlock':
return '{#each} block';
case 'RawMustacheTag':
return '{@html} block';
case 'DebugTag':
return '{@debug} block';
case 'Element':
case 'InlineComponent':
case 'Slot':
case 'Title':
return `<${node.name}> tag`;
default:
return node.type;
}
}

View file

@ -4,39 +4,42 @@ import { locate } from 'locate-character';
import get_code_frame from './get_code_frame.js';
export class CompileError extends Error {
code: string;
start: { line: number; column: number };
end: { line: number; column: number };
pos: number;
filename: string;
frame: string;
code: string;
start: { line: number; column: number };
end: { line: number; column: number };
pos: number;
filename: string;
frame: string;
toString() {
return `${this.message} (${this.start.line}:${this.start.column})\n${this.frame}`;
}
toString() {
return `${this.message} (${this.start.line}:${this.start.column})\n${this.frame}`;
}
}
export default function error(message: string, props: {
name: string;
code: string;
source: string;
filename: string;
start: number;
end?: number;
}): never {
const error = new CompileError(message);
error.name = props.name;
export default function error(
message: string,
props: {
name: string;
code: string;
source: string;
filename: string;
start: number;
end?: number;
}
): never {
const error = new CompileError(message);
error.name = props.name;
const start = locate(props.source, props.start, { offsetLine: 1 });
const end = locate(props.source, props.end || props.start, { offsetLine: 1 });
const start = locate(props.source, props.start, { offsetLine: 1 });
const end = locate(props.source, props.end || props.start, { offsetLine: 1 });
error.code = props.code;
error.start = start;
error.end = end;
error.pos = props.start;
error.filename = props.filename;
error.code = props.code;
error.start = start;
error.end = end;
error.pos = props.start;
error.filename = props.filename;
error.frame = get_code_frame(props.source, start.line - 1, start.column);
error.frame = get_code_frame(props.source, start.line - 1, start.column);
throw error;
throw error;
}

View file

@ -2,9 +2,9 @@
// Reproduced under MIT License https://github.com/acornjs/acorn/blob/master/LICENSE
export default function full_char_code_at(str: string, i: number): number {
const code = str.charCodeAt(i);
if (code <= 0xd7ff || code >= 0xe000) return code;
const code = str.charCodeAt(i);
if (code <= 0xd7ff || code >= 0xe000) return code;
const next = str.charCodeAt(i + 1);
return (code << 10) + next - 0x35fdc00;
const next = str.charCodeAt(i + 1);
return (code << 10) + next - 0x35fdc00;
}

View file

@ -1,10 +1,10 @@
// @ts-nocheck
export default function fuzzymatch(name: string, names: string[]) {
const set = new FuzzySet(names);
const matches = set.get(name);
const set = new FuzzySet(names);
const matches = set.get(name);
return matches && matches[0] && matches[0][0] > 0.7 ? matches[0][1] : null;
return matches && matches[0] && matches[0][0] > 0.7 ? matches[0][1] : null;
}
// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js
@ -15,225 +15,215 @@ const GRAM_SIZE_UPPER = 3;
// return an edit distance from 0 to 1
function _distance(str1: string, str2: string) {
if (str1 === null && str2 === null) {
throw 'Trying to compare two null values';
}
if (str1 === null || str2 === null) return 0;
str1 = String(str1);
str2 = String(str2);
if (str1 === null && str2 === null) {
throw 'Trying to compare two null values';
}
if (str1 === null || str2 === null) return 0;
str1 = String(str1);
str2 = String(str2);
const distance = levenshtein(str1, str2);
if (str1.length > str2.length) {
return 1 - distance / str1.length;
} else {
return 1 - distance / str2.length;
}
const distance = levenshtein(str1, str2);
if (str1.length > str2.length) {
return 1 - distance / str1.length;
} else {
return 1 - distance / str2.length;
}
}
// helper functions
function levenshtein(str1: string, str2: string) {
const current: number[] = [];
let prev;
let value;
const current: number[] = [];
let prev;
let value;
for (let i = 0; i <= str2.length; i++) {
for (let j = 0; j <= str1.length; j++) {
if (i && j) {
if (str1.charAt(j - 1) === str2.charAt(i - 1)) {
value = prev;
} else {
value = Math.min(current[j], current[j - 1], prev) + 1;
}
} else {
value = i + j;
}
for (let i = 0; i <= str2.length; i++) {
for (let j = 0; j <= str1.length; j++) {
if (i && j) {
if (str1.charAt(j - 1) === str2.charAt(i - 1)) {
value = prev;
} else {
value = Math.min(current[j], current[j - 1], prev) + 1;
}
} else {
value = i + j;
}
prev = current[j];
current[j] = value;
}
}
prev = current[j];
current[j] = value;
}
}
return current.pop();
return current.pop();
}
const non_word_regex = /[^\w, ]+/;
function iterate_grams(value: string, gram_size = 2) {
const simplified = '-' + value.toLowerCase().replace(non_word_regex, '') + '-';
const len_diff = gram_size - simplified.length;
const results = [];
const simplified = '-' + value.toLowerCase().replace(non_word_regex, '') + '-';
const len_diff = gram_size - simplified.length;
const results = [];
if (len_diff > 0) {
for (let i = 0; i < len_diff; ++i) {
value += '-';
}
}
for (let i = 0; i < simplified.length - gram_size + 1; ++i) {
results.push(simplified.slice(i, i + gram_size));
}
return results;
if (len_diff > 0) {
for (let i = 0; i < len_diff; ++i) {
value += '-';
}
}
for (let i = 0; i < simplified.length - gram_size + 1; ++i) {
results.push(simplified.slice(i, i + gram_size));
}
return results;
}
function gram_counter(value: string, gram_size = 2) {
// return an object where key=gram, value=number of occurrences
const result = {};
const grams = iterate_grams(value, gram_size);
let i = 0;
// return an object where key=gram, value=number of occurrences
const result = {};
const grams = iterate_grams(value, gram_size);
let i = 0;
for (i; i < grams.length; ++i) {
if (grams[i] in result) {
result[grams[i]] += 1;
} else {
result[grams[i]] = 1;
}
}
return result;
for (i; i < grams.length; ++i) {
if (grams[i] in result) {
result[grams[i]] += 1;
} else {
result[grams[i]] = 1;
}
}
return result;
}
function sort_descending(a, b) {
return b[0] - a[0];
return b[0] - a[0];
}
class FuzzySet {
exact_set = {};
match_dict = {};
items = {};
exact_set = {};
match_dict = {};
items = {};
constructor(arr: string[]) {
// initialization
for (let i = GRAM_SIZE_LOWER; i < GRAM_SIZE_UPPER + 1; ++i) {
this.items[i] = [];
}
constructor(arr: string[]) {
// initialization
for (let i = GRAM_SIZE_LOWER; i < GRAM_SIZE_UPPER + 1; ++i) {
this.items[i] = [];
}
// add all the items to the set
for (let i = 0; i < arr.length; ++i) {
this.add(arr[i]);
}
}
// add all the items to the set
for (let i = 0; i < arr.length; ++i) {
this.add(arr[i]);
}
}
add(value: string) {
const normalized_value = value.toLowerCase();
if (normalized_value in this.exact_set) {
return false;
}
add(value: string) {
const normalized_value = value.toLowerCase();
if (normalized_value in this.exact_set) {
return false;
}
let i = GRAM_SIZE_LOWER;
for (i; i < GRAM_SIZE_UPPER + 1; ++i) {
this._add(value, i);
}
}
let i = GRAM_SIZE_LOWER;
for (i; i < GRAM_SIZE_UPPER + 1; ++i) {
this._add(value, i);
}
}
_add(value: string, gram_size: number) {
const normalized_value = value.toLowerCase();
const items = this.items[gram_size] || [];
const index = items.length;
_add(value: string, gram_size: number) {
const normalized_value = value.toLowerCase();
const items = this.items[gram_size] || [];
const index = items.length;
items.push(0);
const gram_counts = gram_counter(normalized_value, gram_size);
let sum_of_square_gram_counts = 0;
let gram;
let gram_count;
items.push(0);
const gram_counts = gram_counter(normalized_value, gram_size);
let sum_of_square_gram_counts = 0;
let gram;
let gram_count;
for (gram in gram_counts) {
gram_count = gram_counts[gram];
sum_of_square_gram_counts += Math.pow(gram_count, 2);
if (gram in this.match_dict) {
this.match_dict[gram].push([index, gram_count]);
} else {
this.match_dict[gram] = [[index, gram_count]];
}
}
const vector_normal = Math.sqrt(sum_of_square_gram_counts);
items[index] = [vector_normal, normalized_value];
this.items[gram_size] = items;
this.exact_set[normalized_value] = value;
}
for (gram in gram_counts) {
gram_count = gram_counts[gram];
sum_of_square_gram_counts += Math.pow(gram_count, 2);
if (gram in this.match_dict) {
this.match_dict[gram].push([index, gram_count]);
} else {
this.match_dict[gram] = [[index, gram_count]];
}
}
const vector_normal = Math.sqrt(sum_of_square_gram_counts);
items[index] = [vector_normal, normalized_value];
this.items[gram_size] = items;
this.exact_set[normalized_value] = value;
}
get(value: string) {
const normalized_value = value.toLowerCase();
const result = this.exact_set[normalized_value];
get(value: string) {
const normalized_value = value.toLowerCase();
const result = this.exact_set[normalized_value];
if (result) {
return [[1, result]];
}
if (result) {
return [[1, result]];
}
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
) {
results = this.__get(value, gram_size);
if (results) {
return results;
}
}
return null;
}
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) {
results = this.__get(value, gram_size);
if (results) {
return results;
}
}
return null;
}
__get(value: string, gram_size: number) {
const normalized_value = value.toLowerCase();
const matches = {};
const gram_counts = gram_counter(normalized_value, gram_size);
const items = this.items[gram_size];
let sum_of_square_gram_counts = 0;
let gram;
let gram_count;
let i;
let index;
let other_gram_count;
__get(value: string, gram_size: number) {
const normalized_value = value.toLowerCase();
const matches = {};
const gram_counts = gram_counter(normalized_value, gram_size);
const items = this.items[gram_size];
let sum_of_square_gram_counts = 0;
let gram;
let gram_count;
let i;
let index;
let other_gram_count;
for (gram in gram_counts) {
gram_count = gram_counts[gram];
sum_of_square_gram_counts += Math.pow(gram_count, 2);
if (gram in this.match_dict) {
for (i = 0; i < this.match_dict[gram].length; ++i) {
index = this.match_dict[gram][i][0];
other_gram_count = this.match_dict[gram][i][1];
if (index in matches) {
matches[index] += gram_count * other_gram_count;
} else {
matches[index] = gram_count * other_gram_count;
}
}
}
}
for (gram in gram_counts) {
gram_count = gram_counts[gram];
sum_of_square_gram_counts += Math.pow(gram_count, 2);
if (gram in this.match_dict) {
for (i = 0; i < this.match_dict[gram].length; ++i) {
index = this.match_dict[gram][i][0];
other_gram_count = this.match_dict[gram][i][1];
if (index in matches) {
matches[index] += gram_count * other_gram_count;
} else {
matches[index] = gram_count * other_gram_count;
}
}
}
}
const vector_normal = Math.sqrt(sum_of_square_gram_counts);
let results = [];
let match_score;
const vector_normal = Math.sqrt(sum_of_square_gram_counts);
let results = [];
let match_score;
// 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]
]);
}
// 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.sort(sort_descending);
results.sort(sort_descending);
let new_results = [];
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]
]);
}
results = new_results;
results.sort(sort_descending);
let new_results = [];
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]]);
}
results = new_results;
results.sort(sort_descending);
new_results = [];
for (let i = 0; i < results.length; ++i) {
if (results[i][0] == results[0][0]) {
new_results.push([results[i][0], this.exact_set[results[i][1]]]);
}
}
new_results = [];
for (let i = 0; i < results.length; ++i) {
if (results[i][0] == results[0][0]) {
new_results.push([results[i][0], this.exact_set[results[i][1]]]);
}
}
return new_results;
}
return new_results;
}
}

View file

@ -1,31 +1,27 @@
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
) {
const lines = source.split('\n');
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);
const frame_end = Math.min(line + 3, lines.length);
const frame_start = Math.max(0, line - 2);
const frame_end = Math.min(line + 3, lines.length);
const digits = String(frame_end + 1).length;
const digits = String(frame_end + 1).length;
return lines
.slice(frame_start, frame_end)
.map((str, i) => {
const isErrorLine = frame_start + i === line;
const line_num = String(i + frame_start + 1).padStart(digits, ' ');
return lines
.slice(frame_start, frame_end)
.map((str, i) => {
const isErrorLine = frame_start + i === line;
const line_num = String(i + frame_start + 1).padStart(digits, ' ');
if (isErrorLine) {
const indicator = ' '.repeat(digits + 2 + tabs_to_spaces(str.slice(0, column)).length) + '^';
return `${line_num}: ${tabs_to_spaces(str)}\n${indicator}`;
}
if (isErrorLine) {
const indicator = ' '.repeat(digits + 2 + tabs_to_spaces(str.slice(0, column)).length) + '^';
return `${line_num}: ${tabs_to_spaces(str)}\n${indicator}`;
}
return `${line_num}: ${tabs_to_spaces(str)}`;
})
.join('\n');
return `${line_num}: ${tabs_to_spaces(str)}`;
})
.join('\n');
}

View file

@ -1,4 +1,4 @@
export function link<T extends { next?: T; prev?: T }>(next: T, prev: T) {
prev.next = next;
if (next) next.prev = prev;
prev.next = next;
if (next) next.prev = prev;
}

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
]}`;
if (items.length === 1) return items[0];
return `${items.slice(0, -1).join(', ')} ${conjunction} ${items[items.length - 1]}`;
}

View file

@ -2,138 +2,138 @@ import { isIdentifierStart, isIdentifierChar } from 'acorn';
import full_char_code_at from './full_char_code_at.js';
export const globals = new Set([
'alert',
'Array',
'Boolean',
'clearInterval',
'clearTimeout',
'confirm',
'console',
'Date',
'decodeURI',
'decodeURIComponent',
'document',
'Element',
'encodeURI',
'encodeURIComponent',
'Error',
'EvalError',
'Event',
'EventSource',
'fetch',
'global',
'globalThis',
'history',
'Infinity',
'InternalError',
'Intl',
'isFinite',
'isNaN',
'JSON',
'localStorage',
'location',
'Map',
'Math',
'NaN',
'navigator',
'Number',
'Node',
'Object',
'parseFloat',
'parseInt',
'process',
'Promise',
'prompt',
'RangeError',
'ReferenceError',
'RegExp',
'sessionStorage',
'Set',
'setInterval',
'setTimeout',
'String',
'SyntaxError',
'TypeError',
'undefined',
'URIError',
'URL',
'window'
'alert',
'Array',
'Boolean',
'clearInterval',
'clearTimeout',
'confirm',
'console',
'Date',
'decodeURI',
'decodeURIComponent',
'document',
'Element',
'encodeURI',
'encodeURIComponent',
'Error',
'EvalError',
'Event',
'EventSource',
'fetch',
'global',
'globalThis',
'history',
'Infinity',
'InternalError',
'Intl',
'isFinite',
'isNaN',
'JSON',
'localStorage',
'location',
'Map',
'Math',
'NaN',
'navigator',
'Number',
'Node',
'Object',
'parseFloat',
'parseInt',
'process',
'Promise',
'prompt',
'RangeError',
'ReferenceError',
'RegExp',
'sessionStorage',
'Set',
'setInterval',
'setTimeout',
'String',
'SyntaxError',
'TypeError',
'undefined',
'URIError',
'URL',
'window',
]);
export const reserved = new Set([
'arguments',
'await',
'break',
'case',
'catch',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'enum',
'eval',
'export',
'extends',
'false',
'finally',
'for',
'function',
'if',
'implements',
'import',
'in',
'instanceof',
'interface',
'let',
'new',
'null',
'package',
'private',
'protected',
'public',
'return',
'static',
'super',
'switch',
'this',
'throw',
'true',
'try',
'typeof',
'var',
'void',
'while',
'with',
'yield'
'arguments',
'await',
'break',
'case',
'catch',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'enum',
'eval',
'export',
'extends',
'false',
'finally',
'for',
'function',
'if',
'implements',
'import',
'in',
'instanceof',
'interface',
'let',
'new',
'null',
'package',
'private',
'protected',
'public',
'return',
'static',
'super',
'switch',
'this',
'throw',
'true',
'try',
'typeof',
'var',
'void',
'while',
'with',
'yield',
]);
const void_element_names = /^(?:area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/;
export function is_void(name: string) {
return void_element_names.test(name) || name.toLowerCase() === '!doctype';
return void_element_names.test(name) || name.toLowerCase() === '!doctype';
}
export function is_valid(str: string): boolean {
let i = 0;
let i = 0;
while (i < str.length) {
const code = full_char_code_at(str, i);
if (!(i === 0 ? isIdentifierStart : isIdentifierChar)(code, true)) return false;
while (i < str.length) {
const code = full_char_code_at(str, i);
if (!(i === 0 ? isIdentifierStart : isIdentifierChar)(code, true)) return false;
i += code <= 0xffff ? 1 : 2;
}
i += code <= 0xffff ? 1 : 2;
}
return true;
return true;
}
export function sanitize(name: string) {
return name
.replace(/[^a-zA-Z0-9_]+/g, '_')
.replace(/^_/, '')
.replace(/_$/, '')
.replace(/^[0-9]/, '_$&');
return name
.replace(/[^a-zA-Z0-9_]+/g, '_')
.replace(/^_/, '')
.replace(/_$/, '')
.replace(/^[0-9]/, '_$&');
}

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 };

View file

@ -1,34 +1,34 @@
// @ts-nocheck
export function nodes_match(a, b) {
if (!!a !== !!b) return false;
if (Array.isArray(a) !== Array.isArray(b)) return false;
if (!!a !== !!b) return false;
if (Array.isArray(a) !== Array.isArray(b)) return false;
if (a && typeof a === 'object') {
if (Array.isArray(a)) {
if (a.length !== b.length) return false;
return a.every((child, i) => nodes_match(child, b[i]));
}
if (a && typeof a === 'object') {
if (Array.isArray(a)) {
if (a.length !== b.length) return false;
return a.every((child, i) => nodes_match(child, b[i]));
}
const a_keys = Object.keys(a).sort();
const b_keys = Object.keys(b).sort();
const a_keys = Object.keys(a).sort();
const b_keys = Object.keys(b).sort();
if (a_keys.length !== b_keys.length) return false;
if (a_keys.length !== b_keys.length) return false;
let i = a_keys.length;
while (i--) {
const key = a_keys[i];
if (b_keys[i] !== key) return false;
let i = a_keys.length;
while (i--) {
const key = a_keys[i];
if (b_keys[i] !== key) return false;
if (key === 'start' || key === 'end') continue;
if (key === 'start' || key === 'end') continue;
if (!nodes_match(a[key], b[key])) {
return false;
}
}
if (!nodes_match(a[key], b[key])) {
return false;
}
}
return true;
}
return true;
}
return a === b;
return a === b;
}

View file

@ -1,15 +1,15 @@
import { whitespace } from './patterns.js';
export function trim_start(str: string) {
let i = 0;
while (whitespace.test(str[i])) i += 1;
let i = 0;
while (whitespace.test(str[i])) i += 1;
return str.slice(i);
return str.slice(i);
}
export function trim_end(str: string) {
let i = str.length;
while (whitespace.test(str[i - 1])) i -= 1;
let i = str.length;
while (whitespace.test(str[i - 1])) i -= 1;
return str.slice(0, i);
return str.slice(0, i);
}

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,95 +19,45 @@ 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 'parse-error': {
err.filename = pathRelative(projectRoot.pathname, err.filename);
debugger;
parseError(logging, err);
break;
}
default: {
console.error(err.code, err);
error(logging, 'running hmx', err);
break;
case 500: {
switch (result.type) {
case 'parse-error': {
const err = result.error;
err.filename = pathRelative(projectRoot.pathname, err.filename);
parseError(logging, err);
break;
}
default: {
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);
}