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

View file

@ -9,27 +9,23 @@ export const STYLE_EXTENSIONS = new Set(['.css', '.pcss', '.postcss', '.scss', '
/** find unloaded styles */
export function getStylesForURL(filePath: URL, viteServer: vite.ViteDevServer): Set<string> {
const css = new Set<string>();
const { idToModuleMap } = viteServer.moduleGraph;
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
function crawlCSS(entryModule: string, scanned = new Set<string>()) {
const moduleName = idToModuleMap.get(entryModule);
if (!moduleName) return;
if (!moduleName.id) return;
const moduleName = viteServer.moduleGraph.urlToModuleMap.get(entryModule);
if (!moduleName || !moduleName.id) return;
// mark the entrypoint as scanned to avoid an infinite loop
scanned.add(moduleName.id);
scanned.add(moduleName.url);
for (const importedModule of moduleName.importedModules) {
if (!importedModule.id || scanned.has(importedModule.id)) continue;
const ext = path.extname(importedModule.id.toLowerCase());
if (!importedModule.url || scanned.has(importedModule.url)) continue;
const ext = path.extname(importedModule.url.toLowerCase());
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 {
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);

View file

@ -266,7 +266,8 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO
// run transformIndexHtml() in dev to run Vite dev transformations
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.?)

View file

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

View file

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

View file

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

View file

@ -4,98 +4,125 @@ import { loadFixture } from './test-utils.js';
let fixture;
before(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/react-component/',
renderers: ['@astrojs/renderer-react', '@astrojs/renderer-vue'],
});
await fixture.build();
});
describe('React Components', () => {
it('Can load React', async () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
// 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);
before(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/react-component/',
renderers: ['@astrojs/renderer-react', '@astrojs/renderer-vue'],
});
});
// TODO: fix compiler bug
it.skip('Includes reactroot on hydrating components', async () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
describe('build', () => {
before(async () => {
await fixture.build();
});
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
expect(div.attr('data-reactroot')).to.be.ok;
// test 1: basic component renders
expect($('#react-h2').text()).to.equal('Hello world!');
// test 2: renders correctly
expect(div.html()).to.equal('foo bar <!-- -->1');
// 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);
});
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
it.skip('Throws helpful error message on window SSR', async () => {
const html = await fixture.readFile('/window/index.html');
expect(html).to.include(
`[/window]
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);
}
});
// 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).
Try using \`import.meta.env.SSR\` to write SSR-friendly code.
https://docs.astro.build/reference/api-reference/#importmeta`
);
});
);
});
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');
});
// 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');
// TODO: fix
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);
});
// 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;
// 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 jsxRuntime = component.imports.filter((i) => i.specifier.includes('jsx-runtime'));
const component = await fixture.readFile(componentUrl);
const jsxRuntime = component.imports.filter((i) => i.specifier.includes('jsx-runtime'));
// test 1: react/jsx-runtime is used for the component
expect(jsxRuntime).to.be.ok;
// test 1: react/jsx-runtime is used for the component
expect(jsxRuntime).to.be.ok;
});
});
});

View file

@ -2,22 +2,50 @@ import { expect } from 'chai';
import cheerio from 'cheerio';
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', () => {
it('Can load a component', async () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
let fixture;
// test 1: Works
expect($('.hello')).to.have.lengthOf(1);
before(async () => {
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/',
renderers: ['@astrojs/renderer-svelte'],
});
await fixture.build();
});
it('Works with TypeScript', async () => {
const html = await fixture.readFile('/typescript/index.html');
const $ = cheerio.load(html);
describe('build', () => {
before(async () => {
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/',
renderers: ['@astrojs/renderer-vue'],
});
await fixture.build();
});
it('Can load Vue', async () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
describe('build', () => {
before(async () => {
await fixture.build();
});
const allPreValues = $('pre')
.toArray()
.map((el) => $(el).text());
it('Can load Vue', async () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
// test 1: renders all components correctly
expect(allPreValues).to.deep.equal(['0', '1', '10', '100', '1000']);
const allPreValues = $('pre')
.toArray()
.map((el) => $(el).text());
// test 2: renders 3 <astro-root>s
expect($('astro-root')).to.have.lengthOf(4);
// test 1: renders all components correctly
expect(allPreValues).to.deep.equal(['0', '1', '10', '100', '1000']);
// test 3: all <astro-root>s have uid attributes
expect($('astro-root[uid]')).to.have.lengthOf(4);
// test 2: renders 3 <astro-root>s
expect($('astro-root')).to.have.lengthOf(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);
// test 3: all <astro-root>s have uid attributes
expect($('astro-root[uid]')).to.have.lengthOf(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);
}
});
});
});