@vaadin/hilla-file-router 24.4.0-alpha16 → 24.4.0-alpha18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/hilla-file-router",
3
- "version": "24.4.0-alpha16",
3
+ "version": "24.4.0-alpha18",
4
4
  "description": "Hilla file-based router",
5
5
  "main": "index.js",
6
6
  "module": "index.js",
@@ -8,7 +8,7 @@
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "https://github.com/vaadin/hilla.git",
11
- "directory": "packages/ts/hilla-file-router"
11
+ "directory": "packages/ts/file-router"
12
12
  },
13
13
  "keywords": [
14
14
  "Hilla",
@@ -26,7 +26,7 @@
26
26
  "build:copy": "cd src && copyfiles *.d.ts **/*.d.ts ..",
27
27
  "lint": "eslint src test",
28
28
  "lint:fix": "eslint src test --fix",
29
- "test": "mocha test/**/*.spec.ts --config ../../../.mocharc.cjs",
29
+ "test": "mocha test/**/*.spec.{ts,tsx} --config ../../../.mocharc.cjs",
30
30
  "test:coverage": "c8 -c ../../../.c8rc.json npm test",
31
31
  "typecheck": "tsc --noEmit"
32
32
  },
@@ -80,7 +80,7 @@
80
80
  "type-fest": "^4.9.0"
81
81
  },
