Skip to content

Commit

Permalink
fix(webpack): pass dependencies to cached css files (#1080)
Browse files Browse the repository at this point in the history
When webpack is run with `--watch` or with `webpack-dev-server`, the
generated css output will not be updated when dependencies imported into
Linaria JavaScript source files, causing the resulting output to be
stale.

The changes to the `ICache` interfaces are backward-compatible to avoid
breaking any external implementations provided to the webpack builders
using the prior versions of the interfaces.

Co-authored-by: Jeremy Neander <jeremy.neander@glassdoor.com>
  • Loading branch information
jneander and Jeremy Neander authored Oct 17, 2022
1 parent 8a8be24 commit 2906ec1
Show file tree
Hide file tree
Showing 10 changed files with 131 additions and 56 deletions.
7 changes: 7 additions & 0 deletions .changeset/curly-yaks-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@linaria/webpack4-loader': patch
'@linaria/webpack5-loader': patch
'@linaria/webpack-loader': patch
---

Watch dependencies from cached css files in webpack watch mode.
6 changes: 5 additions & 1 deletion docs/BUNDLERS_INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,14 @@ The loader accepts the following options:
> ```
> interface ICache {
> get: (key: string) => Promise<string>;
> set: (key: string, value: string) => Promise<void>
> set: (key: string, value: string) => Promise<void>;
> getDependencies?: (key: string) => Promise<string[]>;
> setDependencies?: (key: string, value: string[]) => Promise<void>;
> }
> ```
When running webpack with `--watch`, `getDependencies` and `setDependencies` will be used to carry dependencies of the Linaria JavaScript source to the generated css output, ensuring both are rebuilt when dependencies change. When these methods are not present on the cache instance, dependencies for the css output will be ignored and may get out of sync with the JavaScript output. Linaria's default memory cache does not have this issue.
- `extension: string` (default: `'.linaria.css'`):
An extension of the intermediate CSS files.
Expand Down
2 changes: 1 addition & 1 deletion packages/webpack4-loader/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"@types/loader-utils": "^1.1.3",
"@types/mkdirp": "^0.5.2",
"@types/node": "^17.0.39",
"@types/webpack": "^4.41.26",
"@types/webpack": "^4.41.33",
"source-map": "^0.7.3"
},
"engines": {
Expand Down
13 changes: 13 additions & 0 deletions packages/webpack4-loader/src/cache.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
export interface ICache {
get: (key: string) => Promise<string>;
set: (key: string, value: string) => Promise<void>;
getDependencies?: (key: string) => Promise<string[]>;
setDependencies?: (key: string, value: string[]) => Promise<void>;
}

// memory cache, which is the default cache implementation in Linaria

class MemoryCache implements ICache {
private cache: Map<string, string> = new Map();

private dependenciesCache: Map<string, string[]> = new Map();

public get(key: string): Promise<string> {
return Promise.resolve(this.cache.get(key) ?? '');
}
Expand All @@ -16,6 +20,15 @@ class MemoryCache implements ICache {
this.cache.set(key, value);
return Promise.resolve();
}

public getDependencies(key: string): Promise<string[]> {
return Promise.resolve(this.dependenciesCache.get(key) ?? []);
}

public setDependencies(key: string, value: string[]): Promise<void> {
this.dependenciesCache.set(key, value);
return Promise.resolve();
}
}

export const memoryCache = new MemoryCache();
Expand Down
47 changes: 27 additions & 20 deletions packages/webpack4-loader/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,26 +93,33 @@ export default function webpack4Loader(
) ?? []
);

getCacheInstance(cacheProvider)
.then((cacheInstance) =>
cacheInstance.set(this.resourcePath, cssText)
)
.then(() => {
const request = `${outputFileName}!=!${outputCssLoader}?cacheProvider=${encodeURIComponent(
cacheProvider ?? ''
)}!${this.resourcePath}`;
const stringifiedRequest = loaderUtils.stringifyRequest(
this,
request
);

return this.callback(
null,
`${result.code}\n\nrequire(${stringifiedRequest});`,
castSourceMap(result.sourceMap)
);
})
.catch((err: Error) => this.callback(err));
try {
const cacheInstance = await getCacheInstance(cacheProvider);

await cacheInstance.set(this.resourcePath, cssText);

await cacheInstance.setDependencies?.(
this.resourcePath,
this.getDependencies()
);

const request = `${outputFileName}!=!${outputCssLoader}?cacheProvider=${encodeURIComponent(
cacheProvider ?? ''
)}!${this.resourcePath}`;
const stringifiedRequest = loaderUtils.stringifyRequest(
this,
request
);

this.callback(
null,
`${result.code}\n\nrequire(${stringifiedRequest});`,
castSourceMap(result.sourceMap)
);
} catch (err) {
this.callback(err as Error);
}

