next-yak 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -0
- package/dist/cssLiteral.d.ts +11 -0
- package/dist/cssLiteral.d.ts.map +1 -0
- package/dist/cssLiteral.js +63 -0
- package/dist/cssLiteral.js.map +1 -0
- package/dist/cssLiteral.jsx +63 -0
- package/dist/cssLiteral.jsx.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/styled.d.ts +183 -0
- package/dist/styled.d.ts.map +1 -0
- package/dist/styled.js +31 -0
- package/dist/styled.js.map +1 -0
- package/dist/styled.jsx +30 -0
- package/dist/styled.jsx.map +1 -0
- package/loaders/__tests__/cssloader.test.ts +210 -0
- package/loaders/__tests__/tsloader.test.ts +159 -0
- package/loaders/cssloader.cjs +183 -0
- package/loaders/lib/hash.cjs +60 -0
- package/loaders/lib/loadConfigOnce.cjs +17 -0
- package/loaders/lib/quasiClassifier.cjs +33 -0
- package/loaders/lib/replaceQuasiExpressionTokens.cjs +56 -0
- package/loaders/lib/stripCssComments.cjs +49 -0
- package/loaders/tsloader.cjs +240 -0
- package/loaders/withYak.cjs +52 -0
- package/loaders/withYak.d.ts +39 -0
- package/package.json +43 -0
- package/runtime/cssLiteral.tsx +100 -0
- package/runtime/index.ts +2 -0
- package/runtime/styled.tsx +48 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replace tokens with predefined values e.g.
|
|
3
|
+
*
|
|
4
|
+
* ```js
|
|
5
|
+
* css`
|
|
6
|
+
* color: red;
|
|
7
|
+
* ${query.xs} {
|
|
8
|
+
* color: blue;
|
|
9
|
+
* }
|
|
10
|
+
* `
|
|
11
|
+
*
|
|
12
|
+
* ```
|
|
13
|
+
* becomes
|
|
14
|
+
* ```js
|
|
15
|
+
* css`
|
|
16
|
+
* color: red;
|
|
17
|
+
* @media (min-width: 0px) {
|
|
18
|
+
* color: blue;
|
|
19
|
+
* }
|
|
20
|
+
* `
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @param {import("@babel/types").TemplateLiteral} quasi
|
|
24
|
+
* @param {Record<string, Record<string, string>>} replaces
|
|
25
|
+
* @param {import("@babel/types")} t
|
|
26
|
+
*/
|
|
27
|
+
module.exports = function replaceTokensInQuasiExpressions(quasi, replaces, t) {
|
|
28
|
+
for (let i = 0; i < quasi.expressions.length; i++) {
|
|
29
|
+
const expression = quasi.expressions[i];
|
|
30
|
+
if (!t.isMemberExpression(expression)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const object = expression.object;
|
|
34
|
+
if (!t.isIdentifier(object)) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const property = expression.property;
|
|
38
|
+
if (!t.isIdentifier(property)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const objectName = object.name;
|
|
42
|
+
const propertyName = property.name;
|
|
43
|
+
const replacement = replaces[objectName]?.[propertyName];
|
|
44
|
+
if (!replacement) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
// delete expression and append replacement to quasi value
|
|
48
|
+
quasi.expressions.splice(i, 1);
|
|
49
|
+
quasi.quasis[i].value.raw += replacement + quasi.quasis[i + 1].value.raw;
|
|
50
|
+
quasi.quasis[i].value.cooked +=
|
|
51
|
+
replacement + quasi.quasis[i + 1].value.cooked;
|
|
52
|
+
// delete next quasi
|
|
53
|
+
quasi.quasis.splice(i + 1, 1);
|
|
54
|
+
i--;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// from https://github.com/sindresorhus/strip-css-comments/tree/main
|
|
2
|
+
/**
|
|
3
|
+
*
|
|
4
|
+
* @param {string} cssString
|
|
5
|
+
*/
|
|
6
|
+
module.exports = function stripCssComments(cssString) {
|
|
7
|
+
let isInsideString = false;
|
|
8
|
+
let currentCharacter = '';
|
|
9
|
+
let comment = '';
|
|
10
|
+
let returnValue = '';
|
|
11
|
+
|
|
12
|
+
for (let index = 0; index < cssString.length; index++) {
|
|
13
|
+
currentCharacter = cssString[index];
|
|
14
|
+
|
|
15
|
+
if (cssString[index - 1] !== '\\' && (currentCharacter === '"' || currentCharacter === '\'')) {
|
|
16
|
+
if (isInsideString === currentCharacter) {
|
|
17
|
+
isInsideString = false;
|
|
18
|
+
} else if (!isInsideString) {
|
|
19
|
+
isInsideString = currentCharacter;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Find beginning of `/*` type comment
|
|
24
|
+
if (!isInsideString && currentCharacter === '/' && cssString[index + 1] === '*') {
|
|
25
|
+
let index2 = index + 2;
|
|
26
|
+
|
|
27
|
+
// Iterate over comment
|
|
28
|
+
for (; index2 < cssString.length; index2++) {
|
|
29
|
+
// Find end of comment
|
|
30
|
+
if (cssString[index2] === '*' && cssString[index2 + 1] === '/') {
|
|
31
|
+
if (cssString[index2 + 2] === '\n') {
|
|
32
|
+
index2++;
|
|
33
|
+
} else if (cssString[index2 + 2] + cssString[index2 + 3] === '\r\n') {
|
|
34
|
+
index2 += 2;
|
|
35
|
+
}
|
|
36
|
+
comment = '';
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
// Store comment text
|
|
40
|
+
comment += cssString[index2];
|
|
41
|
+
}
|
|
42
|
+
// Resume iteration over CSS string from the end of the comment
|
|
43
|
+
index = index2 + 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
returnValue += currentCharacter;
|
|
47
|
+
}
|
|
48
|
+
return returnValue;
|
|
49
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/// @ts-check
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const babel = require("@babel/core");
|
|
4
|
+
const quasiClassifier = require("./lib/quasiClassifier.cjs");
|
|
5
|
+
const replaceQuasiExpressionTokens = require("./lib/replaceQuasiExpressionTokens.cjs");
|
|
6
|
+
const loadConfigOnce = require("./lib/loadConfigOnce.cjs");
|
|
7
|
+
const murmurhash2_32_gc = require("./lib/hash.cjs");
|
|
8
|
+
const { relative, resolve } = require("path");
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Loader for typescript files that use yacijs, it replaces the css template literal with a call to the 'styled' function
|
|
12
|
+
* @param {string} source
|
|
13
|
+
* @this {any}
|
|
14
|
+
* @returns {Promise<string>}
|
|
15
|
+
*/
|
|
16
|
+
module.exports = async function tsloader(source) {
|
|
17
|
+
// ignore files if they don't use yacijs
|
|
18
|
+
if (!source.includes("next-yak")) {
|
|
19
|
+
return source;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Config for replacing tokens in css template literals
|
|
23
|
+
// can be based on a typescript file
|
|
24
|
+
const options = this.getOptions();
|
|
25
|
+
const config = options.configPath ? await loadConfigOnce(
|
|
26
|
+
async () => await this.importModule(resolve(this.rootContext, options.configPath))
|
|
27
|
+
) : {};
|
|
28
|
+
const replaces = config.replaces || {};
|
|
29
|
+
|
|
30
|
+
/** @type {string | null} */
|
|
31
|
+
let hashedFile = null;
|
|
32
|
+
const { rootContext, resourcePath } = this;
|
|
33
|
+
|
|
34
|
+
const { types: t } = babel;
|
|
35
|
+
const fileName = path.basename(this.resourcePath).replace(/\.tsx?/, "");
|
|
36
|
+
// parse source with babel
|
|
37
|
+
const result = babel.transformSync(source, {
|
|
38
|
+
filename: this.resourcePath,
|
|
39
|
+
// Only for parsing - will be removed once moved to a swc or babel plugin
|
|
40
|
+
plugins: [
|
|
41
|
+
[
|
|
42
|
+
"@babel/plugin-syntax-typescript",
|
|
43
|
+
{ isTSX: this.resourcePath.endsWith(".tsx") },
|
|
44
|
+
],
|
|
45
|
+
/**
|
|
46
|
+
* @returns {import("@babel/core").PluginObj<import("@babel/core").PluginPass & {localVarNames: {css?: string, styled?: string}, isImportedInCurrentFile: boolean, classNameCount: number, varIndex: number}>}
|
|
47
|
+
*/
|
|
48
|
+
function () {
|
|
49
|
+
return {
|
|
50
|
+
name: "next-yak",
|
|
51
|
+
pre() {
|
|
52
|
+
// Initialize state variables
|
|
53
|
+
this.localVarNames = {
|
|
54
|
+
css: undefined,
|
|
55
|
+
styled: undefined,
|
|
56
|
+
};
|
|
57
|
+
this.isImportedInCurrentFile = false;
|
|
58
|
+
this.classNameCount = 0;
|
|
59
|
+
this.varIndex = 0;
|
|
60
|
+
},
|
|
61
|
+
visitor: {
|
|
62
|
+
/**
|
|
63
|
+
* @param {import("@babel/traverse").NodePath<import("@babel/types").ImportDeclaration>} path
|
|
64
|
+
*/
|
|
65
|
+
ImportDeclaration(path) {
|
|
66
|
+
const node = path.node;
|
|
67
|
+
if (
|
|
68
|
+
node.source.value !== "next-yak"
|
|
69
|
+
) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Import 'yacijs' styles and assign to '__styleYak'
|
|
74
|
+
// use webpacks !=! syntax to pretend that the typescript file is actually a css-module
|
|
75
|
+
path.insertAfter(
|
|
76
|
+
t.importDeclaration(
|
|
77
|
+
[t.importDefaultSpecifier(t.identifier("__styleYak"))],
|
|
78
|
+
t.stringLiteral(
|
|
79
|
+
`./${fileName}.yak.module.css!=!./${fileName}?./${fileName}.yak.module.css`
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Process import specifiers
|
|
85
|
+
node.specifiers.forEach((specifier) => {
|
|
86
|
+
if (
|
|
87
|
+
!("imported" in specifier) ||
|
|
88
|
+
!specifier.imported ||
|
|
89
|
+
!t.isIdentifier(specifier.imported)
|
|
90
|
+
) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const importSpecifier = /** @type {babel.types.Identifier} */ (
|
|
95
|
+
specifier.imported
|
|
96
|
+
);
|
|
97
|
+
const localSpecifier = specifier.local || importSpecifier;
|
|
98
|
+
if (
|
|
99
|
+
importSpecifier.name === "styled" ||
|
|
100
|
+
importSpecifier.name === "css"
|
|
101
|
+
) {
|
|
102
|
+
this.localVarNames[importSpecifier.name] =
|
|
103
|
+
localSpecifier.name;
|
|
104
|
+
this.isImportedInCurrentFile = true;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
TaggedTemplateExpression(path) {
|
|
109
|
+
if (!this.isImportedInCurrentFile) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Check if the tag name matches the imported 'css' or 'styled' variable
|
|
113
|
+
const tag = path.node.tag;
|
|
114
|
+
|
|
115
|
+
const isCssLiteral =
|
|
116
|
+
t.isIdentifier(tag) &&
|
|
117
|
+
/** @type {babel.types.Identifier} */ (tag).name ===
|
|
118
|
+
this.localVarNames.css;
|
|
119
|
+
const isStyledLiteral =
|
|
120
|
+
t.isMemberExpression(tag) &&
|
|
121
|
+
t.isIdentifier(
|
|
122
|
+
/** @type {babel.types.MemberExpression} */ (tag).object
|
|
123
|
+
) &&
|
|
124
|
+
/** @type {babel.types.Identifier} */ (
|
|
125
|
+
/** @type {babel.types.MemberExpression} */ (tag).object
|
|
126
|
+
).name === this.localVarNames.styled;
|
|
127
|
+
const isStyledCall =
|
|
128
|
+
t.isCallExpression(tag) &&
|
|
129
|
+
t.isIdentifier(
|
|
130
|
+
/** @type {babel.types.CallExpression} */ (tag).callee
|
|
131
|
+
) &&
|
|
132
|
+
/** @type {babel.types.Identifier} */ (
|
|
133
|
+
/** @type {babel.types.CallExpression} */ (tag).callee
|
|
134
|
+
).name === this.localVarNames.styled;
|
|
135
|
+
|
|
136
|
+
if (!isCssLiteral && !isStyledLiteral && !isStyledCall) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
replaceQuasiExpressionTokens(path.node.quasi, replaces, t);
|
|
141
|
+
|
|
142
|
+
// Keep the same selector for all quasis belonging to the same css block
|
|
143
|
+
const classNameExpression = t.memberExpression(
|
|
144
|
+
t.identifier("__styleYak"),
|
|
145
|
+
t.identifier(`style${this.classNameCount++}`)
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Replace the tagged template expression with a call to the 'styled' function
|
|
149
|
+
const newArguments = new Set();
|
|
150
|
+
const quasis = path.node.quasi.quasis;
|
|
151
|
+
const quasiTypes = quasis.map((quasi) =>
|
|
152
|
+
quasiClassifier(quasi.value.raw)
|
|
153
|
+
);
|
|
154
|
+
const expressions = path.node.quasi.expressions;
|
|
155
|
+
|
|
156
|
+
let cssVariablesInlineStyle;
|
|
157
|
+
|
|
158
|
+
for (let i = 0; i < quasis.length; i++) {
|
|
159
|
+
if (quasiTypes[i].empty) {
|
|
160
|
+
const expression = expressions[i];
|
|
161
|
+
if (expression) {
|
|
162
|
+
newArguments.add(expression);
|
|
163
|
+
}
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// create css class name reference as argument
|
|
168
|
+
// e.g. `font-size: 2rem; display: flex;` -> `__styleYak.style1`
|
|
169
|
+
|
|
170
|
+
// AutoGenerate a unique className
|
|
171
|
+
newArguments.add(classNameExpression);
|
|
172
|
+
|
|
173
|
+
let isMerging = false;
|
|
174
|
+
// loop over all quasis belonging to the same css block
|
|
175
|
+
while (i < quasis.length - 1) {
|
|
176
|
+
const type = quasiTypes[i];
|
|
177
|
+
// expressions after a partial css are converted into css variables
|
|
178
|
+
if (
|
|
179
|
+
type.partialStart ||
|
|
180
|
+
type.partialEnd ||
|
|
181
|
+
(isMerging && type.empty)
|
|
182
|
+
) {
|
|
183
|
+
isMerging = true;
|
|
184
|
+
// expression: `x`
|
|
185
|
+
// { style: { --v0: x}}
|
|
186
|
+
const expression = expressions[i];
|
|
187
|
+
i++;
|
|
188
|
+
if (!expression) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (!cssVariablesInlineStyle) {
|
|
192
|
+
cssVariablesInlineStyle = t.objectExpression([]);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!hashedFile) {
|
|
196
|
+
const relativePath = relative(rootContext, resourcePath);
|
|
197
|
+
hashedFile = murmurhash2_32_gc(relativePath);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
cssVariablesInlineStyle.properties.push(
|
|
201
|
+
t.objectProperty(
|
|
202
|
+
t.stringLiteral(`--🦬${hashedFile}${this.varIndex++}`),
|
|
203
|
+
expression
|
|
204
|
+
)
|
|
205
|
+
);
|
|
206
|
+
} else if (type.empty) {
|
|
207
|
+
// empty quasis can be ignored
|
|
208
|
+
// e.g. `transition: color ${duration} ${easing};`
|
|
209
|
+
} else {
|
|
210
|
+
if (expressions[i]) {
|
|
211
|
+
newArguments.add(expressions[i]);
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (cssVariablesInlineStyle) {
|
|
219
|
+
newArguments.add(
|
|
220
|
+
t.objectExpression([
|
|
221
|
+
t.objectProperty(
|
|
222
|
+
t.stringLiteral(`style`),
|
|
223
|
+
cssVariablesInlineStyle
|
|
224
|
+
),
|
|
225
|
+
])
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const styledCall = t.callExpression(tag, [...newArguments]);
|
|
230
|
+
path.replaceWith(styledCall);
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const code = (result && result.code);
|
|
239
|
+
return code == null ? source : code;
|
|
240
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
/** @typedef {import("./withYak.d.ts").YakConfigOptions} YakConfigOptions */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
Add a Yak to a Next.js app
|
|
7
|
+
@param {YakConfigOptions} yakOptions
|
|
8
|
+
@param {any} nextConfig
|
|
9
|
+
*/
|
|
10
|
+
const addYak = (yakOptions, nextConfig) => {
|
|
11
|
+
const previousConfig = nextConfig.webpack;
|
|
12
|
+
nextConfig.webpack = (webpackConfig, options) => {
|
|
13
|
+
if (previousConfig) {
|
|
14
|
+
webpackConfig = previousConfig(webpackConfig, options);
|
|
15
|
+
}
|
|
16
|
+
webpackConfig.module.rules.push({
|
|
17
|
+
test: /\.tsx?$/,
|
|
18
|
+
loader: require.resolve("./tsloader.cjs"),
|
|
19
|
+
options: yakOptions,
|
|
20
|
+
});
|
|
21
|
+
webpackConfig.module.rules.push({
|
|
22
|
+
test: /\.yak\.module\.css$/,
|
|
23
|
+
loader: require.resolve("./cssloader.cjs"),
|
|
24
|
+
options: yakOptions,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return webpackConfig;
|
|
28
|
+
};
|
|
29
|
+
return nextConfig;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
// Wrapper to allow sync, async, and function configuration of Next.js
|
|
34
|
+
const withYak = (yakOptions, nextConfig) => {
|
|
35
|
+
if (nextConfig === undefined) {
|
|
36
|
+
return withYak({}, yakOptions);
|
|
37
|
+
}
|
|
38
|
+
if (typeof nextConfig === "function") {
|
|
39
|
+
return (...args) => {
|
|
40
|
+
const config = nextConfig(...args);
|
|
41
|
+
if (config.then) {
|
|
42
|
+
return config.then((config) => addYak(yakOptions, config));
|
|
43
|
+
}
|
|
44
|
+
return addYak(yakOptions, config);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return addYak(yakOptions, nextConfig);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
withYak
|
|
52
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add Yak to your Next.js app
|
|
3
|
+
*
|
|
4
|
+
* @usage
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* // next.config.js
|
|
8
|
+
* const { withYak } = require("next-yak/withYak");
|
|
9
|
+
* const nextConfig = {
|
|
10
|
+
* // your next config here
|
|
11
|
+
* };
|
|
12
|
+
* module.exports = withYak(nextConfig);
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* With a custom yakConfig
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* // next.config.js
|
|
19
|
+
* const { withYak } = require("next-yak/withYak");
|
|
20
|
+
* const nextConfig = {
|
|
21
|
+
* // your next config here
|
|
22
|
+
* };
|
|
23
|
+
* const yakConfig = {
|
|
24
|
+
* // your yak config
|
|
25
|
+
* };
|
|
26
|
+
* module.exports = withYak(yakConfig, nextConfig);
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export const withYak: {
|
|
30
|
+
<T extends Record<string, any>>(yakOptions: YakConfigOptions, nextConfig: T): T;
|
|
31
|
+
<T extends () => Record<string, any>> (yakOptions: YakConfigOptions, nextConfig: T): T;
|
|
32
|
+
<T extends () => Promise<Record<string, any>>> (yakOptions: YakConfigOptions, nextConfig: T): T;
|
|
33
|
+
// no yakConfig
|
|
34
|
+
<T extends Record<string, any>>(nextConfig: T): T;
|
|
35
|
+
<T extends () => Record<string, any>> (nextConfig: T): T;
|
|
36
|
+
<T extends () => Promise<Record<string, any>>> (nextConfig: T): T;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type YakConfigOptions = { configPath: string }
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "next-yak",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": "./dist/index.js",
|
|
7
|
+
"./withYak": "./loaders/withYak.cjs"
|
|
8
|
+
},
|
|
9
|
+
"typesVersions": {
|
|
10
|
+
"*": {
|
|
11
|
+
"withYak": ["./loaders/withYak.d.ts"],
|
|
12
|
+
"*": ["./runtime/index.d.ts"]
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"prepublishOnly": "npm run build && npm run test",
|
|
17
|
+
"build": "tsc -p tsconfig.json --outDir dist/",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest --watch -u"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"postcss-nested": "^6.0.1",
|
|
23
|
+
"@babel/core": "^7.16.12",
|
|
24
|
+
"@babel/plugin-syntax-typescript": "^7.16.7"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"react": "^18.2.0",
|
|
28
|
+
"@babel/traverse": "^7.16.12",
|
|
29
|
+
"@types/node": "20.4.5",
|
|
30
|
+
"@types/react": "18.2.16",
|
|
31
|
+
"@types/react-dom": "18.2.7",
|
|
32
|
+
"@types/jest": "29.5.5",
|
|
33
|
+
"@testing-library/jest-dom": "^5.17.0",
|
|
34
|
+
"@testing-library/react": "^14.0.0",
|
|
35
|
+
"vitest": "0.34.5",
|
|
36
|
+
"typescript": "5.1.6"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"loaders",
|
|
41
|
+
"runtime"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
type ComponentStyles<TProps extends Record<string, unknown>> = (
|
|
2
|
+
props: TProps
|
|
3
|
+
) => {
|
|
4
|
+
className: string;
|
|
5
|
+
style?: {
|
|
6
|
+
[key: string]: string;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type CSSInterpolation<TProps extends Record<string, unknown>> =
|
|
11
|
+
| string
|
|
12
|
+
| number
|
|
13
|
+
| undefined
|
|
14
|
+
| null
|
|
15
|
+
| false
|
|
16
|
+
| ComponentStyles<TProps>
|
|
17
|
+
| ((props: TProps) => CSSInterpolation<TProps>);
|
|
18
|
+
|
|
19
|
+
type CSSStyles<TProps extends Record<string, unknown>> = {
|
|
20
|
+
style: { [key: string]: string | ((props: TProps) => string) };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type CSSFunction = <TProps extends Record<string, unknown>>(
|
|
24
|
+
styles: TemplateStringsArray,
|
|
25
|
+
...values: CSSInterpolation<TProps>[]
|
|
26
|
+
) => ComponentStyles<TProps>;
|
|
27
|
+
|
|
28
|
+
const internalImplementation = (
|
|
29
|
+
...args: Array<string | CSSFunction | CSSStyles<any>>
|
|
30
|
+
): ComponentStyles<any> => {
|
|
31
|
+
type PropsToClassNameFn = (props: unknown) => {
|
|
32
|
+
className?: string;
|
|
33
|
+
style?: Record<string, string>;
|
|
34
|
+
};
|
|
35
|
+
const classNames: string[] = [];
|
|
36
|
+
const dynamicCssFunctions: PropsToClassNameFn[] = [];
|
|
37
|
+
const style: Record<string, string> = {};
|
|
38
|
+
for (let i = 0; i < args.length; i++) {
|
|
39
|
+
const arg = args[i];
|
|
40
|
+
if (typeof arg === "string") {
|
|
41
|
+
classNames.push(arg);
|
|
42
|
+
} else if (typeof arg === "function") {
|
|
43
|
+
dynamicCssFunctions.push(arg as unknown as PropsToClassNameFn);
|
|
44
|
+
} else if (typeof arg === "object" && "style" in arg) {
|
|
45
|
+
for (const key in arg.style) {
|
|
46
|
+
const value = arg.style[key];
|
|
47
|
+
if (typeof value === "function") {
|
|
48
|
+
dynamicCssFunctions.push((props: unknown) => ({
|
|
49
|
+
style: { [key]: value(props) },
|
|
50
|
+
}));
|
|
51
|
+
} else {
|
|
52
|
+
style[key] = value;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Non Dynamic CSS
|
|
59
|
+
if (dynamicCssFunctions.length === 0) {
|
|
60
|
+
const className = classNames.join(" ");
|
|
61
|
+
return () => ({ className, style });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Dynamic CSS with runtime logic
|
|
65
|
+
const unwrapProps = (
|
|
66
|
+
props: unknown,
|
|
67
|
+
fn: PropsToClassNameFn,
|
|
68
|
+
classNames: string[],
|
|
69
|
+
style: Record<string, string>
|
|
70
|
+
) => {
|
|
71
|
+
const result = fn(props);
|
|
72
|
+
if (typeof result === "function") {
|
|
73
|
+
unwrapProps(props, result, classNames, style);
|
|
74
|
+
} else if (typeof result === "object") {
|
|
75
|
+
if ("className" in result && result.className) {
|
|
76
|
+
classNames.push(result.className);
|
|
77
|
+
}
|
|
78
|
+
if ("style" in result && result.style) {
|
|
79
|
+
for (const key in result.style) {
|
|
80
|
+
const value = result.style[key];
|
|
81
|
+
style[key] = value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return (props: unknown) => {
|
|
88
|
+
const allClassNames: string[] = [...classNames];
|
|
89
|
+
const allStyles: Record<string, string> = { ...style };
|
|
90
|
+
for (let i = 0; i < dynamicCssFunctions.length; i++) {
|
|
91
|
+
unwrapProps(props, dynamicCssFunctions[i], allClassNames, allStyles);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
className: allClassNames.join(" "),
|
|
95
|
+
style: allStyles,
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const css = internalImplementation as any as CSSFunction;
|
package/runtime/index.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { FunctionComponent } from "react";
|
|
2
|
+
import { CSSInterpolation, css } from "./cssLiteral";
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
5
|
+
const StyledFactory = function (Component: string | FunctionComponent<any>) {
|
|
6
|
+
return <TProps extends Record<string, unknown>>(
|
|
7
|
+
styles: TemplateStringsArray,
|
|
8
|
+
...values: CSSInterpolation<TProps>[]
|
|
9
|
+
) => {
|
|
10
|
+
return (props: TProps) => {
|
|
11
|
+
const runtimeStyles = css(styles, ...values)(props as any);
|
|
12
|
+
const filteredProps =
|
|
13
|
+
typeof Component === "string" ? removePrefixedProperties(props) : props;
|
|
14
|
+
return (
|
|
15
|
+
<Component
|
|
16
|
+
{...filteredProps}
|
|
17
|
+
style={{ ...(props.style || {}), ...runtimeStyles.style }}
|
|
18
|
+
className={
|
|
19
|
+
(props.className ? props.className + " " : "") +
|
|
20
|
+
runtimeStyles.className
|
|
21
|
+
}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const styled = new Proxy(StyledFactory, {
|
|
29
|
+
get(target, TagName) {
|
|
30
|
+
if (typeof TagName !== "string") {
|
|
31
|
+
throw new Error("Only string tags are supported");
|
|
32
|
+
}
|
|
33
|
+
return target(TagName);
|
|
34
|
+
},
|
|
35
|
+
}) as typeof StyledFactory & {
|
|
36
|
+
[TagName in keyof JSX.IntrinsicElements]: ReturnType<typeof StyledFactory>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Remove all entries that start with a $ sign
|
|
40
|
+
function removePrefixedProperties<T extends Record<string, unknown>>(obj: T) {
|
|
41
|
+
const result = {} as T;
|
|
42
|
+
for (const key in obj) {
|
|
43
|
+
if (!key.startsWith("$")) {
|
|
44
|
+
result[key] = obj[key];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|