twynejs 1.0.2 → 1.0.4

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.
Files changed (2) hide show
  1. package/dist/index.js +130 -91
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -70,6 +70,7 @@ export function pagesPlugin(options = {}) {
70
70
  "/__devtools-vite/",
71
71
  "/__devtools-rolldown/",
72
72
  "/__devtools-oxc/",
73
+ "/__vitest__/",
73
74
  ...(options.ignoredPathnames ?? []),
74
75
  ].filter((prefix, index, prefixes) => prefixes.indexOf(prefix) === index);
75
76
  const verbose = options.verbose ?? false;
@@ -542,15 +543,20 @@ declare module "virtual:pages" {
542
543
  }>;
543
544
  }
544
545
 
545
- export interface LiquidPage {
546
- id: string;
547
- type: "liquid";
548
- title: string;
549
- html: string;
550
- styles: string[];
551
- }
552
-
553
- export type PageEntry = ComponentPage | MarkdownPage | LiquidPage;
546
+ // Note: there is intentionally no exported "LiquidPage" variant here.
547
+ // Liquid/EJS template pages are rendered fully at build time as
548
+ // standalone static documents; when one of them also has a sibling
549
+ // client script (\`page.[tj]s\`), it is exposed through \`virtual:pages\`
550
+ // as an ordinary ComponentPage (\`type: "component"\`) so the app can
551
+ // mount it like any other page. Without a sibling script it never
552
+ // appears in \`pages\` at all. A runtime object shaped like
553
+ // \`{ type: "liquid", ... }\` is never actually produced, so including
554
+ // it in this union previously made every \`pages[id]\` access widen to
555
+ // \`ComponentPage | LiquidPage\` after narrowing out "markdown", even
556
+ // though \`LiquidPage\` has no \`load\` — causing spurious
557
+ // "Property 'load' does not exist on type 'LiquidPage'" errors in
558
+ // consumers under \`tsc -b\`.
559
+ export type PageEntry = ComponentPage | MarkdownPage;
554
560
 
555
561
  export const pages: Record<string, PageEntry>;
556
562
  }
@@ -953,16 +959,18 @@ ${ctx.content}
953
959
  return;
954
960
  }
955
961
  const entryJsChunk = jsChunk;
