@haverstack/eleventy 0.2.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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +91 -0
  3. package/dist/assets.d.ts +52 -0
  4. package/dist/assets.d.ts.map +1 -0
  5. package/dist/assets.js +148 -0
  6. package/dist/assets.js.map +1 -0
  7. package/dist/check.d.ts +35 -0
  8. package/dist/check.d.ts.map +1 -0
  9. package/dist/check.js +141 -0
  10. package/dist/check.js.map +1 -0
  11. package/dist/cli.d.ts +15 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +122 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/emit.d.ts +72 -0
  16. package/dist/emit.d.ts.map +1 -0
  17. package/dist/emit.js +147 -0
  18. package/dist/emit.js.map +1 -0
  19. package/dist/errors.d.ts +12 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +15 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/feeds.d.ts +24 -0
  24. package/dist/feeds.d.ts.map +1 -0
  25. package/dist/feeds.js +103 -0
  26. package/dist/feeds.js.map +1 -0
  27. package/dist/index.d.ts +94 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +73 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/load.d.ts +86 -0
  32. package/dist/load.d.ts.map +1 -0
  33. package/dist/load.js +258 -0
  34. package/dist/load.js.map +1 -0
  35. package/dist/markdown.d.ts +23 -0
  36. package/dist/markdown.d.ts.map +1 -0
  37. package/dist/markdown.js +81 -0
  38. package/dist/markdown.js.map +1 -0
  39. package/dist/publish.d.ts +45 -0
  40. package/dist/publish.d.ts.map +1 -0
  41. package/dist/publish.js +79 -0
  42. package/dist/publish.js.map +1 -0
  43. package/dist/resolve.d.ts +119 -0
  44. package/dist/resolve.d.ts.map +1 -0
  45. package/dist/resolve.js +411 -0
  46. package/dist/resolve.js.map +1 -0
  47. package/dist/scaffold.d.ts +22 -0
  48. package/dist/scaffold.d.ts.map +1 -0
  49. package/dist/scaffold.js +98 -0
  50. package/dist/scaffold.js.map +1 -0
  51. package/dist/templates.d.ts +34 -0
  52. package/dist/templates.d.ts.map +1 -0
  53. package/dist/templates.js +171 -0
  54. package/dist/templates.js.map +1 -0
  55. package/dist/types.d.ts +96 -0
  56. package/dist/types.d.ts.map +1 -0
  57. package/dist/types.js +123 -0
  58. package/dist/types.js.map +1 -0
  59. package/package.json +72 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Haverstack, LLC.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # `@haverstack/eleventy`
