Skip to content

Commit

Permalink
Fix outbound requests from scoped CSS
Browse files Browse the repository at this point in the history
Outbound requests to other files like `url()` and `@import` in your CSS were not working before this PR because webpack did not understand the context of our virtual CSS files.

This fixes the context using the same technique as in https://github.com/embroider-build/embroider/blob/f361b719676788554b3a67da5cdb06f977b69b7c/packages/webpack/src/webpack-resolver-plugin.ts#L137-L140
  • Loading branch information
ef4 committed Feb 1, 2024
1 parent 83cc55e commit 5feebef
Show file tree
Hide file tree
Showing 9 changed files with 388 additions and 37 deletions.
9 changes: 6 additions & 3 deletions glimmer-scoped-css/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ export function isScopedCSSRequest(request: string): boolean {
return request.endsWith('.glimmer-scoped.css');
}

const pattern = /\.([^.]*)\.glimmer-scoped.css$/;
const pattern = /^(.*)\.([^.]*)\.glimmer-scoped.css$/;

export function decodeScopedCSSRequest(request: string): string {
export function decodeScopedCSSRequest(request: string): {
css: string;
fromFile: string;
} {
let m = pattern.exec(request);
if (!m) {
throw new Error(`not a scoped CSS request: ${request}`);
}
return atob(decodeURIComponent(m[1]!));
return { fromFile: m[1]!, css: atob(decodeURIComponent(m[2]!)) };
}
5 changes: 4 additions & 1 deletion glimmer-scoped-css/src/virtual-loader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { LoaderContext } from 'webpack';
import { decodeScopedCSSRequest } from '.';
import { dirname } from 'path';

export default function virtualLoader(this: LoaderContext<unknown>) {
let optionsString = this.loaders[this.loaderIndex]?.options;
Expand All @@ -21,5 +22,7 @@ export default function virtualLoader(this: LoaderContext<unknown>) {
}

this.resourcePath = filename;
return decodeScopedCSSRequest(filename);
this.context = dirname(filename);

return decodeScopedCSSRequest(filename).css;
}
68 changes: 47 additions & 21 deletions glimmer-scoped-css/src/webpack.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { Compiler, Module } from 'webpack';
import type { Compiler, Module, ResolveData } from 'webpack';
import { resolve, dirname } from 'path';
import { isScopedCSSRequest } from '.';
import { decodeScopedCSSRequest, isScopedCSSRequest } from '.';

type CB = (err: null | Error, result?: Module | undefined) => void;
export const virtualLoaderName = 'glimmer-scoped-css/virtual-loader';
const virtualLoaderPath = 'glimmer-scoped-css/dist/virtual-loader.js';

export class GlimmerScopedCSSWebpackPlugin {
private addLoaderAlias(compiler: Compiler, name: string, alias: string) {
Expand All @@ -29,12 +30,29 @@ export class GlimmerScopedCSSWebpackPlugin {
compiler.hooks.normalModuleFactory.tap('glimmer-scoped-css', (nmf) => {
nmf.hooks.resolve.tapAsync(
{ name: 'glimmer-scoped-css' },
(state: unknown, callback: CB) => {
if (isRelevantRequest(state) && isScopedCSSRequest(state.request)) {
state.request = `style-loader!css-loader!glimmer-scoped-css/virtual-loader?filename=${resolve(
dirname(state.contextInfo.issuer),
state.request
)}!`;
(state: ResolveData, callback: CB) => {
if (!state.request.startsWith('!')) {
if (!state.contextInfo.issuer) {
// when the files emitted from our virtual-loader try to import things,
// those requests show in webpack as having no issuer. But we can see here
// which requests they are and adjust the issuer so they resolve things from
// the correct logical place.
let filename = identifyVirtualFile(state);
if (filename) {
state.contextInfo.issuer = filename;
state.context = dirname(filename);
}
}

if (
state.contextInfo.issuer !== '' &&
isScopedCSSRequest(state.request)
) {
state.request = `style-loader!css-loader!glimmer-scoped-css/virtual-loader?filename=${resolve(
dirname(state.contextInfo.issuer),
state.request
)}!`;
}
}
callback(null, undefined);
}
Expand All @@ -43,18 +61,26 @@ export class GlimmerScopedCSSWebpackPlugin {
}
}

interface RelevantRequest {
request: string;
context: string;
contextInfo: { issuer: string };
}
function identifyVirtualFile(state: ResolveData): string | undefined {
for (let dep of state.dependencies) {
let userRequest = (dep as any)._parentModule?.userRequest as
| string
| undefined;

if (!userRequest?.includes(virtualLoaderPath)) {
// early return when our loader can't appear at all
return;
}

function isRelevantRequest(state: any): state is RelevantRequest {
return (
typeof state.request === 'string' &&
typeof state.context === 'string' &&
typeof state.contextInfo?.issuer === 'string' &&
state.contextInfo.issuer !== '' &&
!state.request.startsWith('!')
);
for (let part of userRequest.split('!')) {
let url = new URL(part, 'http://not-a-real-origin');
if (url.pathname.endsWith(virtualLoaderPath)) {
let filename = url.searchParams.get('filename');
if (filename && isScopedCSSRequest(filename)) {
let { fromFile } = decodeScopedCSSRequest(filename);
return fromFile;
}
}
}
}
}
Loading

0 comments on commit 5feebef

Please sign in to comment.