skinsuite 0.1.0 → 0.2.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.
Files changed (82) hide show
  1. package/README.md +44 -6
  2. package/dist/constants.d.ts +44 -7
  3. package/dist/constants.d.ts.map +1 -1
  4. package/dist/constants.js +44 -7
  5. package/dist/constants.js.map +1 -1
  6. package/dist/index.d.ts +32 -6
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +25 -4
  9. package/dist/index.js.map +1 -1
  10. package/dist/manifest.d.ts +83 -0
  11. package/dist/manifest.d.ts.map +1 -0
  12. package/dist/manifest.js +90 -0
  13. package/dist/manifest.js.map +1 -0
  14. package/dist/registry.d.ts +73 -0
  15. package/dist/registry.d.ts.map +1 -0
  16. package/dist/registry.js +299 -0
  17. package/dist/registry.js.map +1 -0
  18. package/dist/remote-config.d.ts +25 -0
  19. package/dist/remote-config.d.ts.map +1 -0
  20. package/dist/remote-config.js +43 -0
  21. package/dist/remote-config.js.map +1 -0
  22. package/dist/scheme.d.ts +36 -0
  23. package/dist/scheme.d.ts.map +1 -0
  24. package/dist/scheme.js +47 -0
  25. package/dist/scheme.js.map +1 -0
  26. package/dist/types.d.ts +17 -1
  27. package/dist/types.d.ts.map +1 -1
  28. package/dist/views/device-registry.d.ts +45 -0
  29. package/dist/views/device-registry.d.ts.map +1 -0
  30. package/dist/views/device-registry.js +55 -0
  31. package/dist/views/device-registry.js.map +1 -0
  32. package/dist/views/device.d.ts +63 -0
  33. package/dist/views/device.d.ts.map +1 -0
  34. package/dist/views/device.js +59 -0
  35. package/dist/views/device.js.map +1 -0
  36. package/dist/views/params.d.ts +24 -21
  37. package/dist/views/params.d.ts.map +1 -1
  38. package/dist/views/params.js +11 -11
  39. package/dist/views/params.js.map +1 -1
  40. package/docs/onboarding.md +479 -0
  41. package/package.json +23 -2
  42. package/src/constants.ts +141 -100
  43. package/src/index.ts +128 -65
  44. package/src/manifest.ts +131 -0
  45. package/src/registry.ts +400 -0
  46. package/src/scheme.ts +94 -0
  47. package/src/types.ts +19 -1
  48. package/src/views/device-registry.ts +96 -0
  49. package/src/views/device.ts +107 -0
  50. package/src/views/params.ts +26 -23
  51. package/src/vite/dir.ts +85 -0
  52. package/src/vite/index.ts +41 -0
  53. package/src/vite/plugin.ts +409 -0
  54. package/src/vite/theme.d.ts +49 -0
  55. package/src/vue/ThemeDebugPanel.vue +36 -0
  56. package/src/vue/ThemeDebugPanelBody.vue +505 -0
  57. package/src/vue/ThemeRoot.vue +59 -0
  58. package/src/vue/ThemeView.vue +92 -0
  59. package/src/vue/create-theme-state.ts +116 -0
  60. package/src/vue/define-theme-device.ts +157 -0
  61. package/src/vue/device-containers.ts +73 -0
  62. package/src/vue/device-domain.ts +149 -0
  63. package/src/vue/index.ts +98 -0
  64. package/src/vue/panel.ts +16 -0
  65. package/src/vue/plugin.ts +35 -0
  66. package/src/vue/remote-fetch.ts +74 -0
  67. package/src/vue/theme-intent.ts +157 -0
  68. package/src/vue/theme-persistence.ts +136 -0
  69. package/src/vue/theme-runtime.ts +163 -0
  70. package/src/vue/theme-scope.ts +148 -0
  71. package/src/vue/theme-store.ts +58 -0
  72. package/src/vue/theme-views.ts +39 -0
  73. package/src/vue/token-runtime.ts +81 -0
  74. package/src/vue/types.ts +67 -0
  75. package/src/vue/useParentThemeSync.ts +99 -0
  76. package/src/vue/useThemeDebugPanel.ts +192 -0
  77. package/src/vue/useThemeSources.ts +74 -0
  78. package/src/vue/view-page.ts +99 -0
  79. package/src/vue/view-registry.ts +215 -0
  80. package/src/vue/viewport.ts +54 -0
  81. package/src/mode.ts +0 -38
  82. package/src/tone.ts +0 -56