956
- /* ------------------------------------------------------------------
957
- * Single-file export: one `index.html` with a `#` hash router.
958
- *
959
- * All "component"/"markdown" pages already flow through the same
960
- * entry chunk (thanks to `inlineDynamicImports`, forced above), so
961
- * this just has to inline that chunk's JS and every emitted CSS
962
- * asset straight into one HTML document instead of writing one HTML
963
- * file per page, then remove those now-inlined files from the
964
- * bundle so they aren't also emitted standalone.
965
- * ------------------------------------------------------------------ */
962
+ /*
963
+ * Single-file export: skip the normal per-page HTML emission
964
+ * entirely — the actual `index.html` is assembled in `writeBundle`
965
+ * instead (see below), once every other plugin's `generateBundle`
966
+ * hook (including Vite's own internal dynamic-import / preload
967
+ * handling, e.g. its `__VITE_PRELOAD__` substitution) has already
968
+ * run. Reading `entryJsChunk.code` here, in this plugin's own
969
+ * `generateBundle` (registered with `enforce: "pre"`, so it can run
970
+ * *before* Vite's internal build plugins), would risk capturing
971
+ * unfinished code and inlining an unresolved `__VITE_PRELOAD__`
972
+ * placeholder straight into the page.
973
+ */
966
974
  if (singleFile) {
967
975
  const skippedPages = pages.filter((page) => page.type === "liquid" || page.type === "ejs");
968
976
  if (skippedPages.length > 0) {
@@ -970,78 +978,6 @@ ${ctx.content}
970
978
  .map((page) => page.id)
971
979
  .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.`);
972
980
  }
973
- const otherChunkCount = Object.values(bundle).filter((item) => item.type === "chunk" && item !== entryJsChunk).length;
974
- if (otherChunkCount > 0) {
975
- 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 emitted as separate files, so the build is not a true single file — check for a custom "manualChunks"/"output" config that overrides "inlineDynamicImports".`);
976
- }
977
- const cssParts = [];
978
- const cssFileNames = [];
979
- for (const [fileName, item] of Object.entries(bundle)) {
980
- if (item.type === "asset" &&
981
- fileName.toLowerCase().endsWith(".css")) {
982
- const source = item.source;
983
- cssParts.push(typeof source === "string"
984
- ? source
985
- : Buffer.from(source).toString("utf8"));
986
- cssFileNames.push(fileName);
987
- }
988
- }
989
- const cssContent = cssParts.join("\n");
990
- const bundledPages = pages.filter((page) => page.type === "component" || page.type === "markdown");
991
- const rootId = bundledPages.some((page) => page.id === "index")
992
- ? "index"
993
- : (bundledPages[0]?.id ?? "index");
994
- const title = getTitle(rootId);
995
- const head = getHead(rootId);
996
- const bootstrapScript = singleFileBootstrapScript(rootId);
997
- const inlineJs = escapeScriptClose(entryJsChunk.code);
998
- let html = `<!doctype html>
999
- <html lang="en">
1000
- <head>
1001
- <meta charset="UTF-8" />
1002
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1003
- <title>${escapeHtml(title)}</title>
1004
- ${cssContent ? `<style>\n${cssContent}\n </style>` : ""}
1005
- ${head}
1006
- </head>
1007
- <body>
1008
- <div id="app"></div>
1009
-
1010
- <script>
1011
- ${bootstrapScript}
1012
- </script>
1013
-
1014
- <script type="module">
1015
- ${inlineJs}
1016
- </script>
1017
- </body>
1018
- </html>
1019
- `;
1020
- if (options.minify) {
1021
- html = await minify(html, {
1022
- collapseWhitespace: true,
1023
- removeComments: true,
1024
- removeRedundantAttributes: true,
1025
- removeEmptyAttributes: true,
1026
- useShortDoctype: true,
1027
- minifyCSS: true,
1028
- minifyJS: true,
1029
- });
1030
- }
1031
- // The entry chunk and every CSS asset are now embedded directly
1032
- // in index.html — drop them so they don't also ship as loose files.
1033
- delete bundle[entryJsChunk.fileName];
1034
- for (const fileName of cssFileNames) {
1035
- delete bundle[fileName];
1036
- }
1037
- this.emitFile({
1038
- type: "asset",
1039
- fileName: "index.html",
1040
- source: html,
1041
- });
1042
- if (verbose) {
1043
- console.log(`[vite-plugin-pages-ssg] Emitted single-file build: index.html (${bundledPages.length} page(s) bundled, ${skippedPages.length} skipped)`);
1044
- }
1045
981
  return;
1046
982
  }
1047
983
  for (const page of pages) {
@@ -1208,6 +1144,109 @@ ${inlineJs}
1208
1144
  });
1209
1145
  }
1210
1146
  },
