How it works?
wyw-in-js
creates an entrypoint for each file to process, and transforms this entrypoint by running the workflow action.
Entrypoint
We runs the worlflow action with actionRunner
and actionsCache
for better performance by avoiding running the same action twice. A workflow contains the following nested actions:
workflow
├── processEntrypoint
│ ├── explodeReexports
│ │ ├── resolveImports
│ │ └── getExports
│ │ └── resolveImports
│ └── transform
│ ├── resolveImports
│ └── processImports
├── evalFile
├── collect
└── extract
Workflow Action
The entry point for file processing. Sequentially calls processEntrypoint
, evalFile
, collect
, and extract
. Returns the result of transforming the source code as well as all artifacts obtained from code execution.
Input
import { css } from '@foooo/processor'; /* Your processor */
import { getColor } from './get-color';
export const c1 = css`
background-color: ${getColor('hawk')};
color: ${({ theme }) => theme.palette.primary.main};
font-size: ${({ theme }) => theme.size.font.h1};
`;
export const c2 = css(({ theme }) => ({
backgroundColor: getColor('wild'),
color: theme.palette.error.main,
fontSize: theme.size.font.h2,
}));
/* usually defined in your processor, will be used when we're building the processor */
export const theme = {
palette: {
primary: {
main: 'red',
},
error: {
main: 'orange',
},
},
size: {
font: {
h1: '3rem',
h2: '2.2rem',
},
},
};
// ./get-color.js
export const getColor = (variant) => {
if (variant === 'hawk') {
return 'pink';
}
if (variant === 'wild') {
return 'purple';
}
if (variant === 'king') {
return 'green';
}
return 'white';
};
Output
The output can be different depending on the implementation of your processor!
const c1 = 'abcd';
const c2 = 'wxyz';
.abcd {
background-color: pink;
color: red;
font-size: 3rem;
}
.wxyz {
background-color: purple;
color: orange;
font-size: 2.2rem;
}
ProcessEntrypoint Action
The first stage of processing an entrypoint. This stage is responsible for:
- scheduling the
explodeReexports
action - scheduling the
transform
action - rescheduling itself if the entrypoint is superseded
ExplodeReexports Action
Replaces wildcard reexports with named reexports. Recursively emits getExports
for each reexported module, and replaces wildcard with resolved named.
Input
export * from './foo'; /* wildcard reexports */
// ./foo.js
export const foo1 = 'foo1';
export const foo2 = 'foo2';
export const foo3 = 'foo3';
Output
export { foo1, foo2, foo3 } from './foo';
ResolveImports Action
Resolves specified imports with a provided resolver.
GetExports Action
Collects exports and re-exports. Recursively emits getExports
for each reexported module.
// index.js
export * from 'a'; /* wildcard re-export, emit getExports for 'a' */
// a.js
export * from 'b'; /* wildcard re-export, emit getExports for 'b' */
// b.js
export * from 'c'; /* wildcard re-export, emit getExports for 'c' */
// c.js
export const c = 'c';
Transform Action
Prepares the code for evaluation. This includes removing dead and potentially unsafe code. Emits resolveImports
and processImports
events.
Transform Preeval
Finds the defined processors along with their usages.
import { /* defined processor */ css } from '@foooo/processor';
import { getColor } from './get-color';
export const c1 = /* first usage */ css`
background-color: ${getColor('hawk')};
color: ${({ theme }) => theme.palette.primary.main};
font-size: ${({ theme }) => theme.size.font.h1};
`;
export const c2 = /* second usage */ css(({ theme }) => ({
backgroundColor: getColor('wild'),
color: theme.palette.error.main,
fontSize: theme.size.font.h2,
}));
After all usages are found, we extract the expression for each usage. The extracted expressions _exp
, _exp2
, _exp3
and _exp4
will be inserted as close as possible before its usage.
import { css } from '@foooo/processor';
import { getColor } from './get-color';
const _exp = () => getColor('hawk');
const _exp2 = () => ({ theme }) => theme.palette.primary.main;
const _exp3 = () => ({ theme }) => theme.size.font.h1;
export const c1 = css`
background-color: ${getColor('hawk')};
color: ${({ theme }) => theme.palette.primary.main};
font-size: ${({ theme }) => theme.size.font.h1};
`;
const _exp4 = () => ({ theme }) => ({
backgroundColor: getColor('wild'),
color: theme.palette.error.main,
fontSize: theme.size.font.h2,
});
export const c2 = css(({ theme }) => ({
backgroundColor: getColor('wild'),
color: theme.palette.error.main,
fontSize: theme.size.font.h2,
}));
Each usage requires one processor instance for processing. In this case, we have two processor instance.
Assume the first css
processor has className = 'abcd'
and dependencies = [_exp, _exp2, _exp3]
, second css
processor has className = 'wxyz'
and dependencies = [_exp4]
. After the preeval stage:
import { css } from '@foooo/processor';
import { getColor } from './get-color';
const _exp = () => getColor('hawk');
const _exp2 = () => ({ theme }) => theme.palette.primary.main;
const _exp3 = () => ({ theme }) => theme.size.font.h1;
export const c1 = 'abcd'; /* do evaltime replacement */
const _exp4 = () => ({ theme }) => ({
backgroundColor: getColor('wild'),
color: theme.palette.error.main,
fontSize: theme.size.font.h2,
});
export const c2 = 'wxyz'; /* do evaltime replacement */
export const __wywPreval = { /* collect all dependencies in this object */
_exp: _exp,
_exp2: _exp2,
_exp3: _exp3,
_exp4: _exp4
};
Transform Evaluator
Finds all irrelevant code and cuts it out of the file. We use the shaker
plugin by default. For the root entrypoint, we only care about the __wywPreval
named export. For child entrypoints like the one for get-color.js
in our example, all its code will be shaked out except for the getColor
named export and its dependencies.
ProcessImports Action
Creates new entrypoints and emits processEntrypoint
for each resolved import. In our example, we create a child entrypoint for the get-color
module to process getColor
.
import { css } from '@foooo/processor';
import { getColor } from './get-color'; /* child entrypoint with only=['getColor'] */
const _exp = () => getColor('hawk');
// ...
EvalFile Action
Executes the code prepared in previous steps, the processEntrypoint
action, within the current Entrypoint
. Returns all exports that were requested in only
.
In this step, we create Module
for each entrypoint and evaluate each module recursively with node:vm
(opens in a new tab). After all modules are evaluated, we execute the extracted expressions _exp
, _exp2
, _exp3
and _exp4
which are extracted in Transform Preeval step.
const _exp = () => getColor('hawk');
const _exp2 = () => ({ theme }) => theme.palette.primary.main;
const _exp3 = () => ({ theme }) => theme.size.font.h1;
const _exp4 = () => ({ theme }) => ({
backgroundColor: getColor('wild'),
color: theme.palette.error.main,
fontSize: theme.size.font.h2,
});
Now we have a valueCache
map for building our processor instances of this entrypoint in next step.
const valueCache = new Map([
['_exp', 'pink'],
['_exp2', [Function]], /* ({ theme }) => theme.palette.primary.main */
['_exp3', [Function]], /* ({ theme }) => theme.size.font.h1 */
['_exp4', [Function]], /* ({ thene }) => ({ backgroundColor: getColor('wild'), ... }) */
])
Collect Action
Builds and does runtime replacement for each processor. Removes __wywPreval
object and all related code.
Collect action calls processor.build
with valueCache
as the argument. You can decide how to build the artifacts. Here is a simple example:
export default class CssProcessor extends BaseProcessor {
// ...
build(values: Map<string, unknown>) {
let cssText = '';
const props = { theme };
if (this.callParam[0] === 'template') {
this.callParam[1].forEach((item: any) => {
if ('kind' in item) {
const evaluatedValue = values.get(item.ex.name);
cssText +=
typeof evaluatedValue === 'function'
? evaluatedValue(props)
: evaluatedValue;
} else {
cssText += item.value.cooked;
}
});
} else if (this.callParam[0] === 'call') {
const evaluatedValue = values.get(this.callParam[1].ex.name) as Function;
const obj = evaluatedValue(props);
cssText += '\n';
Object.entries(obj).forEach(([key, value]) => {
cssText += ` ${toKebabCase(key)}: ${value};\n`;
});
}
this.artifacts.push([
'css',
[
/* Rules */
{
[this.adSelector]: {
className: this.className,
cssText,
// ...
},
},
/* Replacements */
[
// ...
],
],
]);
}
// ...
}
The artifacts of our first processor:
[
'css',
{
'.abcd': {
className: 'abcd',
cssText: '\n background-color: pink;\n color: red;\n font-size: 3rem;\n',
// ...
},
},
[
// ...
],
];
The second processor:
[
'css',
{
'.wxyz': {
className: 'wxyz',
cssText: '\n background-color: purple;\n color: orange;\n font-size: 2.2rem;\n',
// ...
},
},
[
// ...
],
];
And after the code removal, our remaining code becomes:
const c1 = 'abcd';
const c2 = 'wxyz';
Extract Action
Extracts artifacts (e.g. CSS) from processors.