@vobs/vite-plugin 0.1.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/dist/client.d.ts +48 -0
- package/dist/client.js +5 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +1127 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vobsjs
|
|
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/dist/client.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** @license MIT
|
|
2
|
+
* Copyright (c) 2026 vobsjs
|
|
3
|
+
* @vobs/vite-plugin
|
|
4
|
+
*/
|
|
5
|
+
declare module '*.html' {
|
|
6
|
+
const template: import('@vobs/runtime-dom').CompiledTemplate<object>;
|
|
7
|
+
export default template;
|
|
8
|
+
}
|
|
9
|
+
declare module '*?vobs-image' {
|
|
10
|
+
export interface VobsImageMetadata {
|
|
11
|
+
readonly src: string;
|
|
12
|
+
readonly width: number;
|
|
13
|
+
readonly height: number;
|
|
14
|
+
readonly format: 'jpeg' | 'png' | 'svg' | 'webp';
|
|
15
|
+
}
|
|
16
|
+
const metadata: VobsImageMetadata;
|
|
17
|
+
export default metadata;
|
|
18
|
+
}
|
|
19
|
+
declare module '*.svg?raw' {
|
|
20
|
+
const source: string;
|
|
21
|
+
export default source;
|
|
22
|
+
}
|
|
23
|
+
declare module 'vobs:icons' {
|
|
24
|
+
const icons: import('@vobs/icons').IconRegistry;
|
|
25
|
+
export { icons };
|
|
26
|
+
export default icons;
|
|
27
|
+
}
|
|
28
|
+
declare module 'vobs:routes' {
|
|
29
|
+
import type { RouteComponent, RouteRecord, RouterOptions } from '@vobs/router';
|
|
30
|
+
import type { RouterPort } from '@vobs/runtime-core';
|
|
31
|
+
export interface GeneratedRoutePathMap {
|
|
32
|
+
}
|
|
33
|
+
export type RoutePath = keyof GeneratedRoutePathMap extends never ? string : Extract<keyof GeneratedRoutePathMap, string>;
|
|
34
|
+
const generatedRoutes: readonly RouteRecord[];
|
|
35
|
+
const generatedRoutePaths: readonly RoutePath[];
|
|
36
|
+
const generatedNotFound: RouteComponent | undefined;
|
|
37
|
+
const generatedError: RouteComponent | undefined;
|
|
38
|
+
export type GeneratedRoutingManifest = {
|
|
39
|
+
readonly routes: typeof generatedRoutes;
|
|
40
|
+
readonly routePaths: typeof generatedRoutePaths;
|
|
41
|
+
readonly notFound: typeof generatedNotFound;
|
|
42
|
+
readonly error: typeof generatedError;
|
|
43
|
+
};
|
|
44
|
+
export function createRoutingRouter(options?: Omit<RouterOptions, 'routes' | 'notFound' | 'error'>): RouterPort | undefined;
|
|
45
|
+
const router: RouterPort | undefined;
|
|
46
|
+
export { generatedError as error, generatedNotFound as notFound, generatedRoutePaths as routePaths, generatedRoutes as routes, };
|
|
47
|
+
export default router;
|
|
48
|
+
}
|
package/dist/client.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** @license MIT
|
|
2
|
+
* Copyright (c) 2026 vobsjs
|
|
3
|
+
* @vobs/vite-plugin
|
|
4
|
+
*/
|
|
5
|
+
export interface VobsVitePlugin {
|
|
6
|
+
readonly name: 'vobs-vite-plugin';
|
|
7
|
+
readonly enforce: 'pre';
|
|
8
|
+
/** Vite/Rollup binds this hook helper at call time. */
|
|
9
|
+
addWatchFile?(fileName: string): void;
|
|
10
|
+
configResolved(config: {
|
|
11
|
+
readonly root: string;
|
|
12
|
+
}): void;
|
|
13
|
+
buildStart(this: VobsBuildStartContext): void;
|
|
14
|
+
closeWatcher(): void;
|
|
15
|
+
resolveId(id: string, importer?: string): string | null;
|
|
16
|
+
load(this: VobsLoadContext, id: string): Promise<string | {
|
|
17
|
+
readonly code: string;
|
|
18
|
+
readonly map: object;
|
|
19
|
+
} | null>;
|
|
20
|
+
transform(this: VobsTransformContext, code: string, id: string): {
|
|
21
|
+
readonly code: string;
|
|
22
|
+
readonly map: object;
|
|
23
|
+
} | null;
|
|
24
|
+
}
|
|
25
|
+
interface VobsTransformContext {
|
|
26
|
+
addWatchFile?(fileName: string): void;
|
|
27
|
+
}
|
|
28
|
+
interface VobsLoadContext {
|
|
29
|
+
addWatchFile?(fileName: string): void;
|
|
30
|
+
}
|
|
31
|
+
interface VobsBuildStartContext {
|
|
32
|
+
addWatchFile?(fileName: string): void;
|
|
33
|
+
}
|
|
34
|
+
export interface VobsVitePluginOptions {
|
|
35
|
+
readonly runtimeModule?: string;
|
|
36
|
+
readonly check?: boolean | VobsViteCheckOptions;
|
|
37
|
+
readonly routes?: boolean | VobsViteRoutesOptions;
|
|
38
|
+
readonly images?: boolean;
|
|
39
|
+
readonly icons?: boolean | VobsViteIconsOptions;
|
|
40
|
+
}
|
|
41
|
+
export interface VobsViteCheckOptions {
|
|
42
|
+
readonly tsconfigPath?: string;
|
|
43
|
+
readonly rootNames?: readonly string[];
|
|
44
|
+
readonly viewFiles?: readonly string[];
|
|
45
|
+
}
|
|
46
|
+
export interface VobsViteRoutesOptions {
|
|
47
|
+
readonly pagesDir?: string;
|
|
48
|
+
readonly typesFile?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface VobsViteIconsOptions {
|
|
51
|
+
readonly config?: string;
|
|
52
|
+
}
|
|
53
|
+
export declare function vobs(options?: VobsVitePluginOptions): VobsVitePlugin;
|
|
54
|
+
export default vobs;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,1127 @@
|
|
|
1
|
+
/** @license MIT
|
|
2
|
+
* Copyright (c) 2026 vobsjs
|
|
3
|
+
* @vobs/vite-plugin
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
6
|
+
import { access, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import * as ts from 'typescript';
|
|
10
|
+
import { compileView, createViewProjectChecker, transformOwnerAutoExpose, } from '@vobs/compiler-dom';
|
|
11
|
+
const routesModuleId = 'vobs:routes';
|
|
12
|
+
const resolvedRoutesModuleId = '\0vobs:routes';
|
|
13
|
+
const iconsModuleId = 'vobs:icons';
|
|
14
|
+
const resolvedIconsModuleId = '\0vobs:icons';
|
|
15
|
+
const imageMetadataQuery = 'vobs-image';
|
|
16
|
+
const resolvedImageMetadataPrefix = '\0vobs:image:';
|
|
17
|
+
const resolvedHtmlViewPrefix = '\0vobs:view:';
|
|
18
|
+
export function vobs(options = {}) {
|
|
19
|
+
let root = process.cwd();
|
|
20
|
+
let projectChecker;
|
|
21
|
+
return {
|
|
22
|
+
name: 'vobs-vite-plugin',
|
|
23
|
+
enforce: 'pre',
|
|
24
|
+
configResolved(config) {
|
|
25
|
+
projectChecker?.dispose();
|
|
26
|
+
projectChecker = undefined;
|
|
27
|
+
root = config.root;
|
|
28
|
+
},
|
|
29
|
+
buildStart() {
|
|
30
|
+
if (options.check !== false) {
|
|
31
|
+
projectChecker ??= createViewProjectChecker(createCheckOptions(root, options.check, options));
|
|
32
|
+
const result = projectChecker.check();
|
|
33
|
+
const errors = result.diagnostics.filter((diagnostic) => diagnostic.severity === 'error');
|
|
34
|
+
const warnings = result.diagnostics.filter((diagnostic) => diagnostic.severity === 'warning');
|
|
35
|
+
if (warnings.length > 0) {
|
|
36
|
+
console.warn(`vobs check warnings\n${formatDiagnostics(warnings)}`);
|
|
37
|
+
}
|
|
38
|
+
if (errors.length > 0) {
|
|
39
|
+
throw new Error(`vobs check failed\n${formatDiagnostics(errors)}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
validateIconProject(root, options, (fileName) => this.addWatchFile?.(fileName));
|
|
43
|
+
},
|
|
44
|
+
closeWatcher() {
|
|
45
|
+
projectChecker?.dispose();
|
|
46
|
+
projectChecker = undefined;
|
|
47
|
+
},
|
|
48
|
+
resolveId(id, importer) {
|
|
49
|
+
if (id === routesModuleId)
|
|
50
|
+
return resolvedRoutesModuleId;
|
|
51
|
+
if (id === iconsModuleId && iconSupportEnabled(options))
|
|
52
|
+
return resolvedIconsModuleId;
|
|
53
|
+
const viewFilePath = resolveHtmlViewRequest(id, root, importer);
|
|
54
|
+
if (viewFilePath !== undefined)
|
|
55
|
+
return `${resolvedHtmlViewPrefix}${encodeViewModuleId(viewFilePath)}`;
|
|
56
|
+
const imageFilePath = resolveImageMetadataRequest(id, root, importer, options.images);
|
|
57
|
+
return imageFilePath === undefined ? null : `${resolvedImageMetadataPrefix}${imageFilePath}`;
|
|
58
|
+
},
|
|
59
|
+
async load(id) {
|
|
60
|
+
if (id.startsWith(resolvedHtmlViewPrefix)) {
|
|
61
|
+
const filePath = decodeViewModuleId(id.slice(resolvedHtmlViewPrefix.length));
|
|
62
|
+
const code = await readFile(filePath, 'utf8');
|
|
63
|
+
this.addWatchFile?.(filePath);
|
|
64
|
+
return compileHtmlView(code, filePath, options, (dependency) => this.addWatchFile?.(dependency));
|
|
65
|
+
}
|
|
66
|
+
if (id.startsWith(resolvedImageMetadataPrefix)) {
|
|
67
|
+
const filePath = id.slice(resolvedImageMetadataPrefix.length);
|
|
68
|
+
const metadata = readImageMetadata(filePath, await readFile(filePath));
|
|
69
|
+
this.addWatchFile?.(filePath);
|
|
70
|
+
return emitImageMetadataModule(root, filePath, metadata);
|
|
71
|
+
}
|
|
72
|
+
if (id === resolvedIconsModuleId) {
|
|
73
|
+
return emitIconsModule(root, options, (fileName) => this.addWatchFile?.(fileName));
|
|
74
|
+
}
|
|
75
|
+
if (id !== resolvedRoutesModuleId)
|
|
76
|
+
return null;
|
|
77
|
+
if (options.routes === false)
|
|
78
|
+
return 'export const routes=[];\nexport const routePaths=[];\nexport const notFound=undefined;\nexport const error=undefined;\n/** @satisfies {import("@vobs/router").RouterOptions} */\nexport function createRoutingRouter(options={}){return undefined;}\nexport default undefined;';
|
|
79
|
+
const manifest = await collectFileRoutes(root, options.routes);
|
|
80
|
+
await writeRoutesTypesFile(root, options.routes, manifest);
|
|
81
|
+
const watchedFiles = new Set();
|
|
82
|
+
for (const route of manifest.routes) {
|
|
83
|
+
watchedFiles.add(route.filePath);
|
|
84
|
+
for (const layout of route.layouts) {
|
|
85
|
+
watchedFiles.add(layout);
|
|
86
|
+
}
|
|
87
|
+
if (route.error !== undefined) {
|
|
88
|
+
watchedFiles.add(route.error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (manifest.notFound !== undefined) {
|
|
92
|
+
watchedFiles.add(manifest.notFound.filePath);
|
|
93
|
+
}
|
|
94
|
+
if (manifest.error !== undefined) {
|
|
95
|
+
watchedFiles.add(manifest.error);
|
|
96
|
+
}
|
|
97
|
+
for (const filePath of watchedFiles) {
|
|
98
|
+
this.addWatchFile?.(filePath);
|
|
99
|
+
}
|
|
100
|
+
return emitRoutesModule(manifest, root);
|
|
101
|
+
},
|
|
102
|
+
transform(code, id) {
|
|
103
|
+
if (id.startsWith(resolvedHtmlViewPrefix))
|
|
104
|
+
return null;
|
|
105
|
+
const cleanId = id.split('?')[0];
|
|
106
|
+
if (cleanId === undefined)
|
|
107
|
+
return null;
|
|
108
|
+
if (isTypeScriptModule(cleanId)) {
|
|
109
|
+
const result = transformOwnerAutoExpose(code, {
|
|
110
|
+
sourceId: cleanId,
|
|
111
|
+
readFile: readFileIfExists,
|
|
112
|
+
globalComponentTags: iconComponentTags(options),
|
|
113
|
+
});
|
|
114
|
+
if (!result.changed)
|
|
115
|
+
return null;
|
|
116
|
+
for (const dependency of result.dependencies) {
|
|
117
|
+
this.addWatchFile?.(dependency);
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
code: result.code,
|
|
121
|
+
map: result.map,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (!cleanId.endsWith('.html'))
|
|
125
|
+
return null;
|
|
126
|
+
if (isViteHtmlEntry(cleanId, root))
|
|
127
|
+
return null;
|
|
128
|
+
return compileHtmlView(code, cleanId, options, (dependency) => this.addWatchFile?.(dependency));
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
export default vobs;
|
|
133
|
+
function isTypeScriptModule(id) {
|
|
134
|
+
return (id.endsWith('.ts') || id.endsWith('.tsx')) && !id.endsWith('.d.ts');
|
|
135
|
+
}
|
|
136
|
+
function resolveHtmlViewRequest(id, root, importer) {
|
|
137
|
+
if (importer === undefined)
|
|
138
|
+
return undefined;
|
|
139
|
+
const request = splitModuleRequest(id);
|
|
140
|
+
if (request.query !== '')
|
|
141
|
+
return undefined;
|
|
142
|
+
if (!request.path.endsWith('.html'))
|
|
143
|
+
return undefined;
|
|
144
|
+
const importerPath = importer.split('?')[0];
|
|
145
|
+
const base = importerPath === undefined ? root : path.dirname(importerPath);
|
|
146
|
+
const filePath = path.resolve(path.isAbsolute(request.path) ? request.path : path.join(base, request.path));
|
|
147
|
+
if (!isPathInsideRoot(root, filePath))
|
|
148
|
+
return undefined;
|
|
149
|
+
if (isViteHtmlEntry(filePath, root))
|
|
150
|
+
return undefined;
|
|
151
|
+
return filePath;
|
|
152
|
+
}
|
|
153
|
+
function encodeViewModuleId(filePath) {
|
|
154
|
+
return Buffer.from(filePath, 'utf8').toString('base64url');
|
|
155
|
+
}
|
|
156
|
+
function decodeViewModuleId(value) {
|
|
157
|
+
return Buffer.from(value, 'base64url').toString('utf8');
|
|
158
|
+
}
|
|
159
|
+
function compileHtmlView(code, sourceId, options, addWatchFile) {
|
|
160
|
+
const result = compileView(code, options.runtimeModule === undefined
|
|
161
|
+
? { sourceId }
|
|
162
|
+
: {
|
|
163
|
+
sourceId,
|
|
164
|
+
runtimeModule: options.runtimeModule,
|
|
165
|
+
});
|
|
166
|
+
const errors = result.diagnostics.filter((diagnostic) => diagnostic.severity === 'error');
|
|
167
|
+
const warnings = result.diagnostics.filter((diagnostic) => diagnostic.severity === 'warning');
|
|
168
|
+
if (warnings.length > 0) {
|
|
169
|
+
console.warn(formatDiagnostics(warnings));
|
|
170
|
+
}
|
|
171
|
+
if (errors.length > 0) {
|
|
172
|
+
throw new Error(errors.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`).join('\n'));
|
|
173
|
+
}
|
|
174
|
+
for (const dependency of result.dependencies) {
|
|
175
|
+
if (dependency !== sourceId) {
|
|
176
|
+
addWatchFile(dependency);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// HMR (P1-5): template module accept — .html change -> recompile -> notify the runtime to replace the template and remount the page
|
|
180
|
+
// (hydratePage is registered automatically; standalone page.mount scenarios must call registerPageHmr explicitly).
|
|
181
|
+
// import.meta.hot is injected by Vite; outside Vite (Node tests) the condition is false, leaving the artifact unaffected.
|
|
182
|
+
const templateIdMatch = result.code.match(/templateId: "([^"]+)"/);
|
|
183
|
+
const templateId = templateIdMatch?.[1] ?? sourceId;
|
|
184
|
+
const hmrBlock = `
|
|
185
|
+
if (import.meta.hot) {
|
|
186
|
+
import.meta.hot.accept((mod) => {
|
|
187
|
+
void import(${JSON.stringify(options.runtimeModule ?? '@vobs/runtime-dom')}).then((rt) => {
|
|
188
|
+
void rt.reloadHmrTemplate(${JSON.stringify(templateId)}, mod?.default);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}`;
|
|
192
|
+
return {
|
|
193
|
+
code: `${result.code}\n${hmrBlock}`,
|
|
194
|
+
map: result.map,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function resolveImageMetadataRequest(id, root, importer, enabled) {
|
|
198
|
+
if (enabled === false)
|
|
199
|
+
return undefined;
|
|
200
|
+
const request = splitModuleRequest(id);
|
|
201
|
+
if (!hasQueryFlag(request.query, imageMetadataQuery))
|
|
202
|
+
return undefined;
|
|
203
|
+
if (!isSupportedImageFile(request.path)) {
|
|
204
|
+
throw new Error(`vobs image metadata only supports PNG, JPEG, WebP, and SVG: ${id}`);
|
|
205
|
+
}
|
|
206
|
+
const importerPath = importer?.split('?')[0];
|
|
207
|
+
const base = importerPath === undefined ? root : path.dirname(importerPath);
|
|
208
|
+
const filePath = path.resolve(path.isAbsolute(request.path) ? request.path : path.join(base, request.path));
|
|
209
|
+
if (!isPathInsideRoot(root, filePath)) {
|
|
210
|
+
throw new Error(`vobs image metadata import must stay inside the Vite root: ${id}`);
|
|
211
|
+
}
|
|
212
|
+
return filePath;
|
|
213
|
+
}
|
|
214
|
+
function splitModuleRequest(id) {
|
|
215
|
+
const queryStart = id.indexOf('?');
|
|
216
|
+
if (queryStart < 0)
|
|
217
|
+
return { path: id, query: '' };
|
|
218
|
+
return { path: id.slice(0, queryStart), query: id.slice(queryStart + 1) };
|
|
219
|
+
}
|
|
220
|
+
function hasQueryFlag(query, flag) {
|
|
221
|
+
return new URLSearchParams(query).has(flag);
|
|
222
|
+
}
|
|
223
|
+
function isSupportedImageFile(filePath) {
|
|
224
|
+
return /\.(?:png|jpe?g|webp|svg)$/iu.test(filePath);
|
|
225
|
+
}
|
|
226
|
+
function isPathInsideRoot(root, filePath) {
|
|
227
|
+
const relative = path.relative(root, filePath);
|
|
228
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
229
|
+
}
|
|
230
|
+
function readImageMetadata(filePath, data) {
|
|
231
|
+
if (isPng(data)) {
|
|
232
|
+
return {
|
|
233
|
+
format: 'png',
|
|
234
|
+
width: data.readUInt32BE(16),
|
|
235
|
+
height: data.readUInt32BE(20),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (isJpeg(data)) {
|
|
239
|
+
return readJpegMetadata(filePath, data);
|
|
240
|
+
}
|
|
241
|
+
if (isWebp(data)) {
|
|
242
|
+
return readWebpMetadata(filePath, data);
|
|
243
|
+
}
|
|
244
|
+
if (filePath.toLowerCase().endsWith('.svg')) {
|
|
245
|
+
return readSvgMetadata(filePath, data.toString('utf8'));
|
|
246
|
+
}
|
|
247
|
+
throw new Error(`vobs image metadata could not read dimensions for ${filePath}`);
|
|
248
|
+
}
|
|
249
|
+
function isPng(data) {
|
|
250
|
+
return (data.length >= 24 &&
|
|
251
|
+
data[0] === 0x89 &&
|
|
252
|
+
data[1] === 0x50 &&
|
|
253
|
+
data[2] === 0x4e &&
|
|
254
|
+
data[3] === 0x47 &&
|
|
255
|
+
data.toString('ascii', 12, 16) === 'IHDR');
|
|
256
|
+
}
|
|
257
|
+
function isJpeg(data) {
|
|
258
|
+
return data.length > 4 && data[0] === 0xff && data[1] === 0xd8;
|
|
259
|
+
}
|
|
260
|
+
function readJpegMetadata(filePath, data) {
|
|
261
|
+
let offset = 2;
|
|
262
|
+
while (offset + 8 < data.length) {
|
|
263
|
+
if (data[offset] !== 0xff)
|
|
264
|
+
break;
|
|
265
|
+
const marker = data[offset + 1];
|
|
266
|
+
offset += 2;
|
|
267
|
+
if (marker === undefined || marker === 0xd9 || marker === 0xda)
|
|
268
|
+
break;
|
|
269
|
+
const length = data.readUInt16BE(offset);
|
|
270
|
+
if (length < 2 || offset + length > data.length)
|
|
271
|
+
break;
|
|
272
|
+
if (isJpegStartOfFrame(marker)) {
|
|
273
|
+
return {
|
|
274
|
+
format: 'jpeg',
|
|
275
|
+
height: data.readUInt16BE(offset + 3),
|
|
276
|
+
width: data.readUInt16BE(offset + 5),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
offset += length;
|
|
280
|
+
}
|
|
281
|
+
throw new Error(`vobs image metadata could not read JPEG dimensions for ${filePath}`);
|
|
282
|
+
}
|
|
283
|
+
function isJpegStartOfFrame(marker) {
|
|
284
|
+
return ((marker >= 0xc0 && marker <= 0xc3) ||
|
|
285
|
+
(marker >= 0xc5 && marker <= 0xc7) ||
|
|
286
|
+
(marker >= 0xc9 && marker <= 0xcb) ||
|
|
287
|
+
(marker >= 0xcd && marker <= 0xcf));
|
|
288
|
+
}
|
|
289
|
+
function isWebp(data) {
|
|
290
|
+
return (data.length >= 30 &&
|
|
291
|
+
data.toString('ascii', 0, 4) === 'RIFF' &&
|
|
292
|
+
data.toString('ascii', 8, 12) === 'WEBP');
|
|
293
|
+
}
|
|
294
|
+
function readWebpMetadata(filePath, data) {
|
|
295
|
+
const chunk = data.toString('ascii', 12, 16);
|
|
296
|
+
if (chunk === 'VP8X') {
|
|
297
|
+
return {
|
|
298
|
+
format: 'webp',
|
|
299
|
+
width: 1 + readUInt24LE(data, 24),
|
|
300
|
+
height: 1 + readUInt24LE(data, 27),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
if (chunk === 'VP8L' && data[20] === 0x2f) {
|
|
304
|
+
return {
|
|
305
|
+
format: 'webp',
|
|
306
|
+
width: 1 + data[21] + ((data[22] & 0x3f) << 8),
|
|
307
|
+
height: 1 + ((data[22] & 0xc0) >> 6) + (data[23] << 2) + ((data[24] & 0x0f) << 10),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
if (chunk === 'VP8 ' && data.toString('hex', 23, 26) === '9d012a') {
|
|
311
|
+
return {
|
|
312
|
+
format: 'webp',
|
|
313
|
+
width: data.readUInt16LE(26) & 0x3fff,
|
|
314
|
+
height: data.readUInt16LE(28) & 0x3fff,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
throw new Error(`vobs image metadata could not read WebP dimensions for ${filePath}`);
|
|
318
|
+
}
|
|
319
|
+
function readUInt24LE(data, offset) {
|
|
320
|
+
return data[offset] + (data[offset + 1] << 8) + (data[offset + 2] << 16);
|
|
321
|
+
}
|
|
322
|
+
function readSvgMetadata(filePath, source) {
|
|
323
|
+
const width = readSvgNumericAttribute(source, 'width');
|
|
324
|
+
const height = readSvgNumericAttribute(source, 'height');
|
|
325
|
+
if (width !== undefined && height !== undefined) {
|
|
326
|
+
return { format: 'svg', width, height };
|
|
327
|
+
}
|
|
328
|
+
const viewBox = /\bviewBox\s*=\s*["']\s*[-\d.]+\s+[-\d.]+\s+([-\d.]+)\s+([-\d.]+)/u.exec(source);
|
|
329
|
+
if (viewBox?.[1] !== undefined && viewBox[2] !== undefined) {
|
|
330
|
+
return {
|
|
331
|
+
format: 'svg',
|
|
332
|
+
width: Number(viewBox[1]),
|
|
333
|
+
height: Number(viewBox[2]),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
throw new Error(`vobs image metadata could not read SVG dimensions for ${filePath}`);
|
|
337
|
+
}
|
|
338
|
+
function readSvgNumericAttribute(source, name) {
|
|
339
|
+
const match = new RegExp(`\\b${name}\\s*=\\s*["']([\\d.]+)(?:px)?["']`, 'u').exec(source);
|
|
340
|
+
return match?.[1] === undefined ? undefined : Number(match[1]);
|
|
341
|
+
}
|
|
342
|
+
function emitImageMetadataModule(root, filePath, metadata) {
|
|
343
|
+
const source = `/${normalizeSlash(path.relative(root, filePath))}`;
|
|
344
|
+
return `import src from ${JSON.stringify(source)};\nexport default {src,width:${metadata.width},height:${metadata.height},format:${JSON.stringify(metadata.format)}};`;
|
|
345
|
+
}
|
|
346
|
+
function readFileIfExists(fileName) {
|
|
347
|
+
try {
|
|
348
|
+
return readFileSync(fileName, 'utf8');
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function createCheckOptions(root, options, pluginOptions) {
|
|
355
|
+
const globalComponentTags = iconComponentTags(pluginOptions);
|
|
356
|
+
if (options === undefined || typeof options === 'boolean') {
|
|
357
|
+
return {
|
|
358
|
+
tsconfigPath: path.join(root, 'tsconfig.json'),
|
|
359
|
+
...(globalComponentTags.length === 0 ? {} : { globalComponentTags }),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
...options,
|
|
364
|
+
tsconfigPath: options.tsconfigPath === undefined
|
|
365
|
+
? path.join(root, 'tsconfig.json')
|
|
366
|
+
: path.resolve(root, options.tsconfigPath),
|
|
367
|
+
...(globalComponentTags.length === 0 ? {} : { globalComponentTags }),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function iconComponentTags(options) {
|
|
371
|
+
const tags = ['k-app-shell'];
|
|
372
|
+
if (iconSupportEnabled(options))
|
|
373
|
+
tags.push('k-icon');
|
|
374
|
+
return tags;
|
|
375
|
+
}
|
|
376
|
+
function iconSupportEnabled(options) {
|
|
377
|
+
return options?.icons !== undefined && options.icons !== false;
|
|
378
|
+
}
|
|
379
|
+
function emitIconsModule(root, options, addWatchFile) {
|
|
380
|
+
const config = readIconConfig(root, options);
|
|
381
|
+
addWatchFile(config.filePath);
|
|
382
|
+
const templateIcons = collectTemplateIconNames(root, options, config, addWatchFile);
|
|
383
|
+
const iconNames = Array.from(new Set([...config.shellIcons, ...templateIcons])).sort();
|
|
384
|
+
const imports = [
|
|
385
|
+
"import { createIconRegistry } from '@vobs/icons';",
|
|
386
|
+
"import { defineIconFromSvg } from '@vobs/icons/svg';",
|
|
387
|
+
];
|
|
388
|
+
const entries = [];
|
|
389
|
+
iconNames.forEach((iconName, index) => {
|
|
390
|
+
const source = resolveConfiguredIcon(root, config, iconName);
|
|
391
|
+
addWatchFile(source.filePath);
|
|
392
|
+
const importName = `__vobsIcon${index}`;
|
|
393
|
+
imports.push(`import ${importName} from ${JSON.stringify(source.importRequest)};`);
|
|
394
|
+
entries.push(`${JSON.stringify(iconName)}: defineIconFromSvg(${JSON.stringify(iconName)}, ${importName})`);
|
|
395
|
+
});
|
|
396
|
+
return `${imports.join('\n')}\n\nconst definitions = {\n ${entries.join(',\n ')}\n};\n\nexport const icons = createIconRegistry(definitions);\nexport default icons;`;
|
|
397
|
+
}
|
|
398
|
+
function validateIconProject(root, options, addWatchFile) {
|
|
399
|
+
if (!iconSupportEnabled(options))
|
|
400
|
+
return;
|
|
401
|
+
const config = readIconConfig(root, options);
|
|
402
|
+
addWatchFile(config.filePath);
|
|
403
|
+
const templateIcons = collectTemplateIconNames(root, options, config, addWatchFile);
|
|
404
|
+
const iconNames = Array.from(new Set([...config.shellIcons, ...templateIcons]));
|
|
405
|
+
for (const iconName of iconNames) {
|
|
406
|
+
const source = resolveConfiguredIcon(root, config, iconName);
|
|
407
|
+
addWatchFile(source.filePath);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function readIconConfig(root, options) {
|
|
411
|
+
const filePath = iconConfigPath(root, options);
|
|
412
|
+
const source = readFileIfExists(filePath);
|
|
413
|
+
if (source === undefined) {
|
|
414
|
+
throw new Error(`vobs icon config not found: ${filePath}`);
|
|
415
|
+
}
|
|
416
|
+
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true);
|
|
417
|
+
const configObject = findDefaultIconConfigObject(sourceFile);
|
|
418
|
+
if (configObject === undefined) {
|
|
419
|
+
throw new Error(`vobs icon config must export default defineIconConfig({...}): ${filePath}`);
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
filePath,
|
|
423
|
+
sources: readIconSources(configObject, filePath),
|
|
424
|
+
...readShellIconManifest(configObject, filePath),
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function iconConfigPath(root, options) {
|
|
428
|
+
const configured = typeof options.icons === 'object' ? options.icons.config : undefined;
|
|
429
|
+
return path.resolve(root, configured ?? 'vobs.icons.ts');
|
|
430
|
+
}
|
|
431
|
+
function findDefaultIconConfigObject(sourceFile) {
|
|
432
|
+
for (const statement of sourceFile.statements) {
|
|
433
|
+
if (!ts.isExportAssignment(statement))
|
|
434
|
+
continue;
|
|
435
|
+
const expression = unwrapExpression(statement.expression);
|
|
436
|
+
if (ts.isCallExpression(expression) && expression.arguments.length === 1) {
|
|
437
|
+
const argument = expression.arguments[0];
|
|
438
|
+
if (argument !== undefined && ts.isObjectLiteralExpression(argument))
|
|
439
|
+
return argument;
|
|
440
|
+
}
|
|
441
|
+
if (ts.isObjectLiteralExpression(expression))
|
|
442
|
+
return expression;
|
|
443
|
+
}
|
|
444
|
+
return undefined;
|
|
445
|
+
}
|
|
446
|
+
function readIconSources(configObject, filePath) {
|
|
447
|
+
const sources = readObjectProperty(configObject, 'sources');
|
|
448
|
+
if (sources === undefined || !ts.isObjectLiteralExpression(sources)) {
|
|
449
|
+
throw new Error(`vobs icon config requires a static sources object: ${filePath}`);
|
|
450
|
+
}
|
|
451
|
+
const result = {};
|
|
452
|
+
for (const property of sources.properties) {
|
|
453
|
+
if (!ts.isPropertyAssignment(property))
|
|
454
|
+
continue;
|
|
455
|
+
const sourceName = readPropertyName(property.name);
|
|
456
|
+
if (sourceName === undefined)
|
|
457
|
+
continue;
|
|
458
|
+
result[sourceName] = readSvgIconSource(property.initializer, filePath, sourceName);
|
|
459
|
+
}
|
|
460
|
+
return result;
|
|
461
|
+
}
|
|
462
|
+
function readSvgIconSource(expression, filePath, sourceName) {
|
|
463
|
+
const call = unwrapExpression(expression);
|
|
464
|
+
if (!ts.isCallExpression(call)) {
|
|
465
|
+
throw new Error(`Icon source "${sourceName}" must use svgIconSource(...): ${filePath}`);
|
|
466
|
+
}
|
|
467
|
+
const packageName = readStringLiteral(call.arguments[0]);
|
|
468
|
+
if (packageName === undefined) {
|
|
469
|
+
throw new Error(`Icon source "${sourceName}" requires a static package name: ${filePath}`);
|
|
470
|
+
}
|
|
471
|
+
const options = call.arguments[1];
|
|
472
|
+
return {
|
|
473
|
+
packageName,
|
|
474
|
+
...(options === undefined ? {} : readSvgIconSourceOptions(options, filePath, sourceName)),
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function readSvgIconSourceOptions(expression, filePath, sourceName) {
|
|
478
|
+
const options = unwrapExpression(expression);
|
|
479
|
+
if (!ts.isObjectLiteralExpression(options)) {
|
|
480
|
+
throw new Error(`Icon source "${sourceName}" options must be a static object: ${filePath}`);
|
|
481
|
+
}
|
|
482
|
+
const size = readOptionalNumberProperty(options, 'size', filePath, sourceName);
|
|
483
|
+
const variant = readOptionalStringProperty(options, 'variant', filePath, sourceName);
|
|
484
|
+
const customPath = readOptionalStringProperty(options, 'path', filePath, sourceName);
|
|
485
|
+
return {
|
|
486
|
+
...(size === undefined ? {} : { size }),
|
|
487
|
+
...(variant === undefined ? {} : { variant }),
|
|
488
|
+
...(customPath === undefined ? {} : { path: customPath }),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
function readShellIconManifest(configObject, filePath) {
|
|
492
|
+
const shell = readObjectProperty(configObject, 'shell');
|
|
493
|
+
if (shell === undefined)
|
|
494
|
+
return { shellIcons: [], shellGroups: new Set() };
|
|
495
|
+
if (!ts.isObjectLiteralExpression(shell)) {
|
|
496
|
+
throw new Error(`vobs icon shell manifest must be a static object: ${filePath}`);
|
|
497
|
+
}
|
|
498
|
+
const icons = [];
|
|
499
|
+
const groups = new Set();
|
|
500
|
+
for (const property of shell.properties) {
|
|
501
|
+
if (!ts.isPropertyAssignment(property))
|
|
502
|
+
continue;
|
|
503
|
+
const propertyName = readPropertyName(property.name);
|
|
504
|
+
if (propertyName === undefined)
|
|
505
|
+
continue;
|
|
506
|
+
groups.add(`shell.${propertyName}`);
|
|
507
|
+
collectStringArrayValues(property.initializer, icons, filePath);
|
|
508
|
+
}
|
|
509
|
+
return { shellIcons: icons, shellGroups: groups };
|
|
510
|
+
}
|
|
511
|
+
function collectTemplateIconNames(root, options, config, addWatchFile) {
|
|
512
|
+
const pagesRoot = path.resolve(root, typeof options.routes === 'object' && options.routes.pagesDir !== undefined
|
|
513
|
+
? options.routes.pagesDir
|
|
514
|
+
: 'src/pages');
|
|
515
|
+
const htmlFiles = walkHtmlFiles(pagesRoot);
|
|
516
|
+
const icons = [];
|
|
517
|
+
for (const file of htmlFiles) {
|
|
518
|
+
addWatchFile(file);
|
|
519
|
+
const source = readFileSync(file, 'utf8');
|
|
520
|
+
for (const tag of source.matchAll(/<k-icon\b[^>]*>/giu)) {
|
|
521
|
+
const tagSource = tag[0];
|
|
522
|
+
diagnoseDynamicIconName(file, tagSource, config);
|
|
523
|
+
const nameMatch = /(?:^|\s)name=(["'])([^"']+)\1/iu.exec(tagSource);
|
|
524
|
+
const name = nameMatch?.[2];
|
|
525
|
+
if (name !== undefined)
|
|
526
|
+
icons.push(name);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return icons;
|
|
530
|
+
}
|
|
531
|
+
function diagnoseDynamicIconName(filePath, tagSource, config) {
|
|
532
|
+
if (!/(?:^|\s):name=(["'])([^"']+)\1/iu.test(tagSource))
|
|
533
|
+
return;
|
|
534
|
+
const manifest = /(?:^|\s)data-icon-manifest=(["'])([^"']+)\1/iu.exec(tagSource)?.[2];
|
|
535
|
+
if (manifest === undefined) {
|
|
536
|
+
throw new Error(`Dynamic k-icon names are not supported. Use static name="source:name" or a declared shell manifest binding: ${filePath}`);
|
|
537
|
+
}
|
|
538
|
+
if (!config.shellGroups.has(manifest)) {
|
|
539
|
+
throw new Error(`Unknown k-icon shell manifest binding "${manifest}": ${filePath}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function walkHtmlFiles(directory) {
|
|
543
|
+
let entries;
|
|
544
|
+
try {
|
|
545
|
+
entries = readdirSyncCompat(directory);
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
return [];
|
|
549
|
+
}
|
|
550
|
+
const files = [];
|
|
551
|
+
for (const entry of entries) {
|
|
552
|
+
const filePath = path.join(directory, entry.name);
|
|
553
|
+
if (entry.isDirectory()) {
|
|
554
|
+
files.push(...walkHtmlFiles(filePath));
|
|
555
|
+
}
|
|
556
|
+
else if (entry.isFile() && entry.name.endsWith('.html')) {
|
|
557
|
+
files.push(filePath);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return files;
|
|
561
|
+
}
|
|
562
|
+
function readdirSyncCompat(directory) {
|
|
563
|
+
return readdirSync(directory, { withFileTypes: true });
|
|
564
|
+
}
|
|
565
|
+
function resolveConfiguredIcon(root, config, iconName) {
|
|
566
|
+
const [sourceName, localName] = splitIconName(iconName);
|
|
567
|
+
const source = config.sources[sourceName];
|
|
568
|
+
if (source === undefined) {
|
|
569
|
+
throw new Error(`Icon source is not configured: ${sourceName} (${iconName})`);
|
|
570
|
+
}
|
|
571
|
+
const directory = resolveIconSourceDirectory(root, source);
|
|
572
|
+
const filePath = path.join(directory, `${localName}.svg`);
|
|
573
|
+
if (readFileIfExists(filePath) === undefined) {
|
|
574
|
+
throw new Error(`Icon SVG not found: ${iconName} -> ${filePath}`);
|
|
575
|
+
}
|
|
576
|
+
return {
|
|
577
|
+
filePath,
|
|
578
|
+
importRequest: iconImportRequest(root, source, localName),
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
function splitIconName(iconName) {
|
|
582
|
+
const match = /^([a-z][a-z0-9-]*):([a-z][a-z0-9-]*)$/u.exec(iconName);
|
|
583
|
+
if (match?.[1] === undefined || match[2] === undefined) {
|
|
584
|
+
throw new Error(`Icon name must use source:name format: ${iconName}`);
|
|
585
|
+
}
|
|
586
|
+
return [match[1], match[2]];
|
|
587
|
+
}
|
|
588
|
+
function resolveIconSourceDirectory(root, source) {
|
|
589
|
+
const base = source.packageName.startsWith('.')
|
|
590
|
+
? path.resolve(root, source.packageName)
|
|
591
|
+
: resolvePackageRoot(root, source.packageName);
|
|
592
|
+
return path.resolve(base, source.path ?? knownIconSourcePath(source));
|
|
593
|
+
}
|
|
594
|
+
function iconImportRequest(root, source, localName) {
|
|
595
|
+
const sourcePath = source.path ?? knownIconSourcePath(source);
|
|
596
|
+
const relativeIconPath = normalizeSlash(path.join(sourcePath, `${localName}.svg`));
|
|
597
|
+
if (!source.packageName.startsWith('.'))
|
|
598
|
+
return `${source.packageName}/${relativeIconPath}?raw`;
|
|
599
|
+
const filePath = path.resolve(root, source.packageName, relativeIconPath);
|
|
600
|
+
return `/${normalizeSlash(path.relative(root, filePath))}?raw`;
|
|
601
|
+
}
|
|
602
|
+
function resolvePackageRoot(root, packageName) {
|
|
603
|
+
const projectRequire = createRequire(path.join(root, 'package.json'));
|
|
604
|
+
return path.dirname(projectRequire.resolve(`${packageName}/package.json`));
|
|
605
|
+
}
|
|
606
|
+
function knownIconSourcePath(source) {
|
|
607
|
+
switch (source.packageName) {
|
|
608
|
+
case 'heroicons':
|
|
609
|
+
return `${source.size ?? 24}/${source.variant ?? 'outline'}`;
|
|
610
|
+
case 'lucide-static':
|
|
611
|
+
case 'bootstrap-icons':
|
|
612
|
+
case 'simple-icons':
|
|
613
|
+
return 'icons';
|
|
614
|
+
case '@tabler/icons':
|
|
615
|
+
return `icons/${source.variant ?? 'outline'}`;
|
|
616
|
+
case '@mdi/svg':
|
|
617
|
+
return 'svg';
|
|
618
|
+
case '@carbon/icons':
|
|
619
|
+
return `svg/${source.size ?? 24}`;
|
|
620
|
+
default:
|
|
621
|
+
return source.path ?? '';
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function collectStringArrayValues(expression, values, filePath) {
|
|
625
|
+
const array = unwrapExpression(expression);
|
|
626
|
+
if (!ts.isArrayLiteralExpression(array)) {
|
|
627
|
+
throw new Error(`vobs icon manifest entries must be static string arrays: ${filePath}`);
|
|
628
|
+
}
|
|
629
|
+
for (const element of array.elements) {
|
|
630
|
+
const value = readStringLiteral(element);
|
|
631
|
+
if (value === undefined) {
|
|
632
|
+
throw new Error(`vobs icon manifest entries must be static strings: ${filePath}`);
|
|
633
|
+
}
|
|
634
|
+
values.push(value);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function readObjectProperty(objectLiteral, name) {
|
|
638
|
+
for (const property of objectLiteral.properties) {
|
|
639
|
+
if (!ts.isPropertyAssignment(property))
|
|
640
|
+
continue;
|
|
641
|
+
if (readPropertyName(property.name) === name)
|
|
642
|
+
return property.initializer;
|
|
643
|
+
}
|
|
644
|
+
return undefined;
|
|
645
|
+
}
|
|
646
|
+
function readPropertyName(name) {
|
|
647
|
+
return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined;
|
|
648
|
+
}
|
|
649
|
+
function readStringLiteral(expression) {
|
|
650
|
+
const unwrapped = expression === undefined ? undefined : unwrapExpression(expression);
|
|
651
|
+
return unwrapped !== undefined && ts.isStringLiteralLike(unwrapped) ? unwrapped.text : undefined;
|
|
652
|
+
}
|
|
653
|
+
function readOptionalStringProperty(objectLiteral, name, filePath, sourceName) {
|
|
654
|
+
const property = readObjectProperty(objectLiteral, name);
|
|
655
|
+
if (property === undefined)
|
|
656
|
+
return undefined;
|
|
657
|
+
const value = readStringLiteral(property);
|
|
658
|
+
if (value === undefined) {
|
|
659
|
+
throw new Error(`Icon source "${sourceName}" option "${name}" must be a string: ${filePath}`);
|
|
660
|
+
}
|
|
661
|
+
return value;
|
|
662
|
+
}
|
|
663
|
+
function readOptionalNumberProperty(objectLiteral, name, filePath, sourceName) {
|
|
664
|
+
const property = readObjectProperty(objectLiteral, name);
|
|
665
|
+
if (property === undefined)
|
|
666
|
+
return undefined;
|
|
667
|
+
const value = unwrapExpression(property);
|
|
668
|
+
if (!ts.isNumericLiteral(value)) {
|
|
669
|
+
throw new Error(`Icon source "${sourceName}" option "${name}" must be a number: ${filePath}`);
|
|
670
|
+
}
|
|
671
|
+
return Number(value.text);
|
|
672
|
+
}
|
|
673
|
+
function unwrapExpression(expression) {
|
|
674
|
+
let current = ts.skipPartiallyEmittedExpressions(expression);
|
|
675
|
+
while (ts.isParenthesizedExpression(current) ||
|
|
676
|
+
ts.isAsExpression(current) ||
|
|
677
|
+
ts.isTypeAssertionExpression(current) ||
|
|
678
|
+
ts.isSatisfiesExpression(current) ||
|
|
679
|
+
ts.isNonNullExpression(current)) {
|
|
680
|
+
current = ts.skipPartiallyEmittedExpressions(current.expression);
|
|
681
|
+
}
|
|
682
|
+
return current;
|
|
683
|
+
}
|
|
684
|
+
function formatDiagnostics(diagnostics) {
|
|
685
|
+
return diagnostics.map(formatDiagnostic).join('\n');
|
|
686
|
+
}
|
|
687
|
+
function formatDiagnostic(diagnostic) {
|
|
688
|
+
const source = diagnostic.sourceId ?? 'project';
|
|
689
|
+
const position = diagnostic.start === undefined ? '' : `:${diagnostic.start.line}:${diagnostic.start.column}`;
|
|
690
|
+
return `${source}${position} ${diagnostic.severity.toUpperCase()} ${diagnostic.code}: ${diagnostic.message}`;
|
|
691
|
+
}
|
|
692
|
+
async function collectFileRoutes(root, options) {
|
|
693
|
+
const pagesRoot = path.resolve(root, typeof options === 'object' && options.pagesDir !== undefined ? options.pagesDir : 'src/pages');
|
|
694
|
+
const [routes, notFound, error] = await Promise.all([
|
|
695
|
+
walkPages(pagesRoot, pagesRoot).catch((error) => {
|
|
696
|
+
if (isFileNotFound(error))
|
|
697
|
+
return [];
|
|
698
|
+
throw error;
|
|
699
|
+
}),
|
|
700
|
+
findNotFoundRoute(pagesRoot),
|
|
701
|
+
findSpecialPage(pagesRoot, '_error.ts'),
|
|
702
|
+
]);
|
|
703
|
+
validateFileRouteConflicts(routes, pagesRoot);
|
|
704
|
+
return {
|
|
705
|
+
routes: routes.sort(compareFileRoutes),
|
|
706
|
+
...(notFound === undefined ? {} : { notFound }),
|
|
707
|
+
...(error === undefined ? {} : { error }),
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
async function findNotFoundRoute(pagesRoot) {
|
|
711
|
+
const filePath = await findSpecialPage(pagesRoot, '_404.ts');
|
|
712
|
+
if (filePath === undefined)
|
|
713
|
+
return undefined;
|
|
714
|
+
return {
|
|
715
|
+
path: '*',
|
|
716
|
+
filePath,
|
|
717
|
+
layouts: [],
|
|
718
|
+
g: false,
|
|
719
|
+
l: false,
|
|
720
|
+
score: [2],
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
async function walkPages(directory, pagesRoot, parentLayouts = [], parentError) {
|
|
724
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
725
|
+
const routes = [];
|
|
726
|
+
const layouts = await layoutChainForDirectory(directory, parentLayouts);
|
|
727
|
+
const error = (await findSpecialPage(directory, '_error.ts')) ?? parentError;
|
|
728
|
+
for (const entry of entries) {
|
|
729
|
+
const entryPath = path.join(directory, entry.name);
|
|
730
|
+
if (entry.isDirectory()) {
|
|
731
|
+
routes.push(...(await walkPages(entryPath, pagesRoot, layouts, error)));
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
if (!isPageFile(entry.name))
|
|
735
|
+
continue;
|
|
736
|
+
const routePath = routePathForFile(entryPath, pagesRoot);
|
|
737
|
+
if (routePath === undefined)
|
|
738
|
+
continue;
|
|
739
|
+
const source = await readFile(entryPath, 'utf8');
|
|
740
|
+
const meta = readStaticRouteMeta(source);
|
|
741
|
+
routes.push({
|
|
742
|
+
path: routePath,
|
|
743
|
+
filePath: entryPath,
|
|
744
|
+
layouts,
|
|
745
|
+
...(error === undefined ? {} : { error }),
|
|
746
|
+
...(meta === undefined ? {} : { meta }),
|
|
747
|
+
g: hasExportedRouteMember(source, 'guard'),
|
|
748
|
+
l: hasExportedRouteMember(source, 'loader'),
|
|
749
|
+
score: scoreRoutePath(routePath),
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
return routes;
|
|
753
|
+
}
|
|
754
|
+
async function findSpecialPage(directory, fileName) {
|
|
755
|
+
const filePath = path.join(directory, fileName);
|
|
756
|
+
try {
|
|
757
|
+
await access(filePath);
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
if (isFileNotFound(error))
|
|
761
|
+
return undefined;
|
|
762
|
+
throw error;
|
|
763
|
+
}
|
|
764
|
+
return filePath;
|
|
765
|
+
}
|
|
766
|
+
function hasExportedRouteMember(source, name) {
|
|
767
|
+
const directExportPattern = new RegExp(`^\\s*export\\s+(?:(?:async\\s+)?function|const|let|var)\\s+${name}\\b`, 'mu');
|
|
768
|
+
if (directExportPattern.test(source)) {
|
|
769
|
+
return true;
|
|
770
|
+
}
|
|
771
|
+
for (const match of source.matchAll(/^\s*export\s*\{([^}]*)\}/gmu)) {
|
|
772
|
+
const specifiers = match[1]?.split(',') ?? [];
|
|
773
|
+
if (specifiers.some((specifier) => isRouteMemberExportSpecifier(specifier, name)))
|
|
774
|
+
return true;
|
|
775
|
+
}
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
function readStaticRouteMeta(source) {
|
|
779
|
+
const match = /^\s*export\s+(?:const|let|var)\s+meta\s*=\s*/mu.exec(source);
|
|
780
|
+
if (match === null)
|
|
781
|
+
return undefined;
|
|
782
|
+
const objectStart = source.indexOf('{', match.index + match[0].length);
|
|
783
|
+
if (objectStart < 0) {
|
|
784
|
+
throw new Error('vobs file route meta must be a static object literal');
|
|
785
|
+
}
|
|
786
|
+
const objectEnd = findMatchingBrace(source, objectStart);
|
|
787
|
+
if (objectEnd < 0) {
|
|
788
|
+
throw new Error('vobs file route meta must be a static object literal');
|
|
789
|
+
}
|
|
790
|
+
return source.slice(objectStart, objectEnd + 1);
|
|
791
|
+
}
|
|
792
|
+
function findMatchingBrace(source, openIndex) {
|
|
793
|
+
let depth = 0;
|
|
794
|
+
let state = 'code';
|
|
795
|
+
for (let index = openIndex; index < source.length; index += 1) {
|
|
796
|
+
const character = source[index];
|
|
797
|
+
const next = source[index + 1];
|
|
798
|
+
if (state === 'line-comment') {
|
|
799
|
+
if (character === '\n')
|
|
800
|
+
state = 'code';
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
if (state === 'block-comment') {
|
|
804
|
+
if (character === '*' && next === '/') {
|
|
805
|
+
state = 'code';
|
|
806
|
+
index += 1;
|
|
807
|
+
}
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
if (state === 'single') {
|
|
811
|
+
if (character === '\\') {
|
|
812
|
+
index += 1;
|
|
813
|
+
}
|
|
814
|
+
else if (character === "'") {
|
|
815
|
+
state = 'code';
|
|
816
|
+
}
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
if (state === 'double') {
|
|
820
|
+
if (character === '\\') {
|
|
821
|
+
index += 1;
|
|
822
|
+
}
|
|
823
|
+
else if (character === '"') {
|
|
824
|
+
state = 'code';
|
|
825
|
+
}
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
if (state === 'template') {
|
|
829
|
+
if (character === '\\') {
|
|
830
|
+
index += 1;
|
|
831
|
+
}
|
|
832
|
+
else if (character === '`') {
|
|
833
|
+
state = 'code';
|
|
834
|
+
}
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
if (character === '/' && next === '/') {
|
|
838
|
+
state = 'line-comment';
|
|
839
|
+
index += 1;
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
if (character === '/' && next === '*') {
|
|
843
|
+
state = 'block-comment';
|
|
844
|
+
index += 1;
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
if (character === "'") {
|
|
848
|
+
state = 'single';
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
if (character === '"') {
|
|
852
|
+
state = 'double';
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
if (character === '`') {
|
|
856
|
+
state = 'template';
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
if (character === '{') {
|
|
860
|
+
depth += 1;
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (character === '}') {
|
|
864
|
+
depth -= 1;
|
|
865
|
+
if (depth === 0)
|
|
866
|
+
return index;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return -1;
|
|
870
|
+
}
|
|
871
|
+
function isRouteMemberExportSpecifier(source, name) {
|
|
872
|
+
const specifier = source.trim().replace(/^type\s+/u, '');
|
|
873
|
+
if (specifier === name)
|
|
874
|
+
return true;
|
|
875
|
+
const [, exported] = /^[$A-Z_a-z][$\w]*\s+as\s+([$A-Z_a-z][$\w]*)$/u.exec(specifier) ?? [];
|
|
876
|
+
return exported === name;
|
|
877
|
+
}
|
|
878
|
+
async function layoutChainForDirectory(directory, parentLayouts) {
|
|
879
|
+
const layoutPath = path.join(directory, '_layout.ts');
|
|
880
|
+
try {
|
|
881
|
+
await access(layoutPath);
|
|
882
|
+
}
|
|
883
|
+
catch (error) {
|
|
884
|
+
if (isFileNotFound(error))
|
|
885
|
+
return parentLayouts;
|
|
886
|
+
throw error;
|
|
887
|
+
}
|
|
888
|
+
return [...parentLayouts, layoutPath];
|
|
889
|
+
}
|
|
890
|
+
function isPageFile(fileName) {
|
|
891
|
+
return fileName.endsWith('.ts') && !fileName.endsWith('.d.ts') && !fileName.startsWith('_');
|
|
892
|
+
}
|
|
893
|
+
function routePathForFile(filePath, pagesRoot) {
|
|
894
|
+
const relative = normalizeSlash(path.relative(pagesRoot, filePath));
|
|
895
|
+
const withoutExtension = relative.slice(0, -'.ts'.length);
|
|
896
|
+
const rawSegments = withoutExtension.split('/');
|
|
897
|
+
const segments = rawSegments.flatMap((segment) => {
|
|
898
|
+
if (segment === 'index')
|
|
899
|
+
return [];
|
|
900
|
+
if (segment.startsWith('(') && segment.endsWith(')'))
|
|
901
|
+
return [];
|
|
902
|
+
if (segment.startsWith('[...') && segment.endsWith(']'))
|
|
903
|
+
return [`*${segment.slice(4, -1)}`];
|
|
904
|
+
if (segment.startsWith('[') && segment.endsWith(']'))
|
|
905
|
+
return [`:${segment.slice(1, -1)}`];
|
|
906
|
+
return [segment];
|
|
907
|
+
});
|
|
908
|
+
if (segments.some((segment) => segment === ':' || segment === '*'))
|
|
909
|
+
return undefined;
|
|
910
|
+
return segments.length === 0 ? '/' : `/${segments.join('/')}`;
|
|
911
|
+
}
|
|
912
|
+
function scoreRoutePath(routePath) {
|
|
913
|
+
if (routePath === '/')
|
|
914
|
+
return [0];
|
|
915
|
+
return routePath.split('/').slice(1).map(scoreRouteSegment);
|
|
916
|
+
}
|
|
917
|
+
function scoreRouteSegment(segment) {
|
|
918
|
+
if (segment.startsWith('*'))
|
|
919
|
+
return 2;
|
|
920
|
+
if (segment.startsWith(':'))
|
|
921
|
+
return 1;
|
|
922
|
+
return 0;
|
|
923
|
+
}
|
|
924
|
+
function compareFileRoutes(left, right) {
|
|
925
|
+
const length = Math.max(left.score.length, right.score.length);
|
|
926
|
+
for (let index = 0; index < length; index += 1) {
|
|
927
|
+
const scoreDelta = (left.score[index] ?? 0) - (right.score[index] ?? 0);
|
|
928
|
+
if (scoreDelta !== 0)
|
|
929
|
+
return scoreDelta;
|
|
930
|
+
}
|
|
931
|
+
if (left.score.length !== right.score.length)
|
|
932
|
+
return left.score.length - right.score.length;
|
|
933
|
+
return left.path.localeCompare(right.path);
|
|
934
|
+
}
|
|
935
|
+
function validateFileRouteConflicts(routes, pagesRoot) {
|
|
936
|
+
const exactPathGroups = groupFileRoutes(routes, (route) => route.path);
|
|
937
|
+
const patternGroups = groupFileRoutes(routes, (route) => routePatternKey(route.path));
|
|
938
|
+
const diagnostics = [];
|
|
939
|
+
for (const [routePath, group] of exactPathGroups) {
|
|
940
|
+
if (group.length < 2)
|
|
941
|
+
continue;
|
|
942
|
+
diagnostics.push(formatFileRouteConflict(`Duplicate file route path "${routePath}"`, group, pagesRoot));
|
|
943
|
+
}
|
|
944
|
+
for (const [pattern, group] of patternGroups) {
|
|
945
|
+
if (group.length < 2 || new Set(group.map((route) => route.path)).size < 2)
|
|
946
|
+
continue;
|
|
947
|
+
diagnostics.push(formatFileRouteConflict(`Ambiguous file route pattern "${pattern}"`, group, pagesRoot));
|
|
948
|
+
}
|
|
949
|
+
if (diagnostics.length === 0)
|
|
950
|
+
return;
|
|
951
|
+
throw new Error(`vobs file route conflict\n${diagnostics.sort().join('\n')}`);
|
|
952
|
+
}
|
|
953
|
+
function groupFileRoutes(routes, readKey) {
|
|
954
|
+
const groups = new Map();
|
|
955
|
+
for (const route of routes) {
|
|
956
|
+
const key = readKey(route);
|
|
957
|
+
const group = groups.get(key);
|
|
958
|
+
if (group === undefined) {
|
|
959
|
+
groups.set(key, [route]);
|
|
960
|
+
}
|
|
961
|
+
else {
|
|
962
|
+
group.push(route);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return groups;
|
|
966
|
+
}
|
|
967
|
+
function routePatternKey(routePath) {
|
|
968
|
+
if (routePath === '/')
|
|
969
|
+
return '/';
|
|
970
|
+
return routePath
|
|
971
|
+
.split('/')
|
|
972
|
+
.map((segment) => {
|
|
973
|
+
if (segment.startsWith(':'))
|
|
974
|
+
return ':';
|
|
975
|
+
if (segment.startsWith('*'))
|
|
976
|
+
return '*';
|
|
977
|
+
return segment;
|
|
978
|
+
})
|
|
979
|
+
.join('/');
|
|
980
|
+
}
|
|
981
|
+
function formatFileRouteConflict(title, routes, pagesRoot) {
|
|
982
|
+
return [
|
|
983
|
+
`- ${title}:`,
|
|
984
|
+
...[...routes]
|
|
985
|
+
.sort((left, right) => relativeFileRoutePath(left, pagesRoot).localeCompare(relativeFileRoutePath(right, pagesRoot)))
|
|
986
|
+
.map((route) => ` - ${route.path} from ${relativeFileRoutePath(route, pagesRoot)}`),
|
|
987
|
+
].join('\n');
|
|
988
|
+
}
|
|
989
|
+
function relativeFileRoutePath(route, pagesRoot) {
|
|
990
|
+
return normalizeSlash(path.relative(pagesRoot, route.filePath));
|
|
991
|
+
}
|
|
992
|
+
function emitRoutesModule(manifest, root) {
|
|
993
|
+
const lines = [];
|
|
994
|
+
const routeTree = createFileRouteTree(manifest.routes);
|
|
995
|
+
const routeNames = new Map();
|
|
996
|
+
manifest.routes.forEach((route, index) => {
|
|
997
|
+
const routeName = `__route${index}`;
|
|
998
|
+
const componentName = `${routeName}Component`;
|
|
999
|
+
routeNames.set(route, routeName);
|
|
1000
|
+
const importPath = toRootImport(route.filePath, root);
|
|
1001
|
+
lines.push(`const ${componentName}=()=>import(${JSON.stringify(importPath)});`);
|
|
1002
|
+
const layouts = route.layouts.length === 0
|
|
1003
|
+
? ''
|
|
1004
|
+
: `,layouts:[${route.layouts
|
|
1005
|
+
.map((layout) => `()=>import(${JSON.stringify(toRootImport(layout, root))})`)
|
|
1006
|
+
.join(',')}]`;
|
|
1007
|
+
const error = route.error === undefined
|
|
1008
|
+
? ''
|
|
1009
|
+
: `,error:()=>import(${JSON.stringify(toRootImport(route.error, root))})`;
|
|
1010
|
+
const meta = route.meta === undefined ? '' : `,meta:${route.meta}`;
|
|
1011
|
+
const guard = route.g ? `,guard:c=>${componentName}().then(m=>m.guard(c))` : '';
|
|
1012
|
+
const loader = route.l ? `,loader:c=>${componentName}().then(m=>m.loader(c))` : '';
|
|
1013
|
+
lines.push(`/** @satisfies {import("@vobs/router").PageRouteRecord<typeof ${componentName}>} */`);
|
|
1014
|
+
lines.push(`const ${routeName}={path:${JSON.stringify(routeTree.emittedPaths.get(route) ?? route.path)},component:${componentName}${layouts}${error}${meta}${guard}${loader}};`);
|
|
1015
|
+
});
|
|
1016
|
+
const rootRouteNames = [];
|
|
1017
|
+
if (routeTree.rootRoute !== undefined) {
|
|
1018
|
+
rootRouteNames.push(routeNames.get(routeTree.rootRoute));
|
|
1019
|
+
}
|
|
1020
|
+
let groupIndex = 0;
|
|
1021
|
+
for (const node of routeTree.nodes) {
|
|
1022
|
+
rootRouteNames.push(emitFileRouteTreeNode(node, true));
|
|
1023
|
+
}
|
|
1024
|
+
lines.push(`export const routes=[${rootRouteNames.join(',')}];`);
|
|
1025
|
+
lines.push(`export const routePaths=[${manifest.routes.map((route) => JSON.stringify(route.path)).join(',')}];`);
|
|
1026
|
+
lines.push('/** @satisfies {import("@vobs/router").RouteComponent | undefined} */');
|
|
1027
|
+
lines.push(manifest.notFound === undefined
|
|
1028
|
+
? 'export const notFound=undefined;'
|
|
1029
|
+
: `export const notFound=()=>import(${JSON.stringify(toRootImport(manifest.notFound.filePath, root))});`);
|
|
1030
|
+
lines.push('/** @satisfies {import("@vobs/router").RouteComponent | undefined} */');
|
|
1031
|
+
lines.push(manifest.error === undefined
|
|
1032
|
+
? 'export const error=undefined;'
|
|
1033
|
+
: `export const error=()=>import(${JSON.stringify(toRootImport(manifest.error, root))});`);
|
|
1034
|
+
lines.push("import { createRouter } from '@vobs/router';");
|
|
1035
|
+
lines.push('/** @satisfies {import("@vobs/router").RouterOptions} */');
|
|
1036
|
+
lines.push('export function createRoutingRouter(options={}){return createRouter({routes,notFound,error,...options});}');
|
|
1037
|
+
lines.push('/** @satisfies {import("@vobs/runtime-core").RouterPort} */');
|
|
1038
|
+
lines.push('const router=createRoutingRouter({history:true});');
|
|
1039
|
+
lines.push('export default router;');
|
|
1040
|
+
return lines.join('\n');
|
|
1041
|
+
function emitFileRouteTreeNode(node, rootNode) {
|
|
1042
|
+
if (node.children.length === 0)
|
|
1043
|
+
return routeNames.get(node.route);
|
|
1044
|
+
const children = [
|
|
1045
|
+
...(node.route === undefined ? [] : [routeNames.get(node.route)]),
|
|
1046
|
+
...node.children.map((child) => emitFileRouteTreeNode(child, false)),
|
|
1047
|
+
];
|
|
1048
|
+
const groupName = `__group${groupIndex++}`;
|
|
1049
|
+
lines.push('/** @satisfies {import("@vobs/router").NestedRouteRecord} */');
|
|
1050
|
+
lines.push(`const ${groupName}={path:${JSON.stringify(rootNode ? `/${node.segment}` : node.segment)},children:[${children.join(',')}]};`);
|
|
1051
|
+
return groupName;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
function createFileRouteTree(routes) {
|
|
1055
|
+
let rootRoute;
|
|
1056
|
+
const nodes = [];
|
|
1057
|
+
const emittedPaths = new Map();
|
|
1058
|
+
for (const route of routes) {
|
|
1059
|
+
if (route.path === '/') {
|
|
1060
|
+
rootRoute = route;
|
|
1061
|
+
emittedPaths.set(route, '/');
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
let siblings = nodes;
|
|
1065
|
+
let node;
|
|
1066
|
+
for (const segment of splitFileRoutePath(route.path)) {
|
|
1067
|
+
node = siblings.find((candidate) => candidate.segment === segment);
|
|
1068
|
+
if (node === undefined) {
|
|
1069
|
+
node = { segment, children: [] };
|
|
1070
|
+
siblings.push(node);
|
|
1071
|
+
}
|
|
1072
|
+
siblings = node.children;
|
|
1073
|
+
}
|
|
1074
|
+
if (node !== undefined)
|
|
1075
|
+
node.route = route;
|
|
1076
|
+
}
|
|
1077
|
+
for (const node of nodes)
|
|
1078
|
+
assignEmittedFileRoutePaths(node, true, emittedPaths);
|
|
1079
|
+
return {
|
|
1080
|
+
...(rootRoute === undefined ? {} : { rootRoute }),
|
|
1081
|
+
nodes,
|
|
1082
|
+
emittedPaths,
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
function assignEmittedFileRoutePaths(node, rootNode, emittedPaths) {
|
|
1086
|
+
if (node.route !== undefined) {
|
|
1087
|
+
emittedPaths.set(node.route, node.children.length === 0 ? (rootNode ? `/${node.segment}` : node.segment) : '');
|
|
1088
|
+
}
|
|
1089
|
+
for (const child of node.children)
|
|
1090
|
+
assignEmittedFileRoutePaths(child, false, emittedPaths);
|
|
1091
|
+
}
|
|
1092
|
+
function splitFileRoutePath(routePath) {
|
|
1093
|
+
return routePath.split('/').filter((segment) => segment !== '');
|
|
1094
|
+
}
|
|
1095
|
+
async function writeRoutesTypesFile(root, options, manifest) {
|
|
1096
|
+
if (typeof options !== 'object' || options.typesFile === undefined)
|
|
1097
|
+
return;
|
|
1098
|
+
const filePath = path.resolve(root, options.typesFile);
|
|
1099
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
1100
|
+
await writeFile(filePath, emitRoutesTypesModule(manifest), 'utf8');
|
|
1101
|
+
}
|
|
1102
|
+
function emitRoutesTypesModule(manifest) {
|
|
1103
|
+
const lines = [
|
|
1104
|
+
'declare module "vobs:routes" {',
|
|
1105
|
+
' export interface GeneratedRoutePathMap {',
|
|
1106
|
+
...manifest.routes.map((route) => ` ${JSON.stringify(route.path)}: true;`),
|
|
1107
|
+
' }',
|
|
1108
|
+
'}',
|
|
1109
|
+
'',
|
|
1110
|
+
];
|
|
1111
|
+
return lines.join('\n');
|
|
1112
|
+
}
|
|
1113
|
+
function toRootImport(filePath, root) {
|
|
1114
|
+
return `/${normalizeSlash(path.relative(root, filePath))}`;
|
|
1115
|
+
}
|
|
1116
|
+
function normalizeSlash(value) {
|
|
1117
|
+
return value.split(path.sep).join('/');
|
|
1118
|
+
}
|
|
1119
|
+
function isViteHtmlEntry(filePath, root) {
|
|
1120
|
+
return (normalizeSlash(path.resolve(filePath)) === normalizeSlash(path.resolve(root, 'index.html')));
|
|
1121
|
+
}
|
|
1122
|
+
function isFileNotFound(error) {
|
|
1123
|
+
return (typeof error === 'object' &&
|
|
1124
|
+
error !== null &&
|
|
1125
|
+
'code' in error &&
|
|
1126
|
+
error.code === 'ENOENT');
|
|
1127
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vobs/vite-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Vite integration, HTML transforms and file routes for vobs.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "vobsjs",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/vobsjs/vobs.git",
|
|
14
|
+
"directory": "plugins/vite"
|
|
15
|
+
},
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/vobsjs/vobs/issues"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/vobsjs/vobs#readme",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@vobs/compiler-dom": "0.1.0",
|
|
22
|
+
"@vobs/runtime-dom": "0.1.0",
|
|
23
|
+
"@vobs/runtime-core": "0.1.0",
|
|
24
|
+
"@vobs/icons": "0.1.0",
|
|
25
|
+
"@vobs/router": "0.1.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"vite": "^7.0.0"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"import": "./dist/index.js"
|
|
40
|
+
},
|
|
41
|
+
"./client": {
|
|
42
|
+
"types": "./dist/client.d.ts",
|
|
43
|
+
"import": "./dist/client.js"
|
|
44
|
+
},
|
|
45
|
+
"./package.json": "./package.json"
|
|
46
|
+
},
|
|
47
|
+
"types": "./dist/index.d.ts",
|
|
48
|
+
"module": "./dist/index.js",
|
|
49
|
+
"main": "./dist/index.js",
|
|
50
|
+
"sideEffects": false,
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=20.19.0"
|
|
53
|
+
}
|
|
54
|
+
}
|