chore: 初始化 trans-form 项目脚手架
Node.js Build / build (push) Has been skipped

- 基于 grill-me Q1-Q61 决策
- Vitest 测试与 CI workflow
- middleware: parseFilters 核心实现
- component: Vue3 TransForm 基础组件 + Storybook

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-07 00:47:58 +00:00
commit fc1d8df60f
21 changed files with 5420 additions and 0 deletions
+132
View File
@@ -0,0 +1,132 @@
name: Node.js Build
on:
push:
branches: ["main", "master"]
workflow_dispatch:
jobs:
build:
if: contains(github.event.head_commit.message, 'chore') && contains(github.event.head_commit.message, '版本') && contains(github.event.head_commit.message, '更新')
runs-on: docker
steps:
- name: Checkout code
env:
GIT_AUTH: ${{ secrets.CI_USER }}:${{ secrets.CI_PASSWORD }}
run: |
git init
git remote add origin http://gitea:3000/windychen/trans-form-component.git
AUTH_B64=$(printf '%s' "$GIT_AUTH" | base64 | tr -d '\n')
git -c http.extraHeader="Authorization: Basic ${AUTH_B64}" fetch --depth 1 origin ${{ github.sha }}
git checkout ${{ github.sha }}
- name: Setup npm for Verdaccio
run: |
npm config set registry http://verdaccio:4873/
npm config set //verdaccio:4873/:_authToken ${{ secrets.NPM_TOKEN }}
- name: Install dependencies
run: npm i --registry http://verdaccio:4873/
- name: Run tests
run: npm test
- name: Run build
run: npm run build
- name: Publish to Verdaccio
id: publish-verdaccio
run: |
VERSION=$(node -p "require('./package.json').version")
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://verdaccio:4873/@windychen%2ftrans-form-component/$VERSION" || echo 404)
if [ "$HTTP_CODE" = "200" ]; then
echo "Verdaccio: version $VERSION already exists, skipping."
echo "verdaccio_result=skipped|版本 ${VERSION} 已存在" >> $GITHUB_OUTPUT
else
if npm publish --access public 2>&1; then
echo "verdaccio_result=published|已发布 v${VERSION}" >> $GITHUB_OUTPUT
else
echo "verdaccio_result=failed|发布失败" >> $GITHUB_OUTPUT
exit 1
fi
fi
- name: Publish to Gitea
id: publish-gitea
env:
GITEA_NPM_AUTH: ${{ secrets.CI_USER }}:${{ secrets.NPM_GITEA_TOKEN }}
run: |
AUTH_B64=$(printf '%s' "$GITEA_NPM_AUTH" | base64 | tr -d '\n')
npm config set //gitea:3000/api/packages/windychen/npm/:_auth "$AUTH_B64"
VERSION=$(node -p "require('./package.json').version")
EXISTS=$(curl -s "http://gitea:3000/api/v1/packages/windychen?q=trans-form-component" -u "${{ secrets.CI_USER }}:${{ secrets.CI_PASSWORD }}" | grep -c "\"version\":\"$VERSION\"" || true)
if [ "$EXISTS" -gt 0 ]; then
echo "Gitea: version $VERSION already exists, skipping."
echo "gitea_result=skipped|版本 ${VERSION} 已存在" >> $GITHUB_OUTPUT
else
if npm publish --registry http://gitea:3000/api/packages/windychen/npm/ 2>&1; then
echo "gitea_result=published|已发布 v${VERSION}" >> $GITHUB_OUTPUT
else
echo "gitea_result=failed|发布失败" >> $GITHUB_OUTPUT
exit 1
fi
fi
- name: Create Gitea Release
if: always() && steps.publish-gitea.outcome == 'success'
run: |
VERSION=$(node -p "require('./package.json').version")
EXISTS=$(curl -s "http://gitea:3000/api/v1/repos/windychen/trans-form-component/releases/tags/v${VERSION}" -u "${{ secrets.CI_USER }}:${{ secrets.CI_PASSWORD }}" | grep -c "\"tag_name\"" || true)
if [ "$EXISTS" = "0" ]; then
curl -s -X POST \
"http://gitea:3000/api/v1/repos/windychen/trans-form-component/releases" \
-u "${{ secrets.CI_USER }}:${{ secrets.CI_PASSWORD }}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"v${VERSION}\",\"name\":\"v${VERSION}\",\"body\":\"## trans-form-component v${VERSION}\",\"target_commitish\":\"main\"}"
else
echo "Release v${VERSION} already exists, skipping."
fi
- name: Notify DingTalk
if: always()
run: |
STATUS="${{ job.status }}"
VERSION=$(node -p "require('./package.json').version")
parse_result() {
local raw="$1"
local target="$2"
local icon="${raw%%|*}"
local detail="${raw#*|}"
case "$icon" in
published) echo "📦 ${target}: ${detail}" ;;
skipped) echo "⏭️ ${target}: ${detail}" ;;
failed) echo "❌ ${target}: ${detail}" ;;
*) echo "⚠️ ${target}: ${detail}" ;;
esac
}
VERD_RESULT="${{ steps.publish-verdaccio.outputs.verdaccio_result }}"
GITEA_RESULT="${{ steps.publish-gitea.outputs.gitea_result }}"
if [ -n "$VERD_RESULT" ]; then
VERD_LINE=$(parse_result "$VERD_RESULT" "Verdaccio")
else
VERD_LINE="⚠️ Verdaccio: 未执行"
fi
if [ -n "$GITEA_RESULT" ]; then
GITEA_LINE=$(parse_result "$GITEA_RESULT" "Gitea")
else
GITEA_LINE="⚠️ Gitea: 未执行"
fi
if [ "$STATUS" = "success" ]; then
TITLE="✅ trans-form-component v${VERSION} 构建完成 chen-fnos"
else
TITLE="❌ trans-form-component v${VERSION} 构建失败 chen-fnos"
fi
curl -s -X POST "https://oapi.dingtalk.com/robot/send?access_token=${{ secrets.DINGDING_TOKEN }}" \
-H "Content-Type: application/json" \
-d "{\"msgtype\":\"markdown\",\"markdown\":{\"title\":\"${TITLE}\",\"text\":\"## ${TITLE}\n\n${VERD_LINE}\n${GITEA_LINE}\n\n- 仓库: windychen/trans-form-component\n- 分支: ${{ github.ref_name }}\n- 提交: ${{ github.sha }}\"}}"
+6
View File
@@ -0,0 +1,6 @@
node_modules/
dist/
storybook-static/
coverage/
*.log
.DS_Store
+1
View File
@@ -0,0 +1 @@
registry=http://verdaccio:4873/
+9
View File
@@ -0,0 +1,9 @@
/** @type {import('@storybook/vue3-vite').StorybookConfig} */
export default {
stories: ['../stories/**/*.stories.@(js|jsx)'],
addons: ['@storybook/addon-essentials'],
framework: {
name: '@storybook/vue3-vite',
options: {},
},
};
+8
View File
@@ -0,0 +1,8 @@
import '../src/styles/trans-form.css';
/** @type {import('@storybook/vue3').Preview} */
export default {
parameters: {
layout: 'padded',
},
};
+32
View File
@@ -0,0 +1,32 @@
# trans-form-component — Design (Q1–Q61)
Status: **implementation scaffold** (v0.1.0)
Companion package: `@windychen/trans-form-middleware`.
## Role
Schema-driven Vue 3 query form → flat filter array with optional `logic`, state flags, and `empty` markers.
## Key decisions (selected)
| # | Decision |
|---|----------|
| 4–5 | No UI framework; headless native controls for string `value-component` |
| 7 | Flat array + `logic` between adjacent conditions |
| 11 | `@change` + `getFilters()` ref API |
| 15 | 12-column grid via `row` |
| 17–18 | Query dropdown; optional `query-component` (defaults to value renderer) |
| 19 | Global singleton `setConfig` / `componentMap` |
| 20 | ESM only (Vite library build) |
| 30, 35 | Inline layout 3:2:7 (name/query/value), globally configurable |
| 47 | Storybook included |
| 48 | `app.use(TransFormPlugin)` |
| 51 | Built-in reset only (`showReset`, `reset()`) |
| 54 | First filter item has no `logic` |
| 55–59 | Reactive schema; preserve matching fields; AND/OR editable; v1 types include tags |
| 60 | Output includes state flags for middleware |
## CI
Same as windychen-utils: commit must contain `chore` + `版本` + `更新`.
+75
View File
@@ -0,0 +1,75 @@
# @windychen/trans-form-component
Vue 3 dynamic query form component. Accepts a schema array, emits structured filter conditions for backend middleware.
## Install
```bash
npm install @windychen/trans-form-component vue@^3.4
```
## Usage
```js
import { createApp } from 'vue';
import { TransFormPlugin, setConfig } from '@windychen/trans-form-component';
import '@windychen/trans-form-component/style.css';
const app = createApp(App);
app.use(TransFormPlugin);
setConfig({
colRatio: { name: 3, query: 2, value: 7 },
});
```
```vue
<template>
<TransForm :schema="schema" @change="onChange" ref="formRef" />
</template>
<script setup>
import { ref } from 'vue';
const formRef = ref();
const schema = [
{
row: 6,
'value-component': 'input',
field: 'name',
'field-name': '名称',
'query-option': [{ value: 'EQ', label: '=' }],
},
];
function onChange(filters) {
console.log(filters);
}
</script>
```
## Output shape
```json
[
{ "field": "name", "value": "xx", "query": "EQ" },
{ "field": "tags", "value": null, "query": "IN", "empty": true, "logic": "OR" }
]
```
Pair with `@windychen/trans-form-middleware` for server-side parsing.
See `DESIGN.md` for full grill-me decisions (Q1–Q61).
## Scripts
```bash
npm install
npm test
npm run build
npm run storybook
```
## License
MIT
+4543
View File
File diff suppressed because it is too large Load Diff
+46
View File
@@ -0,0 +1,46 @@
{
"name": "@windychen/trans-form-component",
"version": "0.1.0",
"description": "Vue 3 dynamic query form: schema in, filter array out",
"type": "module",
"exports": {
".": {
"import": "./dist/trans-form-component.js"
},
"./style.css": "./dist/trans-form-component.css"
},
"files": [
"dist"
],
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "vitest run",
"test:watch": "vitest",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"peerDependencies": {
"vue": "^3.4.0"
},
"devDependencies": {
"@storybook/addon-essentials": "^8.6.14",
"@storybook/vue3": "^8.6.14",
"@storybook/vue3-vite": "^8.6.14",
"@vitejs/plugin-vue": "^5.2.4",
"@vue/test-utils": "^2.4.6",
"jsdom": "^26.1.0",
"storybook": "^8.6.14",
"vite": "^6.3.5",
"vitest": "^3.2.4",
"vue": "^3.5.16"
},
"keywords": [
"vue3",
"form",
"query",
"filter",
"trans-form"
],
"license": "MIT"
}
+141
View File
@@ -0,0 +1,141 @@
<script setup>
import { computed, reactive, watch } from 'vue';
import { getConfig } from './config.js';
import {
applyFilters,
buildFilters,
createFieldState,
groupRows,
} from './composables/useFilters.js';
import HeadlessInput from './components/HeadlessInput.vue';
import UnknownPlaceholder from './components/UnknownPlaceholder.vue';
const props = defineProps({
schema: { type: Array, default: () => [] },
showReset: { type: Boolean, default: true },
});
const emit = defineEmits(['change']);
const initialState = createFieldState(props.schema);
const state = reactive(createFieldState(props.schema));
const rowGroups = computed(() => groupRows(props.schema));
const config = computed(() => getConfig());
const colStyle = computed(() => {
const ratio = config.value.colRatio || { name: 3, query: 2, value: 7 };
return {
'--tf-name-cols': `${ratio.name ?? 3}fr`,
'--tf-query-cols': `${ratio.query ?? 2}fr`,
'--tf-value-cols': `${ratio.value ?? 7}fr`,
};
});
function resolveComponent(fieldDef, kind) {
const key = kind === 'query' ? 'query-component' : 'value-component';
const raw = fieldDef[key] ?? fieldDef['value-component'];
if (raw && typeof raw === 'object') return raw;
const name = typeof raw === 'string' ? raw : 'input';
const mapped = config.value.componentMap[name];
if (mapped) return mapped;
if (name === 'input' || name === 'textarea' || name === 'number' || name === 'date') {
return HeadlessInput;
}
console.warn(`[TransForm] unknown ${key}: "${name}"`);
return UnknownPlaceholder;
}
function emitChange() {
emit('change', getFilters());
}
function getFilters() {
return buildFilters(props.schema, state);
}
function setFilters(filters) {
const next = applyFilters(props.schema, filters);
for (const key of Object.keys(state)) delete state[key];
Object.assign(state, next);
emitChange();
}
function loadFilters(filters) {
setFilters(filters);
}
function reset() {
const next = createFieldState(props.schema);
for (const key of Object.keys(state)) delete state[key];
Object.assign(state, next);
emitChange();
}
watch(
() => props.schema,
(nextSchema) => {
const preserved = applyFilters(nextSchema, getFilters());
for (const key of Object.keys(state)) delete state[key];
Object.assign(state, preserved);
emitChange();
},
{ deep: true },
);
watch(
state,
() => {
if (config.value.changeEmit === 'manual') return;
emitChange();
},
{ deep: true },
);
defineExpose({ getFilters, setFilters, loadFilters, reset });
</script>
<template>
<div class="tf-form" :style="colStyle">
<div v-for="(group, gi) in rowGroups" :key="gi" class="tf-row">
<div
v-for="fieldDef in group"
:key="fieldDef.field"
class="tf-field"
:style="{ '--tf-span': fieldDef.row || 12 }"
v-show="!state[fieldDef.field]?.hidden"
>
<div class="tf-field-inner">
<label class="tf-label">{{ fieldDef['field-name'] || fieldDef.field }}</label>
<select
class="tf-control"
v-model="state[fieldDef.field].query"
:disabled="state[fieldDef.field]?.disabled"
>
<option
v-for="opt in fieldDef['query-option'] || []"
:key="opt.value"
:value="opt.value"
>
{{ opt.label || opt.value }}
</option>
</select>
<component
:is="resolveComponent(fieldDef, 'value')"
v-model="state[fieldDef.field].value"
:disabled="state[fieldDef.field]?.disabled"
:read-only="state[fieldDef.field]?.readOnly"
:name="typeof fieldDef['value-component'] === 'string' ? fieldDef['value-component'] : 'custom'"
:placeholder="fieldDef['field-name']"
/>
</div>
</div>
</div>
<div v-if="showReset" class="tf-actions">
<button type="button" class="tf-btn" @click="reset">重置</button>
</div>
</div>
</template>
+22
View File
@@ -0,0 +1,22 @@
<script setup>
defineProps({
modelValue: { type: [String, Number], default: '' },
disabled: { type: Boolean, default: false },
readOnly: { type: Boolean, default: false },
placeholder: { type: String, default: '' },
});
defineEmits(['update:modelValue']);
</script>
<template>
<input
class="tf-control tf-input"
type="text"
:value="modelValue"
:disabled="disabled"
:readonly="readOnly"
:placeholder="placeholder"
@input="$emit('update:modelValue', ($event.target).value)"
/>
</template>
+11
View File
@@ -0,0 +1,11 @@
<script setup>
defineProps({
name: { type: String, required: true },
});
</script>
<template>
<div class="tf-unknown" role="alert">
Unknown component: {{ name }}
</div>
</template>
+126
View File
@@ -0,0 +1,126 @@
/**
* @typedef {object} SchemaField
* @property {number} [row]
* @property {'AND'|'OR'} [logic]
* @property {string|import('vue').Component} [value-component]
* @property {string|import('vue').Component} [query-component]
* @property {string} field
* @property {string} [field-name]
* @property {Array<{value: string, label?: string}>} [query-option]
* @property {string} [default-query]
* @property {boolean} [hidden]
* @property {boolean} [disabled]
* @property {boolean} [readOnly]
* @property {*} [value]
* @property {string} [query]
*/
/**
* @param {SchemaField[]} schema
*/
export function createFieldState(schema) {
/** @type {Record<string, any>} */
const state = {};
for (const item of schema || []) {
if (!item?.field) continue;
const defaultQuery =
item['default-query'] ??
item.query ??
item['query-option']?.[0]?.value ??
'EQ';
state[item.field] = {
value: item.value ?? '',
query: defaultQuery,
logic: item.logic ?? 'AND',
hidden: item.hidden === true,
disabled: item.disabled === true,
readOnly: item.readOnly === true,
};
}
return state;
}
/**
* @param {SchemaField[]} schema
* @param {Record<string, any>} state
*/
export function buildFilters(schema, state) {
const filters = [];
const fields = (schema || []).filter((s) => s?.field);
for (let i = 0; i < fields.length; i++) {
const fieldDef = fields[i];
const key = fieldDef.field;
const current = state[key] || {};
const value = current.value ?? '';
const empty =
value === '' || value == null || String(value).trim() === '';
/** @type {Record<string, any>} */
const row = {
field: key,
value: empty ? null : value,
query: current.query ?? fieldDef['query-option']?.[0]?.value ?? 'EQ',
hidden: current.hidden === true,
disabled: current.disabled === true,
readOnly: current.readOnly === true,
};
if (empty) row.empty = true;
if (i > 0) row.logic = current.logic ?? fieldDef.logic ?? 'AND';
filters.push(row);
}
return filters;
}
/**
* @param {SchemaField[]} schema
* @param {Array<Record<string, any>>} filters
*/
export function applyFilters(schema, filters) {
const state = createFieldState(schema);
for (const item of filters || []) {
if (!item?.field || !state[item.field]) continue;
state[item.field] = {
...state[item.field],
value: item.value ?? '',
query: item.query ?? state[item.field].query,
logic: item.logic ?? state[item.field].logic,
hidden: item.hidden === true,
disabled: item.disabled === true,
readOnly: item.readOnly === true,
};
}
return state;
}
/**
* Group schema rows by 12-column grid.
* @param {SchemaField[]} schema
*/
export function groupRows(schema) {
const groups = [];
let current = [];
let used = 0;
for (const item of schema || []) {
const span = Math.min(12, Math.max(1, Number(item.row) || 12));
if (used + span > 12 && current.length) {
groups.push(current);
current = [];
used = 0;
}
current.push(item);
used += span;
if (used >= 12) {
groups.push(current);
current = [];
used = 0;
}
}
if (current.length) groups.push(current);
return groups;
}
+34
View File
@@ -0,0 +1,34 @@
/** @typedef {{ componentMap?: Record<string, import('vue').Component>, colRatio?: { name?: number, query?: number, value?: number }, changeEmit?: 'immediate'|'debounce'|'manual' }} TransFormConfig */
/** @type {TransFormConfig & { componentMap: Record<string, import('vue').Component> }} */
const globalConfig = {
componentMap: {},
colRatio: { name: 3, query: 2, value: 7 },
changeEmit: 'immediate',
};
/**
* @param {Partial<TransFormConfig>} patch
*/
export function setConfig(patch) {
if (patch.componentMap) {
globalConfig.componentMap = {
...globalConfig.componentMap,
...patch.componentMap,
};
}
if (patch.colRatio) {
globalConfig.colRatio = { ...globalConfig.colRatio, ...patch.colRatio };
}
if (patch.changeEmit) globalConfig.changeEmit = patch.changeEmit;
}
export function getConfig() {
return globalConfig;
}
export function resetConfig() {
globalConfig.componentMap = {};
globalConfig.colRatio = { name: 3, query: 2, value: 7 };
globalConfig.changeEmit = 'immediate';
}
+3
View File
@@ -0,0 +1,3 @@
import './styles/trans-form.css';
export { default as TransFormPlugin, TransForm, setConfig, getConfig } from './plugin.js';
+11
View File
@@ -0,0 +1,11 @@
import TransForm from './TransForm.vue';
import { setConfig, getConfig } from './config.js';
export { TransForm, setConfig, getConfig };
export default {
install(app) {
app.component('TransForm', TransForm);
app.config.globalProperties.$setTransFormConfig = setConfig;
},
};
+81
View File
@@ -0,0 +1,81 @@
:root {
--tf-gap: 8px;
--tf-border: #d0d7de;
--tf-radius: 6px;
--tf-label-color: #57606a;
--tf-bg: #ffffff;
--tf-unknown-bg: #f6f8fa;
--tf-unknown-color: #8c959f;
}
.tf-form {
display: flex;
flex-direction: column;
gap: var(--tf-gap);
}
.tf-row {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: var(--tf-gap);
}
.tf-field {
display: grid;
grid-template-columns: subgrid;
grid-column: span var(--tf-span, 12);
align-items: center;
gap: var(--tf-gap);
}
.tf-field-inner {
display: grid;
grid-template-columns: var(--tf-name-cols, 3fr) var(--tf-query-cols, 2fr) var(--tf-value-cols, 7fr);
gap: var(--tf-gap);
align-items: center;
}
.tf-label {
color: var(--tf-label-color);
font-size: 14px;
}
.tf-control {
width: 100%;
box-sizing: border-box;
border: 1px solid var(--tf-border);
border-radius: var(--tf-radius);
padding: 6px 8px;
background: var(--tf-bg);
}
.tf-control:disabled,
.tf-control[readonly] {
opacity: 0.65;
}
.tf-unknown {
border: 1px dashed var(--tf-border);
background: var(--tf-unknown-bg);
color: var(--tf-unknown-color);
border-radius: var(--tf-radius);
padding: 8px;
font-size: 13px;
}
.tf-actions {
display: flex;
gap: var(--tf-gap);
}
.tf-btn {
border: 1px solid var(--tf-border);
background: var(--tf-bg);
border-radius: var(--tf-radius);
padding: 6px 12px;
cursor: pointer;
}
.tf-btn:hover {
background: #f6f8fa;
}
+36
View File
@@ -0,0 +1,36 @@
import TransForm from '../src/TransForm.vue';
import '../src/styles/trans-form.css';
export default {
title: 'TransForm/Basic',
component: TransForm,
};
const schema = [
{
row: 6,
logic: 'AND',
'value-component': 'input',
field: 'name',
'field-name': '名称',
'query-option': [
{ value: 'EQ', label: '=', 'desc-cn': '等于' },
{ value: 'LIKE', label: '~', 'desc-cn': '包含' },
],
},
{
row: 6,
logic: 'OR',
'value-component': 'input',
field: 'tags',
'field-name': '标签',
'query-option': [{ value: 'IN', label: '∈', 'desc-cn': '包含任一' }],
},
];
export const Default = {
args: {
schema,
showReset: true,
},
};
+70
View File
@@ -0,0 +1,70 @@
import { describe, expect, it } from 'vitest';
import {
applyFilters,
buildFilters,
createFieldState,
groupRows,
} from '../src/composables/useFilters.js';
const schema = [
{
row: 6,
field: 'name',
'field-name': '名称',
'query-option': [{ value: 'EQ', label: '=' }],
},
{
row: 6,
logic: 'OR',
field: 'status',
'query-option': [{ value: 'NE', label: '≠' }],
},
];
describe('createFieldState', () => {
it('initializes default query from first option', () => {
const state = createFieldState(schema);
expect(state.name.query).toBe('EQ');
expect(state.status.logic).toBe('OR');
});
});
describe('buildFilters', () => {
it('emits logic from second item onward', () => {
const state = createFieldState(schema);
state.name.value = 'alice';
state.status.value = 'active';
const filters = buildFilters(schema, state);
expect(filters[0].logic).toBeUndefined();
expect(filters[1].logic).toBe('OR');
});
it('marks empty values', () => {
const state = createFieldState(schema);
const filters = buildFilters(schema, state);
expect(filters[0].empty).toBe(true);
expect(filters[0].value).toBeNull();
});
});
describe('applyFilters', () => {
it('loads filters back into state', () => {
const loaded = applyFilters(schema, [
{ field: 'name', value: 'bob', query: 'EQ' },
]);
expect(loaded.name.value).toBe('bob');
});
});
describe('groupRows', () => {
it('wraps when row spans exceed 12', () => {
const groups = groupRows([
{ row: 2, field: 'a' },
{ row: 9, field: 'b' },
{ row: 4, field: 'c' },
]);
expect(groups).toHaveLength(2);
expect(groups[0]).toHaveLength(2);
expect(groups[1]).toHaveLength(1);
});
});
+24
View File
@@ -0,0 +1,24 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'node:path';
export default defineConfig({
plugins: [vue()],
build: {
lib: {
entry: resolve(__dirname, 'src/index.js'),
name: 'TransFormComponent',
fileName: 'trans-form-component',
formats: ['es'],
},
rollupOptions: {
external: ['vue'],
output: {
globals: {
vue: 'Vue',
},
},
},
cssCodeSplit: false,
},
});
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom',
},
});