diff --git a/packages/vite/src/node/server/middlewares/transform.ts b/packages/vite/src/node/server/middlewares/transform.ts index 345771162b6bcc..ce5a39105ee670 100644 --- a/packages/vite/src/node/server/middlewares/transform.ts +++ b/packages/vite/src/node/server/middlewares/transform.ts @@ -62,7 +62,10 @@ export function isServerAccessDeniedForTransform( id: string, ): boolean { if (rawRE.test(id) || urlRE.test(id) || inlineRE.test(id) || svgRE.test(id)) { - return checkLoadingAccess(config, id) !== 'allowed' + return ( + checkLoadingAccess(config, cleanUrl(id)) !== 'allowed' || + checkLoadingAccess(config, id) !== 'allowed' + ) } return false } @@ -319,7 +322,13 @@ export function transformMiddleware( } if (e?.code === ERR_DENIED_ID) { const id: string = e.id - const servingAccessResult = checkLoadingAccess(server.config, id) + let servingAccessResult = checkLoadingAccess( + server.config, + cleanUrl(id), + ) + if (servingAccessResult === 'allowed') { + servingAccessResult = checkLoadingAccess(server.config, id) + } if (servingAccessResult === 'denied') { respondWithAccessDenied(id, server, res) return true diff --git a/playground/fs-serve/__tests__/commonTests.ts b/playground/fs-serve/__tests__/commonTests.ts index cbc8d84925c256..06c58fae6de370 100644 --- a/playground/fs-serve/__tests__/commonTests.ts +++ b/playground/fs-serve/__tests__/commonTests.ts @@ -1,5 +1,6 @@ import http from 'node:http' import path from 'node:path' +import fs from 'node:fs' import { pathToFileURL } from 'node:url' import { setTimeout } from 'node:timers/promises' import { @@ -21,13 +22,17 @@ const getViteTestIndexHtmlUrl = () => { return viteTestUrl + srcPrefix + 'src/' } +const safeJsonContent = fs.readFileSync( + path.resolve(import.meta.dirname, '../safe.json'), + 'utf-8', +) const stringified = JSON.stringify(testJSON) -describe.runIf(isServe)('main', () => { - beforeAll(async () => { - await page.goto(getViteTestIndexHtmlUrl()) - }) +beforeAll(async () => { + await page.goto(getViteTestIndexHtmlUrl()) +}) +describe.runIf(isServe)('normal', () => { test('default import', async () => { await expect.poll(() => page.textContent('.full')).toBe(stringified) }) @@ -36,208 +41,264 @@ describe.runIf(isServe)('main', () => { await expect.poll(() => page.textContent('.named')).toBe(testJSON.msg) }) - test('virtual svg module', async () => { - await expect.poll(() => page.textContent('.virtual-svg')).toMatch(' { - await expect.poll(() => page.textContent('.safe-fetch')).toMatch('KEY=safe') - await expect.poll(() => page.textContent('.safe-fetch-status')).toBe('200') - }) - - test('safe fetch with query', async () => { - await expect - .poll(() => page.textContent('.safe-fetch-query')) - .toMatch('KEY=safe') - await expect - .poll(() => page.textContent('.safe-fetch-query-status')) - .toBe('200') - }) - - test('safe fetch with special characters', async () => { - await expect - .poll(() => page.textContent('.safe-fetch-subdir-special-characters')) - .toMatch('KEY=safe') - await expect - .poll(() => - page.textContent('.safe-fetch-subdir-special-characters-status'), - ) - .toBe('200') - }) - - test('unsafe fetch', async () => { - await expect - .poll(() => page.textContent('.unsafe-fetch')) - .toMatch('403 Restricted') - await expect - .poll(() => page.textContent('.unsafe-fetch-status')) - .toBe('403') - }) - - test('unsafe HTML fetch', async () => { - await expect - .poll(() => page.textContent('.unsafe-fetch-html')) - .toMatch('403 Restricted') - await expect - .poll(() => page.textContent('.unsafe-fetch-html-status')) - .toBe('403') - }) - - test('unsafe fetch with special characters (#8498)', async () => { - await expect.poll(() => page.textContent('.unsafe-fetch-8498')).toBe('') - await expect - .poll(() => page.textContent('.unsafe-fetch-8498-status')) - .toBe('404') - }) - - test('unsafe fetch with special characters 2 (#8498)', async () => { - await expect.poll(() => page.textContent('.unsafe-fetch-8498-2')).toBe('') - await expect - .poll(() => page.textContent('.unsafe-fetch-8498-2-status')) - .toBe('404') - }) - - test('unsafe fetch import inline', async () => { - await expect - .poll(() => page.textContent('.unsafe-fetch-import-inline-status')) - .toBe('403') - }) - - test('unsafe fetch raw query import', async () => { - await expect - .poll(() => page.textContent('.unsafe-fetch-raw-query-import-status')) - .toBe('403') - }) - - test('unsafe fetch ?.svg?import', async () => { - await expect - .poll(() => page.textContent('.unsafe-fetch-query-dot-svg-import-status')) - .toBe('403') - }) - - test('unsafe fetch .svg?import', async () => { - await expect - .poll(() => page.textContent('.unsafe-fetch-svg-status')) - .toBe('403') - }) - - test('safe fs fetch', async () => { - await expect - .poll(() => page.textContent('.safe-fs-fetch')) - .toBe(stringified) - await expect - .poll(() => page.textContent('.safe-fs-fetch-status')) - .toBe('200') - }) - - test('safe fs fetch', async () => { - await expect - .poll(() => page.textContent('.safe-fs-fetch-query')) - .toBe(stringified) - await expect - .poll(() => page.textContent('.safe-fs-fetch-query-status')) - .toBe('200') - }) - - test('safe fs fetch with special characters', async () => { - await expect - .poll(() => page.textContent('.safe-fs-fetch-special-characters')) - .toBe(stringified) - await expect - .poll(() => page.textContent('.safe-fs-fetch-special-characters-status')) - .toBe('200') - }) - - test('unsafe fs fetch', async () => { - await expect.poll(() => page.textContent('.unsafe-fs-fetch')).toBe('') - await expect - .poll(() => page.textContent('.unsafe-fs-fetch-status')) - .toBe('403') - }) - - test('unsafe fs fetch', async () => { - await expect.poll(() => page.textContent('.unsafe-fs-fetch-raw')).toBe('') - await expect - .poll(() => page.textContent('.unsafe-fs-fetch-raw-status')) - .toBe('403') - }) - - test('unsafe fs fetch query 1', async () => { - await expect - .poll(() => page.textContent('.unsafe-fs-fetch-raw-query1')) - .toBe('') - await expect - .poll(() => page.textContent('.unsafe-fs-fetch-raw-query1-status')) - .toBe('403') - }) - - test('unsafe fs fetch query 2', async () => { - await expect - .poll(() => page.textContent('.unsafe-fs-fetch-raw-query2')) - .toBe('') - await expect - .poll(() => page.textContent('.unsafe-fs-fetch-raw-query2-status')) - .toBe('403') - }) - - test('unsafe fs fetch with special characters (#8498)', async () => { - await expect.poll(() => page.textContent('.unsafe-fs-fetch-8498')).toBe('') - await expect - .poll(() => page.textContent('.unsafe-fs-fetch-8498-status')) - .toBe('404') - }) - - test('unsafe fs fetch with special characters 2 (#8498)', async () => { - await expect - .poll(() => page.textContent('.unsafe-fs-fetch-8498-2')) - .toBe('') - await expect - .poll(() => page.textContent('.unsafe-fs-fetch-8498-2-status')) - .toBe('404') - }) - - test('unsafe fs fetch import inline', async () => { - await expect - .poll(() => page.textContent('.unsafe-fs-fetch-import-inline-status')) - .toBe('403') - }) - - test('unsafe fs fetch import inline wasm init', async () => { - await expect - .poll(() => - page.textContent('.unsafe-fs-fetch-import-inline-wasm-init-status'), - ) - .toBe('403') - }) - - test('unsafe fs fetch with relative path after query status', async () => { - await expect - .poll(() => - page.textContent('.unsafe-fs-fetch-relative-path-after-query-status'), - ) - .toBe('404') - }) - test('nested entry', async () => { await expect.poll(() => page.textContent('.nested-entry')).toBe('foobar') }) - test('denied', async () => { - await expect.poll(() => page.textContent('.unsafe-dotenv')).toBe('403') + test('virtual svg module', async () => { + await expect.poll(() => page.textContent('.virtual-svg')).toMatch(' { +describe.runIf(isServe)('matrix', () => { + const variants = [ + { variantId: '', variantName: 'normal' }, + { variantId: '-fs', variantName: '/@fs/' }, + ] as const + type VariantId = (typeof variants)[number]['variantId'] + const cases: Array<{ + name: string + testId: string + content: string | RegExp + status: string | string[] + skipVariants?: VariantId[] + isSPAFallback?: boolean + }> = [ + { + name: 'safe fetch', + testId: 'safe', + content: /KEY=safe/, + status: '200', + }, + { + name: 'safe fetch with query', + testId: 'safe-query', + content: /KEY=safe/, + status: '200', + }, + { + name: 'safe fetch in subdir', + testId: 'safe-subdir', + content: /KEY=safe/, + status: '200', + }, + { + name: 'safe fetch with special characters', + testId: 'safe-subdir-special-characters', + content: /KEY=safe/, + status: '200', + }, + { + name: 'safe fetch with special characters 2', + testId: 'safe-subdir-special-characters2', + content: safeJsonContent, + status: '200', + }, + { + name: 'safe fetch imported', + testId: 'safe-imported', + content: safeJsonContent, + status: '200', + skipVariants: [''], + }, + { + name: 'safe fetch imported with query', + testId: 'safe-imported-query', + content: safeJsonContent, + status: '200', + skipVariants: [''], + }, + + { + name: 'unsafe fetch', + testId: 'unsafe', + content: /403 Restricted/, + status: '403', + }, + { + name: 'unsafe JSON fetch', + testId: 'unsafe-json', + content: /403 Restricted/, + status: '403', + skipVariants: [''], + }, + { + name: 'unsafe HTML fetch', + testId: 'unsafe-html', + content: /403 Restricted/, + status: '403', + }, + { + name: 'unsafe HTML fetch outside root', + testId: 'unsafe-html-outside-root', + content: /403 Restricted/, + status: '403', + skipVariants: [''], + }, + { + name: 'unsafe fetch with special characters (#8498)', + testId: 'unsafe-8498', + content: '', + status: '404', + }, + { + name: 'unsafe fetch with special characters 2 (#8498)', + testId: 'unsafe-8498-2', + content: '', + status: '404', + }, + { + name: 'unsafe fetch import inline', + testId: 'unsafe-import-inline', + content: /403 Restricted/, + status: '403', + }, + { + name: 'unsafe fetch raw query import', + testId: 'unsafe-raw-query-import', + content: /403 Restricted/, + status: '403', + }, + { + name: 'unsafe fetch raw import raw outside root', + testId: 'unsafe-raw-import-raw-outside-root', + content: /403 Restricted/, + status: '403', + skipVariants: [''], + }, + { + name: 'unsafe fetch raw import raw outside root 1', + testId: 'unsafe-raw-import-raw-outside-root1', + content: /403 Restricted/, + status: '403', + skipVariants: [''], + }, + { + name: 'unsafe fetch raw import raw outside root 2', + testId: 'unsafe-raw-import-raw-outside-root2', + content: /403 Restricted/, + status: '403', + skipVariants: [''], + }, + { + name: 'unsafe fetch with ?url query', + testId: 'unsafe-url', + content: /403 Restricted/, + status: '403', + }, + { + name: 'unsafe fetch ?.svg?import', + testId: 'unsafe-query-dot-svg-import', + content: /403 Restricted/, + status: '403', + }, + { + name: 'unsafe fetch .svg?import', + testId: 'unsafe-svg', + content: /403 Restricted/, + status: '403', + }, + { + name: 'unsafe fetch import inline wasm init', + testId: 'unsafe-import-inline-wasm-init', + content: /403 Restricted/, + status: '403', + }, + // It is 404 in `fs-serve/base` test, 403 in `fs-serve` test + { + name: 'unsafe fetch with relative path after query', + testId: 'unsafe-relative-path-after-query', + content: /403 Restricted|^$/, + status: ['403', '404'], + isSPAFallback: true, + }, + { + name: 'denied .env', + testId: 'unsafe-dotenv', + content: /403 Restricted/, + status: '403', + }, // It is 403 in case insensitive system, 404 in others - await expect - .poll(() => page.textContent('.unsafe-dotEnV-casing')) - .toStrictEqual(expect.toBeOneOf(['403', '404'])) - }) + { + name: 'denied env casing', + testId: 'unsafe-dotenv-casing', + content: /403 Restricted|^$/, + status: ['403', '404'], + }, + { + name: 'denied .env with raw query', + testId: 'unsafe-dotenv-raw', + content: /403 Restricted/, + status: '403', + }, + { + name: 'denied .env with url query', + testId: 'unsafe-dotenv-url', + content: /403 Restricted/, + status: '403', + }, + { + name: 'denied .env with inline query', + testId: 'unsafe-dotenv-inline', + content: /403 Restricted/, + status: '403', + }, + { + name: 'denied env with ?.svg?.wasm?init', + testId: 'unsafe-dotenv-query-dot-svg-wasm-init', + content: /403 Restricted/, + status: '403', + }, + { + name: 'denied .env with import and raw query', + testId: 'unsafe-dotenv-import-raw', + content: /403 Restricted/, + status: '403', + }, + ] + + for (const { + name, + testId, + content, + status, + skipVariants, + isSPAFallback, + } of cases) { + for (const { variantId, variantName } of variants) { + if (skipVariants?.includes(variantId)) { + continue + } - test('denied env with ?.svg?.wasm?init', async () => { - await expect - .poll(() => page.textContent('.unsafe-dotenv-query-dot-svg-wasm-init')) - .toBe('403') - }) + test.concurrent(`${name} (${variantName})`, async ({ expect }) => { + const baseSelector = `.fetch${variantId}-${testId}` + const actualStatus = expect.poll(() => + page.textContent(`${baseSelector}-status`), + ) + const actualContent = expect.poll(() => + page.textContent(`${baseSelector}-content`), + ) + + if (variantName === 'normal' && isSPAFallback) { + await actualStatus.toBe('200') + await actualContent.toContain('

FS Serve Matrix Test Summary

') + return + } + + if (typeof status === 'string') { + await actualStatus.toBe(status) + } else { + await actualStatus.toBeOneOf(status) + } + + if (typeof content === 'string') { + await actualContent.toBe(content) + } else { + await actualContent.toMatch(content) + } + }) + } + } }) describe('fetch', () => { @@ -530,10 +591,12 @@ describe.runIf(isServe)('fetchModule via WebSocket', () => { describe.runIf(!isServe)('preview HTML', () => { test('unsafe HTML fetch', async () => { - await expect.poll(() => page.textContent('.unsafe-fetch-html')).toBe('') await expect - .poll(() => page.textContent('.unsafe-fetch-html-status')) + .poll(() => page.textContent('.fetch-unsafe-html-status')) .toBe('404') + await expect + .poll(() => page.textContent('.fetch-unsafe-html-content')) + .toBe('') }) }) diff --git a/playground/fs-serve/__tests__/deny/fs-serve-deny.spec.ts b/playground/fs-serve/__tests__/deny/fs-serve-deny.spec.ts index fb60922e86e1ae..0beb709566691b 100644 --- a/playground/fs-serve/__tests__/deny/fs-serve-deny.spec.ts +++ b/playground/fs-serve/__tests__/deny/fs-serve-deny.spec.ts @@ -2,16 +2,23 @@ import { describe, expect, test } from 'vitest' import { isServe, page, viteTestUrl } from '~utils' describe.runIf(isServe)('main', () => { - test('**/deny/** should deny src/deny/deny.txt', async () => { - const res = await page.request.fetch( - new URL('/src/deny/deny.txt', viteTestUrl).href, - ) - expect(res.status()).toBe(403) - }) - test('**/deny/** should deny src/deny/.deny', async () => { - const res = await page.request.fetch( - new URL('/src/deny/.deny', viteTestUrl).href, - ) - expect(res.status()).toBe(403) - }) + for (const { name, urlPath } of [ + { name: 'src/deny/deny.txt', urlPath: '/src/deny/deny.txt' }, + { name: 'src/deny/.deny', urlPath: '/src/deny/.deny' }, + { name: 'src/deny/deny.txt?raw', urlPath: '/src/deny/deny.txt?raw' }, + { name: 'src/deny/deny.txt?url', urlPath: '/src/deny/deny.txt?url' }, + { + name: 'src/deny/deny.txt?import&raw', + urlPath: '/src/deny/deny.txt?import&raw', + }, + { + name: 'src/deny/.deny?.svg?import', + urlPath: '/src/deny/.deny?.svg?import', + }, + ]) { + test(`**/deny/** should deny ${name}`, async () => { + const res = await page.request.fetch(new URL(urlPath, viteTestUrl).href) + expect(res.status()).toBe(403) + }) + } }) diff --git a/playground/fs-serve/__tests__/fs-serve.spec.ts b/playground/fs-serve/__tests__/fs-serve.spec.ts index 0b145e35dc58f6..fc17b6e60280c7 100644 --- a/playground/fs-serve/__tests__/fs-serve.spec.ts +++ b/playground/fs-serve/__tests__/fs-serve.spec.ts @@ -40,71 +40,69 @@ describe.runIf(isServe)('invalid request', () => { const root = path .resolve(import.meta.dirname.replace('playground', 'playground-temp'), '..') .replace(/\\/g, '/') - - test('request with sendRawRequest should work', async () => { - const response = await sendRawRequest(viteTestUrl, '/src/safe.txt') - expect(response).toContain('HTTP/1.1 200 OK') - expect(response).toContain('KEY=safe') - }) - - test('request with sendRawRequest should work with /@fs/', async () => { - const response = await sendRawRequest( - viteTestUrl, - path.posix.join('/@fs/', root, 'root/src/safe.txt'), - ) - expect(response).toContain('HTTP/1.1 200 OK') - expect(response).toContain('KEY=safe') - }) - - test('should reject request that has # in request-target', async () => { - const response = await sendRawRequest( - viteTestUrl, - '/src/safe.txt#/../../unsafe.txt', - ) - expect(response).toContain('HTTP/1.1 400 Bad Request') - }) - - test('should reject request that has # in request-target with /@fs/', async () => { - const response = await sendRawRequest( - viteTestUrl, - path.posix.join('/@fs/', root, 'root/src/safe.txt') + + const testCases: Array<{ + name: string + target: string + status: string + content?: string + }> = [ + { + name: 'basic request', + target: '/src/safe.txt', + status: 'HTTP/1.1 200 OK', + content: 'KEY=safe', + }, + { + name: 'request with /@fs/', + target: path.posix.join('/@fs/', root, 'root/src/safe.txt'), + status: 'HTTP/1.1 200 OK', + content: 'KEY=safe', + }, + { + name: '# in request-target', + target: '/src/safe.txt#/../../unsafe.txt', + status: 'HTTP/1.1 400 Bad Request', + }, + { + name: '# in request-target with /@fs/', + target: + path.posix.join('/@fs/', root, 'root/src/safe.txt') + '#/../../unsafe.txt', - ) - expect(response).toContain('HTTP/1.1 400 Bad Request') - }) - - test('should deny request to denied file when a request has /.', async () => { - const response = await sendRawRequest(viteTestUrl, '/src/dummy.crt/.') - expect(response).toContain('HTTP/1.1 403 Forbidden') - }) - - test('should deny request to denied file when a request ends with \\', async () => { - const response = await sendRawRequest(viteTestUrl, '/src/.env\\') - expect(response).toContain( - isWindows ? 'HTTP/1.1 403 Forbidden' : 'HTTP/1.1 404 Not Found', - ) - }) - - test('should deny request to denied file when a request ends with \\ with /@fs/', async () => { - const response = await sendRawRequest( - viteTestUrl, - path.posix.join('/@fs/', root, 'root/src/.env') + '\\', - ) - expect(response).toContain( - isWindows ? 'HTTP/1.1 403 Forbidden' : 'HTTP/1.1 404 Not Found', - ) - }) - - test('should deny request with /@fs/ to denied file when a request has /.', async () => { - const response = await sendRawRequest( - viteTestUrl, - path.posix.join('/@fs/', root, 'root/src/dummy.crt/') + '.', - ) - expect(response).toContain('HTTP/1.1 403 Forbidden') - }) - - test('should deny request to HTML file outside root by default with relative path', async () => { - const response = await sendRawRequest(viteTestUrl, '/../unsafe.html') - expect(response).toContain('HTTP/1.1 403 Forbidden') - }) + status: 'HTTP/1.1 400 Bad Request', + }, + { + name: 'denied file with /.', + target: '/src/dummy.crt/.', + status: 'HTTP/1.1 403 Forbidden', + }, + { + name: 'denied file ending with \\', + target: '/src/.env\\', + status: isWindows ? 'HTTP/1.1 403 Forbidden' : 'HTTP/1.1 404 Not Found', + }, + { + name: 'denied file ending with \\ with /@fs/', + target: path.posix.join('/@fs/', root, 'root/src/.env') + '\\', + status: isWindows ? 'HTTP/1.1 403 Forbidden' : 'HTTP/1.1 404 Not Found', + }, + { + name: 'denied file with /. with /@fs/', + target: path.posix.join('/@fs/', root, 'root/src/dummy.crt/') + '.', + status: 'HTTP/1.1 403 Forbidden', + }, + { + name: 'HTML outside root with relative path', + target: '/../unsafe.html', + status: 'HTTP/1.1 403 Forbidden', + }, + ] + for (const { name, target, status, content } of testCases) { + test(name, async () => { + const response = await sendRawRequest(viteTestUrl, target) + expect(response).toContain(status) + if (content !== undefined) { + expect(response).toContain(content) + } + }) + } }) diff --git a/playground/fs-serve/package.json b/playground/fs-serve/package.json index ed11de77da1029..c2c48e5b0bad83 100644 --- a/playground/fs-serve/package.json +++ b/playground/fs-serve/package.json @@ -16,6 +16,8 @@ "preview:deny": "vite preview root --config ./root/vite.config-deny.js" }, "devDependencies": { + "@types/escape-html": "^1.0.4", + "escape-html": "^1.0.3", "ws": "^8.20.0" } } diff --git a/playground/fs-serve/root/matrixTestResultPlugin.ts b/playground/fs-serve/root/matrixTestResultPlugin.ts new file mode 100644 index 00000000000000..43665a33c282d5 --- /dev/null +++ b/playground/fs-serve/root/matrixTestResultPlugin.ts @@ -0,0 +1,144 @@ +import type { Plugin } from 'vite' +import escapeHtml from 'escape-html' + +const testIds = [ + 'safe', + 'safe-query', + 'safe-subdir', + 'safe-subdir-special-characters', + 'safe-subdir-special-characters2', + 'safe-imported', + 'safe-imported-query', + 'unsafe', + 'unsafe-json', + 'unsafe-html', + 'unsafe-html-outside-root', + 'unsafe-8498', + 'unsafe-8498-2', + 'unsafe-import-inline', + 'unsafe-raw-query-import', + 'unsafe-raw-import-raw-outside-root', + 'unsafe-raw-import-raw-outside-root1', + 'unsafe-raw-import-raw-outside-root2', + 'unsafe-url', + 'unsafe-query-dot-svg-import', + 'unsafe-svg', + 'unsafe-import-inline-wasm-init', + 'unsafe-relative-path-after-query', + 'unsafe-dotenv', + 'unsafe-dotenv-casing', + 'unsafe-dotenv-raw', + 'unsafe-dotenv-url', + 'unsafe-dotenv-inline', + 'unsafe-dotenv-query-dot-svg-wasm-init', + 'unsafe-dotenv-import-raw', +] + +export default function matrixTestResultPlugin(): Plugin { + return { + name: 'matrix-test-result', + transformIndexHtml(html) { + const summary = ` +

FS Serve Matrix Test Summary

+
+ + + + + + + + + + ${testIds + .map( + (id) => + ` + + + ${['', '-fs'] + .map((variant) => + ` + + `.trim(), + ) + .join('\n')} + + `, + ) + .join('\n')} + +
NameNormal/@fs/
${escapeHtml(id)} + +
+

+ # +
+
+
+ `.trim() + + const contents = ` +

FS Serve Test Contents

+
+ ${['', '-fs'] + .map((variant) => + testIds + .map((id) => + ` +
+

${escapeHtml(id)}

+

+        
+ `.trim(), + ) + .join('\n'), + ) + .join('\n')} +
+ `.trim() + + const style = ` + + `.trim() + + return html.replace( + '', + `\n${summary}\n${contents}\n${style}\n`, + ) + }, + } +} diff --git a/playground/fs-serve/root/src/index.html b/playground/fs-serve/root/src/index.html index 399dc319107272..d0dd2c7dc6c960 100644 --- a/playground/fs-serve/root/src/index.html +++ b/playground/fs-serve/root/src/index.html @@ -1,72 +1,15 @@ -

Normal Import

-

-

+

FS Serve Normal Tests

+
+

Normal Import

+

+  

+  

+  

+
-

Safe Fetch

-

-

-

-

-
-

Safe Fetch Subdirectory

-

-

-

-

-
-

Unsafe Fetch

-

-

-

-

-

-

-

-

-

-

-

-

-
-

Safe /@fs/ Fetch

-

-

-

-

-

-

-
-

Unsafe /@fs/ Fetch

-

-

-

-

-

-

-

-

-

-

-

-

-

-

-

-

-

-
-

Nested Entry

-

-
-

Denied

-

-

-

-
-Virtual SVG module
-

+
 
 
+
+