Skip to content

Commit

Permalink
Resolve components by module ID during compilation (#3300)
Browse files Browse the repository at this point in the history
* WIP: adding test coverage

* test fixes

* moving the shared lib up a directory to reproduce the bug

* fix: transform with the module ID instead of parsing the filepath

* adding the shared lib to the workspaces list

* fix: client-only assets now get the full URL from vite

* why is this needed for windows?

* WIP: using /@fs to handle windows filepaths

* fix: remove /@fs from hoisted script imports

* nit: removing unused imports

* fix: strip off the path root when mapping client:only styles

* had to reverse the `/@fs` handling to work on windows and unix

* chore: adding comments to explain the fix

* chore: adding changeset
  • Loading branch information
Tony Sullivan authored May 12, 2022
1 parent 2fed346 commit b463ddb
Show file tree
Hide file tree
Showing 22 changed files with 407 additions and 41 deletions.
5 changes: 5 additions & 0 deletions .changeset/proud-cups-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Resolve .astro components by module ID to support the use of Astro + framework components in an NPM package
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@
"examples/component/packages/*",
"scripts",
"smoke/*",
"packages/astro/test/fixtures/builtins/packages/*",
"packages/astro/test/fixtures/builtins-polyfillnode",
"packages/astro/test/fixtures/component-library-shared",
"packages/astro/test/fixtures/custom-elements/my-component-lib",
"packages/astro/test/fixtures/static build/pkg"
],
Expand Down
20 changes: 10 additions & 10 deletions packages/astro/src/core/build/internal.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { AstroConfig, RouteData } from '../../@types/astro';
import type { RenderedChunk } from 'rollup';
import type { PageBuildData, ViteID } from './types';

import { fileURLToPath } from 'url';
import { prependForwardSlash } from '../path.js';
import { viteID } from '../util.js';

