Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions packages/@stylexjs/unplugin/__tests__/unplugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -530,4 +530,64 @@ describe('@stylexjs/unplugin', () => {
}
});
});

// Regression: https://github.com/facebook/stylex/issues/1912
describe('Rollup and Vite watch mode', () => {
const source = `
import * as stylex from '@stylexjs/stylex';
import { other } from './other.js';
export const styles = stylex.create({ foo: { color: 'red' } });
`;

test('transform does not wait for its imports to load', async () => {
const plugin = unplugin.raw({ runtimeInjection: false });
plugin.buildStart();
// `./other.js` can't finish loading until this transform returns, like
// the other side of an import cycle in watch mode, or a prebundled
// dependency on a cold start of the Vite dev server.
const ctx = {
meta: { watchMode: true },
parse: () => ({
body: [
{ type: 'ImportDeclaration', source: { value: './other.js' } },
],
}),
resolve: async (spec) => ({ id: `/virtual/${spec}`, external: false }),
load: jest.fn(() => new Promise(() => {})),
};
let timer;
const timeout = new Promise((resolve) => {
timer = setTimeout(() => resolve('timed out'), 1000);
});
const result = await Promise.race([
plugin.transform.call(ctx, source, '/virtual/watch.js'),
timeout,
]);
clearTimeout(timer);
expect(result).not.toBe('timed out');
expect(ctx.load).not.toHaveBeenCalled();
});

test('restores the rules of a cached module from its meta', async () => {
const id = '/virtual/cached.js';
const plugin = unplugin.raw({ runtimeInjection: false });
plugin.buildStart();
const { meta } = await plugin.transform(source, id);

// A build that starts without these rules, for example from a Rollup
// cache saved by another process, skips the unchanged module and only
// gets its meta back.
const saved = globalThis.__stylex_unplugin_store;
globalThis.__stylex_unplugin_store = undefined;
try {
const next = unplugin.raw({ runtimeInjection: false });
next.buildStart();
expect(next.__stylexCollectCss()).toBe('');
expect(next.shouldTransformCachedModule({ id, meta })).toBe(false);
expect(next.__stylexCollectCss()).toContain('color: red');
} finally {
globalThis.__stylex_unplugin_store = saved;
}
});
});
});
57 changes: 19 additions & 38 deletions packages/@stylexjs/unplugin/src/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -439,17 +439,15 @@ export const unpluginFactory = (userOptions = {}, metaOptions) => {

const result = await runBabelTransform(code, file, '@stylexjs/unplugin');
const { metadata } = result;
const rules =
metadata && Array.isArray(metadata.stylex) ? metadata.stylex : [];
if (!stylexOptions.runtimeInjection) {
const hasRules =
metadata &&
Array.isArray(metadata.stylex) &&
metadata.stylex.length > 0;
const shared = getSharedStore();
if (hasRules) {
stylexRulesById.set(id, metadata.stylex);
shared.rulesById.set(id, metadata.stylex);
if (rules.length > 0) {
stylexRulesById.set(id, rules);
shared.rulesById.set(id, rules);
shared.version++;
await persistRulesToDisk(id, metadata.stylex);
await persistRulesToDisk(id, rules);
} else {
stylexRulesById.delete(id);
if (shared.rulesById.has(id)) {
Expand All @@ -460,40 +458,23 @@ export const unpluginFactory = (userOptions = {}, metaOptions) => {
}
}

// Rollup/Vite watch-mode support: collect stylex metadata from cached deps
// Only when running in rollup-like context
// $FlowExpectedError[incompatible-use]
const ctx = this;
if (
ctx &&
ctx.meta &&
ctx.meta.watchMode &&
typeof ctx.parse === 'function'
) {
try {
const ast = ctx.parse(result.code);
for (const stmt of ast.body) {
if (stmt.type === 'ImportDeclaration') {
// $FlowExpectedError[incompatible-call]
const resolved = await ctx.resolve(stmt.source.value, id);
if (resolved && !resolved.external) {
// $FlowExpectedError[incompatible-call]
const loaded = await ctx.load(resolved);
if (loaded && loaded.meta && 'stylex' in loaded.meta) {
stylexRulesById.set(resolved.id, loaded.meta.stylex);
}
}
}
}
} catch {}
}
return { code: result.code, map: result.map };
// Rollup keeps `meta` with a cached module, so when a watch rebuild skips
// an unchanged file, `shouldTransformCachedModule` below gets its rules
// back. Don't load imports from here: awaiting `this.load()` inside
// `transform` never finishes when two StyleX files import each other in
// watch mode, and on a cold start of the Vite dev server it can wait on
// the dependency optimizer, which is waiting on this transform (#1912).
return { code: result.code, map: result.map, meta: { stylex: rules } };
},

// Rollup: ensure cached modules still provide their metadata
shouldTransformCachedModule({ id, meta }) {
if (meta && 'stylex' in meta) {
stylexRulesById.set(id, meta.stylex);
if (!stylexOptions.runtimeInjection && Array.isArray(meta?.stylex)) {
if (meta.stylex.length > 0) {
stylexRulesById.set(id, meta.stylex);
} else {
stylexRulesById.delete(id);
Comment on lines +472 to +476
}
}
return false;
},
Expand Down
Loading