diff --git a/lib/podlet.js b/lib/podlet.js index 04dc673a..dcf02490 100644 --- a/lib/podlet.js +++ b/lib/podlet.js @@ -935,24 +935,6 @@ export default class PodiumPodlet { await this.httpProxy.process(incoming); } - const js = incoming.js.map( - // @ts-ignore - (asset) => asset.toHeader() + '; asset-type=script', - ); - - const css = incoming.css.map( - // @ts-ignore - (asset) => asset.toHeader() + '; asset-type=style', - ); - - // Send early hints to layout client in the form of a 103 status code and a link header with js/css asset information. - // Always send this. If no assets present, send an empty string. - if (incoming.response.writeEarlyHints) { - incoming.response.writeEarlyHints({ - link: [...js, ...css], - }); - } - return incoming; } @@ -986,6 +968,18 @@ export default class PodiumPodlet { // if this is a proxy request then no further work should be done. if (incoming.proxy) return; + const js = incoming.js.map( + // @ts-ignore + (asset) => asset.toHeader() + '; asset-type=script', + ); + + const css = incoming.css.map( + // @ts-ignore + (asset) => asset.toHeader() + '; asset-type=style', + ); + + res.header('Link', [...js, ...css].join(', ')); + // set "incoming" on res.locals.podium objobj.set('locals.podium', incoming, res); @@ -993,8 +987,15 @@ export default class PodiumPodlet { res.header('podlet-version', this.version); } - res.podiumSend = (data, ...args) => - res.send(this.render(incoming, data, ...args)); + res.sendHeaders = () => { + res.write(''); + return res; + }; + + res.podiumSend = (data, ...args) => { + res.write(this.render(incoming, data, ...args)); + res.end(); + }; next(); } catch (error) { diff --git a/tests/podlet.test.js b/tests/podlet.test.js index d7e5ac7c..01c45ffd 100644 --- a/tests/podlet.test.js +++ b/tests/podlet.test.js @@ -83,24 +83,10 @@ class FakeHttpServer { } get(options = {}) { - const hintCallbacks = []; const url = new URL(`${this.address}${options.pathname}`); return { - hints(cb) { - hintCallbacks.push(cb); - }, async result() { - const { statusCode, headers, body } = await request(url, { - // Early hints - onInfo: (info) => { - if (info.statusCode === 103) { - for (const cb of hintCallbacks) { - cb(info.headers); - } - } - }, - }); - + const { statusCode, headers, body } = await request(url); return { statusCode, headers, body }; }, }; @@ -168,6 +154,7 @@ class FakeExpressServer { http.get(opts, (res) => { const chunks = []; + options?.onHeaders && options.onHeaders(res.headers); res.on('data', (chunk) => { chunks.push(chunk); }); @@ -1899,10 +1886,10 @@ tap.test( ); // ############################################# -// Asset sending using 103 Early hints +// Asset sending using Link header // ############################################# -tap.test('assets - .js() - should send 103 Early hints', async (t) => { +tap.test('assets - .js() - should send Link header', async (t) => { t.plan(1); const podlet = new Podlet({ name: 'foo', @@ -1918,25 +1905,49 @@ tap.test('assets - .js() - should send 103 Early hints', async (t) => { data: { foo: 'bar' }, }); - const server = new FakeHttpServer({ podlet }, (incoming) => { - incoming.response.statusCode = 200; - incoming.response.end('OK'); + const server = new FakeExpressServer(podlet, (req, res) => { + res.podiumSend('

OK!

'); }); await server.listen(); - const res = server.get({ pathname: '/' }); - res.hints((info) => { - t.equal( - info.link, - '; async=true; type=module; data-foo=bar; asset-type=script', - ); + const result = await server.get({ raw: true }); + + t.equal( + result.headers.link, + '; async=true; type=module; data-foo=bar; asset-type=script', + ); + await server.close(); +}); + +tap.test('assets - .css() - should send assets respecting scope', async (t) => { + t.plan(1); + const podlet = new Podlet({ + name: 'foo', + version: 'v1.0.0', + pathname: '/', + development: true, }); - await res.result(); + + podlet.css([ + { value: '/styles1.css', scope: 'content' }, + { value: '/styles2.css', scope: 'fallback' }, + ]); + + const server = new FakeExpressServer(podlet, (req, res) => { + res.podiumSend('

OK!

'); + }); + + await server.listen(); + const result = await server.get({ raw: true }); + t.equal( + result.headers.link, + '; type=text/css; rel=stylesheet; scope=content; asset-type=style', + ); await server.close(); }); tap.test( - 'assets - .css() - should send 103 Early hints respecting scope', + 'assets - .css() - should send assets using Link header respecting scope - fallback', async (t) => { t.plan(1); const podlet = new Podlet({ @@ -1944,6 +1955,7 @@ tap.test( version: 'v1.0.0', pathname: '/', development: true, + fallback: '/fallback', }); podlet.css([ @@ -1951,26 +1963,27 @@ tap.test( { value: '/styles2.css', scope: 'fallback' }, ]); - const server = new FakeHttpServer({ podlet }, (incoming) => { - incoming.response.statusCode = 200; - incoming.response.end('OK'); - }); + const server = new FakeExpressServer( + podlet, + undefined, + undefined, + (req, res) => { + res.podiumSend('

OK!

'); + }, + ); await server.listen(); - const res = server.get({ pathname: '/' }); - res.hints((info) => { - t.equal( - info.link, - '; type=text/css; rel=stylesheet; scope=content; asset-type=style', - ); - }); - await res.result(); + const result = await server.get({ path: '/fallback', raw: true }); + t.equal( + result.headers.link, + '; type=text/css; rel=stylesheet; scope=fallback; asset-type=style', + ); await server.close(); }, ); tap.test( - 'assets - .css() - should send 103 Early hints respecting scope - fallback', + 'assets - .js() and .css() - should send assets using Link header', async (t) => { t.plan(1); const podlet = new Podlet({ @@ -1978,36 +1991,35 @@ tap.test( version: 'v1.0.0', pathname: '/', development: true, - fallback: '/fallback', }); - podlet.css([ - { value: '/styles1.css', scope: 'content' }, - { value: '/styles2.css', scope: 'fallback' }, - ]); + podlet.js({ + value: '/scripts.js', + type: 'module', + async: true, + data: [{ key: 'foo', value: 'bar' }], + scope: 'content', + }); + podlet.css({ value: '/styles.css', scope: 'content' }); - const server = new FakeHttpServer({ podlet }, (incoming) => { - incoming.response.statusCode = 200; - incoming.response.end('OK'); + const server = new FakeExpressServer(podlet, (req, res) => { + res.podiumSend('

OK!

'); }); await server.listen(); - const res = server.get({ pathname: '/fallback' }); - res.hints((info) => { - t.equal( - info.link, - '; type=text/css; rel=stylesheet; scope=fallback; asset-type=style', - ); - }); - await res.result(); + const result = await server.get({ raw: true }); + t.equal( + result.headers.link, + '; async=true; type=module; data-foo=bar; scope=content; asset-type=script, ; type=text/css; rel=stylesheet; scope=content; asset-type=style', + ); await server.close(); }, ); tap.test( - 'assets - .js() and .css() - should send 103 Early hints', + 'assets - .js() and .css() - Link headers - should be sent before body', async (t) => { - t.plan(1); + t.plan(3); const podlet = new Podlet({ name: 'foo', version: 'v1.0.0', @@ -2024,20 +2036,29 @@ tap.test( }); podlet.css({ value: '/styles.css', scope: 'content' }); - const server = new FakeHttpServer({ podlet }, (incoming) => { - incoming.response.statusCode = 200; - incoming.response.end('OK'); + const orderArray = []; + + const server = new FakeExpressServer(podlet, (req, res) => { + res.sendHeaders(); + setTimeout(() => { + res.podiumSend('

OK!

'); + }, 1000); }); await server.listen(); - const res = server.get({ pathname: '/' }); - res.hints((info) => { - t.equal( - info.link, - '; async=true; type=module; data-foo=bar; scope=content; asset-type=script, ; type=text/css; rel=stylesheet; scope=content; asset-type=style', - ); + const result = await server.get({ + raw: true, + onHeaders(headers) { + t.equal( + headers.link, + '; async=true; type=module; data-foo=bar; scope=content; asset-type=script, ; type=text/css; rel=stylesheet; scope=content; asset-type=style', + ); + orderArray.push('assets'); + }, }); - await res.result(); + orderArray.push('body'); + t.match(result.response, /

OK!<\/h1>/); + t.same(orderArray, ['assets', 'body']); await server.close(); }, );