Repository: sveltejs/kit
Pull request #4250 (2022-03-06 16:51:01)

use relative asset paths
This was just a quick experiment inspired by #3014 to see if it's possible to create prerendered SvelteKit apps that don't even need a webserver. 

It basically works — if you have routes like `src/routes/index.html.svelte` instead of `index.svelte`, you can double-click on the resulting `build/index.html` file, and you have an (almost) fully working site, because the generated CSS and JS are loaded from `file://` URLs. Might be useful in certain contexts, and is a step towards #595. 

One thing it doesn't account for is fallback routes (i.e. if you visit `/foo/bar/baz` in an SPA and it's served by the fallback page, the relative URLs will be wrong). The biggest issue though is that Vite uses absolute URLs — if you import an asset from JS or CSS, it will be imported from an absolute URL rather than a relative one. I think that could be changed (along with https://github.com/vitejs/vite/issues/2009) for the benefit of all, as it would make every Vite app more portable. Unless and until that happens, I'm going to mark this as draft and not spend any more time on it.

### Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
- [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
- [x] This message body should clearly illustrate what problems it solves.
- [x] Ideally, include a test that fails without this PR but passes with it.

### Tests
- [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` and `pnpm check`

### Changesets
- [ ] If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running `pnpx changeset` and following the prompts. All changesets should be `patch` until SvelteKit 1.0


changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: 9661b21ff858d65dd21afd9e1f82a76d0a62fba4

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/sveltejs/kit/new/relative-assets?filename=.changeset/spicy-plums-chew.md&value=---%0A%22%40sveltejs%2Fkit%22%3A%20patch%0A---%0A%0Ause%20relative%20asset%20paths%0A)


locriacyber: I was able to track it down to `.svelte-kit/output/client/_app/chunks/preload-helper-e4860ae8.js`. Content of the file:
```js
const a = "modulepreload",
  o = {},
  u = "/_app/",
  h = function (s, n) {
    return !n || n.length === 0
      ? s()
      : Promise.all(
          n.map((e) => {
            if (((e = `${u}${e}`), e in o)) return
            o[e] = !0
            const t = e.endsWith(".css"),
              i = t ? '[rel="stylesheet"]' : ""
            if (document.querySelector(`link[href="${e}"]${i}`)) return
            const r = document.createElement("link")
            if (
              ((r.rel = t ? "stylesheet" : a),
              t || ((r.as = "script"), (r.crossOrigin = "")),
              (r.href = e),
              document.head.appendChild(r),
              t)
            )
              return new Promise((l, c) => {
                r.addEventListener("load", l),
                  r.addEventListener("error", () =>
                    c(new Error(`Unable to preload CSS for ${e}`))
                  )
              })
          })
        ).then(() => s())
  }
export { h as _ }
```

Note that "/_app/" was here.

If there is a way to change the way `import(xxxxx)` is compiled, this could be solved.

This was mentioned in https://github.com/vitejs/vite/issues/2024.
Rich-Harris: Ok I think this is ready. The one caveat is that we need to use the user-specified `base` for the SSR build (rather than `./`) because otherwise imported assets will have the wrong URL, which results in things like images 404ing for a split second before hydration kicks in. 

Nothing really changes as a result of this PR (other than apps being more resilient to things like finding themselves on the internet archive, etc), but it hopefully paves the way for #595 etc.
Rich-Harris: > Ok I think this is ready

It's not ready. A weird thing I noticed is that since this change, workers are built to the wrong place. They're _already_ built to the wrong place, but now they're built to a wronger place. If you visit https://kit.svelte.dev/ and inspect the network tab, you'll see this file:

https://kit.svelte.dev/_app/assets/search.0db017ff.js

That's the web worker that powers the search function. It should be in `_app/immutable` but for some reason it's not, it's just in `_app`, which means it won't get properly cached.

As of this PR, it's not even inside `_app` — it's built to `/assets/search.0db017ff.js`. I've tried setting `worker.rollupOptions.output` but it doesn't seem to be having any effect. Still investigating.
Rich-Harris: I believe I've found two bugs in Vite:

1. `worker.rollupOptions.output` is not respected if it's set inside a plugin's `config` hook

2. When building with `base: './'`, workers are instantiated with `new Worker('../../path/to/worker.js')`, rather than with a root-relative path beginning with `base`. But that's wrong — the path will be relative to the _document_, not the module instantiating a worker. I think it should be something like this:
    ```js
    new Worker(new URL('../../path/to/worker.js', import.meta.url).href)
    ```

Will see if I can cobble together a repro later, but cc'ing @bluwy for visibility in the meantime
patak-dev: > 2. When building with `base: './'`, workers are instantiated with `new Worker('../../path/to/worker.js')`, rather than with a root-relative path beginning with `base`. But that's wrong — the path will be relative to the _document_, not the module instantiating the worker. I think it should be something like this:
>    ```js
>    new Worker(new URL('../../path/to/worker.js', import.meta.url).href)
>    ```

This should be fixed in the next patch by:
- https://github.com/vitejs/vite/pull/9204

> 1. `worker.rollupOptions.output` is not respected if it's set inside a plugin's `config` hook

For this one, I'll wait for the repro.
Rich-Harris: thank you, done! https://github.com/vitejs/vite/issues/9205
benmccann: @Rich-Harris both fixes were released as part of Vite 3.0.3
Rich-Harris: I don't think so. This PR still uses an absolute base in SSR mode, as I couldn't figure out an elegant way to use `renderBuiltUrl` to create relative asset paths at SSR time; will leave that problem for another day. Just waiting to see if the windows failure is flakiness or not

Patch:
diff --git a/.changeset/wicked-spoons-leave.md b/.changeset/wicked-spoons-leave.md
new file mode 100644
index 000000000..b83f016ce
--- /dev/null
+++ b/.changeset/wicked-spoons-leave.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Use relative asset paths where possible
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ef527cd88..9f5643fa9 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -48,6 +48,7 @@ You may need to install some dependencies first, e.g. with `npx playwright insta
 If there are tests that fail on the CI, you can retrieve the failed screenshots by going to the summary page of the CI run. You can usually find this by clicking on "Details" of the check results, click "Summary" at the top-left corner, and then scroll to the bottom "Artifacts" section to download the archive.
 
 It is very easy to introduce flakiness in a browser test. If you try to fix the flakiness in a test, you can run it until failure to gain some confidence you've fixed the test with a command like:
+
 ```
 npx playwright test --workers=1 --repeat-each 1000 --max-failures 1 -g "accepts a Request object"
 ```
@@ -60,7 +61,7 @@ If you would like to test local changes to Vite or another dependency, you can b
 {
 	// ...
 	"dependencies": {
-		"vite": "^2.0.0"
+		"vite": "^3.0.0"
 	},
 	"pnpm": {
 		"overrides": {
diff --git a/packages/create-svelte/templates/default/package.json b/packages/create-svelte/templates/default/package.json
index bf0689242..ce127a154 100644
--- a/packages/create-svelte/templates/default/package.json
+++ b/packages/create-svelte/templates/default/package.json
@@ -13,7 +13,7 @@
 		"svelte": "^3.48.0",
 		"svelte-preprocess": "^4.10.6",
 		"typescript": "^4.7.4",
-		"vite": "^3.0.0"
+		"vite": "^3.0.4"
 	},
 	"type": "module",
 	"dependencies": {
diff --git a/packages/create-svelte/templates/default/package.template.json b/packages/create-svelte/templates/default/package.template.json
index 3e51b6a62..d4cc74df2 100644
--- a/packages/create-svelte/templates/default/package.template.json
+++ b/packages/create-svelte/templates/default/package.template.json
@@ -11,7 +11,7 @@
 		"@sveltejs/adapter-auto": "next",
 		"@sveltejs/kit": "next",
 		"svelte": "^3.46.0",
-		"vite": "^3.0.0"
+		"vite": "^3.0.4"
 	},
 	"type": "module",
 	"dependencies": {
diff --git a/packages/create-svelte/templates/skeleton/package.template.json b/packages/create-svelte/templates/skeleton/package.template.json
index 9b213b5b1..0d2623c4e 100644
--- a/packages/create-svelte/templates/skeleton/package.template.json
+++ b/packages/create-svelte/templates/skeleton/package.template.json
@@ -11,7 +11,7 @@
 		"@sveltejs/adapter-auto": "workspace:*",
 		"@sveltejs/kit": "workspace:*",
 		"svelte": "^3.44.0",
-		"vite": "^3.0.0"
+		"vite": "^3.0.4"
 	},
 	"type": "module"
 }
diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js
index 31d9f494a..019229d13 100644
--- a/packages/kit/src/runtime/server/page/render.js
+++ b/packages/kit/src/runtime/server/page/render.js
@@ -150,9 +150,31 @@ export async function render_response({
 
 	const target = hash(body);
 
+	/**
+	 * The prefix to use for static assets. Replaces `%sveltekit.assets%` in the template
+	 * @type {string}
+	 */
+	let assets;
+
+	if (options.paths.assets) {
+		// if an asset path is specified, use it
+		assets = options.paths.assets;
+	} else if (state.prerendering?.fallback) {
+		// if we're creating a fallback page, asset paths need to be root-relative
+		assets = options.paths.base;
+	} else {
+		// otherwise we want asset paths to be relative to the page, so that they
+		// will work in odd contexts like IPFS, the internet archive, and so on
+		const segments = event.url.pathname.slice(options.paths.base.length).split('/').slice(2);
+		assets = segments.length > 0 ? segments.map(() => '..').join('/') : '.';
+	}
+
+	/** @param {string} path */
+	const prefixed = (path) => (path.startsWith('/') ? path : `${assets}/${path}`);
+
 	// prettier-ignore
 	const init_app = `
-		import { set_public_env, start } from ${s(options.prefix + entry.file)};
+		import { set_public_env, start } from ${s(prefixed(entry.file))};
 
 		set_public_env(${s(options.public_env)});
 
@@ -195,7 +217,7 @@ export async function render_response({
 	}
 
 	for (const dep of stylesheets) {
-		const path = options.prefix + dep;
+		const path = prefixed(dep);
 		const attributes = [];
 
 		if (csp.style_needs_nonce) {
@@ -217,7 +239,7 @@ export async function render_response({
 
 	if (page_config.router || page_config.hydrate) {
 		for (const dep of modulepreloads) {
-			const path = options.prefix + dep;
+			const path = prefixed(dep);
 			link_header_preloads.add(`<${encodeURI(path)}>; rel="modulepreload"; nopush`);
 			if (state.prerendering) {
 				head += `\n\t<link rel="modulepreload" href="${path}">`;
@@ -292,10 +314,6 @@ export async function render_response({
 		}
 	}
 
-	const segments = event.url.pathname.slice(options.paths.base.length).split('/').slice(2);
-	const assets =
-		options.paths.assets || (segments.length > 0 ? segments.map(() => '..').join('/') : '.');
-
 	// TODO flush chunks as early as we can
 	const html =
 		(await resolve_opts.transformPageChunk({
diff --git a/packages/kit/src/vite/build/build_server.js b/packages/kit/src/vite/build/build_server.js
index e0a778c5f..7414463b8 100644
--- a/packages/kit/src/vite/build/build_server.js
+++ b/packages/kit/src/vite/build/build_server.js
@@ -69,7 +69,6 @@ export class Server {
 			manifest,
 			method_override: ${s(config.kit.methodOverride)},
 			paths: { base, assets },
-			prefix: assets + '/',
 			prerender: {
 				default: ${config.kit.prerender.default},
 				enabled: ${config.kit.prerender.enabled}
diff --git a/packages/kit/src/vite/build/utils.js b/packages/kit/src/vite/build/utils.js
index 35cdc330a..a1b3da9f8 100644
--- a/packages/kit/src/vite/build/utils.js
+++ b/packages/kit/src/vite/build/utils.js
@@ -88,9 +88,11 @@ export function find_deps(manifest, entry, add_dynamic_css) {
  * @return {import('vite').UserConfig}
  */
 export const get_default_config = function ({ config, input, ssr, outDir }) {
+	const prefix = `${config.kit.appDir}/immutable`;
+
 	return {
 		appType: 'custom',
-		base: assets_base(config.kit),
+		base: ssr ? assets_base(config.kit) : './',
 		build: {
 			cssCodeSplit: true,
 			// don't use the default name to avoid collisions with 'static/manifest.json'
@@ -101,11 +103,9 @@ export const get_default_config = function ({ config, input, ssr, outDir }) {
 				input,
 				output: {
 					format: 'esm',
-					entryFileNames: ssr ? '[name].js' : `${config.kit.appDir}/immutable/[name]-[hash].js`,
-					chunkFileNames: ssr
-						? 'chunks/[name].js'
-						: `${config.kit.appDir}/immutable/chunks/[name]-[hash].js`,
-					assetFileNames: `${config.kit.appDir}/immutable/assets/[name]-[hash][extname]`
+					entryFileNames: ssr ? '[name].js' : `${prefix}/[name]-[hash].js`,
+					chunkFileNames: ssr ? 'chunks/[name].js' : `${prefix}/chunks/[name]-[hash].js`,
+					assetFileNames: `${prefix}/assets/[name]-[hash][extname]`
 				},
 				preserveEntrySignatures: 'strict'
 			},
@@ -125,6 +125,14 @@ export const get_default_config = function ({ config, input, ssr, outDir }) {
 		},
 		ssr: {
 			noExternal: ['@sveltejs/kit']
+		},
+		worker: {
+			rollupOptions: {
+				output: {
+					entryFileNames: `${prefix}/workers/[name]-[hash].js`,
+					chunkFileNames: `${prefix}/workers/chunks/[name]-[hash].js`
+				}
+			}
 		}
 	};
 };
@@ -134,11 +142,7 @@ export const get_default_config = function ({ config, input, ssr, outDir }) {
  * @returns {string}
  */
 export function assets_base(config) {
-	// TODO this is so that Vite's preloading works. Unfortunately, it fails
-	// during `svelte-kit preview`, because we use a local asset path. This
-	// may be fixed in Vite 3: https://github.com/vitejs/vite/issues/2009
-	const { base, assets } = config.paths;
-	return `${assets || base}/`;
+	return config.paths.assets || config.paths.base || './';
 }
 
 const method_names = new Set(['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH']);
diff --git a/packages/kit/src/vite/dev/index.js b/packages/kit/src/vite/dev/index.js
index 0c20b7a91..c67f11fe2 100644
--- a/packages/kit/src/vite/dev/index.js
+++ b/packages/kit/src/vite/dev/index.js
@@ -432,7 +432,6 @@ export async function dev(vite, vite_config, svelte_config, illegal_imports) {
 							base: svelte_config.kit.paths.base,
 							assets
 						},
-						prefix: '',
 						prerender: {
 							default: svelte_config.kit.prerender.default,
 							enabled: svelte_config.kit.prerender.enabled
diff --git a/packages/kit/src/vite/index.js b/packages/kit/src/vite/index.js
index 70a9cf482..a5ef0f32e 100644
--- a/packages/kit/src/vite/index.js
+++ b/packages/kit/src/vite/index.js
@@ -207,7 +207,7 @@ function kit() {
 			paths = {
 				build_dir: `${svelte_config.kit.outDir}/build`,
 				output_dir: `${svelte_config.kit.outDir}/output`,
-				client_out_dir: `${svelte_config.kit.outDir}/output/client/`
+				client_out_dir: `${svelte_config.kit.outDir}/output/client`
 			};
 
 			illegal_imports = new Set([
@@ -230,7 +230,7 @@ function kit() {
 			/** @type {import('vite').UserConfig} */
 			const result = {
 				appType: 'custom',
-				base: '/',
+				base: './',
 				build: {
 					rollupOptions: {
 						// Vite dependency crawler needs an explicit JS entry point
diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts
index a9938d281..9738ab5a0 100644
--- a/packages/kit/types/internal.d.ts
+++ b/packages/kit/types/internal.d.ts
@@ -239,7 +239,6 @@ export interface SSROptions {
 		base: string;
 		assets: string;
 	};
-	prefix: string;
 	prerender: {
 		default: boolean;
 		enabled: boolean;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d13023cd9..92e3c0431 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -247,7 +247,7 @@ importers:
       svelte: ^3.48.0
       svelte-preprocess: ^4.10.6
       typescript: ^4.7.4
-      vite: ^3.0.0
+      vite: ^3.0.4
     dependencies:
       '@fontsource/fira-mono': 4.5.8
       '@lukeed/uuid': 2.0.0
@@ -556,7 +556,7 @@ importers:
       shiki-twoslash: ^3.0.2
       svelte: ^3.48.0
       typescript: ^4.7.4
-      vite: ^3.0.0
+      vite: ^3.0.4
       vite-imagetools: ^4.0.3
     devDependencies:
       '@sveltejs/adapter-auto': link:../../packages/adapter-auto
diff --git a/sites/kit.svelte.dev/package.json b/sites/kit.svelte.dev/package.json
index 616f17709..5be7b5e44 100644
--- a/sites/kit.svelte.dev/package.json
+++ b/sites/kit.svelte.dev/package.json
@@ -21,7 +21,7 @@
 		"shiki-twoslash": "^3.0.2",
 		"svelte": "^3.48.0",
 		"typescript": "^4.7.4",
-		"vite": "^3.0.0",
+		"vite": "^3.0.4",
 		"vite-imagetools": "^4.0.3"
 	},
 	"type": "module"


Test patch:
diff --git a/packages/adapter-static/test/apps/spa/src/routes/fallback/[...rest]/+page.svelte b/packages/adapter-static/test/apps/spa/src/routes/fallback/[...rest]/+page.svelte
new file mode 100644
index 000000000..671b43ec7
--- /dev/null
+++ b/packages/adapter-static/test/apps/spa/src/routes/fallback/[...rest]/+page.svelte
@@ -0,0 +1,1 @@
+<h1>the fallback page was rendered</h1>
diff --git a/packages/adapter-static/test/test.js b/packages/adapter-static/test/test.js
index ffc1f4072..97bef6e5d 100644
--- a/packages/adapter-static/test/test.js
+++ b/packages/adapter-static/test/test.js
@@ -14,8 +14,11 @@ run('prerendered', (test) => {
 });
 
 run('spa', (test) => {
-	test('generates a fallback page', ({ cwd }) => {
+	test('generates a fallback page', async ({ base, cwd, page }) => {
 		assert.ok(fs.existsSync(`${cwd}/build/200.html`));
+
+		await page.goto(`${base}/fallback/a/b/c`);
+		assert.equal(await page.textContent('h1'), 'the fallback page was rendered');
 	});
 
 	test('does not prerender pages without prerender=true', ({ cwd }) => {