@md-plugins/vite-ssg-plugin 0.1.0-beta.23
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/LICENSE.md +21 -0
- package/README.md +185 -0
- package/dist/index.d.mts +224 -0
- package/dist/index.d.ts +224 -0
- package/dist/index.mjs +405 -0
- package/package.json +63 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 md-plugins
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# @md-plugins/vite-ssg-plugin
|
|
2
|
+
|
|
3
|
+
Static-site-generation infrastructure for Q-Press and md-plugins documentation sites.
|
|
4
|
+
|
|
5
|
+
This package currently focuses on route inventory and static route output:
|
|
6
|
+
|
|
7
|
+
- Normalize static route declarations.
|
|
8
|
+
- Discover Q-Press Markdown routes from a `src/markdown` folder.
|
|
9
|
+
- Generate a route manifest during Vite builds.
|
|
10
|
+
- Expose the same manifest through a virtual module.
|
|
11
|
+
- Emit route-specific HTML files from the built app shell so static hosts can serve deep
|
|
12
|
+
links without relying on a SPA fallback rewrite.
|
|
13
|
+
- Accept a custom per-route renderer when a project is ready to generate fully prerendered
|
|
14
|
+
route HTML.
|
|
15
|
+
|
|
16
|
+
By default, generated route HTML uses the built `index.html` app shell. That makes the output
|
|
17
|
+
usable on Netlify or other static hosts today. Q-Press projects can use `qpress-ssg` for
|
|
18
|
+
first-class Vue/Quasar build-time prerendering without enabling Quasar SSR mode.
|
|
19
|
+
|
|
20
|
+
## Why SSG?
|
|
21
|
+
|
|
22
|
+
SSG turns known routes into static HTML at build time. A direct visit or browser refresh can receive
|
|
23
|
+
the route's own `index.html` file, then the Vue/Quasar client bundle hydrates the page and normal
|
|
24
|
+
SPA navigation takes over.
|
|
25
|
+
|
|
26
|
+
That gives docs sites a useful middle ground: static hosting without a runtime SSR server, but with
|
|
27
|
+
route-specific HTML, meta tags, headings, and body content in the first response. This can help SEO,
|
|
28
|
+
indexing crawlers, and social link previews because they no longer need to depend entirely on
|
|
29
|
+
client-side JavaScript rendering. SSG does not guarantee search ranking improvements by itself; it
|
|
30
|
+
simply makes the route content easier to read earlier and more reliably.
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { viteSsgPlugin } from '@md-plugins/vite-ssg-plugin'
|
|
36
|
+
|
|
37
|
+
export default {
|
|
38
|
+
plugins: [
|
|
39
|
+
viteSsgPlugin({
|
|
40
|
+
routes: ['/', '/getting-started/introduction', '/other/releases'],
|
|
41
|
+
}),
|
|
42
|
+
],
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
For Q-Press-style Markdown docs:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { viteSsgPlugin } from '@md-plugins/vite-ssg-plugin'
|
|
50
|
+
|
|
51
|
+
export default {
|
|
52
|
+
plugins: [
|
|
53
|
+
viteSsgPlugin({
|
|
54
|
+
markdown: {
|
|
55
|
+
root: './src/markdown',
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
],
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The plugin emits `q-press-ssg-routes.json` by default:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"base": "/",
|
|
67
|
+
"routes": [
|
|
68
|
+
{
|
|
69
|
+
"path": "/",
|
|
70
|
+
"htmlFile": "index.html",
|
|
71
|
+
"id": "root",
|
|
72
|
+
"meta": {},
|
|
73
|
+
"params": {}
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Route `meta`, `params`, and `data` values should stay JSON-safe because they are written
|
|
80
|
+
directly into the emitted manifest and the virtual module.
|
|
81
|
+
|
|
82
|
+
## Route HTML
|
|
83
|
+
|
|
84
|
+
When Vite emits `index.html`, this plugin creates matching route HTML files such as:
|
|
85
|
+
|
|
86
|
+
```txt
|
|
87
|
+
index.html
|
|
88
|
+
getting-started/introduction/index.html
|
|
89
|
+
other/releases/index.html
|
|
90
|
+
q-press-ssg-routes.json
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Each generated page receives a small JSON payload:
|
|
94
|
+
|
|
95
|
+
```html
|
|
96
|
+
<script type="application/json" id="md-plugins-ssg-route">
|
|
97
|
+
...
|
|
98
|
+
</script>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
That payload helps future hydration or diagnostics know which static route was generated.
|
|
102
|
+
|
|
103
|
+
Projects that need fully prerendered content can provide `renderRoute`:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
viteSsgPlugin({
|
|
107
|
+
routes: ['/', '/guide'],
|
|
108
|
+
async renderRoute(route, { appHtml }) {
|
|
109
|
+
return appHtml.replace('<div id="q-app"></div>', `<div id="q-app">${route.path}</div>`)
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Post-Build Prerendering
|
|
115
|
+
|
|
116
|
+
When a project has a renderer available outside the Vite build, use `prerenderSsgRoutes`.
|
|
117
|
+
This is the intended bridge for SSR-quality output:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { prerenderSsgRoutes } from '@md-plugins/vite-ssg-plugin'
|
|
121
|
+
|
|
122
|
+
await prerenderSsgRoutes({
|
|
123
|
+
outDir: 'dist/spa',
|
|
124
|
+
async renderRoute(route, { appHtml }) {
|
|
125
|
+
const renderedAppHtml = await renderMyAppAt(route.path)
|
|
126
|
+
|
|
127
|
+
return appHtml.replace('<div id="q-app"></div>', `<div id="q-app">${renderedAppHtml}</div>`)
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The helper reads `q-press-ssg-routes.json`, renders every route, and writes each route's
|
|
133
|
+
`index.html` file. The renderer can be a Vue SSR renderer, a Quasar SSR adapter, or any
|
|
134
|
+
project-specific static renderer.
|
|
135
|
+
|
|
136
|
+
## Vue / Quasar Build-Time Rendering
|
|
137
|
+
|
|
138
|
+
For Q-Press apps, run the generated Q-Press command after a normal SPA build:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
pnpm build:ssg
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Projects that already have a Quasar SSR bundle can opt into that renderer with
|
|
145
|
+
`qpress-ssg --renderer quasar-ssr`, but it is not required for the default Q-Press SSG flow.
|
|
146
|
+
|
|
147
|
+
For lower-level Vue or Quasar apps, `createVueSsgRouteRenderer` adapts a per-route SSR app factory
|
|
148
|
+
into the generic `renderRoute` hook. This uses Vue's server renderer at build time only; the
|
|
149
|
+
published output can still be deployed as static files on Netlify or any other static host.
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { prerenderVueSsgRoutes } from '@md-plugins/vite-ssg-plugin'
|
|
153
|
+
import { createQPressSsgApp } from './src/.q-press/ssg/create-app'
|
|
154
|
+
|
|
155
|
+
await prerenderVueSsgRoutes({
|
|
156
|
+
outDir: 'dist/spa',
|
|
157
|
+
createApp: createQPressSsgApp,
|
|
158
|
+
})
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Q-Press generates `src/.q-press/ssg/create-app` and `src/.q-press/ssg/prerender`, and the `qpress-ssg` binary uses that app factory for the common docs-site flow. Non-Q-Press projects can still provide their own app factory. Vue SSR dependencies are optional until this adapter is used. Projects that already build a Quasar SSR bundle can opt into that path with `qpress-ssg --renderer quasar-ssr`.
|
|
162
|
+
|
|
163
|
+
## Local SSR / SSG Proving
|
|
164
|
+
|
|
165
|
+
It is reasonable to create a local branch or throwaway script that boots a Quasar/Vue SSR app and
|
|
166
|
+
feeds it into `prerenderVueSsgRoutes()` while the workflow is still being proven.
|
|
167
|
+
|
|
168
|
+
That scratch harness should not be committed as finalized docs-site code. Commit the reusable
|
|
169
|
+
plugin behavior, the documented options, the generated Q-Press app-factory template, and reusable
|
|
170
|
+
runner behavior such as `qpress-ssg`; leave one-off local test wiring out unless it belongs in the
|
|
171
|
+
shared tooling.
|
|
172
|
+
|
|
173
|
+
## Virtual Module
|
|
174
|
+
|
|
175
|
+
Client or build tooling can import the generated manifest:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import ssgRouteManifest, { ssgRoutes } from 'virtual:md-plugins/ssg-routes'
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Next Steps
|
|
182
|
+
|
|
183
|
+
- Add dynamic-route parameter expansion.
|
|
184
|
+
- Define lazy client hydration behavior for examples and browser-only components.
|
|
185
|
+
- Define how browser-only examples opt out of prerendering.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
|
|
3
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
4
|
+
type JsonPrimitive = string | number | boolean | null;
|
|
5
|
+
type JsonValue = JsonPrimitive | JsonValue[] | {
|
|
6
|
+
[key: string]: JsonValue;
|
|
7
|
+
};
|
|
8
|
+
type SsgRouteParams = Record<string, JsonPrimitive | undefined>;
|
|
9
|
+
type SsgRouteMeta = Record<string, JsonValue | undefined>;
|
|
10
|
+
interface SsgRouteObject {
|
|
11
|
+
path: string;
|
|
12
|
+
meta?: SsgRouteMeta;
|
|
13
|
+
params?: SsgRouteParams;
|
|
14
|
+
data?: JsonValue;
|
|
15
|
+
}
|
|
16
|
+
type SsgRouteInput = string | SsgRouteObject;
|
|
17
|
+
interface SsgRoute {
|
|
18
|
+
path: string;
|
|
19
|
+
htmlFile: string;
|
|
20
|
+
id: string;
|
|
21
|
+
meta: SsgRouteMeta;
|
|
22
|
+
params: SsgRouteParams;
|
|
23
|
+
data?: JsonValue;
|
|
24
|
+
}
|
|
25
|
+
interface SsgRouteManifest {
|
|
26
|
+
base: string;
|
|
27
|
+
routes: SsgRoute[];
|
|
28
|
+
}
|
|
29
|
+
type SsgRouteSource = SsgRouteInput[] | (() => MaybePromise<SsgRouteInput[]>);
|
|
30
|
+
interface MarkdownSsgRoutesOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Directory containing Markdown pages.
|
|
33
|
+
*/
|
|
34
|
+
root: string;
|
|
35
|
+
/**
|
|
36
|
+
* Glob pattern or patterns to include.
|
|
37
|
+
*/
|
|
38
|
+
include?: string | string[];
|
|
39
|
+
/**
|
|
40
|
+
* Glob pattern or patterns to exclude.
|
|
41
|
+
*/
|
|
42
|
+
exclude?: string | string[];
|
|
43
|
+
/**
|
|
44
|
+
* Markdown file that should map to the site root.
|
|
45
|
+
*/
|
|
46
|
+
landingPage?: string;
|
|
47
|
+
}
|
|
48
|
+
interface SsgRouteRenderContext {
|
|
49
|
+
appHtml: string;
|
|
50
|
+
manifest: SsgRouteManifest;
|
|
51
|
+
routeIndex: number;
|
|
52
|
+
}
|
|
53
|
+
type SsgRouteRenderer = (route: SsgRoute, context: SsgRouteRenderContext) => MaybePromise<string | undefined>;
|
|
54
|
+
type SsgRouteHtmlTransformer = (html: string, route: SsgRoute, context: SsgRouteRenderContext) => MaybePromise<string>;
|
|
55
|
+
interface SsgRouteHtmlOptions {
|
|
56
|
+
/**
|
|
57
|
+
* Optional per-route HTML renderer. Returning undefined falls back to the app shell.
|
|
58
|
+
*/
|
|
59
|
+
renderRoute?: SsgRouteRenderer;
|
|
60
|
+
/**
|
|
61
|
+
* Optional per-route HTML transform after rendering or app-shell fallback.
|
|
62
|
+
*/
|
|
63
|
+
transformHtml?: SsgRouteHtmlTransformer;
|
|
64
|
+
/**
|
|
65
|
+
* Injects a JSON payload for the current SSG route into generated HTML.
|
|
66
|
+
*/
|
|
67
|
+
injectRoutePayload?: boolean;
|
|
68
|
+
}
|
|
69
|
+
interface PrerenderSsgRoutesOptions extends SsgRouteHtmlOptions {
|
|
70
|
+
/**
|
|
71
|
+
* Built output directory containing the app shell and route manifest.
|
|
72
|
+
*/
|
|
73
|
+
outDir: string;
|
|
74
|
+
/**
|
|
75
|
+
* Built HTML file used as the app shell. Defaults to index.html.
|
|
76
|
+
*/
|
|
77
|
+
appHtmlFile?: string;
|
|
78
|
+
/**
|
|
79
|
+
* Build asset path for the generated route manifest.
|
|
80
|
+
*/
|
|
81
|
+
manifestFile?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Manifest to use instead of reading one from disk.
|
|
84
|
+
*/
|
|
85
|
+
manifest?: SsgRouteManifest;
|
|
86
|
+
}
|
|
87
|
+
interface PrerenderedSsgRoute {
|
|
88
|
+
path: string;
|
|
89
|
+
htmlFile: string;
|
|
90
|
+
bytes: number;
|
|
91
|
+
}
|
|
92
|
+
interface PrerenderSsgRoutesResult {
|
|
93
|
+
manifest: SsgRouteManifest;
|
|
94
|
+
outDir: string;
|
|
95
|
+
routes: PrerenderedSsgRoute[];
|
|
96
|
+
}
|
|
97
|
+
interface VueSsgRouterAdapter {
|
|
98
|
+
push?: (location: unknown) => MaybePromise<unknown>;
|
|
99
|
+
replace?: (location: unknown) => MaybePromise<unknown>;
|
|
100
|
+
isReady?: () => MaybePromise<unknown>;
|
|
101
|
+
}
|
|
102
|
+
interface VueSsgAppFactoryResult {
|
|
103
|
+
app: unknown;
|
|
104
|
+
router?: VueSsgRouterAdapter;
|
|
105
|
+
ssrContext?: Record<string, unknown>;
|
|
106
|
+
routeLocation?: unknown;
|
|
107
|
+
onRendered?: () => MaybePromise<void>;
|
|
108
|
+
}
|
|
109
|
+
type VueSsgAppFactory = (route: SsgRoute, context: SsgRouteRenderContext) => MaybePromise<VueSsgAppFactoryResult | unknown>;
|
|
110
|
+
type VueSsgRenderToString = (app: unknown, ssrContext?: Record<string, unknown>) => MaybePromise<string>;
|
|
111
|
+
type VueSsgRouteLocationResolver = (route: SsgRoute, context: SsgRouteRenderContext) => unknown;
|
|
112
|
+
type VueSsgAppHtmlReplacer = (appHtml: string, renderedAppHtml: string, route: SsgRoute, context: SsgRouteRenderContext) => string;
|
|
113
|
+
type VueSsgRenderedAppHtmlTransformer = (renderedAppHtml: string, route: SsgRoute, context: SsgRouteRenderContext) => MaybePromise<string>;
|
|
114
|
+
interface VueSsgRouteRendererOptions {
|
|
115
|
+
/**
|
|
116
|
+
* Creates a fresh Vue/Quasar SSR app instance for each route.
|
|
117
|
+
*/
|
|
118
|
+
createApp: VueSsgAppFactory;
|
|
119
|
+
/**
|
|
120
|
+
* Optional renderer. Defaults to lazy-loading @vue/server-renderer when used.
|
|
121
|
+
*/
|
|
122
|
+
renderToString?: VueSsgRenderToString;
|
|
123
|
+
/**
|
|
124
|
+
* DOM id for the app mount element in the built shell. Defaults to q-app.
|
|
125
|
+
*/
|
|
126
|
+
appMountId?: string;
|
|
127
|
+
/**
|
|
128
|
+
* Route location pushed into the returned router before rendering.
|
|
129
|
+
*/
|
|
130
|
+
routeLocation?: VueSsgRouteLocationResolver;
|
|
131
|
+
/**
|
|
132
|
+
* Use router.replace instead of router.push when both are available.
|
|
133
|
+
*/
|
|
134
|
+
useRouterReplace?: boolean;
|
|
135
|
+
/**
|
|
136
|
+
* Optional transform for the SSR-rendered app fragment before shell insertion.
|
|
137
|
+
*/
|
|
138
|
+
transformRenderedAppHtml?: VueSsgRenderedAppHtmlTransformer;
|
|
139
|
+
/**
|
|
140
|
+
* Optional full shell replacer for projects with a custom app placeholder.
|
|
141
|
+
*/
|
|
142
|
+
replaceAppHtml?: VueSsgAppHtmlReplacer;
|
|
143
|
+
}
|
|
144
|
+
interface PrerenderVueSsgRoutesOptions extends Omit<PrerenderSsgRoutesOptions, 'renderRoute'>, VueSsgRouteRendererOptions {
|
|
145
|
+
}
|
|
146
|
+
interface ViteSsgPluginOptions {
|
|
147
|
+
/**
|
|
148
|
+
* Enables manifest emission. The virtual module remains available either way.
|
|
149
|
+
*/
|
|
150
|
+
enabled?: boolean;
|
|
151
|
+
/**
|
|
152
|
+
* Static route declarations or a function that resolves them.
|
|
153
|
+
*/
|
|
154
|
+
routes?: SsgRouteSource;
|
|
155
|
+
/**
|
|
156
|
+
* Optional Markdown route discovery. This can be combined with explicit routes.
|
|
157
|
+
*/
|
|
158
|
+
markdown?: MarkdownSsgRoutesOptions;
|
|
159
|
+
/**
|
|
160
|
+
* Base path used by the generated route manifest.
|
|
161
|
+
* Falls back to Vite's resolved base.
|
|
162
|
+
*/
|
|
163
|
+
base?: string;
|
|
164
|
+
/**
|
|
165
|
+
* Emits static HTML files for each route. Defaults to true.
|
|
166
|
+
*/
|
|
167
|
+
emitHtml?: boolean;
|
|
168
|
+
/**
|
|
169
|
+
* Built HTML file used as the app shell. Defaults to index.html.
|
|
170
|
+
*/
|
|
171
|
+
appHtmlFile?: string;
|
|
172
|
+
/**
|
|
173
|
+
* Optional per-route HTML renderer/transform behavior.
|
|
174
|
+
*/
|
|
175
|
+
renderRoute?: SsgRouteHtmlOptions['renderRoute'];
|
|
176
|
+
transformHtml?: SsgRouteHtmlOptions['transformHtml'];
|
|
177
|
+
injectRoutePayload?: SsgRouteHtmlOptions['injectRoutePayload'];
|
|
178
|
+
/**
|
|
179
|
+
* Build asset path for the generated route manifest.
|
|
180
|
+
*/
|
|
181
|
+
manifestFile?: string;
|
|
182
|
+
/**
|
|
183
|
+
* Virtual module id used to import the generated route manifest.
|
|
184
|
+
*/
|
|
185
|
+
virtualModuleId?: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
declare function escapeJsonForHtml(json: string): string;
|
|
189
|
+
declare function createSsgRoutePayloadScript(route: SsgRoute): string;
|
|
190
|
+
declare function injectSsgRoutePayload(html: string, route: SsgRoute): string;
|
|
191
|
+
declare function createSsgRouteHtml(route: SsgRoute, context: SsgRouteRenderContext, { injectRoutePayload }?: {
|
|
192
|
+
injectRoutePayload?: boolean;
|
|
193
|
+
}): string;
|
|
194
|
+
declare function renderSsgRouteHtml(route: SsgRoute, context: SsgRouteRenderContext, options?: SsgRouteHtmlOptions): Promise<string>;
|
|
195
|
+
|
|
196
|
+
declare function markdownFileToRoutePath(markdownFile: string, { landingPage }?: Pick<MarkdownSsgRoutesOptions, 'landingPage'>): string;
|
|
197
|
+
declare function discoverMarkdownSsgRoutes({ root, include, exclude, landingPage, }: MarkdownSsgRoutesOptions): SsgRouteInput[];
|
|
198
|
+
|
|
199
|
+
declare function prerenderSsgRoutes({ outDir, appHtmlFile, manifestFile, manifest, renderRoute, transformHtml, injectRoutePayload, }: PrerenderSsgRoutesOptions): Promise<PrerenderSsgRoutesResult>;
|
|
200
|
+
|
|
201
|
+
declare const defaultSsgManifestFile = "q-press-ssg-routes.json";
|
|
202
|
+
declare const defaultSsgVirtualModuleId = "virtual:md-plugins/ssg-routes";
|
|
203
|
+
declare function normalizeSsgBase(base?: string): string;
|
|
204
|
+
declare function normalizeSsgRoutePath(path: string): string;
|
|
205
|
+
declare function routePathToHtmlFile(routePath: string): string;
|
|
206
|
+
declare function routePathToId(routePath: string): string;
|
|
207
|
+
declare function normalizeSsgRoute(input: SsgRouteInput): SsgRoute;
|
|
208
|
+
declare function createSsgRouteManifest(routeInputs: SsgRouteInput[], { base }?: {
|
|
209
|
+
base?: string;
|
|
210
|
+
}): SsgRouteManifest;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Creates a route renderer that turns a fresh Vue/Quasar SSR app into static HTML.
|
|
214
|
+
*/
|
|
215
|
+
declare function createVueSsgRouteRenderer(options: VueSsgRouteRendererOptions): SsgRouteRenderer;
|
|
216
|
+
/**
|
|
217
|
+
* Convenience wrapper for projects that prerender after a normal Vite/Quasar build.
|
|
218
|
+
*/
|
|
219
|
+
declare function prerenderVueSsgRoutes(options: PrerenderVueSsgRoutesOptions): Promise<PrerenderSsgRoutesResult>;
|
|
220
|
+
|
|
221
|
+
declare function viteSsgPlugin(options?: ViteSsgPluginOptions): Plugin;
|
|
222
|
+
|
|
223
|
+
export { createSsgRouteHtml, createSsgRouteManifest, createSsgRoutePayloadScript, createVueSsgRouteRenderer, defaultSsgManifestFile, defaultSsgVirtualModuleId, discoverMarkdownSsgRoutes, escapeJsonForHtml, injectSsgRoutePayload, markdownFileToRoutePath, normalizeSsgBase, normalizeSsgRoute, normalizeSsgRoutePath, prerenderSsgRoutes, prerenderVueSsgRoutes, renderSsgRouteHtml, routePathToHtmlFile, routePathToId, viteSsgPlugin };
|
|
224
|
+
export type { JsonPrimitive, JsonValue, MarkdownSsgRoutesOptions, MaybePromise, PrerenderSsgRoutesOptions, PrerenderSsgRoutesResult, PrerenderVueSsgRoutesOptions, PrerenderedSsgRoute, SsgRoute, SsgRouteHtmlOptions, SsgRouteHtmlTransformer, SsgRouteInput, SsgRouteManifest, SsgRouteMeta, SsgRouteObject, SsgRouteParams, SsgRouteRenderContext, SsgRouteRenderer, SsgRouteSource, ViteSsgPluginOptions, VueSsgAppFactory, VueSsgAppFactoryResult, VueSsgAppHtmlReplacer, VueSsgRenderToString, VueSsgRenderedAppHtmlTransformer, VueSsgRouteLocationResolver, VueSsgRouteRendererOptions, VueSsgRouterAdapter };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
|
|
3
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
4
|
+
type JsonPrimitive = string | number | boolean | null;
|
|
5
|
+
type JsonValue = JsonPrimitive | JsonValue[] | {
|
|
6
|
+
[key: string]: JsonValue;
|
|
7
|
+
};
|
|
8
|
+
type SsgRouteParams = Record<string, JsonPrimitive | undefined>;
|
|
9
|
+
type SsgRouteMeta = Record<string, JsonValue | undefined>;
|
|
10
|
+
interface SsgRouteObject {
|
|
11
|
+
path: string;
|
|
12
|
+
meta?: SsgRouteMeta;
|
|
13
|
+
params?: SsgRouteParams;
|
|
14
|
+
data?: JsonValue;
|
|
15
|
+
}
|
|
16
|
+
type SsgRouteInput = string | SsgRouteObject;
|
|
17
|
+
interface SsgRoute {
|
|
18
|
+
path: string;
|
|
19
|
+
htmlFile: string;
|
|
20
|
+
id: string;
|
|
21
|
+
meta: SsgRouteMeta;
|
|
22
|
+
params: SsgRouteParams;
|
|
23
|
+
data?: JsonValue;
|
|
24
|
+
}
|
|
25
|
+
interface SsgRouteManifest {
|
|
26
|
+
base: string;
|
|
27
|
+
routes: SsgRoute[];
|
|
28
|
+
}
|
|
29
|
+
type SsgRouteSource = SsgRouteInput[] | (() => MaybePromise<SsgRouteInput[]>);
|
|
30
|
+
interface MarkdownSsgRoutesOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Directory containing Markdown pages.
|
|
33
|
+
*/
|
|
34
|
+
root: string;
|
|
35
|
+
/**
|
|
36
|
+
* Glob pattern or patterns to include.
|
|
37
|
+
*/
|
|
38
|
+
include?: string | string[];
|
|
39
|
+
/**
|
|
40
|
+
* Glob pattern or patterns to exclude.
|
|
41
|
+
*/
|
|
42
|
+
exclude?: string | string[];
|
|
43
|
+
/**
|
|
44
|
+
* Markdown file that should map to the site root.
|
|
45
|
+
*/
|
|
46
|
+
landingPage?: string;
|
|
47
|
+
}
|
|
48
|
+
interface SsgRouteRenderContext {
|
|
49
|
+
appHtml: string;
|
|
50
|
+
manifest: SsgRouteManifest;
|
|
51
|
+
routeIndex: number;
|
|
52
|
+
}
|
|
53
|
+
type SsgRouteRenderer = (route: SsgRoute, context: SsgRouteRenderContext) => MaybePromise<string | undefined>;
|
|
54
|
+
type SsgRouteHtmlTransformer = (html: string, route: SsgRoute, context: SsgRouteRenderContext) => MaybePromise<string>;
|
|
55
|
+
interface SsgRouteHtmlOptions {
|
|
56
|
+
/**
|
|
57
|
+
* Optional per-route HTML renderer. Returning undefined falls back to the app shell.
|
|
58
|
+
*/
|
|
59
|
+
renderRoute?: SsgRouteRenderer;
|
|
60
|
+
/**
|
|
61
|
+
* Optional per-route HTML transform after rendering or app-shell fallback.
|
|
62
|
+
*/
|
|
63
|
+
transformHtml?: SsgRouteHtmlTransformer;
|
|
64
|
+
/**
|
|
65
|
+
* Injects a JSON payload for the current SSG route into generated HTML.
|
|
66
|
+
*/
|
|
67
|
+
injectRoutePayload?: boolean;
|
|
68
|
+
}
|
|
69
|
+
interface PrerenderSsgRoutesOptions extends SsgRouteHtmlOptions {
|
|
70
|
+
/**
|
|
71
|
+
* Built output directory containing the app shell and route manifest.
|
|
72
|
+
*/
|
|
73
|
+
outDir: string;
|
|
74
|
+
/**
|
|
75
|
+
* Built HTML file used as the app shell. Defaults to index.html.
|
|
76
|
+
*/
|
|
77
|
+
appHtmlFile?: string;
|
|
78
|
+
/**
|
|
79
|
+
* Build asset path for the generated route manifest.
|
|
80
|
+
*/
|
|
81
|
+
manifestFile?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Manifest to use instead of reading one from disk.
|
|
84
|
+
*/
|
|
85
|
+
manifest?: SsgRouteManifest;
|
|
86
|
+
}
|
|
87
|
+
interface PrerenderedSsgRoute {
|
|
88
|
+
path: string;
|
|
89
|
+
htmlFile: string;
|
|
90
|
+
bytes: number;
|
|
91
|
+
}
|
|
92
|
+
interface PrerenderSsgRoutesResult {
|
|
93
|
+
manifest: SsgRouteManifest;
|
|
94
|
+
outDir: string;
|
|
95
|
+
routes: PrerenderedSsgRoute[];
|
|
96
|
+
}
|
|
97
|
+
interface VueSsgRouterAdapter {
|
|
98
|
+
push?: (location: unknown) => MaybePromise<unknown>;
|
|
99
|
+
replace?: (location: unknown) => MaybePromise<unknown>;
|
|
100
|
+
isReady?: () => MaybePromise<unknown>;
|
|
101
|
+
}
|
|
102
|
+
interface VueSsgAppFactoryResult {
|
|
103
|
+
app: unknown;
|
|
104
|
+
router?: VueSsgRouterAdapter;
|
|
105
|
+
ssrContext?: Record<string, unknown>;
|
|
106
|
+
routeLocation?: unknown;
|
|
107
|
+
onRendered?: () => MaybePromise<void>;
|
|
108
|
+
}
|
|
109
|
+
type VueSsgAppFactory = (route: SsgRoute, context: SsgRouteRenderContext) => MaybePromise<VueSsgAppFactoryResult | unknown>;
|
|
110
|
+
type VueSsgRenderToString = (app: unknown, ssrContext?: Record<string, unknown>) => MaybePromise<string>;
|
|
111
|
+
type VueSsgRouteLocationResolver = (route: SsgRoute, context: SsgRouteRenderContext) => unknown;
|
|
112
|
+
type VueSsgAppHtmlReplacer = (appHtml: string, renderedAppHtml: string, route: SsgRoute, context: SsgRouteRenderContext) => string;
|
|
113
|
+
type VueSsgRenderedAppHtmlTransformer = (renderedAppHtml: string, route: SsgRoute, context: SsgRouteRenderContext) => MaybePromise<string>;
|
|
114
|
+
interface VueSsgRouteRendererOptions {
|
|
115
|
+
/**
|
|
116
|
+
* Creates a fresh Vue/Quasar SSR app instance for each route.
|
|
117
|
+
*/
|
|
118
|
+
createApp: VueSsgAppFactory;
|
|
119
|
+
/**
|
|
120
|
+
* Optional renderer. Defaults to lazy-loading @vue/server-renderer when used.
|
|
121
|
+
*/
|
|
122
|
+
renderToString?: VueSsgRenderToString;
|
|
123
|
+
/**
|
|
124
|
+
* DOM id for the app mount element in the built shell. Defaults to q-app.
|
|
125
|
+
*/
|
|
126
|
+
appMountId?: string;
|
|
127
|
+
/**
|
|
128
|
+
* Route location pushed into the returned router before rendering.
|
|
129
|
+
*/
|
|
130
|
+
routeLocation?: VueSsgRouteLocationResolver;
|
|
131
|
+
/**
|
|
132
|
+
* Use router.replace instead of router.push when both are available.
|
|
133
|
+
*/
|
|
134
|
+
useRouterReplace?: boolean;
|
|
135
|
+
/**
|
|
136
|
+
* Optional transform for the SSR-rendered app fragment before shell insertion.
|
|
137
|
+
*/
|
|
138
|
+
transformRenderedAppHtml?: VueSsgRenderedAppHtmlTransformer;
|
|
139
|
+
/**
|
|
140
|
+
* Optional full shell replacer for projects with a custom app placeholder.
|
|
141
|
+
*/
|
|
142
|
+
replaceAppHtml?: VueSsgAppHtmlReplacer;
|
|
143
|
+
}
|
|
144
|
+
interface PrerenderVueSsgRoutesOptions extends Omit<PrerenderSsgRoutesOptions, 'renderRoute'>, VueSsgRouteRendererOptions {
|
|
145
|
+
}
|
|
146
|
+
interface ViteSsgPluginOptions {
|
|
147
|
+
/**
|
|
148
|
+
* Enables manifest emission. The virtual module remains available either way.
|
|
149
|
+
*/
|
|
150
|
+
enabled?: boolean;
|
|
151
|
+
/**
|
|
152
|
+
* Static route declarations or a function that resolves them.
|
|
153
|
+
*/
|
|
154
|
+
routes?: SsgRouteSource;
|
|
155
|
+
/**
|
|
156
|
+
* Optional Markdown route discovery. This can be combined with explicit routes.
|
|
157
|
+
*/
|
|
158
|
+
markdown?: MarkdownSsgRoutesOptions;
|
|
159
|
+
/**
|
|
160
|
+
* Base path used by the generated route manifest.
|
|
161
|
+
* Falls back to Vite's resolved base.
|
|
162
|
+
*/
|
|
163
|
+
base?: string;
|
|
164
|
+
/**
|
|
165
|
+
* Emits static HTML files for each route. Defaults to true.
|
|
166
|
+
*/
|
|
167
|
+
emitHtml?: boolean;
|
|
168
|
+
/**
|
|
169
|
+
* Built HTML file used as the app shell. Defaults to index.html.
|
|
170
|
+
*/
|
|
171
|
+
appHtmlFile?: string;
|
|
172
|
+
/**
|
|
173
|
+
* Optional per-route HTML renderer/transform behavior.
|
|
174
|
+
*/
|
|
175
|
+
renderRoute?: SsgRouteHtmlOptions['renderRoute'];
|
|
176
|
+
transformHtml?: SsgRouteHtmlOptions['transformHtml'];
|
|
177
|
+
injectRoutePayload?: SsgRouteHtmlOptions['injectRoutePayload'];
|
|
178
|
+
/**
|
|
179
|
+
* Build asset path for the generated route manifest.
|
|
180
|
+
*/
|
|
181
|
+
manifestFile?: string;
|
|
182
|
+
/**
|
|
183
|
+
* Virtual module id used to import the generated route manifest.
|
|
184
|
+
*/
|
|
185
|
+
virtualModuleId?: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
declare function escapeJsonForHtml(json: string): string;
|
|
189
|
+
declare function createSsgRoutePayloadScript(route: SsgRoute): string;
|
|
190
|
+
declare function injectSsgRoutePayload(html: string, route: SsgRoute): string;
|
|
191
|
+
declare function createSsgRouteHtml(route: SsgRoute, context: SsgRouteRenderContext, { injectRoutePayload }?: {
|
|
192
|
+
injectRoutePayload?: boolean;
|
|
193
|
+
}): string;
|
|
194
|
+
declare function renderSsgRouteHtml(route: SsgRoute, context: SsgRouteRenderContext, options?: SsgRouteHtmlOptions): Promise<string>;
|
|
195
|
+
|
|
196
|
+
declare function markdownFileToRoutePath(markdownFile: string, { landingPage }?: Pick<MarkdownSsgRoutesOptions, 'landingPage'>): string;
|
|
197
|
+
declare function discoverMarkdownSsgRoutes({ root, include, exclude, landingPage, }: MarkdownSsgRoutesOptions): SsgRouteInput[];
|
|
198
|
+
|
|
199
|
+
declare function prerenderSsgRoutes({ outDir, appHtmlFile, manifestFile, manifest, renderRoute, transformHtml, injectRoutePayload, }: PrerenderSsgRoutesOptions): Promise<PrerenderSsgRoutesResult>;
|
|
200
|
+
|
|
201
|
+
declare const defaultSsgManifestFile = "q-press-ssg-routes.json";
|
|
202
|
+
declare const defaultSsgVirtualModuleId = "virtual:md-plugins/ssg-routes";
|
|
203
|
+
declare function normalizeSsgBase(base?: string): string;
|
|
204
|
+
declare function normalizeSsgRoutePath(path: string): string;
|
|
205
|
+
declare function routePathToHtmlFile(routePath: string): string;
|
|
206
|
+
declare function routePathToId(routePath: string): string;
|
|
207
|
+
declare function normalizeSsgRoute(input: SsgRouteInput): SsgRoute;
|
|
208
|
+
declare function createSsgRouteManifest(routeInputs: SsgRouteInput[], { base }?: {
|
|
209
|
+
base?: string;
|
|
210
|
+
}): SsgRouteManifest;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Creates a route renderer that turns a fresh Vue/Quasar SSR app into static HTML.
|
|
214
|
+
*/
|
|
215
|
+
declare function createVueSsgRouteRenderer(options: VueSsgRouteRendererOptions): SsgRouteRenderer;
|
|
216
|
+
/**
|
|
217
|
+
* Convenience wrapper for projects that prerender after a normal Vite/Quasar build.
|
|
218
|
+
*/
|
|
219
|
+
declare function prerenderVueSsgRoutes(options: PrerenderVueSsgRoutesOptions): Promise<PrerenderSsgRoutesResult>;
|
|
220
|
+
|
|
221
|
+
declare function viteSsgPlugin(options?: ViteSsgPluginOptions): Plugin;
|
|
222
|
+
|
|
223
|
+
export { createSsgRouteHtml, createSsgRouteManifest, createSsgRoutePayloadScript, createVueSsgRouteRenderer, defaultSsgManifestFile, defaultSsgVirtualModuleId, discoverMarkdownSsgRoutes, escapeJsonForHtml, injectSsgRoutePayload, markdownFileToRoutePath, normalizeSsgBase, normalizeSsgRoute, normalizeSsgRoutePath, prerenderSsgRoutes, prerenderVueSsgRoutes, renderSsgRouteHtml, routePathToHtmlFile, routePathToId, viteSsgPlugin };
|
|
224
|
+
export type { JsonPrimitive, JsonValue, MarkdownSsgRoutesOptions, MaybePromise, PrerenderSsgRoutesOptions, PrerenderSsgRoutesResult, PrerenderVueSsgRoutesOptions, PrerenderedSsgRoute, SsgRoute, SsgRouteHtmlOptions, SsgRouteHtmlTransformer, SsgRouteInput, SsgRouteManifest, SsgRouteMeta, SsgRouteObject, SsgRouteParams, SsgRouteRenderContext, SsgRouteRenderer, SsgRouteSource, ViteSsgPluginOptions, VueSsgAppFactory, VueSsgAppFactoryResult, VueSsgAppHtmlReplacer, VueSsgRenderToString, VueSsgRenderedAppHtmlTransformer, VueSsgRouteLocationResolver, VueSsgRouteRendererOptions, VueSsgRouterAdapter };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { globSync } from 'tinyglobby';
|
|
2
|
+
import { Buffer } from 'node:buffer';
|
|
3
|
+
import { readFile, mkdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { resolve, join, dirname } from 'node:path';
|
|
5
|
+
|
|
6
|
+
function escapeJsonForHtml(json) {
|
|
7
|
+
return json.replace(/</g, "\\u003C").replace(/>/g, "\\u003E").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
8
|
+
}
|
|
9
|
+
function createSsgRoutePayloadScript(route) {
|
|
10
|
+
const payload = escapeJsonForHtml(JSON.stringify(route));
|
|
11
|
+
return `<script type="application/json" id="md-plugins-ssg-route">${payload}<\/script>`;
|
|
12
|
+
}
|
|
13
|
+
function injectSsgRoutePayload(html, route) {
|
|
14
|
+
const script = createSsgRoutePayloadScript(route);
|
|
15
|
+
if (html.includes('id="md-plugins-ssg-route"')) {
|
|
16
|
+
return html;
|
|
17
|
+
}
|
|
18
|
+
if (html.includes("</head>")) {
|
|
19
|
+
return html.replace("</head>", `${script}
|
|
20
|
+
</head>`);
|
|
21
|
+
}
|
|
22
|
+
return `${script}
|
|
23
|
+
${html}`;
|
|
24
|
+
}
|
|
25
|
+
function createSsgRouteHtml(route, context, { injectRoutePayload = true } = {}) {
|
|
26
|
+
if (injectRoutePayload === false) {
|
|
27
|
+
return context.appHtml;
|
|
28
|
+
}
|
|
29
|
+
return injectSsgRoutePayload(context.appHtml, route);
|
|
30
|
+
}
|
|
31
|
+
async function renderSsgRouteHtml(route, context, options = {}) {
|
|
32
|
+
const renderedHtml = await options.renderRoute?.(route, context);
|
|
33
|
+
const routeHtml = renderedHtml ?? createSsgRouteHtml(route, context, {
|
|
34
|
+
injectRoutePayload: options.injectRoutePayload
|
|
35
|
+
});
|
|
36
|
+
const html = renderedHtml && options.injectRoutePayload !== false ? injectSsgRoutePayload(routeHtml, route) : routeHtml;
|
|
37
|
+
return await options.transformHtml?.(html, route, context) ?? html;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const markdownExtensionRE = /\.md$/i;
|
|
41
|
+
function normalizePatterns(patterns, fallback) {
|
|
42
|
+
if (patterns === void 0) {
|
|
43
|
+
return fallback;
|
|
44
|
+
}
|
|
45
|
+
return Array.isArray(patterns) ? patterns : [patterns];
|
|
46
|
+
}
|
|
47
|
+
function markdownFileToRoutePath(markdownFile, { landingPage = "landing-page.md" } = {}) {
|
|
48
|
+
const normalizedFile = markdownFile.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
49
|
+
if (normalizedFile === landingPage.replace(/\\/g, "/").replace(/^\.\//, "")) {
|
|
50
|
+
return "/";
|
|
51
|
+
}
|
|
52
|
+
const routeParts = normalizedFile.replace(markdownExtensionRE, "").split("/");
|
|
53
|
+
const lastPart = routeParts.at(-1);
|
|
54
|
+
const parentPart = routeParts.at(-2);
|
|
55
|
+
if (routeParts.length > 1 && lastPart === parentPart) {
|
|
56
|
+
routeParts.pop();
|
|
57
|
+
}
|
|
58
|
+
return `/${routeParts.join("/")}`;
|
|
59
|
+
}
|
|
60
|
+
function discoverMarkdownSsgRoutes({
|
|
61
|
+
root,
|
|
62
|
+
include,
|
|
63
|
+
exclude,
|
|
64
|
+
landingPage
|
|
65
|
+
}) {
|
|
66
|
+
const files = globSync(normalizePatterns(include, ["**/*.md"]), {
|
|
67
|
+
cwd: root,
|
|
68
|
+
ignore: normalizePatterns(exclude, [])
|
|
69
|
+
}).sort();
|
|
70
|
+
return files.map((file) => ({
|
|
71
|
+
path: markdownFileToRoutePath(file, { landingPage }),
|
|
72
|
+
meta: {
|
|
73
|
+
source: "markdown",
|
|
74
|
+
file
|
|
75
|
+
}
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const defaultSsgManifestFile = "q-press-ssg-routes.json";
|
|
80
|
+
const defaultSsgVirtualModuleId = "virtual:md-plugins/ssg-routes";
|
|
81
|
+
function normalizeSsgBase(base = "/") {
|
|
82
|
+
const trimmed = base.trim();
|
|
83
|
+
if (trimmed === "" || trimmed === "/") {
|
|
84
|
+
return "/";
|
|
85
|
+
}
|
|
86
|
+
if (trimmed === "." || trimmed === "./") {
|
|
87
|
+
return "./";
|
|
88
|
+
}
|
|
89
|
+
if (/^[a-z]+:\/\//i.test(trimmed)) {
|
|
90
|
+
return trimmed.replace(/\/+$/, "");
|
|
91
|
+
}
|
|
92
|
+
const withLeadingSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
93
|
+
const withoutTrailingSlash = withLeadingSlash.replace(/\/+$/, "");
|
|
94
|
+
return withoutTrailingSlash || "/";
|
|
95
|
+
}
|
|
96
|
+
function normalizeSsgRoutePath(path) {
|
|
97
|
+
const trimmed = path.trim();
|
|
98
|
+
if (!trimmed) {
|
|
99
|
+
throw new Error("SSG route path cannot be empty");
|
|
100
|
+
}
|
|
101
|
+
const withoutHash = trimmed.split("#")[0] ?? "";
|
|
102
|
+
const withoutQuery = withoutHash.split("?")[0] ?? "";
|
|
103
|
+
const withLeadingSlash = withoutQuery.startsWith("/") ? withoutQuery : `/${withoutQuery}`;
|
|
104
|
+
const compacted = withLeadingSlash.replace(/\/{2,}/g, "/");
|
|
105
|
+
if (compacted === "/") {
|
|
106
|
+
return "/";
|
|
107
|
+
}
|
|
108
|
+
return compacted.replace(/\/+$/, "");
|
|
109
|
+
}
|
|
110
|
+
function routePathToHtmlFile(routePath) {
|
|
111
|
+
const normalized = normalizeSsgRoutePath(routePath);
|
|
112
|
+
if (normalized === "/") {
|
|
113
|
+
return "index.html";
|
|
114
|
+
}
|
|
115
|
+
return `${normalized.slice(1)}/index.html`;
|
|
116
|
+
}
|
|
117
|
+
function routePathToId(routePath) {
|
|
118
|
+
const normalized = normalizeSsgRoutePath(routePath);
|
|
119
|
+
if (normalized === "/") {
|
|
120
|
+
return "root";
|
|
121
|
+
}
|
|
122
|
+
return normalized.slice(1).replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
123
|
+
}
|
|
124
|
+
function normalizeSsgRoute(input) {
|
|
125
|
+
const route = typeof input === "string" ? {
|
|
126
|
+
path: input,
|
|
127
|
+
meta: {},
|
|
128
|
+
params: {}
|
|
129
|
+
} : input;
|
|
130
|
+
const path = normalizeSsgRoutePath(route.path);
|
|
131
|
+
const meta = route.meta ?? {};
|
|
132
|
+
const params = route.params ?? {};
|
|
133
|
+
return {
|
|
134
|
+
path,
|
|
135
|
+
htmlFile: routePathToHtmlFile(path),
|
|
136
|
+
id: routePathToId(path),
|
|
137
|
+
meta,
|
|
138
|
+
params,
|
|
139
|
+
...Object.hasOwn(route, "data") ? { data: route.data } : {}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function createSsgRouteManifest(routeInputs, { base = "/" } = {}) {
|
|
143
|
+
const routes = routeInputs.map(normalizeSsgRoute);
|
|
144
|
+
const routePaths = /* @__PURE__ */ new Set();
|
|
145
|
+
for (const route of routes) {
|
|
146
|
+
if (routePaths.has(route.path)) {
|
|
147
|
+
throw new Error(`Duplicate SSG route path: ${route.path}`);
|
|
148
|
+
}
|
|
149
|
+
routePaths.add(route.path);
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
base: normalizeSsgBase(base),
|
|
153
|
+
routes
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function readSsgRouteManifest(outDir, manifestFile) {
|
|
158
|
+
const manifestJson = await readFile(join(outDir, manifestFile), "utf8");
|
|
159
|
+
return JSON.parse(manifestJson);
|
|
160
|
+
}
|
|
161
|
+
async function prerenderSsgRoutes({
|
|
162
|
+
outDir,
|
|
163
|
+
appHtmlFile = "index.html",
|
|
164
|
+
manifestFile = defaultSsgManifestFile,
|
|
165
|
+
manifest,
|
|
166
|
+
renderRoute,
|
|
167
|
+
transformHtml,
|
|
168
|
+
injectRoutePayload
|
|
169
|
+
}) {
|
|
170
|
+
const resolvedOutDir = resolve(outDir);
|
|
171
|
+
const resolvedManifest = manifest ?? await readSsgRouteManifest(resolvedOutDir, manifestFile);
|
|
172
|
+
const appHtml = await readFile(join(resolvedOutDir, appHtmlFile), "utf8");
|
|
173
|
+
const routes = [];
|
|
174
|
+
for (const [routeIndex, route] of resolvedManifest.routes.entries()) {
|
|
175
|
+
const html = await renderSsgRouteHtml(
|
|
176
|
+
route,
|
|
177
|
+
{
|
|
178
|
+
appHtml,
|
|
179
|
+
manifest: resolvedManifest,
|
|
180
|
+
routeIndex
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
renderRoute,
|
|
184
|
+
transformHtml,
|
|
185
|
+
injectRoutePayload
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
const htmlPath = join(resolvedOutDir, route.htmlFile);
|
|
189
|
+
await mkdir(dirname(htmlPath), { recursive: true });
|
|
190
|
+
await writeFile(htmlPath, html);
|
|
191
|
+
routes.push({
|
|
192
|
+
path: route.path,
|
|
193
|
+
htmlFile: route.htmlFile,
|
|
194
|
+
bytes: Buffer.byteLength(html)
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
manifest: resolvedManifest,
|
|
199
|
+
outDir: resolvedOutDir,
|
|
200
|
+
routes
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const vueServerRendererPackage = "@vue/server-renderer";
|
|
205
|
+
function escapeRegExp(value) {
|
|
206
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
207
|
+
}
|
|
208
|
+
function isVueSsgAppFactoryResult(value) {
|
|
209
|
+
return typeof value === "object" && value !== null && "app" in value;
|
|
210
|
+
}
|
|
211
|
+
async function loadVueRenderToString() {
|
|
212
|
+
try {
|
|
213
|
+
const renderer = await import(vueServerRendererPackage);
|
|
214
|
+
if (typeof renderer.renderToString === "function") {
|
|
215
|
+
return renderer.renderToString;
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
[
|
|
220
|
+
"Unable to load @vue/server-renderer for SSG rendering.",
|
|
221
|
+
"Install it in the app that calls createVueSsgRouteRenderer(),",
|
|
222
|
+
"or pass a custom renderToString option.",
|
|
223
|
+
error instanceof Error ? `Original error: ${error.message}` : void 0
|
|
224
|
+
].filter(Boolean).join(" ")
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
throw new Error("@vue/server-renderer did not export renderToString.");
|
|
228
|
+
}
|
|
229
|
+
function replaceMountElement(appHtml, renderedAppHtml, appMountId) {
|
|
230
|
+
const mountId = escapeRegExp(appMountId);
|
|
231
|
+
const mountElementRE = new RegExp(
|
|
232
|
+
`<([a-zA-Z][\\w:-]*)([^>]*\\bid=["']${mountId}["'][^>]*)>\\s*</\\1>`
|
|
233
|
+
);
|
|
234
|
+
if (!mountElementRE.test(appHtml)) {
|
|
235
|
+
throw new Error(`Could not find empty app mount element with id "${appMountId}".`);
|
|
236
|
+
}
|
|
237
|
+
return appHtml.replace(mountElementRE, `<$1$2>${renderedAppHtml}</$1>`);
|
|
238
|
+
}
|
|
239
|
+
async function pushRouterLocation(appResult, route, context, options) {
|
|
240
|
+
const router = appResult.router;
|
|
241
|
+
if (!router) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const location = appResult.routeLocation ?? options.routeLocation?.(route, context) ?? route.path;
|
|
245
|
+
const navigate = options.useRouterReplace === true ? router.replace ?? router.push : router.push ?? router.replace;
|
|
246
|
+
if (navigate) {
|
|
247
|
+
await navigate.call(router, location);
|
|
248
|
+
}
|
|
249
|
+
await router.isReady?.();
|
|
250
|
+
}
|
|
251
|
+
function createVueSsgRouteRenderer(options) {
|
|
252
|
+
return async (route, context) => {
|
|
253
|
+
const createdApp = await options.createApp(route, context);
|
|
254
|
+
const appResult = isVueSsgAppFactoryResult(createdApp) ? createdApp : { app: createdApp };
|
|
255
|
+
await pushRouterLocation(appResult, route, context, options);
|
|
256
|
+
const renderToString = options.renderToString ?? await loadVueRenderToString();
|
|
257
|
+
const renderedAppHtml = await renderToString(appResult.app, appResult.ssrContext);
|
|
258
|
+
const transformedAppHtml = await options.transformRenderedAppHtml?.(renderedAppHtml, route, context) ?? renderedAppHtml;
|
|
259
|
+
const html = options.replaceAppHtml ? options.replaceAppHtml(context.appHtml, transformedAppHtml, route, context) : replaceMountElement(context.appHtml, transformedAppHtml, options.appMountId ?? "q-app");
|
|
260
|
+
await appResult.onRendered?.();
|
|
261
|
+
return html;
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
async function prerenderVueSsgRoutes(options) {
|
|
265
|
+
const {
|
|
266
|
+
createApp,
|
|
267
|
+
renderToString,
|
|
268
|
+
appMountId,
|
|
269
|
+
routeLocation,
|
|
270
|
+
useRouterReplace,
|
|
271
|
+
transformRenderedAppHtml,
|
|
272
|
+
replaceAppHtml,
|
|
273
|
+
...prerenderOptions
|
|
274
|
+
} = options;
|
|
275
|
+
return prerenderSsgRoutes({
|
|
276
|
+
...prerenderOptions,
|
|
277
|
+
renderRoute: createVueSsgRouteRenderer({
|
|
278
|
+
createApp,
|
|
279
|
+
renderToString,
|
|
280
|
+
appMountId,
|
|
281
|
+
routeLocation,
|
|
282
|
+
useRouterReplace,
|
|
283
|
+
transformRenderedAppHtml,
|
|
284
|
+
replaceAppHtml
|
|
285
|
+
})
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function isBundleAsset(entry) {
|
|
290
|
+
return typeof entry === "object" && entry !== null && "type" in entry && entry.type === "asset" && "fileName" in entry && typeof entry.fileName === "string" && "source" in entry;
|
|
291
|
+
}
|
|
292
|
+
function assetSourceToString(source) {
|
|
293
|
+
return typeof source === "string" ? source : Buffer.from(source).toString("utf8");
|
|
294
|
+
}
|
|
295
|
+
function findHtmlAsset(bundle, fileName) {
|
|
296
|
+
return Object.values(bundle).find(
|
|
297
|
+
(entry) => isBundleAsset(entry) && entry.fileName === fileName
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
function serializeManifestModule(manifest) {
|
|
301
|
+
const serializedManifest = JSON.stringify(manifest, null, 2);
|
|
302
|
+
return [
|
|
303
|
+
`export const ssgRouteManifest = ${serializedManifest}`,
|
|
304
|
+
"export const ssgRoutes = ssgRouteManifest.routes",
|
|
305
|
+
"export default ssgRouteManifest",
|
|
306
|
+
""
|
|
307
|
+
].join("\n");
|
|
308
|
+
}
|
|
309
|
+
async function resolveRouteInputs(options) {
|
|
310
|
+
const routeSource = options.routes;
|
|
311
|
+
const markdownRoutes = options.markdown ? discoverMarkdownSsgRoutes(options.markdown) : [];
|
|
312
|
+
if (!routeSource) {
|
|
313
|
+
return markdownRoutes;
|
|
314
|
+
}
|
|
315
|
+
if (typeof routeSource === "function") {
|
|
316
|
+
return [...markdownRoutes, ...await routeSource()];
|
|
317
|
+
}
|
|
318
|
+
return [...markdownRoutes, ...routeSource];
|
|
319
|
+
}
|
|
320
|
+
function viteSsgPlugin(options = {}) {
|
|
321
|
+
const virtualModuleId = options.virtualModuleId ?? defaultSsgVirtualModuleId;
|
|
322
|
+
const resolvedVirtualModuleId = `\0${virtualModuleId}`;
|
|
323
|
+
let config;
|
|
324
|
+
let manifest;
|
|
325
|
+
async function refreshManifest() {
|
|
326
|
+
const routes = await resolveRouteInputs(options);
|
|
327
|
+
manifest = createSsgRouteManifest(routes, {
|
|
328
|
+
base: options.base ?? config?.base ?? "/"
|
|
329
|
+
});
|
|
330
|
+
return manifest;
|
|
331
|
+
}
|
|
332
|
+
async function getManifest() {
|
|
333
|
+
return manifest ?? refreshManifest();
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
name: "@md-plugins/vite-ssg-plugin",
|
|
337
|
+
enforce: "post",
|
|
338
|
+
configResolved(resolvedConfig) {
|
|
339
|
+
config = resolvedConfig;
|
|
340
|
+
},
|
|
341
|
+
async buildStart() {
|
|
342
|
+
await refreshManifest();
|
|
343
|
+
},
|
|
344
|
+
resolveId(id) {
|
|
345
|
+
if (id === virtualModuleId) {
|
|
346
|
+
return resolvedVirtualModuleId;
|
|
347
|
+
}
|
|
348
|
+
return void 0;
|
|
349
|
+
},
|
|
350
|
+
async load(id) {
|
|
351
|
+
if (id !== resolvedVirtualModuleId) {
|
|
352
|
+
return void 0;
|
|
353
|
+
}
|
|
354
|
+
return serializeManifestModule(await getManifest());
|
|
355
|
+
},
|
|
356
|
+
async generateBundle(_outputOptions, bundle) {
|
|
357
|
+
if (options.enabled === false) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const resolvedManifest = await refreshManifest();
|
|
361
|
+
const appHtmlFile = options.appHtmlFile ?? "index.html";
|
|
362
|
+
const appHtmlAsset = findHtmlAsset(bundle, appHtmlFile);
|
|
363
|
+
this.emitFile({
|
|
364
|
+
type: "asset",
|
|
365
|
+
fileName: options.manifestFile ?? defaultSsgManifestFile,
|
|
366
|
+
source: `${JSON.stringify(resolvedManifest, null, 2)}
|
|
367
|
+
`
|
|
368
|
+
});
|
|
369
|
+
if (options.emitHtml === false) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const htmlAsset = appHtmlAsset;
|
|
373
|
+
if (!htmlAsset) {
|
|
374
|
+
this.warn(
|
|
375
|
+
`Could not find ${appHtmlFile}; skipped SSG route HTML generation. The route manifest was still emitted.`
|
|
376
|
+
);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const appHtml = assetSourceToString(htmlAsset.source);
|
|
380
|
+
for (const [routeIndex, route] of resolvedManifest.routes.entries()) {
|
|
381
|
+
const context = {
|
|
382
|
+
appHtml,
|
|
383
|
+
manifest: resolvedManifest,
|
|
384
|
+
routeIndex
|
|
385
|
+
};
|
|
386
|
+
const transformedHtml = await renderSsgRouteHtml(route, context, {
|
|
387
|
+
renderRoute: options.renderRoute,
|
|
388
|
+
transformHtml: options.transformHtml,
|
|
389
|
+
injectRoutePayload: options.injectRoutePayload
|
|
390
|
+
});
|
|
391
|
+
if (route.htmlFile === appHtmlFile) {
|
|
392
|
+
htmlAsset.source = transformedHtml;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
this.emitFile({
|
|
396
|
+
type: "asset",
|
|
397
|
+
fileName: route.htmlFile,
|
|
398
|
+
source: transformedHtml
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export { createSsgRouteHtml, createSsgRouteManifest, createSsgRoutePayloadScript, createVueSsgRouteRenderer, defaultSsgManifestFile, defaultSsgVirtualModuleId, discoverMarkdownSsgRoutes, escapeJsonForHtml, injectSsgRoutePayload, markdownFileToRoutePath, normalizeSsgBase, normalizeSsgRoute, normalizeSsgRoutePath, prerenderSsgRoutes, prerenderVueSsgRoutes, renderSsgRouteHtml, routePathToHtmlFile, routePathToId, viteSsgPlugin };
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@md-plugins/vite-ssg-plugin",
|
|
3
|
+
"version": "0.1.0-beta.23",
|
|
4
|
+
"description": "A Vite plugin for @md-plugins static route inventory and future SSG output.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"markdown-it",
|
|
7
|
+
"quasarframework",
|
|
8
|
+
"ssg",
|
|
9
|
+
"static-site-generation",
|
|
10
|
+
"utils",
|
|
11
|
+
"vite",
|
|
12
|
+
"vite-plugin",
|
|
13
|
+
"vue"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/md-plugins",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/md-plugins/md-plugins/issues"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "hawkeye64 <galbraith64@gmail.com>",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/md-plugins/md-plugins.git"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"./dist"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"module": "./dist/index.mjs",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"import": "./dist/index.mjs",
|
|
35
|
+
"default": "./dist/index.mjs"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"tinyglobby": "^0.2.17"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"vite": "^8.0.16"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@vue/server-renderer": "^3.5.0",
|
|
49
|
+
"vue": "^3.5.0"
|
|
50
|
+
},
|
|
51
|
+
"peerDependenciesMeta": {
|
|
52
|
+
"@vue/server-renderer": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"vue": {
|
|
56
|
+
"optional": true
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "unbuild",
|
|
61
|
+
"clean": "rm -rf dist/ node_modules/"
|
|
62
|
+
}
|
|
63
|
+
}
|