twynejs 1.0.1 → 1.0.3
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.d.ts +42 -0
- package/dist/index.js +206 -13
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ export interface PagesPluginOptions {
|
|
|
30
30
|
* URLs). The page with id `"index"` is always emitted as the top-level
|
|
31
31
|
* `index.html`, never `index/index.html`.
|
|
32
32
|
*
|
|
33
|
+
* Ignored when `singleFile` is enabled.
|
|
34
|
+
*
|
|
33
35
|
* @default false
|
|
34
36
|
*/
|
|
35
37
|
prettyUrls?: boolean;
|
|
@@ -60,6 +62,9 @@ export interface PagesPluginOptions {
|
|
|
60
62
|
* Wrap/replace the emitted HTML shell entirely. Receives the computed
|
|
61
63
|
* script/style tags and page metadata; must return a full HTML document.
|
|
62
64
|
* Falls back to a minimal built-in template.
|
|
65
|
+
*
|
|
66
|
+
* Not used when `singleFile` is enabled — the single-file shell has its
|
|
67
|
+
* own hash-router bootstrap and is built independently of `template`.
|
|
63
68
|
*/
|
|
64
69
|
template?: (ctx: PageRenderContext) => string;
|
|
65
70
|
/** Minify emitted HTML with html-minifier-terser. @default false */
|
|
@@ -134,6 +139,9 @@ export interface PagesPluginOptions {
|
|
|
134
139
|
* Put each compiled Markdown page into its own dynamically loaded chunk
|
|
135
140
|
* instead of embedding all Markdown HTML into the main bundle.
|
|
136
141
|
*
|
|
142
|
+
* Forced back to `false` when `singleFile` is enabled — a split chunk
|
|
143
|
+
* would defeat the point of a single-file export.
|
|
144
|
+
*
|
|
137
145
|
* @default false
|
|
138
146
|
*/
|
|
139
147
|
splitMarkdown?: boolean;
|
|
@@ -157,6 +165,40 @@ export interface PagesPluginOptions {
|
|
|
157
165
|
* @default "twynejs/jsx-runtime"
|
|
158
166
|
*/
|
|
159
167
|
jsxRuntimePath?: string;
|
|
168
|
+
/**
|
|
169
|
+
* Emit the whole site as a single `index.html` file with client-side,
|
|
170
|
+
* hash-based routing (`#/guide/install`) instead of one HTML file per
|
|
171
|
+
* page. Everything needed to render every "component"/"markdown" page —
|
|
172
|
+
* JS and CSS — is inlined into that one file, so the result can be
|
|
173
|
+
* opened directly via `file://`, embedded, or dropped onto any static
|
|
174
|
+
* host with no server-side rewrites.
|
|
175
|
+
*
|
|
176
|
+
* What this implies automatically, so you normally don't need to set
|
|
177
|
+
* them yourself:
|
|
178
|
+
*
|
|
179
|
+
* - Rollup output is forced to `inlineDynamicImports: true` (like
|
|
180
|
+
* `singleBundle`), so every page ends up in one JS chunk.
|
|
181
|
+
* - `splitMarkdown` is ignored (forced off) and all build data is
|
|
182
|
+
* inlined regardless of size — there is nowhere else to put it.
|
|
183
|
+
*
|
|
184
|
+
* Limitations:
|
|
185
|
+
*
|
|
186
|
+
* - Only `"component"` and `"markdown"` pages participate in the
|
|
187
|
+
* router. Liquid (`.html`) and EJS (`.ejs`) pages are build-time
|
|
188
|
+
* *static* documents and are not compatible with a client-side hash
|
|
189
|
+
* router; if any exist, they are skipped with a build warning.
|
|
190
|
+
* - The shared client `entry` is responsible for reading
|
|
191
|
+
* `window[globalVar]` on load, calling `pages[id].load()` to mount it,
|
|
192
|
+
* and listening for the `window` `"pagechange"` `CustomEvent`
|
|
193
|
+
* (`event.detail.id`) to re-render when the hash changes, since the
|
|
194
|
+
* plugin only owns the HTML shell and the routing bootstrap, not your
|
|
195
|
+
* app's rendering logic.
|
|
196
|
+
* - `template`/`prettyUrls`/per-page output file naming are not used;
|
|
197
|
+
* the output is always a single `index.html`.
|
|
198
|
+
*
|
|
199
|
+
* @default false
|
|
200
|
+
*/
|
|
201
|
+
singleFile?: boolean;
|
|
160
202
|
}
|
|
161
203
|
export interface PageRenderContext {
|
|
162
204
|
id: string;
|
package/dist/index.js
CHANGED
|
@@ -48,6 +48,17 @@ function escapeBareAngles(html) {
|
|
|
48
48
|
return `<${inner.replace(/</g, "<")}>`;
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Escape `</script` inside a string that will be embedded verbatim into an
|
|
53
|
+
* inline `<script>` tag. The HTML tokenizer ends a `<script>` element the
|
|
54
|
+
* moment it sees the literal text `</script`, regardless of whether that
|
|
55
|
+
* text sits inside a JS string/comment — without this, bundled code that
|
|
56
|
+
* happens to contain that substring (e.g. inside a template literal)
|
|
57
|
+
* would truncate the page.
|
|
58
|
+
*/
|
|
59
|
+
function escapeScriptClose(code) {
|
|
60
|
+
return code.replace(/<\/script/gi, "<\\/script");
|
|
61
|
+
}
|
|
51
62
|
let resolvedStyles = new Map();
|
|
52
63
|
const DEFAULT_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"];
|
|
53
64
|
export function pagesPlugin(options = {}) {
|
|
@@ -61,9 +72,12 @@ export function pagesPlugin(options = {}) {
|
|
|
61
72
|
"/__devtools-oxc/",
|
|
62
73
|
...(options.ignoredPathnames ?? []),
|
|
63
74
|
].filter((prefix, index, prefixes) => prefixes.indexOf(prefix) === index);
|
|
64
|
-
const splitMarkdown = options.splitMarkdown ?? false;
|
|
65
75
|
const verbose = options.verbose ?? false;
|
|
66
76
|
const singleBundle = options.singleBundle ?? false;
|
|
77
|
+
// Single-file export needs everything in one JS chunk and everything
|
|
78
|
+
// inlined, so it forces off `splitMarkdown` regardless of what was set.
|
|
79
|
+
const singleFile = options.singleFile ?? false;
|
|
80
|
+
const splitMarkdown = (options.splitMarkdown ?? false) && !singleFile;
|
|
67
81
|
const removeConsole = options.removeConsole ?? false;
|
|
68
82
|
const relativePath = options.relativePaths ?? false;
|
|
69
83
|
const addRawMarkdown = options.addRawMarkdown ?? false;
|
|
@@ -392,6 +406,10 @@ export function pagesPlugin(options = {}) {
|
|
|
392
406
|
const styles = page.styles
|
|
393
407
|
.map((style) => styleImports.get(style))
|
|
394
408
|
.join(", ");
|
|
409
|
+
// Exposed to the client so an app (in particular a `singleFile`
|
|
410
|
+
// hash-router shell) can update `document.title` itself when the
|
|
411
|
+
// active page changes, without a full page reload.
|
|
412
|
+
const titleField = `title: ${JSON.stringify(getTitle(page.id))},`;
|
|
395
413
|
/* Liquid/EJS template pages are fully static (rendered at build
|
|
396
414
|
* time) — they are served/emitted as-is and do not need a
|
|
397
415
|
* client-side entry in the pages map (which would bloat the
|
|
@@ -404,6 +422,7 @@ export function pagesPlugin(options = {}) {
|
|
|
404
422
|
return ` ${JSON.stringify(page.id)}: {
|
|
405
423
|
id: ${JSON.stringify(page.id)},
|
|
406
424
|
type: "component",
|
|
425
|
+
${titleField}
|
|
407
426
|
load: () => import(${JSON.stringify(page.importPath ?? page.scriptSource)}),
|
|
408
427
|
styles: []
|
|
409
428
|
}`;
|
|
@@ -419,6 +438,7 @@ export function pagesPlugin(options = {}) {
|
|
|
419
438
|
return ` ${JSON.stringify(page.id)}: {
|
|
420
439
|
id: ${JSON.stringify(page.id)},
|
|
421
440
|
type: "markdown",
|
|
441
|
+
${titleField}
|
|
422
442
|
load: () => import(${JSON.stringify(markdownModuleId)}),
|
|
423
443
|
styles: [${styles}]
|
|
424
444
|
}`;
|
|
@@ -429,6 +449,7 @@ export function pagesPlugin(options = {}) {
|
|
|
429
449
|
return ` ${JSON.stringify(page.id)}: {
|
|
430
450
|
id: ${JSON.stringify(page.id)},
|
|
431
451
|
type: "markdown",
|
|
452
|
+
${titleField}
|
|
432
453
|
${markdownField}
|
|
433
454
|
html: ${JSON.stringify(page.html ?? "")},
|
|
434
455
|
styles: [${styles}]
|
|
@@ -437,14 +458,17 @@ export function pagesPlugin(options = {}) {
|
|
|
437
458
|
// Große Build-Daten (z. B. gerendertes Markdown) nicht inline in
|
|
438
459
|
// den Entry packen, sondern in einen eigenen Chunk auslagern, der
|
|
439
460
|
// erst beim Laden der Seite per import() geholt wird.
|
|
461
|
+
// In `singleFile` mode there is nowhere else to put a lazily
|
|
462
|
+
// loaded chunk, so everything is always inlined regardless of size.
|
|
440
463
|
const dataSize = typeof page.buildData === "string" ? page.buildData.length : 1024;
|
|
441
|
-
const inlineData = dataSize < 8 * 1024;
|
|
464
|
+
const inlineData = singleFile || dataSize < 8 * 1024;
|
|
442
465
|
const dataField = inlineData
|
|
443
466
|
? `data: ${JSON.stringify(page.buildData ?? null)},`
|
|
444
467
|
: `loadData: () => import(${JSON.stringify(DATA_PREFIX + page.id)}).then((m) => m.default),`;
|
|
445
468
|
return ` ${JSON.stringify(page.id)}: {
|
|
446
469
|
id: ${JSON.stringify(page.id)},
|
|
447
470
|
type: "component",
|
|
471
|
+
${titleField}
|
|
448
472
|
load: () => import(${JSON.stringify(page.importPath)}),
|
|
449
473
|
${dataField}
|
|
450
474
|
styles: [${styles}]
|
|
@@ -490,6 +514,10 @@ declare module "virtual:pages" {
|
|
|
490
514
|
export interface ComponentPage {
|
|
491
515
|
id: string;
|
|
492
516
|
type: "component";
|
|
517
|
+
/** Resolved via the plugin's \`title\` option. Handy for setting
|
|
518
|
+
* \`document.title\` yourself when routing client-side (e.g. in
|
|
519
|
+
* \`singleFile\` / hash-router mode). */
|
|
520
|
+
title: string;
|
|
493
521
|
data?: unknown;
|
|
494
522
|
/** Lazy-loaded build data (large payloads). */
|
|
495
523
|
loadData?: () => Promise<unknown>;
|
|
@@ -500,6 +528,7 @@ declare module "virtual:pages" {
|
|
|
500
528
|
export interface MarkdownPage {
|
|
501
529
|
id: string;
|
|
502
530
|
type: "markdown";
|
|
531
|
+
title: string;
|
|
503
532
|
markdown?: string;
|
|
504
533
|
html: string;
|
|
505
534
|
styles: string[];
|
|
@@ -513,14 +542,20 @@ declare module "virtual:pages" {
|
|
|
513
542
|
}>;
|
|
514
543
|
}
|
|
515
544
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
545
|
+
// Note: there is intentionally no exported "LiquidPage" variant here.
|
|
546
|
+
// Liquid/EJS template pages are rendered fully at build time as
|
|
547
|
+
// standalone static documents; when one of them also has a sibling
|
|
548
|
+
// client script (\`page.[tj]s\`), it is exposed through \`virtual:pages\`
|
|
549
|
+
// as an ordinary ComponentPage (\`type: "component"\`) so the app can
|
|
550
|
+
// mount it like any other page. Without a sibling script it never
|
|
551
|
+
// appears in \`pages\` at all. A runtime object shaped like
|
|
552
|
+
// \`{ type: "liquid", ... }\` is never actually produced, so including
|
|
553
|
+
// it in this union previously made every \`pages[id]\` access widen to
|
|
554
|
+
// \`ComponentPage | LiquidPage\` after narrowing out "markdown", even
|
|
555
|
+
// though \`LiquidPage\` has no \`load\` — causing spurious
|
|
556
|
+
// "Property 'load' does not exist on type 'LiquidPage'" errors in
|
|
557
|
+
// consumers under \`tsc -b\`.
|
|
558
|
+
export type PageEntry = ComponentPage | MarkdownPage;
|
|
524
559
|
|
|
525
560
|
export const pages: Record<string, PageEntry>;
|
|
526
561
|
}
|
|
@@ -580,6 +615,31 @@ ${ctx.content}
|
|
|
580
615
|
</body>
|
|
581
616
|
</html>
|
|
582
617
|
`;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Bootstrap script for `singleFile` mode: derives the current page id
|
|
621
|
+
* from `location.hash` (`#/guide/install` → `"guide/install"`, empty →
|
|
622
|
+
* `rootId`), exposes it on `window[globalVar]` just like the multi-page
|
|
623
|
+
* template does, and re-derives it on every `hashchange`, dispatching a
|
|
624
|
+
* `"pagechange"` `CustomEvent` (`event.detail.id`) so the app's entry can
|
|
625
|
+
* re-render without a full page reload.
|
|
626
|
+
*/
|
|
627
|
+
function singleFileBootstrapScript(rootId) {
|
|
628
|
+
return `(function () {
|
|
629
|
+
var DEFAULT_PAGE = ${JSON.stringify(rootId)};
|
|
630
|
+
function currentPageId() {
|
|
631
|
+
var hash = window.location.hash || "";
|
|
632
|
+
hash = hash.replace(/^#\\/?/, "").replace(/\\/+$/, "");
|
|
633
|
+
return hash || DEFAULT_PAGE;
|
|
634
|
+
}
|
|
635
|
+
window.${globalVar} = currentPageId();
|
|
636
|
+
window.addEventListener("hashchange", function () {
|
|
637
|
+
window.${globalVar} = currentPageId();
|
|
638
|
+
window.dispatchEvent(
|
|
639
|
+
new CustomEvent("pagechange", { detail: { id: window.${globalVar} } })
|
|
640
|
+
);
|
|
641
|
+
});
|
|
642
|
+
})();`;
|
|
583
643
|
}
|
|
584
644
|
return {
|
|
585
645
|
name: "vite-plugin-pages-ssg",
|
|
@@ -624,6 +684,7 @@ ${ctx.content}
|
|
|
624
684
|
export default ${JSON.stringify({
|
|
625
685
|
id: page.id,
|
|
626
686
|
type: "markdown",
|
|
687
|
+
title: getTitle(page.id),
|
|
627
688
|
...(addRawMarkdown ? { markdown: page.markdown ?? "" } : {}),
|
|
628
689
|
html: page.html ?? "",
|
|
629
690
|
})};
|
|
@@ -686,6 +747,12 @@ ${ctx.content}
|
|
|
686
747
|
// Serve each page's HTML on its own dev URL (e.g. /guide/installation
|
|
687
748
|
// or /guide/installation.html), mirroring what generateBundle emits
|
|
688
749
|
// for production, so `vite dev` is multi-page too — not just the build.
|
|
750
|
+
//
|
|
751
|
+
// Note: `singleFile` only changes the *production build* output.
|
|
752
|
+
// The dev server keeps serving one URL per page id as usual — the
|
|
753
|
+
// hash-router bootstrap only exists in the emitted single-file
|
|
754
|
+
// production HTML — since normal multi-URL dev navigation is more
|
|
755
|
+
// convenient while developing.
|
|
689
756
|
const entry = "/" +
|
|
690
757
|
(options.entry ?? "src/main.ts")
|
|
691
758
|
.replace(/\\/g, "/")
|
|
@@ -816,9 +883,9 @@ ${ctx.content}
|
|
|
816
883
|
res.end(transformed);
|
|
817
884
|
});
|
|
818
885
|
},
|
|
819
|
-
// for single bundle
|
|
886
|
+
// for single bundle / single file
|
|
820
887
|
config() {
|
|
821
|
-
if (!singleBundle && !removeConsole) {
|
|
888
|
+
if (!singleBundle && !removeConsole && !singleFile) {
|
|
822
889
|
return {};
|
|
823
890
|
}
|
|
824
891
|
const esbuild = {};
|
|
@@ -829,7 +896,9 @@ ${ctx.content}
|
|
|
829
896
|
esbuild.drop = ["console"];
|
|
830
897
|
}
|
|
831
898
|
const build = {};
|
|
832
|
-
if (singleBundle) {
|
|
899
|
+
if (singleBundle || singleFile) {
|
|
900
|
+
// `singleFile` needs exactly one JS chunk to inline into the HTML
|
|
901
|
+
// shell, same requirement as `singleBundle`.
|
|
833
902
|
build.rollupOptions = {
|
|
834
903
|
output: {
|
|
835
904
|
inlineDynamicImports: true,
|
|
@@ -889,6 +958,27 @@ ${ctx.content}
|
|
|
889
958
|
return;
|
|
890
959
|
}
|
|
891
960
|
const entryJsChunk = jsChunk;
|
|
961
|
+
/*
|
|
962
|
+
* Single-file export: skip the normal per-page HTML emission
|
|
963
|
+
* entirely — the actual `index.html` is assembled in `writeBundle`
|
|
964
|
+
* instead (see below), once every other plugin's `generateBundle`
|
|
965
|
+
* hook (including Vite's own internal dynamic-import / preload
|
|
966
|
+
* handling, e.g. its `__VITE_PRELOAD__` substitution) has already
|
|
967
|
+
* run. Reading `entryJsChunk.code` here, in this plugin's own
|
|
968
|
+
* `generateBundle` (registered with `enforce: "pre"`, so it can run
|
|
969
|
+
* *before* Vite's internal build plugins), would risk capturing
|
|
970
|
+
* unfinished code and inlining an unresolved `__VITE_PRELOAD__`
|
|
971
|
+
* placeholder straight into the page.
|
|
972
|
+
*/
|
|
973
|
+
if (singleFile) {
|
|
974
|
+
const skippedPages = pages.filter((page) => page.type === "liquid" || page.type === "ejs");
|
|
975
|
+
if (skippedPages.length > 0) {
|
|
976
|
+
this.warn(`[vite-plugin-pages-ssg] singleFile: skipping ${skippedPages.length} Liquid/EJS page(s) (${skippedPages
|
|
977
|
+
.map((page) => page.id)
|
|
978
|
+
.join(", ")}) — they are static, build-time-rendered documents and are not compatible with the single-file hash router. Only "component" and "markdown" pages are bundled into index.html.`);
|
|
979
|
+
}
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
892
982
|
for (const page of pages) {
|
|
893
983
|
const htmlFileName = outputFileName(page.id);
|
|
894
984
|
const htmlDir = path.dirname(htmlFileName);
|
|
@@ -1053,6 +1143,109 @@ ${ctx.content}
|
|
|
1053
1143
|
});
|
|
1054
1144
|
}
|
|
1055
1145
|
},
|
|
1146
|
+
/*
|
|
1147
|
+
* Runs strictly after every plugin's `generateBundle` hook has
|
|
1148
|
+
* finished and Rollup has already written the bundle to disk — so by
|
|
1149
|
+
* now Vite's own internal transforms (dynamic-import preload
|
|
1150
|
+
* handling, `__VITE_PRELOAD__` substitution, etc.) are guaranteed to
|
|
1151
|
+
* be fully resolved in `bundle[...].code`. This is what makes it safe
|
|
1152
|
+
* to read the entry chunk's final code and inline it, unlike doing
|
|
1153
|
+
* the same thing inside `generateBundle` (see the comment there).
|
|
1154
|
+
*/
|
|
1155
|
+
async writeBundle(outputOptions, bundle) {
|
|
1156
|
+
if (!singleFile) {
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
const entryJsChunk = Object.values(bundle).find((item) => item.type === "chunk" &&
|
|
1160
|
+
item.isEntry &&
|
|
1161
|
+
item.fileName.endsWith(".js"));
|
|
1162
|
+
if (!entryJsChunk || entryJsChunk.type !== "chunk") {
|
|
1163
|
+
this.error("[vite-plugin-pages-ssg] singleFile: could not find the generated entry .js chunk while writing the bundle.");
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
const otherChunkCount = Object.values(bundle).filter((item) => item.type === "chunk" && item !== entryJsChunk).length;
|
|
1167
|
+
if (otherChunkCount > 0) {
|
|
1168
|
+
this.warn(`[vite-plugin-pages-ssg] singleFile: found ${otherChunkCount} extra JS chunk(s) besides the entry chunk that could not be inlined into index.html. They will still be left as separate files, so the build is not a true single file — check for a custom "manualChunks"/"output" config that overrides "inlineDynamicImports".`);
|
|
1169
|
+
}
|
|
1170
|
+
const cssParts = [];
|
|
1171
|
+
const cssFileNames = [];
|
|
1172
|
+
for (const [fileName, item] of Object.entries(bundle)) {
|
|
1173
|
+
if (item.type === "asset" && fileName.toLowerCase().endsWith(".css")) {
|
|
1174
|
+
const source = item.source;
|
|
1175
|
+
cssParts.push(typeof source === "string"
|
|
1176
|
+
? source
|
|
1177
|
+
: Buffer.from(source).toString("utf8"));
|
|
1178
|
+
cssFileNames.push(fileName);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
const cssContent = cssParts.join("\n");
|
|
1182
|
+
const bundledPages = pages.filter((page) => page.type === "component" || page.type === "markdown");
|
|
1183
|
+
const rootId = bundledPages.some((page) => page.id === "index")
|
|
1184
|
+
? "index"
|
|
1185
|
+
: (bundledPages[0]?.id ?? "index");
|
|
1186
|
+
const title = getTitle(rootId);
|
|
1187
|
+
const head = getHead(rootId);
|
|
1188
|
+
const bootstrapScript = singleFileBootstrapScript(rootId);
|
|
1189
|
+
const inlineJs = escapeScriptClose(entryJsChunk.code);
|
|
1190
|
+
let html = `<!doctype html>
|
|
1191
|
+
<html lang="en">
|
|
1192
|
+
<head>
|
|
1193
|
+
<meta charset="UTF-8" />
|
|
1194
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1195
|
+
<title>${escapeHtml(title)}</title>
|
|
1196
|
+
${cssContent ? `<style>\n${cssContent}\n </style>` : ""}
|
|
1197
|
+
${head}
|
|
1198
|
+
</head>
|
|
1199
|
+
<body>
|
|
1200
|
+
<div id="app"></div>
|
|
1201
|
+
|
|
1202
|
+
<script>
|
|
1203
|
+
${bootstrapScript}
|
|
1204
|
+
</script>
|
|
1205
|
+
|
|
1206
|
+
<script type="module">
|
|
1207
|
+
${inlineJs}
|
|
1208
|
+
</script>
|
|
1209
|
+
</body>
|
|
1210
|
+
</html>
|
|
1211
|
+
`;
|
|
1212
|
+
if (options.minify) {
|
|
1213
|
+
html = await minify(html, {
|
|
1214
|
+
collapseWhitespace: true,
|
|
1215
|
+
removeComments: true,
|
|
1216
|
+
removeRedundantAttributes: true,
|
|
1217
|
+
removeEmptyAttributes: true,
|
|
1218
|
+
useShortDoctype: true,
|
|
1219
|
+
minifyCSS: true,
|
|
1220
|
+
minifyJS: true,
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
const outDir = path.resolve(config.root, outputOptions.dir ?? config.build.outDir ?? "dist");
|
|
1224
|
+
fs.writeFileSync(path.join(outDir, "index.html"), html, "utf8");
|
|
1225
|
+
// The entry chunk and every CSS asset are now embedded directly in
|
|
1226
|
+
// index.html (already written above) — remove the now-redundant
|
|
1227
|
+
// loose files from disk so only index.html remains.
|
|
1228
|
+
const filesToRemove = [entryJsChunk.fileName, ...cssFileNames];
|
|
1229
|
+
const dirsTouched = new Set();
|
|
1230
|
+
for (const fileName of filesToRemove) {
|
|
1231
|
+
const filePath = path.join(outDir, fileName);
|
|
1232
|
+
if (fs.existsSync(filePath)) {
|
|
1233
|
+
fs.rmSync(filePath);
|
|
1234
|
+
dirsTouched.add(path.dirname(filePath));
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
// Clean up asset directories (e.g. "assets/") left empty behind.
|
|
1238
|
+
for (const dir of dirsTouched) {
|
|
1239
|
+
if (dir !== outDir &&
|
|
1240
|
+
fs.existsSync(dir) &&
|
|
1241
|
+
fs.readdirSync(dir).length === 0) {
|
|
1242
|
+
fs.rmdirSync(dir);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
if (verbose) {
|
|
1246
|
+
console.log(`[vite-plugin-pages-ssg] Emitted single-file build: index.html (${bundledPages.length} page(s) bundled)`);
|
|
1247
|
+
}
|
|
1248
|
+
},
|
|
1056
1249
|
};
|
|
1057
1250
|
}
|
|
1058
1251
|
function getPageId(url, pages) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "twynejs",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "twine A Vite SSG plugin that turns .ts/.tsx pages, Markdown docs and Liquid/EJS templates into a multi-page static build with a virtual:pages module.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|