Fix Windows dev script proxying (#2052)

* Add tests for script proxying

* Fix Windows script proxying

#2053
This commit is contained in:
Drew Powers 2021-11-30 11:54:37 -07:00 committed by GitHub
parent 606fa81b94
commit 03cabc5171
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 245 additions and 142 deletions

View file

@ -21,7 +21,6 @@ import { createVite } from '../create-vite.js';
import * as msg from './messages.js'; import * as msg from './messages.js';
import notFoundTemplate, { subpathNotUsedTemplate } from './template/4xx.js'; import notFoundTemplate, { subpathNotUsedTemplate } from './template/4xx.js';
import serverErrorTemplate from './template/5xx.js'; import serverErrorTemplate from './template/5xx.js';
import { viteifyURL } from '../util.js';
export interface DevOptions { export interface DevOptions {
logging: LogOptions; logging: LogOptions;
@ -329,14 +328,7 @@ export class AstroDevServer {
res.end(); res.end();
} catch (err: any) { } catch (err: any) {
const statusCode = 500; const statusCode = 500;
const mod = filePath && this.viteServer.moduleGraph.getModuleById(viteifyURL(filePath)); await this.viteServer.moduleGraph.invalidateAll();
if (mod) {
for (const m of [mod, ...mod.importedModules]) {
this.viteServer.moduleGraph.invalidateModule(m);
}
} else {
this.viteServer.moduleGraph.invalidateAll();
}
this.viteServer.ws.send({ type: 'error', err }); this.viteServer.ws.send({ type: 'error', err });
let html = serverErrorTemplate({ let html = serverErrorTemplate({
statusCode, statusCode,

View file

@ -9,27 +9,23 @@ export const STYLE_EXTENSIONS = new Set(['.css', '.pcss', '.postcss', '.scss', '
/** find unloaded styles */ /** find unloaded styles */
export function getStylesForURL(filePath: URL, viteServer: vite.ViteDevServer): Set<string> { export function getStylesForURL(filePath: URL, viteServer: vite.ViteDevServer): Set<string> {
const css = new Set<string>(); const css = new Set<string>();
const { idToModuleMap } = viteServer.moduleGraph;
const rootID = viteifyURL(filePath); const rootID = viteifyURL(filePath);
const moduleGraph = idToModuleMap.get(rootID);
if (!moduleGraph) return css;
// recursively crawl module graph to get all style files imported by parent id // recursively crawl module graph to get all style files imported by parent id
function crawlCSS(entryModule: string, scanned = new Set<string>()) { function crawlCSS(entryModule: string, scanned = new Set<string>()) {
const moduleName = idToModuleMap.get(entryModule); const moduleName = viteServer.moduleGraph.urlToModuleMap.get(entryModule);
if (!moduleName) return; if (!moduleName || !moduleName.id) return;
if (!moduleName.id) return;
// mark the entrypoint as scanned to avoid an infinite loop // mark the entrypoint as scanned to avoid an infinite loop
scanned.add(moduleName.id); scanned.add(moduleName.url);
for (const importedModule of moduleName.importedModules) { for (const importedModule of moduleName.importedModules) {
if (!importedModule.id || scanned.has(importedModule.id)) continue; if (!importedModule.url || scanned.has(importedModule.url)) continue;
const ext = path.extname(importedModule.id.toLowerCase()); const ext = path.extname(importedModule.url.toLowerCase());
if (STYLE_EXTENSIONS.has(ext)) { if (STYLE_EXTENSIONS.has(ext)) {
css.add(importedModule.url || importedModule.id); // if style file, add to list css.add(importedModule.url); // if style file, add to list
} else { } else {
crawlCSS(importedModule.id, scanned); // otherwise, crawl file to see if it imports any CSS crawlCSS(importedModule.url, scanned); // otherwise, crawl file to see if it imports any CSS
} }
scanned.add(importedModule.id); scanned.add(importedModule.url);
} }
} }
crawlCSS(rootID); crawlCSS(rootID);

View file

@ -266,7 +266,8 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO
// run transformIndexHtml() in dev to run Vite dev transformations // run transformIndexHtml() in dev to run Vite dev transformations
if (mode === 'development') { if (mode === 'development') {
html = await viteServer.transformIndexHtml(viteifyURL(filePath), html, pathname); const relativeURL = filePath.href.replace(astroConfig.projectRoot.href, '/');
html = await viteServer.transformIndexHtml(relativeURL, html, pathname);
} }
// inject <!doctype html> if missing (TODO: is a more robust check needed for comments, etc.?) // inject <!doctype html> if missing (TODO: is a more robust check needed for comments, etc.?)

View file

@ -77,9 +77,9 @@ export function resolveDependency(dep: string, astroConfig: AstroConfig) {
* Vite-ify URL * Vite-ify URL
* Given a file URL, return an ID that matches Vites module graph. Needed for resolution and stack trace fixing. * Given a file URL, return an ID that matches Vites module graph. Needed for resolution and stack trace fixing.
* Must match the following format: * Must match the following format:
* Linux/Mac: /Users/astro/code/my-project/src/pages/index.astro * Linux/Mac: /@fs/Users/astro/code/my-project/src/pages/index.astro
* Windows: C:/Users/astro/code/my-project/src/pages/index.astro * Windows: /@fs/C:/Users/astro/code/my-project/src/pages/index.astro
*/ */
export function viteifyURL(filePath: URL): string { export function viteifyURL(filePath: URL): string {
return slash(fileURLToPath(filePath)); return `/@fs${filePath.pathname}`;
} }

View file

@ -4,6 +4,8 @@ import Hello from '../components/Hello.jsx';
<html> <html>
<head><title>Solid</title></head> <head><title>Solid</title></head>
<body> <body>
<div><Hello /></div> <div>
<Hello client:load />
</div>
</body> </body>
</html> </html>

View file

@ -19,7 +19,7 @@ import TypeScript from '../components/TypeScript.svelte'
</head> </head>
<body> <body>
<main> <main>
<TypeScript message="Hello, TypeScript" /> <TypeScript message="Hello, TypeScript" client:load />
</main> </main>
</body> </body>
</html> </html>

View file

@ -4,98 +4,125 @@ import { loadFixture } from './test-utils.js';
let fixture; let fixture;
before(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/react-component/',
renderers: ['@astrojs/renderer-react', '@astrojs/renderer-vue'],
});
await fixture.build();
});
describe('React Components', () => { describe('React Components', () => {
it('Can load React', async () => { before(async () => {
const html = await fixture.readFile('/index.html'); fixture = await loadFixture({
const $ = cheerio.load(html); projectRoot: './fixtures/react-component/',
renderers: ['@astrojs/renderer-react', '@astrojs/renderer-vue'],
// test 1: basic component renders });
expect($('#react-h2').text()).to.equal('Hello world!');
// test 2: no reactroot
expect($('#react-h2').attr('data-reactroot')).to.equal(undefined);
// test 3: Can use function components
expect($('#arrow-fn-component')).to.have.lengthOf(1);
// test 4: Can use spread for components
expect($('#component-spread-props')).to.have.lengthOf(1);
// test 5: spread props renders
expect($('#component-spread-props').text(), 'Hello world!');
// test 6: Can use TS components
expect($('.ts-component')).to.have.lengthOf(1);
// test 7: Can use Pure components
expect($('#pure')).to.have.lengthOf(1);
}); });
// TODO: fix compiler bug describe('build', () => {
it.skip('Includes reactroot on hydrating components', async () => { before(async () => {
const html = await fixture.readFile('/index.html'); await fixture.build();
const $ = cheerio.load(html); });
const div = $('#research'); it('Can load React', async () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
// test 1: has the hydration attr // test 1: basic component renders
expect(div.attr('data-reactroot')).to.be.ok; expect($('#react-h2').text()).to.equal('Hello world!');
// test 2: renders correctly // test 2: no reactroot
expect(div.html()).to.equal('foo bar <!-- -->1'); expect($('#react-h2').attr('data-reactroot')).to.equal(undefined);
// test 3: Can use function components
expect($('#arrow-fn-component')).to.have.lengthOf(1);
// test 4: Can use spread for components
expect($('#component-spread-props')).to.have.lengthOf(1);
// test 5: spread props renders
expect($('#component-spread-props').text(), 'Hello world!');
// test 6: Can use TS components
expect($('.ts-component')).to.have.lengthOf(1);
// test 7: Can use Pure components
expect($('#pure')).to.have.lengthOf(1);
});
it('Can load Vue', async () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
expect($('#vue-h2').text()).to.equal('Hasta la vista, baby');
});
it('Can use a pragma comment', async () => {
const html = await fixture.readFile('/pragma-comment/index.html');
const $ = cheerio.load(html);
// test 1: rendered the PragmaComment component
expect($('.pragma-comment')).to.have.lengthOf(2);
});
// TODO: is this still a relevant test?
it.skip('Includes reactroot on hydrating components', async () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
const div = $('#research');
// test 1: has the hydration attr
expect(div.attr('data-reactroot')).to.be.ok;
// test 2: renders correctly
expect(div.html()).to.equal('foo bar <!-- -->1');
});
}); });
// TODO: Vite does not throw a helpful error message on window SSR describe('dev', () => {
it.skip('Throws helpful error message on window SSR', async () => { let devServer;
const html = await fixture.readFile('/window/index.html');
expect(html).to.include( before(async () => {
`[/window] devServer = await fixture.startDevServer();
});
after(async () => {
devServer && (await devServer.stop());
});
it('scripts proxy correctly', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerio.load(html);
for (const script of $('script').toArray()) {
const { src } = script.attribs;
if (!src) continue;
expect((await fixture.fetch(src)).status, `404: ${src}`).to.equal(200);
}
});
// TODO: move this to separate dev test?
it.skip('Throws helpful error message on window SSR', async () => {
const html = await fixture.fetch('/window/index.html');
expect(html).to.include(
`[/window]
The window object is not available during server-side rendering (SSR). The window object is not available during server-side rendering (SSR).
Try using \`import.meta.env.SSR\` to write SSR-friendly code. Try using \`import.meta.env.SSR\` to write SSR-friendly code.
https://docs.astro.build/reference/api-reference/#importmeta` https://docs.astro.build/reference/api-reference/#importmeta`
); );
}); });
it('Can load Vue', async () => { // In moving over to Vite, the jsx-runtime import is now obscured. TODO: update the method of finding this.
const html = await fixture.readFile('/index.html'); it.skip('uses the new JSX transform', async () => {
const $ = cheerio.load(html); const html = await fixture.fetch('/index.html');
expect($('#vue-h2').text()).to.equal('Hasta la vista, baby');
});
// TODO: fix // Grab the imports
it('Can use a pragma comment', async () => { const exp = /import\("(.+?)"\)/g;
const html = await fixture.readFile('/pragma-comment/index.html'); let match, componentUrl;
const $ = cheerio.load(html); while ((match = exp.exec(html))) {
if (match[1].includes('Research.js')) {
// test 1: rendered the PragmaComment component componentUrl = match[1];
expect($('.pragma-comment')).to.have.lengthOf(2); break;
}); }
// In moving over to Vite, the jsx-runtime import is now obscured. TODO: update the method of finding this.
it.skip('uses the new JSX transform', async () => {
const html = await fixture.fetch('/index.html');
// Grab the imports
const exp = /import\("(.+?)"\)/g;
let match, componentUrl;
while ((match = exp.exec(html))) {
if (match[1].includes('Research.js')) {
componentUrl = match[1];
break;
} }
} const component = await fixture.readFile(componentUrl);
const component = await fixture.readFile(componentUrl); const jsxRuntime = component.imports.filter((i) => i.specifier.includes('jsx-runtime'));
const jsxRuntime = component.imports.filter((i) => i.specifier.includes('jsx-runtime'));
// test 1: react/jsx-runtime is used for the component // test 1: react/jsx-runtime is used for the component
expect(jsxRuntime).to.be.ok; expect(jsxRuntime).to.be.ok;
});
}); });
}); });

View file

@ -2,22 +2,50 @@ import { expect } from 'chai';
import cheerio from 'cheerio'; import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js'; import { loadFixture } from './test-utils.js';
let fixture;
before(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/solid-component/',
renderers: ['@astrojs/renderer-solid'],
});
await fixture.build();
});
describe('Solid component', () => { describe('Solid component', () => {
it('Can load a component', async () => { let fixture;
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
// test 1: Works before(async () => {
expect($('.hello')).to.have.lengthOf(1); fixture = await loadFixture({
projectRoot: './fixtures/solid-component/',
renderers: ['@astrojs/renderer-solid'],
});
});
describe('build', () => {
before(async () => {
await fixture.build();
});
it('Can load a component', async () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
// test 1: Works
expect($('.hello')).to.have.lengthOf(1);
});
});
describe('dev', () => {
let devServer;
before(async () => {
devServer = await fixture.startDevServer();
});
after(async () => {
devServer & devServer.stop();
});
it('scripts proxy correctly', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerio.load(html);
for (const script of $('script').toArray()) {
const { src } = script.attribs;
if (!src) continue;
expect((await fixture.fetch(src)).status, `404: ${src}`).to.equal(200);
}
});
}); });
}); });

