@writedocs/generator 0.4.3 → 0.4.5
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/astro.config.mjs +41 -0
- package/package.json +1 -1
- package/src/lib/styles-asset-integration.js +244 -210
package/astro.config.mjs
CHANGED
|
@@ -147,6 +147,40 @@ function collectNoindexIds(rootContentDir) {
|
|
|
147
147
|
|
|
148
148
|
const noindexIds = siteUrl ? collectNoindexIds(contentDir) : new Set();
|
|
149
149
|
|
|
150
|
+
/** O @astrojs/sitemap sempre grava um sitemap-index.xml apontando para
|
|
151
|
+
* sitemap-0.xml, sitemap-1.xml, ... - mesmo quando há um arquivo só, que é
|
|
152
|
+
* o caso de todo site de docs abaixo de 50.000 páginas. Aqui o único
|
|
153
|
+
* sitemap-0.xml vira sitemap.xml (o endereço que crawler e ferramenta de
|
|
154
|
+
* SEO procuram primeiro) e o índice é apagado. Com mais de um arquivo o
|
|
155
|
+
* índice é necessário (o protocolo não permite um único arquivo acima de
|
|
156
|
+
* 50.000 URLs), então tudo fica como o plugin gravou. Um sitemap.xml
|
|
157
|
+
* vindo do public/ do próprio site também não é sobrescrito. */
|
|
158
|
+
function singleSitemapFile() {
|
|
159
|
+
return {
|
|
160
|
+
name: 'writedocs-single-sitemap',
|
|
161
|
+
hooks: {
|
|
162
|
+
'astro:build:done': ({ dir }) => {
|
|
163
|
+
const outDir = fileURLToPath(dir);
|
|
164
|
+
const chunks = fs.readdirSync(outDir).filter((f) => /^sitemap-\d+\.xml$/.test(f));
|
|
165
|
+
const target = path.join(outDir, 'sitemap.xml');
|
|
166
|
+
if (chunks.length !== 1) {
|
|
167
|
+
console.log(`[writedocs] ${chunks.length} sitemap files generated - keeping sitemap-index.xml.`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (fs.existsSync(target)) {
|
|
171
|
+
console.log('[writedocs] public/sitemap.xml exists - keeping it and the generated sitemap-index.xml.');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
fs.renameSync(path.join(outDir, chunks[0]), target);
|
|
175
|
+
fs.rmSync(path.join(outDir, 'sitemap-index.xml'), { force: true });
|
|
176
|
+
// O próprio plugin acabou de logar "sitemap-index.xml created"; esta
|
|
177
|
+
// linha diz o que de fato ficou no build.
|
|
178
|
+
console.log(`[writedocs] Replaced ${chunks[0]} + sitemap-index.xml with a single sitemap.xml.`);
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
150
184
|
// writedocs.json's `redirects` array -> Astro's own `redirects` config option
|
|
151
185
|
// (a plain `{ [source]: destination }` record - see redirectSchema in
|
|
152
186
|
// lib/config.ts for why there's no `permanent`/status-code field: this
|
|
@@ -207,12 +241,19 @@ export default defineConfig({
|
|
|
207
241
|
...(siteUrl
|
|
208
242
|
? [
|
|
209
243
|
sitemap({
|
|
244
|
+
// Teto do protocolo de sitemap por arquivo (o default do
|
|
245
|
+
// @astrojs/sitemap é 45.000). Até aqui, tudo cabe num único
|
|
246
|
+
// sitemap.xml - ver singleSitemapFile() abaixo.
|
|
247
|
+
entryLimit: 50000,
|
|
210
248
|
filter: (page) => {
|
|
211
249
|
const pathname = new URL(page).pathname;
|
|
212
250
|
const id = normalizeEntryId(pathname);
|
|
213
251
|
return !noindexIds.has(id);
|
|
214
252
|
},
|
|
215
253
|
}),
|
|
254
|
+
// Precisa vir depois do sitemap(): o Astro roda os hooks
|
|
255
|
+
// astro:build:done na ordem deste array, um de cada vez.
|
|
256
|
+
singleSitemapFile(),
|
|
216
257
|
]
|
|
217
258
|
: []),
|
|
218
259
|
// Lets styles.favicon/styles.logo/styles.background.images/seo.ogImage/
|
package/package.json
CHANGED
|
@@ -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
|
-
// (``, 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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
+
// (``, 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
|
+
}
|