Skip to content

Commit e8eff00

Browse files
committed
πŸ• Always run PostCSS over Vue <style> blocks in clay compile scripts
CLAYCLI_COMPILE_SCRIPTS_SKIP_STYLE_POSTCSS was treating two unrelated concerns as a single switch: 1. The PostCSS plugin chain (cssImport β†’ autoprefixer β†’ mixins β†’ nested β†’ simple-vars), needed for every <style> block to flatten `&-foo` nesting and resolve simple variables. 2. The sass/scss preprocessor (node-sass β†’ dart-sass), introduced to dodge node-sass native-binding failures in some Docker stages. When the env var was set, the chain in (1) was disabled too, so kiln plugin <style> blocks reached the browser as nested CSS no UA can parse. Modals built from local .vue files (article-picker, agora, mediaplay-picker, …) consequently rendered unstyled β€” even on sites served by the existing Browserify pipeline. This commit decouples them: the PostCSS chain now always runs; the env var only swaps in the dart-sass preprocessor. Plugins are resolved from the host project's node_modules first so the chain runs against the host's PostCSS major. If the host has no PostCSS installed at all, claycli's bundled copies are used instead. Mixing the two trees is avoided β€” picking individual plugins across versions triggers "PostCSS plugin X requires PostCSS Y" failures because each plugin's `Symbol.for('postcss')` is anchored to whichever postcss it was installed alongside. Plugins absent from the chosen tree are dropped silently (postcss-mixins is a transitive of vueify only and may legitimately be absent from a host). Verified locally on nymag/sites: clay compile scripts emits a 65 KB _kiln-plugins.css with zero raw `&-` selectors (was 56 KB / 180 nested matches), and the agora / article-picker modals render correctly. Companion to the equivalent fix in lib/cmd/vite/plugins/vue2.js so the two pipelines produce equivalent output for kiln plugin SFCs. Made-with: Cursor
1 parent 6537199 commit e8eff00

1 file changed

Lines changed: 87 additions & 17 deletions

File tree

β€Žlib/cmd/compile/scripts.jsβ€Ž

Lines changed: 87 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,10 @@ const _ = require('lodash'),
2323
vueify = require('@nymag/vueify'),
2424
uglifyify = require('uglifyify'),
2525
extractCSS = require('@nymag/vueify/plugins/extract-css'),
26-
autoprefixer = require('autoprefixer'),
27-
cssImport = require('postcss-import'),
28-
mixins = require('postcss-mixins'),
29-
nested = require('postcss-nested'),
30-
simpleVars = require('postcss-simple-vars'),
26+
// PostCSS plugins applied to Vue <style> blocks are resolved lazily from
27+
// the host project's node_modules in buildVuePostcssChain() so the chain
28+
// runs against the host's PostCSS major. Keeping them off the top-level
29+
// require list avoids pinning claycli's bundled (PostCSS 8) copies.
3130
reporters = require('../../reporters'),
3231
helpers = require('../../compilation-helpers'),
3332
// globbing patterns
@@ -66,6 +65,69 @@ const _ = require('lodash'),
6665
},
6766
temporaryIDs = {};
6867

