twynejs 1.0.2 → 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.
Files changed (2) hide show
  1. package/dist/index.js +129 -91
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -542,15 +542,20 @@ declare module "virtual:pages" {
542
542
  }>;
543
543
  }
544
544
 
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;
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;
554
559
 
555
560
  export const pages: Record<string, PageEntry>;
556
561
  }
@@ -953,16 +958,18 @@ ${ctx.content}
953
958
  return;
954
959
  }
955
960
  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
- * ------------------------------------------------------------------ */
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
+ */
966
973
  if (singleFile) {
967
974
  const skippedPages = pages.filter((page) => page.type === "liquid" || page.type === "ejs");
968
975
  if (skippedPages.length > 0) {
@@ -970,78 +977,6 @@ ${ctx.content}
970
977
  .map((page) => page.id)
971
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.`);
972
979
  }
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
980
  return;
1046
981
  }
1047
982
  for (const page of pages) {
@@ -1208,6 +1143,109 @@ ${inlineJs}
1208
1143
  });
1209
1144
  }
1210
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
+ },
1211
1249
  };
1212
1250
  }
1213
1251
  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.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",