Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: generate single file build with module
- Loading branch information
1 parent
94c04d7
commit 8038092
Showing
9 changed files
with
200 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,69 @@ | ||
import { defineNuxtModule, addPlugin, createResolver } from "@nuxt/kit"; | ||
import { defineNuxtModule, addServerPlugin, createResolver } from "@nuxt/kit"; | ||
import { RUNTIME_BUILD_DIR_KEY } from "./runtime/constants"; | ||
|
||
// Module options TypeScript interface definition | ||
export interface ModuleOptions {} | ||
export interface ModuleOptions { | ||
buildDirPath?: string; | ||
} | ||
|
||
export interface ModuleRuntimeConfig { | ||
NUXTSINGLEFILE_BUILD_DIR_PATH: string; | ||
} | ||
|
||
export default defineNuxtModule<ModuleOptions>({ | ||
meta: { | ||
name: "my-module", | ||
configKey: "myModule", | ||
name: "nuxt-singlefile", | ||
configKey: "nuxtSingleFile", | ||
}, | ||
// Default configuration options of the Nuxt module | ||
defaults: {}, | ||
setup(options, nuxt) { | ||
hooks: { | ||
"vite:extendConfig": async (config) => { | ||
config.build ||= {}; | ||
config.build.rollupOptions ||= {}; | ||
config.build.rollupOptions.output ||= {}; | ||
// output typing from rollup allows array, but it should never be relevant | ||
if (Array.isArray(config.build.rollupOptions.output)) | ||
return null as never; | ||
// Disable code splitting | ||
config.build.rollupOptions.output.inlineDynamicImports = true; | ||
}, | ||
"nitro:config": async (config) => { | ||
// we only want to prerender the index page | ||
// so the static preset is the best option | ||
config.preset = "static"; | ||
config.prerender ||= {}; | ||
config.prerender.ignore ||= []; | ||
// the 200 and 404 pages are rendered by default in the static preset | ||
// so we need to ignore them | ||
config.prerender.ignore.push("/200", "/404"); | ||
}, | ||
"build:manifest": async (manifest) => { | ||
Object.entries(manifest).forEach(([key, value]) => { | ||
manifest[key] = { | ||
...value, | ||
// Disable preloading since we are inlining the code | ||
preload: false, | ||
}; | ||
}); | ||
}, | ||
}, | ||
setup({ buildDirPath }, nuxt) { | ||
const resolver = createResolver(import.meta.url); | ||
// Disable SSR | ||
nuxt.options.ssr = false; | ||
|
||
// Set the router in hash mode for client only routing | ||
nuxt.options.router.options.hashMode = true; | ||
|
||
// Disable the app manifest since we won't have a server to handle the get request | ||
nuxt.options.experimental.appManifest = false; | ||
|
||
// Provide the build dir path to the runtime config for the nitro plugin | ||
nuxt.options.runtimeConfig[RUNTIME_BUILD_DIR_KEY] = | ||
buildDirPath ?? nuxt.options.buildDir; | ||
|
||
// Do not add the extension since the `.ts` will be transpiled to `.mjs` after `npm run prepack` | ||
addPlugin(resolver.resolve("./runtime/plugin")); | ||
addServerPlugin(resolver.resolve("./runtime/nitro-plugin")); | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import type { ModuleRuntimeConfig } from "../../module"; | ||
|
||
export const RUNTIME_BUILD_DIR_KEY: keyof ModuleRuntimeConfig = | ||
"NUXTSINGLEFILE_BUILD_DIR_PATH"; | ||
|
||
// the capture group is the path to the file, relative to the public dir | ||
export const regexesPerInlinedType = { | ||
script: new RegExp(`<script[^>]*? src="[./]*(.+)"[^>]*></script>`, "g"), | ||
style: new RegExp(`<link[^>]*? href="[./]*(.+)"[^>]*?>`, "g"), | ||
} as const; | ||
|
||
export type InlinedTagType = keyof typeof regexesPerInlinedType; | ||
|
||
export const inlinedTagTypes = Object.keys( | ||
regexesPerInlinedType, | ||
) as Array<InlinedTagType>; | ||
|
||
export type InlineContentForTagsType = { | ||
baseHtml: string; | ||
pathToFiles: string; | ||
type: InlinedTagType; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import type { ModuleRuntimeConfig } from "../module"; | ||
import { RUNTIME_BUILD_DIR_KEY } from "./constants"; | ||
import { generateSingleFileRenderContext } from "./utils"; | ||
|
||
export default defineNitroPlugin((nitroApp) => { | ||
nitroApp.hooks.hook("render:html", async (response) => { | ||
if (process.env.NODE_ENV !== "prerender") return; | ||
|
||
const runtimeConfig = useRuntimeConfig(); | ||
// TODO: improve runtime config typing | ||
const buildDir = runtimeConfig[ | ||
RUNTIME_BUILD_DIR_KEY | ||
] as ModuleRuntimeConfig[typeof RUNTIME_BUILD_DIR_KEY]; | ||
|
||
const generatedContext = await generateSingleFileRenderContext( | ||
response, | ||
buildDir, | ||
); | ||
|
||
Object.assign(response, generatedContext); | ||
}); | ||
}); |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import type { InlinedTagType } from "../constants"; | ||
|
||
export const generateInlinedStringForTag = ( | ||
content: string, | ||
type: InlinedTagType, | ||
) => { | ||
const attributes = type === "script" ? ' type="module"' : ""; | ||
return `<${type}${attributes}>${content}</${type}>`; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import type { NuxtRenderHTMLContext } from "nuxt/dist/core/runtime/nitro/renderer"; | ||
|
||
import { resolve } from "node:path"; | ||
import { getInlinedHeadContent } from "./index"; | ||
|
||
export const generateSingleFileRenderContext = async ( | ||
baseHtmlRenderContext: NuxtRenderHTMLContext, | ||
buildDir: string, | ||
) => { | ||
const pathToFiles = resolve(buildDir, "./dist/client"); | ||
|
||
const inlinedHead = await getInlinedHeadContent( | ||
baseHtmlRenderContext.head, | ||
pathToFiles, | ||
); | ||
|
||
const result: NuxtRenderHTMLContext = { | ||
...baseHtmlRenderContext, | ||
head: inlinedHead, | ||
}; | ||
return result; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { resolve } from "node:path"; | ||
import { readFile } from "node:fs/promises"; | ||
import { | ||
regexesPerInlinedType, | ||
type InlineContentForTagsType, | ||
} from "../constants"; | ||
|
||
export const getInlineContentForTagsType = async ({ | ||
baseHtml, | ||
pathToFiles, | ||
type, | ||
}: InlineContentForTagsType) => { | ||
if (!regexesPerInlinedType[type]) | ||
return Promise.reject(`invalid type: ${type}`); | ||
|
||
const regex = regexesPerInlinedType[type]; | ||
|
||
const matchedTags = [...baseHtml.matchAll(regex)]; | ||
|
||
return Promise.all( | ||
matchedTags.map(async (match) => { | ||
// the first element is the full match, the second is the capture group | ||
const [tagString, publicPath] = match; | ||
|
||
const filePath = resolve(pathToFiles, `./${publicPath}`); | ||
const content = await readFile(filePath, "utf8"); | ||
|
||
return { tagString, content, type }; | ||
}), | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import type { NuxtRenderHTMLContext } from "nuxt/dist/core/runtime/nitro/renderer"; | ||
|
||
import { inlinedTagTypes } from "../constants"; | ||
import { | ||
generateInlinedStringForTag, | ||
getInlineContentForTagsType, | ||
} from "./index"; | ||
|
||
export const getInlinedHeadContent = async ( | ||
head: NuxtRenderHTMLContext["head"], | ||
pathToFiles: string, | ||
) => | ||
Promise.all( | ||
head.map(async (baseHeadHtml) => { | ||
const inlinedTagsByType = await Promise.all( | ||
inlinedTagTypes.map((type) => | ||
getInlineContentForTagsType({ | ||
baseHtml: baseHeadHtml, | ||
pathToFiles, | ||
type, | ||
}), | ||
), | ||
); | ||
return inlinedTagsByType | ||
.flat() | ||
.reduce( | ||
(html, { tagString, content, type }) => | ||
html.replace(tagString, () => | ||
generateInlinedStringForTag(content, type), | ||
), | ||
baseHeadHtml, | ||
); | ||
}), | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export * from "./generateSingleFileRenderContext"; | ||
export * from "./generateInlinedStringForTag"; | ||
export * from "./getInlinedHeadContent"; | ||
export * from "./getInlineContentForTagsType"; |