lvyjs-plugin-postcss 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 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,160 @@
1
- # Temporary Holding Version
1
+ # lvyjs-plugin-postcss
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ LVY 的通用样式插件。通过公开 `resolveId / load / emitAsset` 接口处理 CSS、可选 Sass/SCSS/Less,并将生成 CSS 的文件绝对路径导出给应用。
4
+
5
+ ## 安装与使用
6
+
7
+ ```sh
8
+ npm install -D lvyjs lvyjs-plugin-postcss
9
+ ```
10
+
11
+ 需要 `lvyjs >= 0.3.0-alpha.6`,Node.js 22.18+(22.x)或 24.11+。未发布时先在仓库根目录运行 `yarn install`,在本包目录运行 `npm run build`,通过 workspace 使用。
12
+
13
+ ```ts
14
+ // lvy.config.ts
15
+ import { defineConfig } from 'lvyjs'
16
+ import postcss from 'lvyjs-plugin-postcss'
17
+
18
+ export default defineConfig({
19
+ plugins: [postcss()]
20
+ })
21
+ ```
22
+
23
+ ```ts
24
+ // src/index.ts
25
+ import stylesheet from './style.css'
26
+ import { readFile } from 'node:fs/promises'
27
+ const css = await readFile(stylesheet, 'utf8')
28
+ ```
29
+
30
+ 把 CSS 内容交给页面模板、服务端响应或渲染器。插件不会自动注入浏览器样式。开发中依赖变化会重新编译并重启应用;构建目录中的 CSS 由 LVY 输出,移动完整构建目录后仍可读取。
31
+
32
+ ```sh
33
+ lvy src/index.ts
34
+ lvy build
35
+ ```
36
+
37
+ TypeScript 的样式导入声明已经从 LVY 迁入本包:
38
+
39
+ ```ts
40
+ // src/env.d.ts
41
+ /// <reference types="lvyjs-plugin-postcss/env" />
42
+ ```
43
+
44
+ ## PostCSS 配置
45
+
46
+ 使用 `postcss-load-config` 在项目目录发现配置,支持 `postcss.config.cjs/mjs/js/ts`、`.postcssrc` 和 `package.json` 的 `postcss` 字段。配置错误或缺失的插件会导致编译失败;没有配置文件时使用默认步骤。`config` 指定搜索目录,搜索不会越过这个目录。
47
+
48
+ ```js
49
+ // postcss.config.mjs
50
+ import cssnano from 'cssnano'
51
+ export default {
52
+ plugins: [cssnano({ preset: 'default' })]
53
+ }
54
+ ```
55
+
56
+ ```js
57
+ // postcss.config.cjs
58
+ module.exports = {
59
+ plugins: {
60
+ cssnano: { preset: 'default' }
61
+ }
62
+ }
63
+ ```
64
+
65
+ 额外插件需要由项目安装。也可以在 LVY 配置中传入 `postcss({ config: false, plugins: [...] })`。开发时支持修改、添加、删除配置文件,以及 PostCSS `dependency` 和 `dir-dependency` 消息声明的依赖。CJS 配置的本地辅助模块也会被监听和重新加载;ESM 辅助模块的修改建议通过重启开发命令重新加载。
66
+
67
+ 默认处理顺序:预处理 Sass/Less → `postcss-import` → URL 别名替换 → autoprefixer → 配置文件插件 → `plugins` 参数中的插件。如果配置文件显式提供同名默认插件,默认实例不会重复添加,其参数和顺序由配置文件控制。
68
+
69
+ ## 别名和预处理器
70
+
71
+ CSS 入口、CSS 内部 `@import`、Sass `@use` 和 Less `@import` 支持 LVY `alias.entries` 的边界匹配;普通 CSS 相对路径和包导入保留 `postcss-import` 的解析能力。
72
+
73
+ ```css
74
+ @import '@src/theme.css';
75
+ @import './base.css';
76
+ ```
77
+
78
+ 按需安装预处理器:
79
+
80
+ ```sh
81
+ npm install -D sass less
82
+ ```
83
+
84
+ ```scss
85
+ @use '@src/tokens';
86
+ .card {
87
+ color: tokens.$brand;
88
+ }
89
+ ```
90
+
91
+ Sass/Less 是可选 peer dependencies,纯 CSS 项目无需安装。编译依赖会参与开发监听。CSS 中 `@import` 引入预处理器文件时,会先编译再内联。
92
+
93
+ `url()` 默认只替换别名,不复制图片/字体资源。生成的 CSS 位于资源目录,相对 URL 应由应用或部署流程处理;CSS 文件本身的可移动性不包含它引用的外部资源。暂不提供 CSS Modules;`?raw/?url` 导入留给其他插件。
94
+
95
+ ## 与 Tailwind 插件的关系
96
+
97
+ 两个包是独立的 LVY 插件,不相互依赖。同一 CSS 入口由第一个匹配的 `load` 处理,不会因为插件排列自动组成 CSS 流水线。
98
+
99
+ - Tailwind CSS 4 专用项目使用 `lvyjs-plugin-tailwindcss`。
100
+ - 通用 PostCSS 项目使用本包。
101
+ - 两者共存时,通过互不重叠的 `include` 区分入口。
102
+
103
+ ```ts
104
+ plugins: [
105
+ tailwindcss({ include: /\.tailwind\.css$/ }),
106
+ postcss({ include: /\.postcss\.(css|scss|sass|less)$/ })
107
+ ]
108
+ ```
109
+
110
+ 同一个入口需要 Tailwind 加其他 PostCSS 步骤时,只启用本包,在配置中使用官方适配器:
111
+
112
+ ```sh
113
+ npm install -D tailwindcss@^4.3.3 @tailwindcss/postcss@^4.3.3
114
+ ```
115
+
116
+ ```ts
117
+ plugins: [postcss({ imports: false, urls: false, autoprefixer: false })]
118
+ ```
119
+
120
+ ```js
121
+ // postcss.config.mjs
122
+ import tailwindcss from '@tailwindcss/postcss'
123
+ import cssnano from 'cssnano'
124
+ export default {
125
+ plugins: [tailwindcss(), cssnano({ preset: 'default' })]
126
+ }
127
+ ```
128
+
129
+ 关闭默认步骤后,让官方 Tailwind 适配器负责导入、URL 和前缀处理,避免重复加工。类名扫描和增量缓存行为由该适配器决定;若需要本仓库“删除类名立即清除 CSS”的专用验收保证,使用独立 Tailwind 插件。
130
+
131
+ ## 参数
132
+
133
+ | 参数 | 默认值 | 说明 |
134
+ | ---------------- | ------------------------------ | ------------------------------------------------------------------ |
135
+ | `include` | `/\.(css\|scss\|sass\|less)$/` | 入口匹配规则 |
136
+ | `config` | 项目根目录 | 配置搜索目录;`false` 关闭发现 |
137
+ | `plugins` | `[]` | 追加 PostCSS 插件 |
138
+ | `imports` | `true` | 默认内联导入与别名解析 |
139
+ | `urls` | `true` | 默认 URL 别名处理 |
140
+ | `autoprefixer` | `true` | `false` 关闭,也可传 autoprefixer 参数 |
141
+ | `processOptions` | `{}` | 覆盖 PostCSS 处理选项;`from/to` 由入口确定 |
142
+ | `sourcemap` | 遵循配置 | `true` 嵌入 map,`false` 关闭;配置开启的外部 map 也转换为内嵌 map |
143
+ | `watchDelay` | `100` | 开发重启防抖毫秒数 |
144
+
145
+ 旧配置 `styles: { filter: ... }` 迁移为 `plugins: [postcss({ include: ... })]`,旧 `styles: false` 则不启用样式插件。LVY 核心已移除样式处理及 PostCSS 依赖。
146
+
147
+ ## 验收
148
+
149
+ ```sh
150
+ npm run typecheck
151
+ npm run build
152
+ npm test
153
+ npm pack --dry-run
154
+ ```
155
+
156
+ 测试运行真实 LVY CLI,覆盖配置与依赖更新、预处理器、别名、前缀、map、构建目录移动、错误退出、两个独立插件的入口分工,以及官方 Tailwind PostCSS 适配器的组合处理。
157
+
158
+ 首次入口 CSS 或 PostCSS 配置编译失败后,修正相应文件也可触发开发进程恢复;监听在编译开始前注册。
159
+
160
+ 同时使用两个插件时,只引用 `lvyjs-plugin-postcss/env`,它已包含 CSS、Sass/SCSS/Less 声明。无需同时引用两个插件的 `env`,以免产生重复的 CSS 默认导出声明。
package/env.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ declare module '*.css' {
2
+ const path: string
3
+ export default path
4
+ }
5
+
6
+ declare module '*.scss' {
7
+ const path: string
8
+ export default path
9
+ }
10
+
11
+ declare module '*.sass' {
12
+ const path: string
13
+ export default path
14
+ }
15
+
16
+ declare module '*.less' {
17
+ const path: string
18
+ export default path
19
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { AcceptedPlugin, ProcessOptions } from "postcss";
2
+ import autoprefixer from "autoprefixer";
3
+ import { LvyPlugin } from "lvyjs";
4
+ //#region src/index.d.ts
5
+ interface PostcssOptions {
6
+ /** Entries handled by this plugin. Defaults to CSS/SCSS/Sass/Less. */
7
+ include?: RegExp;
8
+ /** Config search directory relative to the project, or false to disable discovery. */
9
+ config?: string | false;
10
+ /** Additional PostCSS plugins, after the plugins in the config file. */
11
+ plugins?: AcceptedPlugin[];
12
+ /** Inline CSS imports with alias support. Defaults to true. */
13
+ imports?: boolean;
14
+ /** Rewrite URL aliases. Defaults to true. Does not copy resources. */
15
+ urls?: boolean;
16
+ /** Add vendor prefixes. Defaults to true. */
17
+ autoprefixer?: boolean | Parameters<typeof autoprefixer>[0];
18
+ /** Override PostCSS process options. from/to always refer to the source entry. */
19
+ processOptions?: Omit<ProcessOptions, 'from' | 'to'>;
20
+ /** Embed a CSS source map; overrides the config's map option when specified. */
21
+ sourcemap?: boolean;
22
+ /** Debounce development restarts, in milliseconds. Defaults to 100. */
23
+ watchDelay?: number;
24
+ }
25
+ declare function postcssPlugin(options?: PostcssOptions): LvyPlugin;
26
+ //#endregion
27
+ export { PostcssOptions, postcssPlugin as default, postcssPlugin as postcss };
package/lib/index.js ADDED
@@ -0,0 +1,212 @@
1
+ import { SourceWatcher } from "./watch.js";
2
+ import { createRequire } from "node:module";
3
+ import { createHash } from "node:crypto";
4
+ import postcss from "postcss";
5
+ import loadConfig from "postcss-load-config";
6
+ import postcssImport from "postcss-import";
7
+ import postcssUrl from "postcss-url";
8
+ import autoprefixer from "autoprefixer";
9
+ import { readFile } from "node:fs/promises";
10
+ import { basename, dirname, isAbsolute, resolve } from "node:path";
11
+ import { fileURLToPath, pathToFileURL } from "node:url";
12
+ //#region src/index.ts
13
+ const ownRequire = createRequire(import.meta.url);
14
+ const prefix = "\0lvy-postcss:";
15
+ const configNames = [
16
+ "package.json",
17
+ ".postcssrc",
18
+ ...[
19
+ "json",
20
+ "js",
21
+ "cjs",
22
+ "mjs",
23
+ "ts",
24
+ "cts",
25
+ "mts",
26
+ "yaml",
27
+ "yml"
28
+ ].flatMap((ext) => [`.postcssrc.${ext}`, `postcss.config.${ext}`])
29
+ ];
30
+ function postcssPlugin(options = {}) {
31
+ let env = {
32
+ cwd: process.cwd(),
33
+ command: "dev"
34
+ };
35
+ let aliases = [];
36
+ let watcher;
37
+ let excluded = [];
38
+ let configDirectory = env.cwd;
39
+ let projectRequire = createRequire(resolve(env.cwd, "package.json"));
40
+ const entries = /* @__PURE__ */ new Map();
41
+ const include = options.include ?? /\.(css|scss|sass|less)$/;
42
+ const knownConfigFiles = /* @__PURE__ */ new Set();
43
+ const aliasPath = (source) => {
44
+ const alias = aliases.find((alias) => source === alias.find || source.startsWith(alias.find + "/"));
45
+ return alias ? source.replace(alias.find, alias.replacement) : source;
46
+ };
47
+ const close = async () => {
48
+ await watcher?.close();
49
+ entries.clear();
50
+ };
51
+ const preprocess = async (file, files) => {
52
+ files.add(file);
53
+ if (/\.less$/.test(file)) {
54
+ let less;
55
+ try {
56
+ less = projectRequire("less");
57
+ } catch {
58
+ throw new Error("LESS requires the optional dependency: npm install -D less");
59
+ }
60
+ const result = await less.render(await readFile(file, "utf8"), {
61
+ filename: file,
62
+ plugins: [{ install(engine, manager) {
63
+ const fileManager = new engine.FileManager();
64
+ fileManager.loadFile = function(name, current, ...args) {
65
+ return engine.FileManager.prototype.loadFile.call(this, aliasPath(name), current, ...args);
66
+ };
67
+ manager.addFileManager(fileManager);
68
+ } }]
69
+ });
70
+ for (const path of result.imports ?? []) files.add(resolve(path));
71
+ return result.css;
72
+ }
73
+ if (/\.(scss|sass)$/.test(file)) {
74
+ let sass;
75
+ try {
76
+ sass = projectRequire("sass");
77
+ } catch {
78
+ throw new Error("Sass/SCSS requires the optional dependency: npm install -D sass");
79
+ }
80
+ const result = await sass.compileAsync(file, { importers: [{ findFileUrl(source) {
81
+ const mapped = aliasPath(source);
82
+ return mapped === source ? null : pathToFileURL(resolve(env.cwd, mapped));
83
+ } }] });
84
+ for (const url of result.loadedUrls) if (url.protocol === "file:") files.add(fileURLToPath(url));
85
+ return result.css;
86
+ }
87
+ return readFile(file, "utf8");
88
+ };
89
+ return {
90
+ name: "lvy:postcss",
91
+ enforce: "pre",
92
+ configResolved(config, environment) {
93
+ env = environment;
94
+ if (options.watchDelay !== void 0 && (!Number.isFinite(options.watchDelay) || options.watchDelay < 0)) throw new Error("watchDelay must be a nonnegative number");
95
+ if (!options.include && config.plugins?.some((plugin) => plugin && typeof plugin === "object" && "name" in plugin && plugin.name === "lvy:tailwindcss")) console.warn("[lvy:postcss] PostCSS 与 Tailwind 同时启用时,请用 include 区分入口;同一入口由先匹配的插件处理。");
96
+ configDirectory = resolve(env.cwd, options.config || ".");
97
+ projectRequire = createRequire(resolve(env.cwd, "package.json"));
98
+ const build = config.build && typeof config.build === "object" ? config.build : {};
99
+ excluded = [
100
+ build.dir ?? build.OutputOptions?.dir ?? "lib",
101
+ ".lvy",
102
+ ...typeof build.tsdown === "object" && typeof build.tsdown.outDir === "string" ? [build.tsdown.outDir] : []
103
+ ].map((path) => resolve(env.cwd, path));
104
+ aliases = config.alias ? config.alias.entries ?? [] : [];
105
+ },
106
+ configureDev(context) {
107
+ watcher = new SourceWatcher(context, excluded, options.watchDelay ?? 100, (message) => console.warn(`[lvy:postcss] ${message}`));
108
+ return close;
109
+ },
110
+ resolveId(source, importer) {
111
+ if (source.startsWith(prefix)) return source;
112
+ if (source.includes("?") || source.includes("#")) return null;
113
+ include.lastIndex = 0;
114
+ if (!include.test(source)) return null;
115
+ const mapped = aliasPath(source);
116
+ if (!isAbsolute(mapped) && !mapped.startsWith(".") && !mapped.startsWith("file:") && mapped === source) return null;
117
+ const path = mapped.startsWith("file:") ? fileURLToPath(mapped) : resolve(importer && isAbsolute(importer) ? dirname(importer) : env.cwd, mapped);
118
+ const id = prefix + createHash("sha256").update(path).digest("hex").slice(0, 24) + ".js";
119
+ entries.set(id, path);
120
+ return id;
121
+ },
122
+ async load(id) {
123
+ if (!id.startsWith(prefix)) return null;
124
+ const file = entries.get(id);
125
+ if (!file) return null;
126
+ const files = /* @__PURE__ */ new Set([file]);
127
+ if (options.config !== false) for (const name of configNames) files.add(resolve(configDirectory, name));
128
+ if (this.command === "dev") await watcher?.watchFiles(file, files);
129
+ for (const path of knownConfigFiles) delete projectRequire.cache[path];
130
+ let configured;
131
+ let collectConfigFiles = () => {};
132
+ if (options.config !== false) {
133
+ try {
134
+ configured = await loadConfig({
135
+ cwd: env.cwd,
136
+ env: env.command === "build" ? "production" : "development"
137
+ }, configDirectory, { stopDir: configDirectory });
138
+ files.add(configured.file);
139
+ knownConfigFiles.add(configured.file);
140
+ const visited = /* @__PURE__ */ new Set();
141
+ const collect = (path) => {
142
+ if (visited.has(path)) return;
143
+ visited.add(path);
144
+ const mod = projectRequire.cache[path];
145
+ for (const child of mod?.children ?? []) {
146
+ if (child.filename.includes("/node_modules/")) continue;
147
+ knownConfigFiles.add(child.filename);
148
+ files.add(child.filename);
149
+ collect(child.filename);
150
+ }
151
+ };
152
+ collectConfigFiles = () => collect(configured.file);
153
+ } catch (error) {
154
+ if (!(error instanceof Error) || !error.message.startsWith("No PostCSS Config found")) throw error;
155
+ }
156
+ for (const name of configNames) files.add(resolve(configDirectory, name));
157
+ }
158
+ const plugins = [];
159
+ const userPlugins = [...configured?.plugins ?? [], ...options.plugins ?? []];
160
+ const configuredNames = new Set(userPlugins.map((plugin) => plugin.postcssPlugin));
161
+ if (options.imports !== false && !configuredNames.has("postcss-import")) {
162
+ const importOptions = { load: (filename) => preprocess(filename, files) };
163
+ if (aliases.length) importOptions.resolve = (source, from, opts, node) => {
164
+ const mapped = aliasPath(source);
165
+ if (mapped !== source) return resolve(from, mapped);
166
+ return ownRequire("postcss-import/lib/resolve-id.js")(source, from, opts, node ?? { error: (message) => new Error(message) });
167
+ };
168
+ plugins.push(postcssImport(importOptions));
169
+ }
170
+ if (options.urls !== false && !configuredNames.has("postcss-url")) plugins.push(postcssUrl({ url: (asset) => aliasPath(asset.url).replace(/\\/g, "/") }));
171
+ if (options.autoprefixer !== false && !configuredNames.has("autoprefixer")) plugins.push(autoprefixer(typeof options.autoprefixer === "object" ? options.autoprefixer : {}));
172
+ plugins.push(...userPlugins);
173
+ const input = await preprocess(file, files);
174
+ const processOptions = {
175
+ ...configured?.options,
176
+ ...options.processOptions,
177
+ from: file,
178
+ to: file
179
+ };
180
+ if (options.sourcemap !== void 0) processOptions.map = options.sourcemap ? { inline: true } : false;
181
+ else if (processOptions.map) processOptions.map = {
182
+ ...typeof processOptions.map === "object" ? processOptions.map : {},
183
+ inline: true
184
+ };
185
+ const result = await postcss(plugins).process(input, processOptions);
186
+ collectConfigFiles();
187
+ for (const warning of result.warnings()) this.warn(warning.toString());
188
+ const globs = [];
189
+ for (const message of result.messages) {
190
+ if (message.type === "dependency" && typeof message.file === "string") files.add(resolve(message.file));
191
+ if (message.type === "dir-dependency" && typeof message.dir === "string") globs.push({
192
+ base: resolve(message.dir),
193
+ pattern: typeof message.glob === "string" ? message.glob : "**/*"
194
+ });
195
+ }
196
+ if (this.command === "build") {
197
+ for (const path of files) this.addWatchFile(path);
198
+ for (const glob of globs) this.addWatchFile(glob.base);
199
+ } else await watcher?.update(file, {
200
+ files,
201
+ globs
202
+ });
203
+ return this.emitAsset({
204
+ name: /\.css$/.test(file) ? basename(file) : basename(file) + ".css",
205
+ source: result.css
206
+ });
207
+ },
208
+ closeBundle: close
209
+ };
210
+ }
211
+ //#endregion
212
+ export { postcssPlugin as default, postcssPlugin as postcss };
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("PostCSS dependency 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,79 @@
1
1
  {
2
2
  "name": "lvyjs-plugin-postcss",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "PostCSS and optional Sass/Less integration for LVY",
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
+ "sass": "^1.0.0",
36
+ "less": "^4.0.0"
37
+ },
38
+ "dependencies": {
39
+ "postcss": "^8.4.47",
40
+ "postcss-import": "^16.1.0",
41
+ "postcss-url": "^10.1.3",
42
+ "autoprefixer": "^10.4.20",
43
+ "postcss-load-config": "^6.0.1",
44
+ "chokidar": "^5.0.0",
45
+ "picomatch": "^4.0.3",
46
+ "tsx": "^4.23.12"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^26.2.0",
50
+ "@types/picomatch": "^4.0.2",
51
+ "lvyjs": "0.3.0-alpha.6",
52
+ "tsdown": "^0.22.14",
53
+ "typescript": "^7.0.2",
54
+ "@types/postcss-import": "^14.0.3",
55
+ "@types/postcss-url": "^10.0.4",
56
+ "@types/less": "^3.0.8",
57
+ "sass": "^1.99.0",
58
+ "less": "^4.6.4",
59
+ "@tailwindcss/postcss": "^4.3.3",
60
+ "tailwindcss": "^4.3.3"
61
+ },
62
+ "publishConfig": {
63
+ "registry": "https://registry.npmjs.org",
64
+ "access": "public"
65
+ },
66
+ "repository": {
67
+ "type": "git",
68
+ "url": "https://github.com/lemonade-lab/lvyjs.git",
69
+ "directory": "packages/lvyjs-plugin-postcss"
70
+ },
71
+ "peerDependenciesMeta": {
72
+ "sass": {
73
+ "optional": true
74
+ },
75
+ "less": {
76
+ "optional": true
77
+ }
78
+ }
6
79
  }