2
+
3
+ An Eleventy plugin that builds a static site from a [Haverstack](https://github.com/haverstack/core)
4
+ stack. Records are the content source; there are no content files on disk.
5
+
6
+ > **Status:** Early development, but the whole pipeline works — load, resolve, asset
7
+ > staging, markdown, emit (pages, collections, feeds, sitemap), built-in templates that a
8
+ > site can override with its own, and the `check` / `publish` commands.
9
+ > [`docs/design.md`](./docs/design.md) covers how it works and why.
10
+
11
+ ## Usage
12
+
13
+ ```js
14
+ import { haverstack } from '@haverstack/eleventy';
15
+ import { LocalAdapter } from '@haverstack/adapter-local';
16
+ import { Stack } from '@haverstack/core';
17
+
18
+ export default async function (eleventyConfig) {
19
+ const stack = await Stack.create(await LocalAdapter.open({ path: './stack.db' }));
20
+
21
+ eleventyConfig.addPlugin(haverstack, {
22
+ stack,
23
+ site: 'personal',
24
+ assetDir: '_stack-assets',
25
+ slugStrategy: 'title',
26
+ });
27
+ }
28
+ ```
29
+
30
+ The plugin takes a `Stack`, not a URL — so a build can run against a remote server
31
+ (`APIAdapter`), a local SQLite file (`LocalAdapter`), or an in-memory stack
32
+ (`MemoryAdapter`, for tests). Whoever constructs the stack has already dealt with auth;
33
+ the plugin never handles credentials and never writes.
34
+
35
+ Out of the box it renders a legible site with its built-in templates. The pages are
36
+ ordinary Eleventy pages: transforms and global data apply, and every listed page/member
37
+ carries `tags` (`haverstack`, its type, its tag associations) and `eleventyNavigation`,
38
+ so `collections.*`, navigation plugins, and anything reading `collections.all` see them.
39
+
40
+ To use your own layouts, map `template` names to files in your `_includes`:
41
+
42
+ ```js
43
+ eleventyConfig.addPlugin(haverstack, {
44
+ stack,
45
+ site: 'personal',
46
+ templates: { base: 'layouts/base', listing: 'layouts/index', article: 'layouts/longform' },
47
+ });
48
+ ```
49
+
50
+ A mapped page emits its content fragment (`{{ content }}`) plus `record`, `meta`, `url`,
51
+ `canonical`, `body` / `bodyRaw`, `collection`, and the `haverstack` globals; the layout
52
+ does the rest and may chain to another. `base` catches any unmapped name; anything still
53
+ unmapped keeps the built-in render. `haverstack-eleventy eject` writes a starter
54
+ `haverstack-base.njk` to get going. Other options: `eleventyCollections: false` for full
55
+ isolation, `pageData: (ctx) => ({…})` to compute extra template data per record.
56
+
57
+ ## Types
58
+
59
+ **Consumes** the commons publishing types — `site@1`, `page@1`, `article@1`, `post@1`,
60
+ `photo@1`, `bookmark@1` — plus `_entity@1` and `_attachment@1`.
61
+
62
+ **Owns** two sidecar types, minted under `org.haverstack.eleventy`:
63
+
64
+ | Type | Role |
65
+ | ------------------------------------- | ----------------------------------------------------------------------------------------------------- |
66
+ | `org.haverstack.eleventy/page-meta@1` | Per-record, per-site overrides: `slug`, `template`, `order`, `hidden`, `draft`. Every field optional. |
67
+ | `org.haverstack.eleventy/menu@1` | A named, ordered navigation menu belonging to one site. |
68
+
69
+ `defineEleventyTypes(stack)` registers the whole set. It is idempotent and safe to call
70
+ on every build; under a non-owner credential it tolerates types that already exist.
71
+
72
+ ## Commands
73
+
74
+ ```
75
+ haverstack-eleventy check load + resolve, report structural problems, write nothing
76
+ haverstack-eleventy publish stamp article.url / post.url with the canonical location
77
+ haverstack-eleventy eject write a starter haverstack-base layout into _includes
78
+ ```
79
+
80
+ `check` and `publish` read the stack from a config module (`--config`, default
81
+ `./haverstack.config.mjs`) that default-exports a `Stack`, a `{ stack, site? }`, or a
82
+ function returning one — the same place a project builds the stack for
83
+ `eleventy.config.js`. `--site <handle>` overrides the config's site.
84
+
85
+ `check` is read-only and safe against production data; it exits non-zero when it finds a
86
+ build failure (a path collision, conflicting sidecars). `publish` writes — run it
87
+ deliberately after a good build; `--dry-run` shows what it would stamp.
88
+
89
+ ## License
90
+
91
+ [MIT](./LICENSE)
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @haverstack/eleventy — asset staging
3
+ * -------------------------------------------------------
4
+ * Eleventy does not emit binaries well, so attachment bytes are staged to
5
+ * disk before the build proper and passed through. Content addressing
6
+ * makes the cache trivially correct: a `fileId` already on disk is never
7
+ * refetched, and it can never go stale.
8
+ *
9
+ * `collectAssets` is pure — it decides which files a site needs and what
10
+ * they will be named. `stageAssets` does the fetching and writing.
11
+ *
12
+ * See docs/design.md § Stage assets.
13
+ */
14
+ import type { FileId, RecordId, StackClient } from '@haverstack/core';
15
+ import type { StackIndex } from './load.js';
16
+ import type { ResolvedSite, ResolveWarning } from './resolve.js';
17
+ export interface StagedFile {
18
+ fileId: FileId;
19
+ /** `<fileId><ext>` — the name on disk and in the URL. */
20
+ fileName: string;
21
+ /** Site-root-relative URL, e.g. `/_stack-assets/<fileId>.png`. */
22
+ assetPath: string;
23
+ mimeType: string;
24
+ }
25
+ export interface AssetPlan {
26
+ /** As configured, e.g. `_stack-assets`. */
27
+ assetDir: string;
28
+ /** Every file this site references, by fileId. */
29
+ files: Map<FileId, StagedFile>;
30
+ /** Per record: the body filename → staged asset path map for embed substitution. */
31
+ embedsByRecord: Map<RecordId, Map<string, string>>;
32
+ warnings: ResolveWarning[];
33
+ }
34
+ /**
35
+ * Walk every record this site builds — pages and members, listed or not —
36
+ * and collect the attachment files it references, by `embed` association
37
+ * and by `file-ref` content field. Filenames for embed substitution are
38
+ * resolved per record, never globally: two records can each carry an
39
+ * `image.png` with different bytes.
40
+ */
41
+ export declare function collectAssets(resolved: ResolvedSite, index: StackIndex, assetDir: string): AssetPlan;
42
+ export interface StageResult {
43
+ written: number;
44
+ skipped: number;
45
+ }
46
+ /**
47
+ * Fetch and write every file in `plan` into `outDir`, skipping any that
48
+ * are already there — the content-addressed cache. `outDir` is the
49
+ * on-disk location of `plan.assetDir`. Downloads run in a bounded pool.
50
+ */
51
+ export declare function stageAssets(stack: StackClient, plan: AssetPlan, outDir: string): Promise<StageResult>;
52
+ //# sourceMappingURL=assets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../src/assets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAe,MAAM,kBAAkB,CAAC;AAGnF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AA2BjE,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,QAAQ,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAC/B,oFAAoF;IACpF,cAAc,EAAE,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,QAAQ,EAAE,cAAc,EAAE,CAAC;CAC5B;AAmBD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,YAAY,EACtB,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,MAAM,GACf,SAAS,CAsDX;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAKD;;;;GAIG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,WAAW,EAClB,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,WAAW,CAAC,CAqBtB"}
package/dist/assets.js ADDED
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @haverstack/eleventy — asset staging
3
+ * -------------------------------------------------------
4
+ * Eleventy does not emit binaries well, so attachment bytes are staged to
5
+ * disk before the build proper and passed through. Content addressing
6
+ * makes the cache trivially correct: a `fileId` already on disk is never
7
+ * refetched, and it can never go stale.
8
+ *
9
+ * `collectAssets` is pure — it decides which files a site needs and what
10
+ * they will be named. `stageAssets` does the fetching and writing.
11
+ *
12
+ * See docs/design.md § Stage assets.
13
+ */
14
+ import { existsSync } from 'node:fs';
15
+ import { mkdir, writeFile } from 'node:fs/promises';
16
+ import { join } from 'node:path';
17
+ import { PHOTO } from '@haverstack/commons';
18
+ import { EMBED_LABEL } from './types.js';
19
+ const baseId = (typeId) => typeId.split('@')[0];
20
+ const EXT_BY_MIME = {
21
+ 'image/png': '.png',
22
+ 'image/jpeg': '.jpg',
23
+ 'image/gif': '.gif',
24
+ 'image/webp': '.webp',
25
+ 'image/avif': '.avif',
26
+ 'image/svg+xml': '.svg',
27
+ 'application/pdf': '.pdf',
28
+ 'video/mp4': '.mp4',
29
+ 'audio/mpeg': '.mp3',
30
+ 'text/plain': '.txt',
31
+ };
32
+ function extForMime(mime) {
33
+ const known = EXT_BY_MIME[mime];
34
+ if (known)
35
+ return known;
36
+ const sub = mime
37
+ .split('/')[1]
38
+ ?.split('+')[0]
39
+ ?.replace(/[^a-z0-9.-]/gi, '');
40
+ return sub ? `.${sub}` : '.bin';
41
+ }
42
+ function fileRefsIn(record) {
43
+ // The only `file-ref` content field in the commons publishing types.
44
+ // Custom types with their own file-ref fields are not scanned yet.
45
+ if (baseId(record.typeId) === baseId(PHOTO.id) && typeof record.content.image === 'string') {
46
+ return [record.content.image];
47
+ }
48
+ return [];
49
+ }
50
+ function embedFileIds(record) {
51
+ const out = [];
52
+ for (const a of record.associations ?? []) {
53
+ if (a.kind === 'attachment' && a.label === EMBED_LABEL)
54
+ out.push(a.fileId);
55
+ }
56
+ return out;
57
+ }
58
+ /**
59
+ * Walk every record this site builds — pages and members, listed or not —
60
+ * and collect the attachment files it references, by `embed` association
61
+ * and by `file-ref` content field. Filenames for embed substitution are
62
+ * resolved per record, never globally: two records can each carry an
63
+ * `image.png` with different bytes.
64
+ */
65
+ export function collectAssets(resolved, index, assetDir) {
66
+ const files = new Map();
67
+ const embedsByRecord = new Map();
68
+ const warnings = [];
69
+ const dir = assetDir.replace(/^\/+|\/+$/g, '');
70
+ const stage = (fileId, forRecord) => {
71
+ const meta = index.attachmentsByFileId.get(fileId)?.[0];
72
+ if (!meta) {
73
+ warnings.push({
74
+ kind: 'attachment-missing-metadata',
75
+ recordId: forRecord,
76
+ message: `Record ${forRecord} references file ${fileId}, which has no _attachment@1 record; skipped.`,
77
+ });
78
+ return null;
79
+ }
80
+ let staged = files.get(fileId);
81
+ if (!staged) {
82
+ const mimeType = String(meta.content.mimeType ?? 'application/octet-stream');
83
+ const fileName = `${fileId}${extForMime(mimeType)}`;
84
+ staged = { fileId, fileName, assetPath: `/${dir}/${fileName}`, mimeType };
85
+ files.set(fileId, staged);
86
+ }
87
+ return staged;
88
+ };
89
+ const visit = (record) => {
90
+ for (const fileId of fileRefsIn(record))
91
+ stage(fileId, record.id);
92
+ const embedMap = new Map();
93
+ for (const fileId of embedFileIds(record)) {
94
+ const staged = stage(fileId, record.id);
95
+ if (!staged)
96
+ continue;
97
+ // One fileId can have several _attachment@1 records (one per upload),
98
+ // each with its own filename. The body might reference any of them,
99
+ // so every known name points at the one staged path.
100
+ for (const meta of index.attachmentsByFileId.get(fileId) ?? []) {
101
+ const filename = meta.content.filename;
102
+ if (typeof filename === 'string' && filename)
103
+ embedMap.set(filename, staged.assetPath);
104
+ }
105
+ }
106
+ if (embedMap.size > 0)
107
+ embedsByRecord.set(record.id, embedMap);
108
+ };
109
+ const walkPages = (pages) => {
110
+ for (const page of pages) {
111
+ visit(page.record);
112
+ walkPages(page.children);
113
+ }
114
+ };
115
+ walkPages(resolved.pages);
116
+ for (const member of resolved.members)
117
+ visit(member.record);
118
+ return { assetDir: dir, files, embedsByRecord, warnings };
119
+ }
120
+ /** How many attachments to fetch at once — a remote stack is the case this bounds. */
121
+ const DOWNLOAD_CONCURRENCY = 6;
122
+ /**
123
+ * Fetch and write every file in `plan` into `outDir`, skipping any that
124
+ * are already there — the content-addressed cache. `outDir` is the
125
+ * on-disk location of `plan.assetDir`. Downloads run in a bounded pool.
126
+ */
127
+ export async function stageAssets(stack, plan, outDir) {
128
+ await mkdir(outDir, { recursive: true });
129
+ const toFetch = [];
130
+ let skipped = 0;
131
+ for (const file of plan.files.values()) {
132
+ if (existsSync(join(outDir, file.fileName)))
133
+ skipped++;
134
+ else
135
+ toFetch.push(file);
136
+ }
137
+ let next = 0;
138
+ const worker = async () => {
139
+ while (next < toFetch.length) {
140
+ const file = toFetch[next++];
141
+ const bytes = await stack.getAttachment(file.fileId);
142
+ await writeFile(join(outDir, file.fileName), bytes);
143
+ }
144
+ };
145
+ await Promise.all(Array.from({ length: Math.min(DOWNLOAD_CONCURRENCY, toFetch.length) }, worker));
146
+ return { written: toFetch.length, skipped };
147
+ }
148
+ //# sourceMappingURL=assets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assets.js","sourceRoot":"","sources":["../src/assets.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @haverstack/eleventy — the `check` command
3
+ * -------------------------------------------------------
4
+ * Load + resolve, report structural problems, write and emit nothing.
5
+ * Read-only and safe against production data. These are the faults an
6
+ * editing tool structurally cannot catch, because they are only defined
7
+ * relative to a build: a slug collides only relative to a path, a sidecar
8
+ * is misdirected only relative to which sites a record is published on.
9
+ *
10
+ * See docs/design.md § check.
11
+ */
12
+ import type { StackClient } from '@haverstack/core';
13
+ import { type SlugStrategy } from './resolve.js';
14
+ export interface CheckItem {
15
+ kind: string;
16
+ message: string;
17
+ }
18
+ export interface CheckReport {
19
+ /** The resolved site's handle, or `null` for a single-site stack. */
20
+ site: string | null;
21
+ /** Faults that would also fail a build. */
22
+ failures: CheckItem[];
23
+ /** Problems a build tolerates. */
24
+ warnings: CheckItem[];
25
+ /** Not problems — an audit of what the build did with edge-case records. */
26
+ info: CheckItem[];
27
+ }
28
+ export interface CheckOptions {
29
+ site?: string;
30
+ slugStrategy?: SlugStrategy;
31
+ }
32
+ export declare function runCheck(stack: StackClient, opts?: CheckOptions): Promise<CheckReport>;
33
+ export declare function formatCheckReport(report: CheckReport): string;
34
+ export declare const checkExitCode: (report: CheckReport) => number;
35
+ //# sourceMappingURL=check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAgB,WAAW,EAAe,MAAM,kBAAkB,CAAC;AAG/E,OAAO,EAA8B,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAK7E,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,qEAAqE;IACrE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,2CAA2C;IAC3C,QAAQ,EAAE,SAAS,EAAE,CAAC;IACtB,kCAAkC;IAClC,QAAQ,EAAE,SAAS,EAAE,CAAC;IACtB,4EAA4E;IAC5E,IAAI,EAAE,SAAS,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AA8BD,wBAAsB,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAiFhG;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAuB7D;AAED,eAAO,MAAM,aAAa,GAAI,QAAQ,WAAW,KAAG,MAA8C,CAAC"}
package/dist/check.js ADDED
@@ -0,0 +1,141 @@
1
+ /**
2
+ * @haverstack/eleventy — the `check` command
3
+ * -------------------------------------------------------
4
+ * Load + resolve, report structural problems, write and emit nothing.
5
+ * Read-only and safe against production data. These are the faults an
6
+ * editing tool structurally cannot catch, because they are only defined
7
+ * relative to a build: a slug collides only relative to a path, a sidecar
8
+ * is misdirected only relative to which sites a record is published on.
9
+ *
10
+ * See docs/design.md § check.
11
+ */
12
+ import { ARTICLE, BOOKMARK, PHOTO, POST } from '@haverstack/commons';
13
+ import { load } from './load.js';
14
+ import { resolve } from './resolve.js';
15
+ import { SITE_MEMBERSHIP_LABEL } from './types.js';
16
+ const MEMBER_TYPES = [ARTICLE.id, POST.id, PHOTO.id, BOOKMARK.id];
17
+ async function queryAll(stack, filter) {
18
+ const out = [];
19
+ let cursor;
20
+ do {
21
+ const page = await stack.query({ filter, cursor, limit: 100 });
22
+ out.push(...page.records);
23
+ cursor = page.cursor ?? undefined;
24
+ } while (cursor);
25
+ return out;
26
+ }
27
+ const siteMembershipIds = (record) => {
28
+ const out = [];
29
+ for (const a of record.associations ?? []) {
30
+ if (a.kind === 'relationship' &&
31
+ a.label === SITE_MEMBERSHIP_LABEL &&
32
+ a.target.scope === 'record') {
33
+ out.push(a.target.recordId);
34
+ }
35
+ }
36
+ return out;
37
+ };
38
+ const title = (r) => String(r.content.title ?? r.content.caption ?? r.content.text ?? r.id).slice(0, 60);
39
+ export async function runCheck(stack, opts = {}) {
40
+ const index = await load(stack, { site: opts.site });
41
+ const resolved = resolve(index, {
42
+ slugStrategy: opts.slugStrategy ?? 'title',
43
+ strict: false,
44
+ });
45
+ const failures = resolved.errors.map((e) => ({ kind: e.kind, message: e.message }));
46
+ const warnings = resolved.warnings.map((w) => ({
47
+ kind: w.kind,
48
+ message: w.message,
49
+ }));
50
+ const info = [];
51
+ // A sidecar scoped to a site the record it describes isn't published on.
52
+ for (const [parentId, set] of index.sidecarsByParent) {
53
+ const record = index.byId.get(parentId);
54
+ if (!record)
55
+ continue;
56
+ const memberships = new Set(siteMembershipIds(record));
57
+ for (const scopedSiteId of set.bySite.keys()) {
58
+ if (memberships.has(scopedSiteId))
59
+ continue;
60
+ const scopedSite = index.sitesById.get(scopedSiteId);
61
+ warnings.push({
62
+ kind: 'sidecar-for-unpublished-site',
63
+ message: `Record ${parentId} ("${title(record)}") has a page-meta scoped to ` +
64
+ `${scopedSite ? `site "${scopedSite.content.handle}"` : scopedSiteId}, ` +
65
+ `which it carries no membership association for — usually a forgotten \`site\` association.`,
66
+ });
67
+ }
68
+ }
69
+ // Member records carrying no `site` association at all.
70
+ for (const typeId of MEMBER_TYPES) {
71
+ for (const record of await queryAll(stack, {
72
+ typeId,
73
+ ...(index.unlistedVisible ? { includeUnlisted: true } : {}),
74
+ })) {
75
+ if (siteMembershipIds(record).length === 0) {
76
+ warnings.push({
77
+ kind: 'record-on-no-site',
78
+ message: `${record.id} (${record.typeId}) "${title(record)}" is published on no site.`,
79
+ });
80
+ }
81
+ }
82
+ }
83
+ // Unlisted records built this run — nothing else in the output points at them.
84
+ if (!index.unlistedVisible) {
85
+ info.push({
86
+ kind: 'unlisted-not-visible',
87
+ message: 'Running under a credential that cannot see unlisted records; any built this run ' +
88
+ 'are missing and cannot be reported.',
89
+ });
90
+ }
91
+ else {
92
+ const report = (url, record, meta) => {
93
+ const mechanism = record.unlistedAt ? 'unlistedAt' : meta.hidden ? 'hidden' : 'unknown';
94
+ info.push({
95
+ kind: 'unlisted-built',
96
+ message: `${url} (${mechanism}) "${title(record)}"`,
97
+ });
98
+ };
99
+ const walk = (pages) => {
100
+ for (const page of pages) {
101
+ if (page.unlisted)
102
+ report(page.url, page.record, page.meta);
103
+ walk(page.children);
104
+ }
105
+ };
106
+ walk(resolved.pages);
107
+ for (const member of resolved.members) {
108
+ if (member.unlisted)
109
+ report(member.url, member.record, member.meta);
110
+ }
111
+ }
112
+ return {
113
+ site: resolved.site ? resolved.site.content.handle : null,
114
+ failures,
115
+ warnings,
116
+ info,
117
+ };
118
+ }
119
+ export function formatCheckReport(report) {
120
+ const lines = [];
121
+ const where = report.site ? `site "${report.site}"` : 'the single site';
122
+ lines.push(`Checked ${where}.\n`);
123
+ const section = (label, items) => {
124
+ if (items.length === 0)
125
+ return;
126
+ lines.push(`${label} (${items.length})`);
127
+ for (const item of items) {
128
+ lines.push(item.message.includes('\n') ? item.message : ` - ${item.message}`);
129
+ }
130
+ lines.push('');
131
+ };
132
+ section('FAIL', report.failures);
133
+ section('WARN', report.warnings);
134
+ section('INFO', report.info);
135
+ lines.push(report.failures.length > 0
136
+ ? `${report.failures.length} failure(s), ${report.warnings.length} warning(s).`
137
+ : `OK — no failures, ${report.warnings.length} warning(s).`);
138
+ return lines.join('\n');
139
+ }
140
+ export const checkExitCode = (report) => (report.failures.length > 0 ? 1 : 0);
141
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.js","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @haverstack/eleventy — the `haverstack-eleventy` CLI
4
+ * -------------------------------------------------------
5
+ * Two commands that sit alongside the build:
6
+ *
7
+ * haverstack-eleventy check load + resolve, report problems, write nothing
8
+ * haverstack-eleventy publish stamp canonical URLs on articles and posts
9
+ *
10
+ * Both need a stack. They read it from a config module — the same place a
11
+ * project already builds one for `eleventy.config.js` — so the plugin
12
+ * package never has to know about adapters or credentials.
13
+ */
14
+ export {};
15
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;GAWG"}
package/dist/cli.js ADDED
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @haverstack/eleventy — the `haverstack-eleventy` CLI
4
+ * -------------------------------------------------------
5
+ * Two commands that sit alongside the build:
6
+ *
7
+ * haverstack-eleventy check load + resolve, report problems, write nothing
8
+ * haverstack-eleventy publish stamp canonical URLs on articles and posts
9
+ *
10
+ * Both need a stack. They read it from a config module — the same place a
11
+ * project already builds one for `eleventy.config.js` — so the plugin
12
+ * package never has to know about adapters or credentials.
13
+ */
14
+ import { existsSync } from 'node:fs';
15
+ import { resolve as resolvePath } from 'node:path';
16
+ import { parseArgs } from 'node:util';
17
+ import { pathToFileURL } from 'node:url';
18
+ import { checkExitCode, formatCheckReport, runCheck } from './check.js';
19
+ import { formatPublishReport, runPublish } from './publish.js';
20
+ import { formatEjectResult, runEject } from './scaffold.js';
21
+ const USAGE = `haverstack-eleventy <command> [options]
22
+
23
+ Commands:
24
+ check Load and resolve the site; report structural problems. Writes nothing.
25
+ publish Stamp canonical URLs on articles and posts that lack one.
26
+ eject Write a starter haverstack-base layout into _includes for editing.
27
+
28
+ Options:
29
+ --config <path> check/publish: module providing the stack
30
+ (default: ./haverstack.config.mjs). Default-exports a Stack,
31
+ a { stack, site? }, or a function returning one.
32
+ --site <handle> Site to build. Overrides the config's default.
33
+ --slug-strategy <s> 'title' (default) or 'recordId'.
34
+ --dry-run publish: show what would be written, write nothing.
35
+ --includes-dir <path> eject: where to write (default: _includes).
36
+ --force eject: overwrite existing files.
37
+ -h, --help
38
+ `;
39
+ async function loadStack(configPath) {
40
+ const abs = resolvePath(process.cwd(), configPath);
41
+ if (!existsSync(abs)) {
42
+ throw new Error(`Config not found: ${configPath}\n` +
43
+ 'Create one that default-exports your Stack (or { stack, site }), or pass --config.');
44
+ }
45
+ const mod = (await import(pathToFileURL(abs).href));
46
+ let value = mod.default ?? mod.stack ?? mod;
47
+ if (typeof value === 'function')
48
+ value = await value();
49
+ const asRecord = value;
50
+ if (asRecord && typeof asRecord.stack === 'object' && asRecord.stack) {
51
+ return { stack: asRecord.stack, site: asRecord.site };
52
+ }
53
+ if (asRecord && typeof asRecord.query === 'function') {
54
+ return { stack: value };
55
+ }
56
+ throw new Error('Config must default-export a Stack, a { stack, site? }, or a function returning one.');
57
+ }
58
+ async function main() {
59
+ const { values, positionals } = parseArgs({
60
+ allowPositionals: true,
61
+ options: {
62
+ config: { type: 'string' },
63
+ site: { type: 'string' },
64
+ 'slug-strategy': { type: 'string' },
65
+ 'dry-run': { type: 'boolean' },
66
+ 'includes-dir': { type: 'string' },
67
+ force: { type: 'boolean' },
68
+ help: { type: 'boolean', short: 'h' },
69
+ },
70
+ });
71
+ const command = positionals[0];
72
+ if (values.help || !command) {
73
+ process.stdout.write(USAGE);
74
+ process.exit(values.help ? 0 : 1);
75
+ }
76
+ if (command !== 'check' && command !== 'publish' && command !== 'eject') {
77
+ process.stderr.write(`Unknown command: ${command}\n\n${USAGE}`);
78
+ process.exit(1);
79
+ }
80
+ if (command === 'eject') {
81
+ const dir = values['includes-dir'] ?? '_includes';
82
+ const result = await runEject(dir, { force: Boolean(values.force) });
83
+ process.stdout.write(`${formatEjectResult(result, dir)}\n`);
84
+ process.exit(0);
85
+ }
86
+ const strategy = values['slug-strategy'];
87
+ if (strategy && strategy !== 'title' && strategy !== 'recordId') {
88
+ process.stderr.write('--slug-strategy must be "title" or "recordId".\n');
89
+ process.exit(1);
90
+ }
91
+ const slugStrategy = (strategy ?? 'title');
92
+ const { stack, site: configSite } = await loadStack(values.config ?? './haverstack.config.mjs');
93
+ const site = values.site ?? configSite;
94
+ try {
95
+ if (command === 'check') {
96
+ const report = await runCheck(stack, { site, slugStrategy });
97
+ process.stdout.write(`${formatCheckReport(report)}\n`);
98
+ await stack.close();
99
+ process.exit(checkExitCode(report));
100
+ }
101
+ else {
102
+ const report = await runPublish(stack, {
103
+ site,
104
+ slugStrategy,
105
+ dryRun: Boolean(values['dry-run']),
106
+ });
107
+ process.stdout.write(`${formatPublishReport(report)}\n`);
108
+ await stack.close();
109
+ process.exit(0);
110
+ }
111
+ }
112
+ catch (err) {
113
+ process.stderr.write(`\n${err instanceof Error ? err.message : String(err)}\n`);
114
+ await stack.close().catch(() => { });
115
+ process.exit(1);
116
+ }
117
+ }
118
+ main().catch((err) => {
119
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
120
+ process.exit(1);
121
+ });
122
+ //# sourceMappingURL=cli.js.map