Skip to content

Commit

Permalink
Feature/kill config file (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
jantimon authored Nov 7, 2023
1 parent 8223b15 commit dfa3e32
Show file tree
Hide file tree
Showing 23 changed files with 210 additions and 172 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,8 @@ This is a proof of concept. There are a lot of things that need to be done befor
- [ ] better sourcemaps
- [ ] improve runtime code size and typings
- [ ] maybe remove proxy by compiling `styled.button -> styled("button")`
- [ ] replace the current config apporach with a solution similar to vanilla-extracts `.styles.ts` files
- [ ] better error messages
- [x] replace the current config apporach with a solution similar to vanilla-extracts `.styles.ts` files
- [x] add theme provider (which works for Server Components)
- [x] add support for forwardRef
- [x] add support for attrs
Expand Down
6 changes: 3 additions & 3 deletions packages/example/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { YakThemeProvider, css, styled } from "next-yak";
import styles from "./page.module.css";
import { queries } from "@/theme";
import { queries, colors } from "@/theme/constants.yak";
import { Clock } from "./Clock";
import { Inputs } from "@/app/Input";
import { HighContrastToggle } from "./HighContrastToggle";
Expand All @@ -12,7 +12,7 @@ const headline = css<{ $primary?: boolean }>`
${({ theme }) =>
theme.highContrast
? css`
color: #000;
color: ${colors.dark};
`
: css`
color: blue;
Expand Down Expand Up @@ -53,7 +53,7 @@ const Button = styled.button<{ $primary?: boolean }>`
${({ theme }) =>
theme.highContrast
? css`
color: #000;
color: ${colors.dark};
`
: css`
color: #009688;
Expand Down
6 changes: 1 addition & 5 deletions packages/example/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,4 @@ const nextConfig = {

}

const yakConfig = {
configPath: "./yak.config.ts",
}

module.exports = withYak(yakConfig, nextConfig)
module.exports = withYak(nextConfig)
3 changes: 3 additions & 0 deletions packages/example/theme/colors.yak.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const colors = {
dark: "#000"
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export const queries = {
md: "@media(max-width: 768px)",
lg: "@media(max-width: 1024px)",
};

export { colors } from "./colors.yak";
17 changes: 0 additions & 17 deletions packages/example/yak.config.ts

This file was deleted.

8 changes: 4 additions & 4 deletions packages/next-yak/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,13 @@ This is a proof of concept. There are a lot of things that need to be done befor

- [ ] improve js parsing - right now it not reusing babel..
- [ ] better sourcemaps
- [ ] add theme provider (which works for Server Components)
- [ ] add support for forwardRef
- [ ] add support for attrs
- [ ] improve runtime code size and typings
- [ ] maybe remove proxy by compiling `styled.button -> styled("button")`
- [ ] replace the current config apporach with a solution similar to vanilla-extracts `.styles.ts` files
- [ ] better error messages
- [x] replace the current config apporach with a solution similar to vanilla-extracts `.styles.ts` files
- [x] add theme provider (which works for Server Components)
- [x] add support for forwardRef
- [x] add support for attrs
- [x] config hot module reloading


Expand Down
2 changes: 1 addition & 1 deletion packages/next-yak/dist/context/index.cjs.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 1 addition & 4 deletions packages/next-yak/dist/context/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/next-yak/dist/context/index.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/next-yak/dist/index.cjs.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/next-yak/dist/index.js.map

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions packages/next-yak/loaders/__tests__/cssloader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@ const loaderContext = {
rootContext: "/some",
importModule: () => {
return {
replaces: {
queries: {
sm: "@media (min-width: 640px)",
md: "@media (min-width: 768px)",
lg: "@media (min-width: 1024px)",
xl: "@media (min-width: 1280px)",
xxl: "@media (min-width: 1536px)",
},
},
};
},
getOptions: () => ({
Expand Down Expand Up @@ -232,7 +230,7 @@ it("should replace breakpoint references with actual media queries", async () =>
loaderContext,
`
import { css } from "next-yak";
import { queries } from "@/theme";
import { queries } from "@/theme.yak";
const headline = css\`
color: blue;
Expand Down
2 changes: 1 addition & 1 deletion packages/next-yak/loaders/babel-yak-plugin.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const murmurhash2_32_gc = require("./lib/hash.cjs");
const { relative, resolve, basename } = require("path");
const localIdent = require("./lib/localIdent.cjs");

/** @typedef {{replaces: Record<string, Record<string, string>>, rootContext?: string}} YakBabelPluginOptions */
/** @typedef {{replaces: Record<string, unknown>, rootContext?: string}} YakBabelPluginOptions */

/**
* Babel plugin for typescript files that use yak - it will do things:
Expand Down
25 changes: 15 additions & 10 deletions packages/next-yak/loaders/cssloader.cjs
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
/// @ts-check
const getYakImports = require("./lib/getYakImports.cjs");
const babel = require("@babel/core");
const quasiClassifier = require("./lib/quasiClassifier.cjs");
const localIdent = require("./lib/localIdent.cjs");
const replaceQuasiExpressionTokens = require("./lib/replaceQuasiExpressionTokens.cjs");
const murmurhash2_32_gc = require("./lib/hash.cjs");
const { relative, resolve } = require("path");
const { relative } = require("path");

/**
* @param {string} source
* @this {any}
* @returns {Promise<string>}
*/
module.exports = async function cssLoader(source) {
// Config for replacing tokens in css template literals
// can be based on a typescript file
const options = this.getOptions();
const config = options.configPath
? await this.importModule(resolve(this.rootContext, options.configPath), {
layer: "yak-importModule",
})
: {};
const replaces = config.replaces || {};
// The user may import constants from a yak file
// e.g. import { primary } from './colors.yak'
const importedYakConstants = getYakImports(source);
/** @type {Record<string, unknown>} */
const replaces = {};
await Promise.all(importedYakConstants.map(async ({imports, from}) => {
const constantValues = await this.importModule(from, {
layer: "yak-importModule",
});
imports.forEach(({localName, importedName}) => {
replaces[localName] = constantValues[importedName];
});
}));

// parse source with babel
const ast = babel.parseSync(source, {
Expand Down
38 changes: 38 additions & 0 deletions packages/next-yak/loaders/lib/getYakImports.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// @ts-check
/**
* Finds all imports in a given code string which import from a .yak file
*
* Uses regex to work with typescript and javascript
* Does not support lazy imports
*
* @param {string} code
* @returns { { imports: { localName: string, importedName: string }[], from: string }[] }
*/
const getYakImports = (code) => {
const codeWithoutComments = code.replace(/\/\*[\s\S]*?\*\//g, '');
const allImports = codeWithoutComments.matchAll(/(^|\n|;)\s*import\s+(?:(\w+(?:\s+as\s+\w+)?)\s*,?\s*)?(?:{([^}]*)})?\s+from\s+"([^"]+\.yak)"(;|\n)/g);
return [...allImports].map(([, , defaultImport, namedImports, from,]) => {
// parse named imports to { localName: string, importedName: string }[]
const imports = namedImports?.split(',').map((namedImport) => {
const [importedName, localName = importedName] = namedImport.replace(/^type\s+/, "").trim().split(/\s+as\s+/);
return { localName, importedName };
}) ?? [];
// parse default import to { localName: string, importedName: string }[]
if (defaultImport) {
imports.push(parseDefaultImport(defaultImport));
}
return { imports, from };
});
};

/**
* Parses a default import string
* @param {string} defaultImport
* @returns {{ localName: string, importedName: string }}
*/
function parseDefaultImport(defaultImport) {
const defaultImportArray = defaultImport.split(/\s+as\s+/);
return { localName: defaultImportArray[1] ?? defaultImportArray[0], importedName: defaultImportArray[0] }
}

module.exports = getYakImports;
73 changes: 47 additions & 26 deletions packages/next-yak/loaders/lib/replaceQuasiExpressionTokens.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,36 +21,57 @@
* ```
*
* @param {import("@babel/types").TemplateLiteral} quasi
* @param {Record<string, Record<string, string>>} replaces
* @param {Record<string, unknown>} replaces
* @param {import("@babel/types")} t
*/
module.exports = function replaceTokensInQuasiExpressions(quasi, replaces, t) {
for (let i = 0; i < quasi.expressions.length; i++) {
const expression = quasi.expressions[i];
if (!t.isMemberExpression(expression)) {
continue;
// replace direct identifiers e.g. ${query}
if (t.isIdentifier(expression)) {
if (!(expression.name in replaces)) {
continue;
}
const replacement = replaces[expression.name];
replaceExpressionAndMergeQuasis(quasi, i, replacement);
i--;
}
// replace member expressions e.g. ${query.xs}
// replace deeply nested member expressions e.g. ${query.xs.min}
else if (t.isMemberExpression(expression) && t.isIdentifier(expression.object)) {
if (!(expression.object.name in replaces)) {
continue;
}
/** @type {import("@babel/types").Expression} */
let object = expression;
/** @type {any} */
let value = replaces[expression.object.name];
while (t.isMemberExpression(object)) {
if (!t.isIdentifier(object.property)) {
break;
}
if (typeof value !== "object" || value === null) {
break;
}
value = value[object.property.name];
object = object.object;
}
replaceExpressionAndMergeQuasis(quasi, i, value);
i--;
}
const object = expression.object;
if (!t.isIdentifier(object)) {
continue;
}
const property = expression.property;
if (!t.isIdentifier(property)) {
continue;
}
const objectName = object.name;
const propertyName = property.name;
const replacement = replaces[objectName]?.[propertyName];
if (!replacement) {
continue;
}
// delete expression and append replacement to quasi value
quasi.expressions.splice(i, 1);
quasi.quasis[i].value.raw += replacement + quasi.quasis[i + 1].value.raw;
quasi.quasis[i].value.cooked +=
replacement + quasi.quasis[i + 1].value.cooked;
// delete next quasi
quasi.quasis.splice(i + 1, 1);
i--;
}
};
}

/**
* Replace tokens with predefined values
* @param {import("@babel/types").TemplateLiteral} quasi
* @param {number} expressionIndex
* @param {unknown} replacement
*/
function replaceExpressionAndMergeQuasis(quasi, expressionIndex, replacement) {
const stringReplacement = typeof replacement === "string" ? replacement : replacement == null ? "" : JSON.stringify(replacement);
quasi.expressions.splice(expressionIndex, 1);
quasi.quasis[expressionIndex].value.raw += stringReplacement + quasi.quasis[expressionIndex + 1].value.raw;
quasi.quasis[expressionIndex].value.cooked += stringReplacement + quasi.quasis[expressionIndex + 1].value.cooked;
quasi.quasis.splice(expressionIndex + 1, 1);
}
16 changes: 6 additions & 10 deletions packages/next-yak/loaders/tsloader.cjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// @ts-check
const babel = require("@babel/core");
const { resolve } = require("path");
const getYakImports = require("./lib/getYakImports.cjs");

/**
* Loader for typescript files that use yak, it replaces the css template literal with a call to the 'styled' function
Expand All @@ -15,15 +15,11 @@ module.exports = async function tsloader(source) {
}
const callback = this.async();

// Config for replacing tokens in css template literals
// can be based on a typescript file
const options = this.getOptions();
const config = options.configPath
? await this.importModule(resolve(this.rootContext, options.configPath), {
layer: "yak-importModule",
})
: {};
const replaces = config.replaces || {};
// The user may import constants from a yak file
// e.g. import { primary } from './colors.yak'
const importedYakConstantNames = getYakImports(source).map(({ imports }) => imports.map(({ localName }) => localName)).flat(2);
const replaces = Object.fromEntries(importedYakConstantNames.map((name) => [name, null]));

const { rootContext, resourcePath } = this;
// Compile the typescript file with babel - this will:
// - inject the import to the css-module (with .yak.module.css extension)
Expand Down
2 changes: 1 addition & 1 deletion packages/next-yak/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"test:watch": "vitest --watch"
},
"dependencies": {
"@babel/core": "7.23.0",
"@babel/core": "7.23.2",
"@babel/plugin-syntax-typescript": "7.22.5",
"postcss-nested": "^6.0.1"
},
Expand Down
Loading

0 comments on commit dfa3e32

Please sign in to comment.