@t09tanaka/stoneage 0.1.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 +23 -0
- package/LICENSE +21 -0
- package/README.md +768 -0
- package/dist/agent-skill.d.ts +1 -0
- package/dist/agent-skill.js +140 -0
- package/dist/assets.d.ts +125 -0
- package/dist/assets.js +341 -0
- package/dist/cli.d.ts +236 -0
- package/dist/cli.js +3077 -0
- package/dist/core.d.ts +473 -0
- package/dist/core.js +2897 -0
- package/dist/data.d.ts +121 -0
- package/dist/data.js +358 -0
- package/dist/deploy.d.ts +34 -0
- package/dist/deploy.js +203 -0
- package/dist/dev.d.ts +36 -0
- package/dist/dev.js +313 -0
- package/dist/example.d.ts +134 -0
- package/dist/example.js +1272 -0
- package/dist/fragment/client.d.ts +13 -0
- package/dist/fragment/client.js +150 -0
- package/dist/fragment.d.ts +15 -0
- package/dist/fragment.js +27 -0
- package/dist/html.d.ts +57 -0
- package/dist/html.js +208 -0
- package/dist/images.d.ts +95 -0
- package/dist/images.js +292 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/migration.d.ts +157 -0
- package/dist/migration.js +983 -0
- package/dist/optimize.d.ts +15 -0
- package/dist/optimize.js +215 -0
- package/dist/pagination.d.ts +26 -0
- package/dist/pagination.js +62 -0
- package/dist/paths.d.ts +1 -0
- package/dist/paths.js +10 -0
- package/dist/search.d.ts +32 -0
- package/dist/search.js +55 -0
- package/dist/testing.d.ts +66 -0
- package/dist/testing.js +97 -0
- package/dist/validate.d.ts +2 -0
- package/dist/validate.js +1 -0
- package/jsx-loader.mjs +52 -0
- package/jsx-register.mjs +7 -0
- package/package.json +135 -0
- package/tsconfig.base.json +16 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const stoneAgeAgentSkill = "---\nname: stoneage\ndescription: Use when working on a StoneAge static data-site, adding routes or artifacts, debugging generated output, validating publish readiness, or helping an agent use StoneAge in a project.\n---\n\n# StoneAge\n\nUse this skill when a project uses StoneAge or asks to generate, validate, inspect, optimize, or serve a StoneAge static data-site.\n\n## Project Direction\n\nStoneAge is a TypeScript static site generator for large data sites. Optimize for plain HTML output, explicit data flow, typed route generation, incremental generation from dependency hashes, publish validation, and small assets. Do not turn StoneAge into a general web application framework.\n\n## Minimal Example\n\nUse these real API names and import paths; do not invent others. Core build: `buildSite`. HTML pages: `defineRouteFamily`. Public data outputs: `defineArtifactFamily` with `jsonArtifact` / `csvArtifact`. Deferred HTML fragments: `defineFragmentFamily` plus `deferredFragment` from the `/fragment` subpath. HTML rendering: `html` / `renderToString` from the `/html` subpath.\n\n```ts\nimport {\n buildSite,\n defineRouteFamily,\n defineArtifactFamily,\n defineFragmentFamily,\n jsonArtifact,\n csvArtifact,\n type SiteConfig,\n} from \"@t09tanaka/stoneage\";\nimport { deferredFragment } from \"@t09tanaka/stoneage/fragment\";\nimport { html, renderToString } from \"@t09tanaka/stoneage/html\";\n\nconst members = [{ id: \"m1\", slug: \"ada\", name: \"Ada\", district: \"Central\" }];\n\nconst site: SiteConfig = {\n baseUrl: \"https://example.com\",\n title: \"Example\",\n description: \"A generated data-site.\",\n};\n\nconst memberDetails = defineFragmentFamily({\n name: \"memberDetails\",\n pattern: \"members/:slug/details\",\n entries: () =>\n members.map((member) => ({\n params: { slug: member.slug },\n dependencies: [\"member:\" + member.id],\n render: () => renderToString(html(\"section\", null, member.name + \" details\")),\n })),\n});\n\nconst memberPages = defineRouteFamily({\n name: \"members\",\n pattern: \"/members/:slug/\",\n entries: () =>\n members.map((member) => ({\n params: { slug: member.slug },\n dependencies: [\"member:\" + member.id],\n metadata: { title: member.name, description: member.name + \" (\" + member.district + \")\" },\n render: () =>\n renderToString(\n html(\n \"main\",\n null,\n html(\"h1\", null, member.name),\n deferredFragment({\n src: memberDetails.path({ slug: member.slug }),\n fallback: html(\"a\", { href: \"/members/\" + member.slug + \"/\" }, \"Read details\"),\n }),\n ),\n ),\n })),\n});\n\nconst memberData = defineArtifactFamily({\n name: \"memberData\",\n pattern: \"/data/members.json\",\n entries: () => [\n {\n params: {},\n dependencies: members.map((member) => \"member:\" + member.id),\n render: () => jsonArtifact(members),\n },\n ],\n});\n\nawait buildSite({\n outDir: \"dist\",\n site,\n routes: [memberPages],\n artifacts: [memberData],\n fragments: [memberDetails],\n});\n```\n\n`render` must return a string (wrap TSX or nodes with `renderToString`). Use `csvArtifact(text)` for CSV outputs. `dependencies` is a `Dependency[]`: a plain `\"key\"` string, or `{ key, hash }` / `{ key, value }` / `{ key, file }` when the site computes its own fingerprints.\n\n## Before Editing\n\n- Search existing buildSite usage before changing a site.\n- Read the local site build entrypoint, route families, artifact families, data normalization modules, and validation config.\n- Keep public data artifacts separate from HTML-only view models.\n- Prefer structured data models and parsers over ad hoc string manipulation.\n- If changing shared types, route patterns, artifact patterns, or build config contracts, inspect references and run TypeScript diagnostics.\n\n## Implementation Rules\n\n- Use route families for HTML pages and artifact families for public JSON, CSV, XML, text, or endpoint-style outputs.\n- Use fragment families for supplemental HTML that may load after the initial page. `deferredFragment()` provides a fragment self-link fallback by default; override it with a canonical page link when one exists, or pass `fallback: null` only when no-JS omission is acceptable. Do not defer the only copy of primary page content.\n- Attach dependencies to generated pages and artifacts so incremental rebuilds can skip unchanged output.\n- Use stable hashes for normalized data or source fingerprints when the site already computes them.\n- Keep rendering functions focused on HTML; keep normalization outside rendering.\n- Add metadata, sitemap, assets, redirects, headers, and validation settings explicitly in the build config.\n- Avoid browser runtime assumptions in core rendering code.\n\n## Useful Commands\n\n- `stoneage dev --out-dir <dir> --watch <path> --build-command \"<command>\"`: serve generated output with hot reload after successful rebuilds.\n- `stoneage validate --out-dir <dir>`: validate generated output before publishing.\n- `stoneage inspect --out-dir <dir> --dependency <key>`: list generated outputs depending on one dependency key.\n- `stoneage plan --out-dir <dir> --dependency <key>`: plan outputs affected by dependency keys.\n- `stoneage optimize --out-dir <dir>`: write precompressed public output sidecars.\n- `stoneage deploy --out-dir <dir> --provider netlify`: emit deploy artifacts.\n\n## Verification\n\n- Run the project's build command after route, artifact, or data-flow changes.\n- Run `stoneage validate` for publish-facing output changes.\n- Run targeted tests for changed data, routes, artifacts, CLI behavior, or validation logic.\n- For package changes in the StoneAge repository, run `npm run typecheck`, relevant `npm test -- <files>`, and `npm run check:package`.\n\n## Avoid\n\n- Do not mix public data exports into HTML-only view models.\n- Do not hide metadata or sitemap decisions inside templates.\n- Do not add compatibility layers, runtime hydration, or app-framework abstractions unless the user explicitly asks.\n- Do not rewrite generated output paths, route contracts, or dependency keys without checking existing manifests and callers.\n\n## References\n\nWhen a detail is not covered here, read the package docs instead of guessing the API: `getting-started.md` (minimal site), `site-build.md` (route families, artifact families, metadata, assets, validation, publishing), `fragments.md` (deferred fragments and triggers), `components.md` (`html()`, `renderToString()`, TSX, islands), `data-flow.md` (normalized data, dependencies, incremental rebuilds), and `migration.md` (SvelteKit). The public API is exported from `@t09tanaka/stoneage`, `@t09tanaka/stoneage/html`, and `@t09tanaka/stoneage/fragment`.\n";
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
export const stoneAgeAgentSkill = `---
|
|
2
|
+
name: stoneage
|
|
3
|
+
description: Use when working on a StoneAge static data-site, adding routes or artifacts, debugging generated output, validating publish readiness, or helping an agent use StoneAge in a project.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# StoneAge
|
|
7
|
+
|
|
8
|
+
Use this skill when a project uses StoneAge or asks to generate, validate, inspect, optimize, or serve a StoneAge static data-site.
|
|
9
|
+
|
|
10
|
+
## Project Direction
|
|
11
|
+
|
|
12
|
+
StoneAge is a TypeScript static site generator for large data sites. Optimize for plain HTML output, explicit data flow, typed route generation, incremental generation from dependency hashes, publish validation, and small assets. Do not turn StoneAge into a general web application framework.
|
|
13
|
+
|
|
14
|
+
## Minimal Example
|
|
15
|
+
|
|
16
|
+
Use these real API names and import paths; do not invent others. Core build: \`buildSite\`. HTML pages: \`defineRouteFamily\`. Public data outputs: \`defineArtifactFamily\` with \`jsonArtifact\` / \`csvArtifact\`. Deferred HTML fragments: \`defineFragmentFamily\` plus \`deferredFragment\` from the \`/fragment\` subpath. HTML rendering: \`html\` / \`renderToString\` from the \`/html\` subpath.
|
|
17
|
+
|
|
18
|
+
\`\`\`ts
|
|
19
|
+
import {
|
|
20
|
+
buildSite,
|
|
21
|
+
defineRouteFamily,
|
|
22
|
+
defineArtifactFamily,
|
|
23
|
+
defineFragmentFamily,
|
|
24
|
+
jsonArtifact,
|
|
25
|
+
csvArtifact,
|
|
26
|
+
type SiteConfig,
|
|
27
|
+
} from "@t09tanaka/stoneage";
|
|
28
|
+
import { deferredFragment } from "@t09tanaka/stoneage/fragment";
|
|
29
|
+
import { html, renderToString } from "@t09tanaka/stoneage/html";
|
|
30
|
+
|
|
31
|
+
const members = [{ id: "m1", slug: "ada", name: "Ada", district: "Central" }];
|
|
32
|
+
|
|
33
|
+
const site: SiteConfig = {
|
|
34
|
+
baseUrl: "https://example.com",
|
|
35
|
+
title: "Example",
|
|
36
|
+
description: "A generated data-site.",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const memberDetails = defineFragmentFamily({
|
|
40
|
+
name: "memberDetails",
|
|
41
|
+
pattern: "members/:slug/details",
|
|
42
|
+
entries: () =>
|
|
43
|
+
members.map((member) => ({
|
|
44
|
+
params: { slug: member.slug },
|
|
45
|
+
dependencies: ["member:" + member.id],
|
|
46
|
+
render: () => renderToString(html("section", null, member.name + " details")),
|
|
47
|
+
})),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const memberPages = defineRouteFamily({
|
|
51
|
+
name: "members",
|
|
52
|
+
pattern: "/members/:slug/",
|
|
53
|
+
entries: () =>
|
|
54
|
+
members.map((member) => ({
|
|
55
|
+
params: { slug: member.slug },
|
|
56
|
+
dependencies: ["member:" + member.id],
|
|
57
|
+
metadata: { title: member.name, description: member.name + " (" + member.district + ")" },
|
|
58
|
+
render: () =>
|
|
59
|
+
renderToString(
|
|
60
|
+
html(
|
|
61
|
+
"main",
|
|
62
|
+
null,
|
|
63
|
+
html("h1", null, member.name),
|
|
64
|
+
deferredFragment({
|
|
65
|
+
src: memberDetails.path({ slug: member.slug }),
|
|
66
|
+
fallback: html("a", { href: "/members/" + member.slug + "/" }, "Read details"),
|
|
67
|
+
}),
|
|
68
|
+
),
|
|
69
|
+
),
|
|
70
|
+
})),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const memberData = defineArtifactFamily({
|
|
74
|
+
name: "memberData",
|
|
75
|
+
pattern: "/data/members.json",
|
|
76
|
+
entries: () => [
|
|
77
|
+
{
|
|
78
|
+
params: {},
|
|
79
|
+
dependencies: members.map((member) => "member:" + member.id),
|
|
80
|
+
render: () => jsonArtifact(members),
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await buildSite({
|
|
86
|
+
outDir: "dist",
|
|
87
|
+
site,
|
|
88
|
+
routes: [memberPages],
|
|
89
|
+
artifacts: [memberData],
|
|
90
|
+
fragments: [memberDetails],
|
|
91
|
+
});
|
|
92
|
+
\`\`\`
|
|
93
|
+
|
|
94
|
+
\`render\` must return a string (wrap TSX or nodes with \`renderToString\`). Use \`csvArtifact(text)\` for CSV outputs. \`dependencies\` is a \`Dependency[]\`: a plain \`"key"\` string, or \`{ key, hash }\` / \`{ key, value }\` / \`{ key, file }\` when the site computes its own fingerprints.
|
|
95
|
+
|
|
96
|
+
## Before Editing
|
|
97
|
+
|
|
98
|
+
- Search existing buildSite usage before changing a site.
|
|
99
|
+
- Read the local site build entrypoint, route families, artifact families, data normalization modules, and validation config.
|
|
100
|
+
- Keep public data artifacts separate from HTML-only view models.
|
|
101
|
+
- Prefer structured data models and parsers over ad hoc string manipulation.
|
|
102
|
+
- If changing shared types, route patterns, artifact patterns, or build config contracts, inspect references and run TypeScript diagnostics.
|
|
103
|
+
|
|
104
|
+
## Implementation Rules
|
|
105
|
+
|
|
106
|
+
- Use route families for HTML pages and artifact families for public JSON, CSV, XML, text, or endpoint-style outputs.
|
|
107
|
+
- Use fragment families for supplemental HTML that may load after the initial page. \`deferredFragment()\` provides a fragment self-link fallback by default; override it with a canonical page link when one exists, or pass \`fallback: null\` only when no-JS omission is acceptable. Do not defer the only copy of primary page content.
|
|
108
|
+
- Attach dependencies to generated pages and artifacts so incremental rebuilds can skip unchanged output.
|
|
109
|
+
- Use stable hashes for normalized data or source fingerprints when the site already computes them.
|
|
110
|
+
- Keep rendering functions focused on HTML; keep normalization outside rendering.
|
|
111
|
+
- Add metadata, sitemap, assets, redirects, headers, and validation settings explicitly in the build config.
|
|
112
|
+
- Avoid browser runtime assumptions in core rendering code.
|
|
113
|
+
|
|
114
|
+
## Useful Commands
|
|
115
|
+
|
|
116
|
+
- \`stoneage dev --out-dir <dir> --watch <path> --build-command "<command>"\`: serve generated output with hot reload after successful rebuilds.
|
|
117
|
+
- \`stoneage validate --out-dir <dir>\`: validate generated output before publishing.
|
|
118
|
+
- \`stoneage inspect --out-dir <dir> --dependency <key>\`: list generated outputs depending on one dependency key.
|
|
119
|
+
- \`stoneage plan --out-dir <dir> --dependency <key>\`: plan outputs affected by dependency keys.
|
|
120
|
+
- \`stoneage optimize --out-dir <dir>\`: write precompressed public output sidecars.
|
|
121
|
+
- \`stoneage deploy --out-dir <dir> --provider netlify\`: emit deploy artifacts.
|
|
122
|
+
|
|
123
|
+
## Verification
|
|
124
|
+
|
|
125
|
+
- Run the project's build command after route, artifact, or data-flow changes.
|
|
126
|
+
- Run \`stoneage validate\` for publish-facing output changes.
|
|
127
|
+
- Run targeted tests for changed data, routes, artifacts, CLI behavior, or validation logic.
|
|
128
|
+
- For package changes in the StoneAge repository, run \`npm run typecheck\`, relevant \`npm test -- <files>\`, and \`npm run check:package\`.
|
|
129
|
+
|
|
130
|
+
## Avoid
|
|
131
|
+
|
|
132
|
+
- Do not mix public data exports into HTML-only view models.
|
|
133
|
+
- Do not hide metadata or sitemap decisions inside templates.
|
|
134
|
+
- Do not add compatibility layers, runtime hydration, or app-framework abstractions unless the user explicitly asks.
|
|
135
|
+
- Do not rewrite generated output paths, route contracts, or dependency keys without checking existing manifests and callers.
|
|
136
|
+
|
|
137
|
+
## References
|
|
138
|
+
|
|
139
|
+
When a detail is not covered here, read the package docs instead of guessing the API: \`getting-started.md\` (minimal site), \`site-build.md\` (route families, artifact families, metadata, assets, validation, publishing), \`fragments.md\` (deferred fragments and triggers), \`components.md\` (\`html()\`, \`renderToString()\`, TSX, islands), \`data-flow.md\` (normalized data, dependencies, incremental rebuilds), and \`migration.md\` (SvelteKit). The public API is exported from \`@t09tanaka/stoneage\`, \`@t09tanaka/stoneage/html\`, and \`@t09tanaka/stoneage/fragment\`.
|
|
140
|
+
`;
|
package/dist/assets.d.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export type ClientAssetAttributeValue = string | number | boolean | null | undefined;
|
|
2
|
+
export type AssetFetchResponse = {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
status: number;
|
|
5
|
+
text: () => Promise<string>;
|
|
6
|
+
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
7
|
+
};
|
|
8
|
+
export type AssetFetch = (url: string, init?: {
|
|
9
|
+
headers?: Record<string, string>;
|
|
10
|
+
}) => Promise<AssetFetchResponse>;
|
|
11
|
+
export type GoogleFontsStylesheetOptions = {
|
|
12
|
+
families: string[];
|
|
13
|
+
text?: string;
|
|
14
|
+
display?: string;
|
|
15
|
+
endpoint?: string;
|
|
16
|
+
fontBasePath?: string;
|
|
17
|
+
userAgent?: string;
|
|
18
|
+
fetch?: AssetFetch;
|
|
19
|
+
};
|
|
20
|
+
export type ClientStylesheetAsset = string | {
|
|
21
|
+
href: string;
|
|
22
|
+
source: string;
|
|
23
|
+
immutable?: boolean;
|
|
24
|
+
} | {
|
|
25
|
+
href: string;
|
|
26
|
+
googleFonts: GoogleFontsStylesheetOptions;
|
|
27
|
+
immutable?: boolean;
|
|
28
|
+
};
|
|
29
|
+
export type ClientScriptAsset = {
|
|
30
|
+
src: string;
|
|
31
|
+
source?: string;
|
|
32
|
+
immutable?: boolean;
|
|
33
|
+
module?: boolean;
|
|
34
|
+
defer?: boolean;
|
|
35
|
+
async?: boolean;
|
|
36
|
+
attributes?: Record<string, ClientAssetAttributeValue>;
|
|
37
|
+
};
|
|
38
|
+
export type ClientAssetDeclaration = {
|
|
39
|
+
stylesheets?: ClientStylesheetAsset[];
|
|
40
|
+
scripts?: ClientScriptAsset[];
|
|
41
|
+
};
|
|
42
|
+
export type ClientAssetRegistry = Record<string, ClientAssetDeclaration | ClientAssetDeclaration[]>;
|
|
43
|
+
export type ResolvedClientAssets = {
|
|
44
|
+
stylesheets: ClientStylesheetAsset[];
|
|
45
|
+
scripts: ClientScriptAsset[];
|
|
46
|
+
};
|
|
47
|
+
export type ViteManifestChunk = {
|
|
48
|
+
file: string;
|
|
49
|
+
css?: string[];
|
|
50
|
+
imports?: string[];
|
|
51
|
+
assets?: string[];
|
|
52
|
+
isEntry?: boolean;
|
|
53
|
+
};
|
|
54
|
+
export type ViteManifest = Record<string, ViteManifestChunk>;
|
|
55
|
+
export type ViteManifestAssetOptions = {
|
|
56
|
+
base?: string;
|
|
57
|
+
entriesOnly?: boolean;
|
|
58
|
+
};
|
|
59
|
+
export type ViteManifestPublicAssetOptions = {
|
|
60
|
+
sourceDir: string;
|
|
61
|
+
base?: string;
|
|
62
|
+
};
|
|
63
|
+
export type ViteBuildAssetOptions = ViteManifestPublicAssetOptions & ViteManifestAssetOptions;
|
|
64
|
+
export type ViteBuildAssets = {
|
|
65
|
+
client: ClientAssetRegistry;
|
|
66
|
+
public: PublicAssetInput[];
|
|
67
|
+
};
|
|
68
|
+
export type TailwindUtilitiesInput = {
|
|
69
|
+
candidates: string[];
|
|
70
|
+
css?: string;
|
|
71
|
+
};
|
|
72
|
+
export type PublicAssetInput = {
|
|
73
|
+
source: string;
|
|
74
|
+
publicPath: string;
|
|
75
|
+
} | {
|
|
76
|
+
content: string | Buffer | Uint8Array;
|
|
77
|
+
publicPath: string;
|
|
78
|
+
};
|
|
79
|
+
export type CopyPublicAssetsOptions = {
|
|
80
|
+
outDir: string;
|
|
81
|
+
assets: PublicAssetInput[];
|
|
82
|
+
};
|
|
83
|
+
export type CopyPublicAssetsResult = {
|
|
84
|
+
copied: string[];
|
|
85
|
+
skipped: string[];
|
|
86
|
+
};
|
|
87
|
+
export type ExpandPublicDirOptions = {
|
|
88
|
+
base?: string;
|
|
89
|
+
};
|
|
90
|
+
export declare function defineClientAssets<T extends ClientAssetRegistry>(assets: T): T;
|
|
91
|
+
export type StylesheetAssetOptions = {
|
|
92
|
+
source: string;
|
|
93
|
+
immutable?: boolean;
|
|
94
|
+
};
|
|
95
|
+
export declare function stylesheet(href: string, options?: StylesheetAssetOptions): ClientAssetDeclaration;
|
|
96
|
+
export declare function googleFontsStylesheet(href: string, options: GoogleFontsStylesheetOptions): ClientStylesheetAsset;
|
|
97
|
+
export declare function script(src: string, options?: Omit<ClientScriptAsset, "src">): ClientAssetDeclaration;
|
|
98
|
+
export declare function island(src: string, options?: Omit<ClientScriptAsset, "src" | "module">): ClientAssetDeclaration;
|
|
99
|
+
export declare function resolveClientAssets(registry: ClientAssetRegistry, ids: string[]): ResolvedClientAssets;
|
|
100
|
+
export declare function assetsFromViteManifest(manifest: ViteManifest, options?: ViteManifestAssetOptions): ClientAssetRegistry;
|
|
101
|
+
export declare function loadAssetsFromViteManifest(path: string, options?: ViteManifestAssetOptions): Promise<ClientAssetRegistry>;
|
|
102
|
+
export declare function publicAssetsFromViteManifest(manifest: ViteManifest, options: ViteManifestPublicAssetOptions): PublicAssetInput[];
|
|
103
|
+
export declare function loadPublicAssetsFromViteManifest(path: string, options: ViteManifestPublicAssetOptions): Promise<PublicAssetInput[]>;
|
|
104
|
+
export declare function viteBuildAssetsFromManifest(manifest: ViteManifest, options: ViteBuildAssetOptions): ViteBuildAssets;
|
|
105
|
+
export declare function loadViteBuildAssetsFromManifest(path: string, options: ViteBuildAssetOptions): Promise<ViteBuildAssets>;
|
|
106
|
+
/**
|
|
107
|
+
* Recursively walk a directory and expand every file into a `PublicAssetInput`
|
|
108
|
+
* whose `publicPath` mirrors the file's location relative to `dir`. The base of
|
|
109
|
+
* `dir` is the output root, so `public/images/logo.png` becomes
|
|
110
|
+
* `/images/logo.png` and `public/CNAME` becomes `/CNAME`.
|
|
111
|
+
*
|
|
112
|
+
* - Dotfiles and extensionless files (CNAME, .nojekyll) are included.
|
|
113
|
+
* - Nested subdirectories are walked to any depth.
|
|
114
|
+
* - Empty directories produce no entries (only regular files are copied).
|
|
115
|
+
* - Symlinks are not followed: a symlinked directory is not traversed (avoids
|
|
116
|
+
* cycles) and a symlinked file is skipped (its `Dirent` is neither a
|
|
117
|
+
* directory nor a regular file). Materialize the target into `dir` if you
|
|
118
|
+
* need it copied.
|
|
119
|
+
* - A missing `dir` yields an empty list so a build can run before the
|
|
120
|
+
* directory exists.
|
|
121
|
+
*/
|
|
122
|
+
export declare function expandPublicDir(dir: string, options?: ExpandPublicDirOptions): Promise<PublicAssetInput[]>;
|
|
123
|
+
export declare function copyPublicAssets(options: CopyPublicAssetsOptions): Promise<CopyPublicAssetsResult>;
|
|
124
|
+
export declare function buildTailwindUtilitiesCss(input: TailwindUtilitiesInput): Promise<string>;
|
|
125
|
+
export declare function writeTailwindUtilitiesCss(path: string, input: TailwindUtilitiesInput): Promise<boolean>;
|
package/dist/assets.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { copyFile, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, relative, sep } from "node:path";
|
|
3
|
+
import { publicAssetOutputPath } from "./paths.js";
|
|
4
|
+
export function defineClientAssets(assets) {
|
|
5
|
+
return assets;
|
|
6
|
+
}
|
|
7
|
+
export function stylesheet(href, options) {
|
|
8
|
+
return {
|
|
9
|
+
stylesheets: [options ? { href, ...options } : href],
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function googleFontsStylesheet(href, options) {
|
|
13
|
+
return {
|
|
14
|
+
href,
|
|
15
|
+
googleFonts: options,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function script(src, options = {}) {
|
|
19
|
+
return {
|
|
20
|
+
scripts: [{ src, ...options }],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function island(src, options = {}) {
|
|
24
|
+
return script(src, { module: true, ...options });
|
|
25
|
+
}
|
|
26
|
+
export function resolveClientAssets(registry, ids) {
|
|
27
|
+
const stylesheets = [];
|
|
28
|
+
const scripts = [];
|
|
29
|
+
const stylesheetSet = new Set();
|
|
30
|
+
const scriptSet = new Set();
|
|
31
|
+
for (const id of [...new Set(ids)]) {
|
|
32
|
+
const declarations = registry[id];
|
|
33
|
+
if (!declarations) {
|
|
34
|
+
throw new Error(`Unknown client asset: ${id}`);
|
|
35
|
+
}
|
|
36
|
+
for (const declaration of Array.isArray(declarations) ? declarations : [declarations]) {
|
|
37
|
+
for (const item of declaration.stylesheets ?? []) {
|
|
38
|
+
const key = stableStylesheetKey(item);
|
|
39
|
+
if (!stylesheetSet.has(key)) {
|
|
40
|
+
stylesheetSet.add(key);
|
|
41
|
+
stylesheets.push(item);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const item of declaration.scripts ?? []) {
|
|
45
|
+
const key = stableScriptKey(item);
|
|
46
|
+
if (!scriptSet.has(key)) {
|
|
47
|
+
scriptSet.add(key);
|
|
48
|
+
scripts.push(item);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
stylesheets,
|
|
55
|
+
scripts,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function assetsFromViteManifest(manifest, options = {}) {
|
|
59
|
+
const base = options.base ?? "/";
|
|
60
|
+
return defineClientAssets(Object.fromEntries(Object.entries(manifest)
|
|
61
|
+
.filter(([, chunk]) => !options.entriesOnly || chunk.isEntry === true)
|
|
62
|
+
.map(([id, chunk]) => [
|
|
63
|
+
id,
|
|
64
|
+
{
|
|
65
|
+
stylesheets: collectViteChunkCss(manifest, chunk, base),
|
|
66
|
+
scripts: [
|
|
67
|
+
{
|
|
68
|
+
src: joinPublicPath(base, chunk.file),
|
|
69
|
+
module: true,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
])));
|
|
74
|
+
}
|
|
75
|
+
export async function loadAssetsFromViteManifest(path, options = {}) {
|
|
76
|
+
const manifest = JSON.parse(await readFile(path, "utf8"));
|
|
77
|
+
return assetsFromViteManifest(manifest, options);
|
|
78
|
+
}
|
|
79
|
+
export function publicAssetsFromViteManifest(manifest, options) {
|
|
80
|
+
const base = options.base ?? "/";
|
|
81
|
+
const seen = new Set();
|
|
82
|
+
const assets = [];
|
|
83
|
+
for (const chunk of Object.values(manifest)) {
|
|
84
|
+
for (const file of [chunk.file, ...(chunk.css ?? []), ...(chunk.assets ?? [])]) {
|
|
85
|
+
if (seen.has(file)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
seen.add(file);
|
|
89
|
+
assets.push({
|
|
90
|
+
source: join(options.sourceDir, file),
|
|
91
|
+
publicPath: joinPublicPath(base, file),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return assets;
|
|
96
|
+
}
|
|
97
|
+
export async function loadPublicAssetsFromViteManifest(path, options) {
|
|
98
|
+
const manifest = JSON.parse(await readFile(path, "utf8"));
|
|
99
|
+
return publicAssetsFromViteManifest(manifest, options);
|
|
100
|
+
}
|
|
101
|
+
export function viteBuildAssetsFromManifest(manifest, options) {
|
|
102
|
+
return {
|
|
103
|
+
client: assetsFromViteManifest(manifest, options),
|
|
104
|
+
public: publicAssetsFromViteManifest(manifest, options),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export async function loadViteBuildAssetsFromManifest(path, options) {
|
|
108
|
+
const manifest = JSON.parse(await readFile(path, "utf8"));
|
|
109
|
+
return viteBuildAssetsFromManifest(manifest, options);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Recursively walk a directory and expand every file into a `PublicAssetInput`
|
|
113
|
+
* whose `publicPath` mirrors the file's location relative to `dir`. The base of
|
|
114
|
+
* `dir` is the output root, so `public/images/logo.png` becomes
|
|
115
|
+
* `/images/logo.png` and `public/CNAME` becomes `/CNAME`.
|
|
116
|
+
*
|
|
117
|
+
* - Dotfiles and extensionless files (CNAME, .nojekyll) are included.
|
|
118
|
+
* - Nested subdirectories are walked to any depth.
|
|
119
|
+
* - Empty directories produce no entries (only regular files are copied).
|
|
120
|
+
* - Symlinks are not followed: a symlinked directory is not traversed (avoids
|
|
121
|
+
* cycles) and a symlinked file is skipped (its `Dirent` is neither a
|
|
122
|
+
* directory nor a regular file). Materialize the target into `dir` if you
|
|
123
|
+
* need it copied.
|
|
124
|
+
* - A missing `dir` yields an empty list so a build can run before the
|
|
125
|
+
* directory exists.
|
|
126
|
+
*/
|
|
127
|
+
export async function expandPublicDir(dir, options = {}) {
|
|
128
|
+
const base = options.base ?? "/";
|
|
129
|
+
const assets = [];
|
|
130
|
+
const walk = async (current) => {
|
|
131
|
+
let entries;
|
|
132
|
+
try {
|
|
133
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
for (const entry of entries) {
|
|
142
|
+
const absolutePath = join(current, entry.name);
|
|
143
|
+
if (entry.isDirectory()) {
|
|
144
|
+
await walk(absolutePath);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (!entry.isFile()) {
|
|
148
|
+
// Skip symlinks (dir or file), sockets, FIFOs, etc. readdir does not
|
|
149
|
+
// follow symlinks, so a symlinked file's Dirent reports neither
|
|
150
|
+
// isDirectory() nor isFile().
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const relativePath = relative(dir, absolutePath).split(sep).join("/");
|
|
154
|
+
assets.push({
|
|
155
|
+
source: absolutePath,
|
|
156
|
+
publicPath: joinPublicPath(base, relativePath),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
await walk(dir);
|
|
161
|
+
assets.sort((left, right) => left.publicPath.localeCompare(right.publicPath));
|
|
162
|
+
return assets;
|
|
163
|
+
}
|
|
164
|
+
export async function copyPublicAssets(options) {
|
|
165
|
+
const copied = [];
|
|
166
|
+
const skipped = [];
|
|
167
|
+
const outputPathsByPublicPath = new Map();
|
|
168
|
+
for (const asset of options.assets) {
|
|
169
|
+
const outputPath = publicAssetOutputPath(asset.publicPath);
|
|
170
|
+
const normalizedPublicPath = `/${outputPath}`;
|
|
171
|
+
if (outputPathsByPublicPath.has(normalizedPublicPath)) {
|
|
172
|
+
throw new Error(`Duplicate public asset path: ${normalizedPublicPath}`);
|
|
173
|
+
}
|
|
174
|
+
outputPathsByPublicPath.set(normalizedPublicPath, outputPath);
|
|
175
|
+
}
|
|
176
|
+
for (const asset of options.assets) {
|
|
177
|
+
const normalizedPublicPath = `/${publicAssetOutputPath(asset.publicPath)}`;
|
|
178
|
+
const outputPath = outputPathsByPublicPath.get(normalizedPublicPath);
|
|
179
|
+
const absoluteOutputPath = join(options.outDir, outputPath);
|
|
180
|
+
if (await publicAssetContentsEqual(asset, absoluteOutputPath)) {
|
|
181
|
+
skipped.push(asset.publicPath);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
await mkdir(dirname(absoluteOutputPath), { recursive: true });
|
|
185
|
+
if ("source" in asset) {
|
|
186
|
+
await copyFile(asset.source, absoluteOutputPath);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
await writeFile(absoluteOutputPath, publicAssetContentBytes(asset.content));
|
|
190
|
+
}
|
|
191
|
+
copied.push(asset.publicPath);
|
|
192
|
+
}
|
|
193
|
+
return { copied, skipped };
|
|
194
|
+
}
|
|
195
|
+
export async function buildTailwindUtilitiesCss(input) {
|
|
196
|
+
const candidates = [...new Set(input.candidates)].sort();
|
|
197
|
+
const { compile } = await import("tailwindcss");
|
|
198
|
+
const compiler = await compile(`@tailwind utilities;${input.css ?? ""}`);
|
|
199
|
+
return minifyCss(compiler.build(candidates));
|
|
200
|
+
}
|
|
201
|
+
export async function writeTailwindUtilitiesCss(path, input) {
|
|
202
|
+
const css = await buildTailwindUtilitiesCss(input);
|
|
203
|
+
const current = await readOptionalText(path);
|
|
204
|
+
if (current === css) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
await mkdir(dirname(path), { recursive: true });
|
|
208
|
+
await writeFile(path, css, { flag: "w" });
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
function minifyCss(css) {
|
|
212
|
+
return css
|
|
213
|
+
.replace(/\/\*!.*?\*\//gs, "")
|
|
214
|
+
.replace(/\s+/g, " ")
|
|
215
|
+
.replace(/\s*([{}:;,>])\s*/g, "$1")
|
|
216
|
+
.replace(/;}/g, "}")
|
|
217
|
+
.trim();
|
|
218
|
+
}
|
|
219
|
+
async function readOptionalText(path) {
|
|
220
|
+
try {
|
|
221
|
+
return await readFile(path, "utf8");
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async function publicAssetContentsEqual(asset, outputPath) {
|
|
231
|
+
return "source" in asset
|
|
232
|
+
? fileContentsEqual(asset.source, outputPath)
|
|
233
|
+
: bytesEqual(publicAssetContentBytes(asset.content), outputPath);
|
|
234
|
+
}
|
|
235
|
+
function publicAssetContentBytes(content) {
|
|
236
|
+
return typeof content === "string" ? Buffer.from(content) : Buffer.from(content);
|
|
237
|
+
}
|
|
238
|
+
async function fileContentsEqual(leftPath, rightPath) {
|
|
239
|
+
let leftStat;
|
|
240
|
+
let rightStat;
|
|
241
|
+
try {
|
|
242
|
+
[leftStat, rightStat] = await Promise.all([stat(leftPath), stat(rightPath)]);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
if (leftStat.size !== rightStat.size) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
const [left, right] = await Promise.all([readFile(leftPath), readFile(rightPath)]);
|
|
254
|
+
return left.equals(right);
|
|
255
|
+
}
|
|
256
|
+
async function bytesEqual(left, rightPath) {
|
|
257
|
+
let rightStat;
|
|
258
|
+
try {
|
|
259
|
+
rightStat = await stat(rightPath);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
if (left.byteLength !== rightStat.size) {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
const right = await readFile(rightPath);
|
|
271
|
+
return left.equals(right);
|
|
272
|
+
}
|
|
273
|
+
function stableStylesheetKey(stylesheet) {
|
|
274
|
+
return typeof stylesheet === "string"
|
|
275
|
+
? stylesheet
|
|
276
|
+
: "source" in stylesheet
|
|
277
|
+
? JSON.stringify({
|
|
278
|
+
href: stylesheet.href,
|
|
279
|
+
source: stylesheet.source,
|
|
280
|
+
immutable: stylesheet.immutable !== false,
|
|
281
|
+
})
|
|
282
|
+
: JSON.stringify({
|
|
283
|
+
href: stylesheet.href,
|
|
284
|
+
googleFonts: {
|
|
285
|
+
families: stylesheet.googleFonts.families,
|
|
286
|
+
text: stylesheet.googleFonts.text,
|
|
287
|
+
display: stylesheet.googleFonts.display,
|
|
288
|
+
endpoint: stylesheet.googleFonts.endpoint,
|
|
289
|
+
fontBasePath: stylesheet.googleFonts.fontBasePath,
|
|
290
|
+
userAgent: stylesheet.googleFonts.userAgent,
|
|
291
|
+
},
|
|
292
|
+
immutable: stylesheet.immutable !== false,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
function stableScriptKey(script) {
|
|
296
|
+
return JSON.stringify({
|
|
297
|
+
src: script.src,
|
|
298
|
+
source: script.source,
|
|
299
|
+
immutable: script.immutable !== false,
|
|
300
|
+
module: script.module === true,
|
|
301
|
+
defer: script.defer === true,
|
|
302
|
+
async: script.async === true,
|
|
303
|
+
attributes: Object.entries(script.attributes ?? {})
|
|
304
|
+
.filter(([, value]) => value !== null && value !== undefined && value !== false)
|
|
305
|
+
.sort(([left], [right]) => left.localeCompare(right)),
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
function collectViteChunkCss(manifest, chunk, base) {
|
|
309
|
+
const stylesheets = [];
|
|
310
|
+
const stylesheetSet = new Set();
|
|
311
|
+
const chunkSet = new Set();
|
|
312
|
+
const visit = (item) => {
|
|
313
|
+
if (chunkSet.has(item)) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
chunkSet.add(item);
|
|
317
|
+
for (const importId of item.imports ?? []) {
|
|
318
|
+
const importedChunk = manifest[importId];
|
|
319
|
+
if (importedChunk) {
|
|
320
|
+
visit(importedChunk);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
for (const href of item.css ?? []) {
|
|
324
|
+
const publicHref = joinPublicPath(base, href);
|
|
325
|
+
if (!stylesheetSet.has(publicHref)) {
|
|
326
|
+
stylesheetSet.add(publicHref);
|
|
327
|
+
stylesheets.push(publicHref);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
visit(chunk);
|
|
332
|
+
return stylesheets;
|
|
333
|
+
}
|
|
334
|
+
function joinPublicPath(base, file) {
|
|
335
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(file) || file.startsWith("//")) {
|
|
336
|
+
return file;
|
|
337
|
+
}
|
|
338
|
+
const normalizedBase = base.endsWith("/") ? base : `${base}/`;
|
|
339
|
+
const normalizedFile = file.replace(/^\/+/, "");
|
|
340
|
+
return `${normalizedBase}${normalizedFile}`;
|
|
341
|
+
}
|