nuxt-imc 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 +152 -0
- package/dist/module.d.mts +22 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +124 -0
- package/dist/types.d.mts +3 -0
- package/package.json +76 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 hzgotb
|
|
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,152 @@
|
|
|
1
|
+
# nuxt-imc
|
|
2
|
+
|
|
3
|
+
`nuxt-imc` 是一个 Nuxt module,用来把“目录式组件库”里的具名导出批量注册成 Nuxt 组件和 auto-import composable。
|
|
4
|
+
|
|
5
|
+
它适合这样的场景:
|
|
6
|
+
|
|
7
|
+
- 组件库不是一堆散落的 `.vue` 文件,而是按目录组织,每个组件目录通过 `index.ts` 或 `index.js` 统一导出。
|
|
8
|
+
- 希望给一组组件统一加前缀,例如 `new`。
|
|
9
|
+
- 希望组件和 `cps*` 风格的 composable 通过同一个入口暴露。
|
|
10
|
+
|
|
11
|
+
## 安装
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add nuxt-imc
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 如何使用
|
|
18
|
+
|
|
19
|
+
在 `nuxt.config.ts` 的 `modules` 里启用,并提供 `imc` 配置:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
export default defineNuxtConfig({
|
|
23
|
+
modules: [
|
|
24
|
+
'nuxt-imc'
|
|
25
|
+
],
|
|
26
|
+
|
|
27
|
+
imc: {
|
|
28
|
+
libraries: [
|
|
29
|
+
{ composablePrefix: 'cps', directory: 'app/components/ui', prefix: 'new' },
|
|
30
|
+
{ directory: 'app/components/atom', ignore: true }
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`libraries` 每一项的含义:
|
|
37
|
+
|
|
38
|
+
- `composablePrefix`: 用来识别 composable 导出名的前缀,默认是 `cps`。
|
|
39
|
+
- `directory`: 组件库根目录,路径相对于 Nuxt root。
|
|
40
|
+
- `ignore`: 为 `true` 时,这一项会作为独立忽略规则;该目录不会参与 Nuxt 组件扫描,也不会作为 IMC library 解析。
|
|
41
|
+
- `prefix`: 注册到模板中的组件名前缀,必须是 `kebab-case`。模块会先把它转成 PascalCase,再和组件导出名拼成最终组件名。
|
|
42
|
+
|
|
43
|
+
## 推荐目录结构
|
|
44
|
+
|
|
45
|
+
当前实现按“一级子目录 + index 导出”的约定工作。`library` 根目录下的每个一级子项都应该是目录,并且目录里要有 `index.ts` 或 `index.js`。
|
|
46
|
+
|
|
47
|
+
```txt
|
|
48
|
+
app/components/ui/
|
|
49
|
+
new-slideover/
|
|
50
|
+
index.ts
|
|
51
|
+
slideover.ts
|
|
52
|
+
cps-slideover.ts
|
|
53
|
+
new-scrollbar/
|
|
54
|
+
index.ts
|
|
55
|
+
scrollbar.ts
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`index.ts` 示例:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
export { cpsSlideover } from './cps-slideover'
|
|
62
|
+
export { Slideover } from './slideover'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
接入后:
|
|
66
|
+
|
|
67
|
+
- `Slideover` 会被注册为 `NewSlideover`
|
|
68
|
+
- `cpsSlideover` 会被注册为 auto import,可在 `script setup` 里直接调用
|
|
69
|
+
|
|
70
|
+
```vue
|
|
71
|
+
<script setup lang="ts">
|
|
72
|
+
const slideover = cpsSlideover()
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<template>
|
|
76
|
+
<NewSlideover />
|
|
77
|
+
</template>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## 识别规则
|
|
81
|
+
|
|
82
|
+
`nuxt-imc` 只解析 `index.ts` 或 `index.js` 的具名导出:
|
|
83
|
+
|
|
84
|
+
- 导出名以大写字母开头:按组件处理,调用 `addComponent`
|
|
85
|
+
- 导出名以当前 library 的 `composablePrefix` 开头:按 auto-import 处理,默认前缀是 `cps`
|
|
86
|
+
- 其它导出名:忽略
|
|
87
|
+
- `default export`: 不会被处理
|
|
88
|
+
|
|
89
|
+
## 原理
|
|
90
|
+
|
|
91
|
+
`nuxt-imc` 的工作流程分成两步。
|
|
92
|
+
|
|
93
|
+
### 1. 解析 library 目录
|
|
94
|
+
|
|
95
|
+
模块会先把每个配置项解析成绝对路径,然后读取它下面的一级子目录,作为后续要处理的组件目录列表。
|
|
96
|
+
|
|
97
|
+
如果某个 `directory` 不存在、不是目录,或者当前进程没有读取权限,它会输出一条 `warn` 并跳过,而不是直接抛错中断整个模块初始化。
|
|
98
|
+
|
|
99
|
+
### 2. 关闭默认扫描,改为按导出注册
|
|
100
|
+
|
|
101
|
+
模块会在 `components:dirs` hook 里把 library 目录和 `ignore: true` 目录插入到 Nuxt 的组件目录列表前面,但故意把 `extensions` 设成 `['.xx']`。这样 Nuxt 不会去按默认规则扫描这些目录里的真实组件文件。
|
|
102
|
+
|
|
103
|
+
接着模块会自己完成注册流程:
|
|
104
|
+
|
|
105
|
+
1. 解析每个 `library` 根目录
|
|
106
|
+
2. 读取根目录下的一级子目录
|
|
107
|
+
3. 跳过根目录下的普通文件
|
|
108
|
+
4. 读取每个子目录里的 `index.ts` 或 `index.js`
|
|
109
|
+
5. 用 `oxc-parser` 解析 AST,而不是用正则匹配源码文本
|
|
110
|
+
6. 遍历具名导出
|
|
111
|
+
7. 根据导出名决定调用 `addComponent` 还是 `addImports`
|
|
112
|
+
|
|
113
|
+
这样做的结果是:
|
|
114
|
+
|
|
115
|
+
- 对外暴露什么组件、什么 composable,完全由 `index.ts/js` 决定
|
|
116
|
+
- 可以在一个组件目录里同时维护组件和它的配套 composable
|
|
117
|
+
- 可以通过 `prefix` 统一加命名空间,减少冲突
|
|
118
|
+
|
|
119
|
+
## 当前实现的限制
|
|
120
|
+
|
|
121
|
+
使用时要特别注意下面几个约束:
|
|
122
|
+
|
|
123
|
+
- `library` 根目录下的普通文件会被忽略,不会参与注册。
|
|
124
|
+
- 配置了 `ignore: true` 的目录会被忽略,不会参与 Nuxt 组件扫描或 IMC 注册。
|
|
125
|
+
- 只扫描一级子目录,不会递归扫描更深层级。
|
|
126
|
+
- 每个组件目录都必须有 `index.ts` 或 `index.js`;空目录或缺少入口文件的目录仍然会报错。
|
|
127
|
+
- 只识别具名导出,不识别 `default export`。
|
|
128
|
+
- `prefix` 必须是 `kebab-case`;不合法的值会直接抛错。
|
|
129
|
+
- 无效的 `directory` 会发出 `warn` 并跳过,不会中断模块初始化。
|
|
130
|
+
|
|
131
|
+
## 本地开发
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pnpm install
|
|
135
|
+
pnpm dev:prepare
|
|
136
|
+
pnpm dev
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
常用检查:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
pnpm lint
|
|
143
|
+
pnpm test
|
|
144
|
+
pnpm test:types
|
|
145
|
+
pnpm prepack
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`playground/` 用于交互开发,`test/fixtures/basic/` 用于真实 Nuxt SSR 集成测试。
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
[MIT](./LICENSE)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
|
|
3
|
+
interface ComponentLibrary {
|
|
4
|
+
composablePrefix?: string;
|
|
5
|
+
prefix?: string;
|
|
6
|
+
/** base nuxt root */
|
|
7
|
+
directory: string;
|
|
8
|
+
}
|
|
9
|
+
interface IgnoredComponentLibraryDirectory {
|
|
10
|
+
/** base nuxt root */
|
|
11
|
+
directory: string;
|
|
12
|
+
ignore: boolean;
|
|
13
|
+
}
|
|
14
|
+
type ComponentLibraryConfig = ComponentLibrary | IgnoredComponentLibraryDirectory;
|
|
15
|
+
interface ModuleOptions {
|
|
16
|
+
libraries: ComponentLibraryConfig[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
20
|
+
|
|
21
|
+
export { _default as default };
|
|
22
|
+
export type { ComponentLibrary, ComponentLibraryConfig, IgnoredComponentLibraryDirectory, ModuleOptions };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { addComponent, addImports, defineNuxtModule, createResolver } from '@nuxt/kit';
|
|
2
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { parseSync } from 'oxc-parser';
|
|
5
|
+
|
|
6
|
+
const COMPONENT_EXPORT_NAME_RE = /^[A-Z]/;
|
|
7
|
+
const KEBAB_CASE_PREFIX_RE = /^[a-z]+(?:-[a-z0-9]+)*$/;
|
|
8
|
+
function toPascalCasePrefix(prefix) {
|
|
9
|
+
return prefix.split("-").map((part) => `${part[0]?.toUpperCase() || ""}${part.slice(1)}`).join("");
|
|
10
|
+
}
|
|
11
|
+
function validateComponentPrefix(prefix) {
|
|
12
|
+
if (!prefix || KEBAB_CASE_PREFIX_RE.test(prefix)) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
throw new Error(`[imc] Component prefix "${prefix}" must be kebab-case`);
|
|
16
|
+
}
|
|
17
|
+
function isIgnoredComponentLibraryDirectory(lib) {
|
|
18
|
+
return "ignore" in lib && lib.ignore;
|
|
19
|
+
}
|
|
20
|
+
function isComponentLibrary(lib) {
|
|
21
|
+
return !isIgnoredComponentLibraryDirectory(lib);
|
|
22
|
+
}
|
|
23
|
+
function getModuleExportName(exported) {
|
|
24
|
+
return exported.type === "Literal" ? exported.value : exported.name;
|
|
25
|
+
}
|
|
26
|
+
function obtainLibDirs(resolve, warn = (message) => console.warn(message)) {
|
|
27
|
+
return async (libConfigs) => {
|
|
28
|
+
const ignoredDirs = new Set(
|
|
29
|
+
libConfigs.filter(isIgnoredComponentLibraryDirectory).map((lib) => resolve(lib.directory))
|
|
30
|
+
);
|
|
31
|
+
const resolvedDirs = await Promise.all(libConfigs.filter(isComponentLibrary).map(async (lib) => {
|
|
32
|
+
validateComponentPrefix(lib.prefix || "");
|
|
33
|
+
const libDir = resolve(lib.directory);
|
|
34
|
+
try {
|
|
35
|
+
const entries = await readdir(libDir, { withFileTypes: true });
|
|
36
|
+
return {
|
|
37
|
+
componentDirs: entries.filter((entry) => entry.isDirectory()).filter((entry) => !ignoredDirs.has(resolve(lib.directory, entry.name))).map((entry) => entry.name).sort(),
|
|
38
|
+
composablePrefix: lib.composablePrefix || "cps",
|
|
39
|
+
fullPath: libDir,
|
|
40
|
+
prefix: lib.prefix || ""
|
|
41
|
+
};
|
|
42
|
+
} catch {
|
|
43
|
+
warn(`[imc] Ignoring invalid library directory "${lib.directory}"`);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}));
|
|
47
|
+
return resolvedDirs.filter((dir) => dir !== null);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function obtainIgnoredDirs(resolve) {
|
|
51
|
+
return (libConfigs) => {
|
|
52
|
+
return libConfigs.filter(isIgnoredComponentLibraryDirectory).map((lib) => resolve(lib.directory));
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function appendComponents(resolve, resolvePath) {
|
|
56
|
+
return async (libDir, compDir, prefix, composablePrefix = "cps") => {
|
|
57
|
+
const indexFilePath = await resolvePath(join(libDir, compDir, "index"), { extensions: [".ts", ".js"] });
|
|
58
|
+
const content = await readFile(indexFilePath, { encoding: "utf8" });
|
|
59
|
+
const ast = parseSync(indexFilePath, content, { sourceType: "module" });
|
|
60
|
+
const componentPrefix = prefix ? toPascalCasePrefix(prefix) : "";
|
|
61
|
+
ast.program.body.filter((node) => node.type === "ExportNamedDeclaration").filter((node) => node.exportKind !== "type").flatMap((node) => node.specifiers.filter((specifier) => specifier.exportKind !== "type").map((specifier) => getModuleExportName(specifier.exported))).forEach((key) => {
|
|
62
|
+
if (COMPONENT_EXPORT_NAME_RE.test(key)) {
|
|
63
|
+
addComponent({
|
|
64
|
+
name: `${componentPrefix}${key}`,
|
|
65
|
+
export: key,
|
|
66
|
+
filePath: resolve(indexFilePath),
|
|
67
|
+
priority: 1
|
|
68
|
+
});
|
|
69
|
+
} else if (key.startsWith(composablePrefix)) {
|
|
70
|
+
addImports({
|
|
71
|
+
name: key,
|
|
72
|
+
from: resolve(indexFilePath),
|
|
73
|
+
priority: 1
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const module$1 = defineNuxtModule({
|
|
81
|
+
meta: {
|
|
82
|
+
name: "nuxt-imc",
|
|
83
|
+
configKey: "imc",
|
|
84
|
+
compatibility: {
|
|
85
|
+
nuxt: ">=3.0.0"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
defaults: {
|
|
89
|
+
libraries: []
|
|
90
|
+
},
|
|
91
|
+
async setup(moduleOptions, nuxt) {
|
|
92
|
+
const { resolve, resolvePath } = createResolver(nuxt.options.rootDir);
|
|
93
|
+
if (moduleOptions.libraries.length === 0) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const libDirs = await obtainLibDirs(resolve)(moduleOptions.libraries);
|
|
97
|
+
const ignoredDirs = obtainIgnoredDirs(resolve)(moduleOptions.libraries);
|
|
98
|
+
nuxt.hook("components:dirs", (dirs) => {
|
|
99
|
+
dirs.unshift(
|
|
100
|
+
...ignoredDirs.map((path) => ({
|
|
101
|
+
path,
|
|
102
|
+
extensions: [".xx"],
|
|
103
|
+
priority: 1
|
|
104
|
+
})),
|
|
105
|
+
...libDirs.map((dir) => ({
|
|
106
|
+
path: dir.fullPath,
|
|
107
|
+
extensions: [".xx"],
|
|
108
|
+
priority: 1
|
|
109
|
+
}))
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
const appendLibraryComponents = appendComponents(resolve, resolvePath);
|
|
113
|
+
await Promise.all(libDirs.flatMap(
|
|
114
|
+
(libDir) => libDir.componentDirs.map((compDir) => appendLibraryComponents(
|
|
115
|
+
libDir.fullPath,
|
|
116
|
+
compDir,
|
|
117
|
+
libDir.prefix,
|
|
118
|
+
libDir.composablePrefix
|
|
119
|
+
))
|
|
120
|
+
));
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
export { module$1 as default };
|
package/dist/types.d.mts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nuxt-imc",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Register named component-library exports as Nuxt components and auto-imported composables.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "github:hzgotb/nuxt-imc"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/hzgotb/nuxt-imc#readme",
|
|
10
|
+
"bugs": "https://github.com/hzgotb/nuxt-imc/issues",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/types.d.mts",
|
|
16
|
+
"import": "./dist/module.mjs"
|
|
17
|
+
},
|
|
18
|
+
"./types": {
|
|
19
|
+
"types": "./dist/types.d.mts",
|
|
20
|
+
"import": "./dist/module.mjs"
|
|
21
|
+
},
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"main": "./dist/module.mjs",
|
|
25
|
+
"types": "./dist/types.d.mts",
|
|
26
|
+
"typesVersions": {
|
|
27
|
+
"*": {
|
|
28
|
+
".": [
|
|
29
|
+
"./dist/types.d.mts"
|
|
30
|
+
],
|
|
31
|
+
"types": [
|
|
32
|
+
"./dist/types.d.mts"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"sideEffects": false,
|
|
40
|
+
"workspaces": [
|
|
41
|
+
"playground"
|
|
42
|
+
],
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@nuxt/kit": "^4.5.0",
|
|
45
|
+
"oxc-parser": "^0.141.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@nuxt/devtools": "^3.2.4",
|
|
49
|
+
"@nuxt/eslint-config": "^1.16.0",
|
|
50
|
+
"@nuxt/module-builder": "^1.0.3",
|
|
51
|
+
"@nuxt/schema": "^4.5.0",
|
|
52
|
+
"@nuxt/test-utils": "^4.0.3",
|
|
53
|
+
"@types/node": "^24.0.0",
|
|
54
|
+
"changelogen": "^0.6.2",
|
|
55
|
+
"eslint": "^10.7.0",
|
|
56
|
+
"nuxt": "^4.5.0",
|
|
57
|
+
"typescript": "~5.9.3",
|
|
58
|
+
"vitest": "^4.1.10",
|
|
59
|
+
"vue-tsc": "^3.3.7"
|
|
60
|
+
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=20.19.0"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"dev": "pnpm dev:prepare && nuxt dev playground",
|
|
66
|
+
"dev:build": "nuxt build playground",
|
|
67
|
+
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
|
|
68
|
+
"lint": "eslint .",
|
|
69
|
+
"test": "vitest run",
|
|
70
|
+
"test:peers": "pnpm peers check",
|
|
71
|
+
"test:unit": "vitest run test/helper.test.ts",
|
|
72
|
+
"test:watch": "vitest watch",
|
|
73
|
+
"test:types": "vue-tsc --noEmit && pnpm --dir playground exec vue-tsc --noEmit",
|
|
74
|
+
"release": "pnpm lint && pnpm test && pnpm test:types && pnpm prepack && changelogen --release && pnpm publish && git push --follow-tags"
|
|
75
|
+
}
|
|
76
|
+
}
|