├── .arc
├── .eslintignore
├── .github
└── workflows
│ ├── CI.yml
│ └── cla.yml
├── .gitignore
├── .npmignore
├── .npmrc
├── LICENSE
├── app
├── api
│ ├── $$.mjs
│ ├── about.mjs
│ ├── contact.mjs
│ ├── cool.mjs
│ ├── index.mjs
│ ├── middleware-test.mjs
│ ├── notes
│ │ ├── $id.mjs
│ │ └── index.mjs
│ ├── one
│ │ ├── $$.mjs
│ │ └── two
│ │ │ └── three.mjs
│ ├── stuff
│ │ └── $$.mjs
│ ├── test
│ │ ├── $$.mjs
│ │ └── one.mjs
│ └── verbs.mjs
├── elements
│ ├── el-footer.mjs
│ └── el-header.mjs
└── pages
│ ├── $$.html
│ ├── about.mjs
│ ├── contact.mjs
│ ├── fun.html
│ ├── index.html
│ ├── middleware-test.mjs
│ ├── notes
│ ├── $id.mjs
│ ├── $id
│ │ └── arg.mjs
│ └── index.mjs
│ ├── stuff
│ └── $$.mjs
│ └── test
│ ├── $$.mjs
│ └── two.mjs
├── models
└── notes
│ ├── create.mjs
│ ├── destroy.mjs
│ ├── fmt.mjs
│ ├── list.mjs
│ ├── read.mjs
│ ├── schema.json
│ ├── update.mjs
│ └── validate.mjs
├── package.json
├── public
├── .gitignore
└── favicon.svg
├── readme.md
├── src
├── http
│ ├── any-catchall
│ │ ├── _backfill-params.mjs
│ │ ├── _clean.mjs
│ │ ├── _fingerprint-paths.mjs
│ │ ├── _get-elements.mjs
│ │ ├── _get-files.mjs
│ │ ├── _get-module.mjs
│ │ ├── _get-page-name.mjs
│ │ ├── _get-preflight.mjs
│ │ ├── _is-json-request.mjs
│ │ ├── _render.mjs
│ │ ├── _sort-routes.mjs
│ │ ├── index.mjs
│ │ ├── router.mjs
│ │ ├── templates
│ │ │ ├── 404.mjs
│ │ │ ├── 500.mjs
│ │ │ ├── head.mjs
│ │ │ └── html-element-wrapper.mjs
│ │ └── vendor
│ │ │ └── path-to-regexp
│ │ │ ├── LICENSE
│ │ │ └── index.mjs
│ └── get-_public-catchall
│ │ └── index.mjs
└── plugins
│ └── enhance.js
└── test
├── _fingerprint-paths.mjs
├── _get-files.mjs
├── _get-module.mjs
├── _get-preflight.mjs
├── _render.mjs
├── _sort-routes.mjs
├── backfill-params.mjs
├── dynamic-routing-pages.mjs
├── dynamic-routing.mjs
├── middleware.mjs
├── mock-apps
└── app
│ ├── api
│ ├── about.mjs
│ ├── backup-data.mjs
│ ├── docs
│ │ └── $$.mjs
│ └── one
│ │ └── two
│ │ └── three-alt.mjs
│ └── pages
│ ├── about.mjs
│ ├── docs
│ └── index.html
│ └── index.html
├── mock-async-middleware
└── app
│ ├── api
│ ├── index.mjs
│ ├── test-redirect-header.mjs
│ ├── test-redirect.mjs
│ └── test-store.mjs
│ ├── elements
│ └── debug-state.mjs
│ └── pages
│ ├── test-redirect.html
│ └── test-store.html
├── mock-dots
└── app
│ └── api
│ ├── .well-known
│ └── webfinger.mjs
│ └── index.mjs
├── mock-dynamic-routes
└── app
│ └── pages
│ ├── $level1.mjs
│ ├── $level1
│ ├── $level2.html
│ └── $level2
│ │ └── $level3.mjs
│ └── docs
│ └── $$.mjs
├── mock-errors
└── app
│ └── pages
│ └── 404.mjs
├── mock-folders
└── app
│ └── api
│ ├── $$extra.mjs
│ ├── foo
│ └── $first
│ │ └── bar
│ │ └── $second
│ │ └── baz
│ │ └── $third.mjs
│ ├── index.mjs
│ └── place
│ └── $id
│ └── $$.mjs
├── mock-headers
└── app
│ ├── api
│ └── index.mjs
│ └── pages
│ └── index.html
├── mock-html-elements
└── app
│ ├── elements
│ └── html-element.html
│ └── pages
│ └── test-element.html
├── mock-preflight
└── app
│ ├── api
│ └── index.mjs
│ ├── bad-preflight
│ └── preflight.mjs
│ ├── elements
│ ├── my-header.mjs
│ └── state-logger.mjs
│ ├── head.mjs
│ ├── pages
│ ├── about.html
│ └── index.html
│ └── preflight.mjs
├── router-alternate-path.mjs
├── router-async-middleware-state.mjs
├── router-async-middleware.mjs
├── router-dots.mjs
├── router-error-codes.mjs
├── router-html-element.mjs
├── router.mjs
└── verbs.mjs
/.arc:
--------------------------------------------------------------------------------
1 | @app
2 | enhance
3 |
4 | @plugins
5 | enhance
6 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | public/bundles/
2 |
--------------------------------------------------------------------------------
/.github/workflows/CI.yml:
--------------------------------------------------------------------------------
1 | name: Node CI
2 |
3 | # Push tests commits; pull_request tests PR merges
4 | on: [ push, pull_request ]
5 |
6 | defaults:
7 | run:
8 | shell: bash
9 |
10 | jobs:
11 |
12 | # Test the build
13 | build:
14 | # Setup
15 | runs-on: ubuntu-latest
16 | strategy:
17 | matrix:
18 | node-version: [ 16.x, 18.x, 20.x ]
19 | os: [ windows-latest, ubuntu-latest, macOS-latest ]
20 |
21 | # Go
22 | steps:
23 | - name: Check out repo
24 | uses: actions/checkout@v4
25 |
26 | - name: Set up Node.js
27 | uses: actions/setup-node@v4
28 | with:
29 | node-version: ${{ matrix.node-version }}
30 |
31 | - name: Env
32 | run: |
33 | echo "Event name: ${{ github.event_name }}"
34 | echo "Git ref: ${{ github.ref }}"
35 | echo "GH actor: ${{ github.actor }}"
36 | echo "SHA: ${{ github.sha }}"
37 | VER=`node --version`; echo "Node ver: $VER"
38 | VER=`npm --version`; echo "npm ver: $VER"
39 |
40 | - name: Install
41 | run: npm install
42 |
43 | - name: Test
44 | run: npm test
45 |
46 | - name: Integration Tests
47 | uses: enhance-dev/actions/integration@main
48 | with:
49 | use_local_package: 'true'
50 |
51 | # ----- Only git tag testing + package publishing beyond this point ----- #
52 |
53 | # Publish to package registries
54 | publish:
55 | # Setup
56 | needs: build
57 | if: startsWith(github.ref, 'refs/tags/v')
58 | runs-on: ubuntu-latest
59 |
60 | # Go
61 | steps:
62 | - name: Check out repo
63 | uses: actions/checkout@v4
64 |
65 | - name: Set up Node.js
66 | uses: actions/setup-node@v4
67 | with:
68 | node-version: 14
69 | registry-url: https://registry.npmjs.org/
70 |
71 | - name: Install
72 | run: npm install
73 |
74 | # Publish to npm
75 | - name: Publish @RC to npm
76 | if: contains(github.ref, 'RC')
77 | run: npm publish --tag RC --access public
78 | env:
79 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
80 |
81 | - name: Publish @latest to npm
82 | if: contains(github.ref, 'RC') == false #'!contains()'' doesn't work lol
83 | run: npm publish --access public
84 | env:
85 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
86 |
--------------------------------------------------------------------------------
/.github/workflows/cla.yml:
--------------------------------------------------------------------------------
1 | name: "CLA Assistant"
2 | on:
3 | issue_comment:
4 | types: [created]
5 | pull_request_target:
6 | types: [opened,closed,synchronize]
7 |
8 | permissions:
9 | actions: write
10 | contents: write
11 | pull-requests: write
12 | statuses: write
13 |
14 | jobs:
15 | CLAAssistant:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: "CLA Assistant"
19 | if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
20 | uses: contributor-assistant/github-action@v2.4.0
21 | env:
22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
23 | PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
24 | with:
25 | path-to-signatures: 'signatures/v1/cla.json'
26 | path-to-document: 'https://github.com/enhance-dev/.github/blob/main/CLA.md'
27 | branch: 'main'
28 | allowlist: brianleroux,colepeters,kristoferjoseph,macdonst,ryanbethel,ryanblock,tbeseda
29 | remote-organization-name: enhance-dev
30 | remote-repository-name: .github
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | .DS_Store
3 | node_modules
4 | .enhance
5 | public/static.json
6 | models/static.json
7 | public/bundles
8 | sam.json
9 | sam.yaml
10 | public/styles.css
11 | models/enhance-styles
12 | src/http/any-catchall/package-lock.json
13 | src/http/get-_public-catchall/package-lock.json
14 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .github/
2 | app/
3 | models/
4 | public/
5 | .arc
6 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright JS Foundation and other contributors, https://js.foundation
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/app/api/$$.mjs:
--------------------------------------------------------------------------------
1 | export async function get (){
2 | return {
3 | json: { where: '/catchall in main app' }
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/app/api/about.mjs:
--------------------------------------------------------------------------------
1 | export async function get (/* req */) {
2 | return {
3 | json: { people: [ 'fred', 'joe', 'mary' ] }
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/app/api/contact.mjs:
--------------------------------------------------------------------------------
1 | import data from '@begin/data'
2 |
3 | export async function post (req) {
4 | let contact = await data.set({
5 | table: 'contacts',
6 | message: req.body.message,
7 | ts: Date.now()
8 | })
9 | console.log('saved', contact)
10 | return {
11 | location: '/contact?success=true'
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/app/api/cool.mjs:
--------------------------------------------------------------------------------
1 | export let get = [ one, two ]
2 |
3 | async function one (req) {
4 | req.value = 'some value'
5 | console.log('hi from one')
6 | }
7 |
8 | async function two (req) {
9 | console.log(req.value)
10 | return {
11 | json: 'apples oranges'.split()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/app/api/index.mjs:
--------------------------------------------------------------------------------
1 | export async function get () {
2 | return { json: { message: 'dynamic content' } }
3 | }
4 |
--------------------------------------------------------------------------------
/app/api/middleware-test.mjs:
--------------------------------------------------------------------------------
1 | export let get = [ one, two ]
2 |
3 | async function one (req) {
4 | req.first = 'thing'
5 | }
6 |
7 | async function two (req) {
8 | const second = false
9 |
10 | return { json: { first: req.first, second } }
11 | }
12 |
--------------------------------------------------------------------------------
/app/api/notes/$id.mjs:
--------------------------------------------------------------------------------
1 | import read from '../../../models/notes/read.mjs'
2 | import update from '../../../models/notes/update.mjs'
3 | import destroy from '../../../models/notes/destroy.mjs'
4 |
5 | // post /notes/:id - update or destroy a note
6 | export async function post (req) {
7 | let method = req.body._method === 'delete' ? destroy : update
8 | let note = { ...req.body }
9 | note.key = req.params.id
10 | let json = await method(req.body)
11 | let location = '/notes'
12 | return { json, location }
13 | }
14 |
15 | // get /notes/:id - read a note
16 | export async function get (req) {
17 | let note = await read(req.params.id)
18 | return { json: { note } }
19 | }
20 |
--------------------------------------------------------------------------------
/app/api/notes/index.mjs:
--------------------------------------------------------------------------------
1 | import create from '../../../models/notes/create.mjs'
2 | import list from '../../../models/notes/list.mjs'
3 |
4 | // get /notes - list notes
5 | export async function get () {
6 | let notes = await list()
7 | return {
8 | json: { notes }
9 | }
10 | }
11 |
12 | // post /notes - create a note
13 | export async function post (req) {
14 | let json = await create(req.body)
15 | let location = '/notes'
16 | return { json, location }
17 | }
18 |
--------------------------------------------------------------------------------
/app/api/one/$$.mjs:
--------------------------------------------------------------------------------
1 | export async function get (){
2 | return {
3 | json: { where: '/one/catchall in main app' }
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/app/api/one/two/three.mjs:
--------------------------------------------------------------------------------
1 | export async function get (){
2 | return {
3 | json: { where: '/one/three.mjs in main app' }
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/app/api/stuff/$$.mjs:
--------------------------------------------------------------------------------
1 | // get /stuff/*
2 | export async function get (req) {
3 | return { json: { req } }
4 | }
5 |
--------------------------------------------------------------------------------
/app/api/test/$$.mjs:
--------------------------------------------------------------------------------
1 | export async function get (/* req */) {
2 | return { json: { data: '/test/$$.mjs' } }
3 | }
4 |
--------------------------------------------------------------------------------
/app/api/test/one.mjs:
--------------------------------------------------------------------------------
1 | export async function get () {
2 | return { json: { data: 'one' } }
3 | }
4 |
--------------------------------------------------------------------------------
/app/api/verbs.mjs:
--------------------------------------------------------------------------------
1 | export async function get () {
2 | return { json: { verb: 'get' } }
3 | }
4 |
5 | export async function head () {
6 | return { json: null }
7 | }
8 |
9 | export async function options () {
10 | return {
11 | headers: {
12 | 'allow': 'OPTIONS, GET, HEAD, POST'
13 | }
14 | }
15 | }
16 |
17 | export async function post () {
18 | return { json: { verb: 'post' } }
19 | }
20 |
21 | export async function put () {
22 | return { json: { verb: 'put' } }
23 | }
24 |
25 | export async function patch () {
26 | return {
27 | headers: {
28 | 'ETag': 'e0023aa4f'
29 | }
30 | }
31 | }
32 |
33 | export async function destroy () {
34 | return { json: { verb: 'delete' } }
35 | }
36 |
--------------------------------------------------------------------------------
/app/elements/el-footer.mjs:
--------------------------------------------------------------------------------
1 | export default function Footer ({ html, state }) {
2 | return html`
3 |
6 | `
7 | }
8 |
--------------------------------------------------------------------------------
/app/elements/el-header.mjs:
--------------------------------------------------------------------------------
1 | export default function Header ({ html }) {
2 | return html`
3 |
14 | `
15 | }
16 |
--------------------------------------------------------------------------------
/app/pages/$$.html:
--------------------------------------------------------------------------------
1 |
stuff
--------------------------------------------------------------------------------
/app/pages/about.mjs:
--------------------------------------------------------------------------------
1 | export default function About ({ html, state }) {
2 | return html`
3 |
4 |
5 | About page
6 | ${JSON.stringify(state, null, 2)}
7 |
8 |
9 | `
10 | }
11 |
--------------------------------------------------------------------------------
/app/pages/contact.mjs:
--------------------------------------------------------------------------------
1 | export default function Contact ({ html, /* state */ }) {
2 | return html`
3 |
4 |
8 |
9 | `
10 | }
11 |
--------------------------------------------------------------------------------
/app/pages/fun.html:
--------------------------------------------------------------------------------
1 |
2 | plain old regular html files are supported
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/pages/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Index page
4 | html content for home page here
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/pages/middleware-test.mjs:
--------------------------------------------------------------------------------
1 | export default function ({ html, state }) {
2 | return html`Hello World ${JSON.stringify(state.store)} `
3 | }
4 |
--------------------------------------------------------------------------------
/app/pages/notes/$id.mjs:
--------------------------------------------------------------------------------
1 | export default function Note ({ html, state }) {
2 | return html`
3 |
4 |
5 | Note page
6 | ${JSON.stringify(state, null, 2)}
7 |
8 |
9 | `
10 | }
11 |
--------------------------------------------------------------------------------
/app/pages/notes/$id/arg.mjs:
--------------------------------------------------------------------------------
1 | export default function Note ({ html, state }) {
2 | return html`
3 |
4 |
5 | Note ARGH page
6 | ${JSON.stringify(state, null, 2)}
7 |
8 |
9 | `
10 | }
11 |
--------------------------------------------------------------------------------
/app/pages/notes/index.mjs:
--------------------------------------------------------------------------------
1 | export default function Notes ({ html, state }) {
2 | let links = state.store.notes?.map(n => `${n.key} `).join('')
3 | return html`
4 |
5 |
6 | Notes page
7 |
8 |
9 | save note
10 |
11 |
12 | ${JSON.stringify(state, null, 2)}
13 |
14 |
15 | `
16 | }
17 |
--------------------------------------------------------------------------------
/app/pages/stuff/$$.mjs:
--------------------------------------------------------------------------------
1 | export default function catchall ({ html, state }) {
2 | return html`hi from catchall ${JSON.stringify(state, null, 2)} `
3 | }
4 |
--------------------------------------------------------------------------------
/app/pages/test/$$.mjs:
--------------------------------------------------------------------------------
1 | export default function ({ html, /* state */ }) {
2 | return html`stuff
`
3 | }
4 |
--------------------------------------------------------------------------------
/app/pages/test/two.mjs:
--------------------------------------------------------------------------------
1 | export default function ({ html, state }){
2 | const data = state.store.data
3 | return html`${data ? data : ''}`
4 | }
5 |
--------------------------------------------------------------------------------
/models/notes/create.mjs:
--------------------------------------------------------------------------------
1 | import data from '@begin/data'
2 | import fmt from './fmt.mjs'
3 |
4 | export default async function create (params) {
5 | let raw = await data.set({ table: 'notes', ...params })
6 | return fmt(raw)
7 | }
8 |
--------------------------------------------------------------------------------
/models/notes/destroy.mjs:
--------------------------------------------------------------------------------
1 | import data from '@begin/data'
2 |
3 | export default async function destroy (params) {
4 | return data.destroy({ table: 'notes', ...params })
5 | }
6 |
--------------------------------------------------------------------------------
/models/notes/fmt.mjs:
--------------------------------------------------------------------------------
1 | export default function fmt (params) {
2 | let { /* table, */ ...rest } = params
3 | return rest
4 | }
5 |
--------------------------------------------------------------------------------
/models/notes/list.mjs:
--------------------------------------------------------------------------------
1 | import data from '@begin/data'
2 |
3 | export default async function list () {
4 | return data.get({ table: 'notes' })
5 | }
6 |
--------------------------------------------------------------------------------
/models/notes/read.mjs:
--------------------------------------------------------------------------------
1 | import data from '@begin/data'
2 |
3 | export default async function read (key) {
4 | return data.get({ table: 'notes', key })
5 | }
6 |
--------------------------------------------------------------------------------
/models/notes/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "properties": {
4 | "table": { "type": "string" },
5 | "key": { "type": "string" },
6 | "text": { "type": "string" }
7 | },
8 | "additionalProperties": false,
9 | "required": [ "table", "key", "text" ]
10 | }
11 |
--------------------------------------------------------------------------------
/models/notes/update.mjs:
--------------------------------------------------------------------------------
1 | import data from '@begin/data'
2 |
3 | export default async function update (params) {
4 | if (!params.key) throw Error('missing_key')
5 | return data.set({ table: 'notes', ...params })
6 | }
7 |
--------------------------------------------------------------------------------
/models/notes/validate.mjs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/enhance-dev/arc-plugin-enhance/0497c4dacfc8795e949983feacbece2b8c5b7280/models/notes/validate.mjs
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@enhance/arc-plugin-enhance",
3 | "version": "11.0.4",
4 | "main": "src/plugins/enhance.js",
5 | "exports": {
6 | ".": "./src/plugins/enhance.js",
7 | "./src/http/any-catchall/index.mjs": "./src/http/any-catchall/index.mjs",
8 | "./src/http/any-catchall/router.mjs": "./src/http/any-catchall/router.mjs"
9 | },
10 | "license": "Apache-2.0",
11 | "homepage": "https://enhance.dev",
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/enhance-dev/arc-plugin-enhance.git"
15 | },
16 | "bugs": {
17 | "url": "https://github.com/enhance-dev/arc-plugin-enhance/issues"
18 | },
19 | "engines": {
20 | "node": ">=16"
21 | },
22 | "scripts": {
23 | "start": "sandbox",
24 | "lint": "eslint . --fix",
25 | "test": "npm run lint && tape test/*.mjs | tap-arc"
26 | },
27 | "files": [
28 | "app/*",
29 | "models/*",
30 | "src/*",
31 | "public/*",
32 | ".arc"
33 | ],
34 | "dependencies": {
35 | "@architect/asap": "^7.0.10",
36 | "@architect/functions": "^8.1.6",
37 | "@begin/data": "^5.0.5",
38 | "@enhance/arc-plugin-rollup": "^2.0.0",
39 | "@enhance/enhance-style-transform": "^0.1.2",
40 | "@enhance/import-transform": "^4.0.1",
41 | "@enhance/ssr": "^4.0.3",
42 | "glob": "^9.3.5",
43 | "header-timers": "^0.3.0"
44 | },
45 | "devDependencies": {
46 | "@architect/eslint-config": "^2.1.2",
47 | "@architect/sandbox": "^6.0.1",
48 | "eslint": "^8.56.0",
49 | "tap-arc": "^1.2.2",
50 | "tape": "^5.7.4",
51 | "tiny-json-http": "^7.5.1"
52 | },
53 | "eslintConfig": {
54 | "extends": "@architect/eslint-config",
55 | "overrides": [
56 | {
57 | "files": [
58 | "*"
59 | ],
60 | "rules": {
61 | "filenames/match-regex": [
62 | "error",
63 | "^[a-z0-9-_.$]+$",
64 | true
65 | ]
66 | }
67 | }
68 | ]
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/public/.gitignore:
--------------------------------------------------------------------------------
1 | bundles
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/enhance-dev/arc-plugin-enhance/0497c4dacfc8795e949983feacbece2b8c5b7280/public/favicon.svg
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # `@enhance-dev/arc-plugin-enhance`
2 | This Architect plugin customizes a default [Architect](https://arc.codes) project to add file based routing and server rendered Custom Elements.
3 |
4 | ## Quick start
5 | `npx "@enhance/create@latest" ./myproject -y`
6 |
7 | ⚠️ This repo is **not** meant to be cloned unless you are filing an issue or adding functionality. It is meant to be used by the generators linked above instead.
8 |
9 | ## Project structure
10 |
11 | ```
12 | app
13 | ├── api ............... data routes
14 | │ └── index.mjs
15 | ├── browser ........... browser JavaScript
16 | │ └── index.mjs
17 | ├── components ........ Single File Web Components
18 | │ └── my-card.mjs
19 | ├── elements .......... Custom Element pure functions
20 | │ └── my-header.mjs
21 | └── pages ............. file-based routing
22 | └── index.html
23 |
24 | ```
25 | [Read the documentation here →](https://enhance.dev)
26 |
--------------------------------------------------------------------------------
/src/http/any-catchall/_backfill-params.mjs:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 |
3 | import { pathToRegexp } from './vendor/path-to-regexp/index.mjs'
4 | import clean from './_clean.mjs'
5 |
6 | /** adds url params back in */
7 | export default function backfill (basePath, apiPath, pagePath, req) {
8 |
9 | // get a clean copy of the params
10 | let { params, ...copy } = { ...req }
11 |
12 | // get the regexp for the given path
13 | let base = apiPath ? path.join(basePath, 'api') : path.join(basePath, 'pages')
14 | let tmpl = apiPath ? apiPath : pagePath
15 |
16 | tmpl = clean({ pathTmpl: tmpl, base, fileNameRegEx: /index\.mjs|\.mjs/ })
17 | let pattern = pathToRegexp(tmpl)
18 |
19 | // resolve matches with param names in tmpl
20 | let matches = copy.rawPath.match(pattern)
21 | let parts = tmpl.split('/').filter((p) => p.startsWith(':'))
22 | parts.forEach((p, index) => {
23 | params[p.replace(':', '')] = matches[index + 1]
24 | })
25 | return params
26 | }
27 |
--------------------------------------------------------------------------------
/src/http/any-catchall/_clean.mjs:
--------------------------------------------------------------------------------
1 | export default function clean ({ pathTmpl, base, fileNameRegEx }) {
2 | if (process.platform === 'win32') {
3 | base = base.replace(/\\/g, '/')
4 | pathTmpl = pathTmpl.replace(/\\/g, '/')
5 | }
6 |
7 | return pathTmpl.replace(base, '')
8 | .replace(fileNameRegEx, '')
9 | .replace(/(\/?)\$\$\/?$/, '$1(.*)') // $$.mjs is catchall
10 | .replace(/\/\$(\w+)/g, '/:$1')
11 | .replace(/\/+$/, '')
12 | .replace(/\\/g, '/')
13 | }
14 |
--------------------------------------------------------------------------------
/src/http/any-catchall/_fingerprint-paths.mjs:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path from 'path'
3 | import url from 'url'
4 | const _local = process.env.ARC_ENV === 'testing'
5 |
6 | let manifest = {}
7 | if (!_local) {
8 | const dirPath = new URL('.', import.meta.url)
9 | const __dirname = url.fileURLToPath(dirPath)
10 | const filePath = path.join(__dirname, 'node_modules', '@architect', 'shared', 'static.json')
11 | if (fs.existsSync(filePath)) {
12 | try {
13 | const manifestFile = fs.readFileSync(filePath)
14 | manifest = JSON.parse(manifestFile)
15 | }
16 | catch (e) {
17 | console.error('Static manifest parsing error', e)
18 | }
19 | }
20 | }
21 | function escapeRegExp (string) {
22 | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
23 | }
24 | export function replaceEvery (str, mapObj) {
25 | const re = new RegExp(Object.keys(mapObj).sort().reverse().map(i => escapeRegExp(i)).join('|'), 'gi')
26 |
27 | return str.replace(re, function (matched) {
28 | return mapObj[matched]
29 | })
30 | }
31 |
32 | let manifestMap = {}
33 | const mapEntries = Object.entries(manifest)
34 | mapEntries.forEach(file => {
35 | manifestMap[`_public/${file[0]}`] = `_public/${file[1]}`
36 | })
37 | export default function (str) {
38 | if (mapEntries.length === 0) {
39 | return str
40 | }
41 | else {
42 | return replaceEvery(str, manifestMap)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/http/any-catchall/_get-elements.mjs:
--------------------------------------------------------------------------------
1 | import { join } from 'path'
2 | import { pathToFileURL } from 'url'
3 | import { existsSync as exists, readFileSync } from 'fs'
4 |
5 | import getFiles from './_get-files.mjs'
6 | import getPageName from './_get-page-name.mjs'
7 | import _404 from './templates/404.mjs'
8 | import _500 from './templates/500.mjs'
9 | import _head from './templates/head.mjs'
10 | import HTMLElementWrapper from './templates/html-element-wrapper.mjs'
11 |
12 | /**
13 | * - files in /elements must be lowcase dasherized to match tag name
14 | * - nested elements will use directory names (eg: /elements/foo/bar.mjs is foo-bar.mjs)
15 | * - TODO elements.mjs has key for each page
16 | * - TODO can run a command to generate it based on app/elements
17 | */
18 | export default async function getElements (basePath) {
19 | let pathToModule = join(basePath, 'elements.mjs')
20 | let pathToPages = join(basePath, 'pages')
21 | let pathToElements = join(basePath, 'elements')
22 | let pathToHead = join(basePath, 'head.mjs')
23 | let pathToComponents = join(basePath, 'components')
24 |
25 | // generate elements manifest
26 | let els = {}
27 |
28 | // Load head element
29 | let head
30 | if (exists(pathToHead) === false) {
31 | head = _head
32 | }
33 | else {
34 | try {
35 | head = (await import(pathToFileURL(pathToHead).href)).default
36 | }
37 | catch (error) {
38 | throw new Error('Issue when trying to import head file.', { cause: error })
39 | }
40 | }
41 |
42 | if (exists(pathToModule)) {
43 | // read explicit elements manifest
44 | let mod
45 | let href = pathToFileURL(pathToModule).href
46 | try {
47 | mod = await import(href)
48 | els = mod.default
49 | }
50 | catch (error) {
51 | throw new Error('Issue when trying to import elements manifest.', { cause: error })
52 | }
53 | }
54 |
55 | // look for pages
56 | if (exists(pathToPages)) {
57 |
58 | // read all the pages
59 | let pages = getFiles(basePath, 'pages').filter(f => f.endsWith('.mjs'))
60 | for (let p of pages) {
61 | let tag = await getPageName(basePath, p)
62 | let mod
63 | try {
64 | mod = await import(pathToFileURL(p).href)
65 | els['page-' + tag] = mod.default
66 | }
67 | catch (error) {
68 | throw new Error(`Issue when trying to import page: ${p}`, { cause: error })
69 | }
70 | }
71 | }
72 |
73 | if (exists(pathToComponents)) {
74 | let componentURL = pathToFileURL(join(basePath, 'components'))
75 | // read all the elements
76 | let files = getFiles(basePath, 'components').filter(f => f.endsWith('.mjs'))
77 | for (let e of files) {
78 | // turn foo/bar.mjs into foo-bar to make sure we have a legit tag name
79 | const fileURL = pathToFileURL(e)
80 | let tag = fileURL.pathname.replace(componentURL.pathname, '').slice(1).replace(/.mjs$/, '').replace(/\//g, '-')
81 | if (/^[a-z][a-z0-9-]*$/.test(tag) === false) {
82 | throw Error(`Illegal element name "${tag}" must be lowercase alphanumeric dash`)
83 | }
84 | // import the element and add to the map
85 | try {
86 | let { default: component } = await import(fileURL.href)
87 | let render = component.render || component.prototype.render
88 | if (render) {
89 | els[tag] = function BrowserElement ({ html, state }) {
90 | return render({ html, state })
91 | }
92 | }
93 | else {
94 | console.warn('Ignoring component files that do not include a render function')
95 | }
96 | }
97 | catch (error) {
98 | throw new Error(`Issue importing component: ${e}`, { cause: error })
99 | }
100 | }
101 |
102 | let externalElements = JSON.parse(process.env.ELEMENTS)
103 | for (let e of externalElements) {
104 | let [ elementName, tag ] = e
105 | // import the element and add to the map
106 | try {
107 | let pkgName = elementName.includes('/') ? `@${elementName}` : elementName
108 | let { default: component } = await import(pkgName)
109 | let render = component.prototype.render
110 | if (render) {
111 | els[tag] = function BrowserElement ({ html, state }) {
112 | return render({ html, state })
113 | }
114 | }
115 | }
116 | catch (error) {
117 | throw new Error(`Issue importing 3rd party element: ${e}`, { cause: error })
118 | }
119 | }
120 |
121 | }
122 |
123 | if (exists(pathToElements)) {
124 | // read all the elements
125 | let files = getFiles(basePath, 'elements').filter(f => f.endsWith('.mjs') || f.endsWith('.html'))
126 | for (let e of files) {
127 | // turn foo/bar.mjs into foo-bar to make sure we have a legit tag name
128 | const fileURL = pathToFileURL(e)
129 | let tag = createTagName(basePath, fileURL, e.endsWith('.mjs') ? /.mjs$/ : /.html$/)
130 | validateTagName(tag)
131 | // import the element and add to the map
132 | let mod
133 | if (e.endsWith('.mjs')) {
134 | try {
135 | mod = await import(fileURL.href)
136 | els[tag] = mod.default
137 | }
138 | catch (error) {
139 | throw new Error(`Issue importing element: ${e}`, { cause: error })
140 | }
141 | }
142 | else {
143 | let template = readFileSync(e)
144 | els[tag] = HTMLElementWrapper({ template: template.toString() })
145 | }
146 | }
147 | }
148 |
149 | if (!els['page-404'])
150 | els['page-404'] = _404
151 |
152 | if (!els['page-500'])
153 | els['page-500'] = _500
154 |
155 | return { head, elements: els }
156 | }
157 |
158 | function createTagName (basePath, fileURL, regex) {
159 | let elementsURL = pathToFileURL(join(basePath, 'elements'))
160 | return fileURL.pathname.replace(elementsURL.pathname, '').slice(1).replace(regex, '').replace(/\//g, '-')
161 | }
162 |
163 | function validateTagName (tag) {
164 | if (/^[a-z][a-z0-9-]*$/.test(tag) === false) {
165 | throw Error(`Illegal element name "${tag}" must be lowercase alphanumeric dash`)
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/http/any-catchall/_get-files.mjs:
--------------------------------------------------------------------------------
1 | import { globSync } from 'glob'
2 | import { join } from 'path'
3 |
4 | let cache = {}
5 |
6 | /** helper to return files from basePath */
7 | export default function getFiles (basePath, folder) {
8 | if (!cache[basePath]) cache[basePath] = {}
9 | if (!cache[basePath][folder]) {
10 | let root = join(basePath, folder)
11 | let raw = globSync('/**', { dot: true, root, nodir: true })
12 | let files = raw.filter(f => f.includes('.'))
13 | // Glob fixed path normalization, but in order to match in Windows we need to re-normalize back to backslashes (lol)
14 | let isWin = process.platform.startsWith('win')
15 | if (isWin) files = files.map(p => p.replace(/\//g, '\\'))
16 | cache[basePath][folder] = files
17 | }
18 | return cache[basePath][folder]
19 | }
20 |
--------------------------------------------------------------------------------
/src/http/any-catchall/_get-module.mjs:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import { pathToFileURL } from 'url'
3 |
4 | import { pathToRegexp } from './vendor/path-to-regexp/index.mjs'
5 |
6 | import getFiles from './_get-files.mjs'
7 | import sort from './_sort-routes.mjs'
8 | import clean from './_clean.mjs'
9 |
10 | // cheap memoize for warm lambda
11 | const cache = {}
12 |
13 | /** helper to get module for given folder/route */
14 | export default function getModule (basePath, folder, route) {
15 | if (!cache[basePath])
16 | cache[basePath] = {}
17 |
18 | if (!cache[basePath][folder])
19 | cache[basePath][folder] = {}
20 |
21 | if (!cache[basePath][folder][route]) {
22 |
23 | let raw = getFiles(basePath, folder).sort(sort)
24 | let base = path.join(basePath, folder)
25 | let basePathname = pathToFileURL(base).pathname
26 | let copy = raw
27 | .slice(0)
28 | .map(p => pathToFileURL(p).pathname)
29 | .map(p => clean({ pathTmpl: p, base: basePathname, fileNameRegEx: /index\.html|index\.mjs|\.mjs|\.html/ }))
30 | .map(p => pathToRegexp(p))
31 |
32 | let index = 0
33 | let found = false
34 |
35 | for (let r of copy) {
36 | if (r.test(route)) {
37 | found = raw[index]
38 | break
39 | }
40 | index += 1
41 | }
42 |
43 | if (found) {
44 | cache[basePath][folder][route] = found
45 | }
46 | }
47 |
48 | return cache[basePath][folder][route] || false
49 | }
50 |
--------------------------------------------------------------------------------
/src/http/any-catchall/_get-page-name.mjs:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 |
3 | /** helper to get page element name */
4 | //
5 | // ... request path .......... filesystem path ..................... element name
6 | // -----------------------------------------------------------------------------
7 | // ... /foobar ............... pages/foobar.mjs .................... page-foobar
8 | // ... /foo/bar/baz .......... pages/foo/bar/baz.mjs ............... page-foo-bar-baz
9 | // ... /foo/bar .............. pages/foo/bar/index.mjs ............. page-foo-bar
10 | // ... /people/13 ............ pages/users/$id.mjs ................. page-users--id
11 | // ... /people/13/things ..... pages/users/$id/things.mjs .......... page-users--id-things
12 | // ... /people/13/things/4 ... pages/users/$id/things/$thingID.mjs . page-users--id-things--thingid
13 | // ... /one/three/four ....... pages/$$.mjs ........................ page---
14 | // ... /one/two .............. pages/$$.mjs ........................ page---
15 | // ... /one .................. pages/$dyn.mjs ...................... page--dyn
16 | //
17 | export default function getPageName (basePath, template) {
18 | // if we have a template we can derive the expected element name
19 | if (template) return fmt(basePath, template)
20 | // otherwise we are 404
21 | return false
22 | }
23 |
24 | /** serialize template name to element name */
25 | function fmt (basePath, templatePath) {
26 | let base = path.join(basePath, 'pages')
27 | let raw = templatePath.replace(base, '')
28 | .replace(/\.mjs/g, '')
29 | .replace(path.sep, '')
30 | .replace(new RegExp('\\' + path.sep, 'g'), '-')
31 | .replace(/\$/g, '-') // replace dynamic markers with extra dashes to ensure unique element names
32 | .replace('-index', '')
33 | .toLowerCase() // custom elements can't have capital letters
34 | return raw
35 | }
36 |
--------------------------------------------------------------------------------
/src/http/any-catchall/_get-preflight.mjs:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import { join } from 'path'
3 | import { pathToFileURL } from 'url'
4 |
5 | export default async function GetPreflight ({ basePath = '' }) {
6 | const localPath = join(basePath, 'preflight.mjs')
7 | if (fs.existsSync(localPath)) {
8 | try {
9 | const { default: preflight } = await import(pathToFileURL(localPath).href)
10 | return preflight
11 | }
12 | catch (error) {
13 | throw new Error(`Issue when trying to import preflight: ${localPath}`, { cause: error })
14 | }
15 | }
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/src/http/any-catchall/_is-json-request.mjs:
--------------------------------------------------------------------------------
1 | /** helper to check if the user-agent requested json */
2 | export default function requestedJSON (headers) {
3 | let accept = headers['accept'] || headers['Accept']
4 | let ctype = headers['content-type'] || headers['Content-Type']
5 | let value = accept || ctype
6 | if (value) {
7 | return value.startsWith('application/json') || value.startsWith('text/json')
8 | }
9 | return false
10 | }
11 |
--------------------------------------------------------------------------------
/src/http/any-catchall/_render.mjs:
--------------------------------------------------------------------------------
1 | import router from './router.mjs'
2 |
3 | export default async function render (basePath, rawPath, session) {
4 | let res = await router({ basePath }, {
5 | rawPath,
6 | method: 'GET',
7 | headers: {
8 | 'accept': 'text/html',
9 | },
10 | session
11 | })
12 | let location = rawPath
13 | return {
14 | location,
15 | session,
16 | json: {
17 | location,
18 | ...res
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/http/any-catchall/_sort-routes.mjs:
--------------------------------------------------------------------------------
1 | /** helper to sort routes from least ambiguous to most */
2 | export default function sorter (a, b) {
3 | // Sorting is done by assinging letters to each part of the path
4 | // and then using alphabetical ordering to sort on.
5 | // They are sorted in reverse alphabetical order so that
6 | // extra path parts at the end will rank higher when reversed.
7 | function pathPartWeight (str) {
8 | // assign a weight to each path parameter
9 | // catchall='A' < dynamic='B' < static='C' < index='D'
10 | if (str === '$$.mjs' || str === '$$.html') return 'A'
11 | if (str.startsWith('$')) return 'B'
12 | if (!(str === 'index.mjs' || str === 'index.html')) return 'C'
13 | if (str === 'index.mjs' || str === 'index.html') return 'D'
14 | }
15 |
16 | function totalWeightByPosition (str) {
17 | // weighted by position in the path
18 | // /highest/high/low/lower/.../lowest
19 | // return result weighted by type and position
20 | // * When sorted in reverse alphabetical order the result is as expected.
21 | // i.e. /index.mjs = 'D'
22 | // i.e. /test/index.mjs = 'CD'
23 | // i.e. /test/this.mjs = 'CC'
24 | // i.e. /test/$id.mjs = 'CB'
25 | // i.e. /test/$$.mjs = 'CA'
26 | if (process.platform === 'win32'){
27 | str = str.replace(/\\/g, '/')
28 | }
29 | return str.split('/').reduce((prev, curr) => {
30 | return (prev + (pathPartWeight(curr) ))
31 | }, '')
32 | }
33 |
34 | const aWeight = totalWeightByPosition(a)
35 | const bWeight = totalWeightByPosition(b)
36 |
37 | let output
38 | if (aWeight < bWeight) output = 1
39 | if (aWeight > bWeight) output = -1
40 | if (aWeight === bWeight) output = 0
41 |
42 | return output
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/src/http/any-catchall/index.mjs:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import url from 'url'
3 | import arc from '@architect/functions'
4 | import router from './router.mjs'
5 |
6 | export function createRouter (base) {
7 | if (!base) {
8 | let here = path.dirname(url.fileURLToPath(import.meta.url))
9 | base = path.join(here, 'node_modules', '@architect', 'views')
10 | }
11 | return arc.http(router.bind({}, { basePath: base }))
12 | }
13 |
14 | export const handler = createRouter()
15 |
--------------------------------------------------------------------------------
/src/http/any-catchall/router.mjs:
--------------------------------------------------------------------------------
1 | import { readFileSync as read } from 'fs'
2 | import { pathToFileURL } from 'url'
3 |
4 | import arc from '@architect/functions'
5 | import enhance from '@enhance/ssr'
6 | import importTransform from '@enhance/import-transform'
7 | import styleTransform from '@enhance/enhance-style-transform'
8 | import headerTimers from 'header-timers'
9 |
10 | import getModule from './_get-module.mjs'
11 | import getElements from './_get-elements.mjs'
12 | import getPageName from './_get-page-name.mjs'
13 | import getPreflight from './_get-preflight.mjs'
14 | import isJSON from './_is-json-request.mjs'
15 | import backfill from './_backfill-params.mjs'
16 | import render from './_render.mjs'
17 | import fingerprintPaths from './_fingerprint-paths.mjs'
18 | import compareRoute from './_sort-routes.mjs'
19 | import path from 'path'
20 | import { brotliDecompressSync, gunzipSync } from 'zlib'
21 |
22 | export default async function api (options, req) {
23 | let timers = headerTimers({ enabled: true })
24 | let { basePath, altPath } = options
25 |
26 | let apiPath = getModule(basePath, 'api', req.rawPath)
27 | let pagePath = getModule(basePath, 'pages', req.rawPath)
28 | let apiBaseUsed = basePath
29 | let pageBaseUsed = basePath
30 | let preflight = await getPreflight({ basePath })
31 |
32 | if (altPath) {
33 | let apiPathPart = apiPath && apiPath.replace(path.join(basePath, 'api'), '')
34 | let pagePathPart = pagePath && pagePath.replace(path.join(basePath, 'pages'), '')
35 |
36 | let altApiPath = getModule(altPath, 'api', req.rawPath)
37 | let altPagePath = getModule(altPath, 'pages', req.rawPath)
38 | let altApiPathPart = altApiPath && altApiPath.replace(path.join(altPath, 'api'), '')
39 | let altPagePathPart = altPagePath && altPagePath.replace(path.join(altPath, 'pages'), '')
40 | if (!apiPath && altApiPath) {
41 | apiPath = altApiPath
42 | apiBaseUsed = altPath
43 | }
44 | else if (apiPath && altApiPath && (compareRoute(apiPathPart, altApiPathPart) === 1)) {
45 | apiPath = altApiPath
46 | apiBaseUsed = altPath
47 | }
48 | if (!pagePath && altPagePath) {
49 | pagePath = altPagePath
50 | pageBaseUsed = altPath
51 | }
52 | else if (pagePath && altPagePath && (compareRoute(pagePathPart, altPagePathPart) === 1)) {
53 | pagePath = altPagePath
54 | pageBaseUsed = altPath
55 | }
56 | }
57 |
58 | // if both are defined but match with different specificity
59 | // (i.e. one is exact and one is a catchall)
60 | // only the most specific route will match
61 | if (apiPath && pagePath) {
62 | let apiPathPart = apiPath.replace(path.join(apiBaseUsed, 'api'), '')
63 | let pagePathPart = pagePath.replace(path.join(pageBaseUsed, 'pages'), '')
64 | if (compareRoute(apiPathPart, pagePathPart) === 1) apiPath = false
65 | if (compareRoute(apiPathPart, pagePathPart) === -1) pagePath = false
66 | }
67 |
68 | let state = {}
69 | let isAsyncMiddleware = false
70 |
71 | let preflightData = {}
72 | if (preflight) {
73 | timers.start('preflight', 'enhance-preflight')
74 | preflightData = await preflight({ req })
75 | timers.stop('preflight')
76 | }
77 |
78 | // rendering a json response or passing state to an html response
79 | if (apiPath) {
80 |
81 | // only import if the module exists and only run if export equals httpMethod
82 | let mod
83 | try {
84 | mod = await import(pathToFileURL(apiPath).href)
85 | }
86 | catch (error) {
87 | throw new Error(`Issue when trying to import API: ${apiPath}`, { cause: error })
88 | }
89 |
90 | let method = req.method.toLowerCase() !== 'delete' ? mod[req.method.toLowerCase()] : mod['destroy']
91 | isAsyncMiddleware = Array.isArray(method)
92 | if (isAsyncMiddleware) {
93 | method.forEach(step => {
94 | if (step.constructor.name !== 'AsyncFunction') {
95 | throw Error(`Middleware function "${step.name}" for "${req.path}" is not an async function. All middleware functions must be "async".`)
96 | }
97 | })
98 | method = arc.http.apply(null, method)
99 | }
100 | if (method) {
101 |
102 | // check to see if we need to modify the req and add in params
103 | req.params = backfill(apiBaseUsed, apiPath, '', req)
104 |
105 | // grab the state from the app/api route
106 | let res = render.bind({}, apiBaseUsed)
107 | timers.start('api', 'enhance-api')
108 | state = await method(req, res)
109 | timers.stop('api')
110 |
111 | // if the api route does nothing backfill empty json response
112 | if (!state) state = { json: {} }
113 |
114 | // if the user-agent requested json return the response immediately
115 | if (isJSON(req.headers)) {
116 | if (!isAsyncMiddleware) {
117 | delete state.location
118 | }
119 | else {
120 | delete state.headers.location
121 | }
122 | return state
123 | }
124 |
125 | // just return the api response if
126 | // - not a GET
127 | // - no corresponding page
128 | // - location has been explicitly passed
129 | let location = state.location || (state?.headers?.['location'] || state?.headers?.['Location'])
130 | if (req.method.toLowerCase() != 'get' || !pagePath || location) {
131 | return state
132 | }
133 |
134 | // architect/functions always returns raw lambda response eg. {statusCode, body, headers}
135 | // but we depend on terse shorthand eg. {json}
136 | if (isAsyncMiddleware) {
137 | let b
138 | if (state.isBase64Encoded) {
139 | let encoding = state?.headers?.['content-encoding'] || state?.headers?.['Content-Encoding']
140 | let body = Buffer.from(state.body, 'base64')
141 | if (encoding === 'br') b = brotliDecompressSync(body).toString()
142 | if (encoding === 'gzip') b = gunzipSync(body).toString()
143 | }
144 | else b = state.body
145 | state.json = JSON.parse(b) || {}
146 | }
147 | }
148 | }
149 |
150 | // rendering an html page
151 | timers.start('elements', 'enhance-elements')
152 | let baseHeadElements = await getElements(basePath)
153 | let altHeadElements = {}
154 | if (altPath) altHeadElements = await getElements(altPath)
155 | let head = baseHeadElements.head || altHeadElements.head
156 | let elements = { ...altHeadElements.elements, ...baseHeadElements.elements }
157 | timers.stop('elements')
158 |
159 | let store = {
160 | ...preflightData,
161 | ...(state.json || {})
162 | }
163 |
164 | function html (str, ...values) {
165 | const _html = enhance({
166 | elements,
167 | scriptTransforms: [
168 | importTransform({ lookup: arc.static })
169 | ],
170 | styleTransforms: [
171 | styleTransform
172 | ],
173 | initialState: store
174 | })
175 | timers.start('html', 'enhance-html')
176 | const htmlString = _html(str, ...values)
177 | timers.stop('html')
178 | const fingerprinted = fingerprintPaths(htmlString)
179 | return fingerprinted
180 | }
181 |
182 | function addTimingToHeaders (res) {
183 | const { headers = {} } = res
184 | const { [timers.key]: existing = null } = headers
185 | const timingValue = timers.value()
186 | return {
187 | ...res,
188 | headers: {
189 | ...headers,
190 | [timers.key]: existing
191 | ? `${existing}, ${timingValue}`
192 | : timingValue
193 | }
194 | }
195 | }
196 |
197 | try {
198 |
199 | // 404
200 | if (!pagePath || state.code === 404 || state.status === 404 || state.statusCode === 404) {
201 | let status = 404
202 | let error = `${req.rawPath} not found`
203 | let fourOhFour = getModule(basePath, 'pages', '/404')
204 | if (altPath && !fourOhFour) fourOhFour = getModule(altPath, 'pages', '/404')
205 | let body = ''
206 | if (fourOhFour && fourOhFour.includes('.html')) {
207 | let raw = read(fourOhFour).toString()
208 | body = html`${head({ req, status, error, store })}${raw}`
209 | }
210 | else {
211 | body = html`${head({ req, status, error, store })} `
212 | }
213 | return { status, html: body }
214 | }
215 |
216 | // 200
217 | let status = state.status || state.code || state.statusCode || 200
218 | let res = {}
219 | let error = false
220 | if (pagePath.includes('.html')) {
221 | let raw = read(pagePath).toString()
222 | res.html = html`${head({ req, status, error, store })}${raw}`
223 | }
224 | else {
225 | let tag = getPageName(pageBaseUsed, pagePath)
226 | res.html = html`${head({ req, status, error, store })} `
227 | }
228 | res.statusCode = status
229 | if (state.session) res.session = state.session
230 | if (isAsyncMiddleware) {
231 | /* eslint-disable-next-line no-unused-vars */
232 | const { 'content-type': contentType, 'content-encoding': contentEncoding, ...otherHeaders } = state.headers
233 | res.headers = otherHeaders
234 | }
235 | else if (!!res.headers === false && state.headers) {
236 | // always pass headers!
237 | res.headers = state.headers
238 | }
239 | res = addTimingToHeaders(res)
240 | return res
241 | }
242 | catch (err) {
243 | // 500
244 | let status = 500
245 | let error = err.stack
246 | console.error(err)
247 | let fiveHundred = getModule(basePath, 'pages', '/500')
248 | if (altPath && !fiveHundred) fiveHundred = getModule(altPath, 'pages', '/500')
249 | let body = ''
250 | if (fiveHundred && fiveHundred.includes('.html')) {
251 | let raw = read(fiveHundred).toString()
252 | body = html`${head({ req, status, error, store })}${raw}`
253 | }
254 | else {
255 | body = html`${head({ req, status, error, store })} `
256 | }
257 | return { status, html: body }
258 | }
259 | }
260 |
--------------------------------------------------------------------------------
/src/http/any-catchall/templates/404.mjs:
--------------------------------------------------------------------------------
1 | export default function FourOFour ({ html, state }) {
2 | return html`
3 |
4 | Oops, something went wrong
5 | ${state.attrs.error || 'page not found'}
6 |
7 | `
8 | }
9 |
--------------------------------------------------------------------------------
/src/http/any-catchall/templates/500.mjs:
--------------------------------------------------------------------------------
1 | export default function FiveHundred ({ html, state }) {
2 | return html`
3 |
11 |
12 | Oops, something went wrong
13 |
14 |
${state.attrs.error || 'system failure please try again'}
15 |
16 |
17 | `
18 | }
19 |
--------------------------------------------------------------------------------
/src/http/any-catchall/templates/head.mjs:
--------------------------------------------------------------------------------
1 | export default function Head () {
2 |
3 | return `
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | `
13 | }
14 |
--------------------------------------------------------------------------------
/src/http/any-catchall/templates/html-element-wrapper.mjs:
--------------------------------------------------------------------------------
1 | function inject (str, obj) {
2 | return str.replace(/\${(.*?)}/g, (x, g) => getPath(obj, g))
3 | }
4 |
5 | function getPath (obj, path) {
6 | try {
7 | return new Function('_', 'return _.' + path)(obj)
8 | }
9 | catch (e) {
10 | return obj[path]
11 | }
12 | }
13 |
14 | export default function HTMLElementWrapper ({ template }) {
15 | return function Element ({ html, state }) {
16 | let innerTemplate = inject(template, state)
17 | return html`${innerTemplate}`
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/http/any-catchall/vendor/path-to-regexp/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/http/any-catchall/vendor/path-to-regexp/index.mjs:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | /**
4 | * Tokenize input string.
5 | */
6 | function lexer (str) {
7 | var tokens = []
8 | var i = 0
9 | while (i < str.length) {
10 | var char = str[i]
11 | if (char === '*' || char === '+' || char === '?') {
12 | tokens.push({ type: 'MODIFIER', index: i, value: str[i++] })
13 | continue
14 | }
15 | if (char === '\\') {
16 | tokens.push({ type: 'ESCAPED_CHAR', index: i++, value: str[i++] })
17 | continue
18 | }
19 | if (char === '{') {
20 | tokens.push({ type: 'OPEN', index: i, value: str[i++] })
21 | continue
22 | }
23 | if (char === '}') {
24 | tokens.push({ type: 'CLOSE', index: i, value: str[i++] })
25 | continue
26 | }
27 | if (char === ':') {
28 | var name = ''
29 | var j = i + 1
30 | while (j < str.length) {
31 | var code = str.charCodeAt(j)
32 | if (
33 | // `0-9`
34 | (code >= 48 && code <= 57) ||
35 | // `A-Z`
36 | (code >= 65 && code <= 90) ||
37 | // `a-z`
38 | (code >= 97 && code <= 122) ||
39 | // `_`
40 | code === 95) {
41 | name += str[j++]
42 | continue
43 | }
44 | break
45 | }
46 | if (!name)
47 | throw new TypeError('Missing parameter name at '.concat(i))
48 | tokens.push({ type: 'NAME', index: i, value: name })
49 | i = j
50 | continue
51 | }
52 | if (char === '(') {
53 | var count = 1
54 | var pattern = ''
55 | var j = i + 1
56 | if (str[j] === '?') {
57 | throw new TypeError('Pattern cannot start with "?" at '.concat(j))
58 | }
59 | while (j < str.length) {
60 | if (str[j] === '\\') {
61 | pattern += str[j++] + str[j++]
62 | continue
63 | }
64 | if (str[j] === ')') {
65 | count--
66 | if (count === 0) {
67 | j++
68 | break
69 | }
70 | }
71 | else if (str[j] === '(') {
72 | count++
73 | if (str[j + 1] !== '?') {
74 | throw new TypeError('Capturing groups are not allowed at '.concat(j))
75 | }
76 | }
77 | pattern += str[j++]
78 | }
79 | if (count)
80 | throw new TypeError('Unbalanced pattern at '.concat(i))
81 | if (!pattern)
82 | throw new TypeError('Missing pattern at '.concat(i))
83 | tokens.push({ type: 'PATTERN', index: i, value: pattern })
84 | i = j
85 | continue
86 | }
87 | tokens.push({ type: 'CHAR', index: i, value: str[i++] })
88 | }
89 | tokens.push({ type: 'END', index: i, value: '' })
90 | return tokens
91 | }
92 | /**
93 | * Parse a string for the raw tokens.
94 | */
95 | export function parse (str, options) {
96 | if (options === void 0) { options = {} }
97 | var tokens = lexer(str)
98 | var _a = options.prefixes, prefixes = _a === void 0 ? './' : _a
99 | var defaultPattern = '[^'.concat(escapeString(options.delimiter || '/#?'), ']+?')
100 | var result = []
101 | var key = 0
102 | var i = 0
103 | var path = ''
104 | var tryConsume = function (type) {
105 | if (i < tokens.length && tokens[i].type === type)
106 | return tokens[i++].value
107 | }
108 | var mustConsume = function (type) {
109 | var value = tryConsume(type)
110 | if (value !== undefined)
111 | return value
112 | var _a = tokens[i], nextType = _a.type, index = _a.index
113 | throw new TypeError('Unexpected '.concat(nextType, ' at ').concat(index, ', expected ').concat(type))
114 | }
115 | var consumeText = function () {
116 | var result = ''
117 | var value
118 | while ((value = tryConsume('CHAR') || tryConsume('ESCAPED_CHAR'))) {
119 | result += value
120 | }
121 | return result
122 | }
123 | while (i < tokens.length) {
124 | var char = tryConsume('CHAR')
125 | var name = tryConsume('NAME')
126 | var pattern = tryConsume('PATTERN')
127 | if (name || pattern) {
128 | var prefix = char || ''
129 | if (prefixes.indexOf(prefix) === -1) {
130 | path += prefix
131 | prefix = ''
132 | }
133 | if (path) {
134 | result.push(path)
135 | path = ''
136 | }
137 | result.push({
138 | name: name || key++,
139 | prefix: prefix,
140 | suffix: '',
141 | pattern: pattern || defaultPattern,
142 | modifier: tryConsume('MODIFIER') || '',
143 | })
144 | continue
145 | }
146 | var value = char || tryConsume('ESCAPED_CHAR')
147 | if (value) {
148 | path += value
149 | continue
150 | }
151 | if (path) {
152 | result.push(path)
153 | path = ''
154 | }
155 | var open = tryConsume('OPEN')
156 | if (open) {
157 | var prefix = consumeText()
158 | var name_1 = tryConsume('NAME') || ''
159 | var pattern_1 = tryConsume('PATTERN') || ''
160 | var suffix = consumeText()
161 | mustConsume('CLOSE')
162 | result.push({
163 | name: name_1 || (pattern_1 ? key++ : ''),
164 | pattern: name_1 && !pattern_1 ? defaultPattern : pattern_1,
165 | prefix: prefix,
166 | suffix: suffix,
167 | modifier: tryConsume('MODIFIER') || '',
168 | })
169 | continue
170 | }
171 | mustConsume('END')
172 | }
173 | return result
174 | }
175 | /**
176 | * Compile a string to a template function for the path.
177 | */
178 | export function compile (str, options) {
179 | return tokensToFunction(parse(str, options), options)
180 | }
181 | /**
182 | * Expose a method for transforming tokens into the path function.
183 | */
184 | export function tokensToFunction (tokens, options) {
185 | if (options === void 0) { options = {} }
186 | var reFlags = flags(options)
187 | var _a = options.encode, encode = _a === void 0 ? function (x) { return x } : _a, _b = options.validate, validate = _b === void 0 ? true : _b
188 | // Compile all the tokens into regexps.
189 | var matches = tokens.map(function (token) {
190 | if (typeof token === 'object') {
191 | return new RegExp('^(?:'.concat(token.pattern, ')$'), reFlags)
192 | }
193 | })
194 | return function (data) {
195 | var path = ''
196 | for (var i = 0; i < tokens.length; i++) {
197 | var token = tokens[i]
198 | if (typeof token === 'string') {
199 | path += token
200 | continue
201 | }
202 | var value = data ? data[token.name] : undefined
203 | var optional = token.modifier === '?' || token.modifier === '*'
204 | var repeat = token.modifier === '*' || token.modifier === '+'
205 | if (Array.isArray(value)) {
206 | if (!repeat) {
207 | throw new TypeError('Expected "'.concat(token.name, '" to not repeat, but got an array'))
208 | }
209 | if (value.length === 0) {
210 | if (optional)
211 | continue
212 | throw new TypeError('Expected "'.concat(token.name, '" to not be empty'))
213 | }
214 | for (var j = 0; j < value.length; j++) {
215 | var segment = encode(value[j], token)
216 | if (validate && !matches[i].test(segment)) {
217 | throw new TypeError('Expected all "'.concat(token.name, '" to match "').concat(token.pattern, '", but got "').concat(segment, '"'))
218 | }
219 | path += token.prefix + segment + token.suffix
220 | }
221 | continue
222 | }
223 | if (typeof value === 'string' || typeof value === 'number') {
224 | var segment = encode(String(value), token)
225 | if (validate && !matches[i].test(segment)) {
226 | throw new TypeError('Expected "'.concat(token.name, '" to match "').concat(token.pattern, '", but got "').concat(segment, '"'))
227 | }
228 | path += token.prefix + segment + token.suffix
229 | continue
230 | }
231 | if (optional)
232 | continue
233 | var typeOfMessage = repeat ? 'an array' : 'a string'
234 | throw new TypeError('Expected "'.concat(token.name, '" to be ').concat(typeOfMessage))
235 | }
236 | return path
237 | }
238 | }
239 | /**
240 | * Create path match function from `path-to-regexp` spec.
241 | */
242 | export function match (str, options) {
243 | var keys = []
244 | var re = pathToRegexp(str, keys, options)
245 | return regexpToFunction(re, keys, options)
246 | }
247 | /**
248 | * Create a path match function from `path-to-regexp` output.
249 | */
250 | export function regexpToFunction (re, keys, options) {
251 | if (options === void 0) { options = {} }
252 | var _a = options.decode, decode = _a === void 0 ? function (x) { return x } : _a
253 | return function (pathname) {
254 | var m = re.exec(pathname)
255 | if (!m)
256 | return false
257 | var path = m[0], index = m.index
258 | var params = Object.create(null)
259 | var _loop_1 = function (i) {
260 | if (m[i] === undefined)
261 | return 'continue'
262 | var key = keys[i - 1]
263 | if (key.modifier === '*' || key.modifier === '+') {
264 | params[key.name] = m[i].split(key.prefix + key.suffix).map(function (value) {
265 | return decode(value, key)
266 | })
267 | }
268 | else {
269 | params[key.name] = decode(m[i], key)
270 | }
271 | }
272 | for (var i = 1; i < m.length; i++) {
273 | _loop_1(i)
274 | }
275 | return { path: path, index: index, params: params }
276 | }
277 | }
278 | /**
279 | * Escape a regular expression string.
280 | */
281 | function escapeString (str) {
282 | return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1')
283 | }
284 | /**
285 | * Get the flags for a regexp from the options.
286 | */
287 | function flags (options) {
288 | return options && options.sensitive ? '' : 'i'
289 | }
290 | /**
291 | * Pull out keys from a regexp.
292 | */
293 | function regexpToRegexp (path, keys) {
294 | if (!keys)
295 | return path
296 | var groupsRegex = /\((?:\?<(.*?)>)?(?!\?)/g
297 | var index = 0
298 | var execResult = groupsRegex.exec(path.source)
299 | while (execResult) {
300 | keys.push({
301 | // Use parenthesized substring match if available, index otherwise
302 | name: execResult[1] || index++,
303 | prefix: '',
304 | suffix: '',
305 | modifier: '',
306 | pattern: '',
307 | })
308 | execResult = groupsRegex.exec(path.source)
309 | }
310 | return path
311 | }
312 | /**
313 | * Transform an array into a regexp.
314 | */
315 | function arrayToRegexp (paths, keys, options) {
316 | var parts = paths.map(function (path) { return pathToRegexp(path, keys, options).source })
317 | return new RegExp('(?:'.concat(parts.join('|'), ')'), flags(options))
318 | }
319 | /**
320 | * Create a path regexp from string input.
321 | */
322 | function stringToRegexp (path, keys, options) {
323 | return tokensToRegexp(parse(path, options), keys, options)
324 | }
325 | /**
326 | * Expose a function for taking tokens and returning a RegExp.
327 | */
328 | export function tokensToRegexp (tokens, keys, options) {
329 | if (options === void 0) { options = {} }
330 | var _a = options.strict, strict = _a === void 0 ? false : _a, _b = options.start, start = _b === void 0 ? true : _b, _c = options.end, end = _c === void 0 ? true : _c, _d = options.encode, encode = _d === void 0 ? function (x) { return x } : _d, _e = options.delimiter, delimiter = _e === void 0 ? '/#?' : _e, _f = options.endsWith, endsWith = _f === void 0 ? '' : _f
331 | var endsWithRe = '['.concat(escapeString(endsWith), ']|$')
332 | var delimiterRe = '['.concat(escapeString(delimiter), ']')
333 | var route = start ? '^' : ''
334 | // Iterate over the tokens and create our regexp string.
335 | for (var _i = 0, tokens_1 = tokens; _i < tokens_1.length; _i++) {
336 | var token = tokens_1[_i]
337 | if (typeof token === 'string') {
338 | route += escapeString(encode(token))
339 | }
340 | else {
341 | var prefix = escapeString(encode(token.prefix))
342 | var suffix = escapeString(encode(token.suffix))
343 | if (token.pattern) {
344 | if (keys)
345 | keys.push(token)
346 | if (prefix || suffix) {
347 | if (token.modifier === '+' || token.modifier === '*') {
348 | var mod = token.modifier === '*' ? '?' : ''
349 | route += '(?:'.concat(prefix, '((?:').concat(token.pattern, ')(?:').concat(suffix).concat(prefix, '(?:').concat(token.pattern, '))*)').concat(suffix, ')').concat(mod)
350 | }
351 | else {
352 | route += '(?:'.concat(prefix, '(').concat(token.pattern, ')').concat(suffix, ')').concat(token.modifier)
353 | }
354 | }
355 | else {
356 | if (token.modifier === '+' || token.modifier === '*') {
357 | route += '((?:'.concat(token.pattern, ')').concat(token.modifier, ')')
358 | }
359 | else {
360 | route += '('.concat(token.pattern, ')').concat(token.modifier)
361 | }
362 | }
363 | }
364 | else {
365 | route += '(?:'.concat(prefix).concat(suffix, ')').concat(token.modifier)
366 | }
367 | }
368 | }
369 | if (end) {
370 | if (!strict)
371 | route += ''.concat(delimiterRe, '?')
372 | route += !options.endsWith ? '$' : '(?='.concat(endsWithRe, ')')
373 | }
374 | else {
375 | var endToken = tokens[tokens.length - 1]
376 | var isEndDelimited = typeof endToken === 'string'
377 | ? delimiterRe.indexOf(endToken[endToken.length - 1]) > -1
378 | : endToken === undefined
379 | if (!strict) {
380 | route += '(?:'.concat(delimiterRe, '(?=').concat(endsWithRe, '))?')
381 | }
382 | if (!isEndDelimited) {
383 | route += '(?='.concat(delimiterRe, '|').concat(endsWithRe, ')')
384 | }
385 | }
386 | return new RegExp(route, flags(options))
387 | }
388 | /**
389 | * Normalize the given path string, returning a regular expression.
390 | *
391 | * An empty array can be passed in for the keys, which will hold the
392 | * placeholder key descriptions. For example, using `/user/:id`, `keys` will
393 | * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`.
394 | */
395 | export function pathToRegexp (path, keys, options) {
396 | if (path instanceof RegExp)
397 | return regexpToRegexp(path, keys)
398 | if (Array.isArray(path))
399 | return arrayToRegexp(path, keys, options)
400 | return stringToRegexp(path, keys, options)
401 | }
402 | // # sourceMappingURL=index.js.map
403 |
--------------------------------------------------------------------------------
/src/http/get-_public-catchall/index.mjs:
--------------------------------------------------------------------------------
1 | import asap from '@architect/asap'
2 |
3 | export async function handler (req) {
4 | req.rawPath = req.rawPath.replace('/_public', '')
5 | const response = await asap()(req)
6 | const size = response?.body?.length
7 | if (size > 5500000) { // 5.5MB
8 | return {
9 | statusCode: 302,
10 | headers: { Location: `/_static${req.rawPath}` }
11 | }
12 | }
13 | return response
14 | }
15 |
--------------------------------------------------------------------------------
/src/plugins/enhance.js:
--------------------------------------------------------------------------------
1 | const { join } = require('path')
2 | const rollup = require('@enhance/arc-plugin-rollup')
3 |
4 | module.exports = {
5 | sandbox: {
6 | async start (params) {
7 | await rollup.sandbox.start(params)
8 | },
9 |
10 | async watcher (params) {
11 | await rollup.sandbox.watcher(params)
12 | }
13 | },
14 |
15 | deploy: {
16 | async start (params) {
17 | await rollup.deploy.start(params)
18 | }
19 | },
20 |
21 | set: {
22 | static () {
23 | return {
24 | fingerprint: true,
25 | prune: true
26 | }
27 | },
28 |
29 | /** frontend logic will *only* be shared w ANY and GET handlers */
30 | views ({ inventory }) {
31 | const cwd = inventory.inv._project.cwd
32 | return {
33 | src: join(cwd, 'app')
34 | }
35 | },
36 |
37 | /** we want to share models business logic across all lambdas */
38 | shared ({ inventory }) {
39 | const cwd = inventory.inv._project.cwd
40 | return {
41 | src: join(cwd, 'shared')
42 | }
43 | },
44 |
45 | /**
46 | * sets up a greedy lambda for the frontend
47 | *
48 | * - userland can still add routes to override this!
49 | * - makes single responsibility functions an opt-in rather than up front cost
50 | */
51 | http () {
52 | let rootCatchallSrcDir = join(__dirname, '..', 'http', 'any-catchall')
53 | let staticAssetSrcDir = join(__dirname, '..', 'http', 'get-_public-catchall')
54 | return [
55 | {
56 | method: 'any',
57 | path: '/*',
58 | src: rootCatchallSrcDir,
59 | },
60 | {
61 | method: 'get',
62 | path: '/_public/*',
63 | src: staticAssetSrcDir,
64 | config: {
65 | // shared: false, // TODO ensure static.json remains, but shared is cleared out
66 | views: false,
67 | }
68 | },
69 | ]
70 | },
71 |
72 | /** adds the begin/data data table */
73 | tables () {
74 | return {
75 | name: 'data',
76 | partitionKey: 'scopeID',
77 | partitionKeyType: 'string',
78 | sortKey: 'dataID',
79 | sortKeyType: 'string',
80 | ttl: 'TTL'
81 | }
82 | },
83 |
84 |
85 | env ({ arc }) {
86 | let elements = arc['enhance-elements'] || []
87 | return {
88 | ELEMENTS: elements
89 | }
90 | }
91 | }
92 |
93 | }
94 |
95 |
--------------------------------------------------------------------------------
/test/_fingerprint-paths.mjs:
--------------------------------------------------------------------------------
1 | import test from 'tape'
2 | import { replaceEvery } from '../src/http/any-catchall/_fingerprint-paths.mjs'
3 |
4 |
5 | test('path fingerpinter', t => {
6 | t.plan(1)
7 | const input = `
8 | /_public/foo.mjs
9 | /_public/foo/foo.mjs
10 | /_public/fooooo.mjs
11 | /_public/bar.mjs
12 | /_public/font/foo.woff2
13 | /_public/font/foo.woff
14 | `
15 | const manifest = {
16 | 'foo.mjs': 'foo-abc.mjs',
17 | 'bar.mjs': 'bar-1$1.mjs',
18 | 'foo/foo.mjs': 'foo/foo-123.mjs',
19 | 'font/foo.woff': 'font/foo-abc.woff',
20 | 'font/foo.woff2': 'font/foo-xyz.woff2'
21 | }
22 | const expected = `
23 | /_public/foo-abc.mjs
24 | /_public/foo/foo-123.mjs
25 | /_public/fooooo.mjs
26 | /_public/bar-1$1.mjs
27 | /_public/font/foo-xyz.woff2
28 | /_public/font/foo-abc.woff
29 | `
30 | let result = replaceEvery(input, manifest)
31 | console.log(result)
32 |
33 | t.equal(result, expected, 'fingerprinted')
34 | })
35 |
--------------------------------------------------------------------------------
/test/_get-files.mjs:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import test from 'tape'
3 | import getFiles from '../src/http/any-catchall/_get-files.mjs'
4 |
5 | test('getFiles', async t => {
6 | t.plan(1)
7 | let base = path.join(process.cwd(), 'app')
8 | let folder = 'pages'
9 | let expected = path.join(base, folder)
10 | let result = await getFiles(base, folder)
11 | let results = result.map(f => f.startsWith(expected))
12 | let truthy = results.filter(Boolean)
13 | t.ok(truthy.length === results.length, 'got filtered list')
14 | })
15 |
16 |
--------------------------------------------------------------------------------
/test/_get-module.mjs:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import test from 'tape'
3 | import url from 'url'
4 | import getModule from '../src/http/any-catchall/_get-module.mjs'
5 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
6 |
7 | test('getModules', async t => {
8 | t.plan(1)
9 | let base = path.join(__dirname, '..', 'app')
10 | let folder = 'pages'
11 | let expected = path.join(base, folder, 'index.html')
12 | let result = await getModule(base, folder, '/')
13 | t.equal(expected, result, 'Got back index')
14 | })
15 |
16 | test('getModules multiple params', async t => {
17 | t.plan(1)
18 | let base = path.join(__dirname, 'mock-folders', 'app')
19 | let folder = 'api/foo/$first/bar/$second/baz'
20 | let file = '$third.mjs'
21 | let expected = path.join(base, folder, file)
22 | let result = await getModule(base, 'api', '/foo/7/bar/8/baz/9')
23 | t.equal(expected, result, 'Got the api with multiple params')
24 | })
25 |
26 | test('getModules catchall', async t => {
27 | t.plan(1)
28 | let base = path.join(process.cwd(), 'test', 'mock-folders', 'app')
29 | let folder = 'api/place/$id'
30 | let file = '$$.mjs'
31 | let expected = path.join(base, folder, file)
32 | let result = await getModule(base, folder, '/place/anything/anywhere')
33 | t.equal(expected, result, 'Got the catchall')
34 | })
35 |
36 | test('getModules no api', async t => {
37 | t.plan(1)
38 | let base = path.join(process.cwd(), 'test', 'mock-apps', 'app')
39 | let folder = 'api'
40 | let expected = false
41 | let result = await getModule(base, folder, '/')
42 | t.equal(expected, result, 'No api')
43 | })
44 |
--------------------------------------------------------------------------------
/test/_get-preflight.mjs:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import test from 'tape'
3 | import url from 'url'
4 | import getPreflight from '../src/http/any-catchall/_get-preflight.mjs'
5 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
6 |
7 | test('getPreflight', async t => {
8 | t.plan(1)
9 | const basePath = path.join(__dirname, 'mock-preflight', 'app')
10 | const expected = {
11 | pageTitle: 'About',
12 | account: {
13 | username: 'bobsyouruncle',
14 | id: '23jk24h24'
15 | }
16 | }
17 | const preflight = await getPreflight({ basePath })
18 | t.deepEqual(expected, preflight({ req: { path: '/about' } }), 'Got preflight')
19 | })
20 |
21 | test('missing preflight', async t => {
22 | t.plan(1)
23 | const basePath = path.join(__dirname, 'mock-app', 'app')
24 | const preflight = await getPreflight({ basePath })
25 | t.notok(preflight, 'Missing preflight is OK')
26 | })
27 |
28 | test('preflight with missing module import inside it should throw', async t => {
29 | t.plan(1)
30 | const basePath = path.join(__dirname, 'mock-preflight', 'app', 'bad-preflight')
31 | try {
32 | await getPreflight({ basePath })
33 | t.fail('Missing module import should throw')
34 | }
35 | catch (error) {
36 | t.ok(error, 'Missing module import throws')
37 | }
38 | })
39 |
--------------------------------------------------------------------------------
/test/_render.mjs:
--------------------------------------------------------------------------------
1 | import test from 'tape'
2 | import url from 'url'
3 | import path from 'path'
4 | import render from '../src/http/any-catchall/_render.mjs'
5 |
6 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
7 |
8 | test('render', async t => {
9 | let base = path.join(__dirname, '..', 'app')
10 | let result = await render(base, '/', {})
11 | t.ok(result, 'got result')
12 | console.log(result)
13 | })
14 |
15 | test('404', async t => {
16 | let base = path.join(__dirname, '..', 'app')
17 | let result = await render(base, '/nope', {})
18 | t.ok(result, 'got 404')
19 | console.log(result)
20 | })
21 |
--------------------------------------------------------------------------------
/test/_sort-routes.mjs:
--------------------------------------------------------------------------------
1 | import test from 'tape'
2 | import sorter from '../src/http/any-catchall/_sort-routes.mjs'
3 |
4 | test('sorter', t => {
5 | t.plan(1)
6 | const good = [
7 | 'index.mjs',
8 | 'views/index.mjs',
9 | 'views/pages/books/index.mjs',
10 | 'views/pages/books/new.mjs',
11 | 'views/pages/books/ack.mjs',
12 | 'views/pages/books/back.mjs',
13 | 'views/pages/books/$id/arg.mjs',
14 | 'views/pages/books/$id.mjs',
15 | 'views/pages/books/$$.mjs',
16 | 'views/pages/$thing/food/arg.mjs',
17 | 'views/pages/$thing/id/$place.mjs',
18 | 'views/pages/$thing/$id/$place.mjs',
19 | 'views/pages/$$.mjs',
20 | 'views/$$.mjs',
21 | '$$.mjs',
22 | ]
23 | const bad = [
24 | '$$.mjs',
25 | 'views/$$.mjs',
26 | 'views/pages/$$.mjs',
27 | 'views/pages/$thing/$id/$place.mjs',
28 | 'views/pages/$thing/id/$place.mjs',
29 | 'views/pages/$thing/food/arg.mjs',
30 | 'views/pages/books/$$.mjs',
31 | 'views/pages/books/$id.mjs',
32 | 'views/pages/books/$id/arg.mjs',
33 | 'views/pages/books/new.mjs',
34 | 'views/pages/books/ack.mjs',
35 | 'views/pages/books/back.mjs',
36 | 'views/pages/books/index.mjs',
37 | 'views/index.mjs',
38 | 'index.mjs',
39 | ]
40 |
41 | let result = bad.sort(sorter)
42 |
43 | t.deepEqual(result, good, 'sorted')
44 | })
45 |
46 | test('sort dynamic parts diff positions ', t => {
47 | t.plan(1)
48 | const good = [
49 | 'views/cats/food/$id.mjs',
50 | 'views/$thing/food/arg.mjs',
51 | ]
52 | const bad = [
53 | 'views/$thing/food/arg.mjs',
54 | 'views/cats/food/$id.mjs',
55 | ]
56 |
57 | let result = bad.sort(sorter)
58 |
59 | t.deepEqual(result, good, 'dynamic parts in different positions sorted')
60 | })
61 |
62 | test('sorting with very deeply nested paths', t => {
63 | t.plan(1)
64 | const good = [
65 | '/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/new.mjs',
66 | '/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/$id.mjs',
67 | '/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/$$.mjs',
68 | ]
69 | const bad = [
70 | '/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/$$.mjs',
71 | '/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/$id.mjs',
72 | '/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/new.mjs',
73 | ]
74 |
75 | let result = bad.sort(sorter)
76 |
77 | t.deepEqual(result, good, 'deeply nested path sorted')
78 | })
79 |
80 | test('sorting mixed types in dynamic routes', t => {
81 | t.plan(1)
82 | const good = [
83 | '$level1/$level2/$level3.mjs',
84 | '$level1/$level2.html',
85 | '$level1.mjs',
86 | ]
87 | const bad = [
88 | '$level1.mjs',
89 | '$level1/$level2.html',
90 | '$level1/$level2/$level3.mjs',
91 | ]
92 |
93 | let result = bad.sort(sorter)
94 |
95 | t.deepEqual(result, good, 'mixed types of dynamic paths')
96 | })
97 |
--------------------------------------------------------------------------------
/test/backfill-params.mjs:
--------------------------------------------------------------------------------
1 | import test from 'tape'
2 | import url from 'url'
3 | import path from 'path'
4 | import backfill from '../src/http/any-catchall/_backfill-params.mjs'
5 |
6 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
7 |
8 | test('backfill - null case', async (t) => {
9 | t.plan(1)
10 | let basePath = path.join(__dirname, '..', 'app')
11 | let apiPath = path.join(basePath, '/api/foo')
12 | let pagePath = undefined
13 | let req = {
14 | rawPath: '/foo',
15 | params: {},
16 | }
17 |
18 | let params = backfill(basePath, apiPath, pagePath, req)
19 | t.deepEqual(params, req.params, 'No params in null case')
20 | })
21 |
22 | test("backfill - one level '/api/foo/$id'", async (t) => {
23 | t.plan(1)
24 | let basePath = path.join(__dirname, '..', 'app')
25 | let apiPath = path.join(basePath, '/api/foo/$id')
26 | let pagePath = undefined
27 | let req = {
28 | rawPath: '/foo/5',
29 | params: {},
30 | }
31 |
32 | let params = backfill(basePath, apiPath, pagePath, req)
33 | t.deepEqual(params, { id: '5' }, '$id param parsed correctly')
34 | })
35 |
36 | test("backfill - pathological '/api/foo/$first/bar/$second/baz/$third'", async (t) => {
37 | t.plan(1)
38 | let basePath = path.join(__dirname, '..', 'app')
39 | let apiPath = path.join(basePath, '/api/foo/$first/bar/$second/baz/$third')
40 | let pagePath = undefined
41 | let req = {
42 | rawPath: '/foo/5/bar/6/baz/7',
43 | params: {},
44 | }
45 |
46 | let params = backfill(basePath, apiPath, pagePath, req)
47 | t.deepEqual(
48 | params,
49 | { first: '5', second: '6', third: '7' },
50 | 'all params parsed correctly'
51 | )
52 | })
53 |
--------------------------------------------------------------------------------
/test/dynamic-routing-pages.mjs:
--------------------------------------------------------------------------------
1 | import test from 'tape'
2 | import url from 'url'
3 | import path from 'path'
4 | import router from '../src/http/any-catchall/router.mjs'
5 |
6 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
7 |
8 | test('router finds right dynamic page mjs', async t => {
9 | t.plan(1)
10 | let req = {
11 | rawPath: '/one/two/three',
12 | method: 'GET',
13 | headers: {
14 | 'accept': 'text/html',
15 | },
16 | }
17 | let basePath = path.join(__dirname, 'mock-dynamic-routes', 'app')
18 | let res = await router.bind({}, { basePath })(req)
19 | t.ok(res.html.includes('LEVEL3'), 'Got the Right mjs Page')
20 | console.log(res)
21 | })
22 |
23 |
24 | test('router finds right dynamic html page', async t => {
25 | t.plan(1)
26 | let req = {
27 | rawPath: '/one/two',
28 | method: 'GET',
29 | headers: {
30 | 'accept': 'text/html',
31 | },
32 | }
33 | let basePath = path.join(__dirname, 'mock-dynamic-routes', 'app')
34 | let res = await router.bind({}, { basePath })(req)
35 | t.ok(res.html.includes('LEVEL2'), 'Got the Right HTML Page')
36 | console.log(res)
37 | })
38 |
39 |
40 | test('router finds right dynamic catch-all html page', async t => {
41 | t.plan(1)
42 | let req = {
43 | rawPath: '/docs/anything',
44 | method: 'GET',
45 | headers: {
46 | 'accept': 'text/html',
47 | },
48 | }
49 | let basePath = path.join(__dirname, 'mock-dynamic-routes', 'app')
50 | let res = await router.bind({}, { basePath })(req)
51 | t.ok(res.html.includes('DOCS'), 'Got the Right HTML Page')
52 | console.log(res)
53 | })
54 |
--------------------------------------------------------------------------------
/test/dynamic-routing.mjs:
--------------------------------------------------------------------------------
1 | import test from 'tape'
2 | import url from 'url'
3 | import path from 'path'
4 | import sandbox from '@architect/sandbox'
5 | import { get } from 'tiny-json-http'
6 |
7 | const baseUrl = 'http://localhost:3333'
8 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
9 |
10 | test(`Start local server`, async t => {
11 | await sandbox.start({ quiet: true, cwd: path.join(__dirname, '..') })
12 | t.pass('local server started')
13 | t.end()
14 | })
15 |
16 | test('Root responds', async t => {
17 | const response = await get({ url: baseUrl })
18 | t.ok(response.body, 'We got a response')
19 | t.end()
20 | })
21 |
22 | test('specific API does not get swallowed by catchall page', async t => {
23 | const response = await get({ url: baseUrl + '/test/one' })
24 | t.ok(response.body.data === 'one', 'API makes it through')
25 | t.end()
26 | })
27 |
28 | test('specific page does not get served by a more generic api handler', async t => {
29 | const response = await get({ url: baseUrl + '/test/two' })
30 | const expectedPartial = ` `
31 | t.ok(response.body.includes(expectedPartial), 'only the specific page handler responds')
32 | t.end()
33 | })
34 |
35 | test('Shut down local server', async t => {
36 | await sandbox.end()
37 | t.pass('Shut down Sandbox')
38 | t.end()
39 | }) === 'one'
40 |
--------------------------------------------------------------------------------
/test/middleware.mjs:
--------------------------------------------------------------------------------
1 | import test from 'tape'
2 | import url from 'url'
3 | import path from 'path'
4 | import sandbox from '@architect/sandbox'
5 | import { get } from 'tiny-json-http'
6 |
7 | const baseUrl = 'http://localhost:3333'
8 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
9 |
10 | test(`Start local server`, async t => {
11 | await sandbox.start({ quiet: true, cwd: path.join(__dirname, '..') })
12 | t.pass('local server started')
13 | t.end()
14 | })
15 |
16 | test('request to route using middleware responds', async t => {
17 | const response = await get({ headers: { 'content-type': 'html/text; charset:utf8' }, url: baseUrl + '/middleware-test' })
18 | const expected = `Hello World {"first":"thing","second":false} `
19 | t.ok(response.body.includes(expected), 'middleware responds')
20 | t.end()
21 | })
22 |
23 | test('Shut down local server', async t => {
24 | await sandbox.end()
25 | t.pass('Shut down Sandbox')
26 | t.end()
27 | }) === 'one'
28 |
--------------------------------------------------------------------------------
/test/mock-apps/app/api/about.mjs:
--------------------------------------------------------------------------------
1 | export async function get (/* req */) {
2 | return {
3 | json: { people: [ 'backup' ] }
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/test/mock-apps/app/api/backup-data.mjs:
--------------------------------------------------------------------------------
1 | // View documentation at: https://enhance.dev/docs/learn/starter-project/api
2 | /**
3 | * @typedef {import('@enhance/types').EnhanceApiFn} EnhanceApiFn
4 | */
5 |
6 | /**
7 | * @type {EnhanceApiFn}
8 | */
9 | export async function get () {
10 | return {
11 | json: { data: [ 'fred', 'joe', 'mary' ] }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/test/mock-apps/app/api/docs/$$.mjs:
--------------------------------------------------------------------------------
1 | // View documentation at: https://enhance.dev/docs/learn/starter-project/api
2 | /**
3 | * @typedef {import('@enhance/types').EnhanceApiFn} EnhanceApiFn
4 | */
5 |
6 | /**
7 | * @type {EnhanceApiFn}
8 | */
9 | export async function get () {
10 | return {
11 | json: { data: [ 'fred', 'joe', 'mary' ] }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/test/mock-apps/app/api/one/two/three-alt.mjs:
--------------------------------------------------------------------------------
1 | export async function get (){
2 | return {
3 | json: { where: '/one/two/three-alt.mjs in alternate app' }
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/test/mock-apps/app/pages/about.mjs:
--------------------------------------------------------------------------------
1 | export default function About ({ html, state }) {
2 | return html`
3 |
4 |
5 |
6 | ${JSON.stringify(state, null, 2)}
7 | backup-markup
8 |
9 |
10 | `
11 | }
12 |
--------------------------------------------------------------------------------
/test/mock-apps/app/pages/docs/index.html:
--------------------------------------------------------------------------------
1 | docs page
2 |
--------------------------------------------------------------------------------
/test/mock-apps/app/pages/index.html:
--------------------------------------------------------------------------------
1 | a page
2 |
--------------------------------------------------------------------------------
/test/mock-async-middleware/app/api/index.mjs:
--------------------------------------------------------------------------------
1 | export let get = [ one, two ]
2 |
3 | async function one (req) {
4 | req.stuff = [ 1, true, 'yas' ]
5 | }
6 |
7 | async function two (req) {
8 | let stuff = req.stuff
9 | return {
10 | session: { works: true },
11 | json: { stuff }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/test/mock-async-middleware/app/api/test-redirect-header.mjs:
--------------------------------------------------------------------------------
1 | export let get = [ one, two ]
2 |
3 | async function one (/* req */) {
4 | // should exit middleware and redirect to /login
5 | return {
6 | location: '/login'
7 | }
8 | }
9 |
10 | async function two (/* req */) {
11 | // should not enter via middleware
12 | throw 'Should never be reached.'
13 | }
14 |
--------------------------------------------------------------------------------
/test/mock-async-middleware/app/api/test-redirect.mjs:
--------------------------------------------------------------------------------
1 | export let get = [ one, two ]
2 |
3 | async function one (/* req */) {
4 | // should exit middleware and redirect to /login
5 | return {
6 | location: '/login'
7 | }
8 | }
9 |
10 | async function two (/* req */) {
11 | // should not enter via middleware
12 | throw 'Should never be reached.'
13 | }
14 |
--------------------------------------------------------------------------------
/test/mock-async-middleware/app/api/test-store.mjs:
--------------------------------------------------------------------------------
1 | export let get = [ one, two ]
2 |
3 | async function one (req) {
4 | req.stuff = [ 1, true, 'yas' ]
5 | }
6 |
7 | async function two (req) {
8 | let stuff = req.stuff
9 | return {
10 | session: { works: true },
11 | json: { stuff }
12 | }
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/test/mock-async-middleware/app/elements/debug-state.mjs:
--------------------------------------------------------------------------------
1 | export default function debug ({ html, state }) {
2 | return html`${JSON.stringify(state, null, 2)} `
3 | }
4 |
--------------------------------------------------------------------------------
/test/mock-async-middleware/app/pages/test-redirect.html:
--------------------------------------------------------------------------------
1 | Should never be reached.
--------------------------------------------------------------------------------
/test/mock-async-middleware/app/pages/test-store.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/mock-dots/app/api/.well-known/webfinger.mjs:
--------------------------------------------------------------------------------
1 | export async function get (req) {
2 | return { json: { hello: true }}
3 | }
4 |
--------------------------------------------------------------------------------
/test/mock-dots/app/api/index.mjs:
--------------------------------------------------------------------------------
1 | export async function get () {
2 | return { json: { root: true } }
3 | }
4 |
--------------------------------------------------------------------------------
/test/mock-dynamic-routes/app/pages/$level1.mjs:
--------------------------------------------------------------------------------
1 | export default function Level1 ({ html }){
2 | return html`LEVEL1`
3 | }
4 |
--------------------------------------------------------------------------------
/test/mock-dynamic-routes/app/pages/$level1/$level2.html:
--------------------------------------------------------------------------------
1 | LEVEL2
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/test/mock-dynamic-routes/app/pages/$level1/$level2/$level3.mjs:
--------------------------------------------------------------------------------
1 | export default function Level4 ({ html }){
2 | return html`LEVEL3`
3 | }
4 |
--------------------------------------------------------------------------------
/test/mock-dynamic-routes/app/pages/docs/$$.mjs:
--------------------------------------------------------------------------------
1 | export default function Level1 ({ html }){
2 | return html`DOCS`
3 | }
4 |
--------------------------------------------------------------------------------
/test/mock-errors/app/pages/404.mjs:
--------------------------------------------------------------------------------
1 | export default function FourOhFour ({ html, state }) {
2 | const { error } = state.attrs
3 |
4 | return html`
5 |
6 | Custom 404
7 | Sorry we can't find that.
8 | ${error && error}
9 |
10 | `
11 | }
12 |
--------------------------------------------------------------------------------
/test/mock-folders/app/api/$$extra.mjs:
--------------------------------------------------------------------------------
1 | export function get (){}
2 |
--------------------------------------------------------------------------------
/test/mock-folders/app/api/foo/$first/bar/$second/baz/$third.mjs:
--------------------------------------------------------------------------------
1 | export function get (){}
2 |
--------------------------------------------------------------------------------
/test/mock-folders/app/api/index.mjs:
--------------------------------------------------------------------------------
1 | export function get (){}
2 |
--------------------------------------------------------------------------------
/test/mock-folders/app/api/place/$id/$$.mjs:
--------------------------------------------------------------------------------
1 | export function get (){}
2 |
--------------------------------------------------------------------------------
/test/mock-headers/app/api/index.mjs:
--------------------------------------------------------------------------------
1 | // View documentation at: https://enhance.dev/docs/learn/starter-project/api
2 | /**
3 | * @typedef {import('@enhance/types').EnhanceApiFn} EnhanceApiFn
4 | */
5 |
6 | /**
7 | * @type {EnhanceApiFn}
8 | *
9 | * testing a custom header
10 | */
11 | export async function get () {
12 | return {
13 | headers: {
14 | 'x-custom-header': 'custom-header-value',
15 | },
16 | json: { data: [ 'sutro', 'turtle', 'mae mae' ] }
17 | }
18 | }
19 |
20 | /**
21 | * @type {EnhanceApiFn}
22 | *
23 | * testing a custom header in midddleware
24 | */
25 | export let post = [ async function fn () {
26 | return {
27 | headers: {
28 | 'x-custom-header': 'custom-header-value',
29 | },
30 | json: { data: [ 'sutro', 'turtle', 'mae mae' ] }
31 | }
32 | } ]
33 |
--------------------------------------------------------------------------------
/test/mock-headers/app/pages/index.html:
--------------------------------------------------------------------------------
1 | fun new (to me) element
2 |
--------------------------------------------------------------------------------
/test/mock-html-elements/app/elements/html-element.html:
--------------------------------------------------------------------------------
1 |
2 |
This is a HTML only element.
3 |
4 |
--------------------------------------------------------------------------------
/test/mock-html-elements/app/pages/test-element.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/mock-preflight/app/api/index.mjs:
--------------------------------------------------------------------------------
1 | export async function get () {
2 | return {
3 | json: {
4 | account: {
5 | username: 'thisshouldoverride',
6 | id: '39nr34n2'
7 | }
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/mock-preflight/app/bad-preflight/preflight.mjs:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line
2 | import { nope } from 'notarealmodule'
3 |
4 | export default function Preflight ({ req }) {
5 | nope(req)
6 | return {
7 | pageTitle: getPageTitle(req.path),
8 | account: {
9 | username: 'bobsyouruncle',
10 | id: '23jk24h24'
11 | }
12 | }
13 | }
14 |
15 | function getPageTitle (path) {
16 | const titleMap = {
17 | '/': 'Home',
18 | '/about': 'About',
19 | '/account': 'My Account'
20 | }
21 |
22 | return titleMap[path] || 'My App Name'
23 | }
24 |
--------------------------------------------------------------------------------
/test/mock-preflight/app/elements/my-header.mjs:
--------------------------------------------------------------------------------
1 | export default function MyHeader ({ html, state }) {
2 | const { store = {} } = state
3 | const { title = 'huh?' } = store
4 | return html`
5 |
8 | `
9 | }
10 |
--------------------------------------------------------------------------------
/test/mock-preflight/app/elements/state-logger.mjs:
--------------------------------------------------------------------------------
1 | export default function StateLogger ({ html, state }) {
2 | const { store = {} } = state
3 | return html`
4 |
5 | ${JSON.stringify(store, null, 2)}
6 |
7 | `
8 | }
9 |
--------------------------------------------------------------------------------
/test/mock-preflight/app/head.mjs:
--------------------------------------------------------------------------------
1 | export default function Head (state) {
2 | const { store = {} } = state
3 | const { pageTitle = 'Enhance Starter Project' } = store
4 | return `
5 |
6 |
7 |
8 |
9 |
10 | ${pageTitle}
11 |
12 |
13 |
14 | `
15 | }
16 |
--------------------------------------------------------------------------------
/test/mock-preflight/app/pages/about.html:
--------------------------------------------------------------------------------
1 | ABOUT
2 |
3 |
--------------------------------------------------------------------------------
/test/mock-preflight/app/pages/index.html:
--------------------------------------------------------------------------------
1 | Home
2 |
3 |
--------------------------------------------------------------------------------
/test/mock-preflight/app/preflight.mjs:
--------------------------------------------------------------------------------
1 | export default function Preflight ({ req }) {
2 | return {
3 | pageTitle: getPageTitle(req.path),
4 | account: {
5 | username: 'bobsyouruncle',
6 | id: '23jk24h24'
7 | }
8 | }
9 | }
10 |
11 | function getPageTitle (path) {
12 | const titleMap = {
13 | '/': 'Home',
14 | '/about': 'About',
15 | '/account': 'My Account'
16 | }
17 |
18 | return titleMap[path] || 'My App Name'
19 | }
20 |
--------------------------------------------------------------------------------
/test/router-alternate-path.mjs:
--------------------------------------------------------------------------------
1 | import test from 'tape'
2 | import url from 'url'
3 | import path from 'path'
4 | import router from '../src/http/any-catchall/router.mjs'
5 |
6 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
7 |
8 | test('router with result in both', async t => {
9 | t.plan(1)
10 | let req = {
11 | rawPath: '/about',
12 | method: 'GET',
13 | headers: {
14 | 'accept': 'text/html'
15 | }
16 | }
17 | let basePath = path.join(__dirname, '..', 'app')
18 | let altPath = path.join(__dirname, 'mock-apps', 'app')
19 | let result = await router.bind({}, { basePath, altPath })(req)
20 | t.ok(result.html.includes('fred'), 'got the right page')
21 | })
22 |
23 | test('router with result in both reversed', async t => {
24 | t.plan(1)
25 | let req = {
26 | rawPath: '/about',
27 | method: 'GET',
28 | headers: {
29 | 'accept': 'text/html'
30 | }
31 | }
32 | let basePath = path.join(__dirname, '..', 'app')
33 | let altPath = path.join(__dirname, 'mock-apps', 'app')
34 | let result = await router.bind({}, { basePath: altPath, altPath: basePath })(req)
35 | t.ok(result.html.includes('backup'), 'got the right page')
36 | })
37 |
38 | test('router with result in alternative', async t => {
39 | t.plan(1)
40 | let req = {
41 | rawPath: '/backup-data',
42 | method: 'GET',
43 | headers: {
44 | 'accept': 'text/html'
45 | }
46 | }
47 | let basePath = path.join(__dirname, '..', 'app')
48 | let altPath = path.join(__dirname, 'mock-apps', 'app')
49 | let result = await router.bind({}, { basePath, altPath })(req)
50 | t.ok(result.json.data.includes('fred'), 'got the right page')
51 | })
52 |
53 | test('router with result in both reversed', async t => {
54 | t.plan(1)
55 | let req = {
56 | rawPath: '/about',
57 | method: 'GET',
58 | headers: {
59 | 'accept': 'text/html'
60 | }
61 | }
62 | let basePath = path.join(__dirname, '..', 'app')
63 | let altPath = path.join(__dirname, 'mock-apps', 'app')
64 | let result = await router.bind({}, { basePath: altPath, altPath: basePath })(req)
65 | console.log(result)
66 | t.ok(result.html.includes('backup'), 'got the right page')
67 | })
68 |
69 | test('router with result in alternative', async t => {
70 | t.plan(1)
71 | let req = {
72 | rawPath: '/backup-data',
73 | method: 'GET',
74 | headers: {
75 | 'accept': 'text/html'
76 | }
77 | }
78 | let basePath = path.join(__dirname, '..', 'app')
79 | let altPath = path.join(__dirname, 'mock-apps', 'app')
80 | let result = await router({ basePath, altPath }, req)
81 | t.deepEqual(result.json.data, [ 'fred', 'joe', 'mary' ], 'got result')
82 | })
83 |
84 | test('router with no pages dir', async t => {
85 | t.plan(1)
86 | let req = {
87 | rawPath: '/',
88 | method: 'GET',
89 | headers: {
90 | 'accept': 'text/html'
91 | }
92 | }
93 | let primaryApp = path.join(__dirname, '..', 'app')
94 | let secondaryApp = path.join(__dirname, 'mock-folders', 'app')
95 | let result = await router({ basePath: primaryApp, altPath: secondaryApp }, req)
96 | t.ok(result, 'got result')
97 | })
98 |
99 | test('router with result in both reversed', async t => {
100 | t.plan(1)
101 | let req = {
102 | rawPath: '/about',
103 | method: 'GET',
104 | headers: {
105 | 'accept': 'text/html'
106 | }
107 | }
108 | let basePath = path.join(__dirname, '..', 'app')
109 | let altPath = path.join(__dirname, 'mock-apps', 'app')
110 | let result = await router.bind({}, { basePath: altPath, altPath: basePath })(req)
111 | console.log(result)
112 | t.ok(result.html.includes('backup'), 'got the right page')
113 | })
114 |
115 | test('router with result in alternative', async t => {
116 | t.plan(1)
117 | let req = {
118 | rawPath: '/backup-data',
119 | method: 'GET',
120 | headers: {
121 | 'accept': 'text/html'
122 | }
123 | }
124 | let basePath = path.join(__dirname, '..', 'app')
125 | let altPath = path.join(__dirname, 'mock-apps', 'app')
126 | let result = await router({ basePath, altPath }, req)
127 | t.deepEqual(result.json.data, [ 'fred', 'joe', 'mary' ], 'got result')
128 | })
129 |
130 | test('router with no pages dir', async t => {
131 | t.plan(1)
132 | let req = {
133 | rawPath: '/',
134 | method: 'GET',
135 | headers: {
136 | 'accept': 'text/html'
137 | }
138 | }
139 | let primaryApp = path.join(__dirname, '..', 'app')
140 | let secondaryApp = path.join(__dirname, 'mock-folders', 'app')
141 | let result = await router({ basePath: primaryApp, altPath: secondaryApp }, req)
142 | t.ok(result, 'got result')
143 | })
144 |
145 | test('Matches in both with higher weight in alternate path', async t => {
146 | t.plan(1)
147 | let req = {
148 | rawPath: '/one/two/three-alt',
149 | method: 'GET',
150 | headers: {
151 | 'accept': 'text/html'
152 | }
153 | }
154 | let basePath = path.join(__dirname, '..', 'app')
155 | let altPath = path.join(__dirname, 'mock-apps', 'app')
156 | let result = await router.bind({}, { basePath: altPath, altPath: basePath })(req)
157 | console.log(result)
158 | t.ok(result?.json?.where?.includes('three-alt'), 'got the right page')
159 | })
160 |
--------------------------------------------------------------------------------
/test/router-async-middleware-state.mjs:
--------------------------------------------------------------------------------
1 | import test from 'tape'
2 | import url from 'url'
3 | import path from 'path'
4 | import router from '../src/http/any-catchall/router.mjs'
5 |
6 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
7 |
8 | test('middleware parsed json state happens', async t => {
9 | t.plan(2)
10 | let req = {
11 | rawPath: '/test-store',
12 | method: 'GET',
13 | headers: {
14 | accept: 'application/json'
15 | }
16 | }
17 | process.env.ARC_SESSION_TABLE_NAME = 'jwe' // if we do not do this we need to setup dynamo!
18 | let basePath = path.join(__dirname, 'mock-async-middleware', 'app')
19 | let res = await router.bind({}, { basePath })(req)
20 | let stuff = JSON.parse(res.body).stuff
21 | t.ok(Array.isArray(stuff), 'stuff')
22 | t.ok(stuff[0] === 1, 'stuff[0] is 1')
23 | console.log(stuff)
24 | })
25 |
26 | test('middleware parsed json state passes thru to html render', async t => {
27 | t.plan(4)
28 | let req = {
29 | rawPath: '/test-store',
30 | method: 'GET',
31 | headers: {
32 | 'accept': 'text/html'
33 | }
34 | }
35 | process.env.ARC_SESSION_TABLE_NAME = 'jwe' // if we do not do this we need to setup dynamo!
36 | let basePath = path.join(__dirname, 'mock-async-middleware', 'app')
37 | let res = await router.bind({}, { basePath })(req)
38 | t.ok(res.html.includes(''), 'res html includes ')
39 | t.ok(res.headers['set-cookie'], 'set-cookie')
40 | let r = new RegExp(']*>(.*?) ')
41 | let s = res.html.replace(/\n/g, '').match(r)[1]
42 | let j = JSON.parse(s)
43 | console.log(j)
44 | t.ok(Array.isArray(j.store.stuff), 'stuff')
45 | t.ok(j.store.stuff[0] === 1, 'stuff[0] is 1')
46 | })
47 |
--------------------------------------------------------------------------------
/test/router-async-middleware.mjs:
--------------------------------------------------------------------------------
1 | import test from 'tape'
2 | import url from 'url'
3 | import path from 'path'
4 | import router from '../src/http/any-catchall/router.mjs'
5 |
6 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
7 |
8 | test('router middleware passes values', async t => {
9 | t.plan(2)
10 | let req = {
11 | rawPath: '/',
12 | method: 'GET',
13 | headers: {
14 | 'accept': 'application/json',
15 | },
16 | }
17 | process.env.ARC_SESSION_TABLE_NAME = 'jwe' // if we do not do this we need to setup dynamo!
18 | let basePath = path.join(__dirname, 'mock-async-middleware', 'app')
19 | let res = await router.bind({}, { basePath })(req)
20 | t.ok(Array.isArray(JSON.parse(res.body).stuff), 'got the right stuff')
21 | t.ok(res.headers['set-cookie'], 'got a set-cookie header')
22 | console.log(res)
23 | })
24 |
25 | test('router middleware respects redirect property', async t => {
26 | t.plan(2)
27 | let req = {
28 | rawPath: '/test-redirect',
29 | method: 'GET',
30 | headers: {
31 | 'accept': 'text/html',
32 | },
33 | }
34 | process.env.ARC_SESSION_TABLE_NAME = 'jwe' // if we do not do this we need to setup dynamo!
35 | let basePath = path.join(__dirname, 'mock-async-middleware', 'app')
36 | let res = await router.bind({}, { basePath })(req)
37 | t.equals(res.headers['location'] || res.headers['Location'], '/login', 'req location matches')
38 | t.equals(res.statusCode, 302, 'proper redirect status')
39 | console.log(res)
40 | })
41 |
42 | test('router middleware respects redirect headers', async t => {
43 | t.plan(2)
44 | let req = {
45 | rawPath: '/test-redirect-header',
46 | method: 'GET',
47 | headers: {
48 | 'accept': 'text/html',
49 | },
50 | }
51 | process.env.ARC_SESSION_TABLE_NAME = 'jwe' // if we do not do this we need to setup dynamo!
52 | let basePath = path.join(__dirname, 'mock-async-middleware', 'app')
53 | let res = await router.bind({}, { basePath })(req)
54 | t.equals(res.headers['location'] || res.headers['Location'], '/login', 'req location matches')
55 | t.equals(res.statusCode, 302, 'proper redirect status')
56 | console.log(res)
57 | })
58 |
--------------------------------------------------------------------------------
/test/router-dots.mjs:
--------------------------------------------------------------------------------
1 | import test from 'tape'
2 | import url from 'url'
3 | import path from 'path'
4 | import router from '../src/http/any-catchall/router.mjs'
5 |
6 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
7 |
8 | test('router dots', async t => {
9 | t.plan(1)
10 | process.env.ARC_SESSION_TABLE_NAME = 'jwe'
11 | let basePath = path.join(__dirname, 'mock-dots', 'app')
12 | let res = await router.bind({}, { basePath })({
13 | rawPath: '/.well-known/webfinger',
14 | method: 'GET',
15 | headers: {
16 | 'accept': 'application/json',
17 | }
18 | })
19 | t.ok(res.json.hello, 'hiii')
20 | console.log(res)
21 | })
22 |
--------------------------------------------------------------------------------
/test/router-error-codes.mjs:
--------------------------------------------------------------------------------
1 | import test from 'tape'
2 | import url from 'url'
3 | import path from 'path'
4 | import router from '../src/http/any-catchall/router.mjs'
5 |
6 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
7 |
8 | test('test default 404 page', async t => {
9 | t.plan(2)
10 | let req = {
11 | rawPath: '/nope',
12 | method: 'GET',
13 | headers: {
14 | 'accept': 'text/html'
15 | }
16 | }
17 | let base = path.join(__dirname, 'mock-folders', 'app')
18 | let result = await router({ basePath: base }, req)
19 | t.ok(result, 'got result')
20 | t.equal(result.status, 404, 'Default 404 page')
21 | console.log(result)
22 | })
23 |
24 | test('overridden 404 page', async t => {
25 | t.plan(2)
26 | let req = {
27 | rawPath: '/nope',
28 | method: 'GET',
29 | headers: {
30 | 'accept': 'text/html'
31 | }
32 | }
33 | let base = path.join(__dirname, 'mock-errors', 'app')
34 | let result = await router({ basePath: base }, req)
35 | t.ok(result.html.includes('Custom 404'), 'got result')
36 | t.equal(result.status, 404, 'Overridden 404 page')
37 | console.log(result)
38 | })
39 |
--------------------------------------------------------------------------------
/test/router-html-element.mjs:
--------------------------------------------------------------------------------
1 | import test from 'tape'
2 | import url from 'url'
3 | import path from 'path'
4 | import router from '../src/http/any-catchall/router.mjs'
5 |
6 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
7 |
8 | test('router page with html element', async t => {
9 | t.plan(1)
10 | let req = {
11 | rawPath: '/test-element',
12 | method: 'GET',
13 | headers: {
14 | 'accept': 'text/html',
15 | },
16 | }
17 | let basePath = path.join(__dirname, 'mock-html-elements', 'app')
18 | let res = await router.bind({}, { basePath })(req)
19 | t.ok(res.html.includes('This is a HTML only element.'), 'rendered html only elements')
20 | })
21 |
--------------------------------------------------------------------------------
/test/router.mjs:
--------------------------------------------------------------------------------
1 | import test from 'tape'
2 | import url from 'url'
3 | import path from 'path'
4 | import router from '../src/http/any-catchall/router.mjs'
5 |
6 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
7 |
8 | test('router', async t => {
9 | t.plan(1)
10 | let req = {
11 | rawPath: '/',
12 | method: 'GET',
13 | headers: {
14 | 'accept': 'text/html'
15 | }
16 | }
17 | let base = path.join(__dirname, '..', 'app')
18 | let result = await router({ basePath: base }, req)
19 | t.ok(result, 'got result')
20 | console.log(result)
21 | })
22 |
23 | test('router with no pages folder', async t => {
24 | t.plan(1)
25 | let req = {
26 | rawPath: '/',
27 | method: 'GET',
28 | headers: {
29 | 'accept': 'text/html'
30 | }
31 | }
32 | let base = path.join(__dirname, 'mock-folders', 'app')
33 | let result = await router({ basePath: base }, req)
34 | t.ok(result, 'got result')
35 | })
36 |
37 | test('headers pass thru application/json', async t => {
38 | t.plan(1)
39 | let req = {
40 | rawPath: '/',
41 | method: 'get',
42 | headers: {
43 | 'accept': 'application/json'
44 | }
45 | }
46 | let base = path.join(__dirname, 'mock-headers', 'app')
47 | let result = await router({ basePath: base }, req)
48 | t.ok(result.headers['x-custom-header'] === 'custom-header-value', 'x-custom-header: custom-header-value')
49 | })
50 |
51 | test('headers pass thru application/json middleware', async t => {
52 | t.plan(1)
53 | let req = {
54 | rawPath: '/',
55 | method: 'post',
56 | headers: {
57 | 'accept': 'application/json'
58 | }
59 | }
60 | let base = path.join(__dirname, 'mock-headers', 'app')
61 | let result = await router({ basePath: base }, req)
62 | t.ok(result.headers['x-custom-header'] === 'custom-header-value', 'x-custom-header: custom-header-value')
63 | })
64 |
65 | test('headers pass thru text/html', async t => {
66 | t.plan(1)
67 | let req = {
68 | rawPath: '/',
69 | method: 'get',
70 | headers: {
71 | 'accept': 'text/html'
72 | }
73 | }
74 | let base = path.join(__dirname, 'mock-headers', 'app')
75 | let result = await router({ basePath: base }, req)
76 | t.ok(result.headers['x-custom-header'] === 'custom-header-value', 'x-custom-header: custom-header-value')
77 | console.log(result)
78 | })
79 |
80 | test('headers pass thru text/html middleware', async t => {
81 | t.plan(1)
82 | let req = {
83 | rawPath: '/',
84 | method: 'post',
85 | headers: {
86 | 'accept': 'text/html'
87 | }
88 | }
89 | let base = path.join(__dirname, 'mock-headers', 'app')
90 | let result = await router({ basePath: base }, req)
91 | t.ok(result.headers['x-custom-header'] === 'custom-header-value', 'x-custom-header: custom-header-value')
92 | })
93 |
--------------------------------------------------------------------------------
/test/verbs.mjs:
--------------------------------------------------------------------------------
1 | import test from 'tape'
2 | import url from 'url'
3 | import path from 'path'
4 | import sandbox from '@architect/sandbox'
5 | import tiny from 'tiny-json-http'
6 |
7 | const baseUrl = 'http://localhost:3333'
8 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
9 |
10 | test(`Start local server`, async t => {
11 | await sandbox.start({ quiet: true, cwd: path.join(__dirname, '..') })
12 | t.pass('local server started')
13 | t.end()
14 | })
15 |
16 | test('GET request', async t => {
17 | const response = await tiny.get({ headers: { 'accept': 'application/json' }, url: baseUrl + '/verbs' })
18 | const expected = `{"verb":"get"}`
19 | t.ok(JSON.stringify(response.body) === expected, 'GET response')
20 | t.end()
21 | })
22 |
23 | test('HEAD request', async t => {
24 | const response = await tiny.head({ headers: { 'accept': 'application/json' }, url: baseUrl + '/verbs' })
25 | const expected = `application/json; charset=utf8`
26 | t.ok(response.headers['content-type'] === expected, 'HEAD response')
27 | t.end()
28 | })
29 |
30 | test('OPTIONS request', async t => {
31 | const response = await tiny.options({ headers: { 'accept': 'application/json' }, url: baseUrl + '/verbs' })
32 | const expected = `OPTIONS, GET, HEAD, POST`
33 | t.ok(response.headers['allow'] === expected, 'OPTIONS response')
34 | t.end()
35 | })
36 |
37 | test('POST request', async t => {
38 | const response = await tiny.post({ headers: { 'accept': 'application/json' }, url: baseUrl + '/verbs', data: {} })
39 | const expected = `{"verb":"post"}`
40 | t.ok(JSON.stringify(response.body) === expected, 'POST response')
41 | t.end()
42 | })
43 |
44 | test('PUT request', async t => {
45 | const response = await tiny.put({ headers: { 'accept': 'application/json' }, url: baseUrl + '/verbs', data: {} })
46 | const expected = `{"verb":"put"}`
47 | t.ok(JSON.stringify(response.body) === expected, 'PUT response')
48 | t.end()
49 | })
50 |
51 | test('PATCH request', async t => {
52 | const response = await tiny.patch({ headers: { 'accept': 'application/json' }, url: baseUrl + '/verbs', data: {} })
53 | const expected = `e0023aa4f`
54 | t.ok(response.headers['etag'] === expected, 'PATCH response')
55 | t.end()
56 | })
57 |
58 | test('DELETE request', async t => {
59 | const response = await tiny.del({ headers: { 'accept': 'application/json' }, url: baseUrl + '/verbs', data: {} })
60 | const expected = `{"verb":"delete"}`
61 | t.ok(JSON.stringify(response.body) === expected, 'DELETE response')
62 | t.end()
63 | })
64 |
65 | test('Shut down local server', async t => {
66 | await sandbox.end()
67 | t.pass('Shut down Sandbox')
68 | t.end()
69 | }) === 'one'
70 |
--------------------------------------------------------------------------------