vite-plugin-vanjs 0.0.10 → 0.1.1

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.
@@ -0,0 +1,271 @@
1
+ /** @typedef {import("./types").RouteConfig} RouteConfig */
2
+ /** @typedef {import("./types").PageFile} PageFile */
3
+ /** @typedef {import("./types").LayoutFile} LayoutFile */
4
+ /** @typedef {import("./types").RouteFile} RouteFile */
5
+
6
+ // import { normalizePath } from "vite";
7
+ import { dirname, join, posix, win32 } from "node:path";
8
+ import { existsSync } from "node:fs";
9
+ import { readdir } from "node:fs/promises";
10
+
11
+ /**
12
+ * Get the file most probable route path for a given potential route.
13
+ * @type {typeof import("./types").fileToRoute}
14
+ */
15
+ export const fileToRoute = (file, routesDir) => {
16
+ const cleanPath = file
17
+ .slice(routesDir.length + 1) // also remove initial slash
18
+ .replace(/\.(jsx|tsx|ts|js)$/, "")
19
+ .replace(/index$/, "")
20
+ .replace(/\(.*\)$/, "") // Remove (file_name) from path
21
+ .replace(/\([^)]+\)\/?/g, "") // Remove (folder_name) from path
22
+ .replace(/\[\.\.\.[^\]]+\]/g, "*")
23
+ .replace(/\[([^\]]+)\]/g, ":$1");
24
+ const slashPath = cleanPath.endsWith("/")
25
+ ? cleanPath.slice(0, -1)
26
+ : cleanPath;
27
+ const path = slashPath === "*"
28
+ ? slashPath
29
+ : slashPath?.length > 0
30
+ ? `/${slashPath}`
31
+ : "/";
32
+
33
+ return path;
34
+ };
35
+
36
+ /**
37
+ * Identify all files in a folder.
38
+ * @type {typeof import("./types").globFiles}
39
+ */
40
+ export const globFiles = async (dir, extensions) => {
41
+ /** @type {string[]} */
42
+ const files = [];
43
+
44
+ /** @param {string} directory */
45
+ async function scan(directory) {
46
+ if (!existsSync(directory)) {
47
+ // console.log('🍦 @vanjs/router: the "routes" folder does not exist.');
48
+ return;
49
+ }
50
+ const entries = await readdir(directory, { withFileTypes: true });
51
+ if (!entries.length) {
52
+ // console.warn('🍦 @vanjs/router: the "routes" folder is empty.');
53
+ return;
54
+ }
55
+
56
+ for (const entry of entries) {
57
+ const fullPath = join(directory, entry.name);
58
+
59
+ // istanbul ignore else
60
+ if (entry.isDirectory()) {
61
+ await scan(fullPath);
62
+ } else if (entry.isFile()) {
63
+ // Check if file has allowed extension
64
+ // istanbul ignore else
65
+ if (extensions.some((ext) => entry.name.endsWith(ext))) {
66
+ files.push(fullPath);
67
+ }
68
+ }
69
+ }
70
+ }
71
+
72
+ await scan(dir);
73
+ return files;
74
+ };
75
+
76
+ const normalizePathRegExp = new RegExp(`\\${win32.sep}`, "g");
77
+ /** @param {string} filename */
78
+ function normalizePath(filename) {
79
+ return filename.replace(normalizePathRegExp, posix.sep);
80
+ }
81
+
82
+ /**
83
+ * Scan routes directory and generate routes.
84
+ * @type {typeof import("./types").scanRoutes}
85
+ */
86
+ export const scanRoutes = async (config, pluginConfig) => {
87
+ const { routesDir, extensions } = pluginConfig;
88
+ const routesPath = join(config.root, routesDir);
89
+ const files = await globFiles(routesPath, extensions);
90
+
91
+ if (!files?.length) {
92
+ return [];
93
+ }
94
+
95
+ // Filter out duplicate routes and layout files that are already used
96
+ const routes = files.map((file) => ({
97
+ path: normalizePath(file),
98
+ routePath: fileToRoute(file, routesPath),
99
+ }));
100
+
101
+ // Remove duplicate routes (prefer non-layout files)
102
+ /** @type {typeof routes} */
103
+ const uniqueRoutes = routes.reduce(
104
+ /**
105
+ * @param {typeof routes} acc
106
+ * @param {typeof routes[0]} route
107
+ * @returns {typeof routes}
108
+ */
109
+ (acc, route) => {
110
+ const existing = acc.find((r) => r.routePath === route.routePath);
111
+ if (
112
+ !existing || (existing.path.includes("(") && !route.path.includes("("))
113
+ ) {
114
+ // Remove the existing route if this is a better match
115
+ // istanbul ignore if - should not be possible
116
+ if (existing) {
117
+ acc = acc.filter((r) => r !== existing);
118
+ }
119
+ acc.push(route);
120
+ }
121
+ return acc;
122
+ },
123
+ [],
124
+ );
125
+
126
+ return uniqueRoutes;
127
+ };
128
+
129
+ /**
130
+ * Find all layout files for a given route.
131
+ * @type {typeof import("./types").findLayouts}
132
+ */
133
+ export const findLayouts = (routePath, config, pluginConfig) => {
134
+ const { routesDir, extensions } = pluginConfig;
135
+ const layouts = [];
136
+ let dir = dirname(routePath);
137
+ const routesPath = join(config.root, routesDir);
138
+
139
+ // Walk up the directory tree looking for layout files
140
+ while (dir.startsWith(routesPath) && dir !== routesPath) { // Stop at routes dir
141
+ let layoutFile = null;
142
+ const dirName = dir.split(/[/\\]/).pop();
143
+
144
+ // istanbul ignore else
145
+ if (dirName) {
146
+ // Look for a layout file in the current directory
147
+ for (const ext of extensions) {
148
+ const layoutPaths = [
149
+ join(dirname(dir), `${dirName}${ext}`),
150
+ join(dirname(dir), `(${dirName.replace(/^\((.*)\)$/, "$1")})${ext}`),
151
+ ];
152
+
153
+ for (const path of layoutPaths) {
154
+ if (existsSync(path)) {
155
+ layoutFile = path;
156
+ break;
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ // istanbul ignore else
163
+ if (layoutFile && layoutFile !== routePath) {
164
+ layouts.unshift({
165
+ id: `Layout${layouts.length}`,
166
+ path: layoutFile,
167
+ });
168
+ }
169
+
170
+ dir = dirname(dir);
171
+ }
172
+
173
+ return layouts;
174
+ };
175
+
176
+ /**
177
+ * Process routes and identify their layouts
178
+ * @type {typeof import("./types").processLayoutRoutes}
179
+ */
180
+ export const processLayoutRoutes = (routes, config, pluginConfig) => {
181
+ if (!routes.length) return [];
182
+
183
+ return routes.map((route) => {
184
+ const layouts = findLayouts(route.path, config, pluginConfig);
185
+ return {
186
+ ...route,
187
+ layouts,
188
+ };
189
+ });
190
+ };
191
+
192
+ /**
193
+ * Scan and process routes and return them
194
+ * @type {typeof import("./types").getRoutes}
195
+ */
196
+ // export const scanRoutes = async (config, pluginConfig) => {}
197
+ export const getRoutes = async (config, pluginConfig) => {
198
+ const routes = await scanRoutes(config, pluginConfig);
199
+ return processLayoutRoutes(routes, config, pluginConfig);
200
+ };
201
+
202
+ /** @type {(route: RouteFile) => string} */
203
+ export const generateRouteProloaders = (route) => {
204
+ const moduleName = "PageModule";
205
+ const layoutName = "Module";
206
+
207
+ return `{
208
+ preload: async (params) => {
209
+ ${
210
+ route.layouts.map((layout) =>
211
+ `if (${layout.id + layoutName}?.route?.preload) await ${
212
+ layout.id + layoutName
213
+ }?.route?.preload(params);`
214
+ ).join("\n ")
215
+ }
216
+ if (${moduleName}?.route?.preload) await ${moduleName}?.route?.preload(params);
217
+ },
218
+ load: async (params) => {
219
+ ${
220
+ route.layouts.map((layout) =>
221
+ `if (${layout.id + layoutName}?.route?.load) await ${
222
+ layout.id + layoutName
223
+ }?.route?.load(params);`
224
+ ).join("\n ")
225
+ }
226
+ if (${moduleName}?.route?.load) await ${moduleName}?.route?.load(params);
227
+ }
228
+ }`;
229
+ };
230
+
231
+ /** @type {(route: RouteFile) => string} */
232
+ export const generateComponentRoute = (route) => {
233
+ if (route.layouts?.length > 0) {
234
+ // Only generate imports for unique layouts
235
+ const layoutImports = route.layouts.map(
236
+ (layout) =>
237
+ `const ${layout.id}Module = await import('${layout.path}');\n` +
238
+ `const ${layout.id}Page = ${layout.id}Module.Layout || ${layout.id}Module.Page || ${layout.id}Module.default;`,
239
+ ).join("\n");
240
+
241
+ // Use both shared and unique layouts for the component chain
242
+ const pageComponent = route.layouts.reduce(
243
+ (acc, layout) => `${layout.id}Page({ children: ${acc} })`,
244
+ "Page()",
245
+ );
246
+
247
+ return `lazy(() => {
248
+ const importFn = async () => {
249
+ ${layoutImports}
250
+ const PageModule = await import('${route.path}');
251
+ const Page = PageModule?.Page || PageModule?.default;
252
+
253
+ return Promise.resolve({
254
+ route: ${generateRouteProloaders(route)},
255
+ Page: () => ${pageComponent},
256
+ });
257
+ };
258
+ return importFn();
259
+ })`;
260
+ }
261
+
262
+ return `lazy(() => import('${route.path}'))`;
263
+ };
264
+
265
+ /** @type {(route: RouteFile) => string} */
266
+ export const generateRoute = (route) => {
267
+ return `Route({
268
+ path: "${route.routePath}",
269
+ component: ${generateComponentRoute(route)},
270
+ });`;
271
+ };
@@ -0,0 +1,206 @@
1
+ /** @typedef {import("vite").ResolvedConfig} ResolvedConfig */
2
+ /** @typedef {import("./types").PageFile} PageFile */
3
+ /** @typedef {import("./types").RouteFile} RouteFile */
4
+
5
+ import { fileURLToPath } from "node:url";
6
+ import { dirname, join, resolve } from "node:path";
7
+ import process from "node:process";
8
+ import { transformWithEsbuild } from "vite";
9
+ import { routes } from "../router/routes.mjs";
10
+ import { generateRoute, getRoutes } from "./helpers.mjs";
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = dirname(__filename);
14
+ /** @param {string} p */
15
+ const toAbsolute = (p) => resolve(__dirname, p);
16
+
17
+ const pluginDefaults = {
18
+ routesDir: "src/routes",
19
+ extensions: [".tsx", ".jsx", ".ts", ".js"],
20
+ };
21
+
22
+ export default function VitePluginVanJS(options = {}) {
23
+ const pluginConfig = { ...pluginDefaults, ...options };
24
+ const { routesDir } = pluginConfig;
25
+
26
+ /** @type {ResolvedConfig} */
27
+ let config;
28
+ /** @type {RouteFile[] | null} */
29
+ let routeCache = null;
30
+
31
+ const virtualModuleId = "virtual:@vanjs/routes";
32
+ const resolvedVirtualModuleId = "\0" + virtualModuleId;
33
+
34
+ return {
35
+ name: "vanjs",
36
+ enforce: "pre",
37
+ config() {
38
+ return {
39
+ optimizeDeps: {
40
+ noDiscovery: true,
41
+ include: [
42
+ "vanjs-core",
43
+ "vanjs-ext",
44
+ "mini-van-plate/van-plate",
45
+ "mini-van-plate/shared",
46
+ ],
47
+ },
48
+ ssr: {
49
+ noExternal: ["vanjs-*", "*-vanjs", "@vanjs/*"],
50
+ },
51
+ resolve: {
52
+ alias: {
53
+ "@vanjs/setup": toAbsolute("../setup/index"),
54
+ "@vanjs/van": toAbsolute("../setup/van"),
55
+ "@vanjs/vanX": toAbsolute("../setup/vanX"),
56
+ "@vanjs/client": toAbsolute("../client"),
57
+ "@vanjs/server": toAbsolute("../server"),
58
+ "@vanjs/meta": toAbsolute("../meta"),
59
+ "@vanjs/router": toAbsolute("../router"),
60
+ "@vanjs/jsx": toAbsolute("../jsx"),
61
+ },
62
+ },
63
+ esbuild: {
64
+ jsx: "automatic",
65
+ jsxImportSource: "@vanjs/jsx",
66
+ },
67
+ };
68
+ },
69
+ /** @param {import("vite").ResolvedConfig} resolvedConfig */
70
+ configResolved(resolvedConfig) {
71
+ config = resolvedConfig;
72
+ },
73
+ /** @param {import("vite").ViteDevServer} server */
74
+ configureServer(server) {
75
+ // Watch routes directory
76
+ const pagesPath = join(config.root, routesDir);
77
+ /** @param {string} file */
78
+ const changeHandler = (file) => {
79
+ // istanbul ignore else
80
+ if (file.startsWith(pagesPath)) {
81
+ routes.length = 0;
82
+ routeCache = null;
83
+ const module = server.moduleGraph.getModuleById(
84
+ resolvedVirtualModuleId,
85
+ );
86
+ // istanbul ignore else
87
+ if (module) {
88
+ server.moduleGraph.invalidateModule(module);
89
+ }
90
+ server.ws.send({ type: "full-reload" });
91
+ }
92
+ };
93
+ server.watcher.add(pagesPath);
94
+
95
+ // Handle file changes in pages directory
96
+ // 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir'
97
+ server.watcher.on("add", changeHandler);
98
+ server.watcher.on("addDir", changeHandler);
99
+ server.watcher.on("unlink", changeHandler);
100
+ server.watcher.on("unlinkDir", changeHandler);
101
+ server.watcher.on("change", changeHandler);
102
+ },
103
+ /** @type {(source: string, importer: string | undefined, ops: { ssr: boolean }) => string | null} */
104
+ resolveId(source, importer, ops) {
105
+ // istanbul ignore else
106
+ if (source === virtualModuleId) {
107
+ return resolvedVirtualModuleId;
108
+ }
109
+ const isVanXFile = importer &&
110
+ /vanjs-ext[\/\\]src[\/\\]van-x/.test(importer);
111
+ const isSetupFile = importer &&
112
+ /vite-plugin-vanjs[\/\\]setup/.test(importer);
113
+ const isProduction = process.env.NODE_ENV === "production";
114
+ const isTest = process.env.NODE_ENV === "test";
115
+ const isJSXImport = source.includes("/vite-plugin-vanjs/jsx/jsx") ||
116
+ importer?.includes("/vite-plugin-vanjs/jsx/jsx.mjs");
117
+
118
+ const resolvedVan = toAbsolute(
119
+ ops.ssr ? "../setup/van-ssr.mjs" : (isJSXImport || isProduction ||
120
+ isTest)
121
+ ? "../setup/van.mjs"
122
+ : "../setup/van-debug.mjs",
123
+ );
124
+ const resolvedVanX = toAbsolute(
125
+ ops.ssr ? "../setup/vanX-ssr.mjs" : "../setup/vanX.mjs",
126
+ );
127
+ const setupResolved = toAbsolute(
128
+ ops.ssr
129
+ ? "../setup/index-ssr.mjs"
130
+ : (isProduction || isTest)
131
+ ? "../setup/index.mjs"
132
+ : "../setup/index-debug.mjs",
133
+ );
134
+
135
+ // Resolve early when source already resolved. EG: @vanjs/van
136
+ if (source === setupResolved || setupResolved.includes(source)) {
137
+ return setupResolved;
138
+ }
139
+ if (source === resolvedVan || resolvedVan.includes(source)) {
140
+ return resolvedVan;
141
+ }
142
+ if (source === resolvedVanX || resolvedVanX.includes(source)) {
143
+ return resolvedVanX;
144
+ }
145
+
146
+ // istanbul ignore else
147
+ if (!isSetupFile && !isVanXFile) {
148
+ if (source === "@vanjs/setup") {
149
+ return setupResolved;
150
+ }
151
+ if (importer?.endsWith("debug.js") && source.endsWith("/van.js")) {
152
+ return toAbsolute("../setup/van.mjs");
153
+ }
154
+ if (source === "vanjs-core" || source === "@vanjs/van") {
155
+ return resolvedVan;
156
+ }
157
+ if (source === "vanjs-ext" || source === "@vanjs/vanX") {
158
+ return resolvedVanX;
159
+ }
160
+ }
161
+
162
+ return null;
163
+ },
164
+ /** @type {(id: string, ops: { ssr: boolean }) => Promise<({ code: string, map: null } | null)>} */
165
+ async load(id, ops) {
166
+ // istanbul ignore else
167
+ if (id === resolvedVirtualModuleId) {
168
+ const currentRoutes = routeCache ||
169
+ await getRoutes(config, pluginConfig);
170
+ if (!currentRoutes || !currentRoutes.length) {
171
+ // don't crash the server if no routes are found
172
+ // devs might not use file system router
173
+ return { code: "", map: null };
174
+ }
175
+
176
+ const routesScript = `
177
+ import { Route, routes } from "@vanjs/router/routes.mjs";
178
+ import { lazy } from "@vanjs/router/lazy.mjs";
179
+
180
+ // Reset current routes
181
+ routes.length = 0;
182
+
183
+ // Register routes
184
+ ${currentRoutes.map(generateRoute).join("\n")}
185
+ ${
186
+ ops.ssr && currentRoutes.length
187
+ ? `console.log(\`🍦 @vanjs/router registered ${currentRoutes.length} routes.\`)`
188
+ : /* istanbul ignore next - satisfied */ ""
189
+ }
190
+ `;
191
+
192
+ const result = await transformWithEsbuild(
193
+ routesScript,
194
+ id,
195
+ { loader: "js" },
196
+ );
197
+
198
+ return {
199
+ code: result.code,
200
+ map: null,
201
+ };
202
+ }
203
+ return null;
204
+ },
205
+ };
206
+ }
@@ -0,0 +1,23 @@
1
+ // plugin/types.ts
2
+ export * from "../jsx/types";
3
+ export * from "../setup/types";
4
+ export * from "../router/types";
5
+ export * from "../meta/types";
6
+ export * from "../server/types";
7
+ export * from "../client/types";
8
+ export * from "../parser/types";
9
+ import type { Plugin } from "vite";
10
+
11
+ export type VanJSPluginOptions = {
12
+ routesDir: string;
13
+ extensions: string[];
14
+ };
15
+
16
+ export type VanJSPlugin = (
17
+ options?: Partial<VanJSPluginOptions>,
18
+ ) => Plugin;
19
+
20
+ // This is what your plugin actually returns, so declare it as a Plugin type
21
+ declare const VitePluginVanJS: VanJSPlugin;
22
+
23
+ export default VitePluginVanJS;
package/router/a.mjs CHANGED
@@ -1,6 +1,5 @@
1
1
  // router/a.mjs
2
2
  import van from "vanjs-core";
3
- import setup from "../setup/index.mjs";
4
3
  import { matchRoute } from "./routes.mjs";
5
4
  import { executeLifecycle, isCurrentPage, navigate } from "./helpers.mjs";
6
5
 
@@ -20,37 +19,37 @@ export const A = (
20
19
  const newProps = {
21
20
  href,
22
21
  ...props,
23
- };
24
-
25
- van.derive(() => {
26
- if (isCurrentPage(href)) {
27
- newProps["aria-current"] = "page";
28
- }
29
- });
30
-
31
- const anchor = van.tags.a(newProps, [...(children || []), ...otherChildren]);
32
- /* istanbul ignore else */
33
- if (!setup.isServer) {
34
- anchor.addEventListener("click", async (e) => {
22
+ onclick: async (e) => {
35
23
  e.preventDefault();
36
24
  /* istanbul ignore next */
37
25
  if (isCurrentPage(href)) return;
38
26
 
27
+ // istanbul ignore else
28
+ if (props.onclick) {
29
+ await props.onclick(e);
30
+ }
31
+
39
32
  const route = matchRoute(href);
40
33
  const module = await route.component();
41
34
  await executeLifecycle(module, route.params);
42
35
 
43
36
  navigate(href);
44
- });
45
-
46
- anchor.addEventListener("mouseenter", () => {
37
+ },
38
+ onmouseenter: () => {
47
39
  const route = matchRoute(href);
48
40
 
49
41
  /* istanbul ignore else */
50
42
  if (route?.component) {
51
43
  route.component();
52
44
  }
53
- });
54
- }
55
- return anchor;
45
+ },
46
+ };
47
+
48
+ van.derive(() => {
49
+ if (isCurrentPage(href)) {
50
+ newProps["aria-current"] = "page";
51
+ }
52
+ });
53
+
54
+ return van.tags.a(newProps, children || otherChildren);
56
55
  };
package/router/cache.mjs CHANGED
@@ -1,8 +1,9 @@
1
1
  /** @typedef {import("./types").ComponentModule} ComponentModule */
2
- /** @typedef {import("./types").GetCachedRoute} GetCachedRoute */
3
- /** @typedef {import("./types").CacheRoute} CacheRoute */
2
+ /** @typedef {import("./types").ImportFn} ImportFn */
3
+ /** @typedef {typeof import("./types").getCached} GetCachedRoute */
4
+ /** @typedef {typeof import("./types").cache} CacheRoute */
4
5
 
5
- /** @type {Map<string, ComponentModule>} */
6
+ /** @type {Map<ImportFn, ComponentModule>} */
6
7
  const routeCache = new Map();
7
8
 
8
9
  /** @type {GetCachedRoute} */