fix(dev): Resolve script and link aliases (#387)

Co-authored-by: Aaron Klinker <aaronklinker1@gmail.com>
This commit is contained in:
Nenad Novaković
2024-01-30 19:05:31 +01:00
committed by GitHub
parent 9cee9eefb4
commit 6557dfe430
3 changed files with 149 additions and 21 deletions
@@ -0,0 +1,64 @@
import { describe, expect, it } from 'vitest';
import { Window } from 'happy-dom';
import { pointToDevServer } from '../devHtmlPrerender';
import {
fakeDevServer,
fakeInternalConfig,
} from '~/core/utils/testing/fake-objects';
import { normalizePath } from '~/core/utils/paths';
import { resolve } from 'node:path';
describe('Dev HTML Prerender Plugin', () => {
describe('pointToDevServer', () => {
it.each([
// File paths should be resolved
['style.css', 'http://localhost:5173/entrypoints/popup/style.css'],
['./style.css', 'http://localhost:5173/entrypoints/popup/style.css'],
['../style.css', 'http://localhost:5173/entrypoints/style.css'],
['~/assets/style.css', 'http://localhost:5173/assets/style.css'],
['~~/assets/style.css', 'http://localhost:5173/assets/style.css'],
['~local/style.css', 'http://localhost:5173/style.css'],
['~absolute/style.css', 'http://localhost:5173/assets/style.css'],
['~file', 'http://localhost:5173/example.css'],
// Absolute paths are loaded with the `/@fs/` base path
[
'~outside/test.css',
`http://localhost:5173/@fs${
process.platform === 'win32'
? '/' + normalizePath(resolve('/some/non-root/test.css')) // "/D:/some/non-root/test.css"
: '/some/non-root/test.css'
}`,
],
// URLs should not be changed
['https://example.com/style.css', 'https://example.com/style.css'],
])('should transform "%s" into "%s"', (input, expected) => {
const { document } = new Window({
url: 'http://localhost',
});
const root = '/some/root';
const config = fakeInternalConfig({
root,
alias: {
'~local': '.',
'~absolute': `${root}/assets`,
'~file': `${root}/example.css`,
'~outside': `${root}/../non-root`,
'~~': root,
'~': root,
},
});
const server = fakeDevServer({
hostname: 'localhost',
port: 5173,
origin: 'http://localhost:5173',
});
const id = root + '/entrypoints/popup/index.html';
document.head.innerHTML = `<link rel="stylesheet" href="${input}" />`;
pointToDevServer(config, server, id, document as any, 'link', 'href');
const actual = document.querySelector('link')!;
expect(actual.getAttribute('href')).toBe(expected);
});
});
});
@@ -1,8 +1,9 @@
import type * as vite from 'vite';
import { InternalConfig } from '~/types';
import { InternalConfig, WxtDevServer } from '~/types';
import { getEntrypointName } from '~/core/utils/entrypoints';
import { parseHTML } from 'linkedom';
import { dirname, isAbsolute, relative, resolve } from 'node:path';
import { dirname, relative, resolve } from 'node:path';
import { normalizePath } from '~/core/utils/paths';
// Cache the preamble script for all devHtmlPrerender plugins, not just one
let reactRefreshPreamble = '';
@@ -47,25 +48,10 @@ export function devHtmlPrerender(
const { document } = parseHTML(code);
const pointToDevServer = (
querySelector: string,
attr: string,
): void => {
document.querySelectorAll(querySelector).forEach((element) => {
const src = element.getAttribute(attr);
if (!src) return;
if (isAbsolute(src)) {
element.setAttribute(attr, server.origin + src);
} else if (src.startsWith('.')) {
const abs = resolve(dirname(id), src);
const pathname = relative(config.root, abs);
element.setAttribute(attr, `${server.origin}/${pathname}`);
}
});
};
pointToDevServer('script[type=module]', 'src');
pointToDevServer('link[rel=stylesheet]', 'href');
const _pointToDevServer = (querySelector: string, attr: string) =>
pointToDevServer(config, server, id, document, querySelector, attr);
_pointToDevServer('script[type=module]', 'src');
_pointToDevServer('link[rel=stylesheet]', 'href');
// Add a script to add page reloading
const reloader = document.createElement('script');
@@ -146,3 +132,63 @@ export function devHtmlPrerender(
},
];
}
export function pointToDevServer(
config: Omit<InternalConfig, 'builder'>,
server: WxtDevServer,
id: string,
document: Document,
querySelector: string,
attr: string,
) {
document.querySelectorAll(querySelector).forEach((element) => {
const src = element.getAttribute(attr);
if (!src || isUrl(src)) return;
let resolvedAbsolutePath: string | undefined;
// Check if src uses a project alias
const matchingAlias = Object.entries(config.alias).find(([key]) =>
src.startsWith(key),
);
if (matchingAlias) {
// Matches a import alias
const [alias, replacement] = matchingAlias;
resolvedAbsolutePath = resolve(
config.root,
src.replace(alias, replacement),
);
} else {
// Some file path relative to the HTML file
resolvedAbsolutePath = resolve(dirname(id), src);
}
// Apply the final file path
if (resolvedAbsolutePath) {
const relativePath = normalizePath(
relative(config.root, resolvedAbsolutePath),
);
if (relativePath.startsWith('.')) {
// Outside the config.root directory, serve the absolute path
let path = normalizePath(resolvedAbsolutePath);
// Add "/" to start of windows paths ("D:/some/path" -> "/D:/some/path")
if (!path.startsWith('/')) path = '/' + path;
element.setAttribute(attr, `${server.origin}/@fs${path}`);
} else {
// Inside the project, use relative path
const url = new URL(relativePath, server.origin);
element.setAttribute(attr, url.href);
}
}
});
}
function isUrl(str: string): boolean {
try {
new URL(str);
return true;
} catch {
return false;
}
}
+18
View File
@@ -22,6 +22,7 @@ import {
UserManifest,
} from '~/types';
import { mock } from 'vitest-mock-extended';
import { vi } from 'vitest';
faker.seed(__TEST_SEED__);
@@ -269,3 +270,20 @@ export const fakeManifestCommand = fakeObjectCreator<Commands.Command>(() => ({
max: 9,
})}`,
}));
export const fakeDevServer = fakeObjectCreator<WxtDevServer>(() => ({
hostname: 'localhost',
origin: 'http://localhost',
port: 5173,
reloadContentScript: vi.fn(),
reloadExtension: vi.fn(),
reloadPage: vi.fn(),
restart: vi.fn(),
restartBrowser: vi.fn(),
stop: vi.fn(),
start: vi.fn(),
watcher: mock(),
transformHtml: vi.fn(),
ws: mock(),
currentOutput: undefined,
}));