@writedocs/generator 0.4.4 → 0.4.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@writedocs/generator",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "Static site generator for docs — a writedocs.json + MDX folder in, a static site out.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,10 +34,23 @@ const askAiId = process.env.WRITEDOCS_ASK_AI_ID || config.integrations.askAi?.id
34
34
  const askAiInitScript = askAiId
35
35
  ? `DocsBotAI.init(${JSON.stringify({ id: askAiId })});`
36
36
  : null;
37
+ // O ClientRouter (BaseLayout.astro) troca o <body> inteiro a cada navegação,
38
+ // e o DocsBot monta o widget num #docsbotai-root que ele mesmo anexa ao
39
+ // <body> - então o widget ia embora junto com o body antigo. Rodar o init de
40
+ // novo não resolve: o router não reexecuta script inline de conteúdo
41
+ // idêntico, e o mount() do DocsBot recusa montar duas vezes sem unmount().
42
+ // Aqui o root vivo é marcado com o mesmo atributo de `transition:persist`
43
+ // que o Astro já sabe mover, e o documento novo ganha um slot com o mesmo
44
+ // nome - o swap do Astro leva o elemento inteiro (shadow root com os
45
+ // estilos, estado do React, chat aberto) para a página nova. Roda uma vez
46
+ // só pelo mesmo motivo acima, e o listener no `document` sobrevive às
47
+ // navegações.
48
+ const askAiPersistScript = `document.addEventListener("astro:before-swap",function(e){var r=document.getElementById("docsbotai-root");if(!r)return;r.setAttribute("data-astro-transition-persist","wd-ask-ai");var s=e.newDocument.createElement("div");s.setAttribute("data-astro-transition-persist","wd-ask-ai");e.newDocument.body.appendChild(s);});`;
37
49
  ---
38
50
  {askAiId && (
39
51
  <>
40
52
  <script is:inline set:html={askAiLoaderScript} />
41
53
  {askAiInitScript && <script is:inline set:html={askAiInitScript} />}
54
+ <script is:inline set:html={askAiPersistScript} />
42
55
  </>
43
56
  )}
