vite-plugin-html-pages 1.2.3 → 1.2.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/dist/index.js +2674 -179
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/plugin.ts +76 -83
- package/src/static-assets.ts +149 -0
- package/src/assets.ts +0 -236
package/package.json
CHANGED
package/src/plugin.ts
CHANGED
|
@@ -7,13 +7,12 @@ import { createPageModuleLoader, closePageModuleLoader } from './module-loader';
|
|
|
7
7
|
import { buildPageIndex } from './page-index';
|
|
8
8
|
import { renderPage } from './render-runtime';
|
|
9
9
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from './assets';
|
|
10
|
+
buildProcessedStaticAssets,
|
|
11
|
+
collectStaticAssets,
|
|
12
|
+
copyStaticAssetSource,
|
|
13
|
+
} from './static-assets';
|
|
14
14
|
|
|
15
15
|
import type { HtPageInfo, HtPageModule, HtPagesPluginOptions } from './types';
|
|
16
|
-
import type { HtmlAssetRef } from './assets';
|
|
17
16
|
import { PLUGIN_NAME, VIRTUAL_BUILD_ENTRY_ID } from './constants';
|
|
18
17
|
|
|
19
18
|
import fs from 'node:fs';
|
|
@@ -51,10 +50,12 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
|
|
|
51
50
|
let root = process.cwd();
|
|
52
51
|
let server: ViteDevServer | null = null;
|
|
53
52
|
let devPages: HtPageInfo[] = [];
|
|
54
|
-
let htmlAssetRefs = new Map<string, HtmlAssetRef>();
|
|
55
53
|
|
|
56
54
|
const cleanUrls = options.cleanUrls ?? true;
|
|
57
55
|
const pagesDir = options.pagesDir ?? 'src';
|
|
56
|
+
const pageExtensions = options.pageExtensions?.length
|
|
57
|
+
? options.pageExtensions
|
|
58
|
+
: ['.ht.js', '.html.js', '.ht.ts', '.html.ts'];
|
|
58
59
|
|
|
59
60
|
function logDebug(enabled: boolean | undefined, ...args: unknown[]) {
|
|
60
61
|
if (!enabled) return;
|
|
@@ -163,53 +164,28 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
|
|
|
163
164
|
|
|
164
165
|
async buildStart() {
|
|
165
166
|
const entries = await discoverEntryPages(root, options);
|
|
166
|
-
|
|
167
|
+
|
|
167
168
|
for (const entry of entries) {
|
|
168
169
|
this.addWatchFile(entry.entryPath);
|
|
169
170
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (server) {
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
htmlAssetRefs.clear();
|
|
177
|
-
|
|
178
|
-
const { modulesByEntry, pages } = await buildPagesPipeline();
|
|
179
|
-
|
|
180
|
-
const htmlByPageKey = new Map<string, { html: string; pageDir?: string }>();
|
|
181
|
-
|
|
182
|
-
for (const page of pages) {
|
|
183
|
-
const mod = modulesByEntry.get(page.entryPath);
|
|
184
|
-
|
|
185
|
-
if (!mod) {
|
|
186
|
-
throw new Error(
|
|
187
|
-
`[${PLUGIN_NAME}] Missing module for page entry: ${page.entryPath}`,
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const html = await renderPage(page, mod, false);
|
|
192
|
-
|
|
193
|
-
htmlByPageKey.set(page.entryPath, {
|
|
194
|
-
html,
|
|
195
|
-
pageDir: path.dirname(page.absolutePath),
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
htmlAssetRefs = await collectHtmlAssetRefs({
|
|
200
|
-
ctx: this,
|
|
171
|
+
|
|
172
|
+
const staticAssets = await collectStaticAssets({
|
|
201
173
|
root,
|
|
202
174
|
pagesDir,
|
|
203
|
-
|
|
175
|
+
pageExtensions,
|
|
204
176
|
});
|
|
205
|
-
|
|
177
|
+
|
|
178
|
+
for (const asset of staticAssets) {
|
|
179
|
+
this.addWatchFile(asset.absolutePath);
|
|
180
|
+
}
|
|
181
|
+
|
|
206
182
|
logDebug(
|
|
207
183
|
options.debug,
|
|
208
|
-
'
|
|
209
|
-
|
|
210
|
-
kind:
|
|
211
|
-
|
|
212
|
-
|
|
184
|
+
'static assets',
|
|
185
|
+
staticAssets.map((asset) => ({
|
|
186
|
+
kind: asset.kind,
|
|
187
|
+
input: asset.relativePathFromSrc,
|
|
188
|
+
output: asset.outputFileName,
|
|
213
189
|
})),
|
|
214
190
|
);
|
|
215
191
|
},
|
|
@@ -248,22 +224,26 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
|
|
|
248
224
|
try {
|
|
249
225
|
const { modulesByEntry, pages } = await buildPagesPipeline();
|
|
250
226
|
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
227
|
+
const staticAssets = await collectStaticAssets({
|
|
228
|
+
root,
|
|
229
|
+
pagesDir,
|
|
230
|
+
pageExtensions,
|
|
255
231
|
});
|
|
256
232
|
|
|
257
233
|
logDebug(
|
|
258
234
|
options.debug,
|
|
259
|
-
'
|
|
260
|
-
|
|
235
|
+
'emitting pages',
|
|
236
|
+
pages.map((p) => p.fileName),
|
|
261
237
|
);
|
|
262
238
|
|
|
263
239
|
logDebug(
|
|
264
240
|
options.debug,
|
|
265
|
-
'emitting
|
|
266
|
-
|
|
241
|
+
'emitting static assets',
|
|
242
|
+
staticAssets.map((asset) => ({
|
|
243
|
+
kind: asset.kind,
|
|
244
|
+
input: asset.relativePathFromSrc,
|
|
245
|
+
output: asset.outputFileName,
|
|
246
|
+
})),
|
|
267
247
|
);
|
|
268
248
|
|
|
269
249
|
const limit = pLimit(options.renderConcurrency ?? 8);
|
|
@@ -271,9 +251,34 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
|
|
|
271
251
|
options.renderBatchSize ??
|
|
272
252
|
Math.max(options.renderConcurrency ?? 8, 32);
|
|
273
253
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
254
|
+
const processedOutputs = await buildProcessedStaticAssets({
|
|
255
|
+
root,
|
|
256
|
+
pagesDir,
|
|
257
|
+
assets: staticAssets,
|
|
258
|
+
minify: true,
|
|
259
|
+
sourcemap: false,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
for (const [fileName, source] of processedOutputs) {
|
|
263
|
+
this.emitFile({
|
|
264
|
+
type: 'asset',
|
|
265
|
+
fileName,
|
|
266
|
+
source,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
for (const asset of staticAssets) {
|
|
271
|
+
if (asset.kind !== 'copy') continue;
|
|
272
|
+
|
|
273
|
+
const source = await copyStaticAssetSource(asset);
|
|
274
|
+
|
|
275
|
+
this.emitFile({
|
|
276
|
+
type: 'asset',
|
|
277
|
+
fileName: asset.outputFileName,
|
|
278
|
+
source,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
277
282
|
for (const batch of chunkArray(pages, batchSize)) {
|
|
278
283
|
await Promise.all(
|
|
279
284
|
batch.map((page) =>
|
|
@@ -286,8 +291,7 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
|
|
|
286
291
|
);
|
|
287
292
|
}
|
|
288
293
|
|
|
289
|
-
|
|
290
|
-
html = rewriteHtmlAssetUrls(html, assetReplacements);
|
|
294
|
+
const html = await renderPage(page, mod, false);
|
|
291
295
|
|
|
292
296
|
this.emitFile({
|
|
293
297
|
type: 'asset',
|
|
@@ -299,9 +303,6 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
|
|
|
299
303
|
);
|
|
300
304
|
}
|
|
301
305
|
|
|
302
|
-
// ---------------------------
|
|
303
|
-
// 2. 404.html
|
|
304
|
-
// ---------------------------
|
|
305
306
|
const notFoundPage = pages.find((p) => p.routePath === '/404');
|
|
306
307
|
|
|
307
308
|
if (notFoundPage) {
|
|
@@ -313,8 +314,7 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
|
|
|
313
314
|
);
|
|
314
315
|
}
|
|
315
316
|
|
|
316
|
-
|
|
317
|
-
html = rewriteHtmlAssetUrls(html, assetReplacements);
|
|
317
|
+
const html = await renderPage(notFoundPage, mod, false);
|
|
318
318
|
|
|
319
319
|
this.emitFile({
|
|
320
320
|
type: 'asset',
|
|
@@ -378,9 +378,6 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
|
|
|
378
378
|
logDebug(options.debug, 'generated default 404.html');
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
-
// ---------------------------
|
|
382
|
-
// 3. Sitemap
|
|
383
|
-
// ---------------------------
|
|
384
381
|
const sitemapBase = options.site ?? '';
|
|
385
382
|
|
|
386
383
|
const sitemapRoutes = [...new Set(pages.map((p) => p.routePath))].filter(
|
|
@@ -401,35 +398,31 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
|
|
|
401
398
|
logDebug(options.debug, 'generated sitemap.xml');
|
|
402
399
|
}
|
|
403
400
|
|
|
404
|
-
|
|
405
|
-
// 4. RSS
|
|
406
|
-
// ---------------------------
|
|
407
|
-
if (options.rss?.site) {
|
|
408
|
-
const routePrefix = options.rss.routePrefix ?? '/blog';
|
|
401
|
+
const rss = options.rss;
|
|
409
402
|
|
|
403
|
+
if (rss?.site) {
|
|
404
|
+
const routePrefix = rss.routePrefix ?? '/blog';
|
|
405
|
+
|
|
410
406
|
const rssItems = pages
|
|
411
407
|
.filter((page) => page.routePath.startsWith(routePrefix))
|
|
412
408
|
.map((page) => {
|
|
413
|
-
const url = `${
|
|
414
|
-
|
|
409
|
+
const url = `${rss.site}${page.routePath}`;
|
|
410
|
+
|
|
415
411
|
return ` <item>\n <title>${page.routePath}</title>\n <link>${url}</link>\n <guid>${url}</guid>\n </item>`;
|
|
416
412
|
})
|
|
417
413
|
.join('\n');
|
|
418
|
-
|
|
419
|
-
const
|
|
420
|
-
|
|
414
|
+
|
|
415
|
+
const rssXml = `<?xml version="1.0" encoding="UTF-8"?>\n<rss version="2.0">\n<channel>\n <title>${rss.title ?? PLUGIN_NAME}</title>\n <link>${rss.site}</link>\n <description>${rss.description ?? 'RSS feed'}</description>\n${rssItems}\n</channel>\n</rss>\n`;
|
|
416
|
+
|
|
421
417
|
this.emitFile({
|
|
422
418
|
type: 'asset',
|
|
423
419
|
fileName: 'rss.xml',
|
|
424
|
-
source:
|
|
420
|
+
source: rssXml,
|
|
425
421
|
});
|
|
426
|
-
|
|
422
|
+
|
|
427
423
|
logDebug(options.debug, 'generated rss.xml');
|
|
428
424
|
}
|
|
429
425
|
|
|
430
|
-
// ---------------------------
|
|
431
|
-
// 5. Remove virtual entry chunk
|
|
432
|
-
// ---------------------------
|
|
433
426
|
for (const [fileName, output] of Object.entries(bundle)) {
|
|
434
427
|
if (
|
|
435
428
|
output.type === 'chunk' &&
|
|
@@ -441,8 +434,8 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
|
|
|
441
434
|
} finally {
|
|
442
435
|
await closePageModuleLoader();
|
|
443
436
|
}
|
|
444
|
-
}
|
|
437
|
+
}
|
|
445
438
|
};
|
|
446
439
|
}
|
|
447
440
|
|
|
448
|
-
export default htPages;
|
|
441
|
+
export default htPages;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
|
+
import * as esbuild from 'esbuild';
|
|
5
|
+
|
|
6
|
+
export interface StaticAssetFile {
|
|
7
|
+
absolutePath: string;
|
|
8
|
+
relativePathFromSrc: string;
|
|
9
|
+
outputFileName: string;
|
|
10
|
+
kind: 'copy' | 'process';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CollectStaticAssetsArgs {
|
|
14
|
+
root: string;
|
|
15
|
+
pagesDir: string;
|
|
16
|
+
pageExtensions: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeSlashes(value: string): string {
|
|
20
|
+
return value.replace(/\\/g, '/');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hasAnySuffix(value: string, suffixes: string[]): boolean {
|
|
24
|
+
return suffixes.some((suffix) => value.endsWith(suffix));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function shouldIgnoreFile(rel: string): boolean {
|
|
28
|
+
return (
|
|
29
|
+
rel.endsWith('.d.ts') ||
|
|
30
|
+
rel.endsWith('.map') ||
|
|
31
|
+
rel.endsWith('.tsbuildinfo') ||
|
|
32
|
+
rel.startsWith('.') ||
|
|
33
|
+
rel.includes('/.')
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isProcessableAsset(rel: string): boolean {
|
|
38
|
+
return (
|
|
39
|
+
rel.endsWith('.js') ||
|
|
40
|
+
rel.endsWith('.mjs') ||
|
|
41
|
+
rel.endsWith('.ts') ||
|
|
42
|
+
rel.endsWith('.css')
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function toOutputFileName(relativePathFromSrc: string): string {
|
|
47
|
+
if (relativePathFromSrc.endsWith('.ts')) {
|
|
48
|
+
return relativePathFromSrc.slice(0, -3) + '.js';
|
|
49
|
+
}
|
|
50
|
+
return relativePathFromSrc;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function collectStaticAssets(
|
|
54
|
+
args: CollectStaticAssetsArgs,
|
|
55
|
+
): Promise<StaticAssetFile[]> {
|
|
56
|
+
const { root, pagesDir, pageExtensions } = args;
|
|
57
|
+
const srcDir = path.join(root, pagesDir);
|
|
58
|
+
|
|
59
|
+
const entries = await fg('**/*', {
|
|
60
|
+
cwd: srcDir,
|
|
61
|
+
onlyFiles: true,
|
|
62
|
+
dot: false,
|
|
63
|
+
absolute: false,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const assets: StaticAssetFile[] = [];
|
|
67
|
+
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
const rel = normalizeSlashes(entry);
|
|
70
|
+
|
|
71
|
+
if (shouldIgnoreFile(rel)) continue;
|
|
72
|
+
if (hasAnySuffix(rel, pageExtensions)) continue;
|
|
73
|
+
|
|
74
|
+
const absolutePath = path.join(srcDir, rel);
|
|
75
|
+
|
|
76
|
+
assets.push({
|
|
77
|
+
absolutePath,
|
|
78
|
+
relativePathFromSrc: rel,
|
|
79
|
+
outputFileName: normalizeSlashes(toOutputFileName(rel)),
|
|
80
|
+
kind: isProcessableAsset(rel) ? 'process' : 'copy',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return assets;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function copyStaticAssetSource(
|
|
88
|
+
asset: StaticAssetFile,
|
|
89
|
+
): Promise<Uint8Array> {
|
|
90
|
+
return fs.readFile(asset.absolutePath);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function buildProcessedStaticAssets(args: {
|
|
94
|
+
root: string;
|
|
95
|
+
pagesDir: string;
|
|
96
|
+
assets: StaticAssetFile[];
|
|
97
|
+
minify?: boolean;
|
|
98
|
+
sourcemap?: boolean;
|
|
99
|
+
}): Promise<Map<string, string | Uint8Array>> {
|
|
100
|
+
const { root, pagesDir, assets, minify = true, sourcemap = false } = args;
|
|
101
|
+
|
|
102
|
+
const processable = assets.filter((a) => a.kind === 'process');
|
|
103
|
+
const out = new Map<string, string | Uint8Array>();
|
|
104
|
+
|
|
105
|
+
if (processable.length === 0) {
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const srcDir = path.join(root, pagesDir);
|
|
110
|
+
const distDir = path.join(root, 'dist');
|
|
111
|
+
|
|
112
|
+
const result = await esbuild.build({
|
|
113
|
+
entryPoints: processable.map((a) => a.absolutePath),
|
|
114
|
+
absWorkingDir: root,
|
|
115
|
+
outbase: srcDir,
|
|
116
|
+
outdir: distDir,
|
|
117
|
+
bundle: true,
|
|
118
|
+
splitting: true,
|
|
119
|
+
treeShaking: true,
|
|
120
|
+
minify,
|
|
121
|
+
sourcemap,
|
|
122
|
+
format: 'esm',
|
|
123
|
+
target: 'es2020',
|
|
124
|
+
platform: 'browser',
|
|
125
|
+
write: false,
|
|
126
|
+
entryNames: '[dir]/[name]',
|
|
127
|
+
assetNames: '[dir]/[name]',
|
|
128
|
+
loader: {
|
|
129
|
+
'.css': 'css',
|
|
130
|
+
'.png': 'file',
|
|
131
|
+
'.jpg': 'file',
|
|
132
|
+
'.jpeg': 'file',
|
|
133
|
+
'.gif': 'file',
|
|
134
|
+
'.svg': 'file',
|
|
135
|
+
'.webp': 'file',
|
|
136
|
+
'.woff': 'file',
|
|
137
|
+
'.woff2': 'file',
|
|
138
|
+
'.ttf': 'file',
|
|
139
|
+
'.otf': 'file',
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
for (const file of result.outputFiles) {
|
|
144
|
+
const rel = normalizeSlashes(path.relative(distDir, file.path));
|
|
145
|
+
out.set(rel, file.text ?? file.contents);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return out;
|
|
149
|
+
}
|
package/src/assets.ts
DELETED
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import type {
|
|
4
|
-
OutputBundle,
|
|
5
|
-
PluginContext,
|
|
6
|
-
} from 'rollup';
|
|
7
|
-
|
|
8
|
-
export type HtmlAssetKind = 'css' | 'js';
|
|
9
|
-
|
|
10
|
-
export interface HtmlAssetRef {
|
|
11
|
-
kind: HtmlAssetKind;
|
|
12
|
-
originalUrl: string;
|
|
13
|
-
absolutePath: string;
|
|
14
|
-
refId: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface ExtractedHtmlAsset {
|
|
18
|
-
kind: HtmlAssetKind;
|
|
19
|
-
url: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const EXTERNAL_URL_RE = /^(?:[a-z]+:)?\/\//i;
|
|
23
|
-
|
|
24
|
-
export function isLocalAssetUrl(url: string): boolean {
|
|
25
|
-
return (
|
|
26
|
-
!!url &&
|
|
27
|
-
!url.startsWith('data:') &&
|
|
28
|
-
!url.startsWith('mailto:') &&
|
|
29
|
-
!url.startsWith('tel:') &&
|
|
30
|
-
!url.startsWith('#') &&
|
|
31
|
-
!EXTERNAL_URL_RE.test(url)
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function stripQueryAndHash(url: string): string {
|
|
36
|
-
return url.split('#')[0].split('?')[0];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function extractHtmlAssets(html: string): ExtractedHtmlAsset[] {
|
|
40
|
-
const assets: ExtractedHtmlAsset[] = [];
|
|
41
|
-
|
|
42
|
-
for (const match of html.matchAll(
|
|
43
|
-
/<link\b[^>]*\brel=["']stylesheet["'][^>]*\bhref=["']([^"']+)["'][^>]*>/gi,
|
|
44
|
-
)) {
|
|
45
|
-
assets.push({ kind: 'css', url: match[1] });
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
for (const match of html.matchAll(
|
|
49
|
-
/<script\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/gi,
|
|
50
|
-
)) {
|
|
51
|
-
assets.push({ kind: 'js', url: match[1] });
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return dedupeExtractedAssets(assets);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function dedupeExtractedAssets(
|
|
58
|
-
assets: ExtractedHtmlAsset[],
|
|
59
|
-
): ExtractedHtmlAsset[] {
|
|
60
|
-
const seen = new Set<string>();
|
|
61
|
-
const out: ExtractedHtmlAsset[] = [];
|
|
62
|
-
|
|
63
|
-
for (const asset of assets) {
|
|
64
|
-
const key = `${asset.kind}:${asset.url}`;
|
|
65
|
-
if (seen.has(key)) continue;
|
|
66
|
-
seen.add(key);
|
|
67
|
-
out.push(asset);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return out;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function resolveLocalAssetPath(args: {
|
|
74
|
-
root: string;
|
|
75
|
-
pagesDir: string;
|
|
76
|
-
pageDir?: string;
|
|
77
|
-
url: string;
|
|
78
|
-
}): string | null {
|
|
79
|
-
const { root, pagesDir, pageDir, url } = args;
|
|
80
|
-
|
|
81
|
-
if (!isLocalAssetUrl(url)) return null;
|
|
82
|
-
|
|
83
|
-
const cleanUrl = stripQueryAndHash(url);
|
|
84
|
-
|
|
85
|
-
let abs: string;
|
|
86
|
-
|
|
87
|
-
if (cleanUrl.startsWith('/')) {
|
|
88
|
-
abs = path.join(root, pagesDir, cleanUrl.slice(1));
|
|
89
|
-
} else if (cleanUrl.startsWith(`${pagesDir}/`)) {
|
|
90
|
-
abs = path.join(root, cleanUrl);
|
|
91
|
-
} else {
|
|
92
|
-
const baseDir = pageDir ?? path.join(root, pagesDir);
|
|
93
|
-
abs = path.resolve(baseDir, cleanUrl);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return fs.existsSync(abs) ? abs : null;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function emitHtmlAsset(args: {
|
|
100
|
-
ctx: PluginContext;
|
|
101
|
-
kind: HtmlAssetKind;
|
|
102
|
-
absolutePath: string;
|
|
103
|
-
}): string {
|
|
104
|
-
const { ctx, kind, absolutePath } = args;
|
|
105
|
-
|
|
106
|
-
if (kind === 'css' || kind === 'js') {
|
|
107
|
-
return ctx.emitFile({
|
|
108
|
-
type: 'chunk',
|
|
109
|
-
id: absolutePath,
|
|
110
|
-
name: path.basename(absolutePath, path.extname(absolutePath)),
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
throw new Error(`[vite-plugin-html-pages] Unsupported asset kind: ${kind}`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function replaceAllLiteral(
|
|
118
|
-
input: string,
|
|
119
|
-
search: string,
|
|
120
|
-
replacement: string,
|
|
121
|
-
): string {
|
|
122
|
-
return input.split(search).join(replacement);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function rewriteHtmlAssetUrls(
|
|
126
|
-
html: string,
|
|
127
|
-
replacements: Map<string, string>,
|
|
128
|
-
): string {
|
|
129
|
-
let out = html;
|
|
130
|
-
|
|
131
|
-
for (const [originalUrl, builtUrl] of replacements) {
|
|
132
|
-
out = replaceAllLiteral(
|
|
133
|
-
out,
|
|
134
|
-
`href="${originalUrl}"`,
|
|
135
|
-
`href="${builtUrl}"`,
|
|
136
|
-
);
|
|
137
|
-
out = replaceAllLiteral(
|
|
138
|
-
out,
|
|
139
|
-
`href='${originalUrl}'`,
|
|
140
|
-
`href='${builtUrl}'`,
|
|
141
|
-
);
|
|
142
|
-
out = replaceAllLiteral(
|
|
143
|
-
out,
|
|
144
|
-
`src="${originalUrl}"`,
|
|
145
|
-
`src="${builtUrl}"`,
|
|
146
|
-
);
|
|
147
|
-
out = replaceAllLiteral(
|
|
148
|
-
out,
|
|
149
|
-
`src='${originalUrl}'`,
|
|
150
|
-
`src='${builtUrl}'`,
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return out;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export async function collectHtmlAssetRefs(args: {
|
|
158
|
-
ctx: PluginContext;
|
|
159
|
-
root: string;
|
|
160
|
-
pagesDir: string;
|
|
161
|
-
htmlByPageKey: Map<string, { html: string; pageDir?: string }>;
|
|
162
|
-
}): Promise<Map<string, HtmlAssetRef>> {
|
|
163
|
-
const { ctx, root, pagesDir, htmlByPageKey } = args;
|
|
164
|
-
const refs = new Map<string, HtmlAssetRef>();
|
|
165
|
-
|
|
166
|
-
for (const { html, pageDir } of htmlByPageKey.values()) {
|
|
167
|
-
const assets = extractHtmlAssets(html);
|
|
168
|
-
|
|
169
|
-
for (const asset of assets) {
|
|
170
|
-
const abs = resolveLocalAssetPath({
|
|
171
|
-
root,
|
|
172
|
-
pagesDir,
|
|
173
|
-
pageDir,
|
|
174
|
-
url: asset.url,
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
if (!abs) continue;
|
|
178
|
-
|
|
179
|
-
const key = `${asset.kind}:${asset.url}`;
|
|
180
|
-
if (refs.has(key)) continue;
|
|
181
|
-
|
|
182
|
-
const refId = emitHtmlAsset({
|
|
183
|
-
ctx,
|
|
184
|
-
kind: asset.kind,
|
|
185
|
-
absolutePath: abs,
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
refs.set(key, {
|
|
189
|
-
kind: asset.kind,
|
|
190
|
-
originalUrl: asset.url,
|
|
191
|
-
absolutePath: abs,
|
|
192
|
-
refId,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return refs;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export function buildHtmlAssetReplacementMap(args: {
|
|
201
|
-
ctx: PluginContext;
|
|
202
|
-
refs: Map<string, HtmlAssetRef>;
|
|
203
|
-
bundle: OutputBundle;
|
|
204
|
-
}): Map<string, string> {
|
|
205
|
-
const { ctx, refs, bundle } = args;
|
|
206
|
-
const replacements = new Map<string, string>();
|
|
207
|
-
|
|
208
|
-
for (const ref of refs.values()) {
|
|
209
|
-
if (ref.kind === 'js') {
|
|
210
|
-
const fileName = ctx.getFileName(ref.refId);
|
|
211
|
-
replacements.set(ref.originalUrl, `/${fileName}`);
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (ref.kind === 'css') {
|
|
216
|
-
const jsEntryFile = ctx.getFileName(ref.refId);
|
|
217
|
-
const jsChunk = bundle[jsEntryFile];
|
|
218
|
-
|
|
219
|
-
if (
|
|
220
|
-
jsChunk &&
|
|
221
|
-
jsChunk.type === 'chunk' &&
|
|
222
|
-
'viteMetadata' in jsChunk &&
|
|
223
|
-
jsChunk.viteMetadata?.importedCss &&
|
|
224
|
-
jsChunk.viteMetadata.importedCss.size > 0
|
|
225
|
-
) {
|
|
226
|
-
const cssFile = [...jsChunk.viteMetadata.importedCss][0];
|
|
227
|
-
replacements.set(ref.originalUrl, `/${cssFile}`);
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
replacements.set(ref.originalUrl, `/${jsEntryFile}`);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return replacements;
|
|
236
|
-
}
|