dsh-taiwind 0.1.0 → 0.3.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.md CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2025-PRESENT dsh-tauri-desk
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.
1
+ MIT License
2
+
3
+ Copyright (c) 2025-PRESENT dsh-tauri-desk
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 CHANGED
@@ -32,12 +32,10 @@ flowchart LR
32
32
  ## ✨ 特性
33
33
 
34
34
  - **CSS-first**:保留未编译的 `main.css`,支持 `@theme`、`@utility`、`@custom-variant`、`@apply`、`@source inline(...)`。
35
- - **按需扫描**:从 `clientModules.graph()` 取当前客户端包,`clientPath(id)` 定位构建产物,不依赖插件源码。
36
35
  - **官方编译器**:Tailwind 扫描器提取候选类,统一编译;不加载 Preflight,不重置宿主界面。
37
- - **单一 style**:首屏注入 `<head>`,客户端接管同一个 `<style data-plugin="dsh-taiwind" data-plugin-css="utilities">`。
38
- - **自动收缩**:删除类名、分包或卸载插件后,下次成功编译即收缩;无磁盘缓存。
39
- - **失败保留**:运行期编译失败返回 503,已打开页面保留上一份有效样式,修复后自动恢复。
40
- - **条件请求**:带 ETag 的 CSS 接口,命中返回 304;插件图变化立即刷新,另每两秒检查产物与配置。
36
+ - **单一入口**:首屏注入 `<head>`,客户端接管同一个 `<style data-plugin="dsh-taiwind" data-plugin-css="utilities">`。
37
+ - **热更新**:插件安装/卸载/启停,或客户端产物、`main.css` 改动后都会自动重新编译;已打开的页面无需刷新即可换上新样式,也不必重启宿主。
38
+ - **条件请求**:带 ETag 的 CSS 接口,命中返回 304;内容没变就不读产物、不跑 Tailwind。
41
39
  - **完整清理**:停用移除路由、监听、定时器与客户端样式,并取消进行中的请求。
42
40
 
43
41
  ## 🚀 快速开始
@@ -58,15 +56,24 @@ pnpm add dsh-taiwind
58
56
  name: dsh-taiwind
59
57
  ```
60
58
 
61
- 用户启用业务插件时,`dsh-taiwind` 将随行一起启用,而客户端半也必须有一行启用中的 Loader 条目才会被 `dsh-client-modules` 加载。
59
+ 用户启用业务插件时,`dsh-taiwind` 将随行一起启用。
62
60
 
63
- - 它没有界面、没有工具,唯一产物是共享样式接口 `plugins/dsh-taiwind.css` 和页面上的一个 `<style data-plugin="dsh-taiwind">`。
64
- - 宿主半要求 `clientModules`、`webServer`、`connection` 三件服务齐备。
61
+ - 它没有界面和工具,唯一产物是共享样式接口 `plugins/taiwind.css` 和页面上的 `<style data-plugin="dsh-taiwind">`。
65
62
  - 扫描范围是当前客户端图里所有插件的构建产物,插件图变化会自动触发重新编译。
66
63
 
67
- ### 2. 在插件包里写样式
64
+ ### 2. 声明样式入口
68
65
 
69
- 在插件包中保留未编译的 `main.css`,不需要 `tailwind.config.js`,也不用把路径传给本插件:
66
+ 在插件包的 `package.json` 里显式声明未编译的 CSS 入口(相对包根,必须位于包内):
67
+
68
+ ```json
69
+ {
70
+ "dsh": {
71
+ "taiwind": "src/main.css"
72
+ }
73
+ }
74
+ ```
75
+
76
+ `main.css` 保留未编译的 CSS-first 写法:
70
77
 
71
78
  ```css
72
79
  @import "tailwindcss";
@@ -81,17 +88,13 @@ pnpm add dsh-taiwind
81
88
  }
