saloe 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +19 -0
  2. package/demos/cloudflare-worker-actions/.wrangler/state/v3/cache/miniflare-CacheObject/9f458c07675338a7426a7b81ac4fb1baf92d034efbcaaf4336379640ed744ded.sqlite +0 -0
  3. package/demos/cloudflare-worker-actions/package.json +29 -0
  4. package/demos/cloudflare-worker-actions/public/input.js +5 -0
  5. package/demos/cloudflare-worker-actions/public/load.js +5 -0
  6. package/demos/cloudflare-worker-actions/public/submit.js +6 -0
  7. package/demos/cloudflare-worker-actions/server.js +93 -0
  8. package/demos/cloudflare-worker-actions/vite.config.js +25 -0
  9. package/demos/cloudflare-worker-actions/wrangler.toml +35 -0
  10. package/demos/cloudflare-worker-server/package.json +29 -0
  11. package/demos/cloudflare-worker-server/server.js +75 -0
  12. package/demos/cloudflare-worker-server/vite.config.js +25 -0
  13. package/demos/cloudflare-worker-server/wrangler.toml +32 -0
  14. package/dist/actions.cjs.js +280 -0
  15. package/dist/actions.es.js +280 -0
  16. package/dist/cloudflare-kv.cjs.js +29 -0
  17. package/dist/cloudflare-kv.es.js +29 -0
  18. package/dist/cloudflare-worker.cjs.js +19 -0
  19. package/dist/cloudflare-worker.es.js +19 -0
  20. package/dist/cookie.cjs.js +18 -0
  21. package/dist/cookie.es.js +18 -0
  22. package/dist/html.cjs.js +78 -0
  23. package/dist/html.es.js +78 -0
  24. package/dist/router.cjs.js +51 -0
  25. package/dist/router.es.js +51 -0
  26. package/dist/urlpattern.cjs.js +23 -0
  27. package/dist/urlpattern.es.js +6 -0
  28. package/dist/util.cjs.js +36 -0
  29. package/dist/util.es.js +36 -0
  30. package/dist/vite.cjs.js +102 -0
  31. package/dist/vite.es.js +101 -0
  32. package/dist/worker.cjs.js +43 -0
  33. package/dist/worker.es.js +43 -0
  34. package/package.json +63 -0
  35. package/src/actions.js +284 -0
  36. package/src/cloudflare-kv.js +36 -0
  37. package/src/cloudflare-worker.js +23 -0
  38. package/src/cookie.js +22 -0
  39. package/src/html.js +99 -0
  40. package/src/router.js +61 -0
  41. package/src/urlpattern.js +11 -0
  42. package/src/util.js +44 -0
  43. package/src/vite.js +127 -0
  44. package/src/worker.js +46 -0
  45. package/vite.config.js +36 -0
