adminforth 1.0.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.
Files changed (58) hide show
  1. package/auth.js +66 -0
  2. package/dataConnectors/mongo.js +187 -0
  3. package/dataConnectors/postgres.js +283 -0
  4. package/dataConnectors/sqlite.js +257 -0
  5. package/index.js +722 -0
  6. package/modules/codeInjector.js +313 -0
  7. package/modules/utils.js +12 -0
  8. package/package.json +23 -0
  9. package/servers/express.js +217 -0
  10. package/spa/.eslintrc.cjs +14 -0
  11. package/spa/.vscode/extensions.json +6 -0
  12. package/spa/README.md +39 -0
  13. package/spa/env.d.ts +1 -0
  14. package/spa/index.html +23 -0
  15. package/spa/package-lock.json +4152 -0
  16. package/spa/package.json +40 -0
  17. package/spa/postcss.config.js +6 -0
  18. package/spa/public/favicon.ico +0 -0
  19. package/spa/src/App.vue +172 -0
  20. package/spa/src/assets/base.css +0 -0
  21. package/spa/src/assets/logo.svg +1 -0
  22. package/spa/src/components/AcceptModal.vue +52 -0
  23. package/spa/src/components/Breadcrumbs.vue +40 -0
  24. package/spa/src/components/BreadcrumbsWithButtons.vue +26 -0
  25. package/spa/src/components/CustomDateRangePicker.vue +218 -0
  26. package/spa/src/components/Dropdown.vue +154 -0
  27. package/spa/src/components/Filters.vue +141 -0
  28. package/spa/src/components/HelloWorld.vue +17 -0
  29. package/spa/src/components/MenuLink.vue +25 -0
  30. package/spa/src/components/ResourceForm.vue +198 -0
  31. package/spa/src/components/SingleSkeletLoader.vue +13 -0
  32. package/spa/src/components/ValueRenderer.vue +44 -0
  33. package/spa/src/components/icons/IconCalendar.vue +5 -0
  34. package/spa/src/components/icons/IconCommunity.vue +7 -0
  35. package/spa/src/components/icons/IconDocumentation.vue +7 -0
  36. package/spa/src/components/icons/IconEcosystem.vue +7 -0
  37. package/spa/src/components/icons/IconSupport.vue +7 -0
  38. package/spa/src/components/icons/IconTime.vue +5 -0
  39. package/spa/src/components/icons/IconTooling.vue +19 -0
  40. package/spa/src/index.scss +26 -0
  41. package/spa/src/main.ts +18 -0
  42. package/spa/src/router/index.ts +53 -0
  43. package/spa/src/stores/core.ts +135 -0
  44. package/spa/src/stores/modal.ts +38 -0
  45. package/spa/src/utils.ts +44 -0
  46. package/spa/src/views/CreateView.vue +103 -0
  47. package/spa/src/views/EditView.vue +95 -0
  48. package/spa/src/views/HomeView.vue +8 -0
  49. package/spa/src/views/ListView.vue +466 -0
  50. package/spa/src/views/LoginView.vue +122 -0
  51. package/spa/src/views/ResourceParent.vue +18 -0
  52. package/spa/src/views/ShowView.vue +94 -0
  53. package/spa/tailwind.config.js +12 -0
  54. package/spa/tsconfig.app.json +14 -0
  55. package/spa/tsconfig.json +11 -0
  56. package/spa/tsconfig.node.json +19 -0
  57. package/spa/vite.config.ts +42 -0
  58. package/types.js +34 -0
