Skip to content
Merged

Next #297

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nullstack",
"version": "0.17.0",
"version": "0.17.1",
"description": "Full-stack Javascript Components for one-dev armies",
"main": "nullstack.js",
"author": "Mortaro",
Expand Down
4 changes: 3 additions & 1 deletion server/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ function renderHead(scope) {
scope.head += '/>'
} else {
scope.head += '>'
scope.head += node.attributes.html
if (node.attributes.html) {
scope.head += node.attributes.html
}
scope.head += `</${node.type}>`
}
}
Expand Down
5 changes: 2 additions & 3 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ for (const method of ['get', 'post', 'put', 'patch', 'delete', 'all']) {
const original = server[method].bind(server)
server[method] = function (...args) {
if (typeof args[1] === 'function' && args[1].name === '_invoke') {
original(args[0], bodyParser.text({ limit: server.maximumPayloadSize }), async (request, response) => {
return original(args[0], bodyParser.text({ limit: server.maximumPayloadSize }), async (request, response) => {
const params = {}
for (const key of Object.keys(request.params)) {
params[key] = extractParamValue(request.params[key])
Expand All @@ -59,9 +59,8 @@ for (const method of ['get', 'post', 'put', 'patch', 'delete', 'all']) {
response.status(500).json({})
}
})
} else {
original(...args)
}
return original(...args)
}
}

Expand Down
7 changes: 6 additions & 1 deletion server/template.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,16 @@ export default function ({ head, body, nextBody, context, instances }) {
serializableInstances[key] = value
}
}
const serializedWorker = {
...worker,
staleWhileRevalidate: worker.staleWhileRevalidate.map((matcher) => matcher.toString()),
cacheFirst: worker.cacheFirst.map((matcher) => matcher.toString()),
}
const state = {
page,
environment,
settings,
worker,
worker: serializedWorker,
params,
project,
instances: environment.mode === 'spa' ? {} : serializableInstances,
Expand Down
13 changes: 12 additions & 1 deletion server/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import cacheFirst from '../workers/cacheFirst.js?raw'
import dynamicFetch from '../workers/dynamicFetch.js?raw'
import dynamicInstall from '../workers/dynamicInstall.js?raw'
import load from '../workers/load.js?raw'
import match from '../workers/match.js?raw'
import networkDataFirst from '../workers/networkDataFirst.js?raw'
import networkFirst from '../workers/networkFirst.js?raw'
import staleWhileRevalidate from '../workers/staleWhileRevalidate.js?raw'
Expand All @@ -23,6 +24,8 @@ const worker = {}
worker.enabled = environment.production
worker.fetching = false
worker.preload = []
worker.staleWhileRevalidate = []
worker.cacheFirst = []
worker.headers = {}
worker.api = process.env.NULLSTACK_WORKER_API ?? ''
worker.cdn = process.env.NULLSTACK_WORKER_CDN ?? ''
Expand All @@ -38,6 +41,13 @@ const queuesProxyHandler = {

worker.queues = new Proxy({}, queuesProxyHandler)

function replacer(key, value) {
if (value instanceof RegExp) {
return JSON.stringify({ flags: value.flags, source: value.source })
}
return value
}

export function generateServiceWorker() {
if (files['service-worker.js']) return files['service-worker.js']
const sources = []
Expand All @@ -51,8 +61,9 @@ export function generateServiceWorker() {
const scripts = readdirSync(bundleFolder)
.filter((filename) => filename.includes('.client.'))
.map((filename) => `'/${filename}'`)
sources.push(`self.context = ${JSON.stringify(context, null, 2)};`)
sources.push(`self.context = ${JSON.stringify(context, replacer, 2)};`)
sources.push(load)
sources.push(match)
if (environment.mode === 'ssg') {
sources.push(staticHelpers)
sources.push(cacheFirst)
Expand Down
12 changes: 11 additions & 1 deletion tests/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,18 @@ context.server.use(
}),
)

context.worker.staleWhileRevalidate = [/[0-9]/]
context.worker.cacheFirst = [/[0-9]/]

context.server
.get('/data/get/:param', ExposedServerFunctions.getData)
.get('/chainable-server-function', (request, response) => {
response.json({ chainable: true })
})
.get('/chainable-regular-function', (request, response) => {
response.json({ chainable: true })
})
context.server.get('/data/all/:param', ExposedServerFunctions.getData)
context.server.get('/data/get/:param', ExposedServerFunctions.getData)
context.server.post('/data/post/:param', ExposedServerFunctions.getData)
context.server.put('/data/put/:param', ExposedServerFunctions.getData)
context.server.patch('/data/patch/:param', ExposedServerFunctions.getData)
Expand Down
6 changes: 6 additions & 0 deletions tests/src/DynamicHead.njs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable nullstack/self-closing-comp */
import Nullstack from 'nullstack'

class DynamicHead extends Nullstack {
Expand All @@ -10,6 +11,10 @@ class DynamicHead extends Nullstack {
return <style html={innerComponent} data-inner-component />
}

checkScriptChildren({ element }) {
this.scriptIsEmpty = !element.innerText
}

render() {
const color = this.count % 2 === 0 ? 'red' : 'blue'
const redBlue = `[data-red-blue] { color: ${color}}`
Expand All @@ -21,6 +26,7 @@ class DynamicHead extends Nullstack {
return (
<div>
<head>
<script data-script-is-empty={this.scriptIsEmpty} ref={this.checkScriptChildren}></script>
<style html={redBlue} data-count={this.count} data-red-blue />
{this.count === 0 && <style html={prerenderConditional} data-prerender-conditional />}
{this.count === 1 && <style html={rerenderConditional} data-rerender-conditional />}
Expand Down
6 changes: 6 additions & 0 deletions tests/src/DynamicHead.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,10 @@ describe('DynamicHead', () => {
const element = await page.$('style[data-ternary-head-children]')
expect(element).toBeTruthy()
})

test('empty script tags in head should have no children', async () => {
await page.waitForSelector('[data-script-is-empty]')
const element = await page.$('[data-script-is-empty]')
expect(element).toBeTruthy()
})
})
10 changes: 10 additions & 0 deletions tests/src/ExposedServerFunctions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ async function api(method) {
return data.status
}

async function chainable(type) {
const response = await fetch(`/chainable-${type}-function`)
const data = await response.json()
return data.chainable
}

class ExposedServerFunctions extends Nullstack {

static async getData({ request, project, param, query, truthy, falsy, number, date, string }) {
Expand All @@ -30,6 +36,8 @@ class ExposedServerFunctions extends Nullstack {
}

async hydrate() {
this.chainableServerFunction = await chainable('server')
this.chainableRegularFunction = await chainable('regular')
this.all = await api('get')
this.get = await api('get')
this.post = await api('post')
Expand All @@ -47,6 +55,8 @@ class ExposedServerFunctions extends Nullstack {
data-patch={this.patch}
data-delete={this.delete}
data-all={this.all}
data-chainable-server-function={this.chainableServerFunction}
data-chainable-regular-function={this.chainableRegularFunction}
/>
)
}
Expand Down
12 changes: 12 additions & 0 deletions tests/src/ExposedServerFunctions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ beforeAll(async () => {
})

describe('ExposedServerFunctions', () => {
test('routes are chainable when receiving a server function', async () => {
await page.waitForSelector('[data-chainable-server-function]')
const element = await page.$('[data-chainable-server-function]')
expect(element).toBeTruthy()
})

test('routes are chainable when receiving a regular function', async () => {
await page.waitForSelector('[data-chainable-regular-function]')
const element = await page.$('[data-chainable-regular-function]')
expect(element).toBeTruthy()
})

test('server functions can be exposed to GET and serialize params and query', async () => {
await page.waitForSelector('[data-get]')
const element = await page.$('[data-get]')
Expand Down
6 changes: 6 additions & 0 deletions types/Worker.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,10 @@ export type NullstackWorker = {
* @see https://nullstack.app/service-worker#loading-screens
*/
loading: boolean

headers: Record<string, string>

cacheFirst: RegExp[]

staleWhileRevalidate: RegExp[]
}
10 changes: 10 additions & 0 deletions workers/dynamicFetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ function dynamicStrategy(event) {
event.waitUntil(
(async function () {
const url = new URL(event.request.url)
for (const matcher of self.context.worker.staleWhileRevalidate) {
if (match(matcher, url)) {
return event.respondWith(staleWhileRevalidate(event))
}
}
for (const matcher of self.context.worker.cacheFirst) {
if (match(matcher, url)) {
return event.respondWith(cacheFirst(event))
}
}
if (url.origin !== location.origin) return
if (event.request.method !== 'GET') return
if (url.pathname.indexOf('/nullstack/') > -1) {
Expand Down
4 changes: 4 additions & 0 deletions workers/match.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
function match(serializedMatcher, url) {
const matcher = JSON.parse(serializedMatcher);
return new RegExp(matcher.source, matcher.flags).test(url.href)
}
10 changes: 10 additions & 0 deletions workers/staticFetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ function staticStrategy(event) {
event.waitUntil(
(async function () {
const url = new URL(event.request.url)
for (const matcher of self.context.worker.staleWhileRevalidate) {
if (match(matcher, url)) {
return event.respondWith(staleWhileRevalidate(event))
}
}
for (const matcher of self.context.worker.cacheFirst) {
if (match(matcher, url)) {
return event.respondWith(cacheFirst(event))
}
}
if (url.origin !== location.origin) return
if (event.request.method !== 'GET') return
if (url.pathname.indexOf('/nullstack/') > -1) {
Expand Down