1147
+ /*
1148
+ * Runs strictly after every plugin's `generateBundle` hook has
1149
+ * finished and Rollup has already written the bundle to disk — so by
1150
+ * now Vite's own internal transforms (dynamic-import preload
1151
+ * handling, `__VITE_PRELOAD__` substitution, etc.) are guaranteed to
1152
+ * be fully resolved in `bundle[...].code`. This is what makes it safe
1153
+ * to read the entry chunk's final code and inline it, unlike doing
1154
+ * the same thing inside `generateBundle` (see the comment there).
1155
+ */
1156
+ async writeBundle(outputOptions, bundle) {
1157
+ if (!singleFile) {
1158
+ return;
1159
+ }
1160
+ const entryJsChunk = Object.values(bundle).find((item) => item.type === "chunk" &&
1161
+ item.isEntry &&
1162
+ item.fileName.endsWith(".js"));
1163
+ if (!entryJsChunk || entryJsChunk.type !== "chunk") {
1164
+ this.error("[vite-plugin-pages-ssg] singleFile: could not find the generated entry .js chunk while writing the bundle.");
1165
+ return;
1166
+ }
1167
+ const otherChunkCount = Object.values(bundle).filter((item) => item.type === "chunk" && item !== entryJsChunk).length;
1168
+ if (otherChunkCount > 0) {
1169
+ 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".`);
1170
+ }
1171
+ const cssParts = [];
1172
+ const cssFileNames = [];
1173
+ for (const [fileName, item] of Object.entries(bundle)) {
1174
+ if (item.type === "asset" && fileName.toLowerCase().endsWith(".css")) {
1175
+ const source = item.source;
1176
+ cssParts.push(typeof source === "string"
1177
+ ? source
1178
+ : Buffer.from(source).toString("utf8"));
1179
+ cssFileNames.push(fileName);
1180
+ }
1181
+ }
1182
+ const cssContent = cssParts.join("\n");
1183
+ const bundledPages = pages.filter((page) => page.type === "component" || page.type === "markdown");
1184
+ const rootId = bundledPages.some((page) => page.id === "index")
1185
+ ? "index"
1186
+ : (bundledPages[0]?.id ?? "index");
1187
+ const title = getTitle(rootId);
1188
+ const head = getHead(rootId);
1189
+ const bootstrapScript = singleFileBootstrapScript(rootId);
1190
+ const inlineJs = escapeScriptClose(entryJsChunk.code);
1191
+ let html = `<!doctype html>
1192
+ <html lang="en">
1193
+ <head>
1194
+ <meta charset="UTF-8" />
1195
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1196
+ <title>${escapeHtml(title)}</title>
1197
+ ${cssContent ? `<style>\n${cssContent}\n </style>` : ""}
1198
+ ${head}
1199
+ </head>
1200
+ <body>
1201
+ <div id="app"></div>
1202
+
1203
+ <script>
1204
+ ${bootstrapScript}
1205
+ </script>
1206
+
1207
+ <script type="module">
1208
+ ${inlineJs}
1209
+ </script>
1210
+ </body>
1211
+ </html>
1212
+ `;
1213
+ if (options.minify) {
1214
+ html = await minify(html, {
1215
+ collapseWhitespace: true,
1216
+ removeComments: true,
1217
+ removeRedundantAttributes: true,
1218
+ removeEmptyAttributes: true,
1219
+ useShortDoctype: true,
1220
+ minifyCSS: true,
1221
+ minifyJS: true,
1222
+ });
1223
+ }
1224
+ const outDir = path.resolve(config.root, outputOptions.dir ?? config.build.outDir ?? "dist");
1225
+ fs.writeFileSync(path.join(outDir, "index.html"), html, "utf8");
1226
+ // The entry chunk and every CSS asset are now embedded directly in
1227
+ // index.html (already written above) — remove the now-redundant
1228
+ // loose files from disk so only index.html remains.
1229
+ const filesToRemove = [entryJsChunk.fileName, ...cssFileNames];
1230
+ const dirsTouched = new Set();
1231
+ for (const fileName of filesToRemove) {
1232
+ const filePath = path.join(outDir, fileName);
1233
+ if (fs.existsSync(filePath)) {
1234
+ fs.rmSync(filePath);
1235
+ dirsTouched.add(path.dirname(filePath));
1236
+ }
1237
+ }
1238
+ // Clean up asset directories (e.g. "assets/") left empty behind.
1239
+ for (const dir of dirsTouched) {
1240
+ if (dir !== outDir &&
1241
+ fs.existsSync(dir) &&
1242
+ fs.readdirSync(dir).length === 0) {
1243
+ fs.rmdirSync(dir);
1244
+ }
1245
+ }
1246
+ if (verbose) {
1247
+ console.log(`[vite-plugin-pages-ssg] Emitted single-file build: index.html (${bundledPages.length} page(s) bundled)`);
1248
+ }
1249
+ },
1211
1250
  };
1212
1251
  }
1213
1252
  function getPageId(url, pages) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twynejs",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
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",