82
89
  ```
83
90
 
84
- 客户端直接使用 `bg-notes-accent w-notes-panel notes-compact` 即可。
85
-
86
- 入口发现顺序:
91
+ 🚀 客户端直接使用 taiwind 类名,例如 `bg-notes-accent w-notes-panel notes-compact` 即可。
87
92
 
88
- 1. 包根目录的 `main.css`
89
- 2. 客户端入口同目录的 `client/main.css`
90
- 3. 找到多个会明确报错,不任意选择
93
+ 没有声明 `dsh.taiwind` 的包不参与样式编译;声明了但路径不是相对 `.css`、文件不存在或指向包外都会明确报错,不会去猜别的 `main.css`。
91
94
 
92
95
  ### 3. 发布时携带样式文件
93
96
 
94
- 在 `package.json` 中添加 `files` 字段,发布时需要携带 `main.css`:
97
+ 在 `package.json` 中添加 `files` 字段,发布时需要携带入口 CSS 及它引用的相对 CSS:
95
98
 
96
99
  ```json
97
100
  {
@@ -104,7 +107,7 @@ pnpm add dsh-taiwind
104
107
 
105
108
  ## ⚙️ 配置文件(可选)
106
109
 
107
- 可在包根目录放一份 `tailwind.config.js`(也认 `.cjs` / `.mjs`),或用 CSS 的 `@config` 显式引用:
110
+ 可在包根目录放一份 `tailwind.config.js`,或用 CSS 的 `@config` 显式引用:
108
111
 
109
112
  ```js
110
113
  /** @type {import("tailwindcss").Config} */
package/cordis.patch.yml CHANGED
@@ -1,3 +1,3 @@
1
- - insert:
2
- - id: dsh-taiwind
3
- name: dsh-taiwind
1
+ - insert:
2
+ - id: dsh-taiwind
3
+ name: dsh-taiwind
package/dist/client.cjs CHANGED
@@ -1,4 +1,4 @@
1
1
  window.__ModuleLoader__.load({id:"dsh-taiwind",factory:(require)=>{var module={exports:{}};var exports=module.exports;
2
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=`dsh-taiwind`;function t(){let t=document.head.querySelector(`[data-plugin="dsh-taiwind"][data-plugin-css="utilities"]`),n=t??document.createElement(`style`);n.dataset.plugin=e,n.dataset.pluginCss=`utilities`,t||document.head.append(n);let r=new AbortController,i=``,a=!1,o,s=``,c=async()=>{let t=new AbortController,l=setTimeout(()=>t.abort(),15e3),u=()=>t.abort();r.signal.addEventListener(`abort`,u,{once:!0});try{let e=await fetch(new URL(`plugins/dsh-taiwind.css`,document.baseURI),{credentials:`same-origin`,cache:`no-cache`,headers:i?{"If-None-Match":i}:{},signal:t.signal});if(e.status!==304){if(!e.ok)throw Error(`Stylesheet request failed (${e.status})`);let t=await e.text();a||(n.textContent!==t&&(n.textContent=t),i=e.headers.get(`ETag`)??``)}s=``}catch(t){if(!a){let n=t instanceof Error?t.message:String(t);s!==n&&(s=n,console.warn(`${e}: ${n}; keeping previous stylesheet`))}}finally{clearTimeout(l),r.signal.removeEventListener(`abort`,u),a||(o=setTimeout(()=>void c(),2e3))}};return c(),()=>{a=!0,r.abort(),clearTimeout(o),n.remove()}}function n(e){e.effect(t)}exports.apply=n;
2
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=`dsh-taiwind`;function t(){let t=document.head.querySelector(`[data-plugin="dsh-taiwind"][data-plugin-css="utilities"]`),n=t??document.createElement(`style`);n.dataset.plugin=e,n.dataset.pluginCss=`utilities`,t||document.head.append(n);let r=new AbortController,i=``,a=!1,o,s=``,c=async()=>{let t=new AbortController,l=setTimeout(()=>t.abort(),15e3),u=()=>t.abort();r.signal.addEventListener(`abort`,u,{once:!0});try{let e=await fetch(new URL(`plugins/taiwind.css`,document.baseURI),{credentials:`same-origin`,cache:`no-cache`,headers:i?{"If-None-Match":i}:{},signal:t.signal});if(e.status!==304){if(!e.ok)throw Error(`Stylesheet request failed (${e.status})`);let t=await e.text();a||(n.textContent!==t&&(n.textContent=t),i=e.headers.get(`ETag`)??``)}s=``}catch(t){if(!a){let n=t instanceof Error?t.message:String(t);s!==n&&(s=n,console.warn(`${e}: ${n}; keeping previous stylesheet`))}}finally{clearTimeout(l),r.signal.removeEventListener(`abort`,u),a||(o=setTimeout(()=>void c(),2e3))}};return c(),()=>{a=!0,r.abort(),clearTimeout(o),n.remove()}}function n(e){e.effect(t)}exports.apply=n;
3
3
  return module.exports;}});
4
4
  //# sourceMappingURL=client.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.cjs","names":[],"sources":["../src/constants.ts","../src/client/styles.ts","../src/client/index.ts"],"sourcesContent":["export const PLUGIN_ID = 'dsh-taiwind'\r\nexport const STYLESHEET_PATH = 'plugins/dsh-taiwind.css'\r\nexport const STYLE_SELECTOR = '[data-plugin=\"dsh-taiwind\"][data-plugin-css=\"utilities\"]'\r\nexport const POLL_INTERVAL_MS = 2000\r\n","import { PLUGIN_ID, POLL_INTERVAL_MS, STYLE_SELECTOR, STYLESHEET_PATH } from '../constants'\r\n\r\nexport function mountStyles(): () => void {\r\n const existing = document.head.querySelector<HTMLStyleElement>(STYLE_SELECTOR)\r\n const style = existing ?? document.createElement('style')\r\n style.dataset.plugin = PLUGIN_ID\r\n style.dataset.pluginCss = 'utilities'\r\n if (!existing)\r\n document.head.append(style)\r\n\r\n const controller = new AbortController()\r\n let etag = ''\r\n let disposed = false\r\n let timer: ReturnType<typeof setTimeout> | undefined\r\n let lastError = ''\r\n\r\n const update = async (): Promise<void> => {\r\n const request = new AbortController()\r\n const timeout = setTimeout(() => request.abort(), 15000)\r\n const abort = (): void => request.abort()\r\n controller.signal.addEventListener('abort', abort, { once: true })\r\n try {\r\n const response = await fetch(new URL(STYLESHEET_PATH, document.baseURI), {\r\n credentials: 'same-origin',\r\n cache: 'no-cache',\r\n headers: etag ? { 'If-None-Match': etag } : {},\r\n signal: request.signal,\r\n })\r\n if (response.status !== 304) {\r\n if (!response.ok)\r\n throw new Error(`Stylesheet request failed (${response.status})`)\r\n const css = await response.text()\r\n if (!disposed) {\r\n if (style.textContent !== css)\r\n style.textContent = css\r\n etag = response.headers.get('ETag') ?? ''\r\n }\r\n }\r\n lastError = ''\r\n }\r\n catch (error) {\r\n if (!disposed) {\r\n const message = error instanceof Error ? error.message : String(error)\r\n if (lastError !== message) {\r\n lastError = message\r\n console.warn(`${PLUGIN_ID}: ${message}; keeping previous stylesheet`)\r\n }\r\n }\r\n }\r\n finally {\r\n clearTimeout(timeout)\r\n controller.signal.removeEventListener('abort', abort)\r\n if (!disposed)\r\n timer = setTimeout(() => void update(), POLL_INTERVAL_MS)\r\n }\r\n }\r\n void update()\r\n\r\n return () => {\r\n disposed = true\r\n controller.abort()\r\n clearTimeout(timer)\r\n style.remove()\r\n }\r\n}\r\n","import type { Context } from '@deepseek-ai/cordis'\r\nimport { mountStyles } from './styles'\r\n\r\nexport function apply(ctx: Context): void {\r\n ctx.effect(mountStyles)\r\n}\r\n"],"mappings":";mEAAA,MAAa,EAAY,cCEzB,SAAgB,GAA0B,CACxC,IAAM,EAAW,SAAS,KAAK,cAAgC,0DAAc,EACvE,EAAQ,GAAY,SAAS,cAAc,OAAO,EACxD,EAAM,QAAQ,OAAS,EACvB,EAAM,QAAQ,UAAY,YACrB,GACH,SAAS,KAAK,OAAO,CAAK,EAE5B,IAAM,EAAa,IAAI,gBACnB,EAAO,GACP,EAAW,GACX,EACA,EAAY,GAEV,EAAS,SAA2B,CACxC,IAAM,EAAU,IAAI,gBACd,EAAU,eAAiB,EAAQ,MAAM,EAAG,IAAK,EACjD,MAAoB,EAAQ,MAAM,EACxC,EAAW,OAAO,iBAAiB,QAAS,EAAO,CAAE,KAAM,EAAK,CAAC,EACjE,GAAI,CACF,IAAM,EAAW,MAAM,MAAM,IAAI,IAAI,0BAAiB,SAAS,OAAO,EAAG,CACvE,YAAa,cACb,MAAO,WACP,QAAS,EAAO,CAAE,gBAAiB,CAAK,EAAI,CAAC,EAC7C,OAAQ,EAAQ,MAClB,CAAC,EACD,GAAI,EAAS,SAAW,IAAK,CAC3B,GAAI,CAAC,EAAS,GACZ,MAAU,MAAM,8BAA8B,EAAS,OAAO,EAAE,EAClE,IAAM,EAAM,MAAM,EAAS,KAAK,EAC3B,IACC,EAAM,cAAgB,IACxB,EAAM,YAAc,GACtB,EAAO,EAAS,QAAQ,IAAI,MAAM,GAAK,GAE3C,CACA,EAAY,EACd,OACO,EAAO,CACZ,GAAI,CAAC,EAAU,CACb,IAAM,EAAU,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,EACjE,IAAc,IAChB,EAAY,EACZ,QAAQ,KAAK,GAAG,EAAU,IAAI,EAAQ,8BAA8B,EAExE,CACF,QACQ,CACN,aAAa,CAAO,EACpB,EAAW,OAAO,oBAAoB,QAAS,CAAK,EAC/C,IACH,EAAQ,eAAiB,KAAK,EAAO,EAAG,GAAgB,EAC5D,CACF,EAGA,OAFA,EAAY,MAEC,CACX,EAAW,GACX,EAAW,MAAM,EACjB,aAAa,CAAK,EAClB,EAAM,OAAO,CACf,CACF,CC7DA,SAAgB,EAAM,EAAoB,CACxC,EAAI,OAAO,CAAW,CACxB"}
1
+ {"version":3,"file":"client.cjs","names":[],"sources":["../src/constants.ts","../src/client/styles.ts","../src/client/index.ts"],"sourcesContent":["export const PLUGIN_ID = 'dsh-taiwind'\nexport const STYLESHEET_PATH = 'plugins/taiwind.css'\nexport const STYLE_SELECTOR = '[data-plugin=\"dsh-taiwind\"][data-plugin-css=\"utilities\"]'\nexport const POLL_INTERVAL_MS = 2000\n","import { PLUGIN_ID, POLL_INTERVAL_MS, STYLE_SELECTOR, STYLESHEET_PATH } from '../constants'\n\nexport function mountStyles(): () => void {\n const existing = document.head.querySelector<HTMLStyleElement>(STYLE_SELECTOR)\n const style = existing ?? document.createElement('style')\n style.dataset.plugin = PLUGIN_ID\n style.dataset.pluginCss = 'utilities'\n if (!existing)\n document.head.append(style)\n\n const controller = new AbortController()\n let etag = ''\n let disposed = false\n let timer: ReturnType<typeof setTimeout> | undefined\n let lastError = ''\n\n const update = async (): Promise<void> => {\n const request = new AbortController()\n const timeout = setTimeout(() => request.abort(), 15000)\n const abort = (): void => request.abort()\n controller.signal.addEventListener('abort', abort, { once: true })\n try {\n const response = await fetch(new URL(STYLESHEET_PATH, document.baseURI), {\n credentials: 'same-origin',\n cache: 'no-cache',\n headers: etag ? { 'If-None-Match': etag } : {},\n signal: request.signal,\n })\n if (response.status !== 304) {\n if (!response.ok)\n throw new Error(`Stylesheet request failed (${response.status})`)\n const css = await response.text()\n if (!disposed) {\n if (style.textContent !== css)\n style.textContent = css\n etag = response.headers.get('ETag') ?? ''\n }\n }\n lastError = ''\n }\n catch (error) {\n if (!disposed) {\n const message = error instanceof Error ? error.message : String(error)\n if (lastError !== message) {\n lastError = message\n console.warn(`${PLUGIN_ID}: ${message}; keeping previous stylesheet`)\n }\n }\n }\n finally {\n clearTimeout(timeout)\n controller.signal.removeEventListener('abort', abort)\n if (!disposed)\n timer = setTimeout(() => void update(), POLL_INTERVAL_MS)\n }\n }\n void update()\n\n return () => {\n disposed = true\n controller.abort()\n clearTimeout(timer)\n style.remove()\n }\n}\n","import type { Context } from '@deepseek-ai/cordis'\nimport { mountStyles } from './styles'\n\nexport function apply(ctx: Context): void {\n ctx.effect(mountStyles)\n}\n"],"mappings":";mEAAA,MAAa,EAAY,cCEzB,SAAgB,GAA0B,CACxC,IAAM,EAAW,SAAS,KAAK,cAAgC,0DAAc,EACvE,EAAQ,GAAY,SAAS,cAAc,OAAO,EACxD,EAAM,QAAQ,OAAS,EACvB,EAAM,QAAQ,UAAY,YACrB,GACH,SAAS,KAAK,OAAO,CAAK,EAE5B,IAAM,EAAa,IAAI,gBACnB,EAAO,GACP,EAAW,GACX,EACA,EAAY,GAEV,EAAS,SAA2B,CACxC,IAAM,EAAU,IAAI,gBACd,EAAU,eAAiB,EAAQ,MAAM,EAAG,IAAK,EACjD,MAAoB,EAAQ,MAAM,EACxC,EAAW,OAAO,iBAAiB,QAAS,EAAO,CAAE,KAAM,EAAK,CAAC,EACjE,GAAI,CACF,IAAM,EAAW,MAAM,MAAM,IAAI,IAAI,sBAAiB,SAAS,OAAO,EAAG,CACvE,YAAa,cACb,MAAO,WACP,QAAS,EAAO,CAAE,gBAAiB,CAAK,EAAI,CAAC,EAC7C,OAAQ,EAAQ,MAClB,CAAC,EACD,GAAI,EAAS,SAAW,IAAK,CAC3B,GAAI,CAAC,EAAS,GACZ,MAAU,MAAM,8BAA8B,EAAS,OAAO,EAAE,EAClE,IAAM,EAAM,MAAM,EAAS,KAAK,EAC3B,IACC,EAAM,cAAgB,IACxB,EAAM,YAAc,GACtB,EAAO,EAAS,QAAQ,IAAI,MAAM,GAAK,GAE3C,CACA,EAAY,EACd,OACO,EAAO,CACZ,GAAI,CAAC,EAAU,CACb,IAAM,EAAU,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,EACjE,IAAc,IAChB,EAAY,EACZ,QAAQ,KAAK,GAAG,EAAU,IAAI,EAAQ,8BAA8B,EAExE,CACF,QACQ,CACN,aAAa,CAAO,EACpB,EAAW,OAAO,oBAAoB,QAAS,CAAK,EAC/C,IACH,EAAQ,eAAiB,KAAK,EAAO,EAAG,GAAgB,EAC5D,CACF,EAGA,OAFA,EAAY,MAEC,CACX,EAAW,GACX,EAAW,MAAM,EACjB,aAAa,CAAK,EAClB,EAAM,OAAO,CACf,CACF,CC7DA,SAAgB,EAAM,EAAoB,CACxC,EAAI,OAAO,CAAW,CACxB"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { createHash } from "node:crypto";
3
- import { readFile, readdir, realpath } from "node:fs/promises";
3
+ import { readFile, readdir, realpath, stat } from "node:fs/promises";
4
4
  import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { Scanner } from "@tailwindcss/oxide";
@@ -138,34 +138,20 @@ async function bundles(directory) {
138
138
  }
139
139
  return files;
140
140
  }
141
- async function cssEntry(root, clientDirectory) {
142
- for (const directory of /* @__PURE__ */ new Set([root, clientDirectory])) {
143
- const entry = (await readdir(directory, { withFileTypes: true })).find((item) => item.name === "main.css");
144
- if (entry) {
145
- if (!entry.isFile()) throw new Error(`${directory}: main.css must be a regular file`);
146
- return join(directory, entry.name);
147
- }
141
+ async function cssEntry(root, owner) {
142
+ const entry = JSON.parse(await readFile(join(root, "package.json"), "utf8")).dsh?.taiwind;
143
+ if (entry === void 0) {
144
+ if (await stat(join(root, "main.css")).then((info) => info.isFile(), () => false)) throw new Error(`${owner}: main.css is not discovered automatically anymore; declare "dsh": { "taiwind": "main.css" } in package.json`);
145
+ return;
148
146
  }
149
- const found = [];
150
- const visit = async (directory) => {
151
- for (const item of await readdir(directory, { withFileTypes: true })) {
152
- if ([
153
- "node_modules",
154
- "archive",
155
- ".git"
156
- ].includes(item.name) || item.isSymbolicLink()) continue;
157
- const path = join(directory, item.name);
158
- if (item.isDirectory()) await visit(path);
159
- else if (item.isFile() && item.name === "main.css") found.push(path);
160
- }
161
- };
162
- await visit(root);
163
- if (found.length > 1) throw new Error(`${root}: multiple main.css entries found: ${found.sort().join(", ")}`);
164
- return found[0];
147
+ if (typeof entry !== "string" || !entry.trim() || extname(entry) !== ".css") throw new Error(`${owner}: dsh.taiwind must be a relative .css path`);
148
+ const path = resolve(root, entry);
149
+ if (!await stat(path).then((info) => info.isFile(), () => false)) throw new Error(`${owner}: dsh.taiwind entry not found: ${entry}`);
150
+ return path;
165
151
  }
166
152
  async function compilePlugins(entries) {
167
153
  const hash = createHash("sha256");
168
- const sources = [];
154
+ const bundleFiles = [];
169
155
  const extend = Object.create(null);
170
156
  const merged = { theme: { extend } };
171
157
  const owners = /* @__PURE__ */ new Map();
@@ -281,14 +267,16 @@ async function compilePlugins(entries) {
281
267
  for (const file of await bundles(dirname(path))) {
282
268
  if (seen.has(file)) continue;
283
269
  seen.add(file);
284
- const content = await readFile(file, "utf8");
285
- hash.update(JSON.stringify([file, content]));
286
- sources.push({
287
- content,
288
- extension: extname(file).slice(1)
289
- });
270
+ const info = await stat(file);
271
+ hash.update(JSON.stringify([
272
+ file,
273
+ info.mtimeMs,
274
+ info.ctimeMs,
275
+ info.size
276
+ ]));
277
+ bundleFiles.push(file);
290
278
  }
291
- const stylesheet = await cssEntry(root, dirname(path));
279
+ const stylesheet = await cssEntry(root, entry.id);
292
280
  if (stylesheet) {
293
281
  cssImports.push(await loadCss(stylesheet, root, entry.id));
294
282
  continue;
@@ -305,7 +293,12 @@ async function compilePlugins(entries) {
305
293
  const utilities = await readFile(new URL(import.meta.resolve("tailwindcss/utilities.css")), "utf8");
306
294
  hash.update(JSON.stringify([theme, utilities]));
307
295
  const digest = hash.digest("hex");
308
- if (cached?.hash === digest) return cached.css;
296
+ if (cached?.signature === digest) return cached.css;
297
+ const sources = [];
298
+ for (const file of bundleFiles) sources.push({
299
+ content: await readFile(file, "utf8"),
300
+ extension: extname(file).slice(1)
301
+ });
309
302
  const candidates = new Scanner({}).scanFiles(sources);
310
303
  const compiler = await compile(`@layer theme, base, components, utilities;\n@layer theme {\n${theme}\n}\n${cssImports.map((path) => `@import ${JSON.stringify(path.replaceAll("\\", "/"))};`).join("\n")}\n@layer utilities {\n${utilities}\n}\n@config "dsh-taiwind:merged";`, {
311
304
  loadModule: async (id) => {
@@ -330,7 +323,7 @@ async function compilePlugins(entries) {
330
323
  if (compiler.sources.length || compiler.root !== null && compiler.root !== "none") throw new Error("Tailwind source(...) directory scanning is not supported; file candidates come from client bundles");
331
324
  const css = compiler.build(candidates);
332
325
  cached = {
333
- hash: digest,
326
+ signature: digest,
334
327
  css
335
328
  };
336
329
  return css;
@@ -338,7 +331,7 @@ async function compilePlugins(entries) {
338
331
  //#endregion
339
332
  //#region src/constants.ts
340
333
  const PLUGIN_ID = "dsh-taiwind";
341
- const STYLESHEET_PATH = "plugins/dsh-taiwind.css";
334
+ const STYLESHEET_PATH = "plugins/taiwind.css";
342
335
  const POLL_INTERVAL_MS = 2e3;
343
336
  //#endregion
344
337
  //#region src/index.ts
@@ -351,7 +344,7 @@ const inject = [
351
344
  async function apply(ctx) {
352
345
  let css = "";
353
346
  let etag = "";
354
- let failed = false;
347
+ let failed = true;
355
348
  let disposed = false;
356
349
  let generation = 0;
357
350
  let pending;
@@ -385,23 +378,24 @@ async function apply(ctx) {
385
378
  ctx.effect(() => () => {
386
379
  disposed = true;
387
380
  });
381
+ const fail = (error) => {
382
+ if (disposed) return;
383
+ failed = true;
384
+ const message = error instanceof Error ? error.message : String(error);
385
+ if (message !== lastError) {
386
+ lastError = message;
387
+ ctx.logger.warn(`${PLUGIN_ID}: ${message}; keeping previous stylesheet`);
388
+ }
389
+ };
388
390
  const update = () => {
389
- refresh().catch((error) => {
390
- if (disposed) return;
391
- failed = true;
392
- const message = error instanceof Error ? error.message : String(error);
393
- if (message !== lastError) {
394
- lastError = message;
395
- ctx.logger.warn(`${PLUGIN_ID}: ${message}; keeping previous stylesheet`);
396
- }
397
- });
391
+ refresh().catch(fail);
398
392
  };
399
- ctx.effect(() => ctx.clientModules.onGraphChanged(() => {
393
+ const invalidate = () => {
400
394
  generation++;
401
395
  update();
402
- }));
403
- await refresh();
404
- if (disposed) return;
396
+ };
397
+ ctx.effect(() => ctx.clientModules.onGraphChanged(invalidate));
398
+ ctx.effect(() => ctx.clientModules.onRebuilt(invalidate));
405
399
  ctx.effect(() => ctx.webServer.register({
406
400
  kind: "exact",
407
401
  path: `/${STYLESHEET_PATH}`,
@@ -447,6 +441,11 @@ async function apply(ctx) {
447
441
  timer.unref();
448
442
  return () => clearInterval(timer);
449
443
  });
444
+ try {
445
+ await refresh();
446
+ } catch (error) {
447
+ fail(error);
448
+ }
450
449
  }
451
450
  //#endregion
452
451
  export { apply, inject, name };
package/icon.svg CHANGED
@@ -1 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#102a43"/><g fill="none" stroke="#67e8f9" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 18h12l9 14h15M14 46h12l9-14M14 32h36"/><path d="m44 26 6 6-6 6"/></g></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#102a43"/><g fill="none" stroke="#67e8f9" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 18h12l9 14h15M14 46h12l9-14M14 32h36"/><path d="m44 26 6 6-6 6"/></g></svg>
package/locale/en.json CHANGED
@@ -1,6 +1,6 @@
1
- {
2
- "meta": {
3
- "title": "Shared Tailwind CSS",
4
- "description": "Combines client bundle utility classes into one stylesheet, with per-plugin static Tailwind configuration."
5
- }
6
- }
1
+ {
2
+ "meta": {
3
+ "title": "Shared Tailwind CSS",
4
+ "description": "Combines client bundle utility classes into one stylesheet, with per-plugin static Tailwind configuration."
5
+ }
6
+ }
package/locale/zh.json CHANGED
@@ -1,6 +1,6 @@
1
- {
2
- "meta": {
3
- "title": "共享 Tailwind 样式",
4
- "description": "聚合客户端插件的工具类,生成一份去重样式,支持各插件独立的静态 Tailwind 配置。"
5
- }
6
- }
1
+ {
2
+ "meta": {
3
+ "title": "共享 Tailwind 样式",
4
+ "description": "聚合客户端插件的工具类,生成一份去重样式,支持各插件独立的静态 Tailwind 配置。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-taiwind",
3
3
  "type": "module",
4
- "version": "0.1.0",
4
+ "version": "0.3.0",
5
5
  "description": "One shared Tailwind stylesheet for DSH plugins with per-plugin configuration",
6
6
  "author": "Hairy <wwu710632@gmail.com>",
7
7
  "license": "MIT",