Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
attr-transform.macro
Advanced tools
This is a babel-plugin-macros for built-time, pre-processor, transformation of jsx attributes (JSX.Attribute)
It is inspired by twin.macro and original design as pre-processor for that macro.
But it can be used to transform attributes in many scenarios.
A lot of the logic for the transformation is set in the config file, so it's up to the user to define the transformation.
There will most likely be npm packages with predefined config for different use cases in the future.
It can also (but is not design for that purpose only), validate attributes on an JSX.Element and throw an error if the attributes are not valid.
npm install attr-transform.macro --save-dev
Or
yarn add -D attr-transform.macro
This is a babel-plugin-macros, and work from babel detecting an import with the ".macro" post name.
The pre-build step is triggered by an import of the "attr-transform.macro" in a file.
import "attr-transform.macro"
The order of import define the order of the pre-process.
If you want to use it with twin.macro, the import most come before twin.macro.
import "attr-transform.macro"
import "twin.macro"
The config is a list of elements and attributes to transform, including a list of options for the transformation.
// Rename attribute
let from = <Button onclick={} />;
let to = <Button onClick={} />;
// config:
elms:[
attr:[
{
matchName: /onclick/i,
replaceName: "onClick"
}
]
]
// Change value
let from = <div tw="flex-col" />;
let to = <div tw="flex flex-col" />;
// config:
elms:[
attr:[
{
matchName: "tw",
replaceName: ({value}) => {
const values = value.split(" ");
if (values.includes("flex-col") || !values.includes("flex")) {
values.unshift("flex");
}
return values.join(" ");
},
}
]
]
// Rename attribute and change value
let from = <div name-per data-prop="t" />;
let to = <div name="per" data-prop="t" />;
// config:
elms:[
attr:[
{
matchName: /(\w)\-(\w)/,
dontMatchName: /data\-/,
replaceName: ({match}) => match[0],
replaceValue: ({match}) => match[1]
}
]
]
// Collect and transfor (including remove atttribus and create new attribute)
let from = <div flex between p1 />;
let to = <div tw="flex items-center justify-between p-1" />;
// config:
elms:[
attr:[
{
matchName: "flex",
collect: true,
remove: true,
},
{
matchName: "between",
value: "justify-between",
collect: true,
remove: true,
},
{
name: "padding",
matchName: /p([1-9])/,
value: ({match}) => `p-${match[1]}`,
validate: ({ collectedAttributes }) => {
const countPadding = collectedAttributes.filter((attr) => attr.attrConfig.name === "padding").length
if (countPadding > 1) {
return "You can't use more than one 'padding ( p1, p2, ..., p9 )' on the same element"
}
},
collect: true,
remove: true,
},
{
description: "collect the original value if exists",
matchName: "tw",
collect: true
}
],
actions: [
{
description: "Create tw if not exists",
createAttribute: "tw", // ensure tw attribute exists
condition: ({ collectedAttributes }) => collectedAttributes.length > 0,
value: ({ collectedAttributes }) => {
const flex = collectedAttributes.some((attr) => attr.name === "flex")
const col = collectedAttributes.some((attr) => attr.name === "col")
let value = collectedAttributes.some((attr) => attr.value ?? "").filter(x => !!x).join(" ");
if(flex){
tw += " flex"
if(col){
tw += " flex-col"
}else{
tw += " items-center"
}
}
return value
}
}
]
]
The transformation is done in 5 steps:
This pre-processor "just" take the (js) JSX attribute that matches and transform them.
To have typing for the attribute, you must either add the properties to the component, or add global IntrinsicAttributes properties to a difinitions file.
See TS doc for jsx: https://www.typescriptlang.org/docs/handbook/jsx.html
// ./types/attr-typing.d.ts
declare global {
namespace JSX {
// interface ElementAttributesProperty {
// p1?: boolean
// flex?: boolean
// line?: boolean
// }
interface IntrinsicAttributes {
/** tw: "flex" */
flex?: boolean;
/** tw: "flex items-center justify-start" */
line?: boolean;
/** tw: "p1" */
p1?: boolean;
/** tw: "p2" */
p2?: boolean;
}
}
}
(optional): The config can be added in babel-plugin-macro's config (either in a babel.config.js or in package.json) under the macro name "attr-transform" key.
Or it can be loaded from a config file (default: attr-transform.config.js
) in the root of the project.
(You can change the file name with the "config" properties in the babel-plugin-macros config)
See babel-plugin-macros docs for more info
const config =
{
// a list of elements to transform
elms: [
{
match: "div", // match only div elements (If no "match" are provided, all elements are matched)
attrs:[
{
matchName: "name", // See "AttrConfig" below, for all options
}
]
}
]
}
Full config types (interfaces):
export type AttrTransformMacroParams = Omit<MacroParams, "config"> & {
config?: AttrTransformConfig;
};
export type AttrTransformConfig = {
config?: string | false; // file name (default: attr-transform.config.js)
devMode?: boolean;
elms?: ElmConfig[];
};
export type ElmConfig = {
match?: string | RegExp; // Optional match // Special "*" matches all
dontMatch?: string | RegExp; // Optional dontMatch
attrs: AttrConfig[];
actions?: PostMatchAction[];
};
// elm/action methods
export type ConditionFunc = (matchedAttributes: PostActionMatch) => boolean;
export type ActionValueFunc = (postActionMatch: PostActionMatch) => FullLegalAttributeValues;
export type CreateActionValueFunc = (postActionMatch: PostActionMatch) => string | T.JSXAttribute;
// attri methods
export type MatchValueFunc = (attrMatch: AttrMatch) => boolean;
export type AttrValueFunc = (attrMatch: AttrMatch) => FullLegalAttributeValues;
export type AttrStringValueFunc = (attrMatch: AttrMatch) => string;
export type CreateValueFunc = (attrMatch: AttrMatch) => string | T.JSXAttribute;
export type ValidateValueFunc = (attrMatch: AttrMatch) => string | undefined;
export type ActionFunc = (attrMatch: AttrMatch) => void;
export type AttrConfig = {
// a name for the config (for debugging and overview)
name?: string;
// a name for the config (for debugging and overview)
description?: string;
// a list of tags. Use for more complex match and replace (Like selecteing all collected with a tag)
tags?: string[];
// mosth match the attribute name ( Fx: "padding" or "p1" or /p(1-9)/)
matchName?: string | RegExp;
// most not match the attribute name ( Fx: "padding" or "p1" or /p(1-9)/)
dontMatchName?: string | RegExp;
// Calcalate a value from the AttrMatch Object. ( Fx: "p-1" or ({match}) => `p-${match[1]}` )
value?: string | AttrValueFunc;
// Replace the value of the matched attribute with the calculated value
replaceValue?: string | AttrValueFunc;
replaceName?: string | AttrStringValueFunc;
// Validate the value (Throw MacroError if not valid)
validate?: ValidateValueFunc;
// collect AttrMatch Object to be used by other attributes config (This is not need, mostly to indicate that it's macthed)
collect?: boolean;
// remove this attribute after processing of all attributes
remove?: boolean;
};
export type PostMatchAction = {
// a name for the config (for debugging and overview)
name?: string;
// a name for the config (for debugging and overview)
description?: string;
// a list of tags. Use for more complex match and replace (Like selecteing all collected with a tag)
tags?: string[];
// conditions
condition?: ConditionFunc;
// create the attribute if not exists
createAttribute?: string | CreateActionValueFunc;
replaceName?: string | AttrStringValueFunc;
value?: string | ActionValueFunc;
action?: ActionFunc;
};
export type LegalAttributeValues =
| T.JSXElement
| T.JSXFragment
| T.StringLiteral
| T.JSXExpressionContainer
| null
| undefined;
export type FullLegalAttributeValues = string | number | boolean | LegalAttributeValues;
// Meta data for the attribute use for calculate the value, validate etc in the config
export type PostActionMatch = {
name: string;
value: FullLegalAttributeValues;
postMatchAction: PostMatchAction;
allMatchingAttributes: AttrMatch[]; // all props found for this element
collectedAttributes: AttrMatch[]; // all props found for this element
tagMatch?: RegExpMatchArray | null;
elmNodePath: NodePath<T.JSXOpeningElement>;
macroParams: AttrTransformMacroParams;
};
export type AttrMatch = {
name: string;
value: FullLegalAttributeValues;
attrConfig: AttrConfig;
matchFunction?: MatchValueFunc;
dontMatchFunction?: MatchValueFunc;
validateFunction?: ValidateValueFunc;
valueFunction?: AttrValueFunc;
match?: RegExpMatchArray | null;
tagMatch?: RegExpMatchArray | null;
allMatchingAttributes: AttrMatch[]; // all props found for this element
collectedAttributes: AttrMatch[]; // all props found for this element
// babel types for advanced usage
nodePath: NodePath<T.JSXAttribute>;
elmNodePath: NodePath<T.JSXOpeningElement>;
// babel types for super advanced usage
//state: Babel.PluginPass
macroParams: AttrTransformMacroParams;
collected?: boolean;
remove?: boolean;
};
attr-transform.config.js
/** @type {import('attr-transform.macro/types/attr-transform.config.d.ts').AttrTransformConfig} */
module.exports = {
elms: [
{
// match: "div" // match all divs
dontMatch: "img", // dont match img
attrs: [
{
name: "tw padding",
match: /p([0-9])/,
value: ({ match }) => `p-${match?.[1]}`,
validate: ({ collectedAttributes }) => {
const countPadding = collectedAttributes.filter((attr) => attr.attrConfig.name === "tw padding").length
if (countPadding > 1) {
return "You can't use more than one 'tw padding ( p1, p2, ..., p9 )' on the same element"
}
},
collect: true,
remove: true,
},
{
name: "tw colors",
match: /(red|blue|green)/,
value: ({ match }) => `text-${match?.[1]}-600`,
collect: true,
remove: true,
},
{
name: "flex",
match: "flex",
validate: (matchAttr) => {
const notAllowed = matchAttr.allMatchingAttributes.some((attr) => attr.name === "line")
if (notAllowed) {
return "You can't use both 'flex' and 'line' on the same element"
}
},
value: "flex items-center justify-start",
collect: true,
remove: true,
},
{
name: "standard line element",
match: "line",
value: "flex items-center justify-start",
collect: true,
remove: true,
},
{
name: "tw attribute",
description: "Collect tw value if exists",
match: "tw",
collect: true
}
],
actions: [
{
name: "Update Tw attribute",
description: "Create tw attribute if not exists, and append collected values (including previous tw values)",
createAttribute: "tw", // ensure tw attribute exists
condition: ({ collectedAttributes }) => collectedAttributes.length > 0,
replaceValue: ({ collectedAttributes }) => {
const value = collectedAttributes.map((attr) => attr.value).join(" ")
return value
}
}
]
},
],
}
FAQs
jsx attribute pre-processor
The npm package attr-transform.macro receives a total of 0 weekly downloads. As such, attr-transform.macro popularity was classified as not popular.
We found that attr-transform.macro demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.