return;
}

Expand Down
22 changes: 17 additions & 5 deletions packages/webpack4-loader/src/outputCssLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,23 @@ import { getCacheInstance } from './cache';

type LoaderContext = Parameters<typeof loaderUtils.getOptions>[0];

export default function outputCssLoader(this: LoaderContext) {
export default async function outputCssLoader(this: LoaderContext) {
this.async();
const { cacheProvider } = loaderUtils.getOptions(this) || {};
getCacheInstance(cacheProvider)
.then((cacheInstance) => cacheInstance.get(this.resourcePath))
.then((result) => this.callback(null, result))
.catch((err: Error) => this.callback(err));

try {
const cacheInstance = await getCacheInstance(cacheProvider);

const result = await cacheInstance.get(this.resourcePath);
const dependencies =
(await cacheInstance.getDependencies?.(this.resourcePath)) ?? [];

dependencies.forEach((dependency) => {
this.addDependency(dependency);
});

this.callback(null, result);
} catch (err) {
this.callback(err as Error);
}
}
13 changes: 13 additions & 0 deletions packages/webpack5-loader/src/cache.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
export interface ICache {
get: (key: string) => Promise<string>;
set: (key: string, value: string) => Promise<void>;
getDependencies?: (key: string) => Promise<string[]>;
setDependencies?: (key: string, value: string[]) => Promise<void>;
}

// memory cache, which is the default cache implementation in Linaria

class MemoryCache implements ICache {
private cache: Map<string, string> = new Map();

private dependenciesCache: Map<string, string[]> = new Map();

public get(key: string): Promise<string> {
return Promise.resolve(this.cache.get(key) ?? '');
}
Expand All @@ -16,6 +20,15 @@ class MemoryCache implements ICache {
this.cache.set(key, value);
return Promise.resolve();
}

public getDependencies(key: string): Promise<string[]> {
return Promise.resolve(this.dependenciesCache.get(key) ?? []);
}

public setDependencies(key: string, value: string[]): Promise<void> {
this.dependenciesCache.set(key, value);
return Promise.resolve();
}
}

export const memoryCache = new MemoryCache();
Expand Down
45 changes: 26 additions & 19 deletions packages/webpack5-loader/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,25 +106,32 @@ const webpack5Loader: Loader = function webpack5LoaderPlugin(
) ?? []
);

getCacheInstance(cacheProvider)
.then((cacheInstance) =>
cacheInstance.set(this.resourcePath, cssText)
)
.then(() => {
const request = `${outputFileName}!=!${outputCssLoader}?cacheProvider=${encodeURIComponent(
typeof cacheProvider === 'string' ? cacheProvider : ''
)}!${this.resourcePath}`;
const stringifiedRequest = JSON.stringify(
this.utils.contextify(this.context || this.rootContext, request)
);

return this.callback(
null,
`${result.code}\n\nrequire(${stringifiedRequest});`,
result.sourceMap ?? undefined
);
})
.catch((err: Error) => this.callback(err));
try {
const cacheInstance = await getCacheInstance(cacheProvider);

await cacheInstance.set(this.resourcePath, cssText);

await cacheInstance.setDependencies?.(
this.resourcePath,
this.getDependencies()
);

const request = `${outputFileName}!=!${outputCssLoader}?cacheProvider=${encodeURIComponent(
typeof cacheProvider === 'string' ? cacheProvider : ''
)}!${this.resourcePath}`;
const stringifiedRequest = JSON.stringify(
this.utils.contextify(this.context || this.rootContext, request)
);

this.callback(
null,
`${result.code}\n\nrequire(${stringifiedRequest});`,
result.sourceMap ?? undefined
);
} catch (err) {
this.callback(err as Error);
}

return;
}

Expand Down
22 changes: 17 additions & 5 deletions packages/webpack5-loader/src/outputCssLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,25 @@ import type webpack from 'webpack';
import type { ICache } from './cache';
import { getCacheInstance } from './cache';

export default function outputCssLoader(
export default async function outputCssLoader(
this: webpack.LoaderContext<{ cacheProvider: string | ICache | undefined }>
) {
this.async();
const { cacheProvider } = this.getOptions();
getCacheInstance(cacheProvider)
.then((cacheInstance) => cacheInstance.get(this.resourcePath))
.then((result) => this.callback(null, result))
.catch((err: Error) => this.callback(err));

try {
const cacheInstance = await getCacheInstance(cacheProvider);

const result = await cacheInstance.get(this.resourcePath);
const dependencies =
(await cacheInstance.getDependencies?.(this.resourcePath)) ?? [];

dependencies.forEach((dependency) => {
this.addDependency(dependency);
});

this.callback(null, result);
} catch (err) {
this.callback(err as Error);
}
}
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 2906ec1

Please sign in to comment.