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();
},
);