@@ -0,0 +1,313 @@
1
+ import fs from 'fs';
2
+ import fsExtra from 'fs-extra';
3
+ import filewatcher from 'filewatcher';
4
+ import { exec, spawn } from 'child_process';
5
+ import { promisify } from 'util';
6
+ import path from 'path';
7
+ import { fileURLToPath } from 'url';
8
+ import crypto from 'crypto';
9
+
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.join(path.dirname(__filename), '..');
13
+
14
+ const execAsync = promisify(exec);
15
+
16
+ function hashify(obj) {
17
+ return crypto.createHash
18
+ ('sha256').update(JSON.stringify(obj)).digest('hex');
19
+ }
20
+
21
+ class CodeInjector {
22
+ constructor(adminforth) {
23
+ this.adminforth = adminforth;
24
+ }
25
+
26
+ async runShell({command, verbose = false}) {
27
+ console.time(`Running ${command}...`);
28
+ const { stdout: out, stderr: err } = await execAsync(command);
29
+ console.timeEnd(`Running ${command}...`);
30
+ console.log(`Command ${command} output:`, out, err);
31
+ }
32
+
33
+ async runNpmShell({command, verbose = false, cwd}) {
34
+ const nodeBinary = process.execPath; // Path to the Node.js binary running this script
35
+ const npmPath = path.join(path.dirname(nodeBinary), 'npm'); // Path to the npm executable
36
+
37
+ const env = {
38
+ VUE_APP_ADMINFORTH_PUBLIC_PATH: this.adminforth.config.baseUrl,
39
+ FORCE_COLOR: '1',
40
+ ...process.env,
41
+ };
42
+
43
+ console.time(`Running npm ${command}...`);
44
+ const { stdout: out, stderr: err } = await execAsync(`${nodeBinary} ${npmPath} ${command}`, {
45
+ cwd,
46
+ env,
47
+ });
48
+ console.timeEnd(`Running npm ${command}...`);
49
+
50
+ if (verbose) {
51
+ console.log(`npm ${command} output:`, out);
52
+ }
53
+ if (err) {
54
+ console.error(`npm ${command} errors/warnings:`, err);
55
+ }
56
+ }
57
+
58
+ async rmTmpDir() {
59
+ // remove spa_tmp folder if it is exists
60
+ const spaTmpPath = path.join(__dirname, 'spa_tmp');
61
+ try {
62
+ await fs.promises.rm(spaTmpPath, { recursive: true });
63
+ } catch (e) {
64
+ // ignore
65
+ }
66
+ }
67
+
68
+ async prepareSources({ filesUpdated, verbose = false }) {
69
+ const spaTmpPath = path.join(__dirname, 'spa_tmp');
70
+
71
+ const customFiles = [];
72
+ const icons = [];
73
+ let routes = '';
74
+
75
+ const collectAssetsFromMenu = (menu) => {
76
+ menu.forEach((item) => {
77
+ if (item.icon) {
78
+ icons.push(item.icon);
79
+ }
80
+ if (item.component) {
81
+ customFiles.push(item.component);
82
+ routes += `{
83
+ path: '${item.path}',
84
+ name: '${item.path}',
85
+ component: import('@/custom/${item.component}'),
86
+ },\n`
87
+ }
88
+ if (item.children) {
89
+ collectAssetsFromMenu(item.children);
90
+ }
91
+ });
92
+ };
93
+ collectAssetsFromMenu(this.adminforth.config.menu);
94
+
95
+ if (this.adminforth.config.customization?.vueUsesFile) {
96
+ customFiles.push(this.adminforth.config.customization.vueUsesFile);
97
+ }
98
+
99
+ // create spa_tmp folder, or ignore if it exists
100
+ try {
101
+ await fs.promises.mkdir(spaTmpPath);
102
+ } catch (e) {
103
+ // ignore
104
+ }
105
+
106
+ if (filesUpdated) {
107
+ // copy only updated files
108
+ await Promise.all(filesUpdated.map(async (file) => {
109
+ const src = path.join(__dirname, 'spa', file);
110
+ const dest = path.join(spaTmpPath, file);
111
+ await fsExtra.copy(src, dest);
112
+ }));
113
+ } else {
114
+ await fsExtra.copy(path.join(__dirname, 'spa'), spaTmpPath, {
115
+ filter: (src) => {
116
+ return !src.includes('/node_modules') && !src.includes('/dist');
117
+ },
118
+ });
119
+
120
+ // copy custom files
121
+ await Promise.all(customFiles.map(async (file) => {
122
+ const src = path.join(file);
123
+ const dest = path.join(spaTmpPath, 'src', 'custom', file);
124
+ await fsExtra.copy(src, dest);
125
+ }))
126
+ }
127
+
128
+ //collect all 'icon' fields from resources bulkActions
129
+ this.adminforth.config.resources.forEach((resource) => {
130
+ if (resource.options?.bulkActions) {
131
+ resource.options.bulkActions.forEach((action) => {
132
+ if (action.icon) {
133
+ icons.push(action.icon);
134
+ }
135
+ });
136
+ }
137
+ });
138
+
139
+ const uniqueIcons = Array.from(new Set(icons));
140
+
141
+ // icons are collectionName:iconName. Get list of all unique collection names:
142
+ const collections = new Set(icons.map((icon) => icon.split(':')[0]));
143
+
144
+ // package names @iconify-prerendered/vue-<collection name>
145
+ const packageNames = Array.from(collections).map((collection) => `@iconify-prerendered/vue-${collection}`);
146
+
147
+ // for each icon generate import statement
148
+ const iconImports = uniqueIcons.map((icon) => {
149
+ const [ collection, iconName ] = icon.split(':');
150
+ const PascalIconName = 'Icon' + iconName.split('-').map((part, index) => {
151
+ return part[0].toUpperCase() + part.slice(1);
152
+ }).join('');
153
+ return `import { ${PascalIconName} } from '@iconify-prerendered/vue-${collection}';`;
154
+ }).join('\n');
155
+
156
+ // Generate Vue.component statements for each icon
157
+ const iconComponents = uniqueIcons.map((icon) => {
158
+ const [ collection, iconName ] = icon.split(':');
159
+ const PascalIconName = 'Icon' + iconName.split('-').map((part, index) => {
160
+ return part[0].toUpperCase() + part.slice(1);
161
+ }).join('');
162
+ return `app.component('${PascalIconName}', ${PascalIconName});`;
163
+ }).join('\n');
164
+
165
+ let imports = iconImports + '\n';
166
+
167
+ if (this.adminforth.config.customization?.vueUsesFile) {
168
+ imports += `import addCustomUses from '@/custom/${this.adminforth.config.customization.vueUsesFile}';\n`;
169
+ }
170
+
171
+ // inject that code into spa_tmp/src/App.vue
172
+ const appVuePath = path.join(spaTmpPath, 'src', 'main.ts');
173
+ let appVueContent = await fs.promises.readFile(appVuePath, 'utf-8');
174
+ appVueContent = appVueContent.replace('/* IMPORTANT:ADMINFORTH IMPORTS */', imports);
175
+ appVueContent = appVueContent.replace('/* IMPORTANT:ADMINFORTH COMPONENT REGISTRATIONS */', iconComponents + '\n' );
176
+ if (this.adminforth.config.customization?.vueUsesFile) {
177
+ appVueContent = appVueContent.replace('/* IMPORTANT:ADMINFORTH CUSTOM USES */', 'addCustomUses(app);');
178
+ }
179
+ await fs.promises.writeFile(appVuePath, appVueContent);
180
+
181
+
182
+ /* generate custom rotes */
183
+ const routerVuePath = path.join(spaTmpPath, 'src', 'router', 'index.ts');
184
+ let routerVueContent = await fs.promises.readFile(routerVuePath, 'utf-8');
185
+ routerVueContent = routerVueContent.replace('/* IMPORTANT:ADMINFORTH ROUTES */', routes);
186
+ await fs.promises.writeFile(routerVuePath, routerVueContent);
187
+
188
+
189
+ /* hash checking */
190
+ const packageLockPath = path.join(spaTmpPath, 'package-lock.json');
191
+ const packageLock = JSON.parse(await fs.promises.readFile(packageLockPath, 'utf-8'));
192
+ const lockHash = hashify(packageLock);
193
+ /* customPackageLock */
194
+ const customPackagePath = path.join('./package.json');
195
+ const customPackage = JSON.parse(await fs.promises.readFile(customPackagePath, 'utf-8'));
196
+ const customPackageHash = hashify(customPackage);
197
+
198
+ const customLockPath = path.join('./package-lock.json');
199
+ const customLock = JSON.parse(await fs.promises.readFile(customLockPath, 'utf-8'));
200
+ const customLockHash = hashify(customLock);
201
+
202
+ const packagesNamesHash = hashify(packageNames);
203
+
204
+ const fullHash = hashify([lockHash, packagesNamesHash, customLockHash]);
205
+ const hashPath = path.join(spaTmpPath, 'node_modules', '.adminforth_hash');
206
+
207
+ try {
208
+ const existingHash = await fs.promises.readFile(hashPath, 'utf-8');
209
+ if (existingHash === fullHash) {
210
+ console.log('Hashes match, skipping npm ci/install');
211
+ return;
212
+ }
213
+ } catch (e) {
214
+ // ignore
215
+ }
216
+
217
+ await this.runNpmShell({command: 'ci', verbose, cwd: spaTmpPath});
218
+
219
+ // get packages with version from customPackage
220
+ const customPackgeNames = [...Object.keys(customPackage.dependencies), ...Object.keys(customPackage.devDependencies || [])].reduce(
221
+ (acc, packageName) => {
222
+ const version = customLock.packages[`node_modules/${packageName}`].version;
223
+ acc.push(`${packageName}@${version}`);
224
+ return acc;
225
+ }, []);
226
+
227
+ if (packageNames.length) {
228
+ const npmInstallCommand = `install ${[...packageNames, ...customPackgeNames].join(' ')}`;
229
+ await this.runNpmShell({command: npmInstallCommand, cwd: spaTmpPath});
230
+ }
231
+
232
+ await fs.promises.writeFile(hashPath, fullHash);
233
+
234
+ }
235
+
236
+ async watchForReprepare() {
237
+ const spaPath = path.join(__dirname, 'spa');
238
+ // get list of all subdirectories in spa recursively
239
+ const directories = [];
240
+ const collectDirectories = async (dir) => {
241
+ const files = await fs.promises.readdir(dir, { withFileTypes: true });
242
+ for (const file of files) {
243
+ if (file.isDirectory() && ['node_modules', 'dist'].indexOf(file.name) === -1) {
244
+ directories.push(path.join(dir, file.name));
245
+ await collectDirectories(path.join(dir, file.name));
246
+ }
247
+ }
248
+ };
249
+ await collectDirectories(spaPath);
250
+
251
+ // console.log('👌👌Watching for changes in:', directories.join('\n '))
252
+
253
+ const watcher = filewatcher();
254
+ directories.forEach((dir) => {
255
+ watcher.add(dir);
256
+ });
257
+
258
+ watcher.on(
259
+ 'change',
260
+ async (file) => {
261
+ console.log(`File ${file} changed, preparing sources...`);
262
+ await this.prepareSources({ filesUpdated: [file.replace(spaPath + '/', '')] });
263
+ }
264
+ )
265
+ process.on('exit', () => {
266
+ watcher.removeAll();
267
+ });
268
+ }
269
+
270
+ async bundleNow({hotReload = false, verbose = false}) {
271
+ this.adminforth.config.runningHotReload = hotReload;
272
+
273
+ await this.prepareSources({ verbose });
274
+ await this.watchForReprepare();
275
+ console.log('AdminForth bundling');
276
+
277
+ const cwd = path.join(__dirname, 'spa_tmp');
278
+
279
+
280
+
281
+ if (!hotReload) {
282
+ await this.runNpmShell({command: 'run build', verbose, cwd});
283
+ } else {
284
+ const command = 'run dev';
285
+ console.time(`Running npm ${command}...`);
286
+ const nodeBinary = process.execPath;
287
+ const npmPath = path.join(path.dirname(nodeBinary), 'npm');
288
+ const env = {
289
+ VUE_APP_ADMINFORTH_PUBLIC_PATH: this.adminforth.config.baseUrl,
290
+ FORCE_COLOR: '1',
291
+ ...process.env,
292
+ };
293
+
294
+ const devServer = spawn(`${nodeBinary}`, [`${npmPath}`, ...command.split(' ')], {
295
+ cwd,
296
+ env,
297
+ });
298
+ devServer.stdout.on('data', (data) => {
299
+ console.log(`[AdminForth SPA]:`);
300
+ process.stdout.write(data);
301
+ });
302
+ devServer.stderr.on('data', (data) => {
303
+ console.error(`[AdminForth SPA ERR]:`);
304
+ process.stdout.write(data);
305
+
306
+ });
307
+
308
+ console.timeEnd(`Running npm ${command}...`);
309
+ }
310
+ }
311
+ }
312
+
313
+ export default CodeInjector;
@@ -0,0 +1,12 @@
1
+
2
+
3
+ export function guessLabelFromName(name) {
4
+ if (name.includes('_')) {
5
+ return name.split('_').map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(' ');
6
+ } else if (name.includes('-')) {
7
+ return name.split('-').map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(' ');
8
+ } else {
9
+ // split by capital letters
10
+ return name.split(/(?=[A-Z])/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(' ');
11
+ }
12
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "adminforth",
3
+ "version": "1.0.0",
4
+ "description": "OpenSource Vue3 powered forth-generation admin panel",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "author": "devforth.io",
10
+ "license": "ISC",
11
+ "type": "module",
12
+ "dependencies": {
13
+ "better-sqlite3": "^10.0.0",
14
+ "crypto": "^1.0.1",
15
+ "dayjs": "^1.11.11",
16
+ "filewatcher": "^3.0.1",
17
+ "fs-extra": "^11.2.0",
18
+ "jsonwebtoken": "^9.0.2",
19
+ "mongodb": "6.6",
20
+ "pg": "^8.11.5",
21
+ "uuid": "^9.0.1"
22
+ }
23
+ }
@@ -0,0 +1,217 @@
1
+
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import fs from 'fs';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+
10
+ function replaceAtStart(string, substring) {
11
+ if (string.startsWith(substring)) {
12
+ return string.slice(substring.length);
13
+ }
14
+ return string;
15
+ }
16
+
17
+ async function proxyTo(url, res) {
18
+ const r = await fetch(url);
19
+ const body = await r.text();
20
+ res.status(r.status);
21
+ r.headers.forEach((value, name) => {
22
+ res.setHeader(name, value);
23
+ });
24
+ res.send(body);
25
+ }
26
+
27
+ async function parseExpressCookie(req) {
28
+ const cookies = req.headers.cookie;
29
+ if (!cookies) {
30
+ return {};
31
+ }
32
+ const parts = cookies.split('; ');
33
+ const result = {};
34
+ parts.forEach(part => {
35
+ const [key, value] = part.split('=');
36
+ result[key] = value;
37
+ });
38
+ return result;
39
+ }
40
+
41
+
42
+
43
+ const respondNoServer = (title, explanation) => {
44
+ return `
45
+ <!DOCTYPE html>
46
+ <html lang="en">
47
+ <head>
48
+ <meta charset="UTF-8">
49
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
50
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
51
+ <title>AdminForth</title>
52
+ </head>
53
+ <body>
54
+ <div class="center">
55
+ <h1>Oops!</h1>
56
+ <h2>${title}</h2>
57
+ <p>${explanation}</p>
58
+ </div>
59
+ <style>
60
+ body {
61
+ font-family: Arial, sans-serif;
62
+ background-color: #f0f0f0;
63
+ margin: 0;
64
+ padding: 0;
65
+ }
66
+ .center {
67
+ display: flex;
68
+ justify-content: center;
69
+ align-items: center;
70
+ height: 100vh;
71
+ flex-direction: column;
72
+ }
73
+ </style>
74
+ <script>
75
+ setTimeout(() => {
76
+ location.reload();
77
+ }, 1500);
78
+ </script>
79
+ </body>
80
+ `;
81
+ }
82
+ class ExpressServer {
83
+ constructor(adminforth) {
84
+ this.adminforth = adminforth;
85
+ }
86
+
87
+ setupSpaServer() {
88
+ const prefix = this.adminforth.config.baseUrl
89
+
90
+ const slashedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
91
+
92
+ if (this.adminforth.config.runningHotReload) {
93
+ const handler = async (req, res) => {
94
+ // proxy using fetch to webpack dev server
95
+ try {
96
+ await proxyTo(`http://localhost:5173${req.url}`, res);
97
+ } catch (e) {
98
+ res.status(500).send(respondNoServer('AdminForth SPA is not ready yet', 'Vite is still starting up. Please wait a moment...'));
99
+ return;
100
+ }
101
+ }
102
+ this.expressApp.get(`${slashedPrefix}assets/*`, handler);
103
+ this.expressApp.get(`${prefix}*`, handler);
104
+
105
+ } else {
106
+ this.expressApp.get(`${slashedPrefix}assets/*`, (req, res) => {
107
+ res.sendFile(path.join(__dirname, '..', 'spa', 'dist', replaceAtStart(req.url, prefix)))
108
+ })
109
+
110
+ this.expressApp.get(`${prefix}*`, async (req, res) => {
111
+ const fullPath = path.join(__dirname, '..', 'spa', 'dist', 'index.html');
112
+
113
+ let fileExists = true;
114
+ try {
115
+ await fs.promises.access(fullPath, fs.constants.F_OK);
116
+ } catch (e) {
117
+ fileExists = false;
118
+ }
119
+ console.log('fileExists', fileExists);
120
+ if (!fileExists) {
121
+ res.status(500).send(respondNoServer(`${this.adminforth.config.brandName} is still warming up`, 'Please wait a moment...'));
122
+ return;
123
+ }
124
+ res.sendFile(fullPath);
125
+ });
126
+ }
127
+ }
128
+
129
+ serve(app) {
130
+ this.expressApp = app;
131
+ this.adminforth.setupEndpoints(this);
132
+ this.setupSpaServer();
133
+ }
134
+
135
+ authorize(handler) {
136
+ return async (req, res, next) => {
137
+ const cookies = await parseExpressCookie(req);
138
+ const jwt = cookies['adminforth_jwt'];
139
+ if (!jwt) {
140
+ res.status(401).send('Unauthorized by AdminForth');
141
+ return
142
+ }
143
+ const adminforthUser = this.adminforth.auth.verify(jwt);
144
+ if (!adminforthUser) {
145
+ res.status(401).send('Unauthorized by AdminForth');
146
+ } else {
147
+ req.adminUser = adminforthUser;
148
+ handler(req, res, next);
149
+ }
150
+ };
151
+ }
152
+
153
+ endpoint({ method='GET', path, handler, noAuth=false }) {
154
+ if (!path.startsWith('/')) {
155
+ throw new Error(`Path must start with /, got: ${path}`);
156
+ }
157
+ const fullPath = `${this.adminforth.config.baseUrl}/adminapi/v1${path}`;
158
+
159
+ const expressHandler = async (req, res) => {
160
+ let body = req.body || {};
161
+ if (typeof body === 'string') {
162
+ try {
163
+ body = JSON.parse(body);
164
+ } catch (e) {
165
+ console.error('Failed to parse body', e);
166
+ res.status(400).send('Invalid JSON body');
167
+ }
168
+ }
169
+
170
+ const query = req.query;
171
+ const adminUser = req.adminUser;
172
+ const headers = req.headers;
173
+ const cookies = await parseExpressCookie(req);
174
+
175
+ const response = {
176
+ headers: {},
177
+ status: 200,
178
+ message: undefined,
179
+ setHeader(name, value) {
180
+ this.headers[name] = value;
181
+ },
182
+ setStatus(code, message) {
183
+ this.status = code;
184
+ this.message = message;
185
+ }
186
+ };
187
+ const input = { body, query, headers, cookies, response, _raw_express_req: req, _raw_express_res: res};
188
+
189
+ let output;
190
+ try {
191
+ output = await handler(input);
192
+ } catch (e) {
193
+ console.error('Error in handler', e);
194
+ // print full stack trace
195
+ console.error(e.stack);
196
+
197
+ res.status(500).send('Internal server error');
198
+ return;
199
+ }
200
+ Object.keys(response.headers).forEach((name) => {
201
+ res.setHeader(name, response.headers[name]);
202
+ })
203
+ const resp = res.status(response.status);
204
+ if (response.message) {
205
+ resp.send(response.message);
206
+ return;
207
+ }
208
+ res.json(output);
209
+ }
210
+
211
+ console.log(`Adding endpoint ${method} ${fullPath}`);
212
+ this.expressApp[method.toLowerCase()](fullPath, noAuth ? expressHandler : this.authorize(expressHandler));
213
+ }
214
+
215
+ }
216
+
217
+ export default ExpressServer;
@@ -0,0 +1,14 @@
1
+ /* eslint-env node */
2
+ require('@rushstack/eslint-patch/modern-module-resolution')
3
+
4
+ module.exports = {
5
+ root: true,
6
+ 'extends': [
7
+ 'plugin:vue/vue3-essential',
8
+ 'eslint:recommended',
9
+ '@vue/eslint-config-typescript'
10
+ ],
11
+ parserOptions: {
12
+ ecmaVersion: 'latest'
13
+ }
14
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "recommendations": [
3
+ "Vue.volar",
4
+ "dbaeumer.vscode-eslint"
5
+ ]
6
+ }
package/spa/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # spa
2
+
3
+ This template should help get you started developing with Vue 3 in Vite.
4
+
5
+ ## Recommended IDE Setup
6
+
7
+ [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
8
+
9
+ ## Type Support for `.vue` Imports in TS
10
+
11
+ TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
12
+
13
+ ## Customize configuration
14
+
15
+ See [Vite Configuration Reference](https://vitejs.dev/config/).
16
+
17
+ ## Project Setup
18
+
19
+ ```sh
20
+ npm install
21
+ ```
22
+
23
+ ### Compile and Hot-Reload for Development
24
+
25
+ ```sh
26
+ npm run dev
27
+ ```
28
+
29
+ ### Type-Check, Compile and Minify for Production
30
+
31
+ ```sh
32
+ npm run build
33
+ ```
34
+
35
+ ### Lint with [ESLint](https://eslint.org/)
36
+
37
+ ```sh
38
+ npm run lint
39
+ ```
package/spa/env.d.ts ADDED
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
package/spa/index.html ADDED
@@ -0,0 +1,23 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <link rel="icon" href="/favicon.ico">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Vite App</title>
8
+ <!--
9
+ <script>
10
+ // On page load or when changing themes, best to add inline in `head` to avoid FOUC
11
+ if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
12
+ document.documentElement.classList.add('dark');
13
+ } else {
14
+ document.documentElement.classList.remove('dark')
15
+ }
16
+ </script> -->
17
+
18
+ </head>
19
+ <body>
20
+ <div id="app"></div>
21
+ <script type="module" src="/src/main.ts"></script>
22
+ </body>
23
+ </html>