68+
/**
69+
* Resolve a PostCSS plugin from a specific node_modules tree.
70+
* Returns the plugin's module export, or null if it isn't installed there.
71+
*
72+
* @param {string} name npm package name
73+
* @param {string[]} paths resolution roots (passed to require.resolve)
74+
* @returns {*}
75+
*/
76+
function resolvePostcssPlugin(name, paths) {
77+
try {
78+
return require(require.resolve(name, { paths }));
79+
} catch (_) {
80+
return null;
81+
}
82+
}
83+
84+
/**
85+
* Build the PostCSS plugin chain applied to every Vue <style> block.
86+
*
87+
* Mirrors the legacy vueify chain (cssImport β†’ autoprefixer β†’ mixins β†’
88+
* nested β†’ simple-vars).
89+
*
90+
* Resolution strategy: prefer the host project's node_modules so the chain
91+
* runs against the host's PostCSS major. If the host has no PostCSS at all,
92+
* fall back to claycli's bundled copies for the whole chain. We never mix
93+
* the two β€” picking individual plugins across versions causes runtime
94+
* "PostCSS plugin X requires PostCSS Y" failures because each plugin's
95+
* `Symbol.for('postcss')` namespace is anchored to whichever postcss it
96+
* was installed alongside.
97+
*
98+
* Plugins that aren't installed in the chosen tree are dropped silently β€”
99+
* the chain still runs whatever resolved, matching the legacy best-effort
100+
* behaviour. (postcss-mixins, for example, is a transitive of @nymag/vueify
101+
* only and may legitimately be absent from a host project.)
102+
*
103+
* @returns {Array}
104+
*/
105+
function buildVuePostcssChain() {
106+
const hostPaths = [process.cwd()];
107+
const useHost = !!resolvePostcssPlugin('postcss', hostPaths);
108+
const paths = useHost ? hostPaths : undefined; // undefined β†’ claycli's own resolver
109+
const specs = [
110+
{ name: 'postcss-import', args: () => [] },
111+
{ name: 'autoprefixer', args: () => [helpers.getConfigFileOrBrowsersList('autoprefixerOptions')] },
112+
{ name: 'postcss-mixins', args: () => [] },
113+
{ name: 'postcss-nested', args: () => [] },
114+
{ name: 'postcss-simple-vars', args: () => [] },
115+
];
116+
117+
return specs.reduce((chain, { name, args }) => {
118+
let plugin;
119+
120+
if (paths) {
121+
plugin = resolvePostcssPlugin(name, paths);
122+
} else {
123+
try { plugin = require(name); } catch (_) { plugin = null; }
124+
}
125+
126+
if (typeof plugin === 'function') chain.push(plugin(...args()));
127+
return chain;
128+
}, []);
129+
}
130+
69131
/**
70132
* copy kiln js if it has changed
71133
* note: kiln compiles its own js, so this just copies it to the public folder
@@ -377,18 +439,26 @@ function buildScripts(entries, options = {}) {
377439
}
378440
}
379441

380-
if (!skipVueStylePostcss) {
381-
vueifyTransformOptions.postcss = [
382-
cssImport(),
383-
autoprefixer(helpers.getConfigFileOrBrowsersList('autoprefixerOptions')),
384-
mixins(),
385-
nested(),
386-
simpleVars()
387-
];
388-
} else {
389-
// Vueify's default sass/scss compiler requires node-sass.
390-
// During staged rollout we intentionally skip this postcss path and
391-
// compile sass/scss with dart-sass to avoid node-sass runtime failures.
442+
// PostCSS chain over Vue <style> blocks must always run. Source like
443+
// `.foo { &-bar { … } }` reaches the browser as a nested rule no UA can
444+
// parse without postcss-nested, so kiln plugin modals (article-picker,
445+
// agora, mediaplay-picker, …) render unstyled if this is skipped.
446+
//
447+
// Plugins are resolved from the host project's node_modules first so the
448+
// chain runs against the host's PostCSS major (e.g. PostCSS 7 in
449+
// nymag/sites, where claycli's bundled autoprefixer@10 would otherwise
450+
// throw "autoprefixer requires PostCSS 8" at compile time). If a host
451+
// copy isn't present we fall back to claycli's bundled require β€” that
452+
// covers projects already on PostCSS 8 with no host-side install.
453+
// Anything that fails to resolve in both locations is dropped silently.
454+
vueifyTransformOptions.postcss = buildVuePostcssChain();
455+
456+
// CLAYCLI_COMPILE_SCRIPTS_SKIP_STYLE_POSTCSS controls the sass/scss
457+
// preprocessor only. Vueify's default uses node-sass (native binding,
458+
// unbuildable in some Docker stages); during staged rollout we swap in
459+
// dart-sass to avoid that runtime failure. The env var name is kept for
460+
// backwards compatibility but no longer affects the PostCSS chain above.
461+
if (skipVueStylePostcss) {
392462
vueifyTransformOptions.customCompilers = {
393463
sass: compileSassWithDartSass,
394464
scss: compileSassWithDartSass

0 commit comments

Comments
Β (0)