View file

@ -10,13 +10,42 @@ describe('Svelte component', () => {
projectRoot: './fixtures/svelte-component/', projectRoot: './fixtures/svelte-component/',
renderers: ['@astrojs/renderer-svelte'], renderers: ['@astrojs/renderer-svelte'],
}); });
await fixture.build();
}); });
it('Works with TypeScript', async () => { describe('build', () => {
const html = await fixture.readFile('/typescript/index.html'); before(async () => {
const $ = cheerio.load(html); await fixture.build();
});
expect($('#svelte-ts').text()).to.equal('Hello, TypeScript'); it('Works with TypeScript', async () => {
const html = await fixture.readFile('/typescript/index.html');
const $ = cheerio.load(html);
expect($('#svelte-ts').text()).to.equal('Hello, TypeScript');
});
});
describe('dev', () => {
let devServer;
before(async () => {
devServer = await fixture.startDevServer();
});
after(async () => {
devServer && (await devServer.stop());
});
it('scripts proxy correctly', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerio.load(html);
for (const script of $('script').toArray()) {
const { src } = script.attribs;
if (!src) continue;
console.log({ src });
expect((await fixture.fetch(src)).status, `404: ${src}`).to.equal(200);
}
});
}); });
}); });

View file

@ -10,28 +10,56 @@ describe('Vue component', () => {
projectRoot: './fixtures/vue-component/', projectRoot: './fixtures/vue-component/',
renderers: ['@astrojs/renderer-vue'], renderers: ['@astrojs/renderer-vue'],
}); });
await fixture.build();
}); });
it('Can load Vue', async () => { describe('build', () => {
const html = await fixture.readFile('/index.html'); before(async () => {
const $ = cheerio.load(html); await fixture.build();
});
const allPreValues = $('pre') it('Can load Vue', async () => {
.toArray() const html = await fixture.readFile('/index.html');
.map((el) => $(el).text()); const $ = cheerio.load(html);
// test 1: renders all components correctly const allPreValues = $('pre')
expect(allPreValues).to.deep.equal(['0', '1', '10', '100', '1000']); .toArray()
.map((el) => $(el).text());
// test 2: renders 3 <astro-root>s // test 1: renders all components correctly
expect($('astro-root')).to.have.lengthOf(4); expect(allPreValues).to.deep.equal(['0', '1', '10', '100', '1000']);
// test 3: all <astro-root>s have uid attributes // test 2: renders 3 <astro-root>s
expect($('astro-root[uid]')).to.have.lengthOf(4); expect($('astro-root')).to.have.lengthOf(4);
// test 5: all <astro-root>s have unique uid attributes // test 3: all <astro-root>s have uid attributes
const uniqueRootUIDs = $('astro-root').map((i, el) => $(el).attr('uid')); expect($('astro-root[uid]')).to.have.lengthOf(4);
expect(new Set(uniqueRootUIDs).size).to.equal(4);
// test 5: all <astro-root>s have unique uid attributes
const uniqueRootUIDs = $('astro-root').map((i, el) => $(el).attr('uid'));
expect(new Set(uniqueRootUIDs).size).to.equal(4);
});
});
describe('dev', () => {
let devServer;
before(async () => {
devServer = await fixture.startDevServer();
});
after(async () => {
devServer && (await devServer.stop());
});
it('scripts proxy correctly', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerio.load(html);
for (const script of $('script').toArray()) {
const { src } = script.attribs;
if (!src) continue;
expect((await fixture.fetch(src)).status, `404: ${src}`).to.equal(200);
}
});
}); });
}); });