create-lupine 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.
- package/index.js +277 -0
- package/package.json +18 -0
- package/templates/common/.env +57 -0
- package/templates/common/.env.development +7 -0
- package/templates/common/.env.mobile +10 -0
- package/templates/common/.env.production +7 -0
- package/templates/common/apps/server/src/app-loader.ts +41 -0
- package/templates/common/apps/server/src/fetch-data.ts +20 -0
- package/templates/common/apps/server/src/index.ts +66 -0
- package/templates/common/apps/server/src/server-env-keys.ts +22 -0
- package/templates/common/dev/dev-watch.js +422 -0
- package/templates/doc-starter/api/resources/config_default.json +6 -0
- package/templates/doc-starter/api/resources/install.sqlite.sql +4 -0
- package/templates/doc-starter/api/src/index.ts +15 -0
- package/templates/doc-starter/api/src/resources/config_default.json +6 -0
- package/templates/doc-starter/api/src/resources/install.sqlite.sql +4 -0
- package/templates/doc-starter/lupine.json +33 -0
- package/templates/doc-starter/package.json +13 -0
- package/templates/doc-starter/web/assets/android-chrome-192x192.png +0 -0
- package/templates/doc-starter/web/assets/apple-touch-icon.png +0 -0
- package/templates/doc-starter/web/assets/favicon-16x16.png +0 -0
- package/templates/doc-starter/web/assets/favicon-32x32.png +0 -0
- package/templates/doc-starter/web/assets/favicon.ico +0 -0
- package/templates/doc-starter/web/assets/site.webmanifest +14 -0
- package/templates/doc-starter/web/github-pj-name/index.html +21 -0
- package/templates/doc-starter/web/github-pj-name/index.tsx +35 -0
- package/templates/doc-starter/web/markdown/en/essentials/index.md +6 -0
- package/templates/doc-starter/web/markdown/en/essentials/list.md +18 -0
- package/templates/doc-starter/web/markdown/en/guide/install.md +18 -0
- package/templates/doc-starter/web/markdown/en/guide/started.md +22 -0
- package/templates/doc-starter/web/markdown/en/index.md +42 -0
- package/templates/doc-starter/web/markdown/index.md +7 -0
- package/templates/doc-starter/web/markdown/zh/essentials/index.md +6 -0
- package/templates/doc-starter/web/markdown/zh/essentials/list.md +18 -0
- package/templates/doc-starter/web/markdown/zh/guide/install.md +18 -0
- package/templates/doc-starter/web/markdown/zh/guide/started.md +22 -0
- package/templates/doc-starter/web/markdown/zh/index.md +42 -0
- package/templates/doc-starter/web/src/client-env-keys.ts +5 -0
- package/templates/doc-starter/web/src/index.html +21 -0
- package/templates/doc-starter/web/src/index.tsx +33 -0
- package/templates/doc-starter/web/src/markdown-built/en/essentials/index.html +0 -0
- package/templates/doc-starter/web/src/markdown-built/en/essentials/list.html +8 -0
- package/templates/doc-starter/web/src/markdown-built/en/guide/install.html +8 -0
- package/templates/doc-starter/web/src/markdown-built/en/guide/started.html +12 -0
- package/templates/doc-starter/web/src/markdown-built/en/index.html +0 -0
- package/templates/doc-starter/web/src/markdown-built/index.html +0 -0
- package/templates/doc-starter/web/src/markdown-built/markdown-config.ts +25 -0
- package/templates/doc-starter/web/src/markdown-built/zh/essentials/index.html +0 -0
- package/templates/doc-starter/web/src/markdown-built/zh/essentials/list.html +8 -0
- package/templates/doc-starter/web/src/markdown-built/zh/guide/install.html +8 -0
- package/templates/doc-starter/web/src/markdown-built/zh/guide/started.html +12 -0
- package/templates/doc-starter/web/src/markdown-built/zh/index.html +0 -0
- package/templates/doc-starter/web/src/styles/base-css.ts +15 -0
- package/templates/hello-world/api/resources/config_default.json +6 -0
- package/templates/hello-world/api/resources/install.sqlite.sql +4 -0
- package/templates/hello-world/api/src/index.ts +4 -0
- package/templates/hello-world/api/src/service/root-api.ts +18 -0
- package/templates/hello-world/lupine.json +23 -0
- package/templates/hello-world/web/assets/favicon.ico +0 -0
- package/templates/hello-world/web/package.json +6 -0
- package/templates/hello-world/web/src/index.html +16 -0
- package/templates/hello-world/web/src/index.tsx +30 -0
package/index.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
|
|
8
|
+
function generateRandomString(length) {
|
|
9
|
+
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_!@&?-#$';
|
|
10
|
+
return [...crypto.randomBytes(length)].map((x) => chars[x % chars.length]).join('');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const red = (str) => `\x1b[31m${str}\x1b[0m`;
|
|
14
|
+
const green = (str) => `\x1b[32m${str}\x1b[0m`;
|
|
15
|
+
const bold = (str) => `\x1b[1m${str}\x1b[0m`;
|
|
16
|
+
|
|
17
|
+
const argv = process.argv.slice(2).reduce(
|
|
18
|
+
(acc, arg) => {
|
|
19
|
+
if (arg.startsWith('--template=')) {
|
|
20
|
+
acc.template = arg.split('=')[1];
|
|
21
|
+
} else if (arg.startsWith('--')) {
|
|
22
|
+
acc[arg.slice(2)] = true;
|
|
23
|
+
} else {
|
|
24
|
+
acc._.push(arg);
|
|
25
|
+
}
|
|
26
|
+
return acc;
|
|
27
|
+
},
|
|
28
|
+
{ _: [] }
|
|
29
|
+
);
|
|
30
|
+
// Handle -t alias manually if needed, or just support --template
|
|
31
|
+
// The original code supported `argv.t`
|
|
32
|
+
const args = process.argv.slice(2);
|
|
33
|
+
for (let i = 0; i < args.length; i++) {
|
|
34
|
+
if (args[i] === '-t' && args[i + 1]) {
|
|
35
|
+
argv.template = args[i + 1];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const cwd = process.cwd();
|
|
39
|
+
|
|
40
|
+
const TEMPLATES = [
|
|
41
|
+
{
|
|
42
|
+
name: 'hello-world',
|
|
43
|
+
display: 'Hello World',
|
|
44
|
+
itemType: 'frontend',
|
|
45
|
+
color: green,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'doc-starter',
|
|
49
|
+
display: 'Documentation Starter',
|
|
50
|
+
itemType: 'frontend',
|
|
51
|
+
color: green,
|
|
52
|
+
needsPress: true,
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const renameFiles = {
|
|
57
|
+
_gitignore: '.gitignore',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
async function init() {
|
|
61
|
+
let targetDir = argv._[0];
|
|
62
|
+
let template = argv.template || argv.t;
|
|
63
|
+
|
|
64
|
+
const defaultTargetDir = 'lupine-project';
|
|
65
|
+
|
|
66
|
+
function isValidPackageName(projectName) {
|
|
67
|
+
return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(projectName);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function toValidPackageName(projectName) {
|
|
71
|
+
return projectName
|
|
72
|
+
.trim()
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.replace(/\s+/g, '-')
|
|
75
|
+
.replace(/^[._]/, '')
|
|
76
|
+
.replace(/[^a-z0-9-~]+/g, '-');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!targetDir) {
|
|
80
|
+
targetDir = (await prompt(`Project name: (${green(defaultTargetDir)}) `)) || defaultTargetDir;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
while (!isValidPackageName(targetDir)) {
|
|
84
|
+
console.log(red(`✖ Invalid project name: "${targetDir}". Name can not contain spaces or sensitive characters.`));
|
|
85
|
+
const suggestion = toValidPackageName(targetDir);
|
|
86
|
+
targetDir = (await prompt(`Project name: (${green(suggestion)}) `)) || suggestion;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const root = path.join(cwd, targetDir);
|
|
90
|
+
|
|
91
|
+
if (fs.existsSync(root) && !isEmpty(root)) {
|
|
92
|
+
const overwrite = await prompt(
|
|
93
|
+
`Target directory "${green(targetDir)}" is not empty. Remove existing files and continue? (y/N) `
|
|
94
|
+
);
|
|
95
|
+
if (overwrite.toLowerCase() !== 'y') {
|
|
96
|
+
throw new Error(red('✖ Operation cancelled'));
|
|
97
|
+
}
|
|
98
|
+
emptyDir(root);
|
|
99
|
+
} else if (!fs.existsSync(root)) {
|
|
100
|
+
fs.mkdirSync(root, { recursive: true });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!template || !TEMPLATES.find((t) => t.name === template)) {
|
|
104
|
+
if (template && !TEMPLATES.find((t) => t.name === template)) {
|
|
105
|
+
console.log(`"${template}" isn't a valid template. Please choose from below:`);
|
|
106
|
+
} else {
|
|
107
|
+
console.log('Select a template:');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
TEMPLATES.forEach((t, i) => {
|
|
111
|
+
console.log(` ${t.color(i + 1 + '. ' + (t.display || t.name))}`);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const templateIdx = await prompt(`Select a template (${green('1-' + TEMPLATES.length)}): `);
|
|
115
|
+
const selected = TEMPLATES[parseInt(templateIdx) - 1];
|
|
116
|
+
if (!selected) {
|
|
117
|
+
throw new Error(red('✖ Invalid template selected'));
|
|
118
|
+
}
|
|
119
|
+
template = selected.name;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const templateDir = path.resolve(fileURLToPath(import.meta.url), '../templates', template);
|
|
123
|
+
const commonDir = path.resolve(fileURLToPath(import.meta.url), '../templates', 'common');
|
|
124
|
+
|
|
125
|
+
// ... (copy logic remains same)
|
|
126
|
+
|
|
127
|
+
console.log(`\nScaffolding project in ${root}...`);
|
|
128
|
+
|
|
129
|
+
copyDir(commonDir, root);
|
|
130
|
+
|
|
131
|
+
const appsDir = path.join(root, 'apps');
|
|
132
|
+
if (!fs.existsSync(appsDir)) {
|
|
133
|
+
fs.mkdirSync(appsDir);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const appName = path.basename(root);
|
|
137
|
+
const targetAppDir = path.join(appsDir, appName);
|
|
138
|
+
|
|
139
|
+
copyDir(templateDir, targetAppDir);
|
|
140
|
+
|
|
141
|
+
const lupineJsonPath = path.join(targetAppDir, 'lupine.json');
|
|
142
|
+
if (fs.existsSync(lupineJsonPath)) {
|
|
143
|
+
const lupineJson = JSON.parse(fs.readFileSync(lupineJsonPath, 'utf-8'));
|
|
144
|
+
lupineJson.name = appName;
|
|
145
|
+
fs.writeFileSync(lupineJsonPath, JSON.stringify(lupineJson, null, 2));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const pkg = {
|
|
149
|
+
name: appName,
|
|
150
|
+
version: '0.0.0',
|
|
151
|
+
private: true,
|
|
152
|
+
workspaces: ['apps/*', 'apps/*/*', 'packages/*'],
|
|
153
|
+
scripts: {
|
|
154
|
+
'app1:build-win': `electron-builder --win --config apps/${appName}/electron/builder.json`,
|
|
155
|
+
'app1:build-linux': `electron-builder --linux --config apps/${appName}/electron/builder.json`,
|
|
156
|
+
'app1:build-mac': `export CSC_IDENTITY_AUTO_DISCOVERY=true && electron-builder --mac --x64 --config apps/${appName}/electron/builder.json`,
|
|
157
|
+
'app1:unpack-mac': `npx asar extract dist/build/mac-arm64-unpacked/${appName}.app/Contents/Resources/app.asar dist/build/mac-arm64-unpacked/${appName}.app/Contents/Resources/app.asar-unpack`,
|
|
158
|
+
'app1:unpack-linux': `npx asar extract dist/build/linux-arm64-unpacked/resources/app.asar dist/build/linux-arm64-unpacked/resources/app.asar-unpack`,
|
|
159
|
+
'app1:unpack-win': `npx asar extract dist/build/win-unpacked/resources/app.asar dist/build/win-unpacked/resources/app.asar-unpack`,
|
|
160
|
+
'app1:desktop': `electron apps/${appName}/electron/main.js`,
|
|
161
|
+
'app1:sync': `npm run sync --workspace=${appName}-web`,
|
|
162
|
+
'app1:open-ios': `npm run open-ios --workspace=${appName}-web`,
|
|
163
|
+
'app1:open-android': `npm run open-android --workspace=${appName}-web`,
|
|
164
|
+
dev: 'node ./dev/dev-watch --env=.env.development --dev=1 --cmd=start-dev',
|
|
165
|
+
build: 'node ./dev/dev-watch --env=.env.production --dev=0',
|
|
166
|
+
'build-mobile': 'node ./dev/dev-watch --env=.env.mobile --dev=0 --mobile=1',
|
|
167
|
+
'start-dev': 'node dist/server_root/server/app-loader.js --env=.env.development',
|
|
168
|
+
'start-production': 'node dist/server_root/server/app-loader.js --env=.env.production',
|
|
169
|
+
format: 'prettier --write "**/*.{js,json,css,scss,md,html,yaml,ts,jsx,tsx}"',
|
|
170
|
+
},
|
|
171
|
+
dependencies: {
|
|
172
|
+
'lupine.api': 'file:packages/lupine.api',
|
|
173
|
+
},
|
|
174
|
+
devDependencies: {
|
|
175
|
+
esbuild: '^0.24.2',
|
|
176
|
+
electron: '^35.2.0',
|
|
177
|
+
'electron-builder': '^26.0.12',
|
|
178
|
+
typescript: '^5.7.2',
|
|
179
|
+
'@types/node': '^22.10.5',
|
|
180
|
+
prettier: '^2.7.1',
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
pkg.dependencies = {
|
|
185
|
+
'lupine.api': '^1.0.1',
|
|
186
|
+
'lupine.components': '^1.0.1',
|
|
187
|
+
'lupine.web': '^1.0.1',
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const templateObj = TEMPLATES.find((t) => t.name === template);
|
|
191
|
+
if (templateObj && templateObj.needsPress) {
|
|
192
|
+
pkg.dependencies['lupine.press'] = '^1.0.1';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify(pkg, null, 2));
|
|
196
|
+
|
|
197
|
+
const gitignorePath = path.join(root, '_gitignore');
|
|
198
|
+
if (fs.existsSync(gitignorePath)) {
|
|
199
|
+
fs.renameSync(gitignorePath, path.join(root, '.gitignore'));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (fs.existsSync(path.join(root, '.env'))) {
|
|
203
|
+
let envContent = fs.readFileSync(path.join(root, '.env'), 'utf-8');
|
|
204
|
+
|
|
205
|
+
// Auto-generate passwords
|
|
206
|
+
const keysToUpdate = ['ADMIN_PASS=', 'CRYPTO_KEY=', 'DEV_ADMIN_PASS=', 'DEV_CRYPTO_KEY='];
|
|
207
|
+
keysToUpdate.forEach((key) => {
|
|
208
|
+
// Replace line starting with key=
|
|
209
|
+
envContent = envContent.replace(new RegExp(`^${key}.*`, 'gm'), `${key}${generateRandomString(32)}`);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
envContent = envContent.replace(/{APP-NAME}/g, appName);
|
|
213
|
+
fs.writeFileSync(path.join(root, '.env'), envContent);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
console.log(`\nDone. Now run:\n`);
|
|
217
|
+
console.log(` cd ${targetDir}`);
|
|
218
|
+
console.log(` npm install`);
|
|
219
|
+
console.log(` npm run dev`);
|
|
220
|
+
console.log(``);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
import readline from 'node:readline';
|
|
224
|
+
|
|
225
|
+
function prompt(question) {
|
|
226
|
+
const rl = readline.createInterface({
|
|
227
|
+
input: process.stdin,
|
|
228
|
+
output: process.stdout,
|
|
229
|
+
});
|
|
230
|
+
return new Promise((resolve) => {
|
|
231
|
+
rl.question(question, (answer) => {
|
|
232
|
+
rl.close();
|
|
233
|
+
resolve(answer.trim());
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function copy(src, dest) {
|
|
239
|
+
const stat = fs.statSync(src);
|
|
240
|
+
if (stat.isDirectory()) {
|
|
241
|
+
copyDir(src, dest);
|
|
242
|
+
} else {
|
|
243
|
+
fs.copyFileSync(src, dest);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function copyDir(srcDir, destDir) {
|
|
248
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
249
|
+
for (const file of fs.readdirSync(srcDir)) {
|
|
250
|
+
const srcFile = path.resolve(srcDir, file);
|
|
251
|
+
const destFile = path.resolve(destDir, file);
|
|
252
|
+
copy(srcFile, destFile);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function isEmpty(path) {
|
|
257
|
+
const files = fs.readdirSync(path);
|
|
258
|
+
return files.length === 0 || (files.length === 1 && files[0] === '.git');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function emptyDir(dir) {
|
|
262
|
+
if (!fs.existsSync(dir)) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
for (const file of fs.readdirSync(dir)) {
|
|
266
|
+
const abs = path.resolve(dir, file);
|
|
267
|
+
if (file === '.git') {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
fs.rmSync(abs, { recursive: true, force: true });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
init().catch((e) => {
|
|
275
|
+
console.error(e.message);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-lupine",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Scaffolding tool for Lupine.js projects",
|
|
5
|
+
"bin": {
|
|
6
|
+
"create-lupine": "index.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"index.js",
|
|
10
|
+
"templates"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"npm-publish": "npm publish --access public",
|
|
14
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {},
|
|
17
|
+
"type": "module"
|
|
18
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# start with WEB. will be exposed to the frontend file
|
|
2
|
+
WEB.API_PORT=11080
|
|
3
|
+
WEB.API_BASE_URL=
|
|
4
|
+
|
|
5
|
+
# start with “app_name.” will be assigned to the individual app's env object
|
|
6
|
+
# demo.app.WEB.test2=11aa
|
|
7
|
+
# demo.app.test2=11aa
|
|
8
|
+
|
|
9
|
+
# used by Server Side Renderring for fetch (can't be localhost)
|
|
10
|
+
API_BASE_URL=http://127.0.0.1:11080
|
|
11
|
+
HTTP_PORT=11080
|
|
12
|
+
HTTPS_PORT=11083
|
|
13
|
+
#'127.0.0.1' or for all ips: "::"
|
|
14
|
+
BIND_IP=0.0.0.0
|
|
15
|
+
|
|
16
|
+
NODE_TLS_REJECT_UNAUTHORIZED=0
|
|
17
|
+
|
|
18
|
+
LOG_FOLDER=./dist/log
|
|
19
|
+
LOG_FILENAME=log-%index%.log
|
|
20
|
+
# size can be ?kb or ?mb
|
|
21
|
+
LOG_MAX_SIZE=1mb
|
|
22
|
+
LOG_MAX_COUNT=5
|
|
23
|
+
LOG_OUT_TO_FILE=true
|
|
24
|
+
LOG_OUT_TO_CONSOLE=true
|
|
25
|
+
|
|
26
|
+
# use following command to generate random keys
|
|
27
|
+
#node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"
|
|
28
|
+
# use following command to generate more complex strings
|
|
29
|
+
#node -e "l=32;c='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_!@&?-#$';console.log([...require('node:crypto').randomBytes(l)].map(x => c[x%c.length]).join(''))"
|
|
30
|
+
|
|
31
|
+
# server path for web (appName_web), server (server) and data (appName_data)
|
|
32
|
+
SERVER_ROOT_PATH=dist/server_root
|
|
33
|
+
ADMIN_USER=admin
|
|
34
|
+
ADMIN_PASS=
|
|
35
|
+
CRYPTO_KEY=
|
|
36
|
+
|
|
37
|
+
# for development
|
|
38
|
+
DEV_ADMIN_USER=admin
|
|
39
|
+
DEV_ADMIN_PASS=
|
|
40
|
+
DEV_CRYPTO_KEY=
|
|
41
|
+
|
|
42
|
+
# define apps on the same port with different domains (locally or on a real server)
|
|
43
|
+
# in local, the domains should be added to the /etc/hosts (or C:\Windows\System32\drivers\etc\hosts) file
|
|
44
|
+
APPS={APP-NAME}
|
|
45
|
+
# below is empty that means all domains will come to this app
|
|
46
|
+
DOMAINS:{APP-NAME}=
|
|
47
|
+
|
|
48
|
+
# Https Keys and Crts are for domains (not apps), use following command to generate
|
|
49
|
+
# mkcert example.com "*.example.com" localhost 127.0.0.1 ::1
|
|
50
|
+
SSL_KEY_PATH=dev/example.com+4-key.pem
|
|
51
|
+
SSL_CRT_PATH=dev/example.com+4.pem
|
|
52
|
+
HTTPS_KEY_PATH:{APP-NAME}=dev/example.com+4-key.pem
|
|
53
|
+
HTTPS_CRT_PATH:{APP-NAME}=dev/example.com+4.pem
|
|
54
|
+
|
|
55
|
+
DB_TYPE=sqlite
|
|
56
|
+
DB_FILENAME=sqlite3.db
|
|
57
|
+
DB_TYPE:{APP-NAME}=sqlite
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { fork, ChildProcess } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
let app: ChildProcess | null = null;
|
|
5
|
+
const startApp = () => {
|
|
6
|
+
console.log('Loader: starting app...');
|
|
7
|
+
app = fork(path.join(__dirname, './index.js'), [], {
|
|
8
|
+
env: { ...process.env, FROM_LOADER: '1' },
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// child->parent
|
|
12
|
+
app.on('message', async (msg: any) => {
|
|
13
|
+
if (msg?.id === 'debug' && msg?.message === 'restartApp') {
|
|
14
|
+
console.log('Loader: app requested restart.');
|
|
15
|
+
await restartApp();
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// child exit
|
|
20
|
+
app.on('exit', (code) => {
|
|
21
|
+
console.log('Loader: app exited with code', code);
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const restartApp = async () => {
|
|
26
|
+
if (!app) return;
|
|
27
|
+
|
|
28
|
+
return new Promise<void>((resolve) => {
|
|
29
|
+
console.log('Loader: sending shutdown to app...');
|
|
30
|
+
app!.send({ id: 'debug', message: 'shutdown' });
|
|
31
|
+
|
|
32
|
+
// wait for app.ts to exit
|
|
33
|
+
app!.once('exit', () => {
|
|
34
|
+
console.log('Loader: app fully exited, restarting...');
|
|
35
|
+
startApp();
|
|
36
|
+
resolve();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
startApp();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { JsonObject } from 'lupine.api';
|
|
2
|
+
|
|
3
|
+
export const fetchData = async (urlWithoutHost: string, postData: string | JsonObject) => {
|
|
4
|
+
const url = process.env['API_BASE_URL'] + urlWithoutHost;
|
|
5
|
+
console.log('========fetchData', url);
|
|
6
|
+
|
|
7
|
+
const option = {
|
|
8
|
+
method: postData ? 'POST' : 'GET',
|
|
9
|
+
body: postData ? (typeof postData === 'string' ? postData : JSON.stringify(postData)) : undefined,
|
|
10
|
+
};
|
|
11
|
+
const data = await fetch(url, option);
|
|
12
|
+
// const json = await data.json();
|
|
13
|
+
const text = await data.text();
|
|
14
|
+
try {
|
|
15
|
+
const json = JSON.parse(text);
|
|
16
|
+
return { json };
|
|
17
|
+
} catch (e) {
|
|
18
|
+
return { text };
|
|
19
|
+
}
|
|
20
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// initApp should be called before any other logics, so need to avoid `export default new Class()`
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import {
|
|
4
|
+
HostToPathProps,
|
|
5
|
+
appStart,
|
|
6
|
+
bindRenderPageFunctions,
|
|
7
|
+
getDefaultDbConfig,
|
|
8
|
+
getRenderPageFunctions,
|
|
9
|
+
loadEnv,
|
|
10
|
+
} from 'lupine.api';
|
|
11
|
+
import { fetchData } from './fetch-data';
|
|
12
|
+
import { ServerEnvKeys } from './server-env-keys';
|
|
13
|
+
|
|
14
|
+
const initAndStartServer = async () => {
|
|
15
|
+
const envFile = process.argv.find((i) => i.startsWith('--env='))?.substring(6) || '.env';
|
|
16
|
+
// it can use "#!import file_name" to import another env file
|
|
17
|
+
await loadEnv(envFile);
|
|
18
|
+
bindRenderPageFunctions({ fetchData });
|
|
19
|
+
|
|
20
|
+
const dbConfig = { ...getDefaultDbConfig() };
|
|
21
|
+
const serverRootPath = path.resolve(process.env[ServerEnvKeys.SERVER_ROOT_PATH]!);
|
|
22
|
+
const apps = (process.env[ServerEnvKeys.APPS] || '').split(',');
|
|
23
|
+
const webRootMap: HostToPathProps[] = [];
|
|
24
|
+
|
|
25
|
+
for (const app of apps) {
|
|
26
|
+
const appHosts = process.env[`${ServerEnvKeys.DOMAINS}@${app}`] || '';
|
|
27
|
+
const dbFilename =
|
|
28
|
+
process.env[`${ServerEnvKeys.DB_FILENAME}@${app}`] || process.env[`${ServerEnvKeys.DB_FILENAME}`] || 'sqlite3.db';
|
|
29
|
+
webRootMap.push({
|
|
30
|
+
appName: app,
|
|
31
|
+
hosts: appHosts ? appHosts.split(',') : [],
|
|
32
|
+
// web, data, api folders should be created in building process
|
|
33
|
+
webPath: path.join(serverRootPath, app + '_web'),
|
|
34
|
+
dataPath: path.join(serverRootPath, app + '_data'),
|
|
35
|
+
apiPath: path.join(serverRootPath, app + '_api'),
|
|
36
|
+
dbType: process.env[`${ServerEnvKeys.DB_TYPE}@${app}`] || process.env[`${ServerEnvKeys.DB_TYPE}`] || 'sqlite',
|
|
37
|
+
dbConfig: { ...dbConfig, filename: dbFilename },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const bindIp = process.env[ServerEnvKeys.BIND_IP] || '::';
|
|
42
|
+
// 0 to disable http/https server
|
|
43
|
+
const httpPort = Number.parseInt(process.env[ServerEnvKeys.HTTP_PORT] || '8080');
|
|
44
|
+
const httpsPort = Number.parseInt(process.env[ServerEnvKeys.HTTPS_PORT] || '8443');
|
|
45
|
+
const sslKeyPath = process.env[ServerEnvKeys.SSL_KEY_PATH] || '';
|
|
46
|
+
const sslCrtPath = process.env[ServerEnvKeys.SSL_CRT_PATH] || '';
|
|
47
|
+
|
|
48
|
+
// Can't use log until initApp is called (after AppStart.start)
|
|
49
|
+
await appStart.start({
|
|
50
|
+
debug: process.env[ServerEnvKeys.NODE_ENV] === 'development',
|
|
51
|
+
appEnvFile: envFile,
|
|
52
|
+
renderPageFunctions: getRenderPageFunctions(),
|
|
53
|
+
apiConfig: {
|
|
54
|
+
serverRoot: `${serverRootPath}`,
|
|
55
|
+
webHostMap: webRootMap,
|
|
56
|
+
},
|
|
57
|
+
serverConfig: {
|
|
58
|
+
bindIp,
|
|
59
|
+
httpPort,
|
|
60
|
+
httpsPort,
|
|
61
|
+
sslKeyPath,
|
|
62
|
+
sslCrtPath,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
initAndStartServer();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// some like APPS, HTTP_PORT, SERVER_ROOT_PATH are also used in 'dev-watch.js'
|
|
2
|
+
export const enum ServerEnvKeys {
|
|
3
|
+
BIND_IP = 'BIND_IP',
|
|
4
|
+
HTTP_PORT = 'HTTP_PORT',
|
|
5
|
+
HTTPS_PORT = 'HTTPS_PORT',
|
|
6
|
+
SSL_KEY_PATH = 'SSL_KEY_PATH',
|
|
7
|
+
SSL_CRT_PATH = 'SSL_CRT_PATH',
|
|
8
|
+
WEB_ROOT_MAP = 'WEB_ROOT_MAP',
|
|
9
|
+
SERVER_ROOT_PATH = 'SERVER_ROOT_PATH',
|
|
10
|
+
NODE_ENV = 'NODE_ENV',
|
|
11
|
+
APPS = 'APPS',
|
|
12
|
+
DOMAINS = 'DOMAINS',
|
|
13
|
+
DB_TYPE = 'DB_TYPE',
|
|
14
|
+
DB_FILENAME = 'DB_FILENAME',
|
|
15
|
+
LOG_FOLDER = 'LOG_FOLDER',
|
|
16
|
+
LOG_FILENAME = 'LOG_FILENAME',
|
|
17
|
+
LOG_MAX_SIZE = 'LOG_MAX_SIZE',
|
|
18
|
+
LOG_MAX_COUNT = 'LOG_MAX_COUNT',
|
|
19
|
+
LOG_OUT_TO_FILE = 'LOG_OUT_TO_FILE',
|
|
20
|
+
LOG_OUT_TO_CONSOLE = 'LOG_OUT_TO_CONSOLE',
|
|
21
|
+
LOG_LEVEL = 'LOG_LEVEL',
|
|
22
|
+
}
|