package/src/actions.js ADDED
@@ -0,0 +1,284 @@
1
+ import { html } from './html'
2
+
3
+ const LISTENER_SCRIPT = html`
4
+ <script defer>
5
+ (() => {
6
+ const EVENTS_PREVENT_DEFAULT_MANDATORY = [
7
+ 'submit'
8
+ ]
9
+
10
+ const EVENTS_FIRE_DOCUMENT_BODY_LISTENERS = [
11
+ 'mouseover',
12
+ 'click',
13
+ ]
14
+
15
+ const EVENTS = [
16
+ 'submit',
17
+ 'input',
18
+ 'blur',
19
+ 'change',
20
+ 'focus',
21
+ 'invalid',
22
+ ]
23
+
24
+
25
+ const addListener = ({ srcElement, event, listeners }) => {
26
+ srcElement?.addEventListener(event, (e) => {
27
+ executeListeners({ e, srcElement, listeners })
28
+ })
29
+ }
30
+
31
+ const executeListeners = ({ e, srcElement, listeners }) => {
32
+ listeners?.forEach((listener) => {
33
+ if (listener) listener({ e, srcElement })
34
+ })
35
+ }
36
+
37
+ const getListenerFromScript = ({ script, event }) => {
38
+ if (!script) return null
39
+ if (script[event]) return script[event]
40
+ const prev = Object.keys(script)?.find((key) => script[key][event])
41
+ if (!prev) return null
42
+ return script[prev][event]
43
+ }
44
+
45
+ const fetchListeners = async ({ srcElement, event, e }) => {
46
+ if (!srcElement?.getAttribute) return
47
+
48
+ const scriptNames = srcElement?.getAttribute('on-' + event)
49
+ if (!scriptNames) return
50
+
51
+ if (scriptNames && EVENTS_PREVENT_DEFAULT_MANDATORY.includes(event)) e.preventDefault()
52
+
53
+ const scripts = await Promise.all(
54
+ scriptNames?.split(',')?.map((scriptName) => {
55
+ const scriptToImport = '/' + scriptName?.trim() + '.js'
56
+ return import(scriptToImport)?.catch((err) => { })
57
+ })
58
+ )
59
+
60
+ const listeners = scripts?.map((script) => getListenerFromScript({ script, event }))
61
+
62
+ return listeners
63
+
64
+ // if (['load', 'click', 'submit', 'input', 'change'].includes(event)) executeListeners({ e, srcElement, listeners })
65
+ // if (['focus', 'blur', 'invalid', 'click', 'submit', 'input', 'change'].includes(event)) addListener({ srcElement, event, listeners })
66
+
67
+ // srcElement?.removeAttribute('on-' + event)
68
+ }
69
+
70
+ const addScripts = () => {
71
+ const scriptsToLoad = [...document.querySelectorAll('script[data-script-to-load]')]
72
+ return Promise.all(
73
+ scriptsToLoad?.map((scriptToLoad) => {
74
+ const id = scriptToLoad?.getAttribute('data-script-to-load')
75
+ scriptToLoad.removeAttribute('data-script-to-load')
76
+
77
+ const attrs = scriptToLoad?.getAttributeNames()?.reduce((acc, attrName) => {
78
+ const attrValue = scriptToLoad.getAttribute(attrName)
79
+ if (attrValue !== 'text/script-to-load') acc[attrName] = attrValue
80
+ return acc
81
+ }, {})
82
+
83
+ const content = scriptToLoad?.textContent
84
+
85
+ scriptToLoad?.remove()
86
+
87
+ return loadScript({ id, attrs, content }).catch((err) => {
88
+ console.error(err)
89
+ })
90
+ })
91
+ )
92
+ }
93
+
94
+ const loadScript = ({ id, attrs, content }) => {
95
+ const script = document?.createElement('script')
96
+
97
+ Object.keys(attrs)?.forEach((attrKey) => script?.setAttribute(attrKey, attrs[attrKey]))
98
+ script.id = id
99
+
100
+ if (content) script?.insertAdjacentHTML('beforeend', content)
101
+
102
+ return new Promise((resolve, reject) => {
103
+ if (!attrs.src) {
104
+ resolve()
105
+ document?.body?.insertAdjacentElement('beforeend', script)
106
+ return
107
+ }
108
+
109
+ script.onload = script.onreadystatechange = function () {
110
+ if (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete') {
111
+ resolve()
112
+ script.onload = script.onreadystatechange = null
113
+ }
114
+ }
115
+
116
+ script.onerror = () => {
117
+ console.error('script failed to load')
118
+ reject(new Error('Failed to load script with src ' + script.src))
119
+ }
120
+
121
+ document?.body?.insertAdjacentElement('beforeend', script)
122
+ })
123
+ }
124
+
125
+ // load
126
+ const fireLoadListener = () => {
127
+ const event = 'load'
128
+ const srcElements = document?.querySelectorAll('[on-' + event + ']')
129
+
130
+ srcElements?.forEach(async (srcElement) => {
131
+ const listeners = await fetchListeners({ srcElement, event, e: null })
132
+ executeListeners({ e: null, srcElement, listeners })
133
+
134
+ srcElement?.removeAttribute('on-' + event)
135
+ })
136
+ }
137
+
138
+ // invalid
139
+ const fireInvalidListener = () => {
140
+ const event = 'invalid'
141
+ const srcElements = document?.querySelectorAll('[on-' + event + ']')
142
+
143
+ srcElements?.forEach(async (srcElement) => {
144
+ const listeners = await fetchListeners({ srcElement, event, e: null })
145
+ addListener({ srcElement, event, listeners })
146
+ })
147
+ }
148
+
149
+ // blur
150
+ const fireBlurListener = () => {
151
+ const event = 'blur'
152
+ const srcElements = document?.querySelectorAll('[on-' + event + ']')
153
+
154
+ srcElements?.forEach(async (srcElement) => {
155
+ const listeners = await fetchListeners({ srcElement, event, e: null })
156
+ addListener({ srcElement, event, listeners })
157
+ })
158
+ }
159
+
160
+ // focus
161
+ const fireFocusListener = () => {
162
+ const event = 'focus'
163
+ const srcElements = document?.querySelectorAll('[on-' + event + ']')
164
+
165
+ srcElements?.forEach(async (srcElement) => {
166
+ const listeners = await fetchListeners({ srcElement, event, e: null })
167
+ addListener({ srcElement, event, listeners })
168
+ })
169
+ }
170
+
171
+ // observers
172
+ const fireObserverListeners = () => {
173
+ const srcElements = [...document.querySelectorAll('[on-observe]')]
174
+
175
+ const uniqueScriptNames = [...srcElements?.reduce((acc, srcElement) => {
176
+ const attribute = srcElement?.getAttribute('on-observe')
177
+ if (attribute === 'undefined') return acc
178
+
179
+ const scriptNames = attribute?.split(',')
180
+ scriptNames?.forEach((scriptName) => acc?.set(scriptName, 1))
181
+
182
+ return acc
183
+ }, new Map())?.keys()]
184
+
185
+ uniqueScriptNames?.forEach(async (scriptName) => {
186
+ const observedSrcElements = document.querySelectorAll('[on-observe*="' + scriptName + '"]')
187
+
188
+ const script = await import('/' + scriptName?.trim() + '.js')?.catch((err) => { })
189
+ const listener = getListenerFromScript({ script, event: 'observe' })
190
+ if (!listener) return
191
+
192
+ const observer = new IntersectionObserver((entries) => {
193
+ entries.forEach((entry) => listener({ entry, observer }))
194
+ })
195
+
196
+ observedSrcElements?.forEach((observerSrcElement) => {
197
+ observer.observe(observerSrcElement)
198
+
199
+ const observerAttr = observerSrcElement?.getAttribute('on-observe')
200
+ const updatedObserverAttr = observerAttr?.replaceAll(scriptName + ', ', '')?.replaceAll(', ' + scriptName, '')?.replaceAll(scriptName, '')
201
+
202
+ if (updatedObserverAttr === '') observerSrcElement.removeAttribute('on-observe')
203
+ else observerSrcElement.setAttribute('on-observe', updatedObserverAttr)
204
+ })
205
+ })
206
+ }
207
+
208
+ const getSrcElement = ({ srcElement, event }) => {
209
+ if (!srcElement?.hasAttribute) return srcElement
210
+ const attribute = 'on-' + event
211
+ const hasScriptName = srcElement?.hasAttribute(attribute)
212
+ if (hasScriptName) return srcElement
213
+
214
+ const query = ':is(a, button, li)[' + attribute + ']'
215
+ const closestButton = srcElement?.closest(query)
216
+ if (closestButton) return closestButton
217
+
218
+ return srcElement
219
+ }
220
+
221
+ const fireListeners = () => {
222
+ EVENTS_FIRE_DOCUMENT_BODY_LISTENERS?.forEach((event) => {
223
+ document.body['on' + event] = async (e) => {
224
+ await addScripts()
225
+
226
+ fireLoadListener()
227
+ fireObserverListeners()
228
+ }
229
+ })
230
+
231
+ EVENTS?.forEach((event) => {
232
+ document.body['on' + event] = async (e) => {
233
+ const srcElement = getSrcElement({ srcElement: e?.srcElement, event })
234
+ const listeners = await fetchListeners({ srcElement, event, e })
235
+
236
+ executeListeners({ e, srcElement, listeners })
237
+ addListener({ srcElement, event, listeners })
238
+
239
+ if (srcElement?.removeAttribute) srcElement.removeAttribute('on-' + event)
240
+ }
241
+ })
242
+ }
243
+
244
+ fireListeners()
245
+
246
+ window.onload = () => {
247
+ setTimeout(() => {
248
+ document?.body?.click()
249
+ }, 2_500)
250
+ }
251
+ })()
252
+ </script>
253
+ `
254
+
255
+ const SW_REGISTER_SCRIPT = html`
256
+ <script defer>
257
+ (async () => {
258
+ if (!navigator.serviceWorker) return
259
+
260
+ navigator.serviceWorker.register('/sw.worker.js', { scope: '/', type: 'module' })
261
+
262
+ let refreshing
263
+ // check to see if there is a current active service worker
264
+ const oldSw = (await navigator.serviceWorker.getRegistration())?.active?.state
265
+ navigator.serviceWorker.addEventListener('controllerchange', async () => {
266
+ if (refreshing) return
267
+ // when the controllerchange event has fired, we get the new service worker
268
+ const newSw = (await navigator.serviceWorker.getRegistration())?.active?.state
269
+
270
+ // if there was already an old activated service worker, and a new activating service worker, do notify update
271
+ if (oldSw === 'activated' && newSw === 'activating') {
272
+ refreshing = true
273
+ // notifyUpdate()
274
+ location.reload()
275
+ }
276
+ })
277
+ })()
278
+ </script>
279
+ `
280
+
281
+ export {
282
+ LISTENER_SCRIPT,
283
+ SW_REGISTER_SCRIPT,
284
+ }
@@ -0,0 +1,36 @@
1
+ const getKV = ({ env, kv }) => env[kv]
2
+
3
+ const getKVResponse = async ({ env, kv, key }) => {
4
+ const result = await getFromKV({ env, kv, key })
5
+ if (result?.err) return result
6
+
7
+ const response = new Response(result?.data)
8
+ return { response }
9
+ }
10
+
11
+ const getFromKV = async ({ env, kv, key }) => {
12
+ try {
13
+ const data = await getKV({ env, kv }).get(key)
14
+ return { data }
15
+ } catch (err) {
16
+ return { err }
17
+ }
18
+ }
19
+
20
+ const putInKV = async ({ env, kv, key, data }) => {
21
+ try {
22
+ const result = await getKV({ env, kv }).put(key, data)
23
+ return result
24
+ } catch (err) {
25
+ return { err }
26
+ }
27
+ }
28
+
29
+ export {
30
+ getFromKV,
31
+ putInKV,
32
+
33
+ getKVResponse,
34
+
35
+ getKV,
36
+ }
@@ -0,0 +1,23 @@
1
+ import { getAssetFromKV } from '@cloudflare/kv-asset-handler'
2
+
3
+
4
+ const getStaticResponse = async ({ request, waitUntil, manifestJSON, env }) => {
5
+ try {
6
+ const ASSET_MANIFEST = JSON.parse(manifestJSON ?? {})
7
+ const response = await getAssetFromKV({
8
+ request,
9
+ waitUntil
10
+ }, {
11
+ ASSET_NAMESPACE: env.__STATIC_CONTENT,
12
+ ASSET_MANIFEST,
13
+ })
14
+
15
+ return { response }
16
+ } catch (err) {
17
+ return { err }
18
+ }
19
+ }
20
+
21
+ export {
22
+ getStaticResponse,
23
+ }
package/src/cookie.js ADDED
@@ -0,0 +1,22 @@
1
+ const setCookie = ({ key, value }) => {
2
+ return cookieStore?.set(key, value)
3
+ }
4
+
5
+ const getCookie = ({ key }) => {
6
+ return cookieStore?.get(key)
7
+ }
8
+
9
+ const getAllCookies = () => {
10
+ return cookieStore?.getAll()
11
+ }
12
+
13
+ const removeCookie = ({ key }) => {
14
+ return cookieStore?.delete(key)
15
+ }
16
+
17
+ export {
18
+ setCookie,
19
+ getCookie,
20
+ getAllCookies,
21
+ removeCookie,
22
+ }
package/src/html.js ADDED
@@ -0,0 +1,99 @@
1
+ import { stream as streamAsWorker } from './worker'
2
+ import { addRoute, removeRoute } from './router'
3
+ import { getScope, getEnv } from './util'
4
+
5
+
6
+ const html = (s, ...args) => {
7
+ return s?.map((ss, i) => `${ss}${args?.at(i) ?? ''}`)?.join('')
8
+ }
9
+
10
+ const stream = ({ head, body, scripts, env, status, args }) => {
11
+ const headers = new Headers()
12
+ headers.append('Content-Type', 'text/html;charset=UTF-8')
13
+
14
+ const callbacks = [
15
+ () => html`
16
+ <!DOCTYPE html>
17
+ <html lang="${args?.lang ?? 'en'}">
18
+ <head>
19
+ `,
20
+ head,
21
+ () => html`
22
+ </head>
23
+ <body
24
+ data-scope="${getScope({ env })}"
25
+ data-env="${getEnv({ env })}"
26
+ ${args?.isPublic ?? ''}
27
+ ${args?.isLoading ?? ''}
28
+ >
29
+ `,
30
+ body,
31
+ scripts ?? (() => ''),
32
+ () => html`
33
+ </body>
34
+ </html>
35
+ `
36
+ ]
37
+
38
+ return streamAsWorker({ callbacks, headers, status })
39
+ }
40
+
41
+ const awaitHtml = async ({ pending, success, error }) => {
42
+ const id = Math.floor(Math.random() * 1_000_000_000)
43
+
44
+ const pendingId = `pending_${id}`
45
+ const pendingRoutePathname = `/~/components/${pendingId}`
46
+
47
+ const getPage = async () => {
48
+ const headers = {
49
+ 'Content-Type': 'text/html;charset=utf-8',
50
+ 'Transfer-Encoding': 'chunked',
51
+ }
52
+
53
+ const streamResult = streamAsWorker({
54
+ callbacks: [
55
+ async () => html`
56
+ ${
57
+ await success()
58
+ .then((template) => template)
59
+ .catch((err) => {
60
+ console.error(err?.stack)
61
+ return error ? error({ id, err }) : ''
62
+ })
63
+ }
64
+ `
65
+ ],
66
+ headers,
67
+ })
68
+
69
+ removeRoute({ pathname: pendingRoutePathname })
70
+
71
+ return streamResult
72
+ }
73
+
74
+ addRoute({
75
+ pathname: pendingRoutePathname,
76
+ route: { getPage }
77
+ })
78
+
79
+ return html`
80
+ ${await pending({ id: pendingId })}
81
+ <script
82
+ data-script-to-load="await-html_script-${id}"
83
+ type="text/script-to-load"
84
+ >
85
+ (async () => {
86
+ const pendingEl = document?.querySelector('[data-await-pending-template="${pendingId}"]')
87
+ const response = await fetch('${pendingRoutePathname}')
88
+ const templateString = await response.text()
89
+ pendingEl.outerHTML = templateString
90
+ })()
91
+ </script>
92
+ `
93
+ }
94
+
95
+ export {
96
+ html,
97
+ stream,
98
+ awaitHtml,
99
+ }
package/src/router.js ADDED
@@ -0,0 +1,61 @@
1
+ import { getURLPatern } from './urlpattern'
2
+ import { fetch as fetchAsWorker } from './worker'
3
+
4
+
5
+ const router = new Map()
6
+
7
+ const getRouter = () => router
8
+ const getRoute = ({ pathname }) => router.get(pathname)
9
+ const addRoute = ({ pathname, route }) => router.set(pathname, route)
10
+ const removeRoute = ({ pathname }) => router.delete(pathname)
11
+
12
+ const findPatternFromUrl = ({ url }) => {
13
+ const patternPathname = [...new Set(getRouter()?.keys())]
14
+ .find((patternPathname) => {
15
+ const pattern = getURLPatern({ pathname: patternPathname })
16
+ return pattern.test(url.href)
17
+ })
18
+
19
+ return patternPathname ? getURLPatern({ pathname: patternPathname }) : null
20
+ }
21
+
22
+ const getRedirectResponse = ({ origin, pathname, isRedirectableCallback }) => {
23
+ if (origin !== self?.origin) return
24
+ const isRedirectable = isRedirectableCallback({ pathname })
25
+ const response = isRedirectable ? Response.redirect(pathname.slice(0, -1), 301) : null
26
+ return { response }
27
+ }
28
+
29
+ const getNotFoundResponse = async ({ request }) => {
30
+ const status = 404
31
+ const notFoundRoute = getRoute({ pathname: `/${status}` })
32
+ const response = notFoundRoute ? (await notFoundRoute({ request, status }))?.response : new Response('404', { status })
33
+ return { response }
34
+ }
35
+
36
+ const getForbiddenResponse = ({ origin, request, isForbiddenCallback }) => {
37
+ if (origin !== self?.origin) return
38
+ const isForbidden = isForbiddenCallback({ request })
39
+ if (!isForbidden) return
40
+ return { response: new Response(`${request?.url} is forbidden`, { status: 503 }) }
41
+ }
42
+
43
+ const getServerOnlyResponse = ({ origin, request, isServerOnlyCallback }) => {
44
+ if (origin !== self?.origin) return
45
+ const isServerOnly = isServerOnlyCallback({ request })
46
+ if (!isServerOnly) return
47
+ return fetchAsWorker({ request })
48
+ }
49
+
50
+ export {
51
+ getRouter,
52
+ getRoute,
53
+ addRoute,
54
+ removeRoute,
55
+
56
+ findPatternFromUrl,
57
+ getRedirectResponse,
58
+ getNotFoundResponse,
59
+ getForbiddenResponse,
60
+ getServerOnlyResponse,
61
+ }
@@ -0,0 +1,11 @@
1
+ import * as URLPatternPolyfill from 'urlpattern-polyfill'
2
+
3
+
4
+ if (self?.URLPattern) self.URLPattern = URLPatternPolyfill.URLPattern
5
+
6
+ const getURLPatern = ({ pathname }) => new self.URLPattern({ pathname })
7
+
8
+
9
+ export {
10
+ getURLPatern,
11
+ }
package/src/util.js ADDED
@@ -0,0 +1,44 @@
1
+ const Scope = {
2
+ Cloudflare: 'cloudflare-worker',
3
+ ServiceWorker: 'service-worker',
4
+ Window: 'window',
5
+ }
6
+
7
+ const Environment = {
8
+ Production: 'prod',
9
+ Development: 'dev',
10
+ Qa: 'qa',
11
+ }
12
+
13
+ const isCloudflareWorker = ({ env }) => env?.IS_CLOUDFLARE_WORKER
14
+ const isServiceWorker = ({ env }) => env?.IS_SERVICE_WORKER
15
+ const isWindow = () => typeof window === 'object'
16
+
17
+ const getScope = ({ env }) => {
18
+ if (isCloudflareWorker({ env })) return Scope.Cloudflare
19
+ if (isServiceWorker({ env })) return Scope.ServiceWorker
20
+ if (isWindow()) return Scope.Window
21
+ }
22
+
23
+ const getEnv = ({ env } = {}) => {
24
+ if (isCloudflareWorker({ env }) || isServiceWorker({ env })) return env.ENV
25
+ if (isWindow()) return document?.body?.getAttribute('data-env')
26
+ }
27
+
28
+ const isProdEnv = ({ env }) => getEnv({ env }) === Environment.Production
29
+ const isDevEnv = ({ env }) => getEnv({ env }) === Environment.Development
30
+ const isQaEnv = ({ env }) => getEnv({ env }) === Environment.Qa
31
+
32
+
33
+ export {
34
+ getScope,
35
+ getEnv,
36
+
37
+ isProdEnv,
38
+ isDevEnv,
39
+ isQaEnv,
40
+
41
+ isCloudflareWorker,
42
+ isWindow,
43
+ isServiceWorker,
44
+ }