export interface BuildInternals {
Expand Down Expand Up @@ -80,17 +79,16 @@ export function trackPageData(
export function trackClientOnlyPageDatas(
internals: BuildInternals,
pageData: PageBuildData,
clientOnlys: string[],
astroConfig: AstroConfig
clientOnlys: string[]
) {
for (const clientOnlyComponent of clientOnlys) {
const coPath = viteID(new URL('.' + clientOnlyComponent, astroConfig.root));
let pageDataSet: Set<PageBuildData>;
if (internals.pagesByClientOnly.has(coPath)) {
pageDataSet = internals.pagesByClientOnly.get(coPath)!;
// clientOnlyComponent will be similar to `/@fs{moduleID}`
if (internals.pagesByClientOnly.has(clientOnlyComponent)) {
pageDataSet = internals.pagesByClientOnly.get(clientOnlyComponent)!;
} else {
pageDataSet = new Set<PageBuildData>();
internals.pagesByClientOnly.set(coPath, pageDataSet);
internals.pagesByClientOnly.set(clientOnlyComponent, pageDataSet);
}
pageDataSet.add(pageData);
}
Expand All @@ -115,8 +113,10 @@ export function* getPageDatasByClientOnlyChunk(
const pagesByClientOnly = internals.pagesByClientOnly;
if (pagesByClientOnly.size) {
for (const [modulePath] of Object.entries(chunk.modules)) {
if (pagesByClientOnly.has(modulePath)) {
for (const pageData of pagesByClientOnly.get(modulePath)!) {
// prepend with `/@fs` to match the path used in the compiler's transform() call
const pathname = `/@fs${prependForwardSlash(modulePath)}`;
if (pagesByClientOnly.has(pathname)) {
for (const pageData of pagesByClientOnly.get(pathname)!) {
yield pageData;
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/build/static-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export async function staticBuild(opts: StaticBuildOptions) {

// Track client:only usage so we can map their CSS back to the Page they are used in.
const clientOnlys = Array.from(metadata.clientOnlyComponentPaths());
trackClientOnlyPageDatas(internals, pageData, clientOnlys, astroConfig);
trackClientOnlyPageDatas(internals, pageData, clientOnlys);

const topLevelImports = new Set([
// Any component that gets hydrated
Expand Down
7 changes: 6 additions & 1 deletion packages/astro/src/core/build/vite-plugin-hoisted-scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ export function vitePluginHoistedScripts(
if (virtualHoistedEntry(id)) {
let code = '';
for (let path of internals.hoistedScriptIdToHoistedMap.get(id)!) {
code += `import "${path}";`;
let importPath = path;
// `/@fs` is added during the compiler's transform() step
if (importPath.startsWith('/@fs')) {
importPath = importPath.slice('/@fs'.length);
}
code += `import "${importPath}";`;
}
return {
code,
Expand Down
41 changes: 23 additions & 18 deletions packages/astro/src/vite-plugin-astro/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,25 @@ function safelyReplaceImportPlaceholder(code: string) {

const configCache = new WeakMap<AstroConfig, CompilationCache>();

async function compile(
config: AstroConfig,
filename: string,
source: string,
viteTransform: TransformHook,
opts: { ssr: boolean }
): Promise<CompileResult> {
interface CompileProps {
config: AstroConfig;
filename: string;
moduleId: string;
source: string;
ssr: boolean;
viteTransform: TransformHook;
}

async function compile({
config,
filename,
moduleId,
source,
ssr,
viteTransform,
}: CompileProps): Promise<CompileResult> {
const filenameURL = new URL(`file://${filename}`);
const normalizedID = fileURLToPath(filenameURL);
const pathname = filenameURL.pathname.slice(config.root.pathname.length - 1);

let rawCSSDeps = new Set<string>();
let cssTransformError: Error | undefined;
Expand All @@ -52,7 +61,8 @@ async function compile(
// use `sourcemap: "both"` so that sourcemap is included in the code
// result passed to esbuild, but also available in the catch handler.
const transformResult = await transform(source, {
pathname,
// For Windows compat, prepend the module ID with `/@fs`
pathname: `/@fs${prependForwardSlash(moduleId)}`,
projectRoot: config.root.toString(),
site: config.site ? new URL(config.base, config.site).toString() : undefined,
sourcefile: filename,
Expand Down Expand Up @@ -86,7 +96,7 @@ async function compile(
lang,
id: normalizedID,
transformHook: viteTransform,
ssr: opts.ssr,
ssr,
});

let map: SourceMapInput | undefined;
Expand Down Expand Up @@ -131,13 +141,8 @@ export function invalidateCompilation(config: AstroConfig, filename: string) {
}
}

export async function cachedCompilation(
config: AstroConfig,
filename: string,
source: string,
viteTransform: TransformHook,
opts: { ssr: boolean }
): Promise<CompileResult> {
export async function cachedCompilation(props: CompileProps): Promise<CompileResult> {
const { config, filename } = props;
let cache: CompilationCache;
if (!configCache.has(config)) {
cache = new Map();
Expand All @@ -148,7 +153,7 @@ export async function cachedCompilation(
if (cache.has(filename)) {
return cache.get(filename)!;
}
const compileResult = await compile(config, filename, source, viteTransform, opts);
const compileResult = await compile(props);
cache.set(filename, compileResult);
return compileResult;
}
20 changes: 11 additions & 9 deletions packages/astro/src/vite-plugin-astro/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,21 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
if (isPage && config._ctx.scripts.some((s) => s.stage === 'page')) {
source += `\n<script src="${PAGE_SCRIPT_ID}" />`;
}
const compileProps = {
config,
filename,
moduleId: id,
source,
ssr: Boolean(opts?.ssr),
viteTransform,
};
if (query.astro) {
if (query.type === 'style') {
if (typeof query.index === 'undefined') {
throw new Error(`Requests for Astro CSS must include an index.`);
}

const transformResult = await cachedCompilation(config, filename, source, viteTransform, {
ssr: Boolean(opts?.ssr),
});
const transformResult = await cachedCompilation(compileProps);

// Track any CSS dependencies so that HMR is triggered when they change.
await trackCSSDependencies.call(this, {
Expand All @@ -133,9 +139,7 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
};
}

const transformResult = await cachedCompilation(config, filename, source, viteTransform, {
ssr: Boolean(opts?.ssr),
});
const transformResult = await cachedCompilation(compileProps);
const scripts = transformResult.scripts;
const hoistedScript = scripts[query.index];

Expand Down Expand Up @@ -163,9 +167,7 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
}

try {
const transformResult = await cachedCompilation(config, filename, source, viteTransform, {
ssr: Boolean(opts?.ssr),
});
const transformResult = await cachedCompilation(compileProps);

// Compile all TypeScript to JavaScript.
// Also, catches invalid JS/TS in the compiled output before returning.
Expand Down
157 changes: 157 additions & 0 deletions packages/astro/test/component-library.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { expect } from 'chai';
import { load as cheerioLoad } from 'cheerio';
import { loadFixture } from './test-utils.js';

function addLeadingSlash(path) {
return path.startsWith('/') ? path : '/' + path;
}

describe('Component Libraries', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;

before(async () => {
fixture = await loadFixture({
root: './fixtures/component-library/',
});
});

describe('build', async () => {
before(async () => {
await fixture.build();
});

function createFindEvidence(expected, prefix) {
return async function findEvidence(pathname) {
const html = await fixture.readFile(pathname);
const $ = cheerioLoad(html);
const links = $('link[rel=stylesheet]');
for (const link of links) {
const href = $(link).attr('href');

const data = await fixture.readFile(addLeadingSlash(href));
if (expected.test(data)) {
return true;
}
}

return false;
};
}

it('Built .astro pages', async () => {
let html = await fixture.readFile('/with-astro/index.html');
expect(html).to.be.a('string');

html = await fixture.readFile('/with-react/index.html');
expect(html).to.be.a('string');

html = await fixture.readFile('/internal-hydration/index.html');
expect(html).to.be.a('string');
});

it('Works with .astro components', async () => {
const html = await fixture.readFile('/with-astro/index.html');
const $ = cheerioLoad(html);

expect($('button').text()).to.equal('Click me', "Rendered the component's slot");

const findEvidence = createFindEvidence(/border-radius:( )*1rem/);
expect(await findEvidence('with-astro/index.html')).to.equal(
true,
"Included the .astro component's <style>"
);
});

it('Works with react components', async () => {
const html = await fixture.readFile('/with-react/index.html');
const $ = cheerioLoad(html);

expect($('#react-static').text()).to.equal('Hello static!', 'Rendered the static component');

expect($('#react-idle').text()).to.equal(
'Hello idle!',
'Rendered the client hydrated component'
);

expect($('astro-root[uid]')).to.have.lengthOf(1, 'Included one hydration island');
});

it('Works with components hydrated internally', async () => {
const html = await fixture.readFile('/internal-hydration/index.html');
const $ = cheerioLoad(html);

expect($('.counter').length).to.equal(1, 'Rendered the svelte counter');
expect($('.counter-message').text().trim()).to.equal('Hello, Svelte!', "rendered the counter's slot");

expect($('astro-root[uid]')).to.have.lengthOf(1, 'Included one hydration island');
});
});

describe('dev', async () => {
let devServer;

before(async () => {
devServer = await fixture.startDevServer();
});

after(async () => {
await devServer.stop();
});

function createFindEvidence(expected, prefix) {
return async function findEvidence(pathname) {
const html = await fixture.fetch(pathname).then((res) => res.text());
const $ = cheerioLoad(html);
const links = $('link[rel=stylesheet]');
for (const link of links) {
const href = $(link).attr('href');

const data = await fixture.fetch(addLeadingSlash(href)).then((res) => res.text());
if (expected.test(data)) {
return true;
}
}

return false;
};
}

it('Works with .astro components', async () => {
const html = await fixture.fetch('/with-astro/').then((res) => res.text());
const $ = cheerioLoad(html);

expect($('button').text()).to.equal('Click me', "Rendered the component's slot");

const findEvidence = createFindEvidence(/border-radius:( )*1rem/);
expect(await findEvidence('/with-astro/')).to.equal(
true,
"Included the .astro component's <style>"
);
});

it('Works with react components', async () => {
const html = await fixture.fetch('/with-react/').then((res) => res.text());
const $ = cheerioLoad(html);

expect($('#react-static').text()).to.equal('Hello static!', 'Rendered the static component');

expect($('#react-idle').text()).to.equal(
'Hello idle!',
'Rendered the client hydrated component'
);

expect($('astro-root[uid]')).to.have.lengthOf(1, 'Included one hydration island');
});

it('Works with components hydrated internally', async () => {
const html = await fixture.fetch('/internal-hydration/').then((res) => res.text());
const $ = cheerioLoad(html);

expect($('.counter').length).to.equal(1, 'Rendered the svelte counter');
expect($('.counter-message').text().trim()).to.equal('Hello, Svelte!', "rendered the counter's slot");

expect($('astro-root[uid]')).to.have.lengthOf(1, 'Included one hydration island');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<button><slot /></button>

<style>
button {
border-radius: 1rem;
}
</style>
11 changes: 11 additions & 0 deletions packages/astro/test/fixtures/component-library-shared/Counter.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.counter {
display: grid;
font-size: 2em;
grid-template-columns: repeat(3, minmax(0, 1fr));
margin-top: 2em;
place-items: center;
}

.counter-message {
text-align: center;
}
Loading

0 comments on commit b463ddb

Please sign in to comment.