From 257b5d0ed94425ff9f9e6429a0f1326e19bf3ebe Mon Sep 17 00:00:00 2001 From: Dylan Piercey Date: Thu, 16 Nov 2023 20:21:39 -0700 Subject: [PATCH] feat: relative assets transform (#109) --- .changeset/sharp-guests-know.md | 5 ++ README.md | 15 ++++ package-lock.json | 44 ++++++------ package.json | 6 +- .../build.expected.loading.0.html | 13 ++++ .../build.expected.loading.1.html | 13 ++++ .../build.expected.step-0.0.html | 13 ++++ .../__snapshots__/dev.expected.loading.0.html | 13 ++++ .../__snapshots__/dev.expected.loading.1.html | 13 ++++ .../__snapshots__/dev.expected.step-0.0.html | 13 ++++ .../dev-server.mjs | 38 ++++++++++ .../server.mjs | 18 +++++ .../src/components/class-component.marko | 22 ++++++ .../src/components/implicit-component.marko | 9 +++ .../src/components/layout-component.marko | 15 ++++ .../src/components/logo.svg | 72 +++++++++++++++++++ .../src/index.js | 9 +++ .../src/template.marko | 9 +++ .../test.config.ts | 4 ++ .../src/components/class-component.marko | 2 +- src/__tests__/main.test.ts | 2 +- src/index.ts | 2 + src/relative-assets-transform.ts | 62 ++++++++++++++++ 23 files changed, 385 insertions(+), 27 deletions(-) create mode 100644 .changeset/sharp-guests-know.md create mode 100644 src/__tests__/fixtures/isomorphic-relative-asset-import/__snapshots__/build.expected.loading.0.html create mode 100644 src/__tests__/fixtures/isomorphic-relative-asset-import/__snapshots__/build.expected.loading.1.html create mode 100644 src/__tests__/fixtures/isomorphic-relative-asset-import/__snapshots__/build.expected.step-0.0.html create mode 100644 src/__tests__/fixtures/isomorphic-relative-asset-import/__snapshots__/dev.expected.loading.0.html create mode 100644 src/__tests__/fixtures/isomorphic-relative-asset-import/__snapshots__/dev.expected.loading.1.html create mode 100644 src/__tests__/fixtures/isomorphic-relative-asset-import/__snapshots__/dev.expected.step-0.0.html create mode 100644 src/__tests__/fixtures/isomorphic-relative-asset-import/dev-server.mjs create mode 100644 src/__tests__/fixtures/isomorphic-relative-asset-import/server.mjs create mode 100644 src/__tests__/fixtures/isomorphic-relative-asset-import/src/components/class-component.marko create mode 100644 src/__tests__/fixtures/isomorphic-relative-asset-import/src/components/implicit-component.marko create mode 100644 src/__tests__/fixtures/isomorphic-relative-asset-import/src/components/layout-component.marko create mode 100644 src/__tests__/fixtures/isomorphic-relative-asset-import/src/components/logo.svg create mode 100644 src/__tests__/fixtures/isomorphic-relative-asset-import/src/index.js create mode 100644 src/__tests__/fixtures/isomorphic-relative-asset-import/src/template.marko create mode 100644 src/__tests__/fixtures/isomorphic-relative-asset-import/test.config.ts create mode 100644 src/relative-assets-transform.ts diff --git a/.changeset/sharp-guests-know.md b/.changeset/sharp-guests-know.md new file mode 100644 index 0000000..0568cf4 --- /dev/null +++ b/.changeset/sharp-guests-know.md @@ -0,0 +1,5 @@ +--- +"@marko/vite": minor +--- + +Support inline relative asset paths from native tags. diff --git a/README.md b/README.md index 78ca24e..6f26f21 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,21 @@ export default defineConfig({ }); ``` +# Browser asset references + +With @marko/vite when a _static relative path_ is used for certain native tag attributes, the relative asset will be imported and processed by Vite. + +As an example, with the following template, the `logo.svg` will be imported and processed as if it was a `import` at the root of the file. + +``` + + +// Would produce a Vite processed asset and update the src, eg with the following output + +``` + +You can see the list of elements and their attributes which are processed [here](./src/relative-assets-transform.ts). + # Linked Mode By default this plugin operates in `linked` mode (you can disabled this by passing [`linked: false` as an option](#options.linked)). In `linked` mode the plugin automatically discovers all of the entry `.marko` files while compiling the server, and tells `Vite` which modules to load in the browser. diff --git a/package-lock.json b/package-lock.json index d939ede..048c3a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "@types/babel__core": "^7.20.4", "@types/jsdom": "^21.1.5", "@types/mocha": "^10.0.4", - "@types/node": "^20.9.0", + "@types/node": "^20.9.1", "@types/resolve": "^1.20.5", "@types/serve-handler": "^6.1.4", "@typescript-eslint/eslint-plugin": "^6.11.0", @@ -42,12 +42,12 @@ "mocha": "^10.2.0", "mocha-snap": "^5.0.0", "nyc": "^15.1.0", - "playwright": "^1.39.0", + "playwright": "^1.40.0", "prettier": "^3.1.0", "serve-handler": "^6.1.5", "tsx": "^4.1.2", "typescript": "^5.2.2", - "vite": "^5.0.0-beta.17" + "vite": "^5.0.0" }, "peerDependencies": { "@marko/compiler": "^5", @@ -1981,9 +1981,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz", - "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==", + "version": "20.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.1.tgz", + "integrity": "sha512-HhmzZh5LSJNS5O8jQKpJ/3ZcrrlG6L70hpGqMIAoM9YVD0YBRNWYsfwcXq8VnSjlNpCpgLzMXdiPo+dxcvSmiA==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -3470,9 +3470,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.582", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.582.tgz", - "integrity": "sha512-89o0MGoocwYbzqUUjc+VNpeOFSOK9nIdC5wY4N+PVUarUK0MtjyTjks75AZS2bW4Kl8MdewdFsWaH0jLy+JNoA==", + "version": "1.4.586", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.586.tgz", + "integrity": "sha512-qMa+E6yf1fNQbg3G66pHLXeJUP5CCCzNat1VPczOZOqgI2w4u+8y9sQnswMdGs5m4C1rOePq37EVBr/nsPQY7w==", "dev": true }, "node_modules/emoji-regex": { @@ -4955,9 +4955,9 @@ } }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", "dev": true, "engines": { "node": ">= 4" @@ -7221,12 +7221,12 @@ } }, "node_modules/playwright": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz", - "integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==", + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.0.tgz", + "integrity": "sha512-gyHAgQjiDf1m34Xpwzaqb76KgfzYrhK7iih+2IzcOCoZWr/8ZqmdBw+t0RU85ZmfJMgtgAiNtBQ/KS2325INXw==", "dev": true, "dependencies": { - "playwright-core": "1.39.0" + "playwright-core": "1.40.0" }, "bin": { "playwright": "cli.js" @@ -7239,9 +7239,9 @@ } }, "node_modules/playwright-core": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz", - "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==", + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.0.tgz", + "integrity": "sha512-fvKewVJpGeca8t0ipM56jkVSU6Eo0RmFvQ/MaCQNDYm+sdvKkMBBWTE1FdeMqIdumRaXXjZChWHvIzCGM/tA/Q==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -9677,9 +9677,9 @@ } }, "node_modules/vite": { - "version": "5.0.0-beta.19", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.0-beta.19.tgz", - "integrity": "sha512-Huoj7XUlkhSLHhIOf4FgDrxmHJMKgfvG9ocB4kJmTKSeWfLgHIQ86xYC8+eA/RBxFo9zRQXX81VUgW8l7Wri3Q==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.0.tgz", + "integrity": "sha512-ESJVM59mdyGpsiNAeHQOR/0fqNoOyWPYesFto8FFZugfmhdHx8Fzd8sF3Q/xkVhZsyOxHfdM7ieiVAorI9RjFw==", "dev": true, "dependencies": { "esbuild": "^0.19.3", diff --git a/package.json b/package.json index f404c68..c915903 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@types/babel__core": "^7.20.4", "@types/jsdom": "^21.1.5", "@types/mocha": "^10.0.4", - "@types/node": "^20.9.0", + "@types/node": "^20.9.1", "@types/resolve": "^1.20.5", "@types/serve-handler": "^6.1.4", "@typescript-eslint/eslint-plugin": "^6.11.0", @@ -38,12 +38,12 @@ "mocha": "^10.2.0", "mocha-snap": "^5.0.0", "nyc": "^15.1.0", - "playwright": "^1.39.0", + "playwright": "^1.40.0", "prettier": "^3.1.0", "serve-handler": "^6.1.5", "tsx": "^4.1.2", "typescript": "^5.2.2", - "vite": "^5.0.0-beta.17" + "vite": "^5.0.0" }, "files": [ "dist", diff --git a/src/__tests__/fixtures/isomorphic-relative-asset-import/__snapshots__/build.expected.loading.0.html b/src/__tests__/fixtures/isomorphic-relative-asset-import/__snapshots__/build.expected.loading.0.html new file mode 100644 index 0000000..e415c58 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-relative-asset-import/__snapshots__/build.expected.loading.0.html @@ -0,0 +1,13 @@ +
+
+ Mounted: false Clicks: 0 + logo +
+
\ No newline at end of file diff --git a/src/__tests__/fixtures/isomorphic-relative-asset-import/__snapshots__/build.expected.loading.1.html b/src/__tests__/fixtures/isomorphic-relative-asset-import/__snapshots__/build.expected.loading.1.html new file mode 100644 index 0000000..bf208a0 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-relative-asset-import/__snapshots__/build.expected.loading.1.html @@ -0,0 +1,13 @@ +
+
+ Mounted: true Clicks: 0 + logo +
+
\ No newline at end of file diff --git a/src/__tests__/fixtures/isomorphic-relative-asset-import/__snapshots__/build.expected.step-0.0.html b/src/__tests__/fixtures/isomorphic-relative-asset-import/__snapshots__/build.expected.step-0.0.html new file mode 100644 index 0000000..b061209 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-relative-asset-import/__snapshots__/build.expected.step-0.0.html @@ -0,0 +1,13 @@ +
+
+ Mounted: true Clicks: 1 + logo +
+
\ No newline at end of file diff --git a/src/__tests__/fixtures/isomorphic-relative-asset-import/__snapshots__/dev.expected.loading.0.html b/src/__tests__/fixtures/isomorphic-relative-asset-import/__snapshots__/dev.expected.loading.0.html new file mode 100644 index 0000000..88fc941 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-relative-asset-import/__snapshots__/dev.expected.loading.0.html @@ -0,0 +1,13 @@ +
+
+ Mounted: false Clicks: 0 + logo +
+
\ No newline at end of file diff --git a/src/__tests__/fixtures/isomorphic-relative-asset-import/__snapshots__/dev.expected.loading.1.html b/src/__tests__/fixtures/isomorphic-relative-asset-import/__snapshots__/dev.expected.loading.1.html new file mode 100644 index 0000000..8e906ce --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-relative-asset-import/__snapshots__/dev.expected.loading.1.html @@ -0,0 +1,13 @@ +
+
+ Mounted: true Clicks: 0 + logo +
+
\ No newline at end of file diff --git a/src/__tests__/fixtures/isomorphic-relative-asset-import/__snapshots__/dev.expected.step-0.0.html b/src/__tests__/fixtures/isomorphic-relative-asset-import/__snapshots__/dev.expected.step-0.0.html new file mode 100644 index 0000000..90baa51 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-relative-asset-import/__snapshots__/dev.expected.step-0.0.html @@ -0,0 +1,13 @@ +
+
+ Mounted: true Clicks: 1 + logo +
+
\ No newline at end of file diff --git a/src/__tests__/fixtures/isomorphic-relative-asset-import/dev-server.mjs b/src/__tests__/fixtures/isomorphic-relative-asset-import/dev-server.mjs new file mode 100644 index 0000000..64b889d --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-relative-asset-import/dev-server.mjs @@ -0,0 +1,38 @@ +// In dev we'll start a Vite dev server in middleware mode, +// and forward requests to our http request handler. + +import { createServer } from "vite"; +import path from "path"; +import url from "url"; +import { createRequire } from "module"; + +// change to import once marko-vite is updated to ESM +const markoPlugin = createRequire(import.meta.url)("../../..").default; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); + +const devServer = await createServer({ + root: __dirname, + appType: "custom", + logLevel: "silent", + plugins: [markoPlugin()], + optimizeDeps: { force: true }, + server: { + middlewareMode: true, + watch: { + ignored: ["**/node_modules/**", "**/dist/**", "**/__snapshots__/**"], + }, + }, +}); + +export default devServer.middlewares.use(async (req, res, next) => { + try { + const { handler } = await devServer.ssrLoadModule( + path.join(__dirname, "./src/index.js") + ); + await handler(req, res, next); + } catch (err) { + devServer.ssrFixStacktrace(err); + return next(err); + } +}); diff --git a/src/__tests__/fixtures/isomorphic-relative-asset-import/server.mjs b/src/__tests__/fixtures/isomorphic-relative-asset-import/server.mjs new file mode 100644 index 0000000..7f0d142 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-relative-asset-import/server.mjs @@ -0,0 +1,18 @@ +// In production, simply start up the http server. +import path from 'path' +import url from 'url'; +import { createServer } from "http"; +import serve from "serve-handler"; + +globalThis.assetsPath = "/my-prefix/"; +// dyanmic import so globalThis.assetsPath can be set prior to the imported code executing +const { handler } = await import("./dist/index.mjs"); + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); +const serveOpts = { public: path.resolve(__dirname, "dist") }; + +export default createServer(async (req, res) => { + await handler(req, res); + if (res.headersSent) return; + await serve(req, res, serveOpts); +}); diff --git a/src/__tests__/fixtures/isomorphic-relative-asset-import/src/components/class-component.marko b/src/__tests__/fixtures/isomorphic-relative-asset-import/src/components/class-component.marko new file mode 100644 index 0000000..bc17cc9 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-relative-asset-import/src/components/class-component.marko @@ -0,0 +1,22 @@ +class { + onCreate() { + this.state = { + clickCount: 0, + mounted: false + }; + } + onMount() { + this.state.mounted = true; + } + + handleClick() { + this.state.clickCount++; + } +} + + + Mounted: ${state.mounted} + Clicks: ${state.clickCount} + + logo + diff --git a/src/__tests__/fixtures/isomorphic-relative-asset-import/src/components/implicit-component.marko b/src/__tests__/fixtures/isomorphic-relative-asset-import/src/components/implicit-component.marko new file mode 100644 index 0000000..7d8310b --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-relative-asset-import/src/components/implicit-component.marko @@ -0,0 +1,9 @@ +static { + if (typeof window === "object") { + document.body.firstElementChild.append("Loaded Implicit Component"); + } +} + + + + \ No newline at end of file diff --git a/src/__tests__/fixtures/isomorphic-relative-asset-import/src/components/layout-component.marko b/src/__tests__/fixtures/isomorphic-relative-asset-import/src/components/layout-component.marko new file mode 100644 index 0000000..8d60022 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-relative-asset-import/src/components/layout-component.marko @@ -0,0 +1,15 @@ +static { + if (typeof window === "object") { + document.body.firstElementChild.append("Loaded Layout Component"); + } +} + + + + + Hello World + + + <${input.renderBody}/> + + \ No newline at end of file diff --git a/src/__tests__/fixtures/isomorphic-relative-asset-import/src/components/logo.svg b/src/__tests__/fixtures/isomorphic-relative-asset-import/src/components/logo.svg new file mode 100644 index 0000000..7be4302 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-relative-asset-import/src/components/logo.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/__tests__/fixtures/isomorphic-relative-asset-import/src/index.js b/src/__tests__/fixtures/isomorphic-relative-asset-import/src/index.js new file mode 100644 index 0000000..d3f5422 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-relative-asset-import/src/index.js @@ -0,0 +1,9 @@ +import template from "./template.marko"; + +export function handler(req, res) { + if (req.url === "/") { + res.statusCode = 200; + res.setHeader("Content-Type", "text/html; charset=utf-8"); + template.render({}, res); + } +} diff --git a/src/__tests__/fixtures/isomorphic-relative-asset-import/src/template.marko b/src/__tests__/fixtures/isomorphic-relative-asset-import/src/template.marko new file mode 100644 index 0000000..2c5af41 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-relative-asset-import/src/template.marko @@ -0,0 +1,9 @@ +style { + div { color: green } +} + + + + + + \ No newline at end of file diff --git a/src/__tests__/fixtures/isomorphic-relative-asset-import/test.config.ts b/src/__tests__/fixtures/isomorphic-relative-asset-import/test.config.ts new file mode 100644 index 0000000..9e9a321 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-relative-asset-import/test.config.ts @@ -0,0 +1,4 @@ +export const ssr = true; +export async function steps() { + await page.click("#clickable"); +} diff --git a/src/__tests__/fixtures/isomorphic-runtime-base-path/src/components/class-component.marko b/src/__tests__/fixtures/isomorphic-runtime-base-path/src/components/class-component.marko index 65914b5..a70245f 100644 --- a/src/__tests__/fixtures/isomorphic-runtime-base-path/src/components/class-component.marko +++ b/src/__tests__/fixtures/isomorphic-runtime-base-path/src/components/class-component.marko @@ -20,6 +20,6 @@ class { Mounted: ${state.mounted} Clicks: ${state.clickCount} - LOGO_PATH: ${logo.replace(/-[a-z0-9]+(\.\w+)$/i, '-[hash]$1')} + LOGO_PATH: ${logo} ENV: ${import.meta.env.BASE_URL} diff --git a/src/__tests__/main.test.ts b/src/__tests__/main.test.ts index cbe508c..1fd8cef 100644 --- a/src/__tests__/main.test.ts +++ b/src/__tests__/main.test.ts @@ -51,7 +51,7 @@ before(async () => { context.exposeFunction("__track__", (html: string) => { const formatted = defaultSerializer( defaultNormalizer(JSDOM.fragment(html)), - ); + ).replace(/-[a-z0-9]+(\.\w+)/i, "-[hash]$1"); if (changes.at(-1) !== formatted) { changes.push(formatted); diff --git a/src/index.ts b/src/index.ts index dcb03a4..be8d337 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,6 +22,7 @@ import { renderAssetsRuntimeId, } from "./render-assets-runtime"; import renderAssetsTransform from "./render-assets-transform"; +import relativeAssetsTransform from "./relative-assets-transform"; import { ReadOncePersistedStore } from "./read-once-persisted-store"; export namespace API { @@ -258,6 +259,7 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { compiler.taglib.register("@marko/vite", { "": { transformer: renderAssetsTransform }, "": { transformer: renderAssetsTransform }, + "<*>": { transformer: relativeAssetsTransform }, }); } diff --git a/src/relative-assets-transform.ts b/src/relative-assets-transform.ts new file mode 100644 index 0000000..ca6ecaf --- /dev/null +++ b/src/relative-assets-transform.ts @@ -0,0 +1,62 @@ +import type { types } from "@marko/compiler"; +const attrSrc = new Set(["src"]); +const attrHref = new Set(["href"]); +const assetAttrsByTag = new Map([ + ["audio", attrSrc], + ["embed", attrSrc], + ["iframe", attrSrc], + ["img", new Set(["src", "srcset"])], + ["input", attrSrc], + ["source", attrSrc], + ["track", attrSrc], + ["video", new Set(["src", "poster"])], + ["a", attrHref], + ["area", attrHref], + ["link", attrHref], + ["object", new Set(["data"])], + ["body", new Set(["background"])], +]); + +export default function transform( + tag: types.NodePath, + t: typeof types, +) { + const { name, attributes } = tag.node; + if (name.type !== "StringLiteral") { + return; + } + + const assetAttrs = assetAttrsByTag.get(name.value); + if (!assetAttrs) { + return; + } + + for (const attr of attributes) { + if ( + attr.type === "MarkoAttribute" && + attr.value.type === "StringLiteral" && + assetAttrs.has(attr.name) + ) { + const { value } = attr.value; + if ( + !( + ( + value[0] === "/" || // Ignore absolute paths. + !/\.[^.]+$/.test(value) || // Ignore paths without a file extension. + /^[a-z]{2,}:/i.test(value) + ) // Ignore paths with a protocol. + ) + ) { + const importedId = tag.scope.generateUid(value); + attr.value = t.identifier(importedId); + tag.hub.file.path.unshiftContainer( + "body", + t.importDeclaration( + [t.importDefaultSpecifier(t.identifier(importedId))], + t.stringLiteral(value), + ), + ); + } + } + } +}