@morev/bem-vue 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.md +14 -0
- package/README.md +172 -0
- package/dist/client.d.ts +22 -0
- package/dist/compiler-xDNnc6-k.js +466 -0
- package/dist/compiler.d.ts +37 -0
- package/dist/compiler.js +2 -0
- package/dist/types-BHwq8e5H.d.ts +79 -0
- package/dist/unplugin-DAY_Kbnw.js +24 -0
- package/dist/virtual-client.d.ts +3 -0
- package/dist/vite.d.ts +13 -0
- package/dist/vite.js +78 -0
- package/dist/webpack.d.ts +16 -0
- package/dist/webpack.js +110 -0
- package/package.json +94 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright © 2026+ [Maxim Morev](https://github.com/MorevM)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
|
|
6
|
+
to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
7
|
+
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
8
|
+
|
|
9
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
10
|
+
|
|
11
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
12
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
|
|
13
|
+
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
|
14
|
+
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# @morev/bem-vue
|
|
2
|
+
|
|
3
|
+
Compile-time BEM directive and helpers for Vue, powered by
|
|
4
|
+
[`@morev/bem-classnames`](../bem-classnames).
|
|
5
|
+
|
|
6
|
+
The adapters replace `v-bem` with an ordinary `:class` expression while Vue
|
|
7
|
+
compiles the template. There is no runtime directive or global mixin to
|
|
8
|
+
register, and components that do not use `v-bem` or `useBem` do not load the
|
|
9
|
+
helper runtime.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
pnpm add @morev/bem-vue
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Add the client declarations to `tsconfig.json` so Vue and `vue-tsc` recognize
|
|
18
|
+
the directive and the `block` component option:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"compilerOptions": {
|
|
23
|
+
"types": ["@morev/bem-vue/client"]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Vite
|
|
29
|
+
|
|
30
|
+
Place the BEM adapter after the Vue plugin:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import vue from '@vitejs/plugin-vue';
|
|
34
|
+
import bemVue from '@morev/bem-vue/vite';
|
|
35
|
+
import { defineConfig } from 'vite';
|
|
36
|
+
|
|
37
|
+
export default defineConfig({
|
|
38
|
+
plugins: [vue(), bemVue()],
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The adapter supports Vue 3.5 VDOM compilation and Vue 3.6 Vapor through the
|
|
43
|
+
Vue plugin's Vapor mode:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
vue({
|
|
47
|
+
features: { vapor: true },
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Vapor support includes client builds and SSR. A Vapor SFC may use a string
|
|
52
|
+
literal or a top-level/imported identifier for `defineOptions({ block, name })`;
|
|
53
|
+
expressions with repeated evaluation or side effects are rejected.
|
|
54
|
+
|
|
55
|
+
## Webpack
|
|
56
|
+
|
|
57
|
+
Place `bemVue()` before `VueLoaderPlugin`. The adapter must update the static
|
|
58
|
+
`vue-loader` rule before `VueLoaderPlugin` normalizes it:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { VueLoaderPlugin } from 'vue-loader';
|
|
62
|
+
import bemVue from '@morev/bem-vue/webpack';
|
|
63
|
+
|
|
64
|
+
export default {
|
|
65
|
+
module: {
|
|
66
|
+
rules: [
|
|
67
|
+
{
|
|
68
|
+
test: /\.vue$/u,
|
|
69
|
+
loader: 'vue-loader',
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
plugins: [bemVue(), new VueLoaderPlugin()],
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Webpack supports the Vue 3 VDOM pipeline. Vapor is intentionally limited to
|
|
78
|
+
Vite because `vue-loader` does not expose the corresponding compiler pipeline.
|
|
79
|
+
|
|
80
|
+
## Directive syntax
|
|
81
|
+
|
|
82
|
+
Use `v-bem` for the block and an argument for an element:
|
|
83
|
+
|
|
84
|
+
```vue
|
|
85
|
+
<script setup lang="ts">
|
|
86
|
+
const active = true;
|
|
87
|
+
const element = 'title';
|
|
88
|
+
const mix = 'layout__item';
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<template>
|
|
92
|
+
<article v-bem>
|
|
93
|
+
<h2 v-bem:title="{ active }">Title</h2>
|
|
94
|
+
<div v-bem:[element]="[{ active }, mix, 'static-mix']" />
|
|
95
|
+
</article>
|
|
96
|
+
</template>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Values follow the recursive `BemArgument` contract: modifier objects, mixin
|
|
100
|
+
strings, nullish values, or nested arrays of those values. A nullish dynamic
|
|
101
|
+
element means the block itself.
|
|
102
|
+
|
|
103
|
+
Directive modifiers are not supported. Replace the legacy modifier syntax with
|
|
104
|
+
ordinary values:
|
|
105
|
+
|
|
106
|
+
```vue
|
|
107
|
+
<!-- Invalid -->
|
|
108
|
+
<div v-bem.static.[mix] />
|
|
109
|
+
|
|
110
|
+
<!-- Valid -->
|
|
111
|
+
<div v-bem="['static', mix]" />
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The transform keeps the directive's original position among `class`, `:class`,
|
|
115
|
+
and `v-bind` props, leaving Vue responsible for its normal class merging and
|
|
116
|
+
component fallthrough behavior.
|
|
117
|
+
|
|
118
|
+
## Block names and `useBem`
|
|
119
|
+
|
|
120
|
+
The default block is the `.vue` filename without its extension. Use the fixed
|
|
121
|
+
`block` component option when the Vue component name and BEM block differ:
|
|
122
|
+
|
|
123
|
+
```vue
|
|
124
|
+
<script setup lang="ts">
|
|
125
|
+
import { useBem } from 'virtual:@morev/bem-vue';
|
|
126
|
+
|
|
127
|
+
defineOptions({
|
|
128
|
+
name: 'the-component-item',
|
|
129
|
+
block: 'the-component',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const b = useBem();
|
|
133
|
+
const foreign = useBem('foreign-block');
|
|
134
|
+
</script>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The lookup order is an explicit `useBem(name)` argument, `block`, the explicit
|
|
138
|
+
Vue component name, the filename resolver, and Vue's generated `__name` as a
|
|
139
|
+
last fallback. An explicit name works outside a component. Calling `useBem()`
|
|
140
|
+
without any available name throws a descriptive runtime error.
|
|
141
|
+
|
|
142
|
+
Calls imported directly into an SFC receive the filename fallback at compile
|
|
143
|
+
time. A call in an external composable should pass a block explicitly when it
|
|
144
|
+
cannot rely on the current component's `block` or `name`.
|
|
145
|
+
|
|
146
|
+
## Options
|
|
147
|
+
|
|
148
|
+
Both adapters accept the same options:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
bemVue({
|
|
152
|
+
factoryOptions: {
|
|
153
|
+
namespace: 'ui-',
|
|
154
|
+
},
|
|
155
|
+
resolveBlockName: ({ filename, root }) => resolveProjectName({ filename, root }),
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`factoryOptions` is passed to `bemClassnames()` and must be safely serializable
|
|
160
|
+
as JSON. `resolveBlockName` changes the project-wide filename fallback.
|
|
161
|
+
|
|
162
|
+
For custom integrations, `@morev/bem-vue/compiler` exports the stateful compiler
|
|
163
|
+
core with SFC transform, Vue template transform, and virtual-module hooks.
|
|
164
|
+
|
|
165
|
+
## Boundaries
|
|
166
|
+
|
|
167
|
+
- Vue 3 only; there is no Vue 2 or Nuxt-specific module.
|
|
168
|
+
- VDOM works with Vite and Webpack 5; Vapor works with Vite.
|
|
169
|
+
- Rollup and esbuild adapters are not provided because they do not own a Vue
|
|
170
|
+
template compiler pipeline.
|
|
171
|
+
- `v-bem` is rejected on `<template>` and `<slot>`, and an element may contain
|
|
172
|
+
only one `v-bem` directive.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import './virtual-client.js';
|
|
2
|
+
|
|
3
|
+
declare module 'vue' {
|
|
4
|
+
interface ComponentCustomOptions {
|
|
5
|
+
/**
|
|
6
|
+
* BEM block name independent from the Vue component name.
|
|
7
|
+
*/
|
|
8
|
+
block?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface GlobalDirectives {
|
|
12
|
+
/**
|
|
13
|
+
* Compile-time BEM class directive.
|
|
14
|
+
*/
|
|
15
|
+
vBem: import('vue').Directive<
|
|
16
|
+
Element,
|
|
17
|
+
import('@morev/bem-vue/compiler').BemDirectiveValue | true,
|
|
18
|
+
'__bem_modifiers_are_not_supported__',
|
|
19
|
+
string | undefined
|
|
20
|
+
>;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import { basename, extname, isAbsolute, normalize, resolve } from "node:path";
|
|
2
|
+
import { parse } from "@vue/compiler-sfc";
|
|
3
|
+
import { parse as parse$1, parseExpression } from "@babel/parser";
|
|
4
|
+
import traverseModule from "@babel/traverse";
|
|
5
|
+
import { NodeTypes, createCompoundExpression, createSimpleExpression } from "@vue/compiler-core";
|
|
6
|
+
import { baseParse } from "@vue/compiler-dom";
|
|
7
|
+
import MagicString from "magic-string";
|
|
8
|
+
//#region src/diagnostics.ts
|
|
9
|
+
/**
|
|
10
|
+
* Compile-time error tied to a Vue template or script location.
|
|
11
|
+
*/
|
|
12
|
+
var BemVueTransformError = class extends Error {
|
|
13
|
+
/**
|
|
14
|
+
* Creates a diagnostic that bundlers can display without additional context.
|
|
15
|
+
*
|
|
16
|
+
* @param filename Component filename.
|
|
17
|
+
* @param location Vue compiler source location.
|
|
18
|
+
* @param message Concise explanation of the invalid syntax.
|
|
19
|
+
* @param hint Optional migration or recovery hint.
|
|
20
|
+
*/
|
|
21
|
+
constructor(filename, location, message, hint) {
|
|
22
|
+
const diagnostic = `${filename}:${location.start.line}:${location.start.column}: ${message}`;
|
|
23
|
+
super(hint ? `${diagnostic}\n${hint}` : diagnostic);
|
|
24
|
+
this.name = "BemVueTransformError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/virtual-module.ts
|
|
29
|
+
const VIRTUAL_MODULE_ID = "virtual:@morev/bem-vue";
|
|
30
|
+
const WEBPACK_VIRTUAL_MODULE_ID = "@morev/bem-vue/__virtual__";
|
|
31
|
+
const RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`;
|
|
32
|
+
const resolveBemVueOptions = (options) => ({
|
|
33
|
+
...options.factoryOptions !== void 0 && { factoryOptions: options.factoryOptions },
|
|
34
|
+
root: options.root,
|
|
35
|
+
resolveBlockName: options.resolveBlockName ?? (({ filename }) => basename(filename, extname(filename)))
|
|
36
|
+
});
|
|
37
|
+
const serializeFactoryOptions = (factoryOptions) => {
|
|
38
|
+
try {
|
|
39
|
+
const serialized = JSON.stringify(factoryOptions, (_key, value) => {
|
|
40
|
+
if (typeof value === "number" && !Number.isFinite(value)) throw new TypeError("non-finite numbers are not supported");
|
|
41
|
+
if ([
|
|
42
|
+
"bigint",
|
|
43
|
+
"function",
|
|
44
|
+
"symbol",
|
|
45
|
+
"undefined"
|
|
46
|
+
].includes(typeof value)) throw new TypeError(`${typeof value} values are not supported`);
|
|
47
|
+
return value;
|
|
48
|
+
});
|
|
49
|
+
if (!serialized) throw new TypeError("the value cannot be represented as JSON");
|
|
50
|
+
return serialized.replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
53
|
+
throw new TypeError(`@morev/bem-vue: factoryOptions cannot be serialized: ${reason}.`, { cause: error });
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const createVirtualModule = (options) => {
|
|
57
|
+
return `import { bemClassnames } from '@morev/bem-classnames';
|
|
58
|
+
import { getCurrentInstance } from 'vue';
|
|
59
|
+
|
|
60
|
+
const __bemVueFactory = bemClassnames(${options.factoryOptions === void 0 ? "" : serializeFactoryOptions(options.factoryOptions)});
|
|
61
|
+
const __bemVueCache = new Map();
|
|
62
|
+
|
|
63
|
+
const __bemVueGetBlock = (name, fallback) => {
|
|
64
|
+
\tif (name !== undefined) return name;
|
|
65
|
+
|
|
66
|
+
\tconst instance = getCurrentInstance();
|
|
67
|
+
\tconst component = instance?.type;
|
|
68
|
+
\tconst block = component && typeof component === 'object' ? component.block : undefined;
|
|
69
|
+
\tif (typeof block === 'string') return block;
|
|
70
|
+
|
|
71
|
+
\tconst componentName = component && (typeof component === 'object' || typeof component === 'function')
|
|
72
|
+
\t\t? component.name
|
|
73
|
+
\t\t: undefined;
|
|
74
|
+
\tif (typeof componentName === 'string' && componentName) return componentName;
|
|
75
|
+
\tif (fallback !== undefined) return fallback;
|
|
76
|
+
|
|
77
|
+
\tconst generatedName = component && typeof component === 'object' ? component.__name : undefined;
|
|
78
|
+
\tif (typeof generatedName === 'string' && generatedName) return generatedName;
|
|
79
|
+
|
|
80
|
+
\tthrow new Error('@morev/bem-vue: useBem() cannot resolve a block outside a named Vue component. Pass a block name explicitly.');
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const __bemVueGetHelper = (block) => {
|
|
84
|
+
\tlet helper = __bemVueCache.get(block);
|
|
85
|
+
\tif (!helper) {
|
|
86
|
+
\t\thelper = __bemVueFactory(block);
|
|
87
|
+
\t\t__bemVueCache.set(block, helper);
|
|
88
|
+
\t}
|
|
89
|
+
\treturn helper;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const useBem = (name, __filenameFallback) => __bemVueGetHelper(__bemVueGetBlock(name, __filenameFallback));
|
|
93
|
+
const __bemVueClass = (element, value, fallback, staticBlock) => (
|
|
94
|
+
\t__bemVueGetHelper(staticBlock ?? __bemVueGetBlock(undefined, fallback))(element, value)
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
export { __bemVueClass, useBem };
|
|
98
|
+
`;
|
|
99
|
+
};
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/compiler.ts
|
|
102
|
+
const traverse = typeof traverseModule === "function" ? traverseModule : traverseModule.default;
|
|
103
|
+
const VAPOR_HELPER_IDENTIFIER = "__bemVueClass";
|
|
104
|
+
const VAPOR_BLOCK_DIRECTIVE = "bem-block";
|
|
105
|
+
const VAPOR_HELPER_DIRECTIVE = "bem-helper";
|
|
106
|
+
const getNormalizedFilename = (filename, root) => {
|
|
107
|
+
const cleanFilename = filename.split("?", 1)[0];
|
|
108
|
+
return normalize(isAbsolute(cleanFilename) ? cleanFilename : resolve(root, cleanFilename));
|
|
109
|
+
};
|
|
110
|
+
const getSourceLocation = (source, start, end = start) => {
|
|
111
|
+
let line = 1;
|
|
112
|
+
let column = 1;
|
|
113
|
+
for (let index = 0; index < start; index += 1) if (source[index] === "\n") {
|
|
114
|
+
line += 1;
|
|
115
|
+
column = 1;
|
|
116
|
+
} else column += 1;
|
|
117
|
+
return {
|
|
118
|
+
start: {
|
|
119
|
+
column,
|
|
120
|
+
line,
|
|
121
|
+
offset: start
|
|
122
|
+
},
|
|
123
|
+
end: {
|
|
124
|
+
column: column + Math.max(0, end - start),
|
|
125
|
+
line,
|
|
126
|
+
offset: end
|
|
127
|
+
},
|
|
128
|
+
source: source.slice(start, end)
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
const getParserPlugins = (block) => {
|
|
132
|
+
const language = block.lang?.toLowerCase();
|
|
133
|
+
const plugins = [];
|
|
134
|
+
if (language === "ts" || language === "tsx") plugins.push("typescript");
|
|
135
|
+
if (language === "jsx" || language === "tsx") plugins.push("jsx");
|
|
136
|
+
return plugins;
|
|
137
|
+
};
|
|
138
|
+
const analyzeScript = (block, filename) => ({
|
|
139
|
+
ast: parse$1(block.content, {
|
|
140
|
+
plugins: getParserPlugins(block),
|
|
141
|
+
sourceFilename: filename,
|
|
142
|
+
sourceType: "module"
|
|
143
|
+
}),
|
|
144
|
+
block,
|
|
145
|
+
contentOffset: block.loc.start.offset
|
|
146
|
+
});
|
|
147
|
+
const visitTemplate = (node, visitor) => {
|
|
148
|
+
if (node.type === NodeTypes.ELEMENT) visitor(node);
|
|
149
|
+
if (node.type === NodeTypes.ROOT || node.type === NodeTypes.ELEMENT) node.children.forEach((child) => visitTemplate(child, visitor));
|
|
150
|
+
};
|
|
151
|
+
const getBemDirectiveOffsets = (descriptor) => {
|
|
152
|
+
if (!descriptor.template) return [];
|
|
153
|
+
const template = baseParse(descriptor.template.content);
|
|
154
|
+
const offsets = [];
|
|
155
|
+
visitTemplate(template, (element) => {
|
|
156
|
+
const directive = element.props.find((prop) => prop.type === NodeTypes.DIRECTIVE && prop.name === "bem");
|
|
157
|
+
if (directive) offsets.push(directive.loc.end.offset);
|
|
158
|
+
});
|
|
159
|
+
return offsets;
|
|
160
|
+
};
|
|
161
|
+
const collectIdentifiers = (analyses) => {
|
|
162
|
+
const identifiers = /* @__PURE__ */ new Set();
|
|
163
|
+
analyses.forEach(({ ast }) => {
|
|
164
|
+
traverse(ast, { Identifier(path) {
|
|
165
|
+
identifiers.add(path.node.name);
|
|
166
|
+
} });
|
|
167
|
+
});
|
|
168
|
+
return identifiers;
|
|
169
|
+
};
|
|
170
|
+
const getAvailableIdentifier = (base, identifiers) => {
|
|
171
|
+
if (!identifiers.has(base)) return base;
|
|
172
|
+
let suffix = 2;
|
|
173
|
+
while (identifiers.has(`${base}${suffix}`)) suffix += 1;
|
|
174
|
+
return `${base}${suffix}`;
|
|
175
|
+
};
|
|
176
|
+
const getPropertyName = (property) => {
|
|
177
|
+
if (property.computed) return void 0;
|
|
178
|
+
if (property.key.type === "Identifier") return property.key.name;
|
|
179
|
+
if (property.key.type === "StringLiteral") return property.key.value;
|
|
180
|
+
};
|
|
181
|
+
const getStaticOption = (propertyPath, source, filename, contentOffset) => {
|
|
182
|
+
const valuePath = propertyPath.get("value");
|
|
183
|
+
if (valuePath.isStringLiteral()) return { expression: JSON.stringify(valuePath.node.value) };
|
|
184
|
+
if (valuePath.isIdentifier()) {
|
|
185
|
+
if (valuePath.scope.getBinding(valuePath.node.name)?.scope.path.isProgram()) return { expression: valuePath.node.name };
|
|
186
|
+
}
|
|
187
|
+
const start = contentOffset + (valuePath.node.start ?? propertyPath.node.start ?? 0);
|
|
188
|
+
const end = contentOffset + (valuePath.node.end ?? propertyPath.node.end ?? start);
|
|
189
|
+
throw new BemVueTransformError(filename, getSourceLocation(source, start, end), "Vapor requires defineOptions({ block, name }) values to be string literals or top-level/imported identifiers.", "Reuse a stable binding instead of an expression such as getName().");
|
|
190
|
+
};
|
|
191
|
+
const analyzeVaporOptions = (analysis, source, filename) => {
|
|
192
|
+
if (!analysis) return {};
|
|
193
|
+
const options = {};
|
|
194
|
+
traverse(analysis.ast, { CallExpression(path) {
|
|
195
|
+
if (!path.get("callee").isIdentifier({ name: "defineOptions" })) return;
|
|
196
|
+
const argumentPath = path.get("arguments")[0];
|
|
197
|
+
if (!argumentPath?.isObjectExpression()) return;
|
|
198
|
+
argumentPath.get("properties").forEach((propertyPath) => {
|
|
199
|
+
if (!propertyPath.isObjectProperty()) return;
|
|
200
|
+
const propertyName = getPropertyName(propertyPath.node);
|
|
201
|
+
if (propertyName !== "block" && propertyName !== "name") return;
|
|
202
|
+
options[propertyName] = getStaticOption(propertyPath, source, filename, analysis.contentOffset);
|
|
203
|
+
});
|
|
204
|
+
} });
|
|
205
|
+
return options;
|
|
206
|
+
};
|
|
207
|
+
const getVaporBlockExpression = (options, fallback) => {
|
|
208
|
+
const expressions = [
|
|
209
|
+
options.block?.expression,
|
|
210
|
+
options.name?.expression,
|
|
211
|
+
JSON.stringify(fallback)
|
|
212
|
+
].filter((value) => value !== void 0);
|
|
213
|
+
return expressions.length === 1 ? expressions[0] : `(${expressions.join(" ?? ")})`;
|
|
214
|
+
};
|
|
215
|
+
const getUseBemBindings = (analysis) => {
|
|
216
|
+
const bindings = /* @__PURE__ */ new Set();
|
|
217
|
+
traverse(analysis.ast, { ImportDeclaration(path) {
|
|
218
|
+
if (path.node.source.value !== "virtual:@morev/bem-vue") return;
|
|
219
|
+
path.get("specifiers").forEach((specifierPath) => {
|
|
220
|
+
if (!specifierPath.isImportSpecifier()) return;
|
|
221
|
+
const { imported } = specifierPath.node;
|
|
222
|
+
if (imported.type === "Identifier" && imported.name === "useBem" || imported.type === "StringLiteral" && imported.value === "useBem") bindings.add(specifierPath.node);
|
|
223
|
+
});
|
|
224
|
+
} });
|
|
225
|
+
return bindings;
|
|
226
|
+
};
|
|
227
|
+
const isImportedUseBem = (path, bindings) => {
|
|
228
|
+
const calleePath = path.get("callee");
|
|
229
|
+
if (!calleePath.isIdentifier()) return false;
|
|
230
|
+
return bindings.has(calleePath.scope.getBinding(calleePath.node.name)?.path.node);
|
|
231
|
+
};
|
|
232
|
+
const hasUseBemFallbackEdits = (magicString, analysis, fallbackExpression) => {
|
|
233
|
+
const bindings = getUseBemBindings(analysis);
|
|
234
|
+
if (bindings.size === 0) return false;
|
|
235
|
+
let hasEdits = false;
|
|
236
|
+
const visitCall = (path) => {
|
|
237
|
+
if (path.node.arguments.length !== 0 || !isImportedUseBem(path, bindings)) return;
|
|
238
|
+
if (typeof path.node.end !== "number") return;
|
|
239
|
+
magicString.appendLeft(analysis.contentOffset + path.node.end - 1, `undefined, ${fallbackExpression}`);
|
|
240
|
+
hasEdits = true;
|
|
241
|
+
};
|
|
242
|
+
traverse(analysis.ast, {
|
|
243
|
+
CallExpression(path) {
|
|
244
|
+
visitCall(path);
|
|
245
|
+
},
|
|
246
|
+
OptionalCallExpression(path) {
|
|
247
|
+
visitCall(path);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
return hasEdits;
|
|
251
|
+
};
|
|
252
|
+
const addVaporHelperImport = (magicString, descriptor, helperIdentifier, importIdentifier) => {
|
|
253
|
+
const importStatement = [
|
|
254
|
+
`import { __bemVueClass as ${importIdentifier} } from '${VIRTUAL_MODULE_ID}';`,
|
|
255
|
+
`const ${helperIdentifier} = ${importIdentifier};`,
|
|
256
|
+
""
|
|
257
|
+
].join("\n");
|
|
258
|
+
if (descriptor.scriptSetup) {
|
|
259
|
+
magicString.appendLeft(descriptor.scriptSetup.loc.start.offset, importStatement);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
magicString.prepend(`<script setup>\n${importStatement}<\/script>\n\n`);
|
|
263
|
+
};
|
|
264
|
+
const getCompilerHelperIdentifier = (context) => {
|
|
265
|
+
const base = "__bemVueClass";
|
|
266
|
+
const isUnavailable = (identifier) => identifier in context.bindingMetadata || Boolean(context.identifiers[identifier]);
|
|
267
|
+
let identifier = base;
|
|
268
|
+
let suffix = 2;
|
|
269
|
+
while (isUnavailable(identifier)) {
|
|
270
|
+
identifier = `${base}${suffix}`;
|
|
271
|
+
suffix += 1;
|
|
272
|
+
}
|
|
273
|
+
context.imports.push({
|
|
274
|
+
exp: `{ __bemVueClass as ${identifier} }`,
|
|
275
|
+
path: VIRTUAL_MODULE_ID
|
|
276
|
+
});
|
|
277
|
+
return identifier;
|
|
278
|
+
};
|
|
279
|
+
const getBemDirectives = (node) => node.props.filter((prop) => prop.type === NodeTypes.DIRECTIVE && prop.name === "bem");
|
|
280
|
+
const getInternalDirective = (node, name) => node.props.find((prop) => prop.type === NodeTypes.DIRECTIVE && prop.name === name);
|
|
281
|
+
const expressionToString = (expression) => {
|
|
282
|
+
if (typeof expression === "string") return expression;
|
|
283
|
+
if (expression.type === NodeTypes.SIMPLE_EXPRESSION) return expression.content;
|
|
284
|
+
return expression.children.map((child) => {
|
|
285
|
+
if (typeof child === "string") return child;
|
|
286
|
+
if (typeof child === "symbol") throw new TypeError("@morev/bem-vue: unexpected helper symbol in Vapor expression.");
|
|
287
|
+
if (child.type === NodeTypes.INTERPOLATION) return expressionToString(child.content);
|
|
288
|
+
if (child.type === NodeTypes.TEXT) return child.content;
|
|
289
|
+
return expressionToString(child);
|
|
290
|
+
}).join("");
|
|
291
|
+
};
|
|
292
|
+
const getSfcLocation = (location, templateStart) => {
|
|
293
|
+
if (!templateStart) return location;
|
|
294
|
+
const resolvePosition = (position) => ({
|
|
295
|
+
column: position.line === 1 ? templateStart.column + position.column - 1 : position.column,
|
|
296
|
+
line: templateStart.line + position.line - 1,
|
|
297
|
+
offset: templateStart.offset + position.offset
|
|
298
|
+
});
|
|
299
|
+
return {
|
|
300
|
+
...location,
|
|
301
|
+
end: resolvePosition(location.end),
|
|
302
|
+
start: resolvePosition(location.start)
|
|
303
|
+
};
|
|
304
|
+
};
|
|
305
|
+
const createClassExpression = (helper, directive, fallback, staticBlock, useVaporCompiler = false) => {
|
|
306
|
+
const argumentsList = [
|
|
307
|
+
helper,
|
|
308
|
+
"(",
|
|
309
|
+
directive.arg ? directive.arg.type === NodeTypes.SIMPLE_EXPRESSION && directive.arg.isStatic ? JSON.stringify(directive.arg.content) : directive.arg : "null",
|
|
310
|
+
", ",
|
|
311
|
+
directive.exp ?? "undefined",
|
|
312
|
+
", ",
|
|
313
|
+
JSON.stringify(fallback)
|
|
314
|
+
];
|
|
315
|
+
if (staticBlock) argumentsList.push(", ", staticBlock);
|
|
316
|
+
argumentsList.push(")");
|
|
317
|
+
if (useVaporCompiler) {
|
|
318
|
+
const expression = createSimpleExpression(argumentsList.map((argument) => expressionToString(argument)).join(""), false, directive.loc);
|
|
319
|
+
expression.ast = parseExpression(`(${expression.content})`, { plugins: ["typescript"] });
|
|
320
|
+
return expression;
|
|
321
|
+
}
|
|
322
|
+
return createCompoundExpression(argumentsList, directive.loc);
|
|
323
|
+
};
|
|
324
|
+
const createTemplateTransform = (root, metadata, metadataByToken, resolveBlockName) => {
|
|
325
|
+
const helperIdentifiers = /* @__PURE__ */ new WeakMap();
|
|
326
|
+
return (templateNode, context) => {
|
|
327
|
+
if (templateNode.type !== NodeTypes.ELEMENT) return;
|
|
328
|
+
const directives = getBemDirectives(templateNode);
|
|
329
|
+
if (directives.length === 0) return;
|
|
330
|
+
const vaporHelperDirective = getInternalDirective(templateNode, VAPOR_HELPER_DIRECTIVE);
|
|
331
|
+
const metadataToken = vaporHelperDirective?.arg?.type === NodeTypes.SIMPLE_EXPRESSION && vaporHelperDirective.arg.isStatic ? vaporHelperDirective.arg.content : void 0;
|
|
332
|
+
const tokenMetadata = metadataToken ? metadataByToken.get(metadataToken) : void 0;
|
|
333
|
+
const contextFilename = typeof context.filename === "string" && context.filename ? getNormalizedFilename(context.filename, root) : void 0;
|
|
334
|
+
const componentMetadata = contextFilename ? metadata.get(contextFilename) : tokenMetadata;
|
|
335
|
+
const filename = componentMetadata?.filename ?? contextFilename;
|
|
336
|
+
if (!filename) throw new Error("@morev/bem-vue: Vue compiler context did not expose an SFC filename.");
|
|
337
|
+
const directive = directives[0];
|
|
338
|
+
if (directives.length > 1) throw new BemVueTransformError(filename, getSfcLocation(directives[1].loc, componentMetadata?.templateStart), "An element can only have one v-bem directive.");
|
|
339
|
+
const directiveLocation = getSfcLocation(directive.loc, componentMetadata?.templateStart);
|
|
340
|
+
if (templateNode.tag === "template" || templateNode.tag === "slot") throw new BemVueTransformError(filename, directiveLocation, `v-bem cannot be used on <${templateNode.tag}>.`);
|
|
341
|
+
if (directive.modifiers.length > 0) throw new BemVueTransformError(filename, directiveLocation, "v-bem directive modifiers are not supported.", "Replace v-bem.static.[mix] with v-bem=\"['static', mix]\".");
|
|
342
|
+
const fallback = componentMetadata?.blockName ?? resolveBlockName({
|
|
343
|
+
filename,
|
|
344
|
+
root
|
|
345
|
+
});
|
|
346
|
+
if (!fallback) throw new BemVueTransformError(filename, directiveLocation, "resolveBlockName() returned an empty block name.");
|
|
347
|
+
const isVapor = componentMetadata?.vapor ?? false;
|
|
348
|
+
const isVaporCompiler = isVapor && "options" in context;
|
|
349
|
+
const compilerContextKey = context.root;
|
|
350
|
+
const vaporHelper = isVapor ? vaporHelperDirective : void 0;
|
|
351
|
+
const vaporBlockDirective = isVapor ? getInternalDirective(templateNode, VAPOR_BLOCK_DIRECTIVE) : void 0;
|
|
352
|
+
const helper = isVapor ? componentMetadata?.helperIdentifier && (isVaporCompiler ? componentMetadata.helperIdentifier : `$setup.${componentMetadata.helperIdentifier}`) : helperIdentifiers.get(compilerContextKey) ?? getCompilerHelperIdentifier(context);
|
|
353
|
+
if (!helper) throw new BemVueTransformError(filename, directiveLocation, "Vapor helper metadata is unavailable. Run transformSfc() before compiling the template.");
|
|
354
|
+
if (typeof helper === "string") helperIdentifiers.set(compilerContextKey, helper);
|
|
355
|
+
if (vaporHelper) templateNode.props.splice(templateNode.props.indexOf(vaporHelper), 1);
|
|
356
|
+
if (vaporBlockDirective) templateNode.props.splice(templateNode.props.indexOf(vaporBlockDirective), 1);
|
|
357
|
+
const directiveIndex = templateNode.props.indexOf(directive);
|
|
358
|
+
templateNode.props[directiveIndex] = {
|
|
359
|
+
...directive,
|
|
360
|
+
arg: createSimpleExpression("class", true, directive.loc),
|
|
361
|
+
exp: createClassExpression(helper, directive, fallback, vaporBlockDirective?.exp ?? componentMetadata?.staticBlockExpression, isVaporCompiler),
|
|
362
|
+
modifiers: [],
|
|
363
|
+
name: "bind",
|
|
364
|
+
rawName: void 0
|
|
365
|
+
};
|
|
366
|
+
};
|
|
367
|
+
};
|
|
368
|
+
/**
|
|
369
|
+
* Creates the compiler core shared by Vite, Webpack, and future adapters.
|
|
370
|
+
*
|
|
371
|
+
* @param compilerOptions Project paths and public BEM options.
|
|
372
|
+
*
|
|
373
|
+
* @returns Stateful compiler with SFC, template, and virtual-module hooks.
|
|
374
|
+
*/
|
|
375
|
+
const createBemVueCompiler = (compilerOptions) => {
|
|
376
|
+
const options = resolveBemVueOptions({
|
|
377
|
+
...compilerOptions,
|
|
378
|
+
root: resolve(compilerOptions.root)
|
|
379
|
+
});
|
|
380
|
+
const metadata = /* @__PURE__ */ new Map();
|
|
381
|
+
const metadataByToken = /* @__PURE__ */ new Map();
|
|
382
|
+
let metadataTokenCounter = 0;
|
|
383
|
+
const virtualModuleSource = createVirtualModule(options);
|
|
384
|
+
const templateTransform = createTemplateTransform(options.root, metadata, metadataByToken, options.resolveBlockName);
|
|
385
|
+
return {
|
|
386
|
+
getTemplateCompilerOptions: () => ({ nodeTransforms: [templateTransform] }),
|
|
387
|
+
load: (id) => id === RESOLVED_VIRTUAL_MODULE_ID ? virtualModuleSource : null,
|
|
388
|
+
resolveId: (id) => id === "virtual:@morev/bem-vue" || id === "@morev/bem-vue/__virtual__" ? RESOLVED_VIRTUAL_MODULE_ID : null,
|
|
389
|
+
transformSfc(source, context) {
|
|
390
|
+
const filename = getNormalizedFilename(context.filename, options.root);
|
|
391
|
+
const previousMetadata = metadata.get(filename);
|
|
392
|
+
const { descriptor, errors } = parse(source, {
|
|
393
|
+
filename,
|
|
394
|
+
sourceMap: true
|
|
395
|
+
});
|
|
396
|
+
if (errors.length > 0) {
|
|
397
|
+
const error = errors[0];
|
|
398
|
+
throw error instanceof Error ? error : /* @__PURE__ */ new Error(`${filename}: ${String(error)}`);
|
|
399
|
+
}
|
|
400
|
+
const blockName = options.resolveBlockName({
|
|
401
|
+
filename,
|
|
402
|
+
root: options.root
|
|
403
|
+
});
|
|
404
|
+
if (!blockName) throw new TypeError(`@morev/bem-vue: resolveBlockName() returned an empty block for ${filename}.`);
|
|
405
|
+
const scriptBlocks = [];
|
|
406
|
+
if (descriptor.script) scriptBlocks.push(descriptor.script);
|
|
407
|
+
if (descriptor.scriptSetup) scriptBlocks.push(descriptor.scriptSetup);
|
|
408
|
+
const analyses = scriptBlocks.map((block) => analyzeScript(block, filename));
|
|
409
|
+
const magicString = new MagicString(source);
|
|
410
|
+
const bemDirectiveOffsets = getBemDirectiveOffsets(descriptor);
|
|
411
|
+
const hasDirective = bemDirectiveOffsets.length > 0;
|
|
412
|
+
let helperIdentifier;
|
|
413
|
+
let metadataToken;
|
|
414
|
+
const setupAnalysis = analyses.find(({ block }) => block === descriptor.scriptSetup);
|
|
415
|
+
const vaporOptions = context.vapor ? analyzeVaporOptions(setupAnalysis, source, filename) : {};
|
|
416
|
+
const staticBlockExpression = context.vapor ? getVaporBlockExpression(vaporOptions, blockName) : void 0;
|
|
417
|
+
const fallbackExpression = staticBlockExpression ?? JSON.stringify(blockName);
|
|
418
|
+
let hasEdits = analyses.some((analysis) => hasUseBemFallbackEdits(magicString, analysis, fallbackExpression));
|
|
419
|
+
if (context.vapor && hasDirective) {
|
|
420
|
+
const identifiers = collectIdentifiers(analyses);
|
|
421
|
+
const currentHelperIdentifier = getAvailableIdentifier(VAPOR_HELPER_IDENTIFIER, identifiers);
|
|
422
|
+
helperIdentifier = currentHelperIdentifier;
|
|
423
|
+
identifiers.add(currentHelperIdentifier);
|
|
424
|
+
const importIdentifier = getAvailableIdentifier(`${VAPOR_HELPER_IDENTIFIER}Import`, identifiers);
|
|
425
|
+
addVaporHelperImport(magicString, descriptor, currentHelperIdentifier, importIdentifier);
|
|
426
|
+
let currentMetadataToken;
|
|
427
|
+
if (previousMetadata?.token) currentMetadataToken = previousMetadata.token;
|
|
428
|
+
else {
|
|
429
|
+
metadataTokenCounter += 1;
|
|
430
|
+
currentMetadataToken = `m${metadataTokenCounter}`;
|
|
431
|
+
}
|
|
432
|
+
metadataToken = currentMetadataToken;
|
|
433
|
+
const templateOffset = descriptor.template?.loc.start.offset ?? 0;
|
|
434
|
+
const escapedBlockExpression = fallbackExpression.replaceAll("&", "&").replaceAll("\"", """);
|
|
435
|
+
bemDirectiveOffsets.forEach((offset) => {
|
|
436
|
+
magicString.appendLeft(templateOffset + offset, ` v-${VAPOR_HELPER_DIRECTIVE}:${currentMetadataToken}="${currentHelperIdentifier}" v-${VAPOR_BLOCK_DIRECTIVE}="${escapedBlockExpression}"`);
|
|
437
|
+
});
|
|
438
|
+
hasEdits = true;
|
|
439
|
+
}
|
|
440
|
+
const componentMetadata = {
|
|
441
|
+
blockName,
|
|
442
|
+
filename,
|
|
443
|
+
...helperIdentifier && { helperIdentifier },
|
|
444
|
+
...metadataToken && { token: metadataToken },
|
|
445
|
+
...staticBlockExpression && { staticBlockExpression },
|
|
446
|
+
...descriptor.template && { templateStart: descriptor.template.loc.start },
|
|
447
|
+
vapor: context.vapor
|
|
448
|
+
};
|
|
449
|
+
metadata.set(filename, componentMetadata);
|
|
450
|
+
if (previousMetadata?.token && previousMetadata.token !== metadataToken) metadataByToken.delete(previousMetadata.token);
|
|
451
|
+
if (metadataToken) metadataByToken.set(metadataToken, componentMetadata);
|
|
452
|
+
if (!hasEdits) return null;
|
|
453
|
+
return {
|
|
454
|
+
code: magicString.toString(),
|
|
455
|
+
map: magicString.generateMap({
|
|
456
|
+
file: filename,
|
|
457
|
+
hires: true,
|
|
458
|
+
includeContent: true,
|
|
459
|
+
source: filename
|
|
460
|
+
})
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
};
|
|
465
|
+
//#endregion
|
|
466
|
+
export { VIRTUAL_MODULE_ID as n, WEBPACK_VIRTUAL_MODULE_ID as r, createBemVueCompiler as t };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { a as CreateBemVueCompilerOptions, c as UseBem, i as BemVueTransformResult, n as BemDirectiveValue, o as ResolveBlockNameContext, r as BemVueOptions, s as TransformSfcContext, t as BemClassnamesOptions } from "./types-BHwq8e5H.js";
|
|
2
|
+
import { NodeTransform } from "@vue/compiler-core";
|
|
3
|
+
import { BemArgument, BemFunction, BemModifiers } from "@morev/bem-classnames";
|
|
4
|
+
//#region src/compiler.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Public compiler core used by bundler adapters and future integrations.
|
|
7
|
+
*/
|
|
8
|
+
type BemVueCompiler = {
|
|
9
|
+
/**
|
|
10
|
+
* Returns Vue compiler options containing the `v-bem` node transform.
|
|
11
|
+
*/
|
|
12
|
+
getTemplateCompilerOptions: () => {
|
|
13
|
+
nodeTransforms: NodeTransform[];
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Loads the configured virtual helper module.
|
|
17
|
+
*/
|
|
18
|
+
load: (id: string) => string | null;
|
|
19
|
+
/**
|
|
20
|
+
* Resolves the public virtual helper module.
|
|
21
|
+
*/
|
|
22
|
+
resolveId: (id: string) => string | null;
|
|
23
|
+
/**
|
|
24
|
+
* Transforms script calls and Vapor helper imports in one Vue SFC.
|
|
25
|
+
*/
|
|
26
|
+
transformSfc: (source: string, context: TransformSfcContext) => BemVueTransformResult | null;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Creates the compiler core shared by Vite, Webpack, and future adapters.
|
|
30
|
+
*
|
|
31
|
+
* @param compilerOptions Project paths and public BEM options.
|
|
32
|
+
*
|
|
33
|
+
* @returns Stateful compiler with SFC, template, and virtual-module hooks.
|
|
34
|
+
*/
|
|
35
|
+
declare const createBemVueCompiler: (compilerOptions: CreateBemVueCompilerOptions) => BemVueCompiler;
|
|
36
|
+
//#endregion
|
|
37
|
+
export { type BemArgument, type BemClassnamesOptions, type BemDirectiveValue, type BemFunction, type BemModifiers, type BemVueCompiler, type BemVueOptions, type BemVueTransformResult, type CreateBemVueCompilerOptions, type ResolveBlockNameContext, type TransformSfcContext, type UseBem, createBemVueCompiler };
|
package/dist/compiler.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { BemArgument, BemFunction, ModuleOptions } from "@morev/bem-classnames";
|
|
2
|
+
//#region src/types.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Value accepted by the compile-time `v-bem` directive.
|
|
5
|
+
*/
|
|
6
|
+
type BemDirectiveValue = BemArgument;
|
|
7
|
+
/**
|
|
8
|
+
* Returns a BEM helper for an explicit or component-derived block.
|
|
9
|
+
*
|
|
10
|
+
* @param name Optional explicit block name.
|
|
11
|
+
*/
|
|
12
|
+
type UseBem = (name?: string) => BemFunction;
|
|
13
|
+
/**
|
|
14
|
+
* Options accepted by the underlying BEM class names factory.
|
|
15
|
+
*/
|
|
16
|
+
type BemClassnamesOptions = Partial<ModuleOptions>;
|
|
17
|
+
/**
|
|
18
|
+
* Context used to resolve the default BEM block name of a Vue component.
|
|
19
|
+
*/
|
|
20
|
+
type ResolveBlockNameContext = {
|
|
21
|
+
/**
|
|
22
|
+
* Absolute component filename.
|
|
23
|
+
*/
|
|
24
|
+
filename: string;
|
|
25
|
+
/**
|
|
26
|
+
* Absolute project root.
|
|
27
|
+
*/
|
|
28
|
+
root: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Shared configuration for the Vue BEM compiler and its bundler adapters.
|
|
32
|
+
*/
|
|
33
|
+
type BemVueOptions = {
|
|
34
|
+
/**
|
|
35
|
+
* Options passed to `bemClassnames()`.
|
|
36
|
+
*/
|
|
37
|
+
factoryOptions?: BemClassnamesOptions;
|
|
38
|
+
/**
|
|
39
|
+
* Resolves the filename fallback used after explicit component options.
|
|
40
|
+
*/
|
|
41
|
+
resolveBlockName?: (context: ResolveBlockNameContext) => string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Options required to create a project-bound Vue BEM compiler.
|
|
45
|
+
*/
|
|
46
|
+
type CreateBemVueCompilerOptions = BemVueOptions & {
|
|
47
|
+
/**
|
|
48
|
+
* Absolute project root used by the block-name resolver.
|
|
49
|
+
*/
|
|
50
|
+
root: string;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Context for transforming one Vue single-file component.
|
|
54
|
+
*/
|
|
55
|
+
type TransformSfcContext = {
|
|
56
|
+
/**
|
|
57
|
+
* Absolute component filename.
|
|
58
|
+
*/
|
|
59
|
+
filename: string;
|
|
60
|
+
/**
|
|
61
|
+
* Whether the component is compiled by Vue Vapor.
|
|
62
|
+
*/
|
|
63
|
+
vapor: boolean;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Result returned after changing a Vue single-file component.
|
|
67
|
+
*/
|
|
68
|
+
type BemVueTransformResult = {
|
|
69
|
+
/**
|
|
70
|
+
* Transformed component source.
|
|
71
|
+
*/
|
|
72
|
+
code: string;
|
|
73
|
+
/**
|
|
74
|
+
* High-resolution source map generated by MagicString.
|
|
75
|
+
*/
|
|
76
|
+
map: ReturnType<import('magic-string').default['generateMap']>;
|
|
77
|
+
};
|
|
78
|
+
//#endregion
|
|
79
|
+
export { CreateBemVueCompilerOptions as a, UseBem as c, BemVueTransformResult as i, BemDirectiveValue as n, ResolveBlockNameContext as o, BemVueOptions as r, TransformSfcContext as s, BemClassnamesOptions as t };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createUnplugin } from "unplugin";
|
|
2
|
+
//#region src/unplugin.ts
|
|
3
|
+
const bemVueUnplugin = createUnplugin((state) => ({
|
|
4
|
+
name: "@morev/bem-vue",
|
|
5
|
+
enforce: "pre",
|
|
6
|
+
resolveId(id) {
|
|
7
|
+
return state.getCompiler().resolveId(id);
|
|
8
|
+
},
|
|
9
|
+
load(id) {
|
|
10
|
+
return state.getCompiler().load(id);
|
|
11
|
+
},
|
|
12
|
+
transformInclude(id) {
|
|
13
|
+
return !id.includes("?") && id.endsWith(".vue");
|
|
14
|
+
},
|
|
15
|
+
transform(source, id) {
|
|
16
|
+
const filename = id.split("?", 1)[0];
|
|
17
|
+
return state.getCompiler().transformSfc(source, {
|
|
18
|
+
filename,
|
|
19
|
+
vapor: state.isVapor(source, filename)
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}));
|
|
23
|
+
//#endregion
|
|
24
|
+
export { bemVueUnplugin as t };
|
package/dist/vite.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { r as BemVueOptions } from "./types-BHwq8e5H.js";
|
|
2
|
+
import { PluginOption } from "vite";
|
|
3
|
+
//#region src/vite.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Creates the Vite adapter for the compile-time `v-bem` directive.
|
|
6
|
+
*
|
|
7
|
+
* @param options Shared compiler and BEM factory options.
|
|
8
|
+
*
|
|
9
|
+
* @returns Vite plugins for the SFC source and template pipelines.
|
|
10
|
+
*/
|
|
11
|
+
declare const createBemVueVitePlugin: (options?: BemVueOptions) => PluginOption;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { type BemVueOptions, createBemVueVitePlugin as default };
|
package/dist/vite.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { t as createBemVueCompiler } from "./compiler-xDNnc6-k.js";
|
|
2
|
+
import { t as bemVueUnplugin } from "./unplugin-DAY_Kbnw.js";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { parse } from "@vue/compiler-sfc";
|
|
5
|
+
//#region src/vite.ts
|
|
6
|
+
const getVuePlugins = (config) => config.plugins.filter((plugin) => plugin.name === "vite:vue" && "api" in plugin);
|
|
7
|
+
const isVaporSfc = (source, filename, forceVapor) => {
|
|
8
|
+
const { descriptor } = parse(source, { filename });
|
|
9
|
+
if (descriptor.vapor) return true;
|
|
10
|
+
if (!forceVapor) return false;
|
|
11
|
+
if (filename.endsWith(".vue") && descriptor.script && !descriptor.scriptSetup) return false;
|
|
12
|
+
return true;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Creates the Vite adapter for the compile-time `v-bem` directive.
|
|
16
|
+
*
|
|
17
|
+
* @param options Shared compiler and BEM factory options.
|
|
18
|
+
*
|
|
19
|
+
* @returns Vite plugins for the SFC source and template pipelines.
|
|
20
|
+
*/
|
|
21
|
+
const createBemVueVitePlugin = (options = {}) => {
|
|
22
|
+
let compiler;
|
|
23
|
+
let forceVapor = false;
|
|
24
|
+
const getCompiler = () => {
|
|
25
|
+
if (!compiler) throw new Error("@morev/bem-vue: Vite configuration has not been resolved yet.");
|
|
26
|
+
return compiler;
|
|
27
|
+
};
|
|
28
|
+
return [
|
|
29
|
+
bemVueUnplugin.vite({
|
|
30
|
+
getCompiler,
|
|
31
|
+
isVapor: (source, filename) => isVaporSfc(source, filename, forceVapor)
|
|
32
|
+
}),
|
|
33
|
+
{
|
|
34
|
+
name: "@morev/bem-vue:vite-hmr",
|
|
35
|
+
enforce: "pre",
|
|
36
|
+
handleHotUpdate(context) {
|
|
37
|
+
if (!context.file.endsWith(".vue")) return;
|
|
38
|
+
const readSource = context.read;
|
|
39
|
+
let transformedSource;
|
|
40
|
+
context.read = () => {
|
|
41
|
+
transformedSource ??= Promise.resolve(readSource()).then((source) => getCompiler().transformSfc(source, {
|
|
42
|
+
filename: context.file,
|
|
43
|
+
vapor: isVaporSfc(source, context.file, forceVapor)
|
|
44
|
+
})?.code ?? source);
|
|
45
|
+
return transformedSource;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "@morev/bem-vue:vite-adapter",
|
|
51
|
+
enforce: "post",
|
|
52
|
+
configResolved(config) {
|
|
53
|
+
const vuePlugins = getVuePlugins(config);
|
|
54
|
+
if (vuePlugins.length !== 1) throw new Error(`@morev/bem-vue: expected exactly one vite:vue plugin, found ${vuePlugins.length}.`);
|
|
55
|
+
const vuePlugin = vuePlugins[0];
|
|
56
|
+
compiler = createBemVueCompiler({
|
|
57
|
+
...options,
|
|
58
|
+
root: resolve(config.root)
|
|
59
|
+
});
|
|
60
|
+
const compilerOptions = compiler.getTemplateCompilerOptions();
|
|
61
|
+
const vueOptions = vuePlugin.api.options;
|
|
62
|
+
forceVapor = vueOptions.features?.vapor === true;
|
|
63
|
+
vuePlugin.api.options = {
|
|
64
|
+
...vueOptions,
|
|
65
|
+
template: {
|
|
66
|
+
...vueOptions.template,
|
|
67
|
+
compilerOptions: {
|
|
68
|
+
...vueOptions.template?.compilerOptions,
|
|
69
|
+
nodeTransforms: [...vueOptions.template?.compilerOptions?.nodeTransforms ?? [], ...compilerOptions.nodeTransforms]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
];
|
|
76
|
+
};
|
|
77
|
+
//#endregion
|
|
78
|
+
export { createBemVueVitePlugin as default };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { r as BemVueOptions } from "./types-BHwq8e5H.js";
|
|
2
|
+
import { WebpackPluginInstance } from "webpack";
|
|
3
|
+
//#region src/webpack.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Creates the Webpack 5 adapter for the compile-time `v-bem` directive.
|
|
6
|
+
*
|
|
7
|
+
* Place this plugin before `VueLoaderPlugin` so the latter observes the merged
|
|
8
|
+
* `compilerOptions` while normalizing loader rules.
|
|
9
|
+
*
|
|
10
|
+
* @param options Shared compiler and BEM factory options.
|
|
11
|
+
*
|
|
12
|
+
* @returns Webpack plugin that transforms SFC source and configures vue-loader.
|
|
13
|
+
*/
|
|
14
|
+
declare const createBemVueWebpackPlugin: (options?: BemVueOptions) => WebpackPluginInstance;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { type BemVueOptions, createBemVueWebpackPlugin as default };
|
package/dist/webpack.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { n as VIRTUAL_MODULE_ID, r as WEBPACK_VIRTUAL_MODULE_ID, t as createBemVueCompiler } from "./compiler-xDNnc6-k.js";
|
|
2
|
+
import { t as bemVueUnplugin } from "./unplugin-DAY_Kbnw.js";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { normalize, resolve } from "node:path";
|
|
5
|
+
import { parse } from "@vue/compiler-sfc";
|
|
6
|
+
//#region src/webpack.ts
|
|
7
|
+
const isStaticRule = (value) => typeof value === "object" && value !== null;
|
|
8
|
+
const doesRuleTargetVue = (rule) => {
|
|
9
|
+
if (typeof rule.test === "string") return rule.test.includes(".vue");
|
|
10
|
+
if (!(rule.test instanceof RegExp)) return false;
|
|
11
|
+
rule.test.lastIndex = 0;
|
|
12
|
+
return rule.test.test("component.vue");
|
|
13
|
+
};
|
|
14
|
+
const getLoaderName = (loader) => typeof loader === "string" ? loader : loader.loader;
|
|
15
|
+
const getVueLoaderFilename = (context) => {
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
return normalize(require.resolve("vue-loader", { paths: [context] }));
|
|
18
|
+
};
|
|
19
|
+
const isVueLoader = (loader, context, vueLoaderFilename) => {
|
|
20
|
+
const loaderName = getLoaderName(loader);
|
|
21
|
+
if (loaderName === "vue-loader") return true;
|
|
22
|
+
try {
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
return normalize(require.resolve(loaderName, { paths: [context] })) === vueLoaderFilename;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const withCompilerOptions = (descriptor, nodeTransforms) => {
|
|
30
|
+
const options = descriptor.options ?? {};
|
|
31
|
+
const compilerOptionsValue = options.compilerOptions;
|
|
32
|
+
const compilerOptions = typeof compilerOptionsValue === "object" && compilerOptionsValue !== null ? compilerOptionsValue : {};
|
|
33
|
+
const previousTransforms = Array.isArray(compilerOptions.nodeTransforms) ? compilerOptions.nodeTransforms : [];
|
|
34
|
+
return {
|
|
35
|
+
...descriptor,
|
|
36
|
+
options: {
|
|
37
|
+
...options,
|
|
38
|
+
compilerOptions: {
|
|
39
|
+
...compilerOptions,
|
|
40
|
+
nodeTransforms: [...previousTransforms, ...nodeTransforms]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
const findVueLoaderMatches = (rules, context, vueLoaderFilename) => {
|
|
46
|
+
const matches = [];
|
|
47
|
+
rules.forEach((unknownRule) => {
|
|
48
|
+
if (!isStaticRule(unknownRule)) return;
|
|
49
|
+
const rule = unknownRule;
|
|
50
|
+
const doesTargetVue = doesRuleTargetVue(rule);
|
|
51
|
+
if (typeof rule.use === "function" && doesRuleTargetVue(rule)) throw new Error("@morev/bem-vue: function-based Webpack loader rules for .vue files are not supported.");
|
|
52
|
+
if (doesTargetVue && rule.loader && isVueLoader(rule.loader, context, vueLoaderFilename)) matches.push({ setCompilerOptions(nodeTransforms) {
|
|
53
|
+
const updated = withCompilerOptions({
|
|
54
|
+
loader: rule.loader,
|
|
55
|
+
options: rule.options
|
|
56
|
+
}, nodeTransforms);
|
|
57
|
+
rule.loader = updated.loader;
|
|
58
|
+
rule.options = updated.options;
|
|
59
|
+
} });
|
|
60
|
+
if (doesTargetVue && typeof rule.use === "string" && isVueLoader(rule.use, context, vueLoaderFilename)) matches.push({ setCompilerOptions(nodeTransforms) {
|
|
61
|
+
rule.use = withCompilerOptions({ loader: rule.use }, nodeTransforms);
|
|
62
|
+
} });
|
|
63
|
+
else if (doesTargetVue && isStaticRule(rule.use) && "loader" in rule.use && isVueLoader(rule.use, context, vueLoaderFilename)) matches.push({ setCompilerOptions(nodeTransforms) {
|
|
64
|
+
rule.use = withCompilerOptions(rule.use, nodeTransforms);
|
|
65
|
+
} });
|
|
66
|
+
else if (doesTargetVue && Array.isArray(rule.use)) rule.use.forEach((loader, index) => {
|
|
67
|
+
if (typeof loader !== "string" && !isStaticRule(loader) || !isVueLoader(loader, context, vueLoaderFilename)) return;
|
|
68
|
+
matches.push({ setCompilerOptions(nodeTransforms) {
|
|
69
|
+
const currentLoader = rule.use[index];
|
|
70
|
+
const descriptor = typeof currentLoader === "string" ? { loader: currentLoader } : currentLoader;
|
|
71
|
+
rule.use[index] = withCompilerOptions(descriptor, nodeTransforms);
|
|
72
|
+
} });
|
|
73
|
+
});
|
|
74
|
+
if (Array.isArray(rule.rules)) matches.push(...findVueLoaderMatches(rule.rules, context, vueLoaderFilename));
|
|
75
|
+
if (Array.isArray(rule.oneOf)) matches.push(...findVueLoaderMatches(rule.oneOf, context, vueLoaderFilename));
|
|
76
|
+
});
|
|
77
|
+
return matches;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Creates the Webpack 5 adapter for the compile-time `v-bem` directive.
|
|
81
|
+
*
|
|
82
|
+
* Place this plugin before `VueLoaderPlugin` so the latter observes the merged
|
|
83
|
+
* `compilerOptions` while normalizing loader rules.
|
|
84
|
+
*
|
|
85
|
+
* @param options Shared compiler and BEM factory options.
|
|
86
|
+
*
|
|
87
|
+
* @returns Webpack plugin that transforms SFC source and configures vue-loader.
|
|
88
|
+
*/
|
|
89
|
+
const createBemVueWebpackPlugin = (options = {}) => ({ apply(webpackCompiler) {
|
|
90
|
+
const root = resolve(webpackCompiler.context);
|
|
91
|
+
const compiler = createBemVueCompiler({
|
|
92
|
+
...options,
|
|
93
|
+
root
|
|
94
|
+
});
|
|
95
|
+
const rules = webpackCompiler.options.module?.rules ?? [];
|
|
96
|
+
const matches = findVueLoaderMatches(rules, root, getVueLoaderFilename(root));
|
|
97
|
+
if (matches.length !== 1) throw new Error(`@morev/bem-vue: expected exactly one static vue-loader rule, found ${matches.length}.`);
|
|
98
|
+
matches[0].setCompilerOptions(compiler.getTemplateCompilerOptions().nodeTransforms);
|
|
99
|
+
new webpackCompiler.webpack.NormalModuleReplacementPlugin(new RegExp(`^${VIRTUAL_MODULE_ID.replaceAll("/", "\\/")}$`, "u"), WEBPACK_VIRTUAL_MODULE_ID).apply(webpackCompiler);
|
|
100
|
+
bemVueUnplugin.webpack({
|
|
101
|
+
getCompiler: () => compiler,
|
|
102
|
+
isVapor: (source, filename) => {
|
|
103
|
+
const { descriptor } = parse(source, { filename });
|
|
104
|
+
if (descriptor.vapor) throw new Error("@morev/bem-vue: Vue Vapor SFCs are only supported by the Vite adapter.");
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}).apply(webpackCompiler);
|
|
108
|
+
} });
|
|
109
|
+
//#endregion
|
|
110
|
+
export { createBemVueWebpackPlugin as default };
|
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@morev/bem-vue",
|
|
3
|
+
"description": "Compile-time BEM directive and helpers for Vue components.",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"private": false,
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"version": "0.1.0",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22.12"
|
|
10
|
+
},
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public",
|
|
14
|
+
"registry": "https://registry.npmjs.org/"
|
|
15
|
+
},
|
|
16
|
+
"author": {
|
|
17
|
+
"name": "Maxim Morev",
|
|
18
|
+
"email": "max.seainside@gmail.com"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/MorevM/bem/tree/master/packages/bem-vue#readme",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/MorevM/bem",
|
|
24
|
+
"directory": "packages/bem-vue"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"bem",
|
|
28
|
+
"classnames",
|
|
29
|
+
"vue"
|
|
30
|
+
],
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"exports": {
|
|
35
|
+
"./vite": {
|
|
36
|
+
"types": "./dist/vite.d.ts",
|
|
37
|
+
"import": "./dist/vite.js"
|
|
38
|
+
},
|
|
39
|
+
"./webpack": {
|
|
40
|
+
"types": "./dist/webpack.d.ts",
|
|
41
|
+
"import": "./dist/webpack.js"
|
|
42
|
+
},
|
|
43
|
+
"./compiler": {
|
|
44
|
+
"types": "./dist/compiler.d.ts",
|
|
45
|
+
"import": "./dist/compiler.js"
|
|
46
|
+
},
|
|
47
|
+
"./client": {
|
|
48
|
+
"types": "./dist/client.d.ts"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"@vitejs/plugin-vue": "^6.0.0",
|
|
53
|
+
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
|
|
54
|
+
"vue": "^3.5.0 || >=3.6.0-rc.4 <4",
|
|
55
|
+
"vue-loader": "^17.4.0",
|
|
56
|
+
"webpack": "^5.0.0"
|
|
57
|
+
},
|
|
58
|
+
"peerDependenciesMeta": {
|
|
59
|
+
"@vitejs/plugin-vue": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
62
|
+
"vite": {
|
|
63
|
+
"optional": true
|
|
64
|
+
},
|
|
65
|
+
"vue-loader": {
|
|
66
|
+
"optional": true
|
|
67
|
+
},
|
|
68
|
+
"webpack": {
|
|
69
|
+
"optional": true
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"@babel/parser": "^7.29.8",
|
|
74
|
+
"@babel/traverse": "^7.29.0",
|
|
75
|
+
"@babel/types": "^7.29.8",
|
|
76
|
+
"@vue/compiler-core": "^3.5.41",
|
|
77
|
+
"@vue/compiler-dom": "^3.5.41",
|
|
78
|
+
"@vue/compiler-sfc": "^3.5.41",
|
|
79
|
+
"magic-string": "^1.1.0",
|
|
80
|
+
"unplugin": "^3.3.0",
|
|
81
|
+
"@morev/bem-classnames": "^2.1.0"
|
|
82
|
+
},
|
|
83
|
+
"devDependencies": {
|
|
84
|
+
"@types/babel__traverse": "^7.28.0",
|
|
85
|
+
"@vitejs/plugin-vue": "^6.0.8",
|
|
86
|
+
"@vue/compiler-vapor": "3.6.0-rc.4",
|
|
87
|
+
"happy-dom": "^20.8.4",
|
|
88
|
+
"typescript": "^6.0.2",
|
|
89
|
+
"vite": "^8.2.1",
|
|
90
|
+
"vue": "^3.5.41",
|
|
91
|
+
"vue-loader": "^17.4.2",
|
|
92
|
+
"webpack": "^5.101.3"
|
|
93
|
+
}
|
|
94
|
+
}
|