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.
@@ -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;
@@ -0,0 +1,2 @@
1
+ export {css} from "./cssLiteral";
2
+ export * from "./styled";
@@ -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
+ }