tailwind-style-sheets 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michal Shelenberg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # tailwind-style-sheets
2
+
3
+ A Turbopack loader and Next.js plugin for `.twss` files — co-locate your Tailwind class maps alongside components.
4
+
5
+ ## What it does
6
+
7
+ `.twss` files use a CSS-like syntax to define named Tailwind class groups. Each class can be written on its own line — this is the preferred style as it keeps diffs clean and classes easy to scan:
8
+
9
+ ```css
10
+ /* Button.styles.twss */
11
+ .button {
12
+ @apply
13
+ cursor-pointer
14
+ px-4
15
+ py-2
16
+ rounded-full
17
+ text-sm
18
+ font-medium
19
+ transition-all
20
+ active:scale-95
21
+ }
22
+
23
+ .button--primary {
24
+ @apply
25
+ bg-blue-600
26
+ text-white
27
+ hover:bg-blue-700
28
+ }
29
+
30
+ .button--ghost {
31
+ @apply
32
+ bg-transparent
33
+ text-blue-600
34
+ hover:bg-blue-50
35
+ }
36
+ ```
37
+
38
+ Inline is also valid:
39
+
40
+ ```css
41
+ .button {
42
+ @apply cursor-pointer px-4 py-2 rounded-full text-sm font-medium transition-all active:scale-95
43
+ }
44
+ ```
45
+
46
+ Importing a `.twss` file gives you a plain object:
47
+
48
+ ```ts
49
+ import styles from "./Button.styles.twss";
50
+ // styles.button → "cursor-pointer px-4 py-2 rounded-full text-sm font-medium transition-all active:scale-95"
51
+ // styles["button--primary"] → "bg-blue-600 text-white hover:bg-blue-700"
52
+ // styles["button--ghost"] → "bg-transparent text-blue-600 hover:bg-blue-50"
53
+ ```
54
+
55
+ Pair it with [`@michalshelenberg/modcn`](https://www.npmjs.com/package/@michalshelenberg/modcn) to compose BEM classes ergonomically:
56
+
57
+ ```tsx
58
+ import { modcn } from "@michalshelenberg/modcn";
59
+ import styles from "./Button.styles.twss";
60
+
61
+ const cn = modcn(styles);
62
+
63
+ export function Button({ variant = "primary", className, children, ...props }) {
64
+ return (
65
+ <button {...props} className={cn("button", `button--${variant}`, className)}>
66
+ {children}
67
+ </button>
68
+ );
69
+ }
70
+ ```
71
+
72
+ ## Installation
73
+
74
+ ```bash
75
+ npx tailwind-style-sheets init
76
+ ```
77
+
78
+ This scaffolds all required files and installs the package.
79
+
80
+ ## Manual installation
81
+
82
+ ```bash
83
+ npm install tailwind-style-sheets
84
+ ```
85
+
86
+ ### `next.config.ts`
87
+
88
+ ```ts
89
+ import type { NextConfig } from "next";
90
+ import path from "path";
91
+ import { withTwssPlugin } from "tailwind-style-sheets";
92
+
93
+ const nextConfig: NextConfig = {};
94
+
95
+ export default withTwssPlugin(nextConfig, {
96
+ globalsCSS: path.resolve(__dirname, "src/app/globals.css"),
97
+ watchDir: path.resolve(__dirname, "src"),
98
+ });
99
+ ```
100
+
101
+ `withTwssPlugin` options:
102
+
103
+ | Option | Type | Description |
104
+ | ------------ | -------- | ------------------------------------------------------------------------------------ |
105
+ | `globalsCSS` | `string` | Absolute path to your global CSS file. Touched on `.twss` changes to trigger HMR. |
106
+ | `watchDir` | `string` | Directory to watch recursively for `.twss` file changes. Only active in development. |
107
+
108
+ Both options are optional. Omitting them disables the HMR watcher (the loader still works).
109
+
110
+ ### TypeScript
111
+
112
+ Add a declaration file so TypeScript knows `.twss` imports return `Record<string, string>`:
113
+
114
+ ```ts
115
+ // global.d.ts
116
+ declare module "*.twss" {
117
+ const styles: Record<string, string>;
118
+ export default styles;
119
+ }
120
+ ```
121
+
122
+ ### VSCode
123
+
124
+ Install the [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) extension to get Tailwind class autocomplete and hover previews inside `.twss` files.
125
+
126
+ Add to `.vscode/settings.json` to get CSS syntax highlighting and silence the `@apply` warning:
127
+
128
+ ```json
129
+ {
130
+ "css.lint.unknownAtRules": "ignore",
131
+ "files.associations": {
132
+ "*.twss": "css"
133
+ }
134
+ }
135
+ ```
136
+
137
+ ## How it works
138
+
139
+ 1. **Loader** (`loader.ts`) — Turbopack passes the raw `.twss` file content through the loader. A regex extracts each `.className { @apply ... }` block and converts it to `export default { className: "class1 class2 ..." }`.
140
+
141
+ 2. **HMR watcher** (`plugin.ts`) — In development, `fs.watch` monitors `watchDir` for `.twss` changes. When a change is detected, it touches `globalsCSS`, which causes Next.js to re-run Tailwind's class scan and hot-reload styles.
package/dist/cli.js ADDED
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __defProps = Object.defineProperties;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
8
+ var __getOwnPropNames = Object.getOwnPropertyNames;
9
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
10
+ var __getProtoOf = Object.getPrototypeOf;
11
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
12
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
13
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
14
+ var __spreadValues = (a, b) => {
15
+ for (var prop in b || (b = {}))
16
+ if (__hasOwnProp.call(b, prop))
17
+ __defNormalProp(a, prop, b[prop]);
18
+ if (__getOwnPropSymbols)
19
+ for (var prop of __getOwnPropSymbols(b)) {
20
+ if (__propIsEnum.call(b, prop))
21
+ __defNormalProp(a, prop, b[prop]);
22
+ }
23
+ return a;
24
+ };
25
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
26
+ var __copyProps = (to, from, except, desc) => {
27
+ if (from && typeof from === "object" || typeof from === "function") {
28
+ for (let key of __getOwnPropNames(from))
29
+ if (!__hasOwnProp.call(to, key) && key !== except)
30
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
31
+ }
32
+ return to;
33
+ };
34
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
35
+ // If the importer is in node compatibility mode or this is not an ESM
36
+ // file that has been converted to a CommonJS file using a Babel-
37
+ // compatible transform (i.e. "__esModule" has not been set), then set
38
+ // "default" to the CommonJS "module.exports" for node compatibility.
39
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
40
+ mod
41
+ ));
42
+
43
+ // src/cli.ts
44
+ var import_fs = __toESM(require("fs"));
45
+ var import_path = __toESM(require("path"));
46
+ var import_child_process = require("child_process");
47
+ var cwd = process.cwd();
48
+ function writeIfAbsent(filePath, content) {
49
+ if (!import_fs.default.existsSync(filePath)) {
50
+ import_fs.default.writeFileSync(filePath, content, "utf8");
51
+ console.log(`created ${import_path.default.relative(cwd, filePath)}`);
52
+ } else {
53
+ console.log(`skipped ${import_path.default.relative(cwd, filePath)} (already exists)`);
54
+ }
55
+ }
56
+ function appendIfMissing(filePath, line) {
57
+ const existing = import_fs.default.existsSync(filePath) ? import_fs.default.readFileSync(filePath, "utf8") : "";
58
+ if (!existing.includes(line)) {
59
+ import_fs.default.writeFileSync(filePath, existing + (existing.endsWith("\n") || existing === "" ? "" : "\n") + line + "\n", "utf8");
60
+ console.log(`updated ${import_path.default.relative(cwd, filePath)}`);
61
+ } else {
62
+ console.log(`skipped ${import_path.default.relative(cwd, filePath)} (already contains entry)`);
63
+ }
64
+ }
65
+ writeIfAbsent(
66
+ import_path.default.join(cwd, "global.d.ts"),
67
+ `declare module "*.twss" {
68
+ const styles: Record<string, string>;
69
+ export default styles;
70
+ }
71
+ `
72
+ );
73
+ appendIfMissing(import_path.default.join(cwd, ".prettierignore"), "**/*.twss");
74
+ var vscodeDir = import_path.default.join(cwd, ".vscode");
75
+ if (!import_fs.default.existsSync(vscodeDir)) import_fs.default.mkdirSync(vscodeDir);
76
+ var vscodeSettings = import_path.default.join(vscodeDir, "settings.json");
77
+ var settings = import_fs.default.existsSync(vscodeSettings) ? JSON.parse(import_fs.default.readFileSync(vscodeSettings, "utf8")) : {};
78
+ var vscodeChanged = false;
79
+ if (settings["css.lint.unknownAtRules"] !== "ignore") {
80
+ settings["css.lint.unknownAtRules"] = "ignore";
81
+ vscodeChanged = true;
82
+ }
83
+ var _a;
84
+ if (!((_a = settings["files.associations"]) == null ? void 0 : _a["*.twss"])) {
85
+ settings["files.associations"] = __spreadProps(__spreadValues({}, settings["files.associations"]), { "*.twss": "css" });
86
+ vscodeChanged = true;
87
+ }
88
+ if (vscodeChanged) {
89
+ import_fs.default.writeFileSync(vscodeSettings, JSON.stringify(settings, null, 2) + "\n", "utf8");
90
+ console.log(`updated .vscode/settings.json`);
91
+ } else {
92
+ console.log(`skipped .vscode/settings.json (already configured)`);
93
+ }
94
+ console.log("installing tailwind-style-sheets...");
95
+ (0, import_child_process.execSync)("npm install tailwind-style-sheets", { stdio: "inherit", cwd });
96
+ var nextConfigPath = ["next.config.ts", "next.config.mjs", "next.config.js"].map((f) => import_path.default.join(cwd, f)).find((f) => import_fs.default.existsSync(f));
97
+ if (!nextConfigPath) {
98
+ console.log("skipped next.config.ts (not found)");
99
+ } else {
100
+ let src = import_fs.default.readFileSync(nextConfigPath, "utf8");
101
+ if (src.includes("withTwssPlugin")) {
102
+ console.log(`skipped ${import_path.default.relative(cwd, nextConfigPath)} (already contains withTwssPlugin)`);
103
+ } else {
104
+ const lastImportIdx = [...src.matchAll(/^import .+$/gm)].at(-1);
105
+ const insertAfter = lastImportIdx ? lastImportIdx.index + lastImportIdx[0].length : 0;
106
+ const imports = `
107
+ import path from "path";
108
+ import { withTwssPlugin } from "tailwind-style-sheets";`;
109
+ src = src.slice(0, insertAfter) + imports + src.slice(insertAfter);
110
+ src = src.replace(
111
+ /export default (\w+);/,
112
+ `export default withTwssPlugin($1, {
113
+ globalsCSS: path.resolve(__dirname, "src/app/globals.css"),
114
+ watchDir: path.resolve(__dirname, "src"),
115
+ });`
116
+ );
117
+ import_fs.default.writeFileSync(nextConfigPath, src, "utf8");
118
+ console.log(`updated ${import_path.default.relative(cwd, nextConfigPath)}`);
119
+ }
120
+ }
121
+ console.log("\ndone.");
@@ -0,0 +1,34 @@
1
+ import { NextConfig } from 'next';
2
+
3
+ interface TwssPluginOptions {
4
+ /**
5
+ * Absolute path to your global CSS file (e.g. `src/app/globals.css`).
6
+ * Touched on `.twss` file changes to trigger Tailwind's class scan and HMR.
7
+ */
8
+ globalsCSS?: string;
9
+ /**
10
+ * Directory to watch recursively for `.twss` file changes.
11
+ * Only active in `development`. Omit to disable the HMR watcher.
12
+ */
13
+ watchDir?: string;
14
+ }
15
+ /**
16
+ * Next.js plugin that enables `.twss` file imports as Tailwind class maps.
17
+ *
18
+ * Registers the Turbopack loader for `*.twss` files and, in development,
19
+ * watches `watchDir` for changes — touching `globalsCSS` on each change
20
+ * so Next.js re-runs Tailwind's class scan and hot-reloads styles.
21
+ *
22
+ * @example
23
+ * // next.config.ts
24
+ * import path from "path";
25
+ * import { withTwssPlugin } from "tailwind-style-sheets";
26
+ *
27
+ * export default withTwssPlugin({}, {
28
+ * globalsCSS: path.resolve(__dirname, "src/app/globals.css"),
29
+ * watchDir: path.resolve(__dirname, "src"),
30
+ * });
31
+ */
32
+ declare function withTwssPlugin(nextConfig: NextConfig, options?: TwssPluginOptions): NextConfig;
33
+
34
+ export { type TwssPluginOptions, withTwssPlugin };
package/dist/index.js ADDED
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __defProps = Object.defineProperties;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
9
+ var __getProtoOf = Object.getPrototypeOf;
10
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
11
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
12
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
13
+ var __spreadValues = (a, b) => {
14
+ for (var prop in b || (b = {}))
15
+ if (__hasOwnProp.call(b, prop))
16
+ __defNormalProp(a, prop, b[prop]);
17
+ if (__getOwnPropSymbols)
18
+ for (var prop of __getOwnPropSymbols(b)) {
19
+ if (__propIsEnum.call(b, prop))
20
+ __defNormalProp(a, prop, b[prop]);
21
+ }
22
+ return a;
23
+ };
24
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
25
+ var __export = (target, all) => {
26
+ for (var name in all)
27
+ __defProp(target, name, { get: all[name], enumerable: true });
28
+ };
29
+ var __copyProps = (to, from, except, desc) => {
30
+ if (from && typeof from === "object" || typeof from === "function") {
31
+ for (let key of __getOwnPropNames(from))
32
+ if (!__hasOwnProp.call(to, key) && key !== except)
33
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
34
+ }
35
+ return to;
36
+ };
37
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
38
+ // If the importer is in node compatibility mode or this is not an ESM
39
+ // file that has been converted to a CommonJS file using a Babel-
40
+ // compatible transform (i.e. "__esModule" has not been set), then set
41
+ // "default" to the CommonJS "module.exports" for node compatibility.
42
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
43
+ mod
44
+ ));
45
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
46
+
47
+ // src/index.ts
48
+ var index_exports = {};
49
+ __export(index_exports, {
50
+ withTwssPlugin: () => withTwssPlugin
51
+ });
52
+ module.exports = __toCommonJS(index_exports);
53
+
54
+ // src/plugin.ts
55
+ var import_fs = __toESM(require("fs"));
56
+ function withTwssPlugin(nextConfig, options = {}) {
57
+ var _a;
58
+ const { globalsCSS, watchDir } = options;
59
+ if (process.env.NODE_ENV === "development" && globalsCSS && watchDir) {
60
+ import_fs.default.watch(watchDir, { recursive: true }, (_, filename) => {
61
+ if (filename == null ? void 0 : filename.endsWith(".twss")) {
62
+ const now = /* @__PURE__ */ new Date();
63
+ import_fs.default.utimesSync(globalsCSS, now, now);
64
+ }
65
+ });
66
+ }
67
+ return __spreadProps(__spreadValues({}, nextConfig), {
68
+ turbopack: __spreadProps(__spreadValues({}, nextConfig.turbopack), {
69
+ rules: __spreadProps(__spreadValues({}, (_a = nextConfig.turbopack) == null ? void 0 : _a.rules), {
70
+ "*.twss": {
71
+ loaders: [require.resolve("./loader")],
72
+ as: "*.js"
73
+ }
74
+ })
75
+ })
76
+ });
77
+ }
78
+ // Annotate the CommonJS export names for ESM import in node:
79
+ 0 && (module.exports = {
80
+ withTwssPlugin
81
+ });
@@ -0,0 +1,3 @@
1
+ declare const twssLoader: (source: string) => string;
2
+
3
+ export { twssLoader as default };
package/dist/loader.js ADDED
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+
3
+ // src/loader.ts
4
+ var BLOCK_REGEX = /\.(\w[\w-]*)\s*\{\s*@apply\s+([\s\S]*?)\s*\}/g;
5
+ var twssLoader = function(source) {
6
+ const styles = {};
7
+ let match;
8
+ while ((match = BLOCK_REGEX.exec(source)) !== null) {
9
+ const [, className, classes] = match;
10
+ styles[className] = classes.trim().replace(/\s+/g, " ");
11
+ }
12
+ return `export default ${JSON.stringify(styles)};`;
13
+ };
14
+ module.exports = twssLoader;
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "tailwind-style-sheets",
3
+ "version": "0.0.1",
4
+ "main": "./dist/index.js",
5
+ "types": "./dist/index.d.ts",
6
+ "bin": {
7
+ "tailwind-style-sheets": "./dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsup",
11
+ "dev": "tsup --watch"
12
+ },
13
+ "peerDependencies": {
14
+ "next": ">=15"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^20",
21
+ "next": "16.2.1",
22
+ "tsup": "^8.5.1",
23
+ "typescript": "^5"
24
+ }
25
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,95 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { execSync } from "child_process";
4
+
5
+ const cwd = process.cwd();
6
+
7
+ function writeIfAbsent(filePath: string, content: string) {
8
+ if (!fs.existsSync(filePath)) {
9
+ fs.writeFileSync(filePath, content, "utf8");
10
+ console.log(`created ${path.relative(cwd, filePath)}`);
11
+ } else {
12
+ console.log(`skipped ${path.relative(cwd, filePath)} (already exists)`);
13
+ }
14
+ }
15
+
16
+ function appendIfMissing(filePath: string, line: string) {
17
+ const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
18
+ if (!existing.includes(line)) {
19
+ fs.writeFileSync(filePath, existing + (existing.endsWith("\n") || existing === "" ? "" : "\n") + line + "\n", "utf8");
20
+ console.log(`updated ${path.relative(cwd, filePath)}`);
21
+ } else {
22
+ console.log(`skipped ${path.relative(cwd, filePath)} (already contains entry)`);
23
+ }
24
+ }
25
+
26
+ // global.d.ts
27
+ writeIfAbsent(
28
+ path.join(cwd, "global.d.ts"),
29
+ `declare module "*.twss" {\n const styles: Record<string, string>;\n export default styles;\n}\n`
30
+ );
31
+
32
+ // .prettierignore
33
+ appendIfMissing(path.join(cwd, ".prettierignore"), "**/*.twss");
34
+
35
+ // .vscode/settings.json
36
+ const vscodeDir = path.join(cwd, ".vscode");
37
+ if (!fs.existsSync(vscodeDir)) fs.mkdirSync(vscodeDir);
38
+
39
+ const vscodeSettings = path.join(vscodeDir, "settings.json");
40
+ const settings = fs.existsSync(vscodeSettings)
41
+ ? JSON.parse(fs.readFileSync(vscodeSettings, "utf8"))
42
+ : {};
43
+
44
+ let vscodeChanged = false;
45
+ if (settings["css.lint.unknownAtRules"] !== "ignore") {
46
+ settings["css.lint.unknownAtRules"] = "ignore";
47
+ vscodeChanged = true;
48
+ }
49
+ if (!settings["files.associations"]?.["*.twss"]) {
50
+ settings["files.associations"] = { ...settings["files.associations"], "*.twss": "css" };
51
+ vscodeChanged = true;
52
+ }
53
+ if (vscodeChanged) {
54
+ fs.writeFileSync(vscodeSettings, JSON.stringify(settings, null, 2) + "\n", "utf8");
55
+ console.log(`updated .vscode/settings.json`);
56
+ } else {
57
+ console.log(`skipped .vscode/settings.json (already configured)`);
58
+ }
59
+
60
+ // install tailwind-style-sheets
61
+ console.log("installing tailwind-style-sheets...");
62
+ execSync("npm install tailwind-style-sheets", { stdio: "inherit", cwd });
63
+
64
+ // next.config.ts
65
+ const nextConfigPath = ["next.config.ts", "next.config.mjs", "next.config.js"]
66
+ .map((f) => path.join(cwd, f))
67
+ .find((f) => fs.existsSync(f));
68
+
69
+ if (!nextConfigPath) {
70
+ console.log("skipped next.config.ts (not found)");
71
+ } else {
72
+ let src = fs.readFileSync(nextConfigPath, "utf8");
73
+ if (src.includes("withTwssPlugin")) {
74
+ console.log(`skipped ${path.relative(cwd, nextConfigPath)} (already contains withTwssPlugin)`);
75
+ } else {
76
+ // add imports after the last existing import line
77
+ const lastImportIdx = [...src.matchAll(/^import .+$/gm)].at(-1);
78
+ const insertAfter = lastImportIdx
79
+ ? lastImportIdx.index! + lastImportIdx[0].length
80
+ : 0;
81
+ const imports = `\nimport path from "path";\nimport { withTwssPlugin } from "tailwind-style-sheets";`;
82
+ src = src.slice(0, insertAfter) + imports + src.slice(insertAfter);
83
+
84
+ // wrap export default <id>; with withTwssPlugin
85
+ src = src.replace(
86
+ /export default (\w+);/,
87
+ `export default withTwssPlugin($1, {\n globalsCSS: path.resolve(__dirname, "src/app/globals.css"),\n watchDir: path.resolve(__dirname, "src"),\n});`
88
+ );
89
+
90
+ fs.writeFileSync(nextConfigPath, src, "utf8");
91
+ console.log(`updated ${path.relative(cwd, nextConfigPath)}`);
92
+ }
93
+ }
94
+
95
+ console.log("\ndone.");
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { withTwssPlugin } from "./plugin";
2
+ export type { TwssPluginOptions } from "./plugin";
package/src/loader.ts ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Turbopack loader for .twss files.
3
+ *
4
+ * Parses CSS-like blocks using `@apply` and emits a JS module that maps
5
+ * each class name to a plain Tailwind class string.
6
+ *
7
+ * Input (.twss file):
8
+ * .base {
9
+ * @apply px-4 py-2 rounded font-semibold;
10
+ * }
11
+ * .primary {
12
+ * @apply bg-blue-600 text-white hover:bg-blue-700;
13
+ * }
14
+ *
15
+ * Output (JS module):
16
+ * export default { base: "px-4 py-2 rounded font-semibold", primary: "bg-blue-600 text-white hover:bg-blue-700" }
17
+ */
18
+ const BLOCK_REGEX = /\.(\w[\w-]*)\s*\{\s*@apply\s+([\s\S]*?)\s*\}/g;
19
+
20
+ const twssLoader: (source: string) => string = function (source) {
21
+ const styles: Record<string, string> = {};
22
+
23
+ let match: RegExpExecArray | null;
24
+ while ((match = BLOCK_REGEX.exec(source)) !== null) {
25
+ const [, className, classes] = match;
26
+ styles[className] = classes.trim().replace(/\s+/g, " ");
27
+ }
28
+
29
+ return `export default ${JSON.stringify(styles)};`;
30
+ };
31
+
32
+ export = twssLoader;
package/src/plugin.ts ADDED
@@ -0,0 +1,59 @@
1
+ import fs from "fs";
2
+ import type { NextConfig } from "next";
3
+
4
+ export interface TwssPluginOptions {
5
+ /**
6
+ * Absolute path to your global CSS file (e.g. `src/app/globals.css`).
7
+ * Touched on `.twss` file changes to trigger Tailwind's class scan and HMR.
8
+ */
9
+ globalsCSS?: string;
10
+ /**
11
+ * Directory to watch recursively for `.twss` file changes.
12
+ * Only active in `development`. Omit to disable the HMR watcher.
13
+ */
14
+ watchDir?: string;
15
+ }
16
+
17
+ /**
18
+ * Next.js plugin that enables `.twss` file imports as Tailwind class maps.
19
+ *
20
+ * Registers the Turbopack loader for `*.twss` files and, in development,
21
+ * watches `watchDir` for changes — touching `globalsCSS` on each change
22
+ * so Next.js re-runs Tailwind's class scan and hot-reloads styles.
23
+ *
24
+ * @example
25
+ * // next.config.ts
26
+ * import path from "path";
27
+ * import { withTwssPlugin } from "tailwind-style-sheets";
28
+ *
29
+ * export default withTwssPlugin({}, {
30
+ * globalsCSS: path.resolve(__dirname, "src/app/globals.css"),
31
+ * watchDir: path.resolve(__dirname, "src"),
32
+ * });
33
+ */
34
+ export function withTwssPlugin(nextConfig: NextConfig, options: TwssPluginOptions = {}): NextConfig {
35
+ const { globalsCSS, watchDir } = options;
36
+
37
+ if (process.env.NODE_ENV === "development" && globalsCSS && watchDir) {
38
+ fs.watch(watchDir, { recursive: true }, (_, filename) => {
39
+ if (filename?.endsWith(".twss")) {
40
+ const now = new Date();
41
+ fs.utimesSync(globalsCSS, now, now);
42
+ }
43
+ });
44
+ }
45
+
46
+ return {
47
+ ...nextConfig,
48
+ turbopack: {
49
+ ...nextConfig.turbopack,
50
+ rules: {
51
+ ...nextConfig.turbopack?.rules,
52
+ "*.twss": {
53
+ loaders: [require.resolve("./loader")],
54
+ as: "*.js",
55
+ },
56
+ },
57
+ },
58
+ };
59
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "module": "commonjs",
5
+ "moduleResolution": "node",
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "declaration": true,
10
+ "outDir": "./dist",
11
+ "rootDir": "./src"
12
+ },
13
+ "include": ["src"]
14
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig([
4
+ {
5
+ entry: ["src/index.ts", "src/loader.ts"],
6
+ format: ["cjs"],
7
+ external: ["./loader"],
8
+ dts: true,
9
+ clean: true,
10
+ },
11
+ {
12
+ entry: ["src/cli.ts"],
13
+ format: ["cjs"],
14
+ esbuildOptions(options) {
15
+ options.banner = { js: "#!/usr/bin/env node" };
16
+ },
17
+ },
18
+ ]);