@kirchdev/duxt 0.3.4 → 0.3.5-dev-main.44-5337cc1
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/README.md +24 -9
- package/app/app.vue +17 -11
- package/app/assets/css/duxt.css +47 -6
- package/app/components/DuxtAnnouncements.vue +117 -0
- package/app/components/DuxtBrunoEntries.vue +62 -0
- package/app/components/DuxtCodeBlock.vue +18 -34
- package/app/components/DuxtCodeToolbar.vue +14 -0
- package/app/components/DuxtCopyButton.vue +33 -0
- package/app/components/DuxtCopyPage.vue +32 -30
- package/app/components/DuxtFooter.vue +39 -8
- package/app/components/DuxtHeader.vue +107 -48
- package/app/components/DuxtLandingProse.vue +1 -1
- package/app/components/DuxtLinkLabel.vue +36 -0
- package/app/components/DuxtLiveWindow.vue +8 -8
- package/app/components/DuxtLocale.vue +10 -1
- package/app/components/DuxtNavigation.vue +2 -2
- package/app/components/DuxtOpenApiClient.vue +285 -90
- package/app/components/DuxtOpenApiMedia.vue +30 -18
- package/app/components/DuxtOpenApiSchema.vue +10 -7
- package/app/components/DuxtPageFeedback.vue +7 -0
- package/app/components/DuxtPageInfo.vue +15 -12
- package/app/components/DuxtPageNav.vue +41 -27
- package/app/components/DuxtPreviewSkeleton.vue +4 -4
- package/app/components/DuxtProgress.vue +2 -14
- package/app/components/DuxtSearch.vue +230 -90
- package/app/components/DuxtSearchInput.vue +70 -0
- package/app/components/DuxtSections.vue +11 -1
- package/app/components/DuxtSegmented.vue +64 -0
- package/app/components/DuxtShortcuts.vue +16 -11
- package/app/components/DuxtShortcutsTrigger.vue +49 -0
- package/app/components/DuxtSkipLink.vue +1 -1
- package/app/components/DuxtToc.vue +16 -10
- package/app/components/DuxtTranslationBanner.vue +1 -1
- package/app/components/DuxtVersion.vue +53 -12
- package/app/components/DuxtVersionBanner.vue +5 -5
- package/app/components/content/AccordionItem.vue +2 -2
- package/app/components/content/BrunoOverview.vue +260 -0
- package/app/components/content/BrunoRequest.vue +141 -0
- package/app/components/content/BrunoRequests.vue +104 -0
- package/app/components/content/Callout.vue +5 -5
- package/app/components/content/ChangelogReleases.vue +161 -40
- package/app/components/content/CodeGroup.vue +41 -37
- package/app/components/content/Field.vue +1 -1
- package/app/components/content/FileTree.vue +1 -1
- package/app/components/content/OpenApiOperation.vue +4 -4
- package/app/components/content/OpenApiOperations.vue +1 -1
- package/app/components/content/OpenApiOverview.vue +1 -1
- package/app/components/content/PackageManagers.vue +42 -58
- package/app/components/content/PageCards.vue +7 -4
- package/app/components/content/Preview.vue +15 -39
- package/app/components/content/ProseImg.vue +596 -49
- package/app/components/content/Since.vue +1 -1
- package/app/components/content/Steps.vue +6 -4
- package/app/components/content/TabGroup.vue +13 -12
- package/app/components/ui/breadcrumb/BreadcrumbSeparator.vue +1 -1
- package/app/components/ui/command/CommandShortcut.vue +1 -1
- package/app/components/ui/dialog/DialogContent.vue +2 -2
- package/app/components/ui/dialog/DialogHeader.vue +1 -1
- package/app/components/ui/dialog/DialogScrollContent.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +2 -2
- package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +2 -2
- package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +2 -2
- package/app/components/ui/navigation-menu/NavigationMenuContent.vue +1 -1
- package/app/components/ui/navigation-menu/NavigationMenuTrigger.vue +1 -1
- package/app/components/ui/navigation-menu/NavigationMenuViewport.vue +1 -1
- package/app/components/ui/scroll-area/ScrollBar.vue +1 -1
- package/app/components/ui/select/SelectItem.vue +2 -2
- package/app/components/ui/sheet/SheetContent.vue +29 -3
- package/app/components/ui/sidebar/Sidebar.vue +28 -4
- package/app/components/ui/sidebar/SidebarGroupAction.vue +1 -1
- package/app/components/ui/sidebar/SidebarInset.vue +1 -1
- package/app/components/ui/sidebar/SidebarMenuAction.vue +1 -1
- package/app/components/ui/sidebar/SidebarMenuBadge.vue +1 -1
- package/app/components/ui/sidebar/SidebarMenuSub.vue +1 -1
- package/app/components/ui/sidebar/SidebarMenuSubButton.vue +1 -1
- package/app/components/ui/sidebar/SidebarProvider.vue +7 -16
- package/app/components/ui/sidebar/index.ts +10 -2
- package/app/components/ui/sidebar/utils.ts +0 -1
- package/app/components/ui/sonner/Sonner.vue +18 -0
- package/app/components/ui/tabs/TabsList.vue +13 -3
- package/app/components/ui/tabs/TabsTrigger.vue +24 -3
- package/app/components/ui/tooltip/TooltipContent.vue +2 -2
- package/app/composables/useActiveHeading.ts +3 -25
- package/app/composables/useDuxtAnalytics.ts +50 -0
- package/app/composables/useDuxtAnimatedHeight.ts +8 -38
- package/app/composables/useDuxtAnnouncements.ts +153 -0
- package/app/composables/useDuxtCopy.ts +51 -0
- package/app/composables/useDuxtDirection.ts +23 -0
- package/app/composables/useDuxtNavigation.ts +28 -8
- package/app/composables/useDuxtPath.ts +3 -5
- package/app/composables/useDuxtSearch.ts +87 -35
- package/app/composables/useDuxtSection.ts +15 -1
- package/app/composables/useDuxtSectionRow.ts +6 -5
- package/app/composables/useDuxtShortcuts.ts +319 -20
- package/app/composables/useDuxtVersion.ts +57 -6
- package/app/composables/useDuxtViewportMeasure.ts +37 -0
- package/app/composables/useFuzzySearch.ts +9 -4
- package/app/composables/useRecentPages.ts +14 -17
- package/app/error.vue +112 -74
- package/app/layouts/changelog.vue +9 -1
- package/app/layouts/default.vue +9 -0
- package/app/layouts/docs.vue +9 -1
- package/app/layouts/reference.vue +9 -1
- package/app/middleware/duxt-section-layout.global.ts +1 -3
- package/app/pages/[...slug].vue +146 -35
- package/app/pages/index.vue +15 -23
- package/app/types/duxt.d.ts +312 -4
- package/app/utils/analytics.ts +91 -0
- package/app/utils/announcements.ts +205 -0
- package/app/utils/changelog.ts +0 -28
- package/app/utils/code-highlight.ts +3 -1
- package/app/utils/contributor-avatar.ts +27 -0
- package/app/utils/direction.ts +43 -0
- package/app/utils/duxt-config.ts +43 -0
- package/app/utils/duxt-text.ts +10 -0
- package/app/utils/format-date.ts +28 -0
- package/app/utils/generated-sections.ts +68 -12
- package/app/utils/generated-toc.ts +14 -7
- package/app/utils/locale-path.ts +43 -0
- package/app/utils/navigation-tree.ts +51 -1
- package/app/utils/nearest-page.ts +8 -0
- package/app/utils/openapi.ts +16 -11
- package/app/utils/page-controls.ts +215 -0
- package/app/utils/pill.ts +61 -0
- package/app/utils/prose-image.ts +128 -0
- package/app/utils/search-context.ts +110 -0
- package/app/utils/search-display.ts +248 -0
- package/app/utils/section-scope.ts +65 -4
- package/app/utils/stored-json.ts +31 -0
- package/app/utils/version-choices.ts +6 -13
- package/app/utils/version-paths.ts +21 -0
- package/bin/duxt-cache-key.mjs +61 -0
- package/bin/duxt-og-cache.mjs +58 -0
- package/bin/{duxt-report.mjs → duxt.mjs} +13 -7
- package/bruno-model.ts +169 -0
- package/bruno-parse.ts +656 -0
- package/bruno-zip.ts +195 -0
- package/cli.ts +240 -0
- package/duxt-app-config.ts +18 -1
- package/frontmatter.ts +49 -0
- package/git-contributors.ts +322 -0
- package/i18n/locales/de/duxt/announcement.json +7 -0
- package/i18n/locales/de/duxt/bruno.json +25 -0
- package/i18n/locales/de/duxt/changelog.json +1 -1
- package/i18n/locales/de/duxt/code.json +2 -1
- package/i18n/locales/de/duxt/page.json +9 -0
- package/i18n/locales/de/duxt/shortcuts.json +1 -0
- package/i18n/locales/de/duxt/theme.json +2 -1
- package/i18n/locales/en/duxt/announcement.json +7 -0
- package/i18n/locales/en/duxt/bruno.json +25 -0
- package/i18n/locales/en/duxt/changelog.json +1 -1
- package/i18n/locales/en/duxt/code.json +2 -1
- package/i18n/locales/en/duxt/page.json +9 -0
- package/i18n/locales/en/duxt/shortcuts.json +1 -0
- package/i18n/locales/en/duxt/theme.json +2 -1
- package/i18n/locales/es/duxt/announcement.json +7 -0
- package/i18n/locales/es/duxt/bruno.json +25 -0
- package/i18n/locales/es/duxt/changelog.json +1 -1
- package/i18n/locales/es/duxt/code.json +2 -1
- package/i18n/locales/es/duxt/page.json +9 -0
- package/i18n/locales/es/duxt/shortcuts.json +1 -0
- package/i18n/locales/es/duxt/theme.json +2 -1
- package/i18n/locales/fr/duxt/announcement.json +7 -0
- package/i18n/locales/fr/duxt/bruno.json +25 -0
- package/i18n/locales/fr/duxt/changelog.json +1 -1
- package/i18n/locales/fr/duxt/code.json +2 -1
- package/i18n/locales/fr/duxt/page.json +9 -0
- package/i18n/locales/fr/duxt/shortcuts.json +1 -0
- package/i18n/locales/fr/duxt/theme.json +2 -1
- package/i18n/locales/pt/duxt/announcement.json +7 -0
- package/i18n/locales/pt/duxt/bruno.json +25 -0
- package/i18n/locales/pt/duxt/changelog.json +1 -1
- package/i18n/locales/pt/duxt/code.json +2 -1
- package/i18n/locales/pt/duxt/page.json +9 -0
- package/i18n/locales/pt/duxt/shortcuts.json +1 -0
- package/i18n/locales/pt/duxt/theme.json +2 -1
- package/modules/bruno.ts +118 -0
- package/modules/config.ts +26 -0
- package/modules/devtools.ts +2 -3
- package/modules/git-meta.ts +16 -47
- package/modules/search-records.ts +132 -0
- package/nuxt.config.ts +81 -7
- package/og-image-cache.ts +742 -0
- package/package.json +42 -10
- package/public/devtools/checks.html +2 -2
- package/public/devtools/config.html +59 -4
- package/public/devtools/pages.html +8 -1
- package/public/devtools/paths.html +1 -1
- package/public/devtools/sources.html +2 -2
- package/public/devtools/versions.html +3 -0
- package/report.ts +28 -19
- package/search-records.ts +380 -0
- package/section-input.ts +142 -0
- package/section-markdown.ts +67 -0
- package/section-reports.ts +10 -4
- package/sections-bruno.ts +571 -0
- package/sections-changelog.ts +76 -43
- package/sections-openapi.ts +12 -83
- package/sections-resolve.ts +174 -8
- package/sections.ts +33 -10
- package/server/devtools/shell.ts +1 -1
- package/server/mcp/tools/list-pages.ts +77 -36
- package/server/mcp/tools/list-versions.ts +84 -0
- package/server/mcp/tools/read-page.ts +80 -26
- package/server/mcp/tools/search-docs.ts +87 -53
- package/server/middleware/raw-markdown.ts +1 -1
- package/server/routes/llms-full.txt.get.ts +6 -8
- package/server/routes/llms.txt.get.ts +1 -1
- package/server/utils/duxt-server-text.ts +0 -13
- package/server/utils/llms-pages.ts +24 -9
- package/server/utils/mcp-docs.ts +577 -0
- package/sources-cache.ts +376 -0
- package/sources-git.ts +186 -31
- package/sources-resolve.ts +59 -1
- package/sources.ts +122 -13
- package/tfplugindocs.ts +158 -0
|
@@ -13,6 +13,15 @@
|
|
|
13
13
|
"no": "Non",
|
|
14
14
|
"thanks": "Merci."
|
|
15
15
|
},
|
|
16
|
+
"image": {
|
|
17
|
+
"close": "Fermer la visionneuse d’image",
|
|
18
|
+
"original": "Ouvrir l’original dans un nouvel onglet",
|
|
19
|
+
"resetZoom": "Réinitialiser le zoom",
|
|
20
|
+
"zoom": "Agrandir l'image",
|
|
21
|
+
"zoomIn": "Zoom avant",
|
|
22
|
+
"zoomNamed": "Agrandir l'image : {alt}",
|
|
23
|
+
"zoomOut": "Zoom arrière"
|
|
24
|
+
},
|
|
16
25
|
"tabs": "Options",
|
|
17
26
|
"since": "Nouveau dans {version}",
|
|
18
27
|
"copy": {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"duxt": {
|
|
3
|
+
"bruno": {
|
|
4
|
+
"auth": "Autenticação",
|
|
5
|
+
"body": "Corpo",
|
|
6
|
+
"download": "Transferir a coleção",
|
|
7
|
+
"downloadHint": "A coleção exatamente como esta versão e este idioma a descrevem.",
|
|
8
|
+
"environments": "Ambientes",
|
|
9
|
+
"fetch": "Abrir no Bruno",
|
|
10
|
+
"fetchHint": "Clona o repositório no Bruno. Segue o ramo predefinido e não está fixado nesta versão.",
|
|
11
|
+
"folders": "Pastas",
|
|
12
|
+
"format": "Formato",
|
|
13
|
+
"getIt": "Obter a coleção",
|
|
14
|
+
"hasCode": "Este pedido também tem scripts ou testes. Aqui não são mostrados nem executados; estão na transferência.",
|
|
15
|
+
"headers": "Cabeçalhos",
|
|
16
|
+
"name": "Nome",
|
|
17
|
+
"notPublished": "Ambientes, credenciais, scripts e testes nunca são publicados neste site nem executados no seu navegador.",
|
|
18
|
+
"parameters": "Parâmetros",
|
|
19
|
+
"requests": "Pedidos",
|
|
20
|
+
"shared": "Comum a todos os pedidos",
|
|
21
|
+
"value": "Valor",
|
|
22
|
+
"withheld": "retido"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
"preview": "Pré-visualização",
|
|
8
8
|
"source": "Código",
|
|
9
9
|
"previewTabs": "Exemplo e código-fonte",
|
|
10
|
-
"copiedToast": "Copiado para a área de transferência"
|
|
10
|
+
"copiedToast": "Copiado para a área de transferência",
|
|
11
|
+
"copyFailed": "Não foi possível copiar para a área de transferência."
|
|
11
12
|
}
|
|
12
13
|
}
|
|
13
14
|
}
|
|
@@ -13,6 +13,15 @@
|
|
|
13
13
|
"no": "Não",
|
|
14
14
|
"thanks": "Obrigado."
|
|
15
15
|
},
|
|
16
|
+
"image": {
|
|
17
|
+
"close": "Fechar o visualizador de imagens",
|
|
18
|
+
"original": "Abrir o original num novo separador",
|
|
19
|
+
"resetZoom": "Repor o zoom",
|
|
20
|
+
"zoom": "Ampliar imagem",
|
|
21
|
+
"zoomIn": "Aumentar o zoom",
|
|
22
|
+
"zoomNamed": "Ampliar imagem: {alt}",
|
|
23
|
+
"zoomOut": "Diminuir o zoom"
|
|
24
|
+
},
|
|
16
25
|
"tabs": "Opções",
|
|
17
26
|
"since": "Novo em {version}",
|
|
18
27
|
"copy": {
|
package/modules/bruno.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The downloadable archive of every Bruno collection the site publishes.
|
|
3
|
+
*
|
|
4
|
+
* WHY A MODULE AND NOT A ROUTE. The `.bru` files exist at BUILD TIME and
|
|
5
|
+
* nowhere else: a site deployed to a Worker has no filesystem, and one deployed
|
|
6
|
+
* anywhere at all has prerendered its pages long before a reader clicks the
|
|
7
|
+
* download. So the archive is written during the build and registered as a
|
|
8
|
+
* public asset — a real file under `.output/public`, served by the assets
|
|
9
|
+
* binding without the server being invoked, which is what every other
|
|
10
|
+
* prerendered thing on these sites already is.
|
|
11
|
+
*
|
|
12
|
+
* LOCAL COLLECTIONS ONLY, and the limit is the one `section-reports.ts` already
|
|
13
|
+
* states for the same reason: a remote artefact lives wherever Content's
|
|
14
|
+
* hash-cached checkout put it, which is a directory known only inside the
|
|
15
|
+
* collection that declared it, and recomputing it here is exactly the second
|
|
16
|
+
* implementation `remoteCollection` refuses to write. `sections-bruno.ts` knows
|
|
17
|
+
* this — a remote section's overview links no archive at all, so there is no
|
|
18
|
+
* dead link rather than a link to a file this module never wrote.
|
|
19
|
+
*
|
|
20
|
+
* The page's link and this file's name are composed from the same two halves —
|
|
21
|
+
* `DUXT_BRUNO_ASSETS` and `brunoZipName` — so they cannot drift: two agreeing by
|
|
22
|
+
* coincidence is how a download 404s after somebody renames one.
|
|
23
|
+
*/
|
|
24
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import type { Nuxt } from '@nuxt/schema';
|
|
27
|
+
import {
|
|
28
|
+
DUXT_BRUNO_ASSETS,
|
|
29
|
+
brunoZipEntries,
|
|
30
|
+
brunoZipName,
|
|
31
|
+
zipStore
|
|
32
|
+
} from '../bruno-zip';
|
|
33
|
+
import { readDuxtBuildConfig } from '../duxt-app-config';
|
|
34
|
+
import { duxtManifest, duxtSectionTypes } from '../sections-resolve';
|
|
35
|
+
import { diskSectionInput, sectionArtefactExists } from '../section-input';
|
|
36
|
+
import { repositoryRoot } from '../repository-root';
|
|
37
|
+
import { resolveLatestRefs } from '../sources-git';
|
|
38
|
+
|
|
39
|
+
export default function duxtBruno(_options: unknown, nuxt: Nuxt) {
|
|
40
|
+
const dirs = nuxt.options._layers
|
|
41
|
+
.flatMap((entry) => [entry.config.rootDir, entry.config.srcDir])
|
|
42
|
+
.filter(Boolean) as string[];
|
|
43
|
+
|
|
44
|
+
const config = readDuxtBuildConfig(dirs);
|
|
45
|
+
const types = duxtSectionTypes(config?.sectionTypes);
|
|
46
|
+
|
|
47
|
+
const entries = duxtManifest(
|
|
48
|
+
resolveLatestRefs(config?.sources ?? [{ path: 'docs' }]),
|
|
49
|
+
config?.sourceOptions ?? {},
|
|
50
|
+
types
|
|
51
|
+
).filter(
|
|
52
|
+
(entry) => entry.generated?.type === 'bruno' && !entry.generated.remote
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
if (!entries.length) return;
|
|
56
|
+
|
|
57
|
+
// BUILT ONCE, HELD AS BYTES. Both writes below come from this array rather
|
|
58
|
+
// than one of them reading the other's directory back: `buildDir` is not the
|
|
59
|
+
// same path at module setup and at the end of the Nitro build, and a hook
|
|
60
|
+
// that re-read it found nothing there — an empty asset directory and a 404
|
|
61
|
+
// behind the one control the overview is built around.
|
|
62
|
+
const archives = entries.flatMap((entry) => {
|
|
63
|
+
const source = join(repositoryRoot(), entry.path);
|
|
64
|
+
|
|
65
|
+
// Silently skipped rather than thrown on: a declared path that is not there
|
|
66
|
+
// already fails the build where the collection is declared, with a message
|
|
67
|
+
// naming the section. Throwing a second, worse one from here would only
|
|
68
|
+
// decide which of the two a reader sees.
|
|
69
|
+
if (!sectionArtefactExists(source, 'directory')) return [];
|
|
70
|
+
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
name: brunoZipName(entry.collection),
|
|
74
|
+
data: zipStore(
|
|
75
|
+
brunoZipEntries(diskSectionInput(source, entry.path, 'directory'))
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
];
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (!archives.length) return;
|
|
82
|
+
|
|
83
|
+
const dir = join(nuxt.options.buildDir, 'duxt-bruno');
|
|
84
|
+
|
|
85
|
+
write(dir, archives);
|
|
86
|
+
|
|
87
|
+
// TWO REGISTRATIONS, because dev and build ask different questions.
|
|
88
|
+
//
|
|
89
|
+
// `publicAssets` is what the DEV server reads: it serves the directory off
|
|
90
|
+
// disk, and it is also what teaches Nitro that `/_duxt/bruno/` is a public
|
|
91
|
+
// URL rather than a route.
|
|
92
|
+
nuxt.hook('nitro:config', (nitro) => {
|
|
93
|
+
nitro.publicAssets ??= [];
|
|
94
|
+
nitro.publicAssets.push({ dir, baseURL: DUXT_BRUNO_ASSETS, maxAge: 0 });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// The BUILD is the other half, and it is not the same thing said twice:
|
|
98
|
+
// Nitro's own copy pass does not carry a directory inside `.nuxt` into
|
|
99
|
+
// `.output/public`, so registering alone produced a site that knew the URL
|
|
100
|
+
// was an asset and had no file behind it — a 404 on the one control the
|
|
101
|
+
// overview is built around. Copied here, after that pass has run, which is
|
|
102
|
+
// what this hook exists for.
|
|
103
|
+
nuxt.hook('nitro:build:public-assets', (nitro) => {
|
|
104
|
+
write(
|
|
105
|
+
join(nitro.options.output.publicDir, ...DUXT_BRUNO_ASSETS.split('/')),
|
|
106
|
+
archives
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The archives, into one directory. */
|
|
112
|
+
function write(dir: string, archives: { name: string; data: Uint8Array }[]) {
|
|
113
|
+
mkdirSync(dir, { recursive: true });
|
|
114
|
+
|
|
115
|
+
for (const archive of archives) {
|
|
116
|
+
writeFileSync(join(dir, archive.name), archive.data);
|
|
117
|
+
}
|
|
118
|
+
}
|
package/modules/config.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import type { Nuxt } from '@nuxt/schema';
|
|
5
|
+
import { bundledLanguagesInfo } from 'shiki/langs';
|
|
5
6
|
import { enableWriteAheadLog } from '../content-cache';
|
|
6
7
|
import type { DuxtBuildConfig } from '../duxt-app-config';
|
|
7
8
|
import { readDuxtBuildConfig } from '../duxt-app-config';
|
|
@@ -170,6 +171,12 @@ function checkSourceLocales(
|
|
|
170
171
|
* The three at the bottom are not samples: the package-manager block, the
|
|
171
172
|
* response body and the example bodies are highlighted at runtime too, and they
|
|
172
173
|
* are the same three whatever a site configures.
|
|
174
|
+
*
|
|
175
|
+
* THE LANDING DEMOS TOO. A `code` demo in `landing.showcase` goes through the
|
|
176
|
+
* same highlighter, and a file whose language was not in this map rendered as
|
|
177
|
+
* flat text — `openapi.yaml` and `widget.md` beside a coloured `nuxt.config.ts`
|
|
178
|
+
* read as the highlighter being off. So the languages the showcase names are
|
|
179
|
+
* added, and a site pays for them only if its landing page uses them.
|
|
173
180
|
*/
|
|
174
181
|
function writeGrammars(nuxt: Nuxt, config?: DuxtBuildConfig): void {
|
|
175
182
|
const shipped = new Map(
|
|
@@ -192,6 +199,25 @@ function writeGrammars(nuxt: Nuxt, config?: DuxtBuildConfig): void {
|
|
|
192
199
|
if (language) languages.add(language);
|
|
193
200
|
}
|
|
194
201
|
|
|
202
|
+
// Aliases resolve to the grammar's id: `yml` is a name Shiki answers to, not
|
|
203
|
+
// a module under `shiki/langs/`.
|
|
204
|
+
const grammarIds = new Map(
|
|
205
|
+
bundledLanguagesInfo.flatMap((info) =>
|
|
206
|
+
[info.id, ...(info.aliases ?? [])].map((name) => [name, info.id] as const)
|
|
207
|
+
)
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
for (const band of config?.landing?.showcase ?? []) {
|
|
211
|
+
for (const file of band?.demo?.files ?? []) {
|
|
212
|
+
// Only names with a module behind them: `text` and anything Shiki does
|
|
213
|
+
// not ship would be an `import()` Vite cannot resolve, and that fails
|
|
214
|
+
// the build rather than degrading one block.
|
|
215
|
+
const id = grammarIds.get(file?.language?.toLowerCase() ?? '');
|
|
216
|
+
|
|
217
|
+
if (id) languages.add(id);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
195
221
|
const loaders = [...languages]
|
|
196
222
|
.sort()
|
|
197
223
|
.map(
|
package/modules/devtools.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import type { Nuxt } from '@nuxt/schema';
|
|
5
|
+
import { localeCodesOf } from '../app/utils/locale-path';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* A window into the layer that has been hardest to debug.
|
|
@@ -120,7 +121,5 @@ function localeCodes(nuxt: Nuxt): string[] {
|
|
|
120
121
|
}
|
|
121
122
|
).i18n?.locales;
|
|
122
123
|
|
|
123
|
-
return (locales
|
|
124
|
-
typeof locale === 'string' ? locale : locale.code
|
|
125
|
-
);
|
|
124
|
+
return localeCodesOf(locales);
|
|
126
125
|
}
|
package/modules/git-meta.ts
CHANGED
|
@@ -2,9 +2,12 @@ import { execFileSync } from 'node:child_process';
|
|
|
2
2
|
import { dirname, relative, sep } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import type { Nuxt } from '@nuxt/schema';
|
|
5
|
+
import type { DuxtContributor } from '../git-contributors';
|
|
6
|
+
import { contributorsOf } from '../git-contributors';
|
|
5
7
|
import { readDuxtBuildConfig } from '../duxt-app-config';
|
|
6
8
|
import { duxtManifest, duxtSectionTypes } from '../sections-resolve';
|
|
7
9
|
import { resolveLatestRefs } from '../sources-git';
|
|
10
|
+
import { normaliseTfplugindocsPage } from '../tfplugindocs';
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* "Last updated" and the contributor list, from the git history the file
|
|
@@ -30,11 +33,7 @@ interface AfterParseContext {
|
|
|
30
33
|
file?: { path?: string; id?: string };
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
export
|
|
34
|
-
name: string;
|
|
35
|
-
commits: number;
|
|
36
|
-
username?: string;
|
|
37
|
-
}
|
|
36
|
+
export type { DuxtContributor };
|
|
38
37
|
|
|
39
38
|
export default function duxtGitMeta(_options: unknown, nuxt: Nuxt) {
|
|
40
39
|
const layerDir = fileURLToPath(new URL('..', import.meta.url));
|
|
@@ -54,8 +53,8 @@ export default function duxtGitMeta(_options: unknown, nuxt: Nuxt) {
|
|
|
54
53
|
duxtSectionTypes(config?.sectionTypes)
|
|
55
54
|
);
|
|
56
55
|
|
|
57
|
-
const
|
|
58
|
-
sources.map((source) => [source.collection, source
|
|
56
|
+
const sourceByCollection = new Map(
|
|
57
|
+
sources.map((source) => [source.collection, source])
|
|
59
58
|
);
|
|
60
59
|
|
|
61
60
|
nuxt.hook(
|
|
@@ -65,10 +64,19 @@ export default function duxtGitMeta(_options: unknown, nuxt: Nuxt) {
|
|
|
65
64
|
const content = ctx.content;
|
|
66
65
|
if (!file || !content) return;
|
|
67
66
|
|
|
67
|
+
const source = sourceByCollection.get(ctx.collection?.name ?? '');
|
|
68
|
+
if (!source) return;
|
|
69
|
+
|
|
70
|
+
// Source flavours alter metadata only. Their Markdown remains portable to
|
|
71
|
+
// the generator's own publisher, so the body is never rewritten here.
|
|
72
|
+
if (source.flavor === 'tfplugindocs') {
|
|
73
|
+
normaliseTfplugindocsPage(content, ctx.file?.id ?? file);
|
|
74
|
+
}
|
|
75
|
+
|
|
68
76
|
// A source that has not asked for its history is left alone — for a
|
|
69
77
|
// downloaded one that would otherwise answer out of a single-commit
|
|
70
78
|
// clone, which is wrong data rather than missing data.
|
|
71
|
-
if (!
|
|
79
|
+
if (!source.history) return;
|
|
72
80
|
|
|
73
81
|
if (file.includes('/.data/content/')) unshallow(dirname(file));
|
|
74
82
|
|
|
@@ -308,42 +316,3 @@ function gitLog(file: string): Commit[] {
|
|
|
308
316
|
|
|
309
317
|
return historyOf(root).get(path) ?? [];
|
|
310
318
|
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* One entry per person, most commits first.
|
|
314
|
-
*
|
|
315
|
-
* Identity is the email, not the name — the same person commits as "Titus
|
|
316
|
-
* Kirch" and as "titus" and would otherwise appear twice. The GitHub username
|
|
317
|
-
* is read out of a noreply address, which is the only place git actually
|
|
318
|
-
* carries one; without it there is a name and no avatar, which is the truth.
|
|
319
|
-
*/
|
|
320
|
-
export function contributorsOf(commits: Commit[]): DuxtContributor[] {
|
|
321
|
-
const people = new Map<string, DuxtContributor>();
|
|
322
|
-
|
|
323
|
-
for (const commit of commits) {
|
|
324
|
-
const key = commit.email.toLowerCase();
|
|
325
|
-
const existing = people.get(key);
|
|
326
|
-
|
|
327
|
-
if (existing) {
|
|
328
|
-
existing.commits += 1;
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
people.set(key, {
|
|
333
|
-
name: commit.name,
|
|
334
|
-
commits: 1,
|
|
335
|
-
username: githubUsername(commit.email)
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return [...people.values()].sort((a, b) => b.commits - a.commits);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/** `1234567+octocat@users.noreply.github.com` becomes `octocat`. */
|
|
343
|
-
export function githubUsername(email: string): string | undefined {
|
|
344
|
-
const match = /^(?:\d+\+)?([^@]+)@users\.noreply\.github\.com$/i.exec(
|
|
345
|
-
email.trim()
|
|
346
|
-
);
|
|
347
|
-
|
|
348
|
-
return match?.[1];
|
|
349
|
-
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { Nuxt } from '@nuxt/schema';
|
|
2
|
+
import { readContentCache } from '../content-cache';
|
|
3
|
+
import { duxtSearchRecords } from '../search-records';
|
|
4
|
+
import type { DuxtSearchRecordOptions } from '../search-records';
|
|
5
|
+
import type { DuxtResolvedSource } from '../sources-resolve';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* duxt's one build-time extension point, and the whole of it.
|
|
9
|
+
*
|
|
10
|
+
* Why there is exactly one hook rather than a registry of them, and why a
|
|
11
|
+
* `duxt.plugins` API is not coming, is written where the contract is:
|
|
12
|
+
* `search-records.ts`. This half only decides WHEN the payload is built and
|
|
13
|
+
* makes sure it is built for nobody as rarely as possible.
|
|
14
|
+
*
|
|
15
|
+
* `build:done`, and the timing is the whole reason the hook can promise
|
|
16
|
+
* anything:
|
|
17
|
+
*
|
|
18
|
+
* - Content fills its parse cache from a `modules:done` listener, so at
|
|
19
|
+
* `build:done` every source, version, locale and generated section has been
|
|
20
|
+
* parsed — including the repositories Content downloaded into `.data/`,
|
|
21
|
+
* which nothing else on disk describes. Same seam `modules/validate.ts`
|
|
22
|
+
* reads, and for the same reason: `content:file:afterParse` fires only for
|
|
23
|
+
* files whose checksum CHANGED, so a listener built on it reports an empty
|
|
24
|
+
* site on every second build.
|
|
25
|
+
* - `@nuxt/nitro-server` runs the entire Nitro build — prerender and rollup —
|
|
26
|
+
* from a `build:done` listener it registers while bundling, which is long
|
|
27
|
+
* after every module has registered its own. Hooks run in registration
|
|
28
|
+
* order, so this one fires BEFORE Nitro produces anything, and a listener
|
|
29
|
+
* may still write a file into `public/` and have it copied into the output.
|
|
30
|
+
*/
|
|
31
|
+
const HOOK = 'duxt:search:records';
|
|
32
|
+
|
|
33
|
+
export default function duxtSearchRecordsModule(_options: unknown, nuxt: Nuxt) {
|
|
34
|
+
nuxt.hook('build:done', async () => {
|
|
35
|
+
if (!listening(nuxt)) return;
|
|
36
|
+
|
|
37
|
+
// The manifest `modules/config.ts` already resolved, rather than a second
|
|
38
|
+
// pass of its own: resolving it means `git ls-remote` for every source that
|
|
39
|
+
// tracks a moving ref, and asking twice is both a second network round trip
|
|
40
|
+
// and a second chance for the two answers to differ.
|
|
41
|
+
const sources =
|
|
42
|
+
(
|
|
43
|
+
nuxt.options.appConfig.duxt as
|
|
44
|
+
| { resolvedSources?: DuxtResolvedSource[] }
|
|
45
|
+
| undefined
|
|
46
|
+
)?.resolvedSources ?? [];
|
|
47
|
+
|
|
48
|
+
if (!sources.length) return;
|
|
49
|
+
|
|
50
|
+
const cached = readContentCache(
|
|
51
|
+
nuxt,
|
|
52
|
+
sources.map((source) => source.collection)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// No cache at all is not "an empty site": it is a run that parsed nothing.
|
|
56
|
+
// Handing a provider zero records there would tell it to empty its index.
|
|
57
|
+
if (!cached) return;
|
|
58
|
+
|
|
59
|
+
await nuxt.callHook(
|
|
60
|
+
HOOK,
|
|
61
|
+
duxtSearchRecords(sources, cached, routing(nuxt))
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Is anybody listening?
|
|
68
|
+
*
|
|
69
|
+
* The payload is a pass over every parsed page of every language, and on a site
|
|
70
|
+
* with no provider layer — which is every site until one is installed — nobody
|
|
71
|
+
* ever reads it. Building it anyway would be the cost this layer charges for a
|
|
72
|
+
* feature nobody turned on.
|
|
73
|
+
*
|
|
74
|
+
* Hookable keeps its registry private, so this reads a field it is not offered.
|
|
75
|
+
* FAILING OPEN is the point of the shape: a registry that cannot be read costs
|
|
76
|
+
* one wasted pass, while a wrong `false` means the hook silently never fires —
|
|
77
|
+
* and a hook that never fires is a bug with no symptom, which is the failure
|
|
78
|
+
* mode this layer spends most of its comments avoiding.
|
|
79
|
+
*/
|
|
80
|
+
function listening(nuxt: Nuxt): boolean {
|
|
81
|
+
const registry = (
|
|
82
|
+
nuxt.hooks as unknown as {
|
|
83
|
+
_hooks?: Record<string, unknown[] | undefined>;
|
|
84
|
+
}
|
|
85
|
+
)._hooks;
|
|
86
|
+
|
|
87
|
+
return !registry || Boolean(registry[HOOK]?.length);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* What the site's routing does to a path, read where it is actually true.
|
|
92
|
+
*
|
|
93
|
+
* `nuxt.options.i18n.locales` is the WRONG list and looks like the right one.
|
|
94
|
+
* @nuxtjs/i18n collects `i18n.locales` from every layer itself and never reads
|
|
95
|
+
* the merged copy, which is why `restrictLocales` in `modules/config.ts`
|
|
96
|
+
* filters the per-layer configs instead — and therefore why a site that narrowed
|
|
97
|
+
* itself to two languages still has seven in `nuxt.options.i18n`. Reading that
|
|
98
|
+
* would index every translation the layer ships at URLs the site does not route.
|
|
99
|
+
*/
|
|
100
|
+
function routing(nuxt: Nuxt): DuxtSearchRecordOptions {
|
|
101
|
+
const codeOf = (locale: unknown) =>
|
|
102
|
+
typeof locale === 'string'
|
|
103
|
+
? locale
|
|
104
|
+
: (locale as { code?: string } | null)?.code;
|
|
105
|
+
|
|
106
|
+
const locales = [
|
|
107
|
+
...new Set(
|
|
108
|
+
nuxt.options._layers
|
|
109
|
+
.flatMap(
|
|
110
|
+
(entry) =>
|
|
111
|
+
(entry.config as { i18n?: { locales?: unknown[] } }).i18n
|
|
112
|
+
?.locales ?? []
|
|
113
|
+
)
|
|
114
|
+
.map(codeOf)
|
|
115
|
+
.filter(Boolean) as string[]
|
|
116
|
+
)
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const i18n = nuxt.options.i18n as
|
|
120
|
+
| { defaultLocale?: string; strategy?: string }
|
|
121
|
+
| undefined;
|
|
122
|
+
|
|
123
|
+
// A consumer may have dropped i18n entirely. One language, no segment, and
|
|
124
|
+
// the content is whatever language it was written in.
|
|
125
|
+
const defaultLocale = i18n?.defaultLocale ?? locales[0] ?? 'en';
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
locales: locales.length ? locales : [defaultLocale],
|
|
129
|
+
defaultLocale,
|
|
130
|
+
strategy: i18n?.strategy as DuxtSearchRecordOptions['strategy']
|
|
131
|
+
};
|
|
132
|
+
}
|