@t09tanaka/stoneage 0.2.0 → 0.3.0
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.
- package/CHANGELOG.md +8 -0
- package/README.md +30 -0
- package/dist/core.d.ts +23 -0
- package/dist/core.js +82 -15
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 - 2026-06-30
|
|
4
|
+
|
|
5
|
+
- Added global stylesheet auto-inlining via `assets.inlineStylesheets` (inline
|
|
6
|
+
every generated stylesheet) and `assets.inlineStylesheetMaxBytes` (inline only
|
|
7
|
+
bundles whose built CSS is within the byte threshold). A per-stylesheet
|
|
8
|
+
`inline` flag still wins; when both options are set the byte threshold gates
|
|
9
|
+
inlining. Google Fonts WOFF2 stay vendored and preload hints are unchanged.
|
|
10
|
+
|
|
3
11
|
## 0.2.0 - 2026-06-30
|
|
4
12
|
|
|
5
13
|
- Added immutable `Cache-Control: public, max-age=31536000, immutable` coverage
|
package/README.md
CHANGED
|
@@ -270,6 +270,36 @@ files; an inlined `<style>` is subject to the page's Content-Security-Policy
|
|
|
270
270
|
`style-src` directive, so keep the external link if you serve a strict CSP
|
|
271
271
|
without `'unsafe-inline'` (or a matching hash/nonce).
|
|
272
272
|
|
|
273
|
+
To inline without flagging each stylesheet, set a global policy on `assets`.
|
|
274
|
+
`assets.inlineStylesheets: true` inlines every generated stylesheet (top-level
|
|
275
|
+
and per-page client ones), and `assets.inlineStylesheetMaxBytes: <n>` inlines
|
|
276
|
+
only bundles whose built CSS is at most `n` bytes (UTF-8) — handy for inlining a
|
|
277
|
+
small critical bundle while larger ones stay external. A per-stylesheet `inline`
|
|
278
|
+
flag always wins over the global policy: `inline: false` opts a sheet out and
|
|
279
|
+
`inline: true` opts one in regardless of the threshold. When both options are
|
|
280
|
+
set, `inlineStylesheetMaxBytes` acts as a gate — a bundle over the limit stays
|
|
281
|
+
external even with `inlineStylesheets: true` (a non-positive or non-finite limit
|
|
282
|
+
inlines nothing). The hyogo critical-path setup combines this with font preload
|
|
283
|
+
— inline the small site and fonts CSS while still preloading the primary WOFF2:
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
assets: {
|
|
287
|
+
inlineStylesheetMaxBytes: 16_384, // inline bundles ≤ 16 KB
|
|
288
|
+
stylesheets: [
|
|
289
|
+
{ href: "/assets/site.css", source: "dist/site.css" },
|
|
290
|
+
googleFontsStylesheet("/assets/fonts.css", {
|
|
291
|
+
families: ["Zen Maru Gothic:wght@700;900", "Outfit:wght@700;900"],
|
|
292
|
+
text: subsetText,
|
|
293
|
+
preload: 2, // keep the primary WOFF2 off the critical chain
|
|
294
|
+
}),
|
|
295
|
+
],
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Both stylesheets inline as `<style>` (when within the threshold), the WOFF2
|
|
300
|
+
files are still vendored as immutable assets, and the preload hints render ahead
|
|
301
|
+
of the inlined CSS so fonts are discovered without waiting on CSS.
|
|
302
|
+
|
|
273
303
|
To pull self-hosted fonts off the critical request chain (HTML → CSS → WOFF2),
|
|
274
304
|
emit `<link rel="preload" as="font" type="font/woff2" crossorigin>` hints with
|
|
275
305
|
`googleFontsStylesheet("/assets/fonts.css", { families, text, preload: true })`.
|
package/dist/core.d.ts
CHANGED
|
@@ -303,6 +303,29 @@ export type BuildConfig = {
|
|
|
303
303
|
assets?: {
|
|
304
304
|
stylesheets?: ClientStylesheetAsset[];
|
|
305
305
|
client?: ClientAssetRegistry;
|
|
306
|
+
/**
|
|
307
|
+
* Inline every generated stylesheet's CSS into each document `<head>` as a
|
|
308
|
+
* `<style>` element instead of emitting render-blocking
|
|
309
|
+
* `<link rel="stylesheet">`s. Applies to both `assets.stylesheets` and the
|
|
310
|
+
* stylesheets resolved from per-page `assets.client` registries (inlining a
|
|
311
|
+
* client stylesheet turns it into a `<style>` on the pages that use it). A
|
|
312
|
+
* per-stylesheet `inline` flag always wins: `inline: false` opts a sheet out
|
|
313
|
+
* even when this is true, and `inline: true` opts one in. When
|
|
314
|
+
* {@link inlineStylesheetMaxBytes} is also set, the byte threshold gates this:
|
|
315
|
+
* a bundle larger than the threshold stays external even with this enabled.
|
|
316
|
+
* Defaults to the existing external-link behavior. Inlined CSS is subject to
|
|
317
|
+
* the page's CSP `style-src`; see {@link InlineStylesheetAsset} for the caveat.
|
|
318
|
+
*/
|
|
319
|
+
inlineStylesheets?: boolean;
|
|
320
|
+
/**
|
|
321
|
+
* Auto-inline a generated stylesheet only when its built CSS is at most this
|
|
322
|
+
* many bytes (UTF-8), measured after Google Fonts CSS resolution. Lets small,
|
|
323
|
+
* critical bundles inline while larger ones stay external. A per-stylesheet
|
|
324
|
+
* `inline` flag still wins over this gate. When {@link inlineStylesheets} is
|
|
325
|
+
* also true, this acts as the gate: bundles over the threshold are not
|
|
326
|
+
* inlined. A non-positive or non-finite value inlines nothing.
|
|
327
|
+
*/
|
|
328
|
+
inlineStylesheetMaxBytes?: number;
|
|
306
329
|
public?: PublicAssetInput[];
|
|
307
330
|
/**
|
|
308
331
|
* Directory whose tree is copied verbatim onto the output root. Each file
|
package/dist/core.js
CHANGED
|
@@ -617,11 +617,18 @@ async function resolveStaticAssetReferences(assets) {
|
|
|
617
617
|
}
|
|
618
618
|
return hashedPath;
|
|
619
619
|
};
|
|
620
|
-
|
|
620
|
+
// Global auto-inline policy, applied to any stylesheet that does not set its
|
|
621
|
+
// own `inline` flag. Threaded through both top-level and per-page client
|
|
622
|
+
// stylesheet resolution so the behavior is consistent everywhere.
|
|
623
|
+
const inlinePolicy = {
|
|
624
|
+
inlineStylesheets: assets.inlineStylesheets,
|
|
625
|
+
inlineStylesheetMaxBytes: assets.inlineStylesheetMaxBytes,
|
|
626
|
+
};
|
|
627
|
+
const stylesheets = await Promise.all((assets.stylesheets ?? []).map(async (stylesheet) => resolveStylesheetAsset(stylesheet, resolveImmutablePublicAsset, collectFontPreloads, inlinePolicy)));
|
|
621
628
|
// Per-page client stylesheets are resolved without a preload collector: a
|
|
622
629
|
// page-scoped font must not become a global preload hint on every page.
|
|
623
630
|
const client = assets.client
|
|
624
|
-
? await resolveClientAssetRegistry(assets.client, resolveImmutablePublicAsset)
|
|
631
|
+
? await resolveClientAssetRegistry(assets.client, resolveImmutablePublicAsset, inlinePolicy)
|
|
625
632
|
: undefined;
|
|
626
633
|
return {
|
|
627
634
|
assets: {
|
|
@@ -634,25 +641,73 @@ async function resolveStaticAssetReferences(assets) {
|
|
|
634
641
|
fontPreloads,
|
|
635
642
|
};
|
|
636
643
|
}
|
|
637
|
-
|
|
644
|
+
// Decide whether a stylesheet's built CSS should be inlined. A per-stylesheet
|
|
645
|
+
// `inline` flag always wins; otherwise the global policy applies, with
|
|
646
|
+
// `inlineStylesheetMaxBytes` acting as a byte-size gate over `inlineStylesheets`.
|
|
647
|
+
function shouldInlineStylesheet(explicitInline, css, policy) {
|
|
648
|
+
if (explicitInline !== undefined) {
|
|
649
|
+
return explicitInline;
|
|
650
|
+
}
|
|
651
|
+
if (!policy) {
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
const { inlineStylesheets, inlineStylesheetMaxBytes } = policy;
|
|
655
|
+
if (inlineStylesheetMaxBytes !== undefined) {
|
|
656
|
+
// The threshold gates inlining (even when `inlineStylesheets` is true): a
|
|
657
|
+
// non-positive/non-finite limit inlines nothing, otherwise the built CSS
|
|
658
|
+
// must fit within it.
|
|
659
|
+
if (!Number.isFinite(inlineStylesheetMaxBytes) || inlineStylesheetMaxBytes <= 0) {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
return Buffer.byteLength(css, "utf8") <= inlineStylesheetMaxBytes;
|
|
663
|
+
}
|
|
664
|
+
return inlineStylesheets === true;
|
|
665
|
+
}
|
|
666
|
+
// Whether a global policy could ever inline an `inline`-unspecified stylesheet.
|
|
667
|
+
// Lets the resolver skip reading a source file (and keep the byte-identical
|
|
668
|
+
// external path) when inlining is impossible regardless of CSS size.
|
|
669
|
+
function policyMightInline(policy) {
|
|
670
|
+
if (!policy) {
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
const { inlineStylesheets, inlineStylesheetMaxBytes } = policy;
|
|
674
|
+
if (inlineStylesheetMaxBytes !== undefined) {
|
|
675
|
+
return Number.isFinite(inlineStylesheetMaxBytes) && inlineStylesheetMaxBytes > 0;
|
|
676
|
+
}
|
|
677
|
+
return inlineStylesheets === true;
|
|
678
|
+
}
|
|
679
|
+
async function resolveStylesheetAsset(stylesheet, resolveImmutablePublicAsset, onFontPreload, inlinePolicy) {
|
|
638
680
|
// Plain href strings and already-inlined CSS pass through untouched.
|
|
639
681
|
if (typeof stylesheet === "string" || isResolvedInlineStylesheet(stylesheet)) {
|
|
640
682
|
return stylesheet;
|
|
641
683
|
}
|
|
642
|
-
const
|
|
684
|
+
const explicitInline = stylesheet.inline;
|
|
643
685
|
if ("source" in stylesheet) {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
if (stylesheet.immutable === false) {
|
|
686
|
+
// `immutable: false` is a deliberate "serve this href as-is" opt-out, so the
|
|
687
|
+
// global inline policy does not touch it; only an explicit `inline: true`
|
|
688
|
+
// can still inline such a sheet.
|
|
689
|
+
if (explicitInline !== true && stylesheet.immutable === false) {
|
|
649
690
|
return stripStylesheetBuildHints(stylesheet);
|
|
650
691
|
}
|
|
651
|
-
|
|
692
|
+
// When inlining is impossible (explicit opt-out, or no policy that could
|
|
693
|
+
// inline an unspecified sheet), keep the original source-backed external
|
|
694
|
+
// path so its hashed output stays byte-identical to before.
|
|
695
|
+
if (explicitInline === false || (explicitInline === undefined && !policyMightInline(inlinePolicy))) {
|
|
696
|
+
return resolveImmutablePublicAsset({ source: stylesheet.source }, stylesheet.href);
|
|
697
|
+
}
|
|
698
|
+
// Read the CSS once and reuse it for both the size check and inlining so the
|
|
699
|
+
// file is never read twice.
|
|
700
|
+
const css = await readFile(stylesheet.source, "utf8");
|
|
701
|
+
if (shouldInlineStylesheet(explicitInline, css, inlinePolicy)) {
|
|
702
|
+
// Embed the CSS directly: no hashed public file and no `_headers` entry.
|
|
703
|
+
return { inline: css };
|
|
704
|
+
}
|
|
705
|
+
return resolveImmutablePublicAsset({ content: css }, stylesheet.href);
|
|
652
706
|
}
|
|
653
707
|
// Google Fonts: an `immutable: false` reference is not vendored (legacy
|
|
654
|
-
// behavior), so there are no woff2 to preload or CSS to inline.
|
|
655
|
-
|
|
708
|
+
// behavior), so there are no woff2 to preload or CSS to inline. As above, the
|
|
709
|
+
// global policy leaves this opt-out alone unless `inline: true` is explicit.
|
|
710
|
+
if (explicitInline !== true && stylesheet.immutable === false) {
|
|
656
711
|
return stripStylesheetBuildHints(stylesheet);
|
|
657
712
|
}
|
|
658
713
|
const { css, fontPaths } = await resolveGoogleFontsStylesheet(stylesheet.href, stylesheet.googleFonts, resolveImmutablePublicAsset);
|
|
@@ -663,7 +718,9 @@ async function resolveStylesheetAsset(stylesheet, resolveImmutablePublicAsset, o
|
|
|
663
718
|
}
|
|
664
719
|
}
|
|
665
720
|
// The woff2 files are always vendored above; only the CSS is inlined.
|
|
666
|
-
return
|
|
721
|
+
return shouldInlineStylesheet(explicitInline, css, inlinePolicy)
|
|
722
|
+
? { inline: css }
|
|
723
|
+
: resolveImmutablePublicAsset({ content: css }, stylesheet.href);
|
|
667
724
|
}
|
|
668
725
|
function isResolvedInlineStylesheet(stylesheet) {
|
|
669
726
|
// The resolved inline form carries the CSS text as a string; the input forms
|
|
@@ -689,13 +746,17 @@ function selectFontPreloads(fontPaths, preload) {
|
|
|
689
746
|
}
|
|
690
747
|
return fontPaths.slice(0, Math.floor(preload));
|
|
691
748
|
}
|
|
692
|
-
async function resolveClientAssetRegistry(registry, resolveImmutablePublicAsset) {
|
|
749
|
+
async function resolveClientAssetRegistry(registry, resolveImmutablePublicAsset, inlinePolicy) {
|
|
693
750
|
const entries = await Promise.all(Object.entries(registry).map(async ([id, declarations]) => [
|
|
694
751
|
id,
|
|
695
752
|
await Promise.all((Array.isArray(declarations) ? declarations : [declarations]).map(async (declaration) => ({
|
|
696
753
|
...(declaration.stylesheets
|
|
697
754
|
? {
|
|
698
|
-
stylesheets: await Promise.all(declaration.stylesheets.map((stylesheet) =>
|
|
755
|
+
stylesheets: await Promise.all(declaration.stylesheets.map((stylesheet) =>
|
|
756
|
+
// No preload collector here: a page-scoped client font must
|
|
757
|
+
// not become a global hint. The global inline policy still
|
|
758
|
+
// applies so client CSS inlines consistently.
|
|
759
|
+
resolveStylesheetAsset(stylesheet, resolveImmutablePublicAsset, undefined, inlinePolicy))),
|
|
699
760
|
}
|
|
700
761
|
: {}),
|
|
701
762
|
...(declaration.scripts
|
|
@@ -987,6 +1048,12 @@ export async function buildSite(config) {
|
|
|
987
1048
|
const sharedPageFingerprintHash = hashValue({
|
|
988
1049
|
site: config.site,
|
|
989
1050
|
stylesheets: assets?.stylesheets ?? [],
|
|
1051
|
+
// The global inline policy can flip a stylesheet between external and
|
|
1052
|
+
// inlined; including the options invalidates every page when the policy
|
|
1053
|
+
// changes, even for pages whose CSS lives only in `assets.client` (whose
|
|
1054
|
+
// resolved forms are not part of this shared hash).
|
|
1055
|
+
inlineStylesheets: assets?.inlineStylesheets ?? false,
|
|
1056
|
+
inlineStylesheetMaxBytes: assets?.inlineStylesheetMaxBytes ?? null,
|
|
990
1057
|
fontPreloads,
|
|
991
1058
|
prefetch: config.prefetch ?? {},
|
|
992
1059
|
trailingSlash,
|