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 +21 -0
- package/README.md +141 -0
- package/dist/cli.js +121 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +81 -0
- package/dist/loader.d.ts +3 -0
- package/dist/loader.js +14 -0
- package/package.json +25 -0
- package/src/cli.ts +95 -0
- package/src/index.ts +2 -0
- package/src/loader.ts +32 -0
- package/src/plugin.ts +59 -0
- package/tsconfig.json +14 -0
- package/tsup.config.ts +18 -0
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.");
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
});
|
package/dist/loader.d.ts
ADDED
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
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
|
+
]);
|