@moku-labs/web 1.2.0 → 1.3.1

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.
@@ -1853,7 +1853,7 @@ type DataProvider = {
1853
1853
  */
1854
1854
  declare const dataPlugin: import("@moku-labs/core").PluginInstance<"data", DataConfig, DataState, DataProvider, {}> & Record<never, never>;
1855
1855
  declare namespace types_d_exports {
1856
- export { Api, Article, ArticleCard, ComputedFields, Config, ContentApiContext, ContentEvents, ContentProvider, ContentProviderState, FileSystemContentOptions, Frontmatter, State };
1856
+ export { Api, Article, ArticleCard, ComputedFields, Config, ContentApiContext, ContentEvents, ContentProvider, ContentProviderState, FileSystemContentOptions, Frontmatter, LoadAllOptions, State };
1857
1857
  }
1858
1858
  /**
1859
1859
  * YAML frontmatter parsed from each article file.
@@ -2028,11 +2028,18 @@ type Config = {
2028
2028
  *
2029
2029
  * @example
2030
2030
  * ```ts
2031
- * { articles: new Map() }
2031
+ * { articles: new Map(), loadedAll: null }
2032
2032
  * ```
2033
2033
  */
2034
2034
  type State = {
2035
2035
  /** Article cache keyed locale -> (slug -> Article). Starts empty. */articles: Map<string, Map<string, Article>>;
2036
+ /**
2037
+ * Memoized full `loadAll()` result, or `null` when not yet loaded / invalidated. List-route
2038
+ * loaders call `loadAll()` once PER PAGE, so without this every page re-reads + re-renders
2039
+ * every article (the dev-loop killer). The memo makes repeated calls O(1); `invalidate()`
2040
+ * clears it so a dev rebuild reloads (re-resolving only the changed slugs). Starts `null`.
2041
+ */
2042
+ loadedAll: Map<string, Article[]> | null;
2036
2043
  };
2037
2044
  /**
2038
2045
  * Notification-only events emitted by the content plugin.
@@ -2072,6 +2079,26 @@ type ContentApiContext = {
2072
2079
  defaultLocale: () => string; /** The resolved content source (merged from `config.providers`). */
2073
2080
  provider: ContentProvider;
2074
2081
  };
2082
+ /**
2083
+ * Options for {@link Api.loadAll}.
2084
+ *
2085
+ * @example
2086
+ * ```ts
2087
+ * await app.content.loadAll({ reuse: true });
2088
+ * ```
2089
+ */
2090
+ type LoadAllOptions = {
2091
+ /**
2092
+ * Reuse the per-build memo + per-slug cache (re-resolving only slugs a preceding
2093
+ * `invalidate()` dropped). Default `true` — this is what keeps repeated `loadAll()` calls
2094
+ * (a list route's loader runs once per page) cheap, and makes a dev rebuild re-render only
2095
+ * changed articles. Set `false` to force a FRESH full reload (cold build / an
2096
+ * unclassifiable change), which re-reads + re-renders every article and rebuilds the memo.
2097
+ * The post-sort `contentId` ordinals are always recomputed across the full set, so order +
2098
+ * ids match a full load either way.
2099
+ */
2100
+ reuse?: boolean;
2101
+ };
2075
2102
  /**
2076
2103
  * Public API for the content plugin.
2077
2104
  *
@@ -2082,10 +2109,15 @@ type ContentApiContext = {
2082
2109
  */
