@kirchdev/duxt 0.5.0 → 0.5.1-dev-main.48-5d5fca9

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 (79) hide show
  1. package/README.md +3 -3
  2. package/app/components/DuxtBrand.vue +1 -1
  3. package/app/components/DuxtBrunoEntries.vue +1 -1
  4. package/app/components/DuxtCopyPage.vue +1 -1
  5. package/app/components/DuxtLandingShowcase.vue +1 -1
  6. package/app/components/DuxtOpenApiCallbacks.vue +1 -1
  7. package/app/components/DuxtOpenApiClient.vue +1 -1
  8. package/app/components/DuxtOpenApiMedia.vue +1 -1
  9. package/app/components/DuxtOpenApiParameters.vue +1 -1
  10. package/app/components/DuxtOpenApiResponses.vue +1 -1
  11. package/app/components/DuxtOpenApiSchema.vue +1 -1
  12. package/app/components/DuxtOpenApiSecurity.vue +1 -1
  13. package/app/components/content/BrunoOverview.vue +4 -1
  14. package/app/components/content/BrunoRequest.vue +2 -2
  15. package/app/components/content/OpenApiOperation.vue +1 -1
  16. package/app/components/content/OpenApiOperations.vue +1 -1
  17. package/app/components/content/OpenApiOverview.vue +1 -1
  18. package/app/components/content/Partial.vue +1 -1
  19. package/app/components/content/ProseImg.vue +1 -1
  20. package/app/composables/useDuxtCollection.ts +1 -1
  21. package/app/composables/useDuxtNavigation.ts +1 -1
  22. package/app/composables/useDuxtSearch.ts +1 -1
  23. package/app/types/duxt.d.ts +17 -0
  24. package/app/utils/duxt-config.ts +5 -5
  25. package/app/utils/openapi.ts +1 -1
  26. package/app/utils/search-scope.ts +1 -1
  27. package/app/utils/version-choices.ts +8 -5
  28. package/app/utils/version-paths.ts +1 -1
  29. package/bin/duxt-cache-key.mjs +3 -2
  30. package/bin/duxt-og-cache.mjs +3 -1
  31. package/bin/duxt.mjs +2 -2
  32. package/{bruno-parse.ts → build/bruno/bruno-parse.ts} +1 -1
  33. package/{bruno-zip.ts → build/bruno/bruno-zip.ts} +1 -1
  34. package/{report.ts → build/cli/report.ts} +7 -7
  35. package/{validate-report.ts → build/cli/validate-report.ts} +3 -3
  36. package/{duxt-app-config.ts → build/config/duxt-app-config.ts} +5 -2
  37. package/{git-contributors.ts → build/git/git-contributors.ts} +64 -31
  38. package/{search-records.ts → build/search/search-records.ts} +3 -3
  39. package/{section-reports.ts → build/sections/section-reports.ts} +2 -2
  40. package/{sections-bruno.ts → build/sections/sections-bruno.ts} +4 -4
  41. package/{sections-changelog.ts → build/sections/sections-changelog.ts} +4 -4
  42. package/{sections-openapi.ts → build/sections/sections-openapi.ts} +3 -3
  43. package/{sections-resolve.ts → build/sections/sections-resolve.ts} +2 -2
  44. package/{sections.ts → build/sections/sections.ts} +4 -4
  45. package/{sources-cache.ts → build/sources/sources-cache.ts} +2 -2
  46. package/{sources-git.ts → build/sources/sources-git.ts} +46 -31
  47. package/{sources-resolve.ts → build/sources/sources-resolve.ts} +107 -17
  48. package/content.config.ts +4 -4
  49. package/modules/bruno.ts +12 -6
  50. package/modules/config.ts +9 -6
  51. package/modules/git-meta.ts +9 -6
  52. package/modules/redirects.ts +7 -4
  53. package/modules/search-records.ts +4 -4
  54. package/modules/validate.ts +9 -6
  55. package/nuxt.config.ts +1 -1
  56. package/package.json +22 -85
  57. package/server/devtools/content.ts +2 -2
  58. package/server/devtools/preview.ts +3 -3
  59. package/server/devtools/render/content.ts +2 -2
  60. package/server/devtools/render/sources.ts +4 -1
  61. package/server/devtools/sources.ts +1 -1
  62. package/server/mcp/tools/read-page.ts +1 -1
  63. package/server/middleware/raw-markdown.ts +2 -2
  64. package/server/routes/llms-full.txt.get.ts +1 -1
  65. package/server/utils/llms-pages.ts +1 -1
  66. package/server/utils/mcp-docs.ts +2 -2
  67. /package/{bruno-model.ts → build/bruno/bruno-model.ts} +0 -0
  68. /package/{cli.ts → build/cli/cli.ts} +0 -0
  69. /package/{content-cache.ts → build/content/content-cache.ts} +0 -0
  70. /package/{frontmatter.ts → build/content/frontmatter.ts} +0 -0
  71. /package/{highlight-langs.ts → build/content/highlight-langs.ts} +0 -0
  72. /package/{og-image-cache.ts → build/og-image/og-image-cache.ts} +0 -0
  73. /package/{openapi-model.ts → build/openapi/openapi-model.ts} +0 -0
  74. /package/{openapi-parse.ts → build/openapi/openapi-parse.ts} +0 -0
  75. /package/{section-input.ts → build/sections/section-input.ts} +0 -0
  76. /package/{section-markdown.ts → build/sections/section-markdown.ts} +0 -0
  77. /package/{repository-root.ts → build/sources/repository-root.ts} +0 -0
  78. /package/{sources.ts → build/sources/sources.ts} +0 -0
  79. /package/{tfplugindocs.ts → build/sources/tfplugindocs.ts} +0 -0