82
82
  "dependencies": {
83
- "@vaadin/hilla-generator-utils": "24.4.0-alpha16",
83
+ "@vaadin/hilla-generator-utils": "24.4.0-alpha18",
84
84
  "react": "^18.2.0",
85
85
  "rollup": "^4.12.0",
86
86
  "typescript": "5.3.2"
@@ -26,9 +26,9 @@ export type CollectRoutesOptions = Readonly<{
26
26
  /**
27
27
  * Collect route metadata from the file system and build a route tree.
28
28
  *
29
- * It accepts files that start with `$` as special files.
30
- * - `$layout` contains a component that wraps the child components.
31
- * - `$index` contains a component that will be used as the index page of the directory.
29
+ * It accepts files that start with `@` as special files.
30
+ * - `@layout` contains a component that wraps the child components.
31
+ * - `@index` contains a component that will be used as the index page of the directory.
32
32
  *
33
33
  * It accepts files that start with `_` as private files. They will be ignored.
34
34
  *
@@ -1 +1 @@
1
- {"version":3,"file":"collectRoutesFromFS.d.ts","sourceRoot":"","sources":["../src/vite-plugin/collectRoutesFromFS.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAGnC,MAAM,MAAM,SAAS,GAAG,QAAQ,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,QAAQ,EAAE,SAAS,EAAE,CAAC;CACvB,CAAC,CAAC;AAEH;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C;;OAEG;IACH,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,GAAG,CAAC;IACb;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC,CAAC;AAmBH;;;;;;;;;;;;;GAaG;AACH,wBAA8B,mBAAmB,CAC/C,GAAG,EAAE,GAAG,EACR,EAAE,UAAU,EAAE,MAAM,EAAE,MAAY,EAAE,EAAE,oBAAoB,GACzD,OAAO,CAAC,SAAS,CAAC,CAyDpB"}
1
+ {"version":3,"file":"collectRoutesFromFS.d.ts","sourceRoot":"","sources":["../src/vite-plugin/collectRoutesFromFS.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAGnC,MAAM,MAAM,SAAS,GAAG,QAAQ,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,QAAQ,EAAE,SAAS,EAAE,CAAC;CACvB,CAAC,CAAC;AAEH;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C;;OAEG;IACH,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,GAAG,CAAC;IACb;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC,CAAC;AAmBH;;;;;;;;;;;;;GAaG;AACH,wBAA8B,mBAAmB,CAC/C,GAAG,EAAE,GAAG,EACR,EAAE,UAAU,EAAE,MAAM,EAAE,MAAY,EAAE,EAAE,oBAAoB,GACzD,OAAO,CAAC,SAAS,CAAC,CA6DpB"}
@@ -14,9 +14,9 @@ async function checkFile(url, logger) {
14
14
  return url;
15
15
  }
16
16
  const collator = new Intl.Collator("en-US");
17
- const phantomDirPattern = /\/?\$phantom/giu;
17
+ const warningFor = [".ts", ".js"];
18
18
  async function collectRoutesFromFS(dir, { extensions, logger, parent = dir }) {
19
- const path = relative(fileURLToPath(parent), fileURLToPath(dir)).replaceAll(phantomDirPattern, "");
19
+ const path = relative(fileURLToPath(parent), fileURLToPath(dir));
20
20
  let children = [];
21
21
  let layout;
22
22
  for await (const d of await opendir(dir)) {
@@ -25,10 +25,10 @@ async function collectRoutesFromFS(dir, { extensions, logger, parent = dir }) {
25
25
  } else if (d.isFile() && extensions.includes(extname(d.name))) {
26
26
  const file = new URL(d.name, dir);
27
27
  const name = basename(d.name, extname(d.name));
28
- if (name.startsWith("$")) {
29
- if (name === "$layout") {
28
+ if (name.startsWith("@")) {
29
+ if (name === "@layout") {
30
30
  layout = file;
31
- } else if (name === "$index") {
31
+ } else if (name === "@index") {
32
32
  children.push({
33
33
  path: "",
34
34
  file,
@@ -36,7 +36,7 @@ async function collectRoutesFromFS(dir, { extensions, logger, parent = dir }) {
36
36
  });
37
37
  } else {
38
38
  throw new Error(
39
- 'Symbol "$" is reserved for special directories and files; only "$layout", "$phantom" and "$index" are allowed'
39
+ 'Symbol "@" is reserved for special directories and files; only "@layout" and "@index" are allowed'
40
40
  );
41
41
  }
42
42
  } else if (!name.startsWith("_")) {
@@ -46,6 +46,10 @@ async function collectRoutesFromFS(dir, { extensions, logger, parent = dir }) {
46
46
  children: []
47
47
  });
48
48
  }
49
+ } else if (d.isFile() && !d.name.startsWith("_") && warningFor.includes(extname(d.name))) {
50
+ logger.warn(
51
+ `File System based router expects only JSX files in 'Frontend/views/' directory, such as '*.tsx' and '*.jsx'. The file '${d.name}' will be ignored by router, as it doesn't match this convention. Please consider storing it in another directory.`
52
+ );
49
53
  }
50
54
  }
51
55
  [children, layout] = await Promise.all([
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/vite-plugin/collectRoutesFromFS.ts"],
4
- "sourcesContent": ["import { opendir, readFile } from 'node:fs/promises';\nimport { basename, extname, relative } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { Logger } from 'vite';\nimport { cleanUp } from './utils.js';\n\nexport type RouteMeta = Readonly<{\n path: string;\n file?: URL;\n layout?: URL;\n children: RouteMeta[];\n}>;\n\n/**\n * Routes collector options.\n */\nexport type CollectRoutesOptions = Readonly<{\n /**\n * The list of extensions for files that will be collected as routes.\n */\n extensions: readonly string[];\n /**\n * The parent directory of the current directory. This is a\n * nested parameter used inside the function only.\n */\n parent?: URL;\n /**\n * The Vite logger instance.\n */\n logger: Logger;\n}>;\n\nasync function checkFile(url: URL | undefined, logger: Logger): Promise<URL | undefined> {\n if (url) {\n const contents = await readFile(url, 'utf-8');\n if (contents === '') {\n return undefined;\n } else if (!contents.includes('export default')) {\n logger.error(`The file \"${String(url)}\" should contain a default export of a component`);\n }\n }\n\n return url;\n}\n\nconst collator = new Intl.Collator('en-US');\n\nconst phantomDirPattern = /\\/?\\$phantom/giu;\n\n/**\n * Collect route metadata from the file system and build a route tree.\n *\n * It accepts files that start with `$` as special files.\n * - `$layout` contains a component that wraps the child components.\n * - `$index` contains a component that will be used as the index page of the directory.\n *\n * It accepts files that start with `_` as private files. They will be ignored.\n *\n * @param dir - The directory to collect routes from.\n * @param options - The options object.\n *\n * @returns The route metadata tree.\n */\nexport default async function collectRoutesFromFS(\n dir: URL,\n { extensions, logger, parent = dir }: CollectRoutesOptions,\n): Promise<RouteMeta> {\n const path = relative(fileURLToPath(parent), fileURLToPath(dir)).replaceAll(phantomDirPattern, '');\n let children: RouteMeta[] = [];\n let layout: URL | undefined;\n\n for await (const d of await opendir(dir)) {\n if (d.isDirectory() && !d.name.startsWith('_')) {\n children.push(await collectRoutesFromFS(new URL(`${d.name}/`, dir), { extensions, logger, parent: dir }));\n } else if (d.isFile() && extensions.includes(extname(d.name))) {\n const file = new URL(d.name, dir);\n const name = basename(d.name, extname(d.name));\n\n if (name.startsWith('$')) {\n if (name === '$layout') {\n layout = file;\n } else if (name === '$index') {\n children.push({\n path: '',\n file,\n children: [],\n });\n } else {\n throw new Error(\n 'Symbol \"$\" is reserved for special directories and files; only \"$layout\", \"$phantom\" and \"$index\" are allowed',\n );\n }\n } else if (!name.startsWith('_')) {\n children.push({\n path: name,\n file,\n children: [],\n });\n }\n }\n }\n\n [children, layout] = await Promise.all([\n Promise.all(\n children.map(async (child) => {\n let { file: f, layout: l } = child;\n [f, l] = await Promise.all([checkFile(f, logger), checkFile(l, logger)]);\n\n return {\n ...child,\n file: f,\n layout: l,\n };\n }),\n ),\n checkFile(layout, logger),\n ]);\n\n return {\n path,\n layout,\n children: children.sort(({ path: a }, { path: b }) => collator.compare(cleanUp(a), cleanUp(b))),\n };\n}\n"],
5
- "mappings": "AAAA,SAAS,SAAS,gBAAgB;AAClC,SAAS,UAAU,SAAS,gBAAgB;AAC5C,SAAS,qBAAqB;AAE9B,SAAS,eAAe;AA4BxB,eAAe,UAAU,KAAsB,QAA0C;AACvF,MAAI,KAAK;AACP,UAAM,WAAW,MAAM,SAAS,KAAK,OAAO;AAC5C,QAAI,aAAa,IAAI;AACnB,aAAO;AAAA,IACT,WAAW,CAAC,SAAS,SAAS,gBAAgB,GAAG;AAC/C,aAAO,MAAM,aAAa,OAAO,GAAG,CAAC,kDAAkD;AAAA,IACzF;AAAA,EACF;AAEA,SAAO;AACT;AAEA,MAAM,WAAW,IAAI,KAAK,SAAS,OAAO;AAE1C,MAAM,oBAAoB;AAgB1B,eAAO,oBACL,KACA,EAAE,YAAY,QAAQ,SAAS,IAAI,GACf;AACpB,QAAM,OAAO,SAAS,cAAc,MAAM,GAAG,cAAc,GAAG,CAAC,EAAE,WAAW,mBAAmB,EAAE;AACjG,MAAI,WAAwB,CAAC;AAC7B,MAAI;AAEJ,mBAAiB,KAAK,MAAM,QAAQ,GAAG,GAAG;AACxC,QAAI,EAAE,YAAY,KAAK,CAAC,EAAE,KAAK,WAAW,GAAG,GAAG;AAC9C,eAAS,KAAK,MAAM,oBAAoB,IAAI,IAAI,GAAG,EAAE,IAAI,KAAK,GAAG,GAAG,EAAE,YAAY,QAAQ,QAAQ,IAAI,CAAC,CAAC;AAAA,IAC1G,WAAW,EAAE,OAAO,KAAK,WAAW,SAAS,QAAQ,EAAE,IAAI,CAAC,GAAG;AAC7D,YAAM,OAAO,IAAI,IAAI,EAAE,MAAM,GAAG;AAChC,YAAM,OAAO,SAAS,EAAE,MAAM,QAAQ,EAAE,IAAI,CAAC;AAE7C,UAAI,KAAK,WAAW,GAAG,GAAG;AACxB,YAAI,SAAS,WAAW;AACtB,mBAAS;AAAA,QACX,WAAW,SAAS,UAAU;AAC5B,mBAAS,KAAK;AAAA,YACZ,MAAM;AAAA,YACN;AAAA,YACA,UAAU,CAAC;AAAA,UACb,CAAC;AAAA,QACH,OAAO;AACL,gBAAM,IAAI;AAAA,YACR;AAAA,UACF;AAAA,QACF;AAAA,MACF,WAAW,CAAC,KAAK,WAAW,GAAG,GAAG;AAChC,iBAAS,KAAK;AAAA,UACZ,MAAM;AAAA,UACN;AAAA,UACA,UAAU,CAAC;AAAA,QACb,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAEA,GAAC,UAAU,MAAM,IAAI,MAAM,QAAQ,IAAI;AAAA,IACrC,QAAQ;AAAA,MACN,SAAS,IAAI,OAAO,UAAU;AAC5B,YAAI,EAAE,MAAM,GAAG,QAAQ,EAAE,IAAI;AAC7B,SAAC,GAAG,CAAC,IAAI,MAAM,QAAQ,IAAI,CAAC,UAAU,GAAG,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC,CAAC;AAEvE,eAAO;AAAA,UACL,GAAG;AAAA,UACH,MAAM;AAAA,UACN,QAAQ;AAAA,QACV;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IACA,UAAU,QAAQ,MAAM;AAAA,EAC1B,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,UAAU,SAAS,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,SAAS,QAAQ,QAAQ,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC;AAAA,EAChG;AACF;",
4
+ "sourcesContent": ["import { opendir, readFile } from 'node:fs/promises';\nimport { basename, extname, relative } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { Logger } from 'vite';\nimport { cleanUp } from './utils.js';\n\nexport type RouteMeta = Readonly<{\n path: string;\n file?: URL;\n layout?: URL;\n children: RouteMeta[];\n}>;\n\n/**\n * Routes collector options.\n */\nexport type CollectRoutesOptions = Readonly<{\n /**\n * The list of extensions for files that will be collected as routes.\n */\n extensions: readonly string[];\n /**\n * The parent directory of the current directory. This is a\n * nested parameter used inside the function only.\n */\n parent?: URL;\n /**\n * The Vite logger instance.\n */\n logger: Logger;\n}>;\n\nasync function checkFile(url: URL | undefined, logger: Logger): Promise<URL | undefined> {\n if (url) {\n const contents = await readFile(url, 'utf-8');\n if (contents === '') {\n return undefined;\n } else if (!contents.includes('export default')) {\n logger.error(`The file \"${String(url)}\" should contain a default export of a component`);\n }\n }\n\n return url;\n}\n\nconst collator = new Intl.Collator('en-US');\n\nconst warningFor = ['.ts', '.js'];\n\n/**\n * Collect route metadata from the file system and build a route tree.\n *\n * It accepts files that start with `@` as special files.\n * - `@layout` contains a component that wraps the child components.\n * - `@index` contains a component that will be used as the index page of the directory.\n *\n * It accepts files that start with `_` as private files. They will be ignored.\n *\n * @param dir - The directory to collect routes from.\n * @param options - The options object.\n *\n * @returns The route metadata tree.\n */\nexport default async function collectRoutesFromFS(\n dir: URL,\n { extensions, logger, parent = dir }: CollectRoutesOptions,\n): Promise<RouteMeta> {\n const path = relative(fileURLToPath(parent), fileURLToPath(dir));\n let children: RouteMeta[] = [];\n let layout: URL | undefined;\n\n for await (const d of await opendir(dir)) {\n if (d.isDirectory() && !d.name.startsWith('_')) {\n children.push(await collectRoutesFromFS(new URL(`${d.name}/`, dir), { extensions, logger, parent: dir }));\n } else if (d.isFile() && extensions.includes(extname(d.name))) {\n const file = new URL(d.name, dir);\n const name = basename(d.name, extname(d.name));\n\n if (name.startsWith('@')) {\n if (name === '@layout') {\n layout = file;\n } else if (name === '@index') {\n children.push({\n path: '',\n file,\n children: [],\n });\n } else {\n throw new Error(\n 'Symbol \"@\" is reserved for special directories and files; only \"@layout\" and \"@index\" are allowed',\n );\n }\n } else if (!name.startsWith('_')) {\n children.push({\n path: name,\n file,\n children: [],\n });\n }\n } else if (d.isFile() && !d.name.startsWith('_') && warningFor.includes(extname(d.name))) {\n logger.warn(\n `File System based router expects only JSX files in 'Frontend/views/' directory, such as '*.tsx' and '*.jsx'. The file '${d.name}' will be ignored by router, as it doesn't match this convention. Please consider storing it in another directory.`,\n );\n }\n }\n\n [children, layout] = await Promise.all([\n Promise.all(\n children.map(async (child) => {\n let { file: f, layout: l } = child;\n [f, l] = await Promise.all([checkFile(f, logger), checkFile(l, logger)]);\n\n return {\n ...child,\n file: f,\n layout: l,\n };\n }),\n ),\n checkFile(layout, logger),\n ]);\n\n return {\n path,\n layout,\n children: children.sort(({ path: a }, { path: b }) => collator.compare(cleanUp(a), cleanUp(b))),\n };\n}\n"],
5
+ "mappings": "AAAA,SAAS,SAAS,gBAAgB;AAClC,SAAS,UAAU,SAAS,gBAAgB;AAC5C,SAAS,qBAAqB;AAE9B,SAAS,eAAe;AA4BxB,eAAe,UAAU,KAAsB,QAA0C;AACvF,MAAI,KAAK;AACP,UAAM,WAAW,MAAM,SAAS,KAAK,OAAO;AAC5C,QAAI,aAAa,IAAI;AACnB,aAAO;AAAA,IACT,WAAW,CAAC,SAAS,SAAS,gBAAgB,GAAG;AAC/C,aAAO,MAAM,aAAa,OAAO,GAAG,CAAC,kDAAkD;AAAA,IACzF;AAAA,EACF;AAEA,SAAO;AACT;AAEA,MAAM,WAAW,IAAI,KAAK,SAAS,OAAO;AAE1C,MAAM,aAAa,CAAC,OAAO,KAAK;AAgBhC,eAAO,oBACL,KACA,EAAE,YAAY,QAAQ,SAAS,IAAI,GACf;AACpB,QAAM,OAAO,SAAS,cAAc,MAAM,GAAG,cAAc,GAAG,CAAC;AAC/D,MAAI,WAAwB,CAAC;AAC7B,MAAI;AAEJ,mBAAiB,KAAK,MAAM,QAAQ,GAAG,GAAG;AACxC,QAAI,EAAE,YAAY,KAAK,CAAC,EAAE,KAAK,WAAW,GAAG,GAAG;AAC9C,eAAS,KAAK,MAAM,oBAAoB,IAAI,IAAI,GAAG,EAAE,IAAI,KAAK,GAAG,GAAG,EAAE,YAAY,QAAQ,QAAQ,IAAI,CAAC,CAAC;AAAA,IAC1G,WAAW,EAAE,OAAO,KAAK,WAAW,SAAS,QAAQ,EAAE,IAAI,CAAC,GAAG;AAC7D,YAAM,OAAO,IAAI,IAAI,EAAE,MAAM,GAAG;AAChC,YAAM,OAAO,SAAS,EAAE,MAAM,QAAQ,EAAE,IAAI,CAAC;AAE7C,UAAI,KAAK,WAAW,GAAG,GAAG;AACxB,YAAI,SAAS,WAAW;AACtB,mBAAS;AAAA,QACX,WAAW,SAAS,UAAU;AAC5B,mBAAS,KAAK;AAAA,YACZ,MAAM;AAAA,YACN;AAAA,YACA,UAAU,CAAC;AAAA,UACb,CAAC;AAAA,QACH,OAAO;AACL,gBAAM,IAAI;AAAA,YACR;AAAA,UACF;AAAA,QACF;AAAA,MACF,WAAW,CAAC,KAAK,WAAW,GAAG,GAAG;AAChC,iBAAS,KAAK;AAAA,UACZ,MAAM;AAAA,UACN;AAAA,UACA,UAAU,CAAC;AAAA,QACb,CAAC;AAAA,MACH;AAAA,IACF,WAAW,EAAE,OAAO,KAAK,CAAC,EAAE,KAAK,WAAW,GAAG,KAAK,WAAW,SAAS,QAAQ,EAAE,IAAI,CAAC,GAAG;AACxF,aAAO;AAAA,QACL,0HAA0H,EAAE,IAAI;AAAA,MAClI;AAAA,IACF;AAAA,EACF;AAEA,GAAC,UAAU,MAAM,IAAI,MAAM,QAAQ,IAAI;AAAA,IACrC,QAAQ;AAAA,MACN,SAAS,IAAI,OAAO,UAAU;AAC5B,YAAI,EAAE,MAAM,GAAG,QAAQ,EAAE,IAAI;AAC7B,SAAC,GAAG,CAAC,IAAI,MAAM,QAAQ,IAAI,CAAC,UAAU,GAAG,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC,CAAC;AAEvE,eAAO;AAAA,UACL,GAAG;AAAA,UACH,MAAM;AAAA,UACN,QAAQ;AAAA,QACV;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IACA,UAAU,QAAQ,MAAM;AAAA,EAC1B,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,UAAU,SAAS,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,SAAS,QAAQ,QAAQ,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC;AAAA,EAChG;AACF;",
6
6
  "names": []
7
7
  }
@@ -1 +1 @@
1
- {"version":3,"file":"createRoutesFromMeta.d.ts","sourceRoot":"","sources":["../src/vite-plugin/createRoutesFromMeta.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAmDjE;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,oBAAoB,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,eAAe,GAAG,MAAM,CAgD1G"}
1
+ {"version":3,"file":"createRoutesFromMeta.d.ts","sourceRoot":"","sources":["../src/vite-plugin/createRoutesFromMeta.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAmDjE;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,oBAAoB,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,eAAe,GAAG,MAAM,CA0D1G"}
@@ -38,10 +38,16 @@ function createRoutesFromMeta(views, { code: codeFile }) {
38
38
  ([statement]) => statement
39
39
  )
40
40
  ];
41
+ const errors = [];
41
42
  let id = 0;
42
43
  const routes = transformTreeSync(
43
44
  views,
44
- (view) => view.children.values(),
45
+ (view) => {
46
+ const paths = view.children.map((c) => c.path);
47
+ const uniquePaths = new Set(paths);
48
+ paths.filter((p) => !uniquePaths.delete(p)).forEach((p) => errors.push(`console.error("Two views share the same path: ${p}");`));
49
+ return view.children.values();
50
+ },
45
51
  ({ file: file2, layout, path }, children) => {
46
52
  const currentId = id;
47
53
  id += 1;
@@ -59,6 +65,8 @@ function createRoutesFromMeta(views, { code: codeFile }) {
59
65
  const routeDeclaration = template(
60
66
  `import a from 'IMPORTS';
61
67
 
68
+ ${errors.join("\n")}
69
+
62
70
  const routes = ROUTE;
63
71
 
64
72
  export default routes;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/vite-plugin/createRoutesFromMeta.ts"],
4
- "sourcesContent": ["import { sep, relative } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { template, transform as transformer } from '@vaadin/hilla-generator-utils/ast.js';\nimport createSourceFile from '@vaadin/hilla-generator-utils/createSourceFile.js';\nimport ts, {\n type CallExpression,\n type ImportDeclaration,\n type StringLiteral,\n type VariableStatement,\n} from 'typescript';\n\nimport { transformTreeSync } from '../shared/transformTree.js';\nimport type { RouteMeta } from './collectRoutesFromFS.js';\nimport type { RuntimeFileUrls } from './generateRuntimeFiles.js';\nimport { convertFSRouteSegmentToURLPatternFormat } from './utils.js';\n\nconst printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });\n\n/**\n * Convert a file URL to a relative path from the generated directory.\n *\n * @param url - The file URL to convert.\n * @param generatedDir - The directory where the generated view file will be stored.\n */\nfunction relativize(url: URL, generatedDir: URL): string {\n const result = relative(fileURLToPath(generatedDir), fileURLToPath(url)).replaceAll(sep, '/');\n\n if (!result.startsWith('.')) {\n return `./${result}`;\n }\n\n return result;\n}\n\n/**\n * Create an import declaration for a `views` module.\n *\n * @param mod - The name of the route module to import.\n * @param file - The file path of the module.\n */\nfunction createImport(mod: string, file: string): ImportDeclaration {\n const path = `${file.substring(0, file.lastIndexOf('.'))}.js`;\n return template(`import * as ${mod} from '${path}';\\n`, ([statement]) => statement as ts.ImportDeclaration);\n}\n\n/**\n * Create an abstract route creation function call. The nested function calls create a route tree.\n *\n * @param path - The path of the route.\n * @param mod - The name of the route module imported as a namespace.\n * @param children - The list of child route call expressions.\n */\nfunction createRouteData(path: string, mod: string | undefined, children: readonly CallExpression[]): CallExpression {\n return template(\n `const route = createRoute(\"${path}\"${mod ? `, ${mod}` : ''}${children.length > 0 ? `, CHILDREN` : ''})`,\n ([statement]) => (statement as VariableStatement).declarationList.declarations[0].initializer as CallExpression,\n [\n transformer((node) =>\n ts.isIdentifier(node) && node.text === 'CHILDREN' ? ts.factory.createArrayLiteralExpression(children) : node,\n ),\n ],\n );\n}\n\n/**\n * Loads all the files from the received metadata and creates a framework-agnostic route tree.\n *\n * @param views - The abstract route tree.\n * @param generatedDir - The directory where the generated view file will be stored.\n */\nexport default function createRoutesFromMeta(views: RouteMeta, { code: codeFile }: RuntimeFileUrls): string {\n const codeDir = new URL('./', codeFile);\n const imports: ImportDeclaration[] = [\n template(\n 'import { createRoute } from \"@vaadin/hilla-file-router/runtime.js\";',\n ([statement]) => statement as ts.ImportDeclaration,\n ),\n ];\n let id = 0;\n\n const routes = transformTreeSync<RouteMeta, CallExpression>(\n views,\n (view) => view.children.values(),\n ({ file, layout, path }, children) => {\n const currentId = id;\n id += 1;\n\n let mod: string | undefined;\n if (file) {\n mod = `Page${currentId}`;\n imports.push(createImport(mod, relativize(file, codeDir)));\n } else if (layout) {\n mod = `Layout${currentId}`;\n imports.push(createImport(mod, relativize(layout, codeDir)));\n }\n\n return createRouteData(convertFSRouteSegmentToURLPatternFormat(path), mod, children);\n },\n );\n\n const routeDeclaration = template(\n `import a from 'IMPORTS';\n\nconst routes = ROUTE;\n\nexport default routes;\n`,\n [\n transformer((node) =>\n ts.isImportDeclaration(node) && (node.moduleSpecifier as StringLiteral).text === 'IMPORTS' ? imports : node,\n ),\n transformer((node) => (ts.isIdentifier(node) && node.text === 'ROUTE' ? routes : node)),\n ],\n );\n\n const file = createSourceFile(routeDeclaration, 'views.ts');\n\n return printer.printFile(file);\n}\n"],
5
- "mappings": "AAAA,SAAS,KAAK,gBAAgB;AAC9B,SAAS,qBAAqB;AAC9B,SAAS,UAAU,aAAa,mBAAmB;AACnD,OAAO,sBAAsB;AAC7B,OAAO;AAAA,OAKA;AAEP,SAAS,yBAAyB;AAGlC,SAAS,+CAA+C;AAExD,MAAM,UAAU,GAAG,cAAc,EAAE,SAAS,GAAG,YAAY,SAAS,CAAC;AAQrE,SAAS,WAAW,KAAU,cAA2B;AACvD,QAAM,SAAS,SAAS,cAAc,YAAY,GAAG,cAAc,GAAG,CAAC,EAAE,WAAW,KAAK,GAAG;AAE5F,MAAI,CAAC,OAAO,WAAW,GAAG,GAAG;AAC3B,WAAO,KAAK,MAAM;AAAA,EACpB;AAEA,SAAO;AACT;AAQA,SAAS,aAAa,KAAa,MAAiC;AAClE,QAAM,OAAO,GAAG,KAAK,UAAU,GAAG,KAAK,YAAY,GAAG,CAAC,CAAC;AACxD,SAAO,SAAS,eAAe,GAAG,UAAU,IAAI;AAAA,GAAQ,CAAC,CAAC,SAAS,MAAM,SAAiC;AAC5G;AASA,SAAS,gBAAgB,MAAc,KAAyB,UAAqD;AACnH,SAAO;AAAA,IACL,8BAA8B,IAAI,IAAI,MAAM,KAAK,GAAG,KAAK,EAAE,GAAG,SAAS,SAAS,IAAI,eAAe,EAAE;AAAA,IACrG,CAAC,CAAC,SAAS,MAAO,UAAgC,gBAAgB,aAAa,CAAC,EAAE;AAAA,IAClF;AAAA,MACE;AAAA,QAAY,CAAC,SACX,GAAG,aAAa,IAAI,KAAK,KAAK,SAAS,aAAa,GAAG,QAAQ,6BAA6B,QAAQ,IAAI;AAAA,MAC1G;AAAA,IACF;AAAA,EACF;AACF;AAQe,SAAR,qBAAsC,OAAkB,EAAE,MAAM,SAAS,GAA4B;AAC1G,QAAM,UAAU,IAAI,IAAI,MAAM,QAAQ;AACtC,QAAM,UAA+B;AAAA,IACnC;AAAA,MACE;AAAA,MACA,CAAC,CAAC,SAAS,MAAM;AAAA,IACnB;AAAA,EACF;AACA,MAAI,KAAK;AAET,QAAM,SAAS;AAAA,IACb;AAAA,IACA,CAAC,SAAS,KAAK,SAAS,OAAO;AAAA,IAC/B,CAAC,EAAE,MAAAA,OAAM,QAAQ,KAAK,GAAG,aAAa;AACpC,YAAM,YAAY;AAClB,YAAM;AAEN,UAAI;AACJ,UAAIA,OAAM;AACR,cAAM,OAAO,SAAS;AACtB,gBAAQ,KAAK,aAAa,KAAK,WAAWA,OAAM,OAAO,CAAC,CAAC;AAAA,MAC3D,WAAW,QAAQ;AACjB,cAAM,SAAS,SAAS;AACxB,gBAAQ,KAAK,aAAa,KAAK,WAAW,QAAQ,OAAO,CAAC,CAAC;AAAA,MAC7D;AAEA,aAAO,gBAAgB,wCAAwC,IAAI,GAAG,KAAK,QAAQ;AAAA,IACrF;AAAA,EACF;AAEA,QAAM,mBAAmB;AAAA,IACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA;AAAA,MACE;AAAA,QAAY,CAAC,SACX,GAAG,oBAAoB,IAAI,KAAM,KAAK,gBAAkC,SAAS,YAAY,UAAU;AAAA,MACzG;AAAA,MACA,YAAY,CAAC,SAAU,GAAG,aAAa,IAAI,KAAK,KAAK,SAAS,UAAU,SAAS,IAAK;AAAA,IACxF;AAAA,EACF;AAEA,QAAM,OAAO,iBAAiB,kBAAkB,UAAU;AAE1D,SAAO,QAAQ,UAAU,IAAI;AAC/B;",
4
+ "sourcesContent": ["import { sep, relative } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { template, transform as transformer } from '@vaadin/hilla-generator-utils/ast.js';\nimport createSourceFile from '@vaadin/hilla-generator-utils/createSourceFile.js';\nimport ts, {\n type CallExpression,\n type ImportDeclaration,\n type StringLiteral,\n type VariableStatement,\n} from 'typescript';\n\nimport { transformTreeSync } from '../shared/transformTree.js';\nimport type { RouteMeta } from './collectRoutesFromFS.js';\nimport type { RuntimeFileUrls } from './generateRuntimeFiles.js';\nimport { convertFSRouteSegmentToURLPatternFormat } from './utils.js';\n\nconst printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });\n\n/**\n * Convert a file URL to a relative path from the generated directory.\n *\n * @param url - The file URL to convert.\n * @param generatedDir - The directory where the generated view file will be stored.\n */\nfunction relativize(url: URL, generatedDir: URL): string {\n const result = relative(fileURLToPath(generatedDir), fileURLToPath(url)).replaceAll(sep, '/');\n\n if (!result.startsWith('.')) {\n return `./${result}`;\n }\n\n return result;\n}\n\n/**\n * Create an import declaration for a `views` module.\n *\n * @param mod - The name of the route module to import.\n * @param file - The file path of the module.\n */\nfunction createImport(mod: string, file: string): ImportDeclaration {\n const path = `${file.substring(0, file.lastIndexOf('.'))}.js`;\n return template(`import * as ${mod} from '${path}';\\n`, ([statement]) => statement as ts.ImportDeclaration);\n}\n\n/**\n * Create an abstract route creation function call. The nested function calls create a route tree.\n *\n * @param path - The path of the route.\n * @param mod - The name of the route module imported as a namespace.\n * @param children - The list of child route call expressions.\n */\nfunction createRouteData(path: string, mod: string | undefined, children: readonly CallExpression[]): CallExpression {\n return template(\n `const route = createRoute(\"${path}\"${mod ? `, ${mod}` : ''}${children.length > 0 ? `, CHILDREN` : ''})`,\n ([statement]) => (statement as VariableStatement).declarationList.declarations[0].initializer as CallExpression,\n [\n transformer((node) =>\n ts.isIdentifier(node) && node.text === 'CHILDREN' ? ts.factory.createArrayLiteralExpression(children) : node,\n ),\n ],\n );\n}\n\n/**\n * Loads all the files from the received metadata and creates a framework-agnostic route tree.\n *\n * @param views - The abstract route tree.\n * @param generatedDir - The directory where the generated view file will be stored.\n */\nexport default function createRoutesFromMeta(views: RouteMeta, { code: codeFile }: RuntimeFileUrls): string {\n const codeDir = new URL('./', codeFile);\n const imports: ImportDeclaration[] = [\n template(\n 'import { createRoute } from \"@vaadin/hilla-file-router/runtime.js\";',\n ([statement]) => statement as ts.ImportDeclaration,\n ),\n ];\n const errors: string[] = [];\n let id = 0;\n\n const routes = transformTreeSync<RouteMeta, CallExpression>(\n views,\n (view) => {\n const paths = view.children.map((c) => c.path);\n const uniquePaths = new Set(paths);\n paths\n .filter((p) => !uniquePaths.delete(p))\n .forEach((p) => errors.push(`console.error(\"Two views share the same path: ${p}\");`));\n return view.children.values();\n },\n ({ file, layout, path }, children) => {\n const currentId = id;\n id += 1;\n\n let mod: string | undefined;\n if (file) {\n mod = `Page${currentId}`;\n imports.push(createImport(mod, relativize(file, codeDir)));\n } else if (layout) {\n mod = `Layout${currentId}`;\n imports.push(createImport(mod, relativize(layout, codeDir)));\n }\n\n return createRouteData(convertFSRouteSegmentToURLPatternFormat(path), mod, children);\n },\n );\n\n const routeDeclaration = template(\n `import a from 'IMPORTS';\n\n${errors.join('\\n')}\n\nconst routes = ROUTE;\n\nexport default routes;\n`,\n [\n transformer((node) =>\n ts.isImportDeclaration(node) && (node.moduleSpecifier as StringLiteral).text === 'IMPORTS' ? imports : node,\n ),\n transformer((node) => (ts.isIdentifier(node) && node.text === 'ROUTE' ? routes : node)),\n ],\n );\n\n const file = createSourceFile(routeDeclaration, 'views.ts');\n\n return printer.printFile(file);\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["file"]
7
7
  }
package/vite-plugin.d.ts CHANGED
@@ -20,9 +20,15 @@ export type PluginOptions = Readonly<{
20
20
  * The list of extensions that will be collected as routes of the file-based
21
21
  * router.
22
22
  *
23
- * @defaultValue `['.tsx', '.jsx', '.ts', '.js']`
23
+ * @defaultValue `['.tsx', '.jsx']`
24
24
  */
25
25
  extensions?: readonly string[];
26
+ /**
27
+ * The flag to indicate whether the plugin is running in development mode.
28
+ *
29
+ * @defaultValue `false`
30
+ */
31
+ isDevMode?: boolean;
26
32
  }>;
27
33
  /**
28
34
  * A Vite plugin that generates a router from the files in the specific directory.
@@ -30,5 +36,5 @@ export type PluginOptions = Readonly<{
30
36
  * @param options - The plugin options.
31
37
  * @returns A Vite plugin.
32
38
  */
33
- export default function vitePluginFileSystemRouter({ viewsDir, generatedDir, extensions, }?: PluginOptions): Plugin;
39
+ export default function vitePluginFileSystemRouter({ viewsDir, generatedDir, extensions, isDevMode, }?: PluginOptions): Plugin;
34
40
  //# sourceMappingURL=vite-plugin.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vite-plugin.d.ts","sourceRoot":"","sources":["src/vite-plugin.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAU,MAAM,EAAE,MAAM,MAAM,CAAC;AAG3C;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,GAAG,GAAG,MAAM,CAAC;IACxB;;;;OAIG;IACH,YAAY,CAAC,EAAE,GAAG,GAAG,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC,CAAC,CAAC;AAEH;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,0BAA0B,CAAC,EACjD,QAA4B,EAC5B,YAAoC,EACpC,UAA2C,GAC5C,GAAE,aAAkB,GAAG,MAAM,CA8E7B"}
1
+ {"version":3,"file":"vite-plugin.d.ts","sourceRoot":"","sources":["src/vite-plugin.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAU,MAAM,EAAE,MAAM,MAAM,CAAC;AAG3C;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,GAAG,GAAG,MAAM,CAAC;IACxB;;;;OAIG;IACH,YAAY,CAAC,EAAE,GAAG,GAAG,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC,CAAC;AAEH;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,0BAA0B,CAAC,EACjD,QAA4B,EAC5B,YAAoC,EACpC,UAA6B,EAC7B,SAAiB,GAClB,GAAE,aAAkB,GAAG,MAAM,CA+E7B"}
package/vite-plugin.js CHANGED
@@ -4,7 +4,8 @@ import { generateRuntimeFiles } from "./vite-plugin/generateRuntimeFiles.js";
4
4
  function vitePluginFileSystemRouter({
5
5
  viewsDir = "frontend/views/",
6
6
  generatedDir = "frontend/generated/",
7
- extensions = [".tsx", ".jsx", ".ts", ".js"]
7
+ extensions = [".tsx", ".jsx"],
8
+ isDevMode = false
8
9
  } = {}) {
9
10
  const hmrInjectionPattern = /(?<=import\.meta\.hot\.accept[\s\S]+)if\s\(!nextExports\)\s+return;/u;
10
11
  let _viewsDir;
@@ -23,7 +24,7 @@ function vitePluginFileSystemRouter({
23
24
  _logger.info(`The directory of generated files: ${String(_generatedDir)}`);
24
25
  _logger.info(`The output directory: ${String(_outDir)}`);
25
26
  runtimeUrls = {
26
- json: new URL("views.json", _outDir),
27
+ json: new URL("views.json", isDevMode ? _generatedDir : _outDir),
27
28
  code: new URL("views.ts", _generatedDir)
28
29
  };
29
30
  },
@@ -43,6 +44,7 @@ function vitePluginFileSystemRouter({
43
44
  generateRuntimeFiles(_viewsDir, runtimeUrls, extensions, _logger).catch(
44
45
  (e) => _logger.error(String(e))
45
46
  );
47
+ server.hot.send({ type: "full-reload" });
46
48
  };
47
49
  server.watcher.on("add", changeListener);
48
50
  server.watcher.on("change", changeListener);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["src/vite-plugin.ts"],
4
- "sourcesContent": ["import { basename } from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\nimport type { TransformResult } from 'rollup';\nimport type { Logger, Plugin } from 'vite';\nimport { generateRuntimeFiles, type RuntimeFileUrls } from './vite-plugin/generateRuntimeFiles.js';\n\n/**\n * The options for the Vite file-based router plugin.\n */\nexport type PluginOptions = Readonly<{\n /**\n * The base directory for the router. The folders and files in this directory\n * will be used as route paths.\n *\n * @defaultValue `frontend/views`\n */\n viewsDir?: URL | string;\n /**\n * The directory where the generated view file will be stored.\n *\n * @defaultValue `frontend/generated`\n */\n generatedDir?: URL | string;\n /**\n * The list of extensions that will be collected as routes of the file-based\n * router.\n *\n * @defaultValue `['.tsx', '.jsx', '.ts', '.js']`\n */\n extensions?: readonly string[];\n}>;\n\n/**\n * A Vite plugin that generates a router from the files in the specific directory.\n *\n * @param options - The plugin options.\n * @returns A Vite plugin.\n */\nexport default function vitePluginFileSystemRouter({\n viewsDir = 'frontend/views/',\n generatedDir = 'frontend/generated/',\n extensions = ['.tsx', '.jsx', '.ts', '.js'],\n}: PluginOptions = {}): Plugin {\n const hmrInjectionPattern = /(?<=import\\.meta\\.hot\\.accept[\\s\\S]+)if\\s\\(!nextExports\\)\\s+return;/u;\n\n let _viewsDir: URL;\n let _outDir: URL;\n let _logger: Logger;\n let runtimeUrls: RuntimeFileUrls;\n\n return {\n name: 'vite-plugin-file-router',\n configResolved({ logger, root, build: { outDir } }) {\n const _root = pathToFileURL(root);\n const _generatedDir = new URL(generatedDir, _root);\n\n _viewsDir = new URL(viewsDir, _root);\n _outDir = pathToFileURL(outDir);\n _logger = logger;\n\n _logger.info(`The directory of route files: ${String(_viewsDir)}`);\n _logger.info(`The directory of generated files: ${String(_generatedDir)}`);\n _logger.info(`The output directory: ${String(_outDir)}`);\n\n runtimeUrls = {\n json: new URL('views.json', _outDir),\n code: new URL('views.ts', _generatedDir),\n };\n },\n async buildStart() {\n try {\n await generateRuntimeFiles(_viewsDir, runtimeUrls, extensions, _logger);\n } catch (e: unknown) {\n _logger.error(String(e));\n }\n },\n configureServer(server) {\n const dir = fileURLToPath(_viewsDir);\n\n const changeListener = (file: string): void => {\n if (!file.startsWith(dir)) {\n return;\n }\n\n generateRuntimeFiles(_viewsDir, runtimeUrls, extensions, _logger).catch((e: unknown) =>\n _logger.error(String(e)),\n );\n };\n\n server.watcher.on('add', changeListener);\n server.watcher.on('change', changeListener);\n server.watcher.on('unlink', changeListener);\n },\n transform(code, id): Promise<TransformResult> | TransformResult {\n if (id.startsWith(fileURLToPath(_viewsDir)) && !basename(id).startsWith('_')) {\n // To enable HMR for route files with exported configurations, we need\n // to address a limitation in `react-refresh`. This library requires\n // strict equality (`===`) for non-component exports. However, the\n // dynamic nature of HMR makes maintaining this equality between object\n // literals challenging.\n //\n // To work around this, we implement a strategy that preserves the\n // reference to the original configuration object (`currentExports.config`),\n // replacing any newly created configuration objects (`nextExports.config`)\n // with it. This ensures that the HMR mechanism perceives the\n // configuration as unchanged.\n return {\n code: code.replace(\n hmrInjectionPattern,\n `if (!nextExports) return;\n if (Object.keys(nextExports).length === 2 && 'default' in nextExports && 'config' in nextExports) {\n nextExports = { ...nextExports, config: currentExports.config };\n }`,\n ),\n };\n }\n\n return undefined;\n },\n };\n}\n"],
5
- "mappings": "AAAA,SAAS,gBAAgB;AACzB,SAAS,eAAe,qBAAqB;AAG7C,SAAS,4BAAkD;AAkC5C,SAAR,2BAA4C;AAAA,EACjD,WAAW;AAAA,EACX,eAAe;AAAA,EACf,aAAa,CAAC,QAAQ,QAAQ,OAAO,KAAK;AAC5C,IAAmB,CAAC,GAAW;AAC7B,QAAM,sBAAsB;AAE5B,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,MAAI;AAEJ,SAAO;AAAA,IACL,MAAM;AAAA,IACN,eAAe,EAAE,QAAQ,MAAM,OAAO,EAAE,OAAO,EAAE,GAAG;AAClD,YAAM,QAAQ,cAAc,IAAI;AAChC,YAAM,gBAAgB,IAAI,IAAI,cAAc,KAAK;AAEjD,kBAAY,IAAI,IAAI,UAAU,KAAK;AACnC,gBAAU,cAAc,MAAM;AAC9B,gBAAU;AAEV,cAAQ,KAAK,iCAAiC,OAAO,SAAS,CAAC,EAAE;AACjE,cAAQ,KAAK,qCAAqC,OAAO,aAAa,CAAC,EAAE;AACzE,cAAQ,KAAK,yBAAyB,OAAO,OAAO,CAAC,EAAE;AAEvD,oBAAc;AAAA,QACZ,MAAM,IAAI,IAAI,cAAc,OAAO;AAAA,QACnC,MAAM,IAAI,IAAI,YAAY,aAAa;AAAA,MACzC;AAAA,IACF;AAAA,IACA,MAAM,aAAa;AACjB,UAAI;AACF,cAAM,qBAAqB,WAAW,aAAa,YAAY,OAAO;AAAA,MACxE,SAAS,GAAY;AACnB,gBAAQ,MAAM,OAAO,CAAC,CAAC;AAAA,MACzB;AAAA,IACF;AAAA,IACA,gBAAgB,QAAQ;AACtB,YAAM,MAAM,cAAc,SAAS;AAEnC,YAAM,iBAAiB,CAAC,SAAuB;AAC7C,YAAI,CAAC,KAAK,WAAW,GAAG,GAAG;AACzB;AAAA,QACF;AAEA,6BAAqB,WAAW,aAAa,YAAY,OAAO,EAAE;AAAA,UAAM,CAAC,MACvE,QAAQ,MAAM,OAAO,CAAC,CAAC;AAAA,QACzB;AAAA,MACF;AAEA,aAAO,QAAQ,GAAG,OAAO,cAAc;AACvC,aAAO,QAAQ,GAAG,UAAU,cAAc;AAC1C,aAAO,QAAQ,GAAG,UAAU,cAAc;AAAA,IAC5C;AAAA,IACA,UAAU,MAAM,IAAgD;AAC9D,UAAI,GAAG,WAAW,cAAc,SAAS,CAAC,KAAK,CAAC,SAAS,EAAE,EAAE,WAAW,GAAG,GAAG;AAY5E,eAAO;AAAA,UACL,MAAM,KAAK;AAAA,YACT;AAAA,YACA;AAAA;AAAA;AAAA;AAAA,UAIF;AAAA,QACF;AAAA,MACF;AAEA,aAAO;AAAA,IACT;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { basename } from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\nimport type { TransformResult } from 'rollup';\nimport type { Logger, Plugin } from 'vite';\nimport { generateRuntimeFiles, type RuntimeFileUrls } from './vite-plugin/generateRuntimeFiles.js';\n\n/**\n * The options for the Vite file-based router plugin.\n */\nexport type PluginOptions = Readonly<{\n /**\n * The base directory for the router. The folders and files in this directory\n * will be used as route paths.\n *\n * @defaultValue `frontend/views`\n */\n viewsDir?: URL | string;\n /**\n * The directory where the generated view file will be stored.\n *\n * @defaultValue `frontend/generated`\n */\n generatedDir?: URL | string;\n /**\n * The list of extensions that will be collected as routes of the file-based\n * router.\n *\n * @defaultValue `['.tsx', '.jsx']`\n */\n extensions?: readonly string[];\n /**\n * The flag to indicate whether the plugin is running in development mode.\n *\n * @defaultValue `false`\n */\n isDevMode?: boolean;\n}>;\n\n/**\n * A Vite plugin that generates a router from the files in the specific directory.\n *\n * @param options - The plugin options.\n * @returns A Vite plugin.\n */\nexport default function vitePluginFileSystemRouter({\n viewsDir = 'frontend/views/',\n generatedDir = 'frontend/generated/',\n extensions = ['.tsx', '.jsx'],\n isDevMode = false,\n}: PluginOptions = {}): Plugin {\n const hmrInjectionPattern = /(?<=import\\.meta\\.hot\\.accept[\\s\\S]+)if\\s\\(!nextExports\\)\\s+return;/u;\n\n let _viewsDir: URL;\n let _outDir: URL;\n let _logger: Logger;\n let runtimeUrls: RuntimeFileUrls;\n\n return {\n name: 'vite-plugin-file-router',\n configResolved({ logger, root, build: { outDir } }) {\n const _root = pathToFileURL(root);\n const _generatedDir = new URL(generatedDir, _root);\n\n _viewsDir = new URL(viewsDir, _root);\n _outDir = pathToFileURL(outDir);\n _logger = logger;\n\n _logger.info(`The directory of route files: ${String(_viewsDir)}`);\n _logger.info(`The directory of generated files: ${String(_generatedDir)}`);\n _logger.info(`The output directory: ${String(_outDir)}`);\n\n runtimeUrls = {\n json: new URL('views.json', isDevMode ? _generatedDir : _outDir),\n code: new URL('views.ts', _generatedDir),\n };\n },\n async buildStart() {\n try {\n await generateRuntimeFiles(_viewsDir, runtimeUrls, extensions, _logger);\n } catch (e: unknown) {\n _logger.error(String(e));\n }\n },\n configureServer(server) {\n const dir = fileURLToPath(_viewsDir);\n\n const changeListener = (file: string): void => {\n if (!file.startsWith(dir)) {\n return;\n }\n\n generateRuntimeFiles(_viewsDir, runtimeUrls, extensions, _logger).catch((e: unknown) =>\n _logger.error(String(e)),\n );\n server.hot.send({ type: 'full-reload' });\n };\n\n server.watcher.on('add', changeListener);\n server.watcher.on('change', changeListener);\n server.watcher.on('unlink', changeListener);\n },\n transform(code, id): Promise<TransformResult> | TransformResult {\n if (id.startsWith(fileURLToPath(_viewsDir)) && !basename(id).startsWith('_')) {\n // To enable HMR for route files with exported configurations, we need\n // to address a limitation in `react-refresh`. This library requires\n // strict equality (`===`) for non-component exports. However, the\n // dynamic nature of HMR makes maintaining this equality between object\n // literals challenging.\n //\n // To work around this, we implement a strategy that preserves the\n // reference to the original configuration object (`currentExports.config`),\n // replacing any newly created configuration objects (`nextExports.config`)\n // with it. This ensures that the HMR mechanism perceives the\n // configuration as unchanged.\n return {\n code: code.replace(\n hmrInjectionPattern,\n `if (!nextExports) return;\n if (Object.keys(nextExports).length === 2 && 'default' in nextExports && 'config' in nextExports) {\n nextExports = { ...nextExports, config: currentExports.config };\n }`,\n ),\n };\n }\n\n return undefined;\n },\n };\n}\n"],
5
+ "mappings": "AAAA,SAAS,gBAAgB;AACzB,SAAS,eAAe,qBAAqB;AAG7C,SAAS,4BAAkD;AAwC5C,SAAR,2BAA4C;AAAA,EACjD,WAAW;AAAA,EACX,eAAe;AAAA,EACf,aAAa,CAAC,QAAQ,MAAM;AAAA,EAC5B,YAAY;AACd,IAAmB,CAAC,GAAW;AAC7B,QAAM,sBAAsB;AAE5B,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,MAAI;AAEJ,SAAO;AAAA,IACL,MAAM;AAAA,IACN,eAAe,EAAE,QAAQ,MAAM,OAAO,EAAE,OAAO,EAAE,GAAG;AAClD,YAAM,QAAQ,cAAc,IAAI;AAChC,YAAM,gBAAgB,IAAI,IAAI,cAAc,KAAK;AAEjD,kBAAY,IAAI,IAAI,UAAU,KAAK;AACnC,gBAAU,cAAc,MAAM;AAC9B,gBAAU;AAEV,cAAQ,KAAK,iCAAiC,OAAO,SAAS,CAAC,EAAE;AACjE,cAAQ,KAAK,qCAAqC,OAAO,aAAa,CAAC,EAAE;AACzE,cAAQ,KAAK,yBAAyB,OAAO,OAAO,CAAC,EAAE;AAEvD,oBAAc;AAAA,QACZ,MAAM,IAAI,IAAI,cAAc,YAAY,gBAAgB,OAAO;AAAA,QAC/D,MAAM,IAAI,IAAI,YAAY,aAAa;AAAA,MACzC;AAAA,IACF;AAAA,IACA,MAAM,aAAa;AACjB,UAAI;AACF,cAAM,qBAAqB,WAAW,aAAa,YAAY,OAAO;AAAA,MACxE,SAAS,GAAY;AACnB,gBAAQ,MAAM,OAAO,CAAC,CAAC;AAAA,MACzB;AAAA,IACF;AAAA,IACA,gBAAgB,QAAQ;AACtB,YAAM,MAAM,cAAc,SAAS;AAEnC,YAAM,iBAAiB,CAAC,SAAuB;AAC7C,YAAI,CAAC,KAAK,WAAW,GAAG,GAAG;AACzB;AAAA,QACF;AAEA,6BAAqB,WAAW,aAAa,YAAY,OAAO,EAAE;AAAA,UAAM,CAAC,MACvE,QAAQ,MAAM,OAAO,CAAC,CAAC;AAAA,QACzB;AACA,eAAO,IAAI,KAAK,EAAE,MAAM,cAAc,CAAC;AAAA,MACzC;AAEA,aAAO,QAAQ,GAAG,OAAO,cAAc;AACvC,aAAO,QAAQ,GAAG,UAAU,cAAc;AAC1C,aAAO,QAAQ,GAAG,UAAU,cAAc;AAAA,IAC5C;AAAA,IACA,UAAU,MAAM,IAAgD;AAC9D,UAAI,GAAG,WAAW,cAAc,SAAS,CAAC,KAAK,CAAC,SAAS,EAAE,EAAE,WAAW,GAAG,GAAG;AAY5E,eAAO;AAAA,UACL,MAAM,KAAK;AAAA,YACT;AAAA,YACA;AAAA;AAAA;AAAA;AAAA,UAIF;AAAA,QACF;AAAA,MACF;AAEA,aAAO;AAAA,IACT;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }