frappe-ui 0.1.113 → 0.1.115
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/package.json +7 -3
- package/src/components/Autocomplete.vue +67 -17
- package/src/components/Rating/Rating.vue +3 -3
- package/src/components/TextEditor/CodeBlockComponent.vue +1 -1
- package/src/components/Tooltip/Tooltip.vue +1 -1
- package/vite/README.md +184 -0
- package/vite/buildConfig.js +169 -0
- package/{scripts/generateInterface.js → vite/doctypeInterfaceGenerator.js} +48 -22
- package/vite/frappeProxy.js +36 -0
- package/vite/frappeTypes.js +67 -0
- package/vite/generateTypes.js +59 -0
- package/vite/index.js +35 -0
- package/vite/jinjaBootData.js +25 -0
- package/vite/lucideIcons.js +67 -0
- package/vite/utils.js +48 -0
- package/vite.js +0 -106
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frappe-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.115",
|
|
4
4
|
"description": "A set of components and utilities for rapid UI development",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"files": [
|
|
18
18
|
"src",
|
|
19
19
|
"scripts",
|
|
20
|
-
"vite
|
|
20
|
+
"vite"
|
|
21
21
|
],
|
|
22
22
|
"repository": {
|
|
23
23
|
"type": "git",
|
|
@@ -53,13 +53,17 @@
|
|
|
53
53
|
"feather-icons": "^4.28.0",
|
|
54
54
|
"idb-keyval": "^6.2.0",
|
|
55
55
|
"lowlight": "^3.3.0",
|
|
56
|
+
"lucide-static": "^0.479.0",
|
|
56
57
|
"ora": "5.4.1",
|
|
57
58
|
"prettier": "^3.3.2",
|
|
58
59
|
"radix-vue": "^1.5.3",
|
|
60
|
+
"reka-ui": "^2.0.2",
|
|
59
61
|
"showdown": "^2.1.0",
|
|
60
62
|
"socket.io-client": "^4.5.1",
|
|
61
63
|
"tippy.js": "^6.3.7",
|
|
62
|
-
"typescript": "^5.0.2"
|
|
64
|
+
"typescript": "^5.0.2",
|
|
65
|
+
"unplugin-icons": "^22.1.0",
|
|
66
|
+
"unplugin-vue-components": "^28.4.1"
|
|
63
67
|
},
|
|
64
68
|
"peerDependencies": {
|
|
65
69
|
"vue": ">=3.5.0",
|
|
@@ -5,10 +5,28 @@
|
|
|
5
5
|
nullable
|
|
6
6
|
v-slot="{ open: isComboboxOpen }"
|
|
7
7
|
>
|
|
8
|
-
<Popover
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
<Popover
|
|
9
|
+
class="w-full"
|
|
10
|
+
v-model:show="showOptions"
|
|
11
|
+
ref="rootRef"
|
|
12
|
+
:placement="placement"
|
|
13
|
+
>
|
|
14
|
+
<template
|
|
15
|
+
#target="{ open: openPopover, togglePopover, close: closePopover }"
|
|
16
|
+
>
|
|
17
|
+
<slot
|
|
18
|
+
name="target"
|
|
19
|
+
v-bind="{
|
|
20
|
+
open: openPopover,
|
|
21
|
+
close: closePopover,
|
|
22
|
+
togglePopover,
|
|
23
|
+
isOpen: isComboboxOpen,
|
|
24
|
+
}"
|
|
25
|
+
>
|
|
26
|
+
<div class="w-full space-y-1.5">
|
|
27
|
+
<label v-if="props.label" class="block text-xs text-ink-gray-5">
|
|
28
|
+
{{ props.label }}
|
|
29
|
+
</label>
|
|
12
30
|
<button
|
|
13
31
|
class="flex h-7 w-full items-center justify-between gap-2 rounded bg-surface-gray-2 px-2 py-1 transition-colors hover:bg-surface-gray-3 border border-transparent focus:border-outline-gray-4 focus:outline-none focus:ring-2 focus:ring-outline-gray-3"
|
|
14
32
|
:class="{ 'bg-surface-gray-3': isComboboxOpen }"
|
|
@@ -25,6 +43,7 @@
|
|
|
25
43
|
<span class="text-base leading-5 text-ink-gray-4" v-else>
|
|
26
44
|
{{ placeholder || '' }}
|
|
27
45
|
</span>
|
|
46
|
+
<slot name="suffix" />
|
|
28
47
|
</div>
|
|
29
48
|
<FeatherIcon
|
|
30
49
|
name="chevron-down"
|
|
@@ -60,12 +79,17 @@
|
|
|
60
79
|
autocomplete="off"
|
|
61
80
|
placeholder="Search"
|
|
62
81
|
/>
|
|
63
|
-
<
|
|
82
|
+
<div
|
|
64
83
|
class="absolute right-0 inline-flex h-7 w-7 items-center justify-center"
|
|
65
|
-
@click="clearAll"
|
|
66
84
|
>
|
|
67
|
-
<
|
|
68
|
-
|
|
85
|
+
<LoadingIndicator
|
|
86
|
+
v-if="!props.loading"
|
|
87
|
+
class="h-4 w-4 text-ink-gray-5"
|
|
88
|
+
/>
|
|
89
|
+
<button v-else @click="clearAll">
|
|
90
|
+
<FeatherIcon name="x" class="w-4 text-ink-gray-8" />
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
69
93
|
</div>
|
|
70
94
|
</div>
|
|
71
95
|
<div
|
|
@@ -84,17 +108,21 @@
|
|
|
84
108
|
v-for="(option, idx) in group.items.slice(0, 50)"
|
|
85
109
|
:key="idx"
|
|
86
110
|
:value="option"
|
|
111
|
+
:disabled="option.disabled"
|
|
87
112
|
v-slot="{ active, selected }"
|
|
88
113
|
>
|
|
89
114
|
<li
|
|
90
115
|
:class="[
|
|
91
116
|
'flex cursor-pointer items-center justify-between rounded px-2.5 py-1.5 text-base',
|
|
92
|
-
{
|
|
117
|
+
{
|
|
118
|
+
'bg-surface-gray-3': active,
|
|
119
|
+
'opacity-50': option.disabled,
|
|
120
|
+
},
|
|
93
121
|
]"
|
|
94
122
|
>
|
|
95
123
|
<div class="flex flex-1 gap-2 overflow-hidden items-center">
|
|
96
124
|
<div
|
|
97
|
-
v-if="$slots['item-prefix'] ||
|
|
125
|
+
v-if="$slots['item-prefix'] || props.multiple"
|
|
98
126
|
class="flex flex-shrink-0"
|
|
99
127
|
>
|
|
100
128
|
<slot
|
|
@@ -141,7 +169,10 @@
|
|
|
141
169
|
</li>
|
|
142
170
|
</ComboboxOptions>
|
|
143
171
|
|
|
144
|
-
<div
|
|
172
|
+
<div
|
|
173
|
+
v-if="$slots.footer || props.showFooter || multiple"
|
|
174
|
+
class="border-t p-1"
|
|
175
|
+
>
|
|
145
176
|
<slot name="footer" v-bind="{ togglePopover }">
|
|
146
177
|
<div v-if="multiple" class="flex items-center justify-end">
|
|
147
178
|
<Button
|
|
@@ -153,8 +184,12 @@
|
|
|
153
184
|
v-if="areAllOptionsSelected"
|
|
154
185
|
label="Clear All"
|
|
155
186
|
@click.stop="clearAll"
|
|
156
|
-
|
|
157
|
-
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
<div v-else class="flex items-center justify-end">
|
|
190
|
+
<Button label="Clear" @click.stop="clearAll" />
|
|
191
|
+
</div>
|
|
192
|
+
</slot>
|
|
158
193
|
</div>
|
|
159
194
|
</div>
|
|
160
195
|
</div>
|
|
@@ -174,6 +209,7 @@ import { computed, nextTick, ref, watch } from 'vue'
|
|
|
174
209
|
import Popover from './Popover.vue'
|
|
175
210
|
import { Button } from './Button'
|
|
176
211
|
import FeatherIcon from './FeatherIcon.vue'
|
|
212
|
+
import LoadingIndicator from './LoadingIndicator.vue'
|
|
177
213
|
|
|
178
214
|
type Option = {
|
|
179
215
|
label: string
|
|
@@ -195,10 +231,14 @@ type AutocompleteOptionGroup = {
|
|
|
195
231
|
type AutocompleteOptions = AutocompleteOption[] | AutocompleteOptionGroup[]
|
|
196
232
|
|
|
197
233
|
type AutocompleteProps = {
|
|
234
|
+
label?: string
|
|
198
235
|
options: AutocompleteOptions
|
|
199
236
|
hideSearch?: boolean
|
|
200
237
|
placeholder?: string
|
|
201
238
|
bodyClasses?: string | string[]
|
|
239
|
+
loading?: boolean
|
|
240
|
+
placement?: string
|
|
241
|
+
showFooter?: boolean
|
|
202
242
|
} & (
|
|
203
243
|
| {
|
|
204
244
|
multiple: true
|
|
@@ -278,13 +318,19 @@ const filterOptions = (options: Option[]) => {
|
|
|
278
318
|
const selectedValue = computed({
|
|
279
319
|
get() {
|
|
280
320
|
if (!props.multiple) {
|
|
281
|
-
return
|
|
321
|
+
return (
|
|
322
|
+
findOption(props.modelValue as AutocompleteOption) ||
|
|
323
|
+
// if the modelValue is not found in the option list
|
|
324
|
+
// return the modelValue as is
|
|
325
|
+
makeOption(props.modelValue as AutocompleteOption)
|
|
326
|
+
)
|
|
282
327
|
}
|
|
283
328
|
// in case of `multiple`, modelValue is an array of values
|
|
284
329
|
// if the modelValue is a list of values, convert them to options
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
330
|
+
const values = (props.modelValue || []) as AutocompleteOption[]
|
|
331
|
+
return isOption(values[0])
|
|
332
|
+
? values
|
|
333
|
+
: values.map((v) => findOption(v) || makeOption(v))
|
|
288
334
|
},
|
|
289
335
|
set(val) {
|
|
290
336
|
query.value = ''
|
|
@@ -303,6 +349,10 @@ const findOption = (option: AutocompleteOption) => {
|
|
|
303
349
|
return allOptions.value.find((o) => o.value === value)
|
|
304
350
|
}
|
|
305
351
|
|
|
352
|
+
const makeOption = (option: AutocompleteOption) => {
|
|
353
|
+
return isOption(option) ? option : { label: option, value: option }
|
|
354
|
+
}
|
|
355
|
+
|
|
306
356
|
const getLabel = (option: AutocompleteOption) => {
|
|
307
357
|
if (isOption(option)) {
|
|
308
358
|
return option?.label || option?.value || 'No label'
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
>
|
|
13
13
|
<FeatherIcon
|
|
14
14
|
name="star"
|
|
15
|
-
class="fill-gray-
|
|
15
|
+
class="fill-gray-300 text-transparent mr-0.5"
|
|
16
16
|
:class="iconClasses(index)"
|
|
17
17
|
@click="markRating(index)"
|
|
18
18
|
/>
|
|
@@ -55,9 +55,9 @@ const iconClasses = (index: number) => {
|
|
|
55
55
|
]
|
|
56
56
|
|
|
57
57
|
if (index <= hoveredRating.value && index > rating.value) {
|
|
58
|
-
classes.push('fill-yellow-200')
|
|
58
|
+
classes.push('!fill-yellow-200')
|
|
59
59
|
} else if (index <= rating.value) {
|
|
60
|
-
classes.push('fill-yellow-500')
|
|
60
|
+
classes.push('!fill-yellow-500')
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
if (!props.readonly) {
|
package/vite/README.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Frappe UI Vite Plugins
|
|
2
|
+
|
|
3
|
+
A collection of Vite plugins for Frappe applications that handle common
|
|
4
|
+
development tasks when building modern frontends for Frappe.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install frappe-ui
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
In your `vite.config.js` file:
|
|
15
|
+
|
|
16
|
+
```javascript
|
|
17
|
+
import { defineConfig } from 'vite'
|
|
18
|
+
import vue from '@vitejs/plugin-vue'
|
|
19
|
+
import path from 'path'
|
|
20
|
+
import frappeui from 'frappe-ui/vite'
|
|
21
|
+
|
|
22
|
+
export default defineConfig({
|
|
23
|
+
plugins: [
|
|
24
|
+
frappeui({
|
|
25
|
+
frappeProxy: true, // Setup proxy to Frappe backend
|
|
26
|
+
lucideIcons: true, // Configure Lucide icons
|
|
27
|
+
jinjaBootData: true, // Inject server-side boot data
|
|
28
|
+
// Generate TypeScript interfaces from DocTypes
|
|
29
|
+
frappeTypes: {
|
|
30
|
+
input: {
|
|
31
|
+
app_name: ['doctype_1', 'doctype_2'],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
// Production build config for asset paths and HTML output
|
|
35
|
+
buildConfig: {
|
|
36
|
+
indexHtmlPath: '../your_app/www/frontend.html',
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
vue(),
|
|
40
|
+
],
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Plugins
|
|
45
|
+
|
|
46
|
+
### Frappe Proxy Plugin
|
|
47
|
+
|
|
48
|
+
Automatically configures a development server that proxies requests to your
|
|
49
|
+
Frappe backend.
|
|
50
|
+
|
|
51
|
+
#### Features
|
|
52
|
+
|
|
53
|
+
- Sets up a proxy for backend routes (`/api`, `/app`, etc.)
|
|
54
|
+
- Automatically detects Frappe port from `common_site_config.json`
|
|
55
|
+
|
|
56
|
+
#### Configuration
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
frappeui({
|
|
60
|
+
frappeProxy: {
|
|
61
|
+
port: 8080, // Frontend dev server port
|
|
62
|
+
source: '^/(app|login|api|assets|files|private)', // Routes to proxy
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Lucide Icons Plugin
|
|
68
|
+
|
|
69
|
+
Integrates [Lucide icons](https://lucide.dev/) with your app, providing access
|
|
70
|
+
to all Lucide icons with some customization.
|
|
71
|
+
|
|
72
|
+
#### Features
|
|
73
|
+
|
|
74
|
+
- Automatically registers all Lucide icons for use in Vue components
|
|
75
|
+
- Configures icons with standardized stroke-width 1.5 according to our design
|
|
76
|
+
system
|
|
77
|
+
- Support auto-import
|
|
78
|
+
|
|
79
|
+
#### Implicit import
|
|
80
|
+
|
|
81
|
+
```vue
|
|
82
|
+
<template>
|
|
83
|
+
<LucideArrowRight class="size-4" />
|
|
84
|
+
</template>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### Explicit import
|
|
88
|
+
|
|
89
|
+
```vue
|
|
90
|
+
<template>
|
|
91
|
+
<LucideArrowRight class="size-4" />
|
|
92
|
+
<LucideBarChart class="size-4" />
|
|
93
|
+
</template>
|
|
94
|
+
<script setup lang="ts">
|
|
95
|
+
import LucideArrowRight from '~icons/lucide/arrow-right'
|
|
96
|
+
import LucideBarChart from '~icons/lucide/bar-chart'
|
|
97
|
+
</script>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Frappe Types Plugin
|
|
101
|
+
|
|
102
|
+
Generates TypeScript interfaces for your Frappe DocTypes, providing type safety
|
|
103
|
+
when working with Frappe data.
|
|
104
|
+
|
|
105
|
+
#### Features
|
|
106
|
+
|
|
107
|
+
- Automatically generates TypeScript interfaces from DocType JSON files
|
|
108
|
+
- Creates and updates interfaces only when DocTypes change
|
|
109
|
+
|
|
110
|
+
#### Configuration
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
frappeui({
|
|
114
|
+
frappeTypes: {
|
|
115
|
+
input: {
|
|
116
|
+
your_app_name: ['doctype1', 'doctype2'],
|
|
117
|
+
},
|
|
118
|
+
output: 'src/types/doctypes.ts', // (optional)
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Jinja Boot Data Plugin
|
|
124
|
+
|
|
125
|
+
Injects jinja block that reads keys from `boot` context object and sets in
|
|
126
|
+
`window`. Useful to set global values like `csrf_token`, `site_name`, etc.
|
|
127
|
+
|
|
128
|
+
#### Configuration
|
|
129
|
+
|
|
130
|
+
```javascript
|
|
131
|
+
frappeui({
|
|
132
|
+
jinjaBootData: true,
|
|
133
|
+
})
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### Usage
|
|
137
|
+
|
|
138
|
+
In your Python/Jinja template:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
|
|
142
|
+
def get_context(context):
|
|
143
|
+
context.boot = {
|
|
144
|
+
"csrf_token": "...",
|
|
145
|
+
"user": frappe.session.user,
|
|
146
|
+
"user_info": frappe.session.user_info,
|
|
147
|
+
}
|
|
148
|
+
return context
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
In your JavaScript code:
|
|
152
|
+
|
|
153
|
+
```javascript
|
|
154
|
+
// Access injected data
|
|
155
|
+
console.log(window.user)
|
|
156
|
+
console.log(window.user_info)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Build Configuration Plugin
|
|
160
|
+
|
|
161
|
+
Handles production builds with proper asset paths and HTML output.
|
|
162
|
+
|
|
163
|
+
#### Features
|
|
164
|
+
|
|
165
|
+
- Configures output directories for build assets
|
|
166
|
+
- Sets up correct asset paths for Frappe's standard directory structure
|
|
167
|
+
- Copies the built index.html to a specified location (typically in www)
|
|
168
|
+
|
|
169
|
+
#### Configuration
|
|
170
|
+
|
|
171
|
+
```javascript
|
|
172
|
+
frappeui({
|
|
173
|
+
buildConfig: {
|
|
174
|
+
// default: '../app_name/public/frontend', auto-detected if not specified
|
|
175
|
+
outDir: '../app_name/public/frontend',
|
|
176
|
+
// Base URL for assets (auto-detected from outDir if not specified)
|
|
177
|
+
baseUrl: '/assets/app_name/frontend/',
|
|
178
|
+
// required, typically "../app_name/www/app_name.html"
|
|
179
|
+
indexHtmlPath: '../app_name/www/app_name.html',
|
|
180
|
+
emptyOutDir: true, // Clear output directory before build
|
|
181
|
+
sourcemap: true, // Generate sourcemaps
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
```
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
|
|
4
|
+
function buildConfig(options = {}) {
|
|
5
|
+
let outDir = options.outDir || findOutputDir()
|
|
6
|
+
if (!outDir) {
|
|
7
|
+
console.error(
|
|
8
|
+
'[frappeui-build-config-plugin] Could not find build output directory automatically. Please specify it manually.',
|
|
9
|
+
)
|
|
10
|
+
return
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const defaultOptions = {
|
|
14
|
+
outDir,
|
|
15
|
+
emptyOutDir: true,
|
|
16
|
+
sourcemap: true,
|
|
17
|
+
indexHtmlPath: null,
|
|
18
|
+
baseUrl: options.baseUrl || getBaseUrl(outDir),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const mergedOptions = { ...defaultOptions, ...options }
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
name: 'frappeui-build-config-plugin',
|
|
25
|
+
config(config, { command, mode }) {
|
|
26
|
+
const buildConfig = {
|
|
27
|
+
build: {
|
|
28
|
+
outDir: mergedOptions.outDir,
|
|
29
|
+
emptyOutDir: mergedOptions.emptyOutDir,
|
|
30
|
+
commonjsOptions: {
|
|
31
|
+
include: [/tailwind.config.js/, /node_modules/],
|
|
32
|
+
},
|
|
33
|
+
sourcemap: mergedOptions.sourcemap,
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (mode === 'production') {
|
|
38
|
+
// Apply baseUrl only in production mode
|
|
39
|
+
buildConfig.base = mergedOptions.baseUrl
|
|
40
|
+
|
|
41
|
+
if (!mergedOptions.indexHtmlPath) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
'[frappeui-build-config-plugin] indexHtmlPath is required in buildConfig options',
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return buildConfig
|
|
49
|
+
},
|
|
50
|
+
writeBundle(options, bundle) {
|
|
51
|
+
if (mergedOptions.indexHtmlPath) {
|
|
52
|
+
try {
|
|
53
|
+
const sourceHtml = path.join(mergedOptions.outDir, 'index.html')
|
|
54
|
+
if (fs.existsSync(sourceHtml)) {
|
|
55
|
+
// Ensure destination directory exists
|
|
56
|
+
const destDir = path.dirname(mergedOptions.indexHtmlPath)
|
|
57
|
+
if (!fs.existsSync(destDir)) {
|
|
58
|
+
fs.mkdirSync(destDir, { recursive: true })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fs.copyFileSync(sourceHtml, mergedOptions.indexHtmlPath)
|
|
62
|
+
console.log(
|
|
63
|
+
`[frappeui-build-config-plugin] Successfully copied index.html to ${mergedOptions.indexHtmlPath}`,
|
|
64
|
+
)
|
|
65
|
+
} else {
|
|
66
|
+
console.error(
|
|
67
|
+
`[frappeui-build-config-plugin] Source index.html not found at ${sourceHtml}`,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error(
|
|
72
|
+
'[frappeui-build-config-plugin] Error copying index.html:',
|
|
73
|
+
error,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function findOutputDir() {
|
|
82
|
+
let appDir = findAppDir()
|
|
83
|
+
if (appDir) {
|
|
84
|
+
return path.join(appDir, 'public', 'frontend')
|
|
85
|
+
}
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getBaseUrl(outputDir) {
|
|
90
|
+
try {
|
|
91
|
+
// Parse the output directory path to extract app name
|
|
92
|
+
// Expected pattern: /path/to/apps/app_name/app_name/public/frontend
|
|
93
|
+
if (!outputDir) return '/'
|
|
94
|
+
|
|
95
|
+
const parts = outputDir.split(path.sep)
|
|
96
|
+
const publicIndex = parts.indexOf('public')
|
|
97
|
+
|
|
98
|
+
if (publicIndex > 0) {
|
|
99
|
+
const appName = parts[publicIndex - 1] // Get the app name from path
|
|
100
|
+
|
|
101
|
+
// Check if the app name appears twice in sequence (common in Frappe apps)
|
|
102
|
+
const appsIndex = parts.indexOf('apps')
|
|
103
|
+
if (appsIndex >= 0 && publicIndex > appsIndex + 2) {
|
|
104
|
+
// If in standard Frappe app structure, use the app name from apps/app_name path
|
|
105
|
+
const possibleAppName = parts[appsIndex + 1]
|
|
106
|
+
if (possibleAppName === appName) {
|
|
107
|
+
// Determine the part after public (typically 'frontend')
|
|
108
|
+
const subdir = parts[publicIndex + 1] || ''
|
|
109
|
+
return `/assets/${appName}/${subdir}/`
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Fallback: just use the detected app name with standard structure
|
|
114
|
+
const subdir = parts[publicIndex + 1] || ''
|
|
115
|
+
return `/assets/${appName}/${subdir}/`
|
|
116
|
+
}
|
|
117
|
+
// fallback
|
|
118
|
+
return '/'
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error(
|
|
121
|
+
'[frappeui-build-config-plugin] Error calculating base URL:',
|
|
122
|
+
error,
|
|
123
|
+
)
|
|
124
|
+
// fallback on error
|
|
125
|
+
return '/'
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function findAppDir() {
|
|
130
|
+
// currentDir is the vue app directory
|
|
131
|
+
let currentDir = process.cwd()
|
|
132
|
+
// appDir is the frappe app directory
|
|
133
|
+
let appDir = path.resolve(currentDir, '..')
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
// Read directories in the app directory
|
|
137
|
+
const directories = fs
|
|
138
|
+
.readdirSync(appDir)
|
|
139
|
+
.filter((item) => fs.statSync(path.join(appDir, item)).isDirectory())
|
|
140
|
+
|
|
141
|
+
// Find the first directory that contains folders typical of a Frappe app
|
|
142
|
+
for (const dir of directories) {
|
|
143
|
+
const dirPath = path.join(appDir, dir)
|
|
144
|
+
try {
|
|
145
|
+
const contents = fs.readdirSync(dirPath)
|
|
146
|
+
if (
|
|
147
|
+
contents.includes('public') &&
|
|
148
|
+
contents.includes('patches') &&
|
|
149
|
+
contents.includes('www') &&
|
|
150
|
+
contents.includes('hooks.py')
|
|
151
|
+
) {
|
|
152
|
+
return dirPath
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
// Skip directories that can't be read
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error(
|
|
161
|
+
'[frappeui-build-config-plugin] Error finding app directory:',
|
|
162
|
+
error,
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return null
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = { buildConfig }
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
const fs = require('fs').promises
|
|
2
2
|
const path = require('path')
|
|
3
|
-
const ora = require('ora')
|
|
4
3
|
|
|
5
|
-
|
|
4
|
+
class DocTypeInterfaceGenerator {
|
|
6
5
|
constructor(appsPath, appDoctypeMap, outputPath) {
|
|
7
6
|
this.appsPath = appsPath
|
|
8
7
|
this.appDoctypeMap = appDoctypeMap
|
|
@@ -10,8 +9,14 @@ module.exports = class DocTypeInterfaceGenerator {
|
|
|
10
9
|
this.processedDoctypes = new Set()
|
|
11
10
|
this.existingInterfaces = {}
|
|
12
11
|
this.updatedInterfaces = 0
|
|
13
|
-
this.spinner = ora('Generating doctype interfaces...').start()
|
|
14
12
|
this.jsonFileCache = new Map()
|
|
13
|
+
this.summary = {
|
|
14
|
+
processed: 0,
|
|
15
|
+
updated: 0,
|
|
16
|
+
skipped: 0,
|
|
17
|
+
notFound: 0,
|
|
18
|
+
details: [],
|
|
19
|
+
}
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
async generate() {
|
|
@@ -25,6 +30,8 @@ module.exports = class DocTypeInterfaceGenerator {
|
|
|
25
30
|
}
|
|
26
31
|
await Promise.all(promises)
|
|
27
32
|
|
|
33
|
+
this.printSummary()
|
|
34
|
+
|
|
28
35
|
if (this.updatedInterfaces > 0) {
|
|
29
36
|
const baseInterfaces = this.generateBaseInterfaces()
|
|
30
37
|
const interfacesString = [
|
|
@@ -34,11 +41,24 @@ module.exports = class DocTypeInterfaceGenerator {
|
|
|
34
41
|
|
|
35
42
|
await fs.mkdir(path.dirname(this.outputPath), { recursive: true })
|
|
36
43
|
await fs.writeFile(this.outputPath, interfacesString)
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
printSummary() {
|
|
48
|
+
console.log('\nFrappe Type Generation Summary:')
|
|
49
|
+
console.log(`- Total processed: ${this.summary.processed} doctypes`)
|
|
50
|
+
console.log(`- Updated: ${this.summary.updated} interfaces`)
|
|
51
|
+
console.log(`- Skipped: ${this.summary.skipped} (no changes)`)
|
|
52
|
+
if (this.summary.notFound > 0) {
|
|
53
|
+
console.log(`- Not found: ${this.summary.notFound}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (this.updatedInterfaces > 0) {
|
|
57
|
+
console.log(
|
|
58
|
+
`\nOutput file updated with ${this.updatedInterfaces} interface${this.updatedInterfaces === 1 ? '' : 's'}.`,
|
|
39
59
|
)
|
|
40
60
|
} else {
|
|
41
|
-
|
|
61
|
+
console.log('\nNo new schema changes.')
|
|
42
62
|
}
|
|
43
63
|
}
|
|
44
64
|
|
|
@@ -69,10 +89,12 @@ module.exports = class DocTypeInterfaceGenerator {
|
|
|
69
89
|
return
|
|
70
90
|
}
|
|
71
91
|
this.processedDoctypes.add(doctypeName)
|
|
92
|
+
this.summary.processed++
|
|
72
93
|
|
|
73
94
|
const jsonFilePath = await this.findJsonFile(appName, doctypeName)
|
|
74
95
|
if (!jsonFilePath) {
|
|
75
|
-
this.
|
|
96
|
+
this.summary.notFound++
|
|
97
|
+
this.summary.details.push(`${doctypeName}: not found`)
|
|
76
98
|
return
|
|
77
99
|
}
|
|
78
100
|
const jsonData = JSON.parse(await fs.readFile(jsonFilePath, 'utf8'))
|
|
@@ -84,7 +106,8 @@ module.exports = class DocTypeInterfaceGenerator {
|
|
|
84
106
|
existingInterface &&
|
|
85
107
|
existingInterface.includes(`// Last updated: ${lastModified}`)
|
|
86
108
|
) {
|
|
87
|
-
this.
|
|
109
|
+
this.summary.skipped++
|
|
110
|
+
this.summary.details.push(`${doctypeName}: skipped (no changes)`)
|
|
88
111
|
return
|
|
89
112
|
}
|
|
90
113
|
|
|
@@ -166,7 +189,8 @@ module.exports = class DocTypeInterfaceGenerator {
|
|
|
166
189
|
interfaceString += `}\n`
|
|
167
190
|
this.updatedInterfaces++
|
|
168
191
|
this.existingInterfaces[interfaceName] = interfaceString
|
|
169
|
-
this.
|
|
192
|
+
this.summary.updated++
|
|
193
|
+
this.summary.details.push(`${doctypeName}: updated`)
|
|
170
194
|
}
|
|
171
195
|
|
|
172
196
|
async findJsonFile(appName, doctypeName) {
|
|
@@ -204,19 +228,21 @@ module.exports = class DocTypeInterfaceGenerator {
|
|
|
204
228
|
|
|
205
229
|
generateBaseInterfaces() {
|
|
206
230
|
return `interface DocType {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
231
|
+
name: string;
|
|
232
|
+
creation: string;
|
|
233
|
+
modified: string;
|
|
234
|
+
owner: string;
|
|
235
|
+
modified_by: string;
|
|
236
|
+
}
|
|
213
237
|
|
|
214
|
-
interface ChildDocType extends DocType {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
`
|
|
238
|
+
interface ChildDocType extends DocType {
|
|
239
|
+
parent?: string;
|
|
240
|
+
parentfield?: string;
|
|
241
|
+
parenttype?: string;
|
|
242
|
+
idx?: number;
|
|
243
|
+
}
|
|
244
|
+
`
|
|
221
245
|
}
|
|
222
246
|
}
|
|
247
|
+
|
|
248
|
+
exports.DocTypeInterfaceGenerator = DocTypeInterfaceGenerator
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const { getCommonSiteConfig } = require('./utils')
|
|
2
|
+
|
|
3
|
+
function frappeProxy({
|
|
4
|
+
port = 8080,
|
|
5
|
+
source = '^/(app|login|api|assets|files|private)',
|
|
6
|
+
} = {}) {
|
|
7
|
+
const commonSiteConfig = getCommonSiteConfig()
|
|
8
|
+
const webserver_port = commonSiteConfig
|
|
9
|
+
? commonSiteConfig.webserver_port
|
|
10
|
+
: 8000
|
|
11
|
+
if (!commonSiteConfig) {
|
|
12
|
+
console.log('No common_site_config.json found, using default port 8000')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let proxy = {}
|
|
16
|
+
proxy[source] = {
|
|
17
|
+
target: `http://127.0.0.1:${webserver_port}`,
|
|
18
|
+
ws: true,
|
|
19
|
+
router: function (req) {
|
|
20
|
+
const site_name = req.headers.host.split(':')[0]
|
|
21
|
+
return `http://${site_name}:${webserver_port}`
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
name: 'frappeui-proxy-plugin',
|
|
27
|
+
config: () => ({
|
|
28
|
+
server: {
|
|
29
|
+
port: port,
|
|
30
|
+
proxy: proxy,
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
exports.frappeProxy = frappeProxy
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const { spawn } = require('child_process')
|
|
3
|
+
|
|
4
|
+
function frappeTypes(options = {}) {
|
|
5
|
+
let childProcess = null
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
name: 'frappeui-types-plugin',
|
|
9
|
+
config: (config, { command, mode }) => {
|
|
10
|
+
if (mode === 'development') {
|
|
11
|
+
// Run the type generation in a separate process
|
|
12
|
+
const scriptPath = path.join(__dirname, 'generateTypes.js')
|
|
13
|
+
|
|
14
|
+
// Serialize options as a JSON string to pass to the child process
|
|
15
|
+
const optionsArg = JSON.stringify(options)
|
|
16
|
+
|
|
17
|
+
childProcess = spawn('node', [scriptPath, optionsArg], {
|
|
18
|
+
stdio: ['ignore', 'pipe', 'pipe'], // Ignore stdin, pipe stdout/stderr
|
|
19
|
+
detached: false,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
// Pipe stdout and stderr
|
|
23
|
+
childProcess.stdout.pipe(process.stdout)
|
|
24
|
+
childProcess.stderr.pipe(process.stderr)
|
|
25
|
+
|
|
26
|
+
// Handle child process errors
|
|
27
|
+
childProcess.on('error', (err) => {
|
|
28
|
+
console.error('Error in type generation process:', err)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const cleanup = () => {
|
|
32
|
+
if (childProcess && !childProcess.killed) {
|
|
33
|
+
try {
|
|
34
|
+
// Send SIGTERM signal
|
|
35
|
+
childProcess.kill('SIGTERM')
|
|
36
|
+
|
|
37
|
+
// Force kill if needed after a timeout
|
|
38
|
+
setTimeout(() => {
|
|
39
|
+
if (childProcess && !childProcess.killed) {
|
|
40
|
+
childProcess.kill('SIGKILL')
|
|
41
|
+
}
|
|
42
|
+
}, 500)
|
|
43
|
+
} catch (e) {
|
|
44
|
+
// Ignore errors during cleanup
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Register cleanup on exit signals and process exit
|
|
50
|
+
;['SIGINT', 'SIGTERM', 'exit'].forEach((signal) => {
|
|
51
|
+
process.once(signal, cleanup)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// Handle child process exit
|
|
55
|
+
childProcess.on('exit', (code, signal) => {
|
|
56
|
+
childProcess = null
|
|
57
|
+
if (code !== 0 && !signal) {
|
|
58
|
+
console.log(`Type generation process exited with code ${code}`)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
return {}
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
exports.frappeTypes = frappeTypes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const { findAppsFolder } = require('./utils')
|
|
3
|
+
const { DocTypeInterfaceGenerator } = require('./doctypeInterfaceGenerator')
|
|
4
|
+
|
|
5
|
+
// Handle termination signals to exit cleanly
|
|
6
|
+
process.on('SIGINT', () => process.exit(0))
|
|
7
|
+
process.on('SIGTERM', () => process.exit(0))
|
|
8
|
+
|
|
9
|
+
async function main() {
|
|
10
|
+
try {
|
|
11
|
+
// Parse options passed from the plugin
|
|
12
|
+
let options = {}
|
|
13
|
+
if (process.argv[2]) {
|
|
14
|
+
try {
|
|
15
|
+
options = JSON.parse(process.argv[2])
|
|
16
|
+
} catch (err) {
|
|
17
|
+
console.error('Error parsing plugin options:', err)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Use only the plugin options for configuration
|
|
22
|
+
if (!(options && options.input)) {
|
|
23
|
+
console.log('No type generation input specified in options')
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const frontendFolder = process.cwd()
|
|
28
|
+
let outputPath = options.output || 'src/types/doctypes.ts'
|
|
29
|
+
if (!path.isAbsolute(outputPath)) {
|
|
30
|
+
outputPath = path.join(frontendFolder, outputPath)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const appsFolder = findAppsFolder()
|
|
34
|
+
if (!appsFolder) {
|
|
35
|
+
console.error('Could not find frappe-bench/apps folder')
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const generator = new DocTypeInterfaceGenerator(
|
|
40
|
+
appsFolder,
|
|
41
|
+
options.input,
|
|
42
|
+
outputPath,
|
|
43
|
+
)
|
|
44
|
+
await generator.generate()
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('Error generating DocType interfaces:', error)
|
|
47
|
+
process.exit(1)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Execute if run directly
|
|
52
|
+
if (require.main === module) {
|
|
53
|
+
main()
|
|
54
|
+
.then(() => process.exit(0))
|
|
55
|
+
.catch((err) => {
|
|
56
|
+
console.error(err)
|
|
57
|
+
process.exit(1)
|
|
58
|
+
})
|
|
59
|
+
}
|
package/vite/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const { lucideIcons } = require('./lucideIcons')
|
|
2
|
+
const { frappeProxy } = require('./frappeProxy')
|
|
3
|
+
const { frappeTypes } = require('./frappeTypes')
|
|
4
|
+
const { jinjaBootData } = require('./jinjaBootData')
|
|
5
|
+
const { buildConfig } = require('./buildConfig')
|
|
6
|
+
|
|
7
|
+
function frappeuiPlugin(
|
|
8
|
+
options = {
|
|
9
|
+
lucideIcons: true,
|
|
10
|
+
frappeProxy: true,
|
|
11
|
+
frappeTypes: true,
|
|
12
|
+
jinjaBootData: true,
|
|
13
|
+
buildConfig: true,
|
|
14
|
+
},
|
|
15
|
+
) {
|
|
16
|
+
let plugins = []
|
|
17
|
+
if (options.lucideIcons) {
|
|
18
|
+
plugins.push(lucideIcons(options.lucideIcons))
|
|
19
|
+
}
|
|
20
|
+
if (options.frappeProxy) {
|
|
21
|
+
plugins.push(frappeProxy(options.frappeProxy))
|
|
22
|
+
}
|
|
23
|
+
if (options.frappeTypes) {
|
|
24
|
+
plugins.push(frappeTypes(options.frappeTypes))
|
|
25
|
+
}
|
|
26
|
+
if (options.jinjaBootData) {
|
|
27
|
+
plugins.push(jinjaBootData(options.jinjaBootData))
|
|
28
|
+
}
|
|
29
|
+
if (options.buildConfig) {
|
|
30
|
+
plugins.push(buildConfig(options.buildConfig))
|
|
31
|
+
}
|
|
32
|
+
return plugins
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = frappeuiPlugin
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
function jinjaBootData() {
|
|
2
|
+
return {
|
|
3
|
+
name: 'frappeui-jinja-boot-data-plugin',
|
|
4
|
+
transformIndexHtml(html, context) {
|
|
5
|
+
if (!context.server) {
|
|
6
|
+
// context.server is true in dev mode
|
|
7
|
+
// only inject this in production build
|
|
8
|
+
return html.replace(
|
|
9
|
+
/<\/body>/,
|
|
10
|
+
`
|
|
11
|
+
<script>
|
|
12
|
+
{% for key in boot %}
|
|
13
|
+
window["{{ key }}"] = {{ boot[key] | tojson }};
|
|
14
|
+
{% endfor %}
|
|
15
|
+
</script>
|
|
16
|
+
</body>
|
|
17
|
+
`,
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
return html
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = { jinjaBootData }
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const LucideIcons = require('lucide-static')
|
|
2
|
+
const Icons = require('unplugin-icons/vite')
|
|
3
|
+
const Components = require('unplugin-vue-components/vite')
|
|
4
|
+
const IconsResolver = require('unplugin-icons/resolver')
|
|
5
|
+
|
|
6
|
+
function lucideIconsPlugin() {
|
|
7
|
+
return [
|
|
8
|
+
Components.default({
|
|
9
|
+
resolvers: [
|
|
10
|
+
IconsResolver.default({
|
|
11
|
+
prefix: false,
|
|
12
|
+
enabledCollections: ['lucide'],
|
|
13
|
+
}),
|
|
14
|
+
],
|
|
15
|
+
}),
|
|
16
|
+
Icons.default({
|
|
17
|
+
customCollections: {
|
|
18
|
+
lucide: getIcons(),
|
|
19
|
+
},
|
|
20
|
+
}),
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getIcons() {
|
|
25
|
+
let icons = {}
|
|
26
|
+
for (const icon in LucideIcons) {
|
|
27
|
+
if (icon === 'default') {
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
let iconSvg = LucideIcons[icon]
|
|
31
|
+
|
|
32
|
+
// set stroke-width to 1.5
|
|
33
|
+
if (iconSvg && iconSvg.includes('stroke-width')) {
|
|
34
|
+
iconSvg = iconSvg.replace(/stroke-width="2"/g, 'stroke-width="1.5"')
|
|
35
|
+
}
|
|
36
|
+
icons[icon] = iconSvg
|
|
37
|
+
|
|
38
|
+
let dashKeys = camelToDash(icon)
|
|
39
|
+
for (let dashKey of dashKeys) {
|
|
40
|
+
if (dashKey !== icon) {
|
|
41
|
+
icons[dashKey] = iconSvg
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return icons
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function camelToDash(key) {
|
|
49
|
+
// barChart2 -> bar-chart-2
|
|
50
|
+
let withNumber = key.replace(/[A-Z0-9]/g, (m) => '-' + m.toLowerCase())
|
|
51
|
+
if (withNumber.startsWith('-')) {
|
|
52
|
+
withNumber = withNumber.substring(1)
|
|
53
|
+
}
|
|
54
|
+
// barChart2 -> bar-chart2
|
|
55
|
+
let withoutNumber = key.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase())
|
|
56
|
+
if (withoutNumber.startsWith('-')) {
|
|
57
|
+
withoutNumber = withoutNumber.substring(1)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (withNumber !== withoutNumber) {
|
|
61
|
+
// both are required because unplugin icon resolver doesn't put a dash before numbers
|
|
62
|
+
return [withNumber, withoutNumber]
|
|
63
|
+
}
|
|
64
|
+
return [withNumber]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
exports.lucideIcons = lucideIconsPlugin
|
package/vite/utils.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
|
|
4
|
+
function getConfig() {
|
|
5
|
+
let configPath = path.join(process.cwd(), 'frappeui.json')
|
|
6
|
+
if (fs.existsSync(configPath)) {
|
|
7
|
+
return JSON.parse(fs.readFileSync(configPath))
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getCommonSiteConfig() {
|
|
12
|
+
let currentDir = path.resolve('.')
|
|
13
|
+
// traverse up till we find frappe-bench with sites directory
|
|
14
|
+
while (currentDir !== '/') {
|
|
15
|
+
if (
|
|
16
|
+
fs.existsSync(path.join(currentDir, 'sites')) &&
|
|
17
|
+
fs.existsSync(path.join(currentDir, 'apps'))
|
|
18
|
+
) {
|
|
19
|
+
let configPath = path.join(currentDir, 'sites', 'common_site_config.json')
|
|
20
|
+
if (fs.existsSync(configPath)) {
|
|
21
|
+
return JSON.parse(fs.readFileSync(configPath))
|
|
22
|
+
}
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
currentDir = path.resolve(currentDir, '..')
|
|
26
|
+
}
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function findAppsFolder() {
|
|
31
|
+
let currentDir = process.cwd()
|
|
32
|
+
while (currentDir !== '/') {
|
|
33
|
+
if (
|
|
34
|
+
fs.existsSync(path.join(currentDir, 'apps')) &&
|
|
35
|
+
fs.existsSync(path.join(currentDir, 'sites'))
|
|
36
|
+
) {
|
|
37
|
+
return path.join(currentDir, 'apps')
|
|
38
|
+
}
|
|
39
|
+
currentDir = path.resolve(currentDir, '..')
|
|
40
|
+
}
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
getConfig,
|
|
46
|
+
getCommonSiteConfig,
|
|
47
|
+
findAppsFolder,
|
|
48
|
+
}
|
package/vite.js
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
const path = require('path')
|
|
2
|
-
const fs = require('fs')
|
|
3
|
-
const DocTypeInterfaceGenerator = require('./scripts/generateInterface')
|
|
4
|
-
|
|
5
|
-
module.exports = function proxyOptions({
|
|
6
|
-
port = 8080,
|
|
7
|
-
source = '^/(app|login|api|assets|files|private)',
|
|
8
|
-
enableDocTypeInterfaces = true,
|
|
9
|
-
} = {}) {
|
|
10
|
-
const commonSiteConfig = getCommonSiteConfig()
|
|
11
|
-
const webserver_port = commonSiteConfig
|
|
12
|
-
? commonSiteConfig.webserver_port
|
|
13
|
-
: 8000
|
|
14
|
-
if (!commonSiteConfig) {
|
|
15
|
-
console.log('No common_site_config.json found, using default port 8000')
|
|
16
|
-
}
|
|
17
|
-
let proxy = {}
|
|
18
|
-
proxy[source] = {
|
|
19
|
-
target: `http://127.0.0.1:${webserver_port}`,
|
|
20
|
-
ws: true,
|
|
21
|
-
router: function (req) {
|
|
22
|
-
const site_name = req.headers.host.split(':')[0]
|
|
23
|
-
return `http://${site_name}:${webserver_port}`
|
|
24
|
-
},
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
name: 'frappeui-vite-plugin',
|
|
29
|
-
config: async () => {
|
|
30
|
-
if (enableDocTypeInterfaces) {
|
|
31
|
-
await generateDocTypeInterfaces()
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
server: {
|
|
36
|
-
port: port,
|
|
37
|
-
proxy: proxy,
|
|
38
|
-
},
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function generateDocTypeInterfaces() {
|
|
45
|
-
const config = getConfig()
|
|
46
|
-
if (!(config && config.typeGeneration && config.typeGeneration.input)) return
|
|
47
|
-
|
|
48
|
-
const frontendFolder = process.cwd()
|
|
49
|
-
let outputPath = config.typeGeneration.output || 'src/types/doctypes.ts'
|
|
50
|
-
if (!path.isAbsolute(outputPath)) {
|
|
51
|
-
outputPath = path.join(frontendFolder, outputPath)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const appsFolder = findAppsFolder()
|
|
55
|
-
if (!appsFolder) {
|
|
56
|
-
console.error('Could not find frappe-bench/apps folder')
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const generator = new DocTypeInterfaceGenerator(
|
|
61
|
-
appsFolder,
|
|
62
|
-
config.typeGeneration.input,
|
|
63
|
-
outputPath,
|
|
64
|
-
)
|
|
65
|
-
await generator.generate()
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function getCommonSiteConfig() {
|
|
69
|
-
let currentDir = path.resolve('.')
|
|
70
|
-
// traverse up till we find frappe-bench with sites directory
|
|
71
|
-
while (currentDir !== '/') {
|
|
72
|
-
if (
|
|
73
|
-
fs.existsSync(path.join(currentDir, 'sites')) &&
|
|
74
|
-
fs.existsSync(path.join(currentDir, 'apps'))
|
|
75
|
-
) {
|
|
76
|
-
let configPath = path.join(currentDir, 'sites', 'common_site_config.json')
|
|
77
|
-
if (fs.existsSync(configPath)) {
|
|
78
|
-
return JSON.parse(fs.readFileSync(configPath))
|
|
79
|
-
}
|
|
80
|
-
return null
|
|
81
|
-
}
|
|
82
|
-
currentDir = path.resolve(currentDir, '..')
|
|
83
|
-
}
|
|
84
|
-
return null
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function findAppsFolder() {
|
|
88
|
-
let currentDir = process.cwd()
|
|
89
|
-
while (currentDir !== '/') {
|
|
90
|
-
if (
|
|
91
|
-
fs.existsSync(path.join(currentDir, 'apps')) &&
|
|
92
|
-
fs.existsSync(path.join(currentDir, 'sites'))
|
|
93
|
-
) {
|
|
94
|
-
return path.join(currentDir, 'apps')
|
|
95
|
-
}
|
|
96
|
-
currentDir = path.resolve(currentDir, '..')
|
|
97
|
-
}
|
|
98
|
-
return null
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function getConfig() {
|
|
102
|
-
let configPath = path.join(process.cwd(), 'frappeui.json')
|
|
103
|
-
if (fs.existsSync(configPath)) {
|
|
104
|
-
return JSON.parse(fs.readFileSync(configPath))
|
|
105
|
-
}
|
|
106
|
-
}
|