package/README.md CHANGED
@@ -125,7 +125,7 @@ Everything lives under the `duxt` key of `app.config.ts`. The keys most sites to
125
125
  Every text field takes a literal, an i18n key, or a per-locale record — a single-language site never sees the other two.
126
126
 
127
127
  > [!TIP]
128
- > The full surface — `navigation`, `sections`, `landing`, `feed`, `footer` and the rest — is typed, and the types are the documentation: [`app/types/duxt.d.ts`](app/types/duxt.d.ts) carries a comment per key explaining what it costs and when it is read.
128
+ > The full surface — `navigation`, `sections`, `landing`, `feed`, `footer` and the rest — is typed, and the types are the documentation: [`packages/duxt/app/types/duxt.d.ts`](packages/duxt/app/types/duxt.d.ts) carries a comment per key explaining what it costs and when it is read.
129
129
 
130
130
  ## 🧪 Development
131
131
 
@@ -136,7 +136,7 @@ pnpm install # wires the husky hooks
136
136
  pnpm check # lint + format + typecheck + tests + policy parity + build + a11y
137
137
  ```
138
138
 
139
- The repo root **is** the layer — `nuxt.config.ts`, `content.config.ts`, `app/`, `modules/` and `server/` live there, and `package.json` points at them. `www/` beside it is the site that consumes the layer, and the development target: it deliberately carries the awkward cases — two repositories, four refs, one version of each lifecycle. It is not a template; the exemplary starting point lives in [`kirchDev/duxt-starter`](https://github.com/kirchDev/duxt-starter).
139
+ A pnpm workspace driven by Turborepo. The layer is [`packages/duxt`](packages/duxt) — `nuxt.config.ts`, `content.config.ts`, `app/`, `modules/`, `server/` and the build-time modules under `build/`. [`apps/www`](apps/www) is the site that consumes it, and the development target: it deliberately carries the awkward cases — two repositories, four refs, one version of each lifecycle. The root holds only the workspace and meta configuration, plus `docs/`, which `apps/www` publishes. It is not a template; the exemplary starting point lives in [`kirchDev/duxt-starter`](https://github.com/kirchDev/duxt-starter).
140
140
 
141
141
  ## 🎨 Assets & branding
142
142
 
@@ -148,7 +148,7 @@ The mark is the package name with its first letter bracketed — `[d]uxt` — be
148
148
  Every asset, the colour values, why the icon's brackets are redrawn and the font licensing are in [Conventions → Branding](https://duxt.app/conventions/branding).
149
149
 
150
150
  > [!IMPORTANT]
151
- > The layer ships **no** branding. `duxt.logo` is unset by default, so `DuxtBrand` falls back to the consumer's own `duxt.title` beside a generic icon: a site extending duxt shows its own name in the header and footer and its own icon in the tab, never this one. These assets belong to this repository and to `www/`, not to the published package.
151
+ > The layer ships **no** branding. `duxt.logo` is unset by default, so `DuxtBrand` falls back to the consumer's own `duxt.title` beside a generic icon: a site extending duxt shows its own name in the header and footer and its own icon in the tab, never this one. These assets belong to this repository and to `apps/www`, not to the published package.
152
152
 
153
153
  ## 🤝 Contributing
154
154
 
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * THE LAYER STAYS UNBRANDED. `logo` is unset by default and the fallback is a
10
10
  * generic book icon beside `title` — a site extending duxt must show its own
11
- * name, never this one. duxt's own `www/` sets `logo` like any other consumer,
11
+ * name, never this one. duxt's own `apps/www/` sets `logo` like any other consumer,
12
12
  * which is also what keeps the option honest: the development site exercises
13
13
  * the same path a stranger takes.
14
14
  */
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import type { DuxtBrunoEntry } from '../../bruno-model';
2
+ import type { DuxtBrunoEntry } from '../../build/bruno/bruno-model';
3
3
 
4
4
  /**
5
5
  * A dictionary block — headers, query parameters, a form body — as a table.
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { stripFrontmatter } from '../../frontmatter';
2
+ import { stripFrontmatter } from '../../build/content/frontmatter';
3
3
 
4
4
  /**
5
5
  * The top action: hand this page to a model, or read its source.
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { sourcesForRoute } from '../../sources-resolve';
2
+ import { sourcesForRoute } from '../../build/sources/sources-resolve';
3
3
 
4
4
  /**
5
5
  * One feature, given a whole band: its prose on one side and the thing itself
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import type { DuxtOpenApiCallback } from '../../openapi-model';
2
+ import type { DuxtOpenApiCallback } from '../../build/openapi/openapi-model';
3
3
 
4
4
  /**
5
5
  * The requests the API sends BACK, and where it sends them.
@@ -6,7 +6,7 @@ import type {
6
6
  DuxtOpenApiSecurity,
7
7
  DuxtOpenApiSecurityScheme,
8
8
  DuxtOpenApiServer
9
- } from '../../openapi-model';
9
+ } from '../../build/openapi/openapi-model';
10
10
 
11
11
  /**
12
12
  * The try-it client: the one part of a reference that is not a document.
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import type { DuxtOpenApiMediaType } from '../../openapi-model';
2
+ import type { DuxtOpenApiMediaType } from '../../build/openapi/openapi-model';
3
3
 
4
4
  /**
5
5
  * One body, in each media type the document offers it in.
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import type { DuxtOpenApiParameter } from '../../openapi-model';
2
+ import type { DuxtOpenApiParameter } from '../../build/openapi/openapi-model';
3
3
 
4
4
  /**
5
5
  * The parameters an operation takes, grouped by where they go.
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import type { DuxtOpenApiResponse } from '../../openapi-model';
2
+ import type { DuxtOpenApiResponse } from '../../build/openapi/openapi-model';
3
3
 
4
4
  /**
5
5
  * The responses an operation can send, in the order the parser put them.
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import type { DuxtOpenApiSchema } from '../../openapi-model';
2
+ import type { DuxtOpenApiSchema } from '../../build/openapi/openapi-model';
3
3
 
4
4
  /**
5
5
  * One schema, drawn as the tree it is — and recursive, because schemas are.
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import type { DuxtOpenApiSecurity } from '../../openapi-model';
2
+ import type { DuxtOpenApiSecurity } from '../../build/openapi/openapi-model';
3
3
 
4
4
  /**
5
5
  * What a request has to prove before the endpoint answers it.
@@ -1,5 +1,8 @@
1
1
  <script setup lang="ts">
2
- import type { DuxtBrunoAuth, DuxtBrunoEntry } from '../../../bruno-model';
2
+ import type {
3
+ DuxtBrunoAuth,
4
+ DuxtBrunoEntry
5
+ } from '../../../build/bruno/bruno-model';
3
6
 
4
7
  /**
5
8
  * The collection's front page: what is in it, and how to get it.
@@ -1,9 +1,9 @@
1
1
  <script setup lang="ts">
2
- import type { DuxtBrunoRequest } from '../../../bruno-model';
2
+ import type { DuxtBrunoRequest } from '../../../build/bruno/bruno-model';
3
3
  import type {
4
4
  DuxtOpenApiOperation,
5
5
  DuxtOpenApiServer
6
- } from '../../../openapi-model';
6
+ } from '../../../build/openapi/openapi-model';
7
7
 
8
8
  /**
9
9
  * One Bruno request, as the two columns of the reference.
@@ -4,7 +4,7 @@ import type {
4
4
  DuxtOpenApiSecurity,
5
5
  DuxtOpenApiSecurityScheme,
6
6
  DuxtOpenApiServer
7
- } from '../../../openapi-model';
7
+ } from '../../../build/openapi/openapi-model';
8
8
 
9
9
  /**
10
10
  * One operation, as the two right-hand columns of the reference.
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import type { DuxtOpenApiExternalDocs } from '../../../openapi-model';
2
+ import type { DuxtOpenApiExternalDocs } from '../../../build/openapi/openapi-model';
3
3
 
4
4
  /**
5
5
  * The endpoints under one tag, as the list a reader scans.
@@ -4,7 +4,7 @@ import type {
4
4
  DuxtOpenApiSecurity,
5
5
  DuxtOpenApiSecurityScheme,
6
6
  DuxtOpenApiServer
7
- } from '../../../openapi-model';
7
+ } from '../../../build/openapi/openapi-model';
8
8
 
9
9
  /**
10
10
  * The reference's front page: what the API is, where it lives, how to get in.
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { partialsCollection } from '../../../sources-resolve';
2
+ import { partialsCollection } from '../../../build/sources/sources-resolve';
3
3
 
4
4
  /**
5
5
  * `:partial{name="install"}` — a block written once and rendered in every
@@ -101,7 +101,7 @@ const props = withDefaults(
101
101
  * focus ring and the dialog were all switched off on every image in the
102
102
  * site. It survived from the day the component was written because nothing
103
103
  * in the repository rendered one: every `![…]` in the reference sits inside
104
- * a code block. `www/demo/docs/3.images.md` and `check:images` are the two
104
+ * a code block. `apps/www/demo/docs/3.images.md` and `check:images` are the two
105
105
  * halves of making sure it cannot again.
106
106
  *
107
107
  * A default is what suppresses the cast — `isAbsent && !hasDefault` is the
@@ -1,4 +1,4 @@
1
- import { sourcesForRoute } from '../../sources-resolve';
1
+ import { sourcesForRoute } from '../../build/sources/sources-resolve';
2
2
 
3
3
  /**
4
4
  * Which collection serves the current route.
@@ -1,5 +1,5 @@
1
1
  import type { ContentNavigationItem } from '@nuxt/content';
2
- import { tfplugindocsNavigation } from '../../tfplugindocs';
2
+ import { tfplugindocsNavigation } from '../../build/sources/tfplugindocs';
3
3
 
4
4
  /**
5
5
  * Which collections the navigation is being fetched from.
@@ -1,4 +1,4 @@
1
- import type { DuxtResolvedSource } from '../../sources-resolve';
1
+ import type { DuxtResolvedSource } from '../../build/sources/sources-resolve';
2
2
  import { searchSources } from '../utils/search-scope';
3
3
  // Imported rather than auto-imported, the same as `searchSources` above: this
4
4
  // composable is covered by a plain node test, which has no Nuxt globals.
@@ -271,6 +271,23 @@ declare global {
271
271
  status?: DuxtSourceStatusInput;
272
272
  /** Lifecycle defaults by ref kind; an explicit ref status wins. */
273
273
  statusDefaults?: DuxtSourceStatusDefaultsInput;
274
+ /**
275
+ * Discover release tags to publish as versions, beside or instead of
276
+ * `refs`: every release, or the newest one per minor or major line. An
277
+ * explicit ref still overrides a discovered tag's label, lifecycle, default
278
+ * and locales. Pre-releases are left out unless asked for.
279
+ */
280
+ releases?: {
281
+ select: 'all' | 'minor' | 'major';
282
+ prereleases?: boolean;
283
+ };
284
+ /**
285
+ * The one package whose tags this source publishes, in a repository that
286
+ * tags `<component>@vX.Y.Z` — `'duxt'` reads `duxt@v0.4.0` and ignores
287
+ * `duxt-typesense@v0.1.0`. Applies to `latest` and `releases`; plain
288
+ * `vX.Y.Z` tags still count.
289
+ */
290
+ tagComponent?: string;
274
291
  /**
275
292
  * The repository a source read off disk lives in, for links back to it.
276
293
  * Not `repo`, which is what makes Content download a source.
@@ -76,7 +76,7 @@ export const duxtDefaults: DuxtConfig = {
76
76
  // the INTERFACE and not a tree or a repository. The dropdown that used to
77
77
  // sit beside it pointed at duxt's own tech stack — five links belonging to
78
78
  // this project and to no site that extends it. It lives in
79
- // `www/app/app.config.ts` now.
79
+ // `apps/www/app/app.config.ts` now.
80
80
  { label: 'duxt.defaults.navigation.docs', icon: 'lucide:book-open-text' }
81
81
  ],
82
82
 
@@ -96,7 +96,7 @@ export const duxtDefaults: DuxtConfig = {
96
96
  *
97
97
  * Left empty, the row does not render and the sidebar falls back to the whole
98
98
  * tree, which is the right shape for a site that has not split its docs into
99
- * sections at all. duxt's own sections live in `www/app/app.config.ts`.
99
+ * sections at all. duxt's own sections live in `apps/www/app/app.config.ts`.
100
100
  */
101
101
  sections: [],
102
102
 
@@ -107,7 +107,7 @@ export const duxtDefaults: DuxtConfig = {
107
107
  * whoever runs the site. Shipping duxt's own repository here gave a stranger
108
108
  * extending the layer a GitHub icon pointing at somebody else's project —
109
109
  * the same mistake the legal row already settled. kirchDev's own links live
110
- * in `www/app/app.config.ts`, where they are an example rather than a
110
+ * in `apps/www/app/app.config.ts`, where they are an example rather than a
111
111
  * default.
112
112
  *
113
113
  * Their i18n keys went with them. `duxt.defaults.*` translates the defaults
@@ -130,7 +130,7 @@ export const duxtDefaults: DuxtConfig = {
130
130
  // something about the state of a project — "beta", "v2 is out" — and the
131
131
  // headline and the paragraph under it say what the project IS. The layer
132
132
  // knows none of the three. Those two were duxt's OWN marketing showing
133
- // through every site that extended it; they live in `www/app/app.config.ts`
133
+ // through every site that extended it; they live in `apps/www/app/app.config.ts`
134
134
  // now, in the record form, because the Nitro routes that read them have no
135
135
  // i18n — see `server/utils/duxt-server-text.ts`.
136
136
  //
@@ -160,7 +160,7 @@ export const duxtDefaults: DuxtConfig = {
160
160
  *
161
161
  * Their translations left with them — `duxt.defaults.*` translates the
162
162
  * interface the layer draws, never content a site writes. duxt's own cards
163
- * live in `www/app/app.config.ts`, where each one POINTS at the page that
163
+ * live in `apps/www/app/app.config.ts`, where each one POINTS at the page that
164
164
  * explains it, which the layer could never know.
165
165
  *
166
166
  * Left empty, the band does not render: the landing gates the whole section
@@ -18,7 +18,7 @@ import type {
18
18
  DuxtOpenApiResponse,
19
19
  DuxtOpenApiSchema,
20
20
  DuxtOpenApiServer
21
- } from '../../openapi-model';
21
+ } from '../../build/openapi/openapi-model';
22
22
 
23
23
  /** How deep an example is derived before it is called recursive enough. */
24
24
  /**
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  sourcesForRoute,
3
3
  type DuxtResolvedSource
4
- } from '../../sources-resolve';
4
+ } from '../../build/sources/sources-resolve';
5
5
 
6
6
  /** Select the editions participating in site-wide search. */
7
7
  export function searchSources(
@@ -11,7 +11,10 @@
11
11
  * rest of it is: every version bug this layer has had came from prefix work
12
12
  * done inline in a component, where it could only be checked by clicking.
13
13
  */
14
- import { compareVersionTags } from '../../sources-resolve';
14
+ import {
15
+ compareVersionTags,
16
+ parseVersionTag
17
+ } from '../../build/sources/sources-resolve';
15
18
 
16
19
  /**
17
20
  * What a reader needs to see first: work that may change, the edition they
@@ -33,11 +36,11 @@ function compareEditions(a: DuxtResolvedSource, b: DuxtResolvedSource): number {
33
36
 
34
37
  const aVersion = a.version!;
35
38
  const bVersion = b.version!;
36
- const semver = /^v?\d+\.\d+\.\d+(?:-.+)?$/;
37
39
 
38
- // The shared comparator knows full tags. Demo editions such as `v3.x` are
39
- // deliberately not tags, but still sort newest-first inside their group.
40
- return semver.test(aVersion) && semver.test(bVersion)
40
+ // The shared comparator knows full tags, component-prefixed ones included.
41
+ // Demo editions such as `v3.x` are deliberately not tags, but still sort
42
+ // newest-first inside their group.
43
+ return parseVersionTag(aVersion) && parseVersionTag(bVersion)
41
44
  ? compareVersionTags(aVersion, bVersion)
42
45
  : bVersion.localeCompare(aVersion);
43
46
  }
@@ -86,4 +86,4 @@ export function versionSwitchPath(
86
86
  * Nuxt scans for auto-imports, and `sources-resolve.ts` is plain logic with no
87
87
  * Node imports — the same property that lets `app.config.ts` read it.
88
88
  */
89
- export { versionRelation } from '../../sources-resolve';
89
+ export { versionRelation } from '../../build/sources/sources-resolve';
@@ -37,8 +37,9 @@ Documentation: ${guide}`);
37
37
 
