@bakery-framework/plugin-vue 2.1.0 → 2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bakery-framework/plugin-vue",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "Bakery vue plugin.",
5
5
  "keywords": [
6
6
  "bakery",
@@ -43,7 +43,7 @@
43
43
  "vue": "^3.5.38"
44
44
  },
45
45
  "dependencies": {
46
- "@bakery-framework/core": "^2.1.0"
46
+ "@bakery-framework/core": "^2.1.1"
47
47
  },
48
48
  "engines": {
49
49
  "bun": ">=1.4.0"
package/src/handler.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  JsonResponseData,
16
16
  response,
17
17
  toHash,
18
+ Try,
18
19
  } from '@bakery-framework/core/utils'
19
20
  import { ETag, injectIfHtml } from '@bakery-framework/core/utils/http'
20
21
 
@@ -44,6 +45,7 @@ import {
44
45
  parseVueMeta,
45
46
  RX_EXPORT_BRACE,
46
47
  RX_EXPORT_HANGING,
48
+ absolutizeRelativeImports,
47
49
  rewriteVueImports,
48
50
  VUE_SERVER_DATA_TOKEN,
49
51
  } from './utils'
@@ -52,6 +54,16 @@ function normalizePath(path: string) {
52
54
  return path.endsWith('.js') ? path.slice(0, -3) : path
53
55
  }
54
56
 
57
+ /**
58
+ * Whether a request names the route's file itself, rather than reaching it
59
+ * through a route parameter. Decoded, because an import of a bracketed file
60
+ * arrives percent-encoded (`absolutizeRelativeImports`).
61
+ */
62
+ function requestNamesFile(path: string, routePath: string): boolean {
63
+ const requested = Try.return(() => decodeURIComponent(path), path)
64
+ return requested === routePath.replace(/\\/g, '/')
65
+ }
66
+
55
67
  const RX_SERVER_DATA_TOKEN = new RegExp(`\\b${VUE_SERVER_DATA_TOKEN}\\b`, 'g')
56
68
 
57
69
  /**
@@ -434,6 +446,10 @@ export class VueHandler extends DynamicHandler {
434
446
  if (code.includes('.vue"') || code.includes(".vue'")) {
435
447
  code = rewriteVueImports(code)
436
448
  }
449
+ // After the `.vue` rewrite, which only appends a query and leaves the
450
+ // specifier relative. See `absolutizeRelativeImports` for why relative
451
+ // imports cannot survive into the browser.
452
+ code = absolutizeRelativeImports(code, routePath)
437
453
 
438
454
  if (serverDataReplacement !== undefined) {
439
455
  code = code.replace(RX_SERVER_DATA_TOKEN, () => serverDataReplacement)
@@ -726,6 +742,19 @@ async function sharedHandler(
726
742
  accept.includes('text/javascript') ||
727
743
  req.headers.get('sec-fetch-dest') === 'script'
728
744
 
745
+ // An import of some other file that reached this page only through a route
746
+ // parameter: `/admin/faculty/pages/dashboard.vue?__vue_script=module`
747
+ // matched by `admin/[...slug].vue`. It was answered with this page's own
748
+ // script, a 200 the browser took for the module it asked for, and that is
749
+ // what kept a whole class of wrong import URLs out of sight. Refused now.
750
+ // The page's own root script and stylesheet still answer at its URL: that
751
+ // is where 2.0.4 links them (`__vue_script=root`, `__vue_css`).
752
+ const isImport =
753
+ vueScriptParam === 'module' || (vueScriptParam === null && isScript)
754
+ if (isImport && !requestNamesFile(path, routePath)) {
755
+ return response.error('Not Found', 404)
756
+ }
757
+
729
758
  const parsed = await VueHandler.parseVueFile(
730
759
  id,
731
760
  diskFile,
package/src/utils.ts CHANGED
@@ -635,6 +635,35 @@ export async function getServerResponse(options: ServerResponseOptions) {
635
635
  }
636
636
  }
637
637
 
638
+ /** A relative specifier in a static import, an export-from or a dynamic import. */
639
+ const RX_RELATIVE_SPECIFIER =
640
+ /((?:\bfrom|\bimport)\s*\(?\s*)(['"])(\.{1,2}\/[^'"]*)\2/g
641
+
642
+ /**
643
+ * Relative specifiers made absolute from the file's own place under the
644
+ * serve root, so a script resolves the same however deep the URL it was
645
+ * served from.
646
+ *
647
+ * A browser resolves a relative import against the importing script's URL.
648
+ * Since 2.0.4 a dynamic page's root script is served at the page's URL, which
649
+ * shares the source file's directory only one segment deep: at
650
+ * `/admin/faculty/add` the root script of `admin/[...slug].vue` resolved
651
+ * `../shared/usePath` to `/admin/shared/usePath`. Absolute, every file has one
652
+ * URL, which is also what keeps a module from loading twice under two names.
653
+ *
654
+ * Brackets are percent-encoded: a requested name is read as a glob, where a
655
+ * bare `[id]` is a character class and never matches the file of that name.
656
+ */
657
+ export function absolutizeRelativeImports(code: string, routePath: string): string {
658
+ const base = `http://bakery.invalid${routePath.replace(/\\/g, '/')}`
659
+ return code.replace(RX_RELATIVE_SPECIFIER, (match, lead: string, quote: string, spec: string) => {
660
+ const url = Try.return(() => new URL(spec, base), null)
661
+ if (!url) return match
662
+ const path = url.pathname.replaceAll('[', '%5B').replaceAll(']', '%5D')
663
+ return `${lead}${quote}${path}${url.search}${url.hash}${quote}`
664
+ })
665
+ }
666
+
638
667
  export function rewriteVueImports(code: string): string {
639
668
  return code.replace(
640
669
  RX_IMPORT_VUE_FILE,