lvyjs-plugin-tailwindcss 0.0.0-stage → 0.1.0
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 +130 -2
- package/env.d.ts +4 -0
- package/lib/index.d.ts +21 -0
- package/lib/index.js +137 -0
- package/lib/watch.d.ts +36 -0
- package/lib/watch.js +122 -0
- package/package.json +57 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2013-present, Yuxi (Evan) You
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,131 @@
|
|
|
1
|
-
#
|
|
1
|
+
# lvyjs-plugin-tailwindcss
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
在 LVY 的开发和构建流程中使用 Tailwind CSS 4。使用官方 `@tailwindcss/node` 编译器和 `@tailwindcss/oxide` 扫描器,通过 LVY 公开插件接口接入,无需配置 PostCSS。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
需要支持插件的 `lvyjs >= 0.3.0-alpha.6`、Tailwind CSS 4.3.3 或更新的 4.x,以及 Node.js 22.18+(22.x)或 24.11+。
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install -D lvyjs lvyjs-plugin-tailwindcss tailwindcss@^4.3.3
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
本包尚未发布时,在仓库根目录运行 `yarn install`,然后在本包目录运行 `npm run build`。仓库的 workspace 会提供本包。
|
|
14
|
+
|
|
15
|
+
## 使用
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// lvy.config.ts
|
|
19
|
+
import { defineConfig } from 'lvyjs'
|
|
20
|
+
import tailwindcss from 'lvyjs-plugin-tailwindcss'
|
|
21
|
+
|
|
22
|
+
export default defineConfig({
|
|
23
|
+
plugins: [tailwindcss()]
|
|
24
|
+
})
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```css
|
|
28
|
+
/* src/style.css */
|
|
29
|
+
@import 'tailwindcss';
|
|
30
|
+
|
|
31
|
+
@theme {
|
|
32
|
+
--color-brand: #123456;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.card {
|
|
36
|
+
@apply flex rounded-lg;
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
在入口中导入 CSS,得到生成的 CSS 文件的绝对路径:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import stylesheet from './style.css'
|
|
44
|
+
import { readFile } from 'node:fs/promises'
|
|
45
|
+
|
|
46
|
+
const css = await readFile(stylesheet, 'utf8')
|
|
47
|
+
// 将 css 交给页面模板、服务端响应或你的渲染器。
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
这与 LVY 的资源导入约定一致。插件不负责把样式注入浏览器;开发时检测到源文件变化会重新生成 CSS 并重启应用,不提供 Vite 的浏览器 HMR。
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
lvy src/index.ts # 开发
|
|
54
|
+
lvy build # 构建,默认压缩 CSS
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
支持相对路径、绝对路径、`file:` URL 和 LVY `alias.entries` 配置的 CSS 导入,也支持 CSS 内部的别名 `@import`。构建生成的 CSS 由 LVY `emitAsset` 写入资源目录,移动完整构建目录后,JavaScript 仍可定位 CSS。
|
|
58
|
+
|
|
59
|
+
## 类名扫描与更新
|
|
60
|
+
|
|
61
|
+
默认从项目根目录扫描源文件;构建输出目录和 `.lvy` 缓存目录会被排除,防止生成文件再次被扫描或触发重启。
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
plugins: [tailwindcss({ base: './src', exclude: ['coverage'] })]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
使用 Tailwind 的 CSS 指令显式配置额外来源或关闭自动扫描:
|
|
68
|
+
|
|
69
|
+
```css
|
|
70
|
+
@import 'tailwindcss';
|
|
71
|
+
@source "../templates";
|
|
72
|
+
@source "../node_modules/my-ui";
|
|
73
|
+
@source inline("underline font-bold");
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```css
|
|
77
|
+
@import 'tailwindcss' source(none);
|
|
78
|
+
@source "../templates";
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`@source` 路径相对于声明它的 CSS 文件。开发时监听扫描目录与编译依赖:修改类名、添加和删除源文件、修改被导入的 CSS 都会触发更新。每次编译使用新的编译器和扫描器,因此删除的类名不会残留在增量候选缓存中。这个选择优先保证正确性,大项目的重编译成本需要实际测量。
|
|
82
|
+
|
|
83
|
+
类名需要完整出现在源文件中;运行时拼接出的类名应改为完整字符串或通过 `@source inline()` 注册。
|
|
84
|
+
|
|
85
|
+
## 配置
|
|
86
|
+
|
|
87
|
+
| 参数 | 默认值 | 用途 |
|
|
88
|
+
| ------------ | ------------------------- | ------------------------------------------------- |
|
|
89
|
+
| `base` | 项目根目录 | 自动扫描根目录,相对于 LVY 项目目录 |
|
|
90
|
+
| `include` | `/\.css$/` | 要处理的 CSS 入口匹配规则 |
|
|
91
|
+
| `optimize` | 开发 `false`,构建 `true` | 启用优化;也可传 `{ minify: false }` 只优化不压缩 |
|
|
92
|
+
| `sourcemap` | `false` | 在 CSS 中嵌入 source map |
|
|
93
|
+
| `exclude` | `[]` | 额外排除的目录,相对于 LVY 项目目录 |
|
|
94
|
+
| `watchDelay` | `100` | 源文件变化后的重启防抖时间,单位毫秒 |
|
|
95
|
+
|
|
96
|
+
支持普通 CSS、`@theme`、`@source`、`@apply` 和 Tailwind 官方编译器处理的 CSS 指令。无效指令会使构建失败,错误包含插件名 `lvy:tailwindcss`。
|
|
97
|
+
|
|
98
|
+
本包处理 CSS,不编译 Sass/Less,也不提供 CSS Modules。`?raw`、`?url` 等导入交给其他插件。CSS 中图片、字体等 `url()` 引用不会由本包复制到构建目录;这些资源应使用可访问的 URL 或由应用另行部署。这里的构建目录可移动保证覆盖生成 CSS 文件本身。
|
|
99
|
+
|
|
100
|
+
## 本包开发与验收
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
npm run typecheck
|
|
104
|
+
npm run build
|
|
105
|
+
npm test
|
|
106
|
+
npm pack --dry-run
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
测试使用临时项目运行真实 LVY CLI,覆盖开发更新、源文件新增与删除、CSS 依赖更新、构建 CSS 生成与目录移动、编译错误退出,以及 source(none)、内联来源和 source map。
|
|
110
|
+
|
|
111
|
+
## 类型声明与 PostCSS 共存
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
// src/env.d.ts
|
|
115
|
+
/// <reference types="lvyjs-plugin-tailwindcss/env" />
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
LVY 核心已移除内置样式处理。该插件独立提供 CSS 编译能力,不依赖 `lvyjs-plugin-postcss`。同一 CSS 入口由先匹配的插件接管,不会自动流经另一个插件;共存时明确区分入口:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
plugins: [
|
|
122
|
+
tailwindcss({ include: /\.tailwind\.css$/ }),
|
|
123
|
+
postcss({ include: /\.postcss\.(css|scss|sass|less)$/ })
|
|
124
|
+
]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
同一入口需要额外 PostCSS 步骤时,改用 `lvyjs-plugin-postcss` 加官方 `@tailwindcss/postcss` 适配器,详见 [PostCSS 插件](../lvyjs-plugin-postcss/README.md)。这条路径的扫描与缓存行为由官方适配器管理。
|
|
128
|
+
|
|
129
|
+
首次入口 CSS 编译失败后,修正该文件也可触发开发进程恢复;监听在编译开始前注册。
|
|
130
|
+
|
|
131
|
+
同时使用两个插件时,只引用 `lvyjs-plugin-postcss/env`,它已包含 CSS、Sass/SCSS/Less 声明。无需同时引用两个插件的 `env`,以免产生重复的 CSS 默认导出声明。
|
package/env.d.ts
ADDED
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { LvyPlugin } from "lvyjs";
|
|
2
|
+
//#region src/index.d.ts
|
|
3
|
+
interface TailwindcssOptions {
|
|
4
|
+
/** Directory for automatic class detection. Defaults to the LVY project root. */
|
|
5
|
+
base?: string;
|
|
6
|
+
/** CSS entries handled by this plugin. Defaults to /\.css$/. */
|
|
7
|
+
include?: RegExp;
|
|
8
|
+
/** Optimize CSS. Defaults to true for builds and false during development. */
|
|
9
|
+
optimize?: boolean | {
|
|
10
|
+
minify?: boolean;
|
|
11
|
+
};
|
|
12
|
+
/** Embed a CSS source map. Defaults to false. */
|
|
13
|
+
sourcemap?: boolean;
|
|
14
|
+
/** Additional directories excluded from scanning and watching, relative to the project. */
|
|
15
|
+
exclude?: string[];
|
|
16
|
+
/** Debounce before requesting an application restart. Defaults to 100ms. */
|
|
17
|
+
watchDelay?: number;
|
|
18
|
+
}
|
|
19
|
+
declare function tailwindcss(options?: TailwindcssOptions): LvyPlugin;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { TailwindcssOptions, tailwindcss as default, tailwindcss };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { SourceWatcher, within } from "./watch.js";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { Features, compile, optimize, toSourceMap } from "@tailwindcss/node";
|
|
4
|
+
import { clearRequireCache } from "@tailwindcss/node/require-cache";
|
|
5
|
+
import { Scanner } from "@tailwindcss/oxide";
|
|
6
|
+
import { readFile, stat } from "node:fs/promises";
|
|
7
|
+
import { basename, dirname, isAbsolute, resolve, sep } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
//#region src/index.ts
|
|
10
|
+
const prefix = "\0lvy-tailwindcss:";
|
|
11
|
+
const posix = (path) => path.split(sep).join("/");
|
|
12
|
+
function tailwindcss(options = {}) {
|
|
13
|
+
let env = {
|
|
14
|
+
cwd: process.cwd(),
|
|
15
|
+
command: "dev"
|
|
16
|
+
};
|
|
17
|
+
let base = env.cwd;
|
|
18
|
+
let aliases = [];
|
|
19
|
+
let excluded = [];
|
|
20
|
+
let watcher;
|
|
21
|
+
const dependenciesByEntry = /* @__PURE__ */ new Map();
|
|
22
|
+
const entries = /* @__PURE__ */ new Map();
|
|
23
|
+
const include = options.include ?? /\.css$/;
|
|
24
|
+
const applyAlias = (source) => {
|
|
25
|
+
const alias = aliases.find((alias) => source === alias.find || source.startsWith(alias.find + "/"));
|
|
26
|
+
return alias ? source.replace(alias.find, alias.replacement) : source;
|
|
27
|
+
};
|
|
28
|
+
const resolver = async (source, from) => {
|
|
29
|
+
const mapped = applyAlias(source);
|
|
30
|
+
return mapped === source ? void 0 : resolve(from, mapped);
|
|
31
|
+
};
|
|
32
|
+
const close = async () => {
|
|
33
|
+
await watcher?.close();
|
|
34
|
+
entries.clear();
|
|
35
|
+
dependenciesByEntry.clear();
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
name: "lvy:tailwindcss",
|
|
39
|
+
enforce: "pre",
|
|
40
|
+
configResolved(config, environment) {
|
|
41
|
+
env = environment;
|
|
42
|
+
if (options.watchDelay !== void 0 && (!Number.isFinite(options.watchDelay) || options.watchDelay < 0)) throw new Error("watchDelay must be a nonnegative number");
|
|
43
|
+
base = resolve(env.cwd, options.base ?? ".");
|
|
44
|
+
aliases = config.alias ? config.alias.entries ?? [] : [];
|
|
45
|
+
const build = config.build && typeof config.build === "object" ? config.build : {};
|
|
46
|
+
const nativeOutput = build.tsdown && typeof build.tsdown === "object" ? build.tsdown.outDir : void 0;
|
|
47
|
+
excluded = [
|
|
48
|
+
...[build.dir ?? build.OutputOptions?.dir ?? "lib", ...typeof nativeOutput === "string" ? [nativeOutput] : []],
|
|
49
|
+
".lvy",
|
|
50
|
+
...options.exclude ?? []
|
|
51
|
+
].map((path) => resolve(env.cwd, path));
|
|
52
|
+
if (excluded.some((directory) => within(base, directory))) throw new Error("Tailwind source base must not be inside an excluded/output directory");
|
|
53
|
+
},
|
|
54
|
+
configureDev(context) {
|
|
55
|
+
watcher = new SourceWatcher(context, excluded, options.watchDelay ?? 100, (message) => console.warn(`[lvy:tailwindcss] ${message}`));
|
|
56
|
+
return close;
|
|
57
|
+
},
|
|
58
|
+
resolveId(source, importer) {
|
|
59
|
+
if (source.startsWith(prefix)) return source;
|
|
60
|
+
if (source.includes("?") || source.includes("#")) return null;
|
|
61
|
+
include.lastIndex = 0;
|
|
62
|
+
if (!include.test(source)) return null;
|
|
63
|
+
const mapped = applyAlias(source);
|
|
64
|
+
if (!isAbsolute(mapped) && !mapped.startsWith(".") && !mapped.startsWith("file:") && mapped === source) return null;
|
|
65
|
+
const path = mapped.startsWith("file:") ? fileURLToPath(mapped) : resolve(importer && isAbsolute(importer) ? dirname(importer) : env.cwd, mapped);
|
|
66
|
+
const id = prefix + createHash("sha256").update(path).digest("hex").slice(0, 24) + ".js";
|
|
67
|
+
entries.set(id, path);
|
|
68
|
+
return id;
|
|
69
|
+
},
|
|
70
|
+
async load(id) {
|
|
71
|
+
if (!id.startsWith(prefix)) return null;
|
|
72
|
+
const entry = entries.get(id);
|
|
73
|
+
if (!entry) return null;
|
|
74
|
+
if (this.command === "dev") await watcher?.watchFiles(entry, [entry]);
|
|
75
|
+
const input = await readFile(entry, "utf8");
|
|
76
|
+
clearRequireCache([...dependenciesByEntry.get(entry) ?? []]);
|
|
77
|
+
const files = /* @__PURE__ */ new Set([entry]);
|
|
78
|
+
const compiler = await compile(input, {
|
|
79
|
+
base: dirname(entry),
|
|
80
|
+
from: entry,
|
|
81
|
+
shouldRewriteUrls: true,
|
|
82
|
+
onDependency: (path) => files.add(resolve(path)),
|
|
83
|
+
customCssResolver: resolver,
|
|
84
|
+
customJsResolver: resolver
|
|
85
|
+
});
|
|
86
|
+
const sources = [
|
|
87
|
+
...compiler.root === "none" ? [] : compiler.root === null ? [{
|
|
88
|
+
base,
|
|
89
|
+
pattern: "**/*",
|
|
90
|
+
negated: false
|
|
91
|
+
}] : [{
|
|
92
|
+
...compiler.root,
|
|
93
|
+
negated: false
|
|
94
|
+
}],
|
|
95
|
+
...compiler.sources,
|
|
96
|
+
...excluded.map((directory) => ({
|
|
97
|
+
base: dirname(directory),
|
|
98
|
+
pattern: posix(basename(directory)) + "/**",
|
|
99
|
+
negated: true
|
|
100
|
+
}))
|
|
101
|
+
];
|
|
102
|
+
if (compiler.root && compiler.root !== "none") {
|
|
103
|
+
const root = resolve(compiler.root.base, compiler.root.pattern);
|
|
104
|
+
if (!(await stat(root)).isDirectory()) throw new Error(`Tailwind source() must point to a directory: ${root}`);
|
|
105
|
+
}
|
|
106
|
+
const scanner = new Scanner({ sources });
|
|
107
|
+
const candidates = compiler.features & Features.Utilities ? scanner.scan() : [];
|
|
108
|
+
for (const path of scanner.files) files.add(resolve(path));
|
|
109
|
+
dependenciesByEntry.set(entry, files);
|
|
110
|
+
if (this.command === "build") for (const file of files) this.addWatchFile(file);
|
|
111
|
+
else await watcher?.update(entry, {
|
|
112
|
+
files,
|
|
113
|
+
globs: scanner.globs
|
|
114
|
+
});
|
|
115
|
+
let css = compiler.build(candidates);
|
|
116
|
+
let map = options.sourcemap ? toSourceMap(compiler.buildSourceMap()).raw : void 0;
|
|
117
|
+
const optimization = options.optimize ?? this.command === "build";
|
|
118
|
+
if (optimization) {
|
|
119
|
+
const result = optimize(css, {
|
|
120
|
+
file: entry,
|
|
121
|
+
minify: typeof optimization === "object" ? optimization.minify ?? true : true,
|
|
122
|
+
map
|
|
123
|
+
});
|
|
124
|
+
css = result.code;
|
|
125
|
+
map = result.map;
|
|
126
|
+
}
|
|
127
|
+
if (map) css += "\n" + toSourceMap(map).comment(toSourceMap(map).inline);
|
|
128
|
+
return this.emitAsset({
|
|
129
|
+
name: basename(entry),
|
|
130
|
+
source: css
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
closeBundle: close
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
export { tailwindcss as default, tailwindcss };
|
package/lib/watch.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { DevContext } from "lvyjs";
|
|
2
|
+
//#region src/watch.d.ts
|
|
3
|
+
type WatchSnapshot = {
|
|
4
|
+
files: Set<string>;
|
|
5
|
+
globs: {
|
|
6
|
+
base: string;
|
|
7
|
+
pattern: string;
|
|
8
|
+
}[];
|
|
9
|
+
};
|
|
10
|
+
declare const within: (path: string, directory: string) => boolean;
|
|
11
|
+
/** Own the directory watch: LVY's literal file API cannot express filtered source globs. */
|
|
12
|
+
declare class SourceWatcher {
|
|
13
|
+
private dev;
|
|
14
|
+
private excluded;
|
|
15
|
+
private delay;
|
|
16
|
+
private warn;
|
|
17
|
+
private watcher?;
|
|
18
|
+
private timer?;
|
|
19
|
+
private snapshots;
|
|
20
|
+
private matches;
|
|
21
|
+
private files;
|
|
22
|
+
private explicitModules;
|
|
23
|
+
private targets;
|
|
24
|
+
private ready?;
|
|
25
|
+
private closed;
|
|
26
|
+
private updates;
|
|
27
|
+
private settleReady?;
|
|
28
|
+
constructor(dev: DevContext, excluded: string[], delay: number, warn: (message: string) => void);
|
|
29
|
+
private ignored;
|
|
30
|
+
watchFiles(entry: string, files: Iterable<string>): Promise<void>;
|
|
31
|
+
update(entry: string, snapshot: WatchSnapshot): Promise<void>;
|
|
32
|
+
private updateSnapshot;
|
|
33
|
+
close(): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { SourceWatcher, WatchSnapshot, within };
|
package/lib/watch.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
|
+
import chokidar from "chokidar";
|
|
3
|
+
import picomatch from "picomatch";
|
|
4
|
+
//#region src/watch.ts
|
|
5
|
+
const within = (path, directory) => {
|
|
6
|
+
const part = relative(directory, path);
|
|
7
|
+
return part === "" || !part.startsWith(`..${sep}`) && part !== ".." && !isAbsolute(part);
|
|
8
|
+
};
|
|
9
|
+
const posix = (path) => path.split(sep).join("/");
|
|
10
|
+
/** Own the directory watch: LVY's literal file API cannot express filtered source globs. */
|
|
11
|
+
var SourceWatcher = class {
|
|
12
|
+
dev;
|
|
13
|
+
excluded;
|
|
14
|
+
delay;
|
|
15
|
+
warn;
|
|
16
|
+
watcher;
|
|
17
|
+
timer;
|
|
18
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
19
|
+
matches = [];
|
|
20
|
+
files = /* @__PURE__ */ new Set();
|
|
21
|
+
explicitModules = [];
|
|
22
|
+
targets = /* @__PURE__ */ new Set();
|
|
23
|
+
ready;
|
|
24
|
+
closed = false;
|
|
25
|
+
updates = Promise.resolve();
|
|
26
|
+
settleReady;
|
|
27
|
+
constructor(dev, excluded, delay, warn) {
|
|
28
|
+
this.dev = dev;
|
|
29
|
+
this.excluded = excluded;
|
|
30
|
+
this.delay = delay;
|
|
31
|
+
this.warn = warn;
|
|
32
|
+
}
|
|
33
|
+
ignored = (input) => {
|
|
34
|
+
const path = resolve(input);
|
|
35
|
+
if (this.excluded.some((directory) => within(path, directory))) return true;
|
|
36
|
+
if (/(?:^|[\\/])(?:\.git|\.lvy)(?:[\\/]|$)/.test(path)) return true;
|
|
37
|
+
if (this.files.has(path)) return false;
|
|
38
|
+
if (/(?:^|[\\/])node_modules(?:[\\/]|$)/.test(path)) return !this.explicitModules.some((directory) => within(path, directory) || within(directory, path));
|
|
39
|
+
return false;
|
|
40
|
+
};
|
|
41
|
+
async watchFiles(entry, files) {
|
|
42
|
+
const previous = this.snapshots.get(entry);
|
|
43
|
+
await this.update(entry, {
|
|
44
|
+
files: /* @__PURE__ */ new Set([...previous?.files ?? [], ...files]),
|
|
45
|
+
globs: previous?.globs ?? []
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
update(entry, snapshot) {
|
|
49
|
+
const pending = this.updates.then(() => this.updateSnapshot(entry, snapshot));
|
|
50
|
+
this.updates = pending.catch(() => {});
|
|
51
|
+
return pending;
|
|
52
|
+
}
|
|
53
|
+
async updateSnapshot(entry, snapshot) {
|
|
54
|
+
if (this.closed) return;
|
|
55
|
+
this.snapshots.set(entry, snapshot);
|
|
56
|
+
this.files = new Set([...this.snapshots.values()].flatMap((value) => [...value.files]));
|
|
57
|
+
const globs = [...this.snapshots.values()].flatMap((value) => value.globs);
|
|
58
|
+
this.explicitModules = globs.filter((glob) => /(?:^|[\\/])node_modules(?:[\\/]|$)/.test(glob.base)).map((glob) => glob.base);
|
|
59
|
+
this.matches = globs.filter((glob) => !glob.pattern.startsWith("!")).map((glob) => {
|
|
60
|
+
const match = picomatch(glob.pattern, { dot: false });
|
|
61
|
+
return (path) => within(path, glob.base) && match(posix(relative(glob.base, path)));
|
|
62
|
+
});
|
|
63
|
+
const directories = globs.filter((glob) => !glob.pattern.startsWith("!")).map((glob) => glob.base);
|
|
64
|
+
const roots = [...new Set(directories)].filter((path) => !directories.some((parent) => parent !== path && within(path, parent)));
|
|
65
|
+
const targets = /* @__PURE__ */ new Set([...roots, ...[...this.files].filter((path) => {
|
|
66
|
+
return !(!/(?:^|[\\/])node_modules(?:[\\/]|$)/.test(path) || this.explicitModules.some((root) => within(path, root))) || !roots.some((root) => within(path, root));
|
|
67
|
+
})]);
|
|
68
|
+
const removed = [...this.targets].filter((path) => !targets.has(path));
|
|
69
|
+
const added = [...targets].filter((path) => !this.targets.has(path));
|
|
70
|
+
if (this.watcher && (removed.length || added.length)) {
|
|
71
|
+
await this.watcher.close();
|
|
72
|
+
this.watcher = void 0;
|
|
73
|
+
this.ready = void 0;
|
|
74
|
+
}
|
|
75
|
+
if (this.closed) return;
|
|
76
|
+
if (!this.watcher) {
|
|
77
|
+
this.watcher = chokidar.watch([...targets], {
|
|
78
|
+
ignoreInitial: true,
|
|
79
|
+
ignored: this.ignored
|
|
80
|
+
});
|
|
81
|
+
this.watcher.on("all", (event, path) => {
|
|
82
|
+
if (![
|
|
83
|
+
"add",
|
|
84
|
+
"change",
|
|
85
|
+
"unlink"
|
|
86
|
+
].includes(event) || this.ignored(path)) return;
|
|
87
|
+
const absolute = resolve(path);
|
|
88
|
+
if (!this.files.has(absolute) && !this.matches.some((match) => match(absolute))) return;
|
|
89
|
+
clearTimeout(this.timer);
|
|
90
|
+
this.timer = setTimeout(() => {
|
|
91
|
+
if (!this.closed) this.dev.restart();
|
|
92
|
+
}, this.delay);
|
|
93
|
+
});
|
|
94
|
+
this.watcher.on("error", (error) => this.warn(`Source watch failed: ${String(error)}`));
|
|
95
|
+
this.ready = new Promise((resolveReady, reject) => {
|
|
96
|
+
const timer = setTimeout(() => reject(/* @__PURE__ */ new Error("Tailwind source watch did not become ready")), 3e4);
|
|
97
|
+
this.settleReady = () => {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
resolveReady();
|
|
100
|
+
};
|
|
101
|
+
this.watcher.once("ready", this.settleReady);
|
|
102
|
+
this.watcher.once("error", (error) => {
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
reject(error);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
} else this.watcher.add([...targets].filter((path) => !this.targets.has(path)));
|
|
108
|
+
this.targets = targets;
|
|
109
|
+
await this.ready;
|
|
110
|
+
}
|
|
111
|
+
async close() {
|
|
112
|
+
if (this.closed) return;
|
|
113
|
+
this.closed = true;
|
|
114
|
+
clearTimeout(this.timer);
|
|
115
|
+
this.settleReady?.();
|
|
116
|
+
await this.updates;
|
|
117
|
+
await this.watcher?.close();
|
|
118
|
+
this.snapshots.clear();
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
//#endregion
|
|
122
|
+
export { SourceWatcher, within };
|
package/package.json
CHANGED
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lvyjs-plugin-tailwindcss",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Tailwind CSS 4 integration for LVY development and builds",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "lemonade",
|
|
8
|
+
"main": "./lib/index.js",
|
|
9
|
+
"types": "./lib/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./lib/index.d.ts",
|
|
13
|
+
"import": "./lib/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./env": {
|
|
16
|
+
"types": "./env.d.ts"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"lib",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"env.d.ts"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsdown",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"test": "node --test tests/*.test.mjs"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": "^22.18.0 || >=24.11.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"lvyjs": ">=0.3.0-alpha.6 <1",
|
|
35
|
+
"tailwindcss": "^4.3.3"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@tailwindcss/node": "^4.3.3",
|
|
39
|
+
"@tailwindcss/oxide": "^4.3.3",
|
|
40
|
+
"chokidar": "^5.0.0",
|
|
41
|
+
"picomatch": "^4.0.3"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^26.2.0",
|
|
45
|
+
"@types/picomatch": "^4.0.2",
|
|
46
|
+
"lvyjs": "0.3.0-alpha.6",
|
|
47
|
+
"tailwindcss": "^4.3.3",
|
|
48
|
+
"tsdown": "^0.22.14",
|
|
49
|
+
"typescript": "^7.0.2"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"registry": "https://registry.npmjs.org",
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "https://github.com/lemonade-lab/lvyjs.git",
|
|
58
|
+
"directory": "packages/lvyjs-plugin-tailwindcss"
|
|
59
|
+
}
|
|
6
60
|
}
|