@forgebook/nuxt 0.0.0-stage → 0.2.0
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 +21 -0
- package/README.md +121 -2
- package/dist/src/app/components.d.ts +119 -0
- package/dist/src/app/components.js +196 -0
- package/dist/src/app/data.d.ts +109 -0
- package/dist/src/app/data.js +155 -0
- package/dist/src/app/define.d.ts +30 -0
- package/dist/src/app/define.js +30 -0
- package/dist/src/app/errors.d.ts +43 -0
- package/dist/src/app/errors.js +41 -0
- package/dist/src/app/fetch.d.ts +63 -0
- package/dist/src/app/fetch.js +73 -0
- package/dist/src/app/head.d.ts +28 -0
- package/dist/src/app/head.js +23 -0
- package/dist/src/app/router.d.ts +69 -0
- package/dist/src/app/router.js +93 -0
- package/dist/src/app/state.d.ts +107 -0
- package/dist/src/app/state.js +137 -0
- package/dist/src/asyncDataRun.d.ts +39 -0
- package/dist/src/asyncDataRun.js +128 -0
- package/dist/src/nuxtError.d.ts +50 -0
- package/dist/src/nuxtError.js +65 -0
- package/dist/src/nuxtRenderer.d.ts +14 -0
- package/dist/src/nuxtRenderer.js +67 -0
- package/dist/src/nuxtStore.d.ts +144 -0
- package/dist/src/nuxtStore.js +196 -0
- package/dist/src/pageProject.d.ts +11 -0
- package/dist/src/pageProject.js +11 -0
- package/dist/src/parameters.d.ts +67 -0
- package/dist/src/parameters.js +89 -0
- package/dist/src/project.d.ts +49 -0
- package/dist/src/project.js +7 -0
- package/dist/src/renderer.d.ts +9 -0
- package/dist/src/renderer.js +10 -0
- package/dist/src/stories.d.ts +54 -0
- package/dist/src/stories.js +11 -0
- package/dist/src/storyCode.d.ts +3 -0
- package/dist/src/storyCode.js +9 -0
- package/dist/src/storyFetch.d.ts +74 -0
- package/dist/src/storyFetch.js +146 -0
- package/dist/src/storyNuxt.d.ts +48 -0
- package/dist/src/storyNuxt.js +26 -0
- package/dist/src/vite/componentNames.d.ts +36 -0
- package/dist/src/vite/componentNames.js +72 -0
- package/dist/src/vite/extractDocs.d.ts +16 -0
- package/dist/src/vite/extractDocs.js +22 -0
- package/dist/src/vite/nuxt.d.ts +13 -0
- package/dist/src/vite/nuxt.js +36 -0
- package/dist/src/vite/nuxtConfig.d.ts +85 -0
- package/dist/src/vite/nuxtConfig.js +261 -0
- package/dist/src/vite/nuxtPlugin.d.ts +51 -0
- package/dist/src/vite/nuxtPlugin.js +466 -0
- package/dist/src/vite/pageRoutes.d.ts +23 -0
- package/dist/src/vite/pageRoutes.js +88 -0
- package/dist/src/vite/placeKeys.d.ts +10 -0
- package/dist/src/vite/placeKeys.js +117 -0
- package/dist/src/vite/standIns.d.ts +26 -0
- package/dist/src/vite/standIns.js +96 -0
- package/package.json +118 -3
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Vite plugin that lets a Nuxt project's components draw outside Nuxt: its aliases, its auto-imports, its
|
|
3
|
+
* components found by name, `#app`, `#imports` and `#components`, and the project the page knows (its pages' routes,
|
|
4
|
+
* its public runtime config, its app config, its style sheets).
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* Nuxt's own build is not run: the plugin reads `nuxt.config` (`nuxtConfig.ts`) and the project's folders, and does
|
|
8
|
+
* what Nuxt's build does for a component's code. A file that uses a composable without importing it gets the import,
|
|
9
|
+
* through unimport, the library Nuxt uses for it; a template that names a component gets the component's import, as
|
|
10
|
+
* Nuxt's loader writes it. The project's folders are read again when a file is added or removed in one, and the page
|
|
11
|
+
* reloads.
|
|
12
|
+
*
|
|
13
|
+
* Nuxt's composables come from this package's stand-ins (`src/app`), which are served from their sources, never
|
|
14
|
+
* pre-bundled: the stand-ins a component imports and the renderer that gives each story its Nuxt must be one module.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
import { readdirSync } from "node:fs";
|
|
19
|
+
import { join, posix, relative, sep } from "node:path";
|
|
20
|
+
import { codeLiteral } from "forgebook/code-literal";
|
|
21
|
+
import MagicString, {} from "magic-string";
|
|
22
|
+
import { createUnimport, scanDirExports, toExports } from "unimport";
|
|
23
|
+
import { loadEnv, normalizePath, parseAst } from "vite";
|
|
24
|
+
import { componentName, wordsOf } from "./componentNames.js";
|
|
25
|
+
import { loadNuxtConfig, withEnvironment } from "./nuxtConfig.js";
|
|
26
|
+
import { pageRoutes } from "./pageRoutes.js";
|
|
27
|
+
import { withPlaceKeys } from "./placeKeys.js";
|
|
28
|
+
import { BUILT_IN_COMPONENT_NAMES, COMPONENTS_MODULE, NUXT_STAND_INS, ROUTER_STAND_INS } from "./standIns.js";
|
|
29
|
+
/** The frame page's module: Forgebook's frame with the Nuxt renderer, after the project's style sheets. */
|
|
30
|
+
export const FRAME_MODULE = "virtual:forgebook-nuxt/frame";
|
|
31
|
+
/** The module the page reads the project from, which the plugin writes in a catalogue (`src/pageProject.ts`). */
|
|
32
|
+
const PROJECT_MODULE = "@forgebook/nuxt/project";
|
|
33
|
+
/** The modules the plugin writes, by the name code imports them with. */
|
|
34
|
+
const VIRTUAL_MODULES = [
|
|
35
|
+
"#app",
|
|
36
|
+
"nuxt/app",
|
|
37
|
+
"#imports",
|
|
38
|
+
"#components",
|
|
39
|
+
PROJECT_MODULE,
|
|
40
|
+
"virtual:forgebook-nuxt/styles",
|
|
41
|
+
FRAME_MODULE
|
|
42
|
+
];
|
|
43
|
+
/** What the name of a module the plugin writes starts with once resolved. */
|
|
44
|
+
const RESOLVED = "\0forgebook-nuxt:";
|
|
45
|
+
/**
|
|
46
|
+
* The id of a module the plugin writes, and back: a `#` would end the address the browser asks for it at, so it is
|
|
47
|
+
* written as `hash:`.
|
|
48
|
+
*
|
|
49
|
+
* @param name - the module, as code imports it.
|
|
50
|
+
*/
|
|
51
|
+
const resolvedId = (name) => `${RESOLVED}${name.replace(/^#/, "hash:")}`;
|
|
52
|
+
/**
|
|
53
|
+
* The module a resolved id names.
|
|
54
|
+
*
|
|
55
|
+
* @param id - the id.
|
|
56
|
+
*/
|
|
57
|
+
const nameOfId = (id) => id.slice(RESOLVED.length).replace(/^hash:/, "#");
|
|
58
|
+
/** Nuxt's build's own modules, which a catalogue cannot give. */
|
|
59
|
+
const BUILD_MODULE = /^#(build|internal|app-manifest|server)(\/|$)/;
|
|
60
|
+
/** A file whose code may use what Nuxt imports for it: a script, or a single-file component's script. */
|
|
61
|
+
const CODE_FILE = /\.(vue|[cm]?[jt]sx?)$/;
|
|
62
|
+
/** A call that may need the key of its place (`placeKeys.ts`): the code is parsed only when it has one. */
|
|
63
|
+
const KEYED_CALL = /\b(useState|useAsyncData|useLazyAsyncData|callOnce)\s*\(/;
|
|
64
|
+
/** A template's look-up of a component by name, as Vue's compiler writes it. */
|
|
65
|
+
const RESOLVE_COMPONENT = /(?<![\w$])_?resolveComponent\(\s*(["'`])([^"'`]+)\1\s*(?:,\s*true\s*)?\)/g;
|
|
66
|
+
/** A component's name, as `#components` and a template's import write it: a JavaScript name. */
|
|
67
|
+
const COMPONENT_NAME = /^[A-Za-z_$][\w$]*$/;
|
|
68
|
+
/**
|
|
69
|
+
* Every file of a folder, as paths from it with `/`; none for a folder that is not there.
|
|
70
|
+
*
|
|
71
|
+
* @param dir - the folder.
|
|
72
|
+
*/
|
|
73
|
+
const filesOf = (dir) => {
|
|
74
|
+
try {
|
|
75
|
+
return readdirSync(dir, { recursive: true, withFileTypes: true })
|
|
76
|
+
.filter((entry) => entry.isFile())
|
|
77
|
+
.map((entry) => relative(dir, join(entry.parentPath, entry.name)).split(sep).join("/"));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* The project's components, by name: the first folder that names one wins, as in Nuxt. A file whose name makes no
|
|
85
|
+
* JavaScript name (`a;b.vue`) is left out, since its name is written into code.
|
|
86
|
+
*
|
|
87
|
+
* @param config - the project's config.
|
|
88
|
+
*/
|
|
89
|
+
export const componentsOf = (config) => {
|
|
90
|
+
const found = new Map();
|
|
91
|
+
const skipped = [];
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
// the deepest folder first (`components/global` before `components`), and a file once, named by the first
|
|
94
|
+
const dirs = [...config.componentsDirs].sort((a, b) => b.path.length - a.path.length);
|
|
95
|
+
for (const dir of dirs)
|
|
96
|
+
for (const file of filesOf(dir.path)) {
|
|
97
|
+
const path = `${dir.path}/${file}`;
|
|
98
|
+
if (seen.has(path))
|
|
99
|
+
continue;
|
|
100
|
+
seen.add(path);
|
|
101
|
+
const name = componentName(file, dir.prefix, dir.pathPrefix);
|
|
102
|
+
if (name === undefined || found.has(name))
|
|
103
|
+
continue;
|
|
104
|
+
if (COMPONENT_NAME.test(name))
|
|
105
|
+
found.set(name, { name, file: path });
|
|
106
|
+
else
|
|
107
|
+
skipped.push(path);
|
|
108
|
+
}
|
|
109
|
+
return { components: found, skipped };
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Reads the project: its routes, its components, and what its files get without importing it.
|
|
113
|
+
*
|
|
114
|
+
* @param config - the project's config.
|
|
115
|
+
* @param env - the environment's variables, by name.
|
|
116
|
+
*/
|
|
117
|
+
const readProject = async (config, env) => {
|
|
118
|
+
// a composables folder's files, and the `index` of each of its folders, as Nuxt scans them
|
|
119
|
+
const scanned = await scanDirExports([...config.importsDirs], {
|
|
120
|
+
filePatterns: ["*.{ts,js,mjs,mts}", "*/index.{ts,js,mjs,mts}"],
|
|
121
|
+
cwd: config.rootDir
|
|
122
|
+
});
|
|
123
|
+
const imports = [...NUXT_STAND_INS, ...ROUTER_STAND_INS, ...scanned];
|
|
124
|
+
const { components, skipped } = componentsOf(config);
|
|
125
|
+
return {
|
|
126
|
+
config,
|
|
127
|
+
publicRuntimeConfig: withEnvironment(config.publicRuntimeConfig, env),
|
|
128
|
+
routes: pageRoutes(filesOf(config.pagesDir)),
|
|
129
|
+
components,
|
|
130
|
+
skipped,
|
|
131
|
+
unimport: createUnimport({ presets: ["vue"], imports, addons: { vueTemplate: true } }),
|
|
132
|
+
importNames: imports.map((each) => `${each.name} ${each.from}`)
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* What a page knows of the project's folders, as one text: two reads with the same one need no reload.
|
|
137
|
+
*
|
|
138
|
+
* @param state - the project.
|
|
139
|
+
*/
|
|
140
|
+
const shapeOf = (state) => JSON.stringify([state.routes, [...state.components.values()], state.importNames]);
|
|
141
|
+
/**
|
|
142
|
+
* The code of `#components`: each of the project's components, its `Lazy` form, and Nuxt's own.
|
|
143
|
+
*
|
|
144
|
+
* @param state - the project.
|
|
145
|
+
*/
|
|
146
|
+
const componentsModule = (state) => [
|
|
147
|
+
'import { defineAsyncComponent } from "vue";',
|
|
148
|
+
`export { ${BUILT_IN_COMPONENT_NAMES.join(", ")} } from ${codeLiteral(COMPONENTS_MODULE)};`,
|
|
149
|
+
...[...state.components.values()].flatMap(({ name, file }) => [
|
|
150
|
+
`export { default as ${name} } from ${codeLiteral(file)};`,
|
|
151
|
+
`export const Lazy${name} = defineAsyncComponent(() => import(${codeLiteral(file)}));`
|
|
152
|
+
])
|
|
153
|
+
].join("\n");
|
|
154
|
+
/**
|
|
155
|
+
* The code of the project the page knows: its routes, its public runtime config, its app config and its components'
|
|
156
|
+
* loaders.
|
|
157
|
+
*
|
|
158
|
+
* @param state - the project.
|
|
159
|
+
*/
|
|
160
|
+
const projectModule = (state) => {
|
|
161
|
+
const { config } = state;
|
|
162
|
+
const runtimeConfig = {
|
|
163
|
+
public: state.publicRuntimeConfig,
|
|
164
|
+
app: { baseURL: config.baseURL, buildAssetsDir: config.buildAssetsDir, cdnURL: "" }
|
|
165
|
+
};
|
|
166
|
+
const loaders = [...state.components.values()].map(({ name, file }) => ` ${codeLiteral(name)}: () => import(${codeLiteral(file)})`);
|
|
167
|
+
return [
|
|
168
|
+
...(config.appConfigFile === undefined
|
|
169
|
+
? []
|
|
170
|
+
: [`import appConfig from ${codeLiteral(config.appConfigFile)};`]),
|
|
171
|
+
"export const project = {",
|
|
172
|
+
` routes: ${codeLiteral(state.routes)},`,
|
|
173
|
+
` runtimeConfig: ${codeLiteral(runtimeConfig)},`,
|
|
174
|
+
` appConfigs: [${codeLiteral(config.appConfig)}${config.appConfigFile === undefined ? "" : ", appConfig"}],`,
|
|
175
|
+
` components: {\n${loaders.join(",\n")}\n }`,
|
|
176
|
+
"};"
|
|
177
|
+
].join("\n");
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* The code of a module the plugin writes.
|
|
181
|
+
*
|
|
182
|
+
* @param name - the module, as code imports it.
|
|
183
|
+
* @param state - the project.
|
|
184
|
+
*/
|
|
185
|
+
const virtualModule = async (name, state) => {
|
|
186
|
+
if (name === "#app" || name === "nuxt/app")
|
|
187
|
+
return toExports([...NUXT_STAND_INS]);
|
|
188
|
+
if (name === "#imports")
|
|
189
|
+
return toExports(await state.unimport.getImports());
|
|
190
|
+
if (name === "#components")
|
|
191
|
+
return componentsModule(state);
|
|
192
|
+
if (name === PROJECT_MODULE)
|
|
193
|
+
return projectModule(state);
|
|
194
|
+
if (name === "virtual:forgebook-nuxt/styles")
|
|
195
|
+
return state.config.css.map((sheet) => `import ${codeLiteral(sheet)};`).join("\n");
|
|
196
|
+
return [
|
|
197
|
+
'import "virtual:forgebook-nuxt/styles";',
|
|
198
|
+
'import { mountFrame as mountForgebookFrame } from "forgebook/frame";',
|
|
199
|
+
'import { nuxtRenderer } from "@forgebook/nuxt/renderer";',
|
|
200
|
+
"export const mountFrame = (container, config) => mountForgebookFrame(container, { ...config, renderer: nuxtRenderer });"
|
|
201
|
+
].join("\n");
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* The component a template names, as written there (`base-badge` or `BaseBadge`), when the plugin knows it.
|
|
205
|
+
*
|
|
206
|
+
* @param written - the name as written.
|
|
207
|
+
* @param components - the project's components, by name.
|
|
208
|
+
*/
|
|
209
|
+
const namedComponent = (written, components) => {
|
|
210
|
+
const name = wordsOf(written)
|
|
211
|
+
.map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1)}`)
|
|
212
|
+
.join("");
|
|
213
|
+
const isLazy = name.startsWith("Lazy") && components.has(name.slice(4));
|
|
214
|
+
const component = components.get(isLazy ? name.slice(4) : name);
|
|
215
|
+
const isBuiltIn = BUILT_IN_COMPONENT_NAMES.some((builtIn) => builtIn === name);
|
|
216
|
+
if (component === undefined && !isBuiltIn)
|
|
217
|
+
return undefined;
|
|
218
|
+
return { name, component, isLazy };
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* The statement that gives a component its local name.
|
|
222
|
+
*
|
|
223
|
+
* @param named - the component.
|
|
224
|
+
* @param local - its local name.
|
|
225
|
+
*/
|
|
226
|
+
const componentImport = ({ name, component, isLazy }, local) => {
|
|
227
|
+
if (component === undefined)
|
|
228
|
+
return `import { ${name} as ${local} } from ${codeLiteral(COMPONENTS_MODULE)};`;
|
|
229
|
+
if (isLazy)
|
|
230
|
+
return `const ${local} = __forgebookDefineAsync(() => import(${codeLiteral(component.file)}));`;
|
|
231
|
+
return `import ${local} from ${codeLiteral(component.file)};`;
|
|
232
|
+
};
|
|
233
|
+
/**
|
|
234
|
+
* A template's components given their imports, as Nuxt's loader writes them: Nuxt's own, the project's, and a project
|
|
235
|
+
* component's `Lazy` form loaded when it is first drawn.
|
|
236
|
+
*
|
|
237
|
+
* @param code - the compiled component.
|
|
238
|
+
* @param components - the project's components, by name.
|
|
239
|
+
* @returns the code changed, or none when it names no component the plugin knows.
|
|
240
|
+
*/
|
|
241
|
+
export const withComponentImports = (code, components) => {
|
|
242
|
+
const locals = new Map();
|
|
243
|
+
const statements = [];
|
|
244
|
+
for (const match of code.original.matchAll(RESOLVE_COMPONENT)) {
|
|
245
|
+
const named = namedComponent(match[2] ?? "", components);
|
|
246
|
+
if (named === undefined)
|
|
247
|
+
continue;
|
|
248
|
+
const local = locals.get(named.name) ?? `__nuxt_component_${locals.size}`;
|
|
249
|
+
if (!locals.has(named.name)) {
|
|
250
|
+
locals.set(named.name, local);
|
|
251
|
+
statements.push(componentImport(named, local));
|
|
252
|
+
}
|
|
253
|
+
code.overwrite(match.index, match.index + match[0].length, local);
|
|
254
|
+
}
|
|
255
|
+
if (locals.size === 0)
|
|
256
|
+
return undefined;
|
|
257
|
+
const isAsyncUsed = statements.some((line) => line.includes("__forgebookDefineAsync"));
|
|
258
|
+
const asyncImport = isAsyncUsed
|
|
259
|
+
? 'import { defineAsyncComponent as __forgebookDefineAsync } from "vue";\n'
|
|
260
|
+
: "";
|
|
261
|
+
code.prepend(`${asyncImport}${statements.join("\n")}\n`);
|
|
262
|
+
return code;
|
|
263
|
+
};
|
|
264
|
+
/** Nuxt 3's names for where code runs, as a project's code writes them. */
|
|
265
|
+
const PROCESS_FLAG = /(?<![\w$.])process\.(client|server|browser|dev)\b/g;
|
|
266
|
+
/**
|
|
267
|
+
* Nuxt 3's `process.client`, `process.server`, `process.browser` and `process.dev` written as their values in a
|
|
268
|
+
* project's file. Only there: given to the whole page (Vite's `define`), they would reach the libraries the frame
|
|
269
|
+
* loads, and one that reads `process.browser` would then read `process.env`, which the page does not have.
|
|
270
|
+
*
|
|
271
|
+
* @param code - the file's code, changed in place.
|
|
272
|
+
* @param isDev - whether the catalogue runs in development.
|
|
273
|
+
* @returns whether one was written.
|
|
274
|
+
*/
|
|
275
|
+
export const withProcessFlags = (code, isDev) => {
|
|
276
|
+
const values = {
|
|
277
|
+
client: "true",
|
|
278
|
+
browser: "true",
|
|
279
|
+
server: "false",
|
|
280
|
+
dev: codeLiteral(isDev)
|
|
281
|
+
};
|
|
282
|
+
let isWritten = false;
|
|
283
|
+
for (const match of code.original.matchAll(PROCESS_FLAG)) {
|
|
284
|
+
code.overwrite(match.index, match.index + match[0].length, values[match[1] ?? ""] ?? match[0]);
|
|
285
|
+
isWritten = true;
|
|
286
|
+
}
|
|
287
|
+
return isWritten;
|
|
288
|
+
};
|
|
289
|
+
/**
|
|
290
|
+
* Whether a file is one of the project's own: inside its folder, not in a folder beside it whose name starts the same
|
|
291
|
+
* (`app-admin` beside `app`).
|
|
292
|
+
*
|
|
293
|
+
* @param file - the file, as Vite names it.
|
|
294
|
+
* @param rootDir - the project's folder.
|
|
295
|
+
*/
|
|
296
|
+
export const isProjectFile = (file, rootDir) => file.startsWith(`${normalizePath(rootDir).replace(/\/$/, "")}/`);
|
|
297
|
+
/**
|
|
298
|
+
* Says which component files were left out, and why.
|
|
299
|
+
*
|
|
300
|
+
* @param logger - Vite's logger.
|
|
301
|
+
* @param skipped - the files.
|
|
302
|
+
*/
|
|
303
|
+
const warnSkipped = (logger, skipped) => {
|
|
304
|
+
for (const file of skipped)
|
|
305
|
+
logger.warn(`[forgebook] ${file} is not a component: its name is not a name code can hold`);
|
|
306
|
+
};
|
|
307
|
+
/**
|
|
308
|
+
* A project file's code given what Nuxt's build gives it: its components' imports, Nuxt 3's flags, the keys of its
|
|
309
|
+
* calls' places and its auto-imports.
|
|
310
|
+
*
|
|
311
|
+
* @param source - the code, changed in place.
|
|
312
|
+
* @param state - the project.
|
|
313
|
+
* @param file - the file, from the project's folder: the keys are made from it.
|
|
314
|
+
* @param id - the module, as Vite names it.
|
|
315
|
+
* @param isDev - whether the catalogue runs in development.
|
|
316
|
+
* @returns whether anything was written.
|
|
317
|
+
*/
|
|
318
|
+
const withNuxtCode = async (source, state, file, id, isDev) => {
|
|
319
|
+
const code = source.original;
|
|
320
|
+
const hasComponents = withComponentImports(source, state.components) !== undefined;
|
|
321
|
+
const hasProcessFlags = withProcessFlags(source, isDev);
|
|
322
|
+
const hasKeys = KEYED_CALL.test(code) && withPlaceKeys(source, parseAst(code), file);
|
|
323
|
+
const result = state.config.isAutoImport
|
|
324
|
+
? await state.unimport.injectImports(source, id, { autoImport: true })
|
|
325
|
+
: undefined;
|
|
326
|
+
return hasComponents || hasProcessFlags || hasKeys || (result !== undefined && result.imports.length > 0);
|
|
327
|
+
};
|
|
328
|
+
/**
|
|
329
|
+
* The aliases of a Nuxt project: its own, then `~~`, `@@`, `~`, `@`, `#shared`, `assets` and `public` as Nuxt gives
|
|
330
|
+
* them.
|
|
331
|
+
*
|
|
332
|
+
* @param config - the project's config.
|
|
333
|
+
*/
|
|
334
|
+
const aliasesOf = (config) => {
|
|
335
|
+
const prefix = (name) => new RegExp(`^${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?=/|$)`);
|
|
336
|
+
return [
|
|
337
|
+
...Object.entries(config.alias).map(([name, path]) => ({ find: prefix(name), replacement: path })),
|
|
338
|
+
{ find: prefix("~~"), replacement: config.rootDir },
|
|
339
|
+
{ find: prefix("@@"), replacement: config.rootDir },
|
|
340
|
+
{ find: prefix("~"), replacement: config.srcDir },
|
|
341
|
+
{ find: prefix("@"), replacement: config.srcDir },
|
|
342
|
+
{ find: prefix("#shared"), replacement: `${config.rootDir}/shared` },
|
|
343
|
+
{ find: /^assets(?=\/)/, replacement: `${config.srcDir}/assets` },
|
|
344
|
+
{ find: /^public(?=\/)/, replacement: `${config.rootDir}/public` }
|
|
345
|
+
];
|
|
346
|
+
};
|
|
347
|
+
/**
|
|
348
|
+
* The plugins of a Nuxt catalogue.
|
|
349
|
+
*
|
|
350
|
+
* @param options - the options of `nuxt()`.
|
|
351
|
+
* @returns the plugins.
|
|
352
|
+
*/
|
|
353
|
+
export const nuxtPlugins = (options = {}) => {
|
|
354
|
+
let state;
|
|
355
|
+
let env = {};
|
|
356
|
+
let problem;
|
|
357
|
+
let isDev = true;
|
|
358
|
+
const project = () => {
|
|
359
|
+
if (state === undefined)
|
|
360
|
+
throw new Error("The Nuxt project is read when Vite's configuration is");
|
|
361
|
+
return state;
|
|
362
|
+
};
|
|
363
|
+
const main = {
|
|
364
|
+
name: "forgebook:nuxt",
|
|
365
|
+
enforce: "pre",
|
|
366
|
+
config: async (userConfig, environment) => {
|
|
367
|
+
const rootDir = join(userConfig.root ?? process.cwd(), options.dir ?? ".");
|
|
368
|
+
const loaded = await loadNuxtConfig(rootDir);
|
|
369
|
+
problem = loaded.problem;
|
|
370
|
+
// Nuxt's variables only: the environment's others never reach the page
|
|
371
|
+
env = loadEnv(environment.mode, rootDir, "NUXT_");
|
|
372
|
+
state = await readProject(loaded.config, env);
|
|
373
|
+
isDev = environment.command === "serve";
|
|
374
|
+
return {
|
|
375
|
+
resolve: { alias: aliasesOf(loaded.config) },
|
|
376
|
+
define: {
|
|
377
|
+
"import.meta.client": "true",
|
|
378
|
+
"import.meta.browser": "true",
|
|
379
|
+
"import.meta.server": "false",
|
|
380
|
+
"import.meta.nitro": "false",
|
|
381
|
+
"import.meta.prerender": "false",
|
|
382
|
+
"import.meta.dev": codeLiteral(isDev),
|
|
383
|
+
"import.meta.test": "false"
|
|
384
|
+
},
|
|
385
|
+
// the stand-ins and the renderer are one module graph, served from their sources: see the module's remarks
|
|
386
|
+
optimizeDeps: { exclude: ["@forgebook/nuxt"] },
|
|
387
|
+
ssr: { noExternal: ["@forgebook/nuxt"] }
|
|
388
|
+
};
|
|
389
|
+
},
|
|
390
|
+
configResolved: (config) => {
|
|
391
|
+
if (problem !== undefined)
|
|
392
|
+
config.logger.warn(`[forgebook] ${problem}`);
|
|
393
|
+
warnSkipped(config.logger, project().skipped);
|
|
394
|
+
},
|
|
395
|
+
resolveId: (id) => {
|
|
396
|
+
if (VIRTUAL_MODULES.some((name) => name === id))
|
|
397
|
+
return resolvedId(id);
|
|
398
|
+
// a deep import of Nuxt's app (`#app/composables/router`) names a part of what `#app` exports
|
|
399
|
+
if (id.startsWith("#app/") || id.startsWith("nuxt/app/"))
|
|
400
|
+
return resolvedId("#app");
|
|
401
|
+
if (BUILD_MODULE.test(id))
|
|
402
|
+
return resolvedId(`build:${id.slice(1)}`);
|
|
403
|
+
return undefined;
|
|
404
|
+
},
|
|
405
|
+
load: async (id) => {
|
|
406
|
+
if (!id.startsWith(RESOLVED))
|
|
407
|
+
return undefined;
|
|
408
|
+
const name = nameOfId(id);
|
|
409
|
+
if (name.startsWith("build:"))
|
|
410
|
+
return `throw new Error(${codeLiteral(`#${name.slice(6)} is written by Nuxt's build, which a catalogue does not run`)});`;
|
|
411
|
+
return virtualModule(name, project());
|
|
412
|
+
},
|
|
413
|
+
configureServer: (server) => {
|
|
414
|
+
const isWatched = (file, isAddedOrRemoved) => {
|
|
415
|
+
const { config } = project();
|
|
416
|
+
// the watcher names a file as the system writes it, the config's folders are in Vite's form
|
|
417
|
+
const isIn = (dir) => normalizePath(file).startsWith(`${dir}/`);
|
|
418
|
+
// an added or removed page or component changes the project; an edited one only its own module
|
|
419
|
+
if (config.importsDirs.some(isIn))
|
|
420
|
+
return true;
|
|
421
|
+
return (isAddedOrRemoved && [config.pagesDir, ...config.componentsDirs.map((dir) => dir.path)].some(isIn));
|
|
422
|
+
};
|
|
423
|
+
const reread = async (file, isAddedOrRemoved) => {
|
|
424
|
+
if (!isWatched(file, isAddedOrRemoved))
|
|
425
|
+
return;
|
|
426
|
+
const before = shapeOf(project());
|
|
427
|
+
state = await readProject(project().config, env);
|
|
428
|
+
warnSkipped(server.config.logger, project().skipped);
|
|
429
|
+
// an edit that changes no export of a composable leaves the page to its hot update
|
|
430
|
+
if (shapeOf(project()) === before)
|
|
431
|
+
return;
|
|
432
|
+
for (const name of VIRTUAL_MODULES) {
|
|
433
|
+
const module = server.moduleGraph.getModuleById(resolvedId(name));
|
|
434
|
+
if (module !== undefined)
|
|
435
|
+
server.moduleGraph.invalidateModule(module);
|
|
436
|
+
}
|
|
437
|
+
server.ws.send({ type: "full-reload" });
|
|
438
|
+
};
|
|
439
|
+
server.watcher.on("add", (file) => void reread(file, true));
|
|
440
|
+
server.watcher.on("unlink", (file) => void reread(file, true));
|
|
441
|
+
server.watcher.on("change", (file) => void reread(file, false));
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
// after Vue's plugin has compiled a component, before Vite's own post plugins resolve the imports this one adds
|
|
445
|
+
const imports = {
|
|
446
|
+
name: "forgebook:nuxt-imports",
|
|
447
|
+
enforce: "post",
|
|
448
|
+
transform: {
|
|
449
|
+
async handler(code, id) {
|
|
450
|
+
const [file = "", query = ""] = id.split("?");
|
|
451
|
+
if (id.startsWith("\0") || !CODE_FILE.test(file) || file.includes("/node_modules/"))
|
|
452
|
+
return undefined;
|
|
453
|
+
if (query !== "" && !query.includes("type=script"))
|
|
454
|
+
return undefined;
|
|
455
|
+
const current = project();
|
|
456
|
+
if (!isProjectFile(file, current.config.rootDir))
|
|
457
|
+
return undefined;
|
|
458
|
+
const source = new MagicString(code);
|
|
459
|
+
if (!(await withNuxtCode(source, current, posix.relative(current.config.rootDir, file), id, isDev)))
|
|
460
|
+
return undefined;
|
|
461
|
+
return { code: source.toString(), map: source.generateMap({ hires: "boundary", source: id }) };
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
return [main, imports];
|
|
466
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The routes of a Nuxt project's pages, named and written as Nuxt makes them from the files: `pages/blog/[slug].vue` is
|
|
3
|
+
* `/blog/:slug()`, named `blog-slug`.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Pure: paths in, routes out. A story needs a page's route to set its params and its name, so `useRoute()` returns
|
|
7
|
+
* what the page would see; it draws its own component, not the page, so the routes are flat: a parent page and its
|
|
8
|
+
* children are each a route with its full path, the parent left out when an `index` child takes its path. `definePageMeta`'s `path` and `name` are not
|
|
9
|
+
* read: they need the page's code run.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import type { ProjectRoute } from "../project.ts";
|
|
14
|
+
/** The extensions of a page. */
|
|
15
|
+
export declare const PAGE_EXTENSIONS: RegExp;
|
|
16
|
+
/**
|
|
17
|
+
* The routes of a project's pages.
|
|
18
|
+
*
|
|
19
|
+
* @param files - the pages' paths from the pages' folder, with `/` (`blog/[slug].vue`).
|
|
20
|
+
* @returns the routes; a page whose folder has an `index` page is left out, since that page carries its path and name,
|
|
21
|
+
* as in Nuxt, where the parent keeps neither.
|
|
22
|
+
*/
|
|
23
|
+
export declare const pageRoutes: (files: readonly string[]) => ProjectRoute[];
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** The extensions of a page. */
|
|
2
|
+
export const PAGE_EXTENSIONS = /\.(vue|tsx|jsx|ts|js|mjs)$/;
|
|
3
|
+
/**
|
|
4
|
+
* A segment of a page's path split into its pieces: `[slug]`, `[[slug]]`, `[...slug]` and `[[...slug]]` are params.
|
|
5
|
+
*
|
|
6
|
+
* @param segment - the segment (`post-[id]`).
|
|
7
|
+
*/
|
|
8
|
+
const tokensOf = (segment) => {
|
|
9
|
+
const tokens = [];
|
|
10
|
+
const pattern = /\[\[\.\.\.([^\]]+)\]\]|\[\[([^\]]+)\]\]|\[\.\.\.([^\]]+)\]|\[([^\]]+)\]/g;
|
|
11
|
+
let last = 0;
|
|
12
|
+
for (const match of segment.matchAll(pattern)) {
|
|
13
|
+
if (match.index > last)
|
|
14
|
+
tokens.push({ kind: "text", value: segment.slice(last, match.index) });
|
|
15
|
+
const [, optionalCatchAll, optional, catchAll, param] = match;
|
|
16
|
+
if (optionalCatchAll !== undefined)
|
|
17
|
+
tokens.push({ kind: "catchAll", value: optionalCatchAll });
|
|
18
|
+
else if (optional !== undefined)
|
|
19
|
+
tokens.push({ kind: "optional", value: optional });
|
|
20
|
+
else if (catchAll !== undefined)
|
|
21
|
+
tokens.push({ kind: "catchAll", value: catchAll });
|
|
22
|
+
else if (param !== undefined)
|
|
23
|
+
tokens.push({ kind: "param", value: param });
|
|
24
|
+
last = match.index + match[0].length;
|
|
25
|
+
}
|
|
26
|
+
if (last < segment.length)
|
|
27
|
+
tokens.push({ kind: "text", value: segment.slice(last) });
|
|
28
|
+
return tokens;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* A segment as vue-router writes it: a catch-all takes the rest of the path when it ends it, else what is left of its
|
|
32
|
+
* own segment, as Nuxt writes it.
|
|
33
|
+
*
|
|
34
|
+
* @param tokens - the segment's pieces.
|
|
35
|
+
* @param isLast - whether the segment ends the path.
|
|
36
|
+
*/
|
|
37
|
+
const pathOfTokens = (tokens, isLast) => tokens
|
|
38
|
+
.map((token) => {
|
|
39
|
+
if (token.kind === "text")
|
|
40
|
+
return token.value;
|
|
41
|
+
if (token.kind === "param")
|
|
42
|
+
return `:${token.value}()`;
|
|
43
|
+
if (token.kind === "optional")
|
|
44
|
+
return `:${token.value}?`;
|
|
45
|
+
return isLast ? `:${token.value}(.*)*` : `:${token.value}([^/]*)*`;
|
|
46
|
+
})
|
|
47
|
+
.join("");
|
|
48
|
+
/**
|
|
49
|
+
* The route of a page.
|
|
50
|
+
*
|
|
51
|
+
* @param file - the page's path from the pages' folder, with `/` (`blog/[slug].vue`).
|
|
52
|
+
* @returns its route, or none for a file Nuxt ignores (`-draft.vue`).
|
|
53
|
+
*/
|
|
54
|
+
const routeOf = (file) => {
|
|
55
|
+
const segments = file.replace(PAGE_EXTENSIONS, "").split("/");
|
|
56
|
+
if (segments.some((segment) => segment.startsWith("-")))
|
|
57
|
+
return undefined;
|
|
58
|
+
// a route group, `(admin)`, is a folder that is not part of the path or the name
|
|
59
|
+
const kept = segments.filter((segment) => !/^\(.*\)$/.test(segment));
|
|
60
|
+
const isIndex = kept.at(-1) === "index";
|
|
61
|
+
const named = isIndex ? kept.slice(0, -1) : kept;
|
|
62
|
+
const path = `/${named.map((segment, index) => pathOfTokens(tokensOf(segment), index === named.length - 1)).join("/")}`;
|
|
63
|
+
const name = named.map((segment) => segment.replace(/\[\[?(\.\.\.)?|\]\]?/g, "")).join("-");
|
|
64
|
+
return { path, name: name === "" ? "index" : name };
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* The routes of a project's pages.
|
|
68
|
+
*
|
|
69
|
+
* @param files - the pages' paths from the pages' folder, with `/` (`blog/[slug].vue`).
|
|
70
|
+
* @returns the routes; a page whose folder has an `index` page is left out, since that page carries its path and name,
|
|
71
|
+
* as in Nuxt, where the parent keeps neither.
|
|
72
|
+
*/
|
|
73
|
+
export const pageRoutes = (files) => {
|
|
74
|
+
const pages = files.filter((file) => PAGE_EXTENSIONS.test(file));
|
|
75
|
+
const indexed = new Set(pages.flatMap((file) => {
|
|
76
|
+
const parts = file.replace(PAGE_EXTENSIONS, "").split("/");
|
|
77
|
+
return parts.length > 1 && parts.at(-1) === "index" ? [parts.slice(0, -1).join("/")] : [];
|
|
78
|
+
}));
|
|
79
|
+
const routes = new Map();
|
|
80
|
+
for (const file of pages) {
|
|
81
|
+
if (indexed.has(file.replace(PAGE_EXTENSIONS, "")))
|
|
82
|
+
continue;
|
|
83
|
+
const route = routeOf(file);
|
|
84
|
+
if (route !== undefined && !routes.has(route.name))
|
|
85
|
+
routes.set(route.name, route);
|
|
86
|
+
}
|
|
87
|
+
return [...routes.values()];
|
|
88
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type MagicString from "magic-string";
|
|
2
|
+
/**
|
|
3
|
+
* Gives each call to a keyed function without a key the key of its place.
|
|
4
|
+
*
|
|
5
|
+
* @param code - the file's code, changed in place.
|
|
6
|
+
* @param program - its syntax tree, whose places are the code's.
|
|
7
|
+
* @param file - the file, as the project names it, which the keys are made from.
|
|
8
|
+
* @returns whether a key was written.
|
|
9
|
+
*/
|
|
10
|
+
export declare const withPlaceKeys: (code: MagicString, program: unknown, file: string) => boolean;
|