Skip to content

Commit 96c5c6d

Browse files
authored
Allow visitors to add dependencies (parcel-bundler#1170)
1 parent 4fe3a4b commit 96c5c6d

File tree

8 files changed

+362
-23
lines changed

8 files changed

+362
-23
lines changed

node/composeVisitors.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
// @ts-check
22
/** @typedef {import('./index').Visitor} Visitor */
3+
/** @typedef {import('./index').VisitorFunction} VisitorFunction */
34

45
/**
56
* Composes multiple visitor objects into a single one.
6-
* @param {Visitor[]} visitors
7-
* @return {Visitor}
7+
* @param {(Visitor | VisitorFunction)[]} visitors
8+
* @return {Visitor | VisitorFunction}
89
*/
910
function composeVisitors(visitors) {
1011
if (visitors.length === 1) {
1112
return visitors[0];
1213
}
14+
15+
if (visitors.some(v => typeof v === 'function')) {
16+
return (opts) => {
17+
let v = visitors.map(v => typeof v === 'function' ? v(opts) : v);
18+
return composeVisitors(v);
19+
};
20+
}
1321

1422
/** @type Visitor */
1523
let res = {};
@@ -366,7 +374,7 @@ function createArrayVisitor(visitors, apply) {
366374
// For each value, call all visitors. If a visitor returns a new value,
367375
// we start over, but skip the visitor that generated the value or saw
368376
// it before (to avoid cycles). This way, visitors can be composed in any order.
369-
for (let v = 0; v < visitors.length;) {
377+
for (let v = 0; v < visitors.length && i < arr.length;) {
370378
if (seen.get(v)) {
371379
v++;
372380
continue;

node/index.d.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export interface TransformOptions<C extends CustomAtRules> {
6363
* For optimal performance, visitors should be as specific as possible about what types of values
6464
* they care about so that JavaScript has to be called as little as possible.
6565
*/
66-
visitor?: Visitor<C>,
66+
visitor?: Visitor<C> | VisitorFunction<C>,
6767
/**
6868
* Defines how to parse custom CSS at-rules. Each at-rule can have a prelude, defined using a CSS
6969
* [syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings), and
@@ -213,6 +213,13 @@ export interface Visitor<C extends CustomAtRules> {
213213
EnvironmentVariableExit?: EnvironmentVariableVisitor | EnvironmentVariableVisitors;
214214
}
215215

216+
export type VisitorDependency = FileDependency | GlobDependency;
217+
export interface VisitorOptions {
218+
addDependency: (dep: VisitorDependency) => void
219+
}
220+
221+
export type VisitorFunction<C extends CustomAtRules> = (options: VisitorOptions) => Visitor<C>;
222+
216223
export interface CustomAtRules {
217224
[name: string]: CustomAtRuleDefinition
218225
}
@@ -358,7 +365,7 @@ export interface DependencyCSSModuleReference {
358365
specifier: string
359366
}
360367

361-
export type Dependency = ImportDependency | UrlDependency;
368+
export type Dependency = ImportDependency | UrlDependency | FileDependency | GlobDependency;
362369

363370
export interface ImportDependency {
364371
type: 'import',
@@ -384,6 +391,16 @@ export interface UrlDependency {
384391
placeholder: string
385392
}
386393

394+
export interface FileDependency {
395+
type: 'file',
396+
filePath: string
397+
}
398+
399+
export interface GlobDependency {
400+
type: 'glob',
401+
glob: string
402+
}
403+
387404
export interface SourceLocation {
388405
/** The file path in which the dependency exists. */
389406
filePath: string,
@@ -438,7 +455,7 @@ export interface TransformAttributeOptions {
438455
* For optimal performance, visitors should be as specific as possible about what types of values
439456
* they care about so that JavaScript has to be called as little as possible.
440457
*/
441-
visitor?: Visitor<never>
458+
visitor?: Visitor<never> | VisitorFunction<never>
442459
}
443460

444461
export interface TransformAttributeResult {
@@ -474,4 +491,4 @@ export declare function bundleAsync<C extends CustomAtRules>(options: BundleAsyn
474491
/**
475492
* Composes multiple visitor objects into a single one.
476493
*/
477-
export declare function composeVisitors<C extends CustomAtRules>(visitors: Visitor<C>[]): Visitor<C>;
494+
export declare function composeVisitors<C extends CustomAtRules>(visitors: (Visitor<C> | VisitorFunction<C>)[]): Visitor<C> | VisitorFunction<C>;

node/index.js

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,47 @@ if (process.platform === 'linux') {
1313
parts.push('msvc');
1414
}
1515

16-
if (process.env.CSS_TRANSFORMER_WASM) {
17-
module.exports = require(`../pkg`);
18-
} else {
19-
try {
20-
module.exports = require(`lightningcss-${parts.join('-')}`);
21-
} catch (err) {
22-
module.exports = require(`../lightningcss.${parts.join('-')}.node`);
23-
}
16+
let native;
17+
try {
18+
native = require(`lightningcss-${parts.join('-')}`);
19+
} catch (err) {
20+
native = require(`../lightningcss.${parts.join('-')}.node`);
2421
}
2522

23+
module.exports.transform = wrap(native.transform);
24+
module.exports.transformStyleAttribute = wrap(native.transformStyleAttribute);
25+
module.exports.bundle = wrap(native.bundle);
26+
module.exports.bundleAsync = wrap(native.bundleAsync);
2627
module.exports.browserslistToTargets = require('./browserslistToTargets');
2728
module.exports.composeVisitors = require('./composeVisitors');
2829
module.exports.Features = require('./flags').Features;
30+
31+
function wrap(call) {
32+
return (options) => {
33+
if (typeof options.visitor === 'function') {
34+
let deps = [];
35+
options.visitor = options.visitor({
36+
addDependency(dep) {
37+
deps.push(dep);
38+
}
39+
});
40+
41+
let result = call(options);
42+
if (result instanceof Promise) {
43+
result = result.then(res => {
44+
if (deps.length) {
45+
res.dependencies ??= [];
46+
res.dependencies.push(...deps);
47+
}
48+
return res;
49+
});
50+
} else if (deps.length) {
51+
result.dependencies ??= [];
52+
result.dependencies.push(...deps);
53+
}
54+
return result;
55+
} else {
56+
return call(options);
57+
}
58+
};
59+
}

node/test/composeVisitors.test.mjs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,4 +800,61 @@ test('StyleSheet', () => {
800800
assert.equal(styleSheetExitCalledCount, 2);
801801
});
802802

803+
test('visitor function', () => {
804+
let res = transform({
805+
filename: 'test.css',
806+
minify: true,
807+
code: Buffer.from(`
808+
@dep "foo.js";
809+
@dep2 "bar.js";
810+
811+
.foo {
812+
width: 32px;
813+
}
814+
`),
815+
visitor: composeVisitors([
816+
({addDependency}) => ({
817+
Rule: {
818+
unknown: {
819+
dep(rule) {
820+
let file = rule.prelude[0].value.value;
821+
addDependency({
822+
type: 'file',
823+
filePath: file
824+
});
825+
return [];
826+
}
827+
}
828+
}
829+
}),
830+
({addDependency}) => ({
831+
Rule: {
832+
unknown: {
833+
dep2(rule) {
834+
let file = rule.prelude[0].value.value;
835+
addDependency({
836+
type: 'file',
837+
filePath: file
838+
});
839+
return [];
840+
}
841+
}
842+
}
843+
})
844+
])
845+
});
846+
847+
assert.equal(res.code.toString(), '.foo{width:32px}');
848+
assert.equal(res.dependencies, [
849+
{
850+
type: 'file',
851+
filePath: 'foo.js'
852+
},
853+
{
854+
type: 'file',
855+
filePath: 'bar.js'
856+
}
857+
]);
858+
});
859+
803860
test.run();

node/test/visitor.test.mjs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,4 +1170,119 @@ test('visit stylesheet', () => {
11701170
assert.equal(res.code.toString(), '.bar{width:80px}.foo{width:32px}');
11711171
});
11721172

1173+
test('visitor function', () => {
1174+
let res = transform({
1175+
filename: 'test.css',
1176+
minify: true,
1177+
code: Buffer.from(`
1178+
@dep "foo.js";
1179+
1180+
.foo {
1181+
width: 32px;
1182+
}
1183+
`),
1184+
visitor: ({addDependency}) => ({
1185+
Rule: {
1186+
unknown: {
1187+
dep(rule) {
1188+
let file = rule.prelude[0].value.value;
1189+
addDependency({
1190+
type: 'file',
1191+
filePath: file
1192+
});
1193+
return [];
1194+
}
1195+
}
1196+
}
1197+
})
1198+
});
1199+
1200+
assert.equal(res.code.toString(), '.foo{width:32px}');
1201+
assert.equal(res.dependencies, [{
1202+
type: 'file',
1203+
filePath: 'foo.js'
1204+
}]);
1205+
});
1206+
1207+
test('visitor function works with style attributes', () => {
1208+
let res = transformStyleAttribute({
1209+
filename: 'test.css',
1210+
minify: true,
1211+
code: Buffer.from('height: 12px'),
1212+
visitor: ({addDependency}) => ({
1213+
Length() {
1214+
addDependency({
1215+
type: 'file',
1216+
filePath: 'test.json'
1217+
});
1218+
}
1219+
})
1220+
});
1221+
1222+
assert.equal(res.dependencies, [{
1223+
type: 'file',
1224+
filePath: 'test.json'
1225+
}]);
1226+
});
1227+
1228+
test('visitor function works with bundler', () => {
1229+
let res = bundle({
1230+
filename: 'tests/testdata/a.css',
1231+
minify: true,
1232+
visitor: ({addDependency}) => ({
1233+
Length() {
1234+
addDependency({
1235+
type: 'file',
1236+
filePath: 'test.json'
1237+
});
1238+
}
1239+
})
1240+
});
1241+
1242+
assert.equal(res.dependencies, [
1243+
{
1244+
type: 'file',
1245+
filePath: 'test.json'
1246+
},
1247+
{
1248+
type: 'file',
1249+
filePath: 'test.json'
1250+
},
1251+
{
1252+
type: 'file',
1253+
filePath: 'test.json'
1254+
}
1255+
]);
1256+
});
1257+
1258+
test('works with async bundler', async () => {
1259+
let res = await bundleAsync({
1260+
filename: 'tests/testdata/a.css',
1261+
minify: true,
1262+
visitor: ({addDependency}) => ({
1263+
Length() {
1264+
addDependency({
1265+
type: 'file',
1266+
filePath: 'test.json'
1267+
});
1268+
}
1269+
})
1270+
});
1271+
1272+
assert.equal(res.dependencies, [
1273+
{
1274+
type: 'file',
1275+
filePath: 'test.json'
1276+
},
1277+
{
1278+
type: 'file',
1279+
filePath: 'test.json'
1280+
},
1281+
{
1282+
type: 'file',
1283+
filePath: 'test.json'
1284+
}
1285+
]);
1286+
});
1287+
11731288
test.run();

wasm/index.mjs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,47 @@ export default async function init(input) {
3838
}
3939

4040
export function transform(options) {
41-
return wasm.transform(options);
41+
return wrap(wasm.transform, options);
4242
}
4343

4444
export function transformStyleAttribute(options) {
45-
return wasm.transformStyleAttribute(options);
45+
return wrap(wasm.transformStyleAttribute, options);
4646
}
4747

4848
export function bundle(options) {
49-
return wasm.bundle(options);
49+
return wrap(wasm.bundle, options);
5050
}
5151

5252
export function bundleAsync(options) {
53-
return bundleAsyncInternal(options);
53+
return wrap(bundleAsyncInternal, options);
54+
}
55+
56+
function wrap(call, options) {
57+
if (typeof options.visitor === 'function') {
58+
let deps = [];
59+
options.visitor = options.visitor({
60+
addDependency(dep) {
61+
deps.push(dep);
62+
}
63+
});
64+
65+
let result = call(options);
66+
if (result instanceof Promise) {
67+
result = result.then(res => {
68+
if (deps.length) {
69+
res.dependencies ??= [];
70+
res.dependencies.push(...deps);
71+
}
72+
return res;
73+
});
74+
} else if (deps.length) {
75+
result.dependencies ??= [];
76+
result.dependencies.push(...deps);
77+
}
78+
return result;
79+
} else {
80+
return call(options);
81+
}
5482
}
5583

5684
export { browserslistToTargets } from './browserslistToTargets.js';

0 commit comments

Comments
 (0)