38
38
  const jiti = createJiti(import.meta.url, { interopDefault: true });
39
39
 
40
- const { duxtSourcesCache, runDuxtSourcesCacheCli } =
41
- await jiti.import('../sources-cache');
40
+ const { duxtSourcesCache, runDuxtSourcesCacheCli } = await jiti.import(
41
+ '../build/sources/sources-cache'
42
+ );
42
43
 
43
44
  try {
44
45
  const rootIndex = argv.indexOf('--root');
@@ -41,7 +41,9 @@ Documentation: ${guide}`);
41
41
 
42
42
  const jiti = createJiti(import.meta.url, { interopDefault: true });
43
43
 
44
- const { duxtOgImageCacheCommand } = await jiti.import('../og-image-cache');
44
+ const { duxtOgImageCacheCommand } = await jiti.import(
45
+ '../build/og-image/og-image-cache'
46
+ );
45
47
 
46
48
  try {
47
49
  const { output, exitCode } = duxtOgImageCacheCommand(argv);
package/bin/duxt.mjs CHANGED
@@ -26,8 +26,8 @@ process.removeAllListeners('warning');
26
26
 
27
27
  const jiti = createJiti(import.meta.url, { interopDefault: true });
28
28
 
29
- const { runDuxtCli } = await jiti.import('../cli');
30
- const { duxtReport } = await jiti.import('../report');
29
+ const { runDuxtCli } = await jiti.import('../build/cli/cli');
30
+ const { duxtReport } = await jiti.import('../build/cli/report');
31
31
 
32
32
  try {
33
33
  // `duxtReport` is passed rather than called: usage has to answer on a site
@@ -39,7 +39,7 @@ import type {
39
39
  DuxtBrunoParam,
40
40
  DuxtBrunoRequest
41
41
  } from './bruno-model';
42
- import type { DuxtSectionInput } from './sections-resolve';
42
+ import type { DuxtSectionInput } from '../sections/sections-resolve';
43
43
 
44
44
  /** The manifest at the root of every Bruno collection. */
45
45
  const MANIFEST = 'bruno.json';
@@ -23,7 +23,7 @@
23
23
  * that rebuilds nightly does not publish a new download for a collection nobody
24
24
  * changed — the archive is a function of the files, and of nothing else.
25
25
  */
26
- import type { DuxtSectionInput } from './sections-resolve';
26
+ import type { DuxtSectionInput } from '../sections/sections-resolve';
27
27
  import { BRUNO_UNPUBLISHED } from './bruno-parse';
28
28
 
29
29
  /** One file in the archive. */
@@ -1,13 +1,13 @@
1
1
  import { join } from 'node:path';
2
- import { contentCacheFile, readContentCacheAt } from './content-cache';
3
- import { readDuxtBuildConfig } from './duxt-app-config';
4
- import { duxtManifest, duxtSectionTypes } from './sections-resolve';
5
- import { readSectionReports } from './section-reports';
6
- import { resolveLatestRefs } from './sources-git';
7
- import { redirectRules } from './modules/redirects';
2
+ import { contentCacheFile, readContentCacheAt } from '../content/content-cache';
3
+ import { readDuxtBuildConfig } from '../config/duxt-app-config';
4
+ import { duxtManifest, duxtSectionTypes } from '../sections/sections-resolve';
5
+ import { readSectionReports } from '../sections/section-reports';
6
+ import { resolveLatestRefs } from '../sources/sources-git';
7
+ import { redirectRules } from '../../modules/redirects';
8
8
  import type { Collected, PageRecord } from './validate-report';
9
9
  import { report, walk } from './validate-report';
10
- import type { DuxtResolvedSource } from './sources-resolve';
10
+ import type { DuxtResolvedSource } from '../sources/sources-resolve';
11
11
 
12
12
  /**
13
13
  * The devtools tab's answers, as text a terminal — or a model — can read.
@@ -8,9 +8,9 @@
8
8
  * can run the SAME checks against the pages the site is actually serving,
9
9
  * rather than reprinting a build log that has already scrolled away.
10
10
  */
11
- import { reservedSegments } from './sources-resolve';
12
- import { packageCommandIssues } from './app/utils/package-command';
13
- import { resolveDocumentationPath } from './app/utils/documentation-link';
11
+ import { reservedSegments } from '../sources/sources-resolve';
12
+ import { packageCommandIssues } from '../../app/utils/package-command';
13
+ import { resolveDocumentationPath } from '../../app/utils/documentation-link';
14
14
 
15
15
  export interface PageRecord {
16
16
  collection: string;
@@ -1,8 +1,11 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { createJiti } from 'jiti';
4
- import type { DuxtSource, DuxtSourcesOptions } from './sources-resolve';
5
- import type { DuxtSectionTypes } from './sections-resolve';
4
+ import type {
5
+ DuxtSource,
6
+ DuxtSourcesOptions
7
+ } from '../sources/sources-resolve';
8
+ import type { DuxtSectionTypes } from '../sections/sections-resolve';
6
9
 
7
10
  /** The build-time half of `app.config`'s `duxt` key. */
8
11
  export interface DuxtBuildConfig {
@@ -107,7 +107,8 @@ const SEPARATOR = '\u001f';
107
107
  const RECORD = '\u0001';
108
108
 
109
109
  /** The fields one record carries, in order. */
110
- const FORMAT = `${RECORD}%P${SEPARATOR}%an${SEPARATOR}%ae${SEPARATOR}%D`;
110
+ const FORMAT =
111
+ `${RECORD}%H${SEPARATOR}%P${SEPARATOR}%an` + `${SEPARATOR}%ae${SEPARATOR}%D`;
111
112
 
112
113
  /** What git is asked, and the whole of why each flag is there. */
113
114
  const ARGUMENTS = [
@@ -119,10 +120,9 @@ const ARGUMENTS = [
119
120
  // bucketing those into the newest tag would credit people for work that has
120
121
  // not shipped.
121
122
  '--tags',
122
- // Keeps a merged branch's commits together instead of letting their commit
123
- // dates interleave them with another line of history. It is what makes the
124
- // walk below equal to one `rev-list <previous>..<tag>` per release without
125
- // paying for one `git` process per release.
123
+ // Children before their parents is what lets the pass below propagate a
124
+ // release through the graph. It does NOT make tag ranges contiguous: two
125
+ // parents of one merge may be walked in either order.
126
126
  '--topo-order',
127
127
  '--decorate=short',
128
128
  '--decorate-refs=refs/tags/*',
@@ -132,12 +132,18 @@ const ARGUMENTS = [
132
132
  /**
133
133
  * Every release tag, with the people whose commits it carries.
134
134
  *
135
- * ONE `git log` FOR THE WHOLE HISTORY, walked newest-first: a commit carrying a
136
- * tag OPENS that tag's release, and every commit after it in the walk belongs
137
- * there until the next tag opens the release before it. That is exactly the
138
- * range `<previous tag>..<tag>` names, and the alternative — a `git log` per
139
- * release — is the cost `modules/git-meta.ts` already paid once: the work is
140
- * never the history, it is spawning the processes.
135
+ * ONE `git log` FOR THE WHOLE HISTORY, walked children-first and reconstructed
136
+ * as a graph in memory. Every tag seeds a release number on its commit; that
137
+ * number flows to its parents, while an older tag replaces a newer number at
138
+ * its own boundary. A commit therefore belongs to the oldest tag that can
139
+ * reach it — exactly the set `<previous tag>..<tag>` names — without paying for
140
+ * one git process per release.
141
+ *
142
+ * THE GRAPH IS LOAD-BEARING. Tag markers do not delimit contiguous slices of a
143
+ * topo-ordered log: where a release merge has the older tag on one parent and
144
+ * new work on another, git may print the older tag before that parallel branch.
145
+ * A mutable "current tag" then gives the work to the release before it and
146
+ * leaves the release that merged it empty.
141
147
  *
142
148
  * A MERGE COMMIT SETS THE BOUNDARY AND CREDITS NOBODY. It has to be walked,
143
149
  * because release-please tags the merge commit its release pull request
@@ -149,40 +155,67 @@ const ARGUMENTS = [
149
155
  export function parseReleaseContributors(
150
156
  output: string
151
157
  ): Map<string, DuxtContributor[]> {
152
- const commits = new Map<string, DuxtCommitAuthor[]>();
153
- const contributors = new Map<string, DuxtContributor[]>();
154
-
155
- /** The tags whose release the commits now being read belong to. */
156
- let current: string[] = [];
158
+ const commits: {
159
+ hash: string;
160
+ parents: string[];
161
+ author: DuxtCommitAuthor;
162
+ merge: boolean;
163
+ tags: string[];
164
+ }[] = [];
165
+ const tagGroups: string[][] = [];
157
166
 
158
167
  for (const record of output.split(RECORD)) {
159
168
  if (!record.trim()) continue;
160
169
 
161
- const [parents, name, email, decoration] = record
170
+ const [hash, parents, name, email, decoration] = record
162
171
  .split('\n')[0]!
163
172
  .split(SEPARATOR);
164
-
165
173
  const tags = tagsOf(decoration ?? '');
166
174
 
167
- if (tags.length) {
168
- current = tags;
169
- for (const tag of tags) commits.set(tag, commits.get(tag) ?? []);
170
- }
175
+ commits.push({
176
+ hash: hash ?? '',
177
+ parents: (parents ?? '').trim().split(/\s+/).filter(Boolean),
178
+ author: { name: name ?? '', email: email ?? '' },
179
+ merge: (parents ?? '').trim().includes(' '),
180
+ tags
181
+ });
171
182
 
172
- if (!current.length) continue;
183
+ if (tags.length) tagGroups.push(tags);
184
+ }
173
185
 
174
- // A merge carries more than one parent. It opened the release above and
175
- // says nothing else.
176
- if ((parents ?? '').trim().includes(' ')) continue;
186
+ /** The release each reachable commit belongs to, newest group numbered 0. */
187
+ const owners = new Map<string, number>();
188
+ const authors = tagGroups.map(() => [] as DuxtCommitAuthor[]);
189
+ let group = 0;
177
190
 
178
- const author = { name: name ?? '', email: email ?? '' };
179
- if (isGitHubApp(author)) continue;
191
+ // Seed every boundary before walking. A newer release reaches an older tag's
192
+ // commit too; the larger number there is what stops that propagation.
193
+ for (const commit of commits) {
194
+ if (!commit.tags.length) continue;
195
+ owners.set(commit.hash, group);
196
+ group += 1;
197
+ }
198
+
199
+ for (const commit of commits) {
200
+ const owner = owners.get(commit.hash);
201
+ if (owner === undefined) continue;
180
202
 
181
- for (const tag of current) commits.get(tag)!.push(author);
203
+ // A merge carries more than one parent. It sets and propagates the boundary
204
+ // but says nothing itself; apps are omitted for the same reason as before.
205
+ if (!commit.merge && !isGitHubApp(commit.author)) {
206
+ authors[owner]!.push(commit.author);
207
+ }
208
+
209
+ for (const parent of commit.parents) {
210
+ const previous = owners.get(parent);
211
+ if (previous === undefined || owner > previous) owners.set(parent, owner);
212
+ }
182
213
  }
183
214
 
184
- for (const [tag, authors] of commits) {
185
- contributors.set(tag, contributorsOf(authors));
215
+ const contributors = new Map<string, DuxtContributor[]>();
216
+ for (const [index, tags] of tagGroups.entries()) {
217
+ const people = contributorsOf(authors[index]!);
218
+ for (const tag of tags) contributors.set(tag, people);
186
219
  }
187
220
 
188
221
  return contributors;
@@ -42,9 +42,9 @@
42
42
  * run it without a build.
43
43
  */
44
44
  import { createHash } from 'node:crypto';
45
- import type { CachedPage } from './content-cache';
46
- import type { DuxtResolvedSource } from './sources-resolve';
47
- import { duxtPageSearchable } from './app/utils/page-controls';
45
+ import type { CachedPage } from '../content/content-cache';
46
+ import type { DuxtResolvedSource } from '../sources/sources-resolve';
47
+ import { duxtPageSearchable } from '../../app/utils/page-controls';
48
48
 
49
49
  /**
50
50
  * One entry of an external search index, as duxt hands it over.
@@ -1,5 +1,5 @@
1
1
  import { join } from 'node:path';
2
- import type { DuxtResolvedSource } from './sources-resolve';
2
+ import type { DuxtResolvedSource } from '../sources/sources-resolve';
3
3
  import type { DuxtSectionTypes } from './sections-resolve';
4
4
  import {
5
5
  duxtSectionTypes,
@@ -11,7 +11,7 @@ import {
11
11
  sectionArtefactExists,
12
12
  sectionInputKind
13
13
  } from './section-input';
14
- import { repositoryRoot } from './repository-root';
14
+ import { repositoryRoot } from '../sources/repository-root';
15
15
 
16
16
  /**
17
17
  * Fill in what each LOCAL artefact had to say, for the manifest a module holds.