@@ -0,0 +1,107 @@
1
+ /**
2
+ * 设备清单机制(**纯逻辑半边**)—— 「哪些设备、怎么判定」的唯一实现(内核版)
3
+ *
4
+ * === 设备 = 路由 + 容器 = 版式信息的一部分 ===
5
+ * 「有哪些设备」是宿主的部署事实(每个应用设备不同、容器位置不同),所以**设备清单
6
+ * (`DeviceDef[]`)由宿主声明**;但「怎么从清单判定出当前设备、怎么归一化外部输入、
7
+ * 怎么取容器」是**通用机制**,收在这里 —— 加一种设备时宿主只加一条声明,判定逻辑零改动。
8
+ *
9
+ * === 为什么这里只有「判定」、没有「组件」 ===
10
+ * 本文件是**纯逻辑**(不含 `Component` 类型),能被内核 tsc 独立检查、也能被任何前端复用。
11
+ * `containers` 用泛型 `T` 表达(宿主的「设备容器加载器」是 `T`),组件加载的**具体机制**
12
+ * (`pageContainers` / `resolveContainerOf`)在 `skinsuite/vue` 的 `device-containers`,
13
+ * 那里才 import Vue 的 `Component`。
14
+ *
15
+ * === 判定顺序就是清单顺序(先命中先用),最后一条必须无条件成立 ===
16
+ * 它是兜底设备。宿主关卡会核这一条。
17
+ *
18
+ * === 为什么判定必须收在一处 ===
19
+ * 三个分发器曾经各写一份「mini 来源」判定,口径不一致 → 同一个链接在不同页面落到
20
+ * 不同设备。现在判定只有一份,各页面自然同形。
21
+ *
22
+ * @author yijiu2025
23
+ * @since 2026-10-17(从 oauth21/src/theme/devices.ts 下沉)
24
+ */
25
+
26
+ /** 判定上下文 —— 判定所需的**全部**外部信息(与页面无关的原始事实) */
27
+ export interface DeviceMatchContext {
28
+ /** `?isMobile=true` —— 显式意图,优先级最高 */
29
+ explicitMobile: boolean;
30
+ /** 路由 path(不含 query / hash) */
31
+ path: string;
32
+ /** `?from=` 的取值(iframe 来源标记,空串表示没给) */
33
+ from: string;
34
+ /** `?fromLogin=` 的取值(历史键:从 mini 登录页跳来) */
35
+ fromLogin: string;
36
+ /** 视口/UA 判定结果 */
37
+ viewportIsMobile: boolean;
38
+ }
39
+
40
+ /** 一条设备声明(`containers` 是泛型:纯逻辑侧不关心它到底是什么,只透传) */
41
+ export interface DeviceDef<T = unknown> {
42
+ /** 设备 id = 主题包内的目录名,只允许 `[a-z0-9-]` */
43
+ id: string;
44
+ /** 中文名(调试面板 / 日志用) */
45
+ label: string;
46
+ /** 缩写名(URL 专属参数后缀用,如 `?th_<urlKey>` / `?th_color_<urlKey>`);缺省回落 `id` */
47
+ urlKey?: string;
48
+ /** 该设备在某页的容器(分发器渲染它);返回 undefined 表示「该设备没有这一页的容器」 */
49
+ containers: ReadonlyMap<string, T>;
50
+ /** 该设备在路由表里占的 path(相对根,`{page}` 由页面名替换) */
51
+ routePath(page: string): string;
52
+ /** 本次访问是否属于这台设备(清单顺序 = 优先级,先命中先用) */
53
+ match(ctx: DeviceMatchContext): boolean;
54
+ }
55
+
56
+ /** 归一化外部传入的设备名;不认识的取值返回 null(**不是**落默认设备) */
57
+ export function asDeviceId<T extends string>(
58
+ known: readonly string[],
59
+ raw: unknown
60
+ ): T | null {
61
+ return typeof raw === 'string' && (known as readonly string[]).includes(raw)
62
+ ? (raw as T)
63
+ : null;
64
+ }
65
+
66
+ /** 设备 id → 中文名(调试面板用;加设备不必再改面板) */
67
+ export function deviceLabel<T>(defs: readonly DeviceDef<T>[], id: string): string {
68
+ return defs.find(def => def.id === id)?.label ?? id;
69
+ }
70
+
71
+ /** 从外部输入判定「本次访问属于哪台设备」;清单顺序 = 优先级,返回值永远非空 */
72
+ export function pickDeviceId<T>(
73
+ defs: readonly DeviceDef<T>[],
74
+ ctx: DeviceMatchContext,
75
+ defaultDevice: string
76
+ ): string {
77
+ for (const def of defs) {
78
+ if (def.match(ctx)) return def.id;
79
+ }
80
+ // 清单被改坏(末条不再无条件成立)时的保底
81
+ return defaultDevice;
82
+ }
83
+
84
+ /** 取某设备的容器表;未知设备返回空表(调用方退到兜底设备) */
85
+ export function containersOf<T>(
86
+ defs: readonly DeviceDef<T>[],
87
+ id: string
88
+ ): ReadonlyMap<string, T> {
89
+ return defs.find(def => def.id === id)?.containers ?? new Map();
90
+ }
91
+
92
+ /** 从当前访问信息构造判定上下文 */
93
+ export function deviceContext(
94
+ query: Record<string, unknown> | undefined,
95
+ path: string,
96
+ viewportIsMobile: boolean
97
+ ): DeviceMatchContext {
98
+ const text = (key: string): string =>
99
+ typeof query?.[key] === 'string' ? (query[key] as string) : '';
100
+ return {
101
+ explicitMobile: text('isMobile') === 'true',
102
+ path: path.split(/[?#]/)[0],
103
+ from: text('from'),
104
+ fromLogin: text('fromLogin'),
105
+ viewportIsMobile
106
+ };
107
+ }
@@ -1,16 +1,22 @@
1
1
  /**
2
- * 版式参数的**设备维度**解析 —— `?view.<设备>` 支持
2
+ * 版式 / 配色参数的**设备维度**解析 —— `?th` / `?th_color`(下划线 + 设备缩写名)
3
+ *
4
+ * === 参数命名(2026-10-18 定稿,统一用下划线、禁用点号)===
5
+ * 版式:`?th`(三端通用) `?th_<设备缩写>`(某设备专属)
6
+ * 配色:`?th_color`(三端通用) `?th_color_<设备缩写>`(某设备专属)
7
+ * 设备专属键的后缀是**设备缩写名(urlKey)**,不是设备 id —— 由设备清单声明
8
+ * (`defineThemeDevice` 的 `urlKey`,缺省回落 `id`),内核不硬编码任何缩写。
3
9
  *
4
10
  * === 解决什么 ===
5
11
  * 同一条链接要能给三台设备各指定一套版式:
6
12
  * ```
7
- * /login?view=compact 三端共用(哪端没有就自动下沉到当前包)
8
- * /login?view.mobile=default&view.standard=compact 各端各指定(mini 走通用/包默认)
9
- * /login?view.mobile=default&view=default 设备专属优先,通用键兜底
13
+ * /login?th=compact 三端共用(哪端没有就自动下沉到当前包)
14
+ * /login?th_mobile=default&th_std=compact 各端各指定(mini 走通用/包默认)
15
+ * /login?th_mobile=default&th=default 设备专属优先,通用键兜底
10
16
  * ```
11
17
  *
12
18
  * === 取值链(每台设备各一条)===
13
- * `view.<设备>` → `view` → (返回 undefined:由调用方走包声明 → 环境变量 → 当前包)
19
+ * `th_<urlKey>` → `th` → (返回 undefined:由调用方走包声明 → 当前包)
14
20
  *
15
21
  * === 为什么收在内核(而不是各页文件里)===
16
22
  * 三个页面(login / register / forgot-password)各有**一份**注册表文件,但"哪个 query
@@ -21,21 +27,18 @@
21
27
  *
22
28
  * === 与配色侧同形(很重要)===
23
29
  * 配色侧(`oauth21/src/stores/theme.ts` 的 `readUrlIntent`)用同样的键形
24
- * (`theme.<设备>` / `skin.<设备>`)与同样的"设备专属优先"链。**两侧必须同形**,
25
- * 否则配置者要记两套规则,`?theme.mobile=blue&view.standard=compact` 这种混搭就写不出来。
30
+ * (`th_color_<urlKey>` / `th_color`)与同样的"设备专属优先"链。**两侧必须同形**,
31
+ * 否则配置者要记两套规则,`?th_color_mobile=blue&th_std=compact` 这种混搭就写不出来。
26
32
  * 但两侧读的入口不同:配色侧读 `window.location.search`(store 是全局的,不依赖路由),
27
33
  * 版式侧读**路由 query**(容器手上只有 `route.query`)—— 所以不是同一个函数,是同一套规则。
28
34
  *
29
- * ⚠️ query key 里带 `.` 是合法的(`URLSearchParams` 与 vue-router 都原样保留),
30
- * 但访问时**必须用下标**(`query['view.standard']`)—— 点号在 JS 里不是合法标识符。
31
- *
32
35
  * @author yijiu2025
33
- * @since 2026-09-26
36
+ * @since 2026-09-26(2026-10-18 改下划线 + urlKey:`?view.` → `?th_`、`?theme.` → `?th_color_`)
34
37
  */
35
38
  /** 支持设备维度的参数名(与配色侧 `readUrlIntent` 的键保持一致) */
36
- export type DeviceScopedParam = 'view' | 'theme';
39
+ export type DeviceScopedParam = 'th' | 'th_color';
37
40
 
38
- export const DEVICE_SCOPED_PARAMS: readonly DeviceScopedParam[] = ['view', 'theme'];
41
+ export const DEVICE_SCOPED_PARAMS: readonly DeviceScopedParam[] = ['th', 'th_color'];
39
42
 
40
43
  /** 值"存在且非空" —— 空串 / 空数组视为**未指定**,不顶掉兜底键 */
41
44
  function isPresent(value: unknown): boolean {
@@ -50,26 +53,26 @@ function isPresent(value: unknown): boolean {
50
53
  /**
51
54
  * 按**设备**读取版式 / 配色参数
52
55
  *
53
- * @param query 路由 query(`route.query`)
54
- * @param name 参数名(`'view'` / `'theme'`)
55
- * @param device 当前设备 —— 容器用自己的设备常量传进来(文件位置即身份)
56
- * @returns `query['<name>.<device>']` 优先;缺席或空值时退 `query[name]`;
56
+ * @param query 路由 query(`route.query`)
57
+ * @param name 参数名(`'th'` / `'th_color'`)
58
+ * @param deviceKey 设备**缩写名(urlKey)** —— 由调用方用 `urlKeyOf(deviceId)` 从设备清单查
59
+ * @returns `query['<name>_<deviceKey>']` 优先;缺席或空值时退 `query[name]`;
57
60
  * 两者都没有则返回 undefined,表示"**这台设备**没有被显式指定"
58
61
  *
59
62
  * ⚠️ 只决定"取哪个键",**不做合法性校验** —— 校验属于各页 `pick*ViewId` 与配色注册表
60
- * (本函数也拿不到"当前包里有没有这套版式")。返回值可能是数组(`?view=a&view=b`),
63
+ * (本函数也拿不到"当前包里有没有这套版式")。返回值可能是数组(`?th=a&th=b`),
61
64
  * 由下游按"非字符串即非法"处理,与改前的行为一致。
62
- * ⚠️ 空串视为"未指定":`?view.standard=` 不该把 `?view=compact` 顶掉 ——
65
+ * ⚠️ 空串视为"未指定":`?th_std=` 不该把 `?th=compact` 顶掉 ——
63
66
  * 部署方常把参数留空当作"这台设备不特殊指定"。
64
- * ⚠️ 设备名是**宿主**的词汇表(本包不认识具体取值),所以这里只把它当字符串拼键;
65
- * 「这个名字认不认得」由宿主的设备清单归一(`asThemeDevice`)。
67
+ * ⚠️ 缩写名是**宿主**的词汇表(本包不认识具体取值),所以这里只把它当字符串拼键;
68
+ * 「这个缩写名认不认得」由宿主的设备清单归一(`urlKeyOf`)。
66
69
  */
67
70
  export function readDeviceParam(
68
71
  query: Record<string, unknown> | undefined,
69
72
  name: DeviceScopedParam,
70
- device: string
73
+ deviceKey: string
71
74
  ): unknown {
72
- const scoped = query?.[`${name}.${device}`];
75
+ const scoped = query?.[`${name}_${deviceKey}`];
73
76
  if (isPresent(scoped)) return scoped;
74
77
  return query?.[name];
75
78
  }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * 主题**根目录**的解析入口(构建期,随 `skinsuite/vite` 一起发布)
3
+ *
4
+ * 属于主题库自带的构建能力:任何宿主(Vue / React / 任意 Vite 工程)引入
5
+ * `skinsuite` 后,只要在 `vite.config.ts` 挂上 `themeAssets()` 插件、并把目录解析
6
+ * 交给本文件的 `resolveThemeDir()`,就**不再需要自己写一行扫描/目录代码**。
7
+ *
8
+ * === 为什么走 env 而不是一份 TS 配置文件 ===
9
+ * 目录是**部署事实**(可能整包换掉一套品牌主题、可能放在 monorepo 的另一处),
10
+ * 写进源码就意味着换目录要改代码 + 重新 review。env 是"同一份产物服务多个部署"
11
+ * 的既有通道,目录也走同一条:
12
+ *
13
+ * ```
14
+ * VITE_THEME_DIR=src/theme # 相对宿主工程根;主题**根目录**
15
+ * ```
16
+ *
17
+ * === `VITE_THEME_DIR` 指向的是「主题根目录」,不是「主题包目录」===
18
+ * 根目录内是**三个固定子目录/文件**(约定俗成、名字固定,换根目录位置时整包跟着走):
19
+ *
20
+ * ```
21
+ * <根目录>/ ← VITE_THEME_DIR(如 src/theme)
22
+ * ├── theme.config.ts 清单:调试开关(契约 = defineThemeManifest)
23
+ * ├── themes/ 主题包目录(一个包 = 一种版式)
24
+ * │ ├── default/ ├── compact/
25
+ * └── views/ 声明目录(devices.ts 设备声明 + login.ts 等页面契约)
26
+ * ```
27
+ *
28
+ * 于是「主题域」是**一个自足目录**:换根目录位置时,主题包、清单、声明一起走,
29
+ * 宿主要改的只有 `VITE_THEME_DIR` 这一个变量。
30
+ *
31
+ * ⚠️ `themes` 与 `views` 是**固定子目录名**(不是可配置项):构建插件扫包目录与
32
+ * 声明目录都认这两个名字 —— 靠「名字固定」对齐,不靠配置。
33
+ *
34
+ * 而**主题域自己的策略**(调试开关、路由投影)不在这里 —— 那份配置跟着主题域走,
35
+ * 住在根目录的 `theme.config.ts`(契约见 `skinsuite` 的 `defineThemeManifest`)。
36
+ * 分工是「宿主说根目录在哪」/「主题域说自己怎么用」,不是同一类东西。
37
+ *
38
+ * ⚠️ 必须用 `loadEnv()` 读,不能直接 `process.env.VITE_THEME_DIR`:
39
+ * vite.config 执行时 Vite 还没加载 `.env` 文件(那只在 config 解析后发生),
40
+ * 直接读 `process.env` 在 `npm run dev` 下恒 undefined —— 于是永远回落默认值,
41
+ * 看着能用、改 env 却不生效,属于最难发现的一类。
42
+ *
43
+ * @author yijiu2025
44
+ * @since 2026-10-17
45
+ */
46
+ import path from 'node:path';
47
+ import { loadEnv } from 'vite';
48
+
49
+ /**
50
+ * 缺省主题根目录(相对宿主工程根)
51
+ *
52
+ * 留这个默认值是为了 **CI 与独立克隆**:`.env` 通常被 gitignore,全新 checkout 里
53
+ * 没有它,此时必须仍能构建 —— 否则"没配 env 就构建失败"会把部署门槛抬高。
54
+ * 宿主可通过 `resolveThemeDir(mode, root, { defaultDir })` 覆盖。
55
+ */
56
+ export const DEFAULT_THEME_DIR = 'src/theme';
57
+
58
+ /** 主题根目录内、主题包的子目录名(固定约定,宿主 `views/*.ts` 的字面量 glob 依赖它稳定) */
59
+ export const THEME_PACKAGES_SUBDIR = 'themes';
60
+
61
+ /** 主题根目录内的版式契约目录名(`views/*.ts`,设备声明 + 页面契约,插件副作用 import 拉齐) */
62
+ export const THEME_VIEWS_SUBDIR = 'views';
63
+
64
+ /** 主题根目录内的清单文件名(插件按它找 `theme.config.ts`) */
65
+ export const DEFAULT_THEME_MANIFEST = 'theme.config.ts';
66
+
67
+ /**
68
+ * 解析主题**根目录**为**绝对路径**
69
+ *
70
+ * @param mode Vite 的 mode(development / production / test …),决定叠加哪个 `.env.<mode>`
71
+ * @param root 以哪个目录为工程根找 `.env`,缺省 `process.cwd()`
72
+ * @param opts 可选覆盖:`defaultDir` 缺省回落目录、`envKey` 环境变量名
73
+ */
74
+ export function resolveThemeDir(
75
+ mode: string,
76
+ root: string = process.cwd(),
77
+ opts: { defaultDir?: string; envKey?: string } = {}
78
+ ): string {
79
+ const envKey = opts.envKey ?? 'VITE_THEME_DIR';
80
+ const defaultDir = opts.defaultDir ?? DEFAULT_THEME_DIR;
81
+ // 第三个参数是前缀白名单:只读 VITE_ 开头的,避免把后端密钥之类带进来
82
+ const env = loadEnv(mode, root, 'VITE_');
83
+ const configured = (env[envKey] ?? '').trim();
84
+ return path.resolve(root, configured || defaultDir);
85
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * `skinsuite/vite` —— 主题库的**构建期适配器**(Vite 插件 + 目录解析)
3
+ *
4
+ * 纯内核(`skinsuite`)与 Vue 绑定(`skinsuite/vue`)都刻意不碰构建器,因为
5
+ * "扫描主题目录"是 Vite 专属的能力(webpack 要写成 `require.context`)。这一半
6
+ * 收在这里,让宿主在 `vite.config.ts` 里三行接完,不必自己抄扫描逻辑:
7
+ *
8
+ * ```ts
9
+ * import { themeAssets, resolveThemeDir } from 'skinsuite/vite';
10
+ * export default defineConfig(({ mode }) => ({
11
+ * plugins: [themeAssets({ dir: resolveThemeDir(mode) })]
12
+ * }));
13
+ * ```
14
+ *
15
+ * === 为什么这个子路径指向源码 `.ts` 而不是 `dist/` ===
16
+ * `vite.config.ts` 由 Vite 用 esbuild 预打包后执行,裸包说明符会被当外部依赖交给
17
+ * Node `import()`。若指向 `dist/`,就要求"包必须先构建" —— CI 里 `npm ci` 不会
18
+ * 构建工作区包,配置会直接起不来。Vite 自己能转译 TS,源码直供就绕开这个前提,
19
+ * 与 `skinsuite/vue` 同一手法。
20
+ *
21
+ * ⚠️ 本层相对 import **必须用显式 `.ts` 后缀**(`./plugin.ts` 而非 `./plugin` 或
22
+ * `./plugin.js`):
23
+ * 这个入口是**唯一在 vite.config / vitest.config 里被 import 的子路径**,config
24
+ * 被 Node 直接执行(Vite 的 config 加载器只 bundle config 自身),Node 既不自动补
25
+ * 无后缀、也不把 `.js` 映射到 `.ts` 源码。而纯内核(`./tone.js`)走 NodeNext 编译、
26
+ * `skinsuite/vue` 走应用代码的 Vite 转译,两者都用不上这个入口的约束,所以这里
27
+ * 自成一套后缀规则。`.ts` 后缀由宿主的 `allowImportingTsExtensions` + Vite 转译共同消化。
28
+ *
29
+ * @author yijiu2025
30
+ * @since 2026-10-17
31
+ */
32
+ /// <reference path="./theme.d.ts" />
33
+
34
+ export { themeAssets } from './plugin.ts';
35
+ export type { ThemeAssetsPluginOptions } from './plugin.ts';
36
+ export {
37
+ DEFAULT_THEME_DIR,
38
+ THEME_PACKAGES_SUBDIR,
39
+ DEFAULT_THEME_MANIFEST,
40
+ resolveThemeDir
41
+ } from './dir.ts';