2083
2110
  type Api = {
2084
2111
  /**
2085
- * Load every article across every active locale, returning a locale-keyed
2086
- * map of date-descending Article arrays. Emits content:ready.
2112
+ * Load every article across every active locale, returning a locale-keyed map of
2113
+ * date-descending Article arrays. Emits content:ready (once per actual load). Cache-first
2114
+ * + memoized: repeated calls (e.g. a list route's loader on every page) return the SAME
2115
+ * cached result with no re-read — so treat the result as READ-ONLY (do not sort/mutate it
2116
+ * in place; slice/copy first). Pass `{ reuse: false }` to force a fresh full reload.
2117
+ *
2118
+ * @param options - Optional load behaviour ({@link LoadAllOptions}); default reuses the cache.
2087
2119
  */
2088
- loadAll(): Promise<Map<string, Article[]>>;
2120
+ loadAll(options?: LoadAllOptions): Promise<Map<string, Article[]>>;
2089
2121
  /**
2090
2122
  * Resolve and render a single article for one locale, with locale fallback.
2091
2123
  *
package/dist/browser.mjs CHANGED
@@ -4355,18 +4355,24 @@ function isPublished(article, isProduction) {
4355
4355
  * locale collection: existing files only, drafts dropped in production, sorted
4356
4356
  * date-descending. The single load+filter+sort step behind {@link createContentApi.loadAll}.
4357
4357
  *
4358
+ * When a `cached` map is supplied (incremental dev rebuild), a slug already present in it
4359
+ * is reused as-is (skipping the re-read + Markdown/Shiki re-render); only slugs absent
4360
+ * from it — the ones a preceding `invalidate()` dropped, plus any never-loaded — are
4361
+ * resolved fresh. With no `cached` map every slug is resolved (the full load).
4362
+ *
4358
4363
  * @param ctx - Kernel-free domain context (provider + i18n helpers + stage).
4359
4364
  * @param slugs - Every known article slug from the provider.
4360
4365
  * @param locale - The locale to resolve and collect.
4366
+ * @param cached - Optional per-locale article cache to reuse for unchanged slugs.
4361
4367
  * @returns The published (date-descending) articles for this locale.
4362
4368
  * @example
4363
4369
  * ```ts
4364
4370
  * const present = await loadAndFilterArticles(ctx, slugs, "en");
4365
4371
  * ```
4366
4372
  */
4367
- async function loadAndFilterArticles(ctx, slugs, locale) {
4373
+ async function loadAndFilterArticles(ctx, slugs, locale, cached) {
4368
4374
  const isProduction = ctx.global.stage === "production";
4369
- return (await Promise.all(slugs.map((slug) => resolveArticle(ctx, slug, locale)))).filter((article) => article !== null).filter((article) => isPublished(article, isProduction)).toSorted(byDateDescending);
4375
+ return (await Promise.all(slugs.map((slug) => cached?.get(slug) ?? resolveArticle(ctx, slug, locale)))).filter((article) => article !== null).filter((article) => isPublished(article, isProduction)).toSorted(byDateDescending);
4370
4376
  }
4371
4377
  /**
4372
4378
  * Derive the article slug from a source file path — the parent directory name
@@ -4403,19 +4409,31 @@ function createContentApi(ctx) {
4403
4409
  * Load every article across every active locale (locale fallback, production
4404
4410
  * draft exclusion, date sort, `contentId` after sort), cache them, emit `content:ready`.
4405
4411
  *
4412
+ * Cache-first by default: repeated calls return the per-build memo (list-route loaders
4413
+ * call this once PER PAGE — without the memo every page would re-read + re-render every
4414
+ * article, the dev-loop killer), and a rebuild after `invalidate()` re-resolves only the
4415
+ * dropped slugs while reusing the cached articles for the rest (`contentId` ordinals are
4416
+ * still recomputed across the FULL sorted set, so ids + order match a full load). Pass
4417
+ * `{ reuse: false }` to force a FRESH full reload (cold build / an unclassifiable change
4418
+ * where the caller cannot pinpoint what changed) — this bypasses the memo + per-slug cache.
4419
+ *
4420
+ * @param options - Optional load behaviour (`reuse`, default `true`).
4406
4421
  * @returns A locale-keyed map of date-descending articles.
4407
4422
  * @example
4408
4423
  * ```ts
4409
4424
  * const byLocale = await api.loadAll();
4410
4425
  * ```
4411
4426
  */
4412
- async loadAll() {
4427
+ async loadAll(options) {
4428
+ const reuse = options?.reuse !== false;
4429
+ const memo = ctx.state.loadedAll;
4430
+ if (reuse && memo !== null) return memo;
4413
4431
  const slugs = await ctx.provider.slugs();
4414
4432
  const locales = ctx.locales();
4415
4433
  const result = /* @__PURE__ */ new Map();
4416
4434
  let total = 0;
4417
4435
  for (const locale of locales) {
4418
- const present = await loadAndFilterArticles(ctx, slugs, locale);
4436
+ const present = await loadAndFilterArticles(ctx, slugs, locale, reuse ? ctx.state.articles.get(locale) : void 0);
4419
4437
  const cache = /* @__PURE__ */ new Map();
4420
4438
  let index = 0;
4421
4439
  for (const article of present) {
@@ -4427,6 +4445,7 @@ function createContentApi(ctx) {
4427
4445
  result.set(locale, present);
4428
4446
  total += present.length;
4429
4447
  }
4448
+ ctx.state.loadedAll = result;
4430
4449
  ctx.emit("content:ready", {
4431
4450
  locales,
4432
4451
  articleCount: total
@@ -4490,6 +4509,7 @@ function createContentApi(ctx) {
4490
4509
  if (slug === void 0) continue;
4491
4510
  for (const cache of ctx.state.articles.values()) cache.delete(slug);
4492
4511
  }
4512
+ if (accepted.length > 0) ctx.state.loadedAll = null;
4493
4513
  ctx.emit("content:invalidated", { paths: accepted });
4494
4514
  },
4495
4515
  /**
@@ -4559,14 +4579,17 @@ const contentEvents = (register) => ({
4559
4579
  * @param _ctx - Minimal context with global and config.
4560
4580
  * @param _ctx.global - Global plugin registry.
4561
4581
  * @param _ctx.config - Resolved plugin configuration.
4562
- * @returns Fresh content shell state: an empty article cache.
4582
+ * @returns Fresh content shell state: an empty article cache + an empty loadAll memo.
4563
4583
  * @example
4564
4584
  * ```ts
4565
4585
  * const state = createContentState({ global: {}, config: { providers: [] } });
4566
4586
  * ```
4567
4587
  */
4568
4588
  function createContentState(_ctx) {
4569
- return { articles: /* @__PURE__ */ new Map() };
4589
+ return {
4590
+ articles: /* @__PURE__ */ new Map(),
4591
+ loadedAll: null
4592
+ };
4570
4593
  }
4571
4594
  //#endregion
4572
4595
  //#region src/plugins/content/validate.ts