@@ -1,210 +1,244 @@
1
- // Lets a static asset referenced by a root-relative URL - a writedocs.json
2
- // field (styles.favicon, styles.logo, footer.logo, styles.background.
3
- // images.{light,dark}, seo.ogImage, styles.fonts.*.source - the exact set
4
- // collectConfiguredAssetPaths() in lib/config.ts resolves) *or* a plain
5
- // image written directly into a page's own MDX/Markdown body
6
- // (`![alt](/images/foo.png)`, a raw `<img src="/images/foo.png">`, the
7
- // `<Image src="...">` component, `<Card img="...">`, ...) - resolve from
8
- // *anywhere* in the project, not just public/. "Any place to store
9
- // assets" is the whole point: a site author shouldn't have to know Astro
10
- // has a public/ convention at all, let alone reorganize their project
11
- // around it, just to reference an image from a doc page.
12
- //
13
- // Deliberately NOT implemented as "copy everything into public/ (or a
14
- // merged scratch dir) before Astro starts, then leave Astro's own
15
- // publicDir alone": that would either (a) write into the user's own
16
- // public/ folder as a side effect - a real file appearing in their
17
- // project tree that they didn't create, which writedocsTempDir()'s own
18
- // doc comment already argues against doing anywhere in this codebase, or
19
- // (b) point publicDir at a one-shot copied scratch dir instead of the
20
- // real public/, which would regress `writedocs dev`'s live file-reload
21
- // for the *existing*, common case of a file genuinely living under
22
- // public/ - Vite's own dev server watches and serves publicDir straight
23
- // off disk per-request with no copy step involved at all today; a byte
24
- // copy taken once at startup wouldn't pick up a live edit to that file
25
- // without a full dev-server restart.
26
- //
27
- // Instead, this is a small Astro integration with two hooks, each
28
- // resolving fresh rather than relying on any one-shot copy:
29
- // - astro:server:setup (dev): adds Vite dev-server middleware that
30
- // intercepts any request whose extension looks like a static asset
31
- // (looksLikeAssetPath() below - images/fonts, the same set
32
- // MIME_BY_EXTENSION already knows how to serve) and - only if it
33
- // doesn't already exist under public/ (that stays Vite's own job,
34
- // unshadowed) - streams it straight from wherever it lives in the
35
- // project. Not scoped to writedocs.json's own configured fields specifically
36
- // (an earlier version of this file was) - a page's own MDX can
37
- // reference an image path this integration has no way to know about
38
- // ahead of time short of parsing every page's content on every
39
- // request, so instead of trying to enumerate what *should* be
40
- // reachable, it just resolves whatever *is* actually requested,
41
- // directly, the same way Vite's own publicDir serving already does.
42
- // - astro:build:done: a one-time pass after Astro's own build finishes.
43
- // First, the same writedocs.json-field-driven copy as before (still needed
44
- // as its own pass - background images/font sources render into CSS
45
- // `url(...)` inside a `<style>` block, and seo.ogImage into a `<meta
46
- // content="...">` tag, neither of which the second pass below would
47
- // ever see). Second, copyReferencedContentAssets() below scans every
48
- // built .html page for `src="/..."` references (an `<img>`, mainly -
49
- // see its own comment) not already present in the output, and resolves
50
- // + copies each the same way - this is what actually covers a plain
51
- // content image, since there's no fixed field name to read it from up
52
- // front the way there is for writedocs.json's own fields. A real static
53
- // build has no "later" for a live-reload concern to apply to, so a
54
- // single pass of each here is sufficient.
55
- import fs from 'node:fs';
56
- import path from 'node:path';
57
- import { fileURLToPath } from 'node:url';
58
- import { loadDocsConfig, collectConfiguredAssetPaths } from './config.ts';
59
-
60
- const MIME_BY_EXTENSION = {
61
- '.svg': 'image/svg+xml',
62
- '.png': 'image/png',
63
- '.jpg': 'image/jpeg',
64
- '.jpeg': 'image/jpeg',
65
- '.gif': 'image/gif',
66
- '.webp': 'image/webp',
67
- '.avif': 'image/avif',
68
- '.ico': 'image/x-icon',
69
- '.woff': 'font/woff',
70
- '.woff2': 'font/woff2',
71
- '.ttf': 'font/ttf',
72
- '.otf': 'font/otf',
73
- };
74
-
75
- function mimeFor(filePath) {
76
- return MIME_BY_EXTENSION[path.extname(filePath).toLowerCase()] || 'application/octet-stream';
77
- }
78
-
79
- /** Resolves a root-relative `urlPath` (e.g. "/images/hero.svg") against
80
- * `contentDir` directly - not `<contentDir>/public` - the fallback
81
- * location for one of the styles/footer/seo asset fields
82
- * (collectConfiguredAssetPaths()) when it isn't sitting under public/.
83
- * Segment-by-segment path-traversal guard (no
84
- * `..`/`.` segments) since `urlPath` ultimately comes from an incoming
85
- * request URL in the dev-server case, not just trusted writedocs.json
86
- * content. Returns an absolute path, or null if nothing real is there. */
87
- function resolveOutsidePublic(urlPath, contentDir) {
88
- const segments = urlPath.replace(/^\/+/, '').split('/');
89
- if (segments.some((segment) => segment === '..' || segment === '.' || segment === '')) return null;
90
- const absolute = path.join(contentDir, ...segments);
91
- try {
92
- return fs.statSync(absolute).isFile() ? absolute : null;
93
- } catch {
94
- return null;
95
- }
96
- }
97
-
98
- function publicPathFor(urlPath, contentDir) {
99
- const segments = urlPath.replace(/^\/+/, '').split('/');
100
- return path.join(contentDir, 'public', ...segments);
101
- }
102
-
103
- /** Root-relative, recognized-extension paths only - the same "does this
104
- * look like a static asset at all" question mimeFor()'s own map already
105
- * answers, reused here as the dev-middleware/build-scan gate instead of
106
- * a fixed list of writedocs.json field names. Deliberately permissive: a page
107
- * or config field asking for a path this returns true for gets a real
108
- * attempt to resolve it from anywhere in the project; anything else
109
- * (an actual API route, a `.html` page request, ...) is left alone,
110
- * same as before this existed. */
111
- function looksLikeAssetPath(urlPath) {
112
- return Object.prototype.hasOwnProperty.call(MIME_BY_EXTENSION, path.extname(urlPath).toLowerCase());
113
- }
114
-
115
- /** Recursively lists every `.html` file under `dir`. Astro's own build
116
- * output is already a flat-ish tree of one .html per route, so this is
117
- * just a plain walk - no globbing dependency needed for something this
118
- * small. */
119
- function listHtmlFiles(dir) {
120
- const results = [];
121
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
122
- const full = path.join(dir, entry.name);
123
- if (entry.isDirectory()) {
124
- results.push(...listHtmlFiles(full));
125
- } else if (entry.isFile() && entry.name.endsWith('.html')) {
126
- results.push(full);
127
- }
128
- }
129
- return results;
130
- }
131
-
132
- /** Scans every built page for root-relative, recognized-extension `src="/..."`
133
- * references (`<img src="...">`, mainly - what `Image.astro`, `Frame.astro`,
134
- * and a raw `![]()`/`<img>` in MDX all ultimately render down to; a
135
- * `srcset="..."` attribute is checked too, since it's the same kind of
136
- * reference and costs nothing extra to also cover) that aren't already
137
- * present in the build output, and copies each one in from wherever it
138
- * resolves in the project via resolveOutsidePublic() - the same
139
- * fallback the dev middleware below already uses, just applied once,
140
- * after the fact, to the rendered HTML instead of to live requests.
141
- * This is what actually covers a plain content-body image: unlike a
142
- * writedocs.json field, there's no fixed set of field names to read a
143
- * content image's path from ahead of time, so instead this reads
144
- * whatever `src` a page's own rendered markup ends up containing. */
145
- function copyReferencedContentAssets(outDir, contentDir) {
146
- const attrPattern = /\b(?:src|srcset)="([^"]+)"/g;
147
- for (const htmlFile of listHtmlFiles(outDir)) {
148
- const html = fs.readFileSync(htmlFile, 'utf8');
149
- let match;
150
- while ((match = attrPattern.exec(html))) {
151
- // srcset can hold a comma-separated list of "url descriptor" pairs -
152
- // split it out; a plain src is already just the one URL.
153
- const candidates = match[1].split(',').map((part) => part.trim().split(/\s+/)[0]).filter(Boolean);
154
- for (const urlPath of candidates) {
155
- if (!urlPath.startsWith('/') || urlPath.startsWith('//')) continue; // only root-relative - not external, not protocol-relative
156
- if (!looksLikeAssetPath(urlPath)) continue;
157
- const segments = urlPath.replace(/^\/+/, '').split('/');
158
- const outPath = path.join(outDir, ...segments);
159
- if (fs.existsSync(outPath)) continue; // already present - via public/'s passthrough copy, or a previous iteration of this same loop
160
- const resolved = resolveOutsidePublic(urlPath, contentDir);
161
- if (!resolved) continue; // doesn't resolve to a real file anywhere in the project either - leave the broken reference as-is, same as today
162
- fs.mkdirSync(path.dirname(outPath), { recursive: true });
163
- fs.copyFileSync(resolved, outPath);
164
- }
165
- }
166
- }
167
- }
168
-
169
- export function stylesAssetFallback(contentDir) {
170
- return {
171
- name: 'writedocs-styles-asset-fallback',
172
- hooks: {
173
- 'astro:server:setup': ({ server }) => {
174
- server.middlewares.use((req, res, next) => {
175
- if (!req.url) return next();
176
- const urlPath = req.url.split('?')[0];
177
- if (!looksLikeAssetPath(urlPath)) return next();
178
- if (fs.existsSync(publicPathFor(urlPath, contentDir))) return next(); // real public/ file - Vite's own static serving already handles it, don't shadow
179
- const resolved = resolveOutsidePublic(urlPath, contentDir);
180
- if (!resolved) return next();
181
- res.setHeader('Content-Type', mimeFor(resolved));
182
- fs.createReadStream(resolved).pipe(res);
183
- });
184
- },
185
- 'astro:build:done': async ({ dir }) => {
186
- const outDir = fileURLToPath(dir);
187
- const config = loadDocsConfig(contentDir);
188
- // Pass 1: writedocs.json's own configured fields. Kept as its own,
189
- // field-name-driven pass rather than folded into pass 2 below -
190
- // background images/font sources render into CSS `url(...)`
191
- // inside a `<style>` block, and seo.ogImage into a `<meta
192
- // content="...">` tag, none of which pass 2's src="..."/
193
- // srcset="..." attribute scan would ever see.
194
- for (const urlPath of collectConfiguredAssetPaths(config)) {
195
- const segments = urlPath.replace(/^\/+/, '').split('/');
196
- const outPath = path.join(outDir, ...segments);
197
- if (fs.existsSync(outPath)) continue; // already present via public/'s normal passthrough copy
198
- const resolved = resolveOutsidePublic(urlPath, contentDir);
199
- if (!resolved) continue; // not configured to a real file anywhere - same as today, just renders a broken <link>/<img>
200
- fs.mkdirSync(path.dirname(outPath), { recursive: true });
201
- fs.copyFileSync(resolved, outPath);
202
- }
203
- // Pass 2: arbitrary content-body assets - anything a page's own
204
- // rendered HTML references by src/srcset that pass 1 didn't
205
- // already account for.
206
- copyReferencedContentAssets(outDir, contentDir);
207
- },
208
- },
209
- };
210
- }
1
+ // Lets a static asset referenced by a root-relative URL - a writedocs.json
2
+ // field (styles.favicon, styles.logo, footer.logo, styles.background.
3
+ // images.{light,dark}, seo.ogImage, styles.fonts.*.source - the exact set
4
+ // collectConfiguredAssetPaths() in lib/config.ts resolves) *or* a plain
5
+ // image written directly into a page's own MDX/Markdown body
6
+ // (`![alt](/images/foo.png)`, a raw `<img src="/images/foo.png">`, the
7
+ // `<Image src="...">` component, `<Card img="...">`, ...) - resolve from
8
+ // *anywhere* in the project, not just public/. "Any place to store
9
+ // assets" is the whole point: a site author shouldn't have to know Astro
10
+ // has a public/ convention at all, let alone reorganize their project
11
+ // around it, just to reference an image from a doc page.
12
+ //
13
+ // Deliberately NOT implemented as "copy everything into public/ (or a
14
+ // merged scratch dir) before Astro starts, then leave Astro's own
15
+ // publicDir alone": that would either (a) write into the user's own
16
+ // public/ folder as a side effect - a real file appearing in their
17
+ // project tree that they didn't create, which writedocsTempDir()'s own
18
+ // doc comment already argues against doing anywhere in this codebase, or
19
+ // (b) point publicDir at a one-shot copied scratch dir instead of the
20
+ // real public/, which would regress `writedocs dev`'s live file-reload
21
+ // for the *existing*, common case of a file genuinely living under
22
+ // public/ - Vite's own dev server watches and serves publicDir straight
23
+ // off disk per-request with no copy step involved at all today; a byte
24
+ // copy taken once at startup wouldn't pick up a live edit to that file
25
+ // without a full dev-server restart.
26
+ //
27
+ // Instead, this is a small Astro integration with two hooks, each
28
+ // resolving fresh rather than relying on any one-shot copy:
29
+ // - astro:server:setup (dev): adds Vite dev-server middleware that
30
+ // intercepts any request whose extension looks like a static asset
31
+ // (looksLikeAssetPath() below - images/fonts, the same set
32
+ // MIME_BY_EXTENSION already knows how to serve) and - only if it
33
+ // doesn't already exist under public/ (that stays Vite's own job,
34
+ // unshadowed) - streams it straight from wherever it lives in the
35
+ // project. Not scoped to writedocs.json's own configured fields specifically
36
+ // (an earlier version of this file was) - a page's own MDX can
37
+ // reference an image path this integration has no way to know about
38
+ // ahead of time short of parsing every page's content on every
39
+ // request, so instead of trying to enumerate what *should* be
40
+ // reachable, it just resolves whatever *is* actually requested,
41
+ // directly, the same way Vite's own publicDir serving already does.
42
+ // - astro:build:done: a one-time pass after Astro's own build finishes.
43
+ // First, the same writedocs.json-field-driven copy as before (still needed
44
+ // as its own pass - background images/font sources render into CSS
45
+ // `url(...)` inside a `<style>` block, and seo.ogImage into a `<meta
46
+ // content="...">` tag, neither of which the second pass below would
47
+ // ever see). Second, copyReferencedContentAssets() below scans every
48
+ // built .html page for `src="/..."` references (an `<img>`, mainly -
49
+ // see its own comment) not already present in the output, and resolves
50
+ // + copies each the same way - this is what actually covers a plain
51
+ // content image, since there's no fixed field name to read it from up
52
+ // front the way there is for writedocs.json's own fields. A real static
53
+ // build has no "later" for a live-reload concern to apply to, so a
54
+ // single pass of each here is sufficient.
55
+ import fs from 'node:fs';
56
+ import path from 'node:path';
57
+ import { fileURLToPath } from 'node:url';
58
+ import { loadDocsConfig, collectConfiguredAssetPaths } from './config.ts';
59
+
60
+ const MIME_BY_EXTENSION = {
61
+ '.svg': 'image/svg+xml',
62
+ '.png': 'image/png',
63
+ '.jpg': 'image/jpeg',
64
+ '.jpeg': 'image/jpeg',
65
+ '.gif': 'image/gif',
66
+ '.webp': 'image/webp',
67
+ '.avif': 'image/avif',
68
+ '.ico': 'image/x-icon',
69
+ '.woff': 'font/woff',
70
+ '.woff2': 'font/woff2',
71
+ '.ttf': 'font/ttf',
72
+ '.otf': 'font/otf',
73
+ };
74
+
75
+ function mimeFor(filePath) {
76
+ return MIME_BY_EXTENSION[path.extname(filePath).toLowerCase()] || 'application/octet-stream';
77
+ }
78
+
79
+ /** Resolves a root-relative `urlPath` (e.g. "/images/hero.svg") against
80
+ * `contentDir` directly - not `<contentDir>/public` - the fallback
81
+ * location for one of the styles/footer/seo asset fields
82
+ * (collectConfiguredAssetPaths()) when it isn't sitting under public/.
83
+ * Segment-by-segment path-traversal guard (no
84
+ * `..`/`.` segments) since `urlPath` ultimately comes from an incoming
85
+ * request URL in the dev-server case, not just trusted writedocs.json
86
+ * content. Returns an absolute path, or null if nothing real is there. */
87
+ function resolveOutsidePublic(urlPath, contentDir) {
88
+ const segments = urlPath.replace(/^\/+/, '').split('/');
89
+ if (segments.some((segment) => segment === '..' || segment === '.' || segment === '')) return null;
90
+ const absolute = path.join(contentDir, ...segments);
91
+ try {
92
+ return fs.statSync(absolute).isFile() ? absolute : null;
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+
98
+ function publicPathFor(urlPath, contentDir) {
99
+ const segments = urlPath.replace(/^\/+/, '').split('/');
100
+ return path.join(contentDir, 'public', ...segments);
101
+ }
102
+
103
+ /** Root-relative, recognized-extension paths only - the same "does this
104
+ * look like a static asset at all" question mimeFor()'s own map already
105
+ * answers, reused here as the dev-middleware/build-scan gate instead of
106
+ * a fixed list of writedocs.json field names. Deliberately permissive: a page
107
+ * or config field asking for a path this returns true for gets a real
108
+ * attempt to resolve it from anywhere in the project; anything else
109
+ * (an actual API route, a `.html` page request, ...) is left alone,
110
+ * same as before this existed. */
111
+ function looksLikeAssetPath(urlPath) {
112
+ return Object.prototype.hasOwnProperty.call(MIME_BY_EXTENSION, path.extname(urlPath).toLowerCase());
113
+ }
114
+
115
+ /** Recursively lists every `.html` file under `dir`. Astro's own build
116
+ * output is already a flat-ish tree of one .html per route, so this is
117
+ * just a plain walk - no globbing dependency needed for something this
118
+ * small. */
119
+ function listHtmlFiles(dir) {
120
+ return listFilesWithExtension(dir, '.html');
121
+ }
122
+
123
+ // Generalização de listHtmlFiles, usada também para achar os .css do build.
124
+ function listFilesWithExtension(dir, extension) {
125
+ const results = [];
126
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
127
+ const full = path.join(dir, entry.name);
128
+ if (entry.isDirectory()) {
129
+ results.push(...listFilesWithExtension(full, extension));
130
+ } else if (entry.isFile() && entry.name.endsWith(extension)) {
131
+ results.push(full);
132
+ }
133
+ }
134
+ return results;
135
+ }
136
+
137
+ /** Scans every built page for root-relative, recognized-extension `src="/..."`
138
+ * references (`<img src="...">`, mainly - what `Image.astro`, `Frame.astro`,
139
+ * and a raw `![]()`/`<img>` in MDX all ultimately render down to; a
140
+ * `srcset="..."` attribute is checked too, since it's the same kind of
141
+ * reference and costs nothing extra to also cover) that aren't already
142
+ * present in the build output, and copies each one in from wherever it
143
+ * resolves in the project via resolveOutsidePublic() - the same
144
+ * fallback the dev middleware below already uses, just applied once,
145
+ * after the fact, to the rendered HTML instead of to live requests.
146
+ * This is what actually covers a plain content-body image: unlike a
147
+ * writedocs.json field, there's no fixed set of field names to read a
148
+ * content image's path from ahead of time, so instead this reads
149
+ * whatever `src` a page's own rendered markup ends up containing. */
150
+ function copyReferencedContentAssets(outDir, contentDir) {
151
+ const attrPattern = /\b(?:src|srcset)="([^"]+)"/g;
152
+ for (const htmlFile of listHtmlFiles(outDir)) {
153
+ const html = fs.readFileSync(htmlFile, 'utf8');
154
+ let match;
155
+ while ((match = attrPattern.exec(html))) {
156
+ // srcset can hold a comma-separated list of "url descriptor" pairs -
157
+ // split it out; a plain src is already just the one URL.
158
+ const candidates = match[1].split(',').map((part) => part.trim().split(/\s+/)[0]).filter(Boolean);
159
+ for (const urlPath of candidates) copyAssetIfMissing(urlPath, outDir, contentDir);
160
+ }
161
+ // Referências em CSS dentro do próprio HTML: um bloco <style> escrito
162
+ // na página (o `<style>` de uma página `mode: custom`, por exemplo) ou
163
+ // um atributo style="...". Sem isto, uma imagem usada só como
164
+ // background-image nunca era copiada para o build e dava 404 em
165
+ // produção, embora funcionasse no dev (onde o middleware resolve
166
+ // qualquer caminho direto do disco).
167
+ for (const urlPath of cssUrlReferences(html)) copyAssetIfMissing(urlPath, outDir, contentDir);
168
+ }
169
+ // O mesmo para as folhas de estilo que o build gera (_astro/*.css): é
170
+ // para onde vai o .css solto na raiz do projeto, carregado automaticamente.
171
+ for (const cssFile of listFilesWithExtension(outDir, '.css')) {
172
+ for (const urlPath of cssUrlReferences(fs.readFileSync(cssFile, 'utf8'))) {
173
+ copyAssetIfMissing(urlPath, outDir, contentDir);
174
+ }
175
+ }
176
+ }
177
+
178
+ /** Every `url(...)` target in a chunk of CSS (or HTML containing CSS),
179
+ * quoted or not. Query strings and fragments (`font.woff2?v=2`,
180
+ * `icons.svg#home`) are dropped - the file on disk has neither. */
181
+ function cssUrlReferences(text) {
182
+ const urls = [];
183
+ const urlPattern = /url\(\s*(['"]?)([^'")]+?)\1\s*\)/g;
184
+ let match;
185
+ while ((match = urlPattern.exec(text))) urls.push(match[2].trim().split(/[?#]/)[0]);
186
+ return urls;
187
+ }
188
+
189
+ /** Copies one root-relative asset reference into the build output from
190
+ * wherever it lives in the project, unless it's already there. */
191
+ function copyAssetIfMissing(urlPath, outDir, contentDir) {
192
+ if (!urlPath.startsWith('/') || urlPath.startsWith('//')) return; // only root-relative - not external, not protocol-relative
193
+ if (!looksLikeAssetPath(urlPath)) return;
194
+ const segments = urlPath.replace(/^\/+/, '').split('/');
195
+ const outPath = path.join(outDir, ...segments);
196
+ if (fs.existsSync(outPath)) return; // already present - via public/'s passthrough copy, or an earlier reference to the same file
197
+ const resolved = resolveOutsidePublic(urlPath, contentDir);
198
+ if (!resolved) return; // doesn't resolve to a real file anywhere in the project either - leave the broken reference as-is
199
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
200
+ fs.copyFileSync(resolved, outPath);
201
+ }
202
+
203
+ export function stylesAssetFallback(contentDir) {
204
+ return {
205
+ name: 'writedocs-styles-asset-fallback',
206
+ hooks: {
207
+ 'astro:server:setup': ({ server }) => {
208
+ server.middlewares.use((req, res, next) => {
209
+ if (!req.url) return next();
210
+ const urlPath = req.url.split('?')[0];
211
+ if (!looksLikeAssetPath(urlPath)) return next();
212
+ if (fs.existsSync(publicPathFor(urlPath, contentDir))) return next(); // real public/ file - Vite's own static serving already handles it, don't shadow
213
+ const resolved = resolveOutsidePublic(urlPath, contentDir);
214
+ if (!resolved) return next();
215
+ res.setHeader('Content-Type', mimeFor(resolved));
216
+ fs.createReadStream(resolved).pipe(res);
217
+ });
218
+ },
219
+ 'astro:build:done': async ({ dir }) => {
220
+ const outDir = fileURLToPath(dir);
221
+ const config = loadDocsConfig(contentDir);
222
+ // Pass 1: writedocs.json's own configured fields. Kept as its own,
223
+ // field-name-driven pass rather than folded into pass 2 below -
224
+ // seo.ogImage renders into a `<meta content="...">` tag, which
225
+ // pass 2 never scans. (Pass 2 agora também lê `url(...)` em CSS,
226
+ // então background images/fontes seriam pegas pelas duas; manter
227
+ // o pass 1 explícito não custa nada.)
228
+ for (const urlPath of collectConfiguredAssetPaths(config)) {
229
+ const segments = urlPath.replace(/^\/+/, '').split('/');
230
+ const outPath = path.join(outDir, ...segments);
231
+ if (fs.existsSync(outPath)) continue; // already present via public/'s normal passthrough copy
232
+ const resolved = resolveOutsidePublic(urlPath, contentDir);
233
+ if (!resolved) continue; // not configured to a real file anywhere - same as today, just renders a broken <link>/<img>
234
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
235
+ fs.copyFileSync(resolved, outPath);
236
+ }
237
+ // Pass 2: arbitrary content-body assets - anything a page's own
238
+ // rendered HTML references by src/srcset that pass 1 didn't
239
+ // already account for.
240
+ copyReferencedContentAssets(outDir, contentDir);
241
+ },
242
+ },
243
+ };
244
+ }