Skip to content

Commit

Permalink
feat: replace early hints with link header
Browse files Browse the repository at this point in the history
  • Loading branch information
digitalsadhu committed Oct 10, 2024
1 parent f2fc136 commit 815ea97
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 89 deletions.
41 changes: 21 additions & 20 deletions lib/podlet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -986,15 +968,34 @@ 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);

if (res.header) {
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) {
Expand Down
159 changes: 90 additions & 69 deletions tests/podlet.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
},
};
Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -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',
Expand All @@ -1918,96 +1905,121 @@ 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('<h1>OK!</h1>');
});

await server.listen();
const res = server.get({ pathname: '/' });
res.hints((info) => {
t.equal(
info.link,
'</scripts.js>; async=true; type=module; data-foo=bar; asset-type=script',
);
const result = await server.get({ raw: true });

t.equal(
result.headers.link,
'</scripts.js>; 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('<h1>OK!</h1>');
});

await server.listen();
const result = await server.get({ raw: true });
t.equal(
result.headers.link,
'</styles1.css>; 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({
name: 'foo',
version: 'v1.0.0',
pathname: '/',
development: true,
fallback: '/fallback',
});

podlet.css([
{ value: '/styles1.css', scope: 'content' },
{ 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('<h1>OK!</h1>');
},
);

await server.listen();
const res = server.get({ pathname: '/' });
res.hints((info) => {
t.equal(
info.link,
'</styles1.css>; 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,
'</styles2.css>; 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({
name: 'foo',
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('<h1>OK!</h1>');
});

await server.listen();
const res = server.get({ pathname: '/fallback' });
res.hints((info) => {
t.equal(
info.link,
'</styles2.css>; 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,
'</scripts.js>; async=true; type=module; data-foo=bar; scope=content; asset-type=script, </styles.css>; 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',
Expand All @@ -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('<h1>OK!</h1>');
}, 1000);
});

await server.listen();
const res = server.get({ pathname: '/' });
res.hints((info) => {
t.equal(
info.link,
'</scripts.js>; async=true; type=module; data-foo=bar; scope=content; asset-type=script, </styles.css>; type=text/css; rel=stylesheet; scope=content; asset-type=style',
);
const result = await server.get({
raw: true,
onHeaders(headers) {
t.equal(
headers.link,
'</scripts.js>; async=true; type=module; data-foo=bar; scope=content; asset-type=script, </styles.css>; type=text/css; rel=stylesheet; scope=content; asset-type=style',
);
orderArray.push('assets');
},
});
await res.result();
orderArray.push('body');
t.match(result.response, /<h1>OK!<\/h1>/);
t.same(orderArray, ['assets', 'body']);
await server.close();
},
);
Expand Down

0 comments on commit 815ea97

Please sign in to comment.