Skip to content

Commit

Permalink
Add support for returning media queries as raw strings in transform API
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett committed Apr 18, 2023
1 parent c97c950 commit f333cd9
Show file tree
Hide file tree
Showing 8 changed files with 566 additions and 183 deletions.
309 changes: 154 additions & 155 deletions node/ast.d.ts

Large diffs are not rendered by default.

19 changes: 12 additions & 7 deletions node/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface TransformOptions<C extends CustomAtRules> {
sourceMap?: boolean,
/** An input source map to extend. */
inputSourceMap?: string,
/**
/**
* An optional project root path, used as the source root in the output source map.
* Also used to generate relative paths for sources used in CSS module hashes.
*/
Expand All @@ -34,7 +34,7 @@ export interface TransformOptions<C extends CustomAtRules> {
* urls later (after bundling). Dependencies are returned as part of the result.
*/
analyzeDependencies?: boolean | DependencyOptions,
/**
/**
* Replaces user action pseudo classes with class names that can be applied from JavaScript.
* This is useful for polyfills, for example.
*/
Expand Down Expand Up @@ -69,15 +69,20 @@ export interface TransformOptions<C extends CustomAtRules> {

// This is a hack to make TS still provide autocomplete for `property` vs. just making it `string`.
type PropertyStart = '-' | '_' | 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';
type ReturnedDeclaration = Declaration | {
export type ReturnedDeclaration = Declaration | {
/** The property name. */
property: `${PropertyStart}${string}`,
/** The raw string value for the declaration. */
raw: string
};

export type ReturnedMediaQuery = MediaQuery | {
/** The raw string value for the media query. */
raw: string
};

type FindByType<Union, Name> = Union extends { type: Name } ? Union : never;
type ReturnedRule = Rule<ReturnedDeclaration>;
export type ReturnedRule = Rule<ReturnedDeclaration, ReturnedMediaQuery>;
type RequiredValue<Rule> = Rule extends { value: object }
? Rule['value'] extends StyleRule
? Rule & { value: Required<StyleRule> & { declarations: Required<DeclarationBlock> } }
Expand Down Expand Up @@ -186,8 +191,8 @@ export interface Visitor<C extends CustomAtRules> {
Time?(time: Time): Time | void;
CustomIdent?(ident: string): string | void;
DashedIdent?(ident: string): string | void;
MediaQuery?(query: MediaQuery): MediaQuery | MediaQuery[] | void;
MediaQueryExit?(query: MediaQuery): MediaQuery | MediaQuery[] | void;
MediaQuery?(query: MediaQuery): ReturnedMediaQuery | ReturnedMediaQuery[] | void;
MediaQueryExit?(query: MediaQuery): ReturnedMediaQuery | ReturnedMediaQuery[] | void;
SupportsCondition?(condition: SupportsCondition): SupportsCondition;
SupportsConditionExit?(condition: SupportsCondition): SupportsCondition;
Selector?(selector: Selector): Selector | Selector[] | void;
Expand Down Expand Up @@ -393,7 +398,7 @@ export interface TransformAttributeOptions {
targets?: Targets,
/**
* Whether to analyze `url()` dependencies.
* When enabled, `url()` dependencies are replaced with hashed placeholders
* When enabled, `url()` dependencies are replaced with hashed placeholders
* that can be replaced with the final urls later (after bundling).
* Dependencies are returned as part of the result.
*/
Expand Down
62 changes: 62 additions & 0 deletions node/test/visitor.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -988,4 +988,66 @@ test('nth of S to nth-of-type', () => {
assert.equal(res.code.toString(), 'a:nth-of-type(2n){color:red}');
});

test('media query raw', () => {
let res = transform({
filename: 'test.css',
minify: true,
code: Buffer.from(`
@breakpoints {
.m-1 {
margin: 10px;
}
}
`),
customAtRules: {
breakpoints: {
prelude: null,
body: "rule-list",
},
},
visitor: {
Rule: {
custom: {
breakpoints({ body, loc }) {
/** @type {import('lightningcss').ReturnedRule[]} */
const value = [];

for (let rule of body.value) {
if (rule.type !== 'style') {
continue;
}
const clone = structuredClone(rule);
for (let selector of clone.value.selectors) {
for (let component of selector) {
if (component.type === 'class') {
component.name = `sm:${component.name}`;
}
}
}

value.push(rule);
value.push({
type: "media",
value: {
rules: [clone],
loc,
query: {
mediaQueries: [
{ raw: '(min-width: 500px)' }
]
}
}
});
}

return value;
}
}
}
}
});

assert.equal(res.code.toString(), '.m-1{margin:10px}@media (width>=500px){.sm\\:m-1{margin:10px}}');
});

test.run();
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
"detect-libc": "^1.0.3"
},
"devDependencies": {
"@babel/parser": "^7.21.4",
"@babel/traverse": "^7.21.4",
"@codemirror/lang-css": "^6.0.1",
"@codemirror/lang-javascript": "^6.1.2",
"@codemirror/lint": "^6.1.0",
Expand Down Expand Up @@ -69,6 +71,7 @@
"posthtml-prism": "^1.0.4",
"process": "^0.11.10",
"puppeteer": "^12.0.1",
"recast": "^0.22.0",
"sharp": "^0.31.1",
"util": "^0.12.4",
"uvu": "^0.5.6"
Expand Down
18 changes: 18 additions & 0 deletions patches/@babel+types+7.21.4.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
diff --git a/node_modules/@babel/types/lib/retrievers/getBindingIdentifiers.js b/node_modules/@babel/types/lib/retrievers/getBindingIdentifiers.js
index 19903eb..6bc04a8 100644
--- a/node_modules/@babel/types/lib/retrievers/getBindingIdentifiers.js
+++ b/node_modules/@babel/types/lib/retrievers/getBindingIdentifiers.js
@@ -59,6 +59,13 @@ getBindingIdentifiers.keys = {
InterfaceDeclaration: ["id"],
TypeAlias: ["id"],
OpaqueType: ["id"],
+ TSDeclareFunction: ["id"],
+ TSEnumDeclaration: ["id"],
+ TSImportEqualsDeclaration: ["id"],
+ TSInterfaceDeclaration: ["id"],
+ TSModuleDeclaration: ["id"],
+ TSNamespaceExportDeclaration: ["id"],
+ TSTypeAliasDeclaration: ["id"],
CatchClause: ["param"],
LabeledStatement: ["label"],
UnaryExpression: ["argument"],
82 changes: 73 additions & 9 deletions scripts/build-ast.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
const { compileFromFile } = require('json-schema-to-typescript');
const fs = require('fs');
const recast = require('recast');
const traverse = require('@babel/traverse').default;
const {parse} = require('@babel/parser');
const t = require('@babel/types');

const skip = {
FillRule: true,
Expand All @@ -17,14 +21,74 @@ const skip = {
compileFromFile('node/ast.json', {
additionalProperties: false
}).then(ts => {
ts = ts.replaceAll('For_DefaultAtRule', '')
.replaceAll('interface DeclarationBlock', 'interface DeclarationBlock<D = Declaration>')
.replaceAll('DeclarationBlock;', 'DeclarationBlock<D>;')
.replaceAll('Declaration[]', 'D[]')
.replace(/(interface )?([A-Z][a-zA-Z]*Rule)/g, (m, x, n) => skip[n] ? m : `${m}<D${x ? ' = Declaration' : ''}>`)
.replaceAll(': Rule', ': Rule<D>')
.replaceAll('type Rule =', 'type Rule<D = Declaration> =')
.replace(/Keyframe(?![a-zA-Z])/g, 'Keyframe<D>')
.replaceAll('StyleSheet', 'StyleSheet<D = Declaration>');
ts = ts.replaceAll('For_DefaultAtRule', '');

// Use recast/babel to make some types generic so we can replace them in index.d.ts.
let ast = recast.parse(ts, {
parser: {
parse() {
return parse(ts, {
sourceType: 'module',
plugins: ['typescript'],
tokens: true
});
}
}
});

traverse(ast, {
Program(path) {
process(path.scope.getBinding('Declaration'));
process(path.scope.getBinding('MediaQuery'));
}
});

ts = recast.print(ast, {objectCurlySpacing: false}).code;
fs.writeFileSync('node/ast.d.ts', ts)
});

function process(binding) {
// Follow the references upward from the binding to add generics.
for (let reference of binding.referencePaths) {
if (reference.node !== binding.identifier) {
genericize(reference, binding.identifier.name);
}
}
}

function genericize(path, name, seen = new Set()) {
if (seen.has(path.node)) return;
seen.add(path.node);

// Find the parent declaration of the reference, and add a generic if needed.
let parent = path.findParent(p => p.isDeclaration());
if (!parent.node.typeParameters) {
parent.node.typeParameters = t.tsTypeParameterDeclaration([]);
}
let params = parent.get('typeParameters');
let param = params.node.params.find(p => p.default.typeName.name === name);
if (!param) {
params.pushContainer('params', t.tsTypeParameter(null, t.tsTypeReference(t.identifier(name)), name[0]));
}

// Replace the reference with the generic, or add a type parameter.
if (path.node.name === name) {
path.replaceWith(t.identifier(name[0]));
} else {
if (!path.parent.typeParameters) {
path.parent.typeParameters = t.tsTypeParameterInstantiation([]);
}
let param = path.parent.typeParameters.params.find(p => p.typeName.name === name[0]);
if (!param) {
path.parentPath.get('typeParameters').pushContainer('params', t.tsTypeReference(t.identifier(name[0])));
}
}

// Keep going to all references of this reference.
let binding = path.scope.getBinding(parent.node.id.name);
for (let reference of binding.referencePaths) {
if (reference.node !== binding.identifier) {
genericize(reference, name, seen);
}
}
}
55 changes: 45 additions & 10 deletions src/media_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,11 +256,7 @@ impl<'a> schemars::JsonSchema for MediaType<'a> {
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(lightningcss_derive::IntoOwned))]
#[cfg_attr(feature = "visitor", visit(visit_media_query, MEDIA_QUERIES))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "camelCase")
)]
#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(rename_all = "camelCase"))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct MediaQuery<'i> {
/// The qualifier for this query.
Expand All @@ -273,11 +269,8 @@ pub struct MediaQuery<'i> {
pub condition: Option<MediaCondition<'i>>,
}

impl<'i> MediaQuery<'i> {
/// Parse a media query given css input.
///
/// Returns an error if any of the expressions is unknown.
pub fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
impl<'i> Parse<'i> for MediaQuery<'i> {
fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let (qualifier, explicit_media_type) = input
.try_parse(|input| -> Result<_, ParseError<'i, ParserError<'i>>> {
let qualifier = input.try_parse(Qualifier::parse).ok();
Expand All @@ -301,7 +294,9 @@ impl<'i> MediaQuery<'i> {
condition,
})
}
}

impl<'i> MediaQuery<'i> {
fn transform_custom_media(
&mut self,
loc: Location,
Expand Down Expand Up @@ -446,6 +441,46 @@ impl<'i> ToCss for MediaQuery<'i> {
}
}

#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(untagged))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
enum MediaQueryOrRaw<'i> {
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
MediaQuery {
qualifier: Option<Qualifier>,
#[cfg_attr(feature = "serde", serde(borrow))]
media_type: MediaType<'i>,
condition: Option<MediaCondition<'i>>,
},
Raw {
raw: CowArcStr<'i>,
},
}

impl<'i, 'de: 'i> serde::Deserialize<'de> for MediaQuery<'i> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let mq = MediaQueryOrRaw::deserialize(deserializer)?;
match mq {
MediaQueryOrRaw::MediaQuery {
qualifier,
media_type,
condition,
} => Ok(MediaQuery {
qualifier,
media_type,
condition,
}),
MediaQueryOrRaw::Raw { raw } => {
let res =
MediaQuery::parse_string(raw.as_ref()).map_err(|_| serde::de::Error::custom("Could not parse value"))?;
Ok(res.into_owned())
}
}
}
}

enum_property! {
/// A binary `and` or `or` operator.
pub enum Operator {
Expand Down
Loading

0 comments on commit f333cd9

Please sign in to comment.