@lazycatcloud/lzc-cli 2.0.0-pre.0 → 2.0.0-pre.2
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/README.md +46 -7
- package/changelog.md +56 -19
- package/lib/app/apkshell.js +7 -44
- package/lib/app/index.js +5 -1
- package/lib/app/lpk_build.js +266 -56
- package/lib/app/lpk_build_images.js +424 -229
- package/lib/app/lpk_build_images_local.js +425 -0
- package/lib/app/lpk_build_images_pack_local.js +409 -0
- package/lib/app/lpk_create.js +158 -83
- package/lib/app/lpk_create_generator.js +35 -42
- package/lib/app/lpk_devshell.js +6 -2
- package/lib/app/lpk_installer.js +4 -3
- package/lib/app/manifest_build.js +259 -0
- package/lib/app/project_cp.js +5 -10
- package/lib/app/project_deploy.js +80 -11
- package/lib/app/project_exec.js +48 -11
- package/lib/app/project_info.js +59 -59
- package/lib/app/project_log.js +5 -10
- package/lib/app/project_runtime.js +113 -18
- package/lib/app/project_start.js +6 -11
- package/lib/app/project_sync.js +499 -0
- package/lib/appstore/apkshell.js +50 -0
- package/lib/appstore/publish.js +54 -15
- package/lib/build_remote.js +0 -1
- package/lib/config/index.js +1 -1
- package/lib/debug_bridge.js +217 -47
- package/lib/i18n/locales/en/translation.json +262 -262
- package/lib/i18n/locales/zh/translation.json +262 -262
- package/lib/lpk/core.js +2 -1
- package/lib/migrate/index.js +52 -0
- package/lib/package_info.js +135 -0
- package/lib/shellapi.js +35 -1
- package/lib/sig/core.js +2 -2
- package/lib/utils.js +92 -15
- package/package.json +89 -89
- package/scripts/cli.js +2 -0
- package/scripts/smoke/frontend-dev-entry.mjs +104 -0
- package/scripts/smoke/template-project.mjs +311 -0
- package/template/_lpk/README.md +6 -3
- package/template/_lpk/gui-vnc.manifest.yml.in +0 -9
- package/template/_lpk/hello-vue.manifest.yml.in +38 -0
- package/template/_lpk/manifest.yml.in +0 -9
- package/template/_lpk/package.yml.in +7 -0
- package/template/_lpk/todolist-golang.manifest.yml.in +23 -9
- package/template/_lpk/todolist-java.manifest.yml.in +23 -9
- package/template/_lpk/todolist-python.manifest.yml.in +31 -9
- package/template/_lpk/todolist-serverless.manifest.yml.in +38 -0
- package/template/blank/lzc-build.dev.yml +4 -0
- package/template/blank/lzc-build.yml +0 -2
- package/template/blank/lzc-manifest.yml +3 -12
- package/template/blank/package.yml +7 -0
- package/template/golang/Dockerfile +1 -1
- package/template/golang/Dockerfile.dev +20 -0
- package/template/golang/README.md +22 -11
- package/template/golang/_lzcdevignore +21 -0
- package/template/golang/lzc-build.dev.yml +12 -0
- package/template/golang/lzc-build.yml +0 -5
- package/template/golang/main.go +1 -1
- package/template/golang/manifest.dev.page.js +24 -0
- package/template/golang/run.sh +7 -0
- package/template/gui-vnc/README.md +5 -1
- package/template/gui-vnc/lzc-build.dev.yml +4 -0
- package/template/gui-vnc/lzc-build.yml +0 -5
- package/template/python/Dockerfile +2 -2
- package/template/python/Dockerfile.dev +18 -0
- package/template/python/README.md +28 -11
- package/template/python/_lzcdevignore +21 -0
- package/template/python/app.py +1 -1
- package/template/python/lzc-build.dev.yml +12 -0
- package/template/python/lzc-build.yml +0 -5
- package/template/python/manifest.dev.page.js +25 -0
- package/template/python/run.sh +12 -1
- package/template/springboot/Dockerfile +1 -1
- package/template/springboot/Dockerfile.dev +20 -0
- package/template/springboot/README.md +22 -11
- package/template/springboot/_lzcdevignore +21 -0
- package/template/springboot/lzc-build.dev.yml +12 -0
- package/template/springboot/lzc-build.yml +0 -5
- package/template/springboot/manifest.dev.page.js +24 -0
- package/template/springboot/run.sh +7 -0
- package/template/vue/README.md +14 -27
- package/template/vue/_gitignore +0 -1
- package/template/vue/lzc-build.dev.yml +7 -0
- package/template/vue/lzc-build.yml +0 -2
- package/template/vue/manifest.dev.page.js +50 -0
- package/template/vue/src/App.vue +1 -1
- package/template/vue-minidb/README.md +11 -19
- package/template/vue-minidb/_gitignore +0 -1
- package/template/vue-minidb/lzc-build.dev.yml +7 -0
- package/template/vue-minidb/lzc-build.yml +0 -2
- package/template/vue-minidb/manifest.dev.page.js +50 -0
- package/template/blank/_gitignore +0 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
|
|
8
|
+
async function loadPlaywright() {
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const hints = [
|
|
11
|
+
process.env.LZC_CLI_PLAYWRIGHT_MODULE,
|
|
12
|
+
path.join(process.cwd(), 'node_modules', 'playwright'),
|
|
13
|
+
'/tmp/pw-lzc-test/node_modules/playwright',
|
|
14
|
+
'playwright',
|
|
15
|
+
].filter(Boolean);
|
|
16
|
+
for (const hint of hints) {
|
|
17
|
+
try {
|
|
18
|
+
const resolved = require.resolve(hint);
|
|
19
|
+
return await import(pathToFileURL(resolved).href);
|
|
20
|
+
} catch {}
|
|
21
|
+
try {
|
|
22
|
+
return await import(hint);
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
25
|
+
throw new Error('playwright module not found; set LZC_CLI_PLAYWRIGHT_MODULE or install playwright');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getArg(name, fallback = '') {
|
|
29
|
+
const prefix = `--${name}=`;
|
|
30
|
+
const hit = process.argv.find((arg) => arg.startsWith(prefix));
|
|
31
|
+
return hit ? hit.slice(prefix.length) : fallback;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function requireArg(name) {
|
|
35
|
+
const value = getArg(name);
|
|
36
|
+
if (!value) {
|
|
37
|
+
throw new Error(`missing --${name}`);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const mode = requireArg('mode');
|
|
43
|
+
const target = requireArg('url');
|
|
44
|
+
const screenshot = getArg('screenshot');
|
|
45
|
+
const username = getArg('username');
|
|
46
|
+
const password = getArg('password');
|
|
47
|
+
const expectedTitle = getArg('title');
|
|
48
|
+
const requireTexts = getArg('require-text').split('||').map((item) => item.trim()).filter(Boolean);
|
|
49
|
+
const forbidTexts = getArg('forbid-text').split('||').map((item) => item.trim()).filter(Boolean);
|
|
50
|
+
|
|
51
|
+
const playwright = await loadPlaywright();
|
|
52
|
+
const chromium = playwright.chromium || playwright.default?.chromium || playwright["module.exports"]?.chromium;
|
|
53
|
+
if (!chromium) {
|
|
54
|
+
throw new Error("playwright chromium export not found");
|
|
55
|
+
}
|
|
56
|
+
const browser = await chromium.launch({ headless: true });
|
|
57
|
+
const page = await browser.newPage();
|
|
58
|
+
try {
|
|
59
|
+
await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 120000 });
|
|
60
|
+
if (page.url().includes('/sys/login')) {
|
|
61
|
+
if (!username || !password) {
|
|
62
|
+
throw new Error('login required but --username/--password not provided');
|
|
63
|
+
}
|
|
64
|
+
await page.fill('#username', username);
|
|
65
|
+
await page.fill('#password', password);
|
|
66
|
+
await Promise.all([
|
|
67
|
+
page.waitForURL((url) => !url.toString().includes('/sys/login'), { timeout: 120000 }),
|
|
68
|
+
page.getByRole('button', { name: 'Login' }).click(),
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
await page.waitForLoadState('networkidle');
|
|
72
|
+
if (mode === 'ready') {
|
|
73
|
+
await page.reload({ waitUntil: 'networkidle', timeout: 120000 });
|
|
74
|
+
}
|
|
75
|
+
const body = await page.locator('body').innerText();
|
|
76
|
+
const title = await page.title();
|
|
77
|
+
if (expectedTitle && title !== expectedTitle) {
|
|
78
|
+
throw new Error(`unexpected title: ${title}`);
|
|
79
|
+
}
|
|
80
|
+
for (const text of requireTexts) {
|
|
81
|
+
if (!body.includes(text)) {
|
|
82
|
+
throw new Error(`required text missing: ${text}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const text of forbidTexts) {
|
|
86
|
+
if (body.includes(text)) {
|
|
87
|
+
throw new Error(`forbidden text found: ${text}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (screenshot) {
|
|
91
|
+
fs.mkdirSync(path.dirname(screenshot), { recursive: true });
|
|
92
|
+
await page.screenshot({ path: screenshot, fullPage: true });
|
|
93
|
+
}
|
|
94
|
+
console.log(JSON.stringify({
|
|
95
|
+
ok: true,
|
|
96
|
+
mode,
|
|
97
|
+
url: page.url(),
|
|
98
|
+
title,
|
|
99
|
+
body_head: body.slice(0, 400),
|
|
100
|
+
screenshot,
|
|
101
|
+
}, null, 2));
|
|
102
|
+
} finally {
|
|
103
|
+
await browser.close();
|
|
104
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
const cliRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..');
|
|
8
|
+
const cliEntrypoint = path.join(cliRoot, 'scripts', 'cli.js');
|
|
9
|
+
const frontendSmokeEntrypoint = path.join(cliRoot, 'scripts', 'smoke', 'frontend-dev-entry.mjs');
|
|
10
|
+
|
|
11
|
+
function getArg(name, fallback = '') {
|
|
12
|
+
const prefix = `--${name}=`;
|
|
13
|
+
const hit = process.argv.find((arg) => arg.startsWith(prefix));
|
|
14
|
+
return hit ? hit.slice(prefix.length) : fallback;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function hasFlag(name) {
|
|
18
|
+
return process.argv.includes(`--${name}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeBoxName(input) {
|
|
22
|
+
return String(input || '').trim().replace(/\.heiyu\.space$/i, '');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function capture(command, args, options = {}) {
|
|
26
|
+
return await new Promise((resolve, reject) => {
|
|
27
|
+
const child = spawn(command, args, {
|
|
28
|
+
cwd: options.cwd || process.cwd(),
|
|
29
|
+
env: { ...process.env, ...(options.env || {}) },
|
|
30
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
31
|
+
shell: false,
|
|
32
|
+
});
|
|
33
|
+
let stdout = '';
|
|
34
|
+
let stderr = '';
|
|
35
|
+
child.stdout.on('data', (chunk) => {
|
|
36
|
+
stdout += chunk.toString();
|
|
37
|
+
});
|
|
38
|
+
child.stderr.on('data', (chunk) => {
|
|
39
|
+
stderr += chunk.toString();
|
|
40
|
+
});
|
|
41
|
+
child.on('error', reject);
|
|
42
|
+
child.on('close', (code) => {
|
|
43
|
+
if (code === 0) {
|
|
44
|
+
resolve({ stdout, stderr });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
reject(new Error(`${command} ${args.join(' ')} failed with code ${code}\n${stdout}\n${stderr}`));
|
|
48
|
+
});
|
|
49
|
+
child.stdin.end(options.stdinText || '');
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function spawnStreaming(command, args, options = {}) {
|
|
54
|
+
const child = spawn(command, args, {
|
|
55
|
+
cwd: options.cwd || process.cwd(),
|
|
56
|
+
env: { ...process.env, ...(options.env || {}) },
|
|
57
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
58
|
+
shell: false,
|
|
59
|
+
});
|
|
60
|
+
let stdout = '';
|
|
61
|
+
let stderr = '';
|
|
62
|
+
child.stdout.on('data', (chunk) => {
|
|
63
|
+
const text = chunk.toString();
|
|
64
|
+
stdout += text;
|
|
65
|
+
process.stdout.write(text);
|
|
66
|
+
});
|
|
67
|
+
child.stderr.on('data', (chunk) => {
|
|
68
|
+
const text = chunk.toString();
|
|
69
|
+
stderr += text;
|
|
70
|
+
process.stderr.write(text);
|
|
71
|
+
});
|
|
72
|
+
if (options.stdinText) {
|
|
73
|
+
child.stdin.write(options.stdinText);
|
|
74
|
+
}
|
|
75
|
+
child.stdin.end();
|
|
76
|
+
return { child, getStdout: () => stdout, getStderr: () => stderr };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function runCli(projectDir, argv, options = {}) {
|
|
80
|
+
const { stdout, stderr } = await capture(process.execPath, [cliEntrypoint, ...argv], {
|
|
81
|
+
cwd: projectDir,
|
|
82
|
+
stdinText: options.stdinText || '',
|
|
83
|
+
});
|
|
84
|
+
if (stdout.trim()) {
|
|
85
|
+
process.stdout.write(stdout);
|
|
86
|
+
}
|
|
87
|
+
if (stderr.trim()) {
|
|
88
|
+
process.stderr.write(stderr);
|
|
89
|
+
}
|
|
90
|
+
return { stdout, stderr };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function runSmoke(argv, options = {}) {
|
|
94
|
+
const { stdout, stderr } = await capture(process.execPath, [frontendSmokeEntrypoint, ...argv], options);
|
|
95
|
+
if (stdout.trim()) {
|
|
96
|
+
process.stdout.write(stdout);
|
|
97
|
+
}
|
|
98
|
+
if (stderr.trim()) {
|
|
99
|
+
process.stderr.write(stderr);
|
|
100
|
+
}
|
|
101
|
+
return JSON.parse(stdout);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function readDefaultBoxName() {
|
|
105
|
+
const { stdout } = await capture(process.execPath, [cliEntrypoint, 'box', 'default']);
|
|
106
|
+
const box = normalizeBoxName(stdout.trim());
|
|
107
|
+
if (!box) {
|
|
108
|
+
throw new Error('default box is empty');
|
|
109
|
+
}
|
|
110
|
+
return box;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function extractTarEntry(lpkPath, entryName) {
|
|
114
|
+
const result = spawnSync('tar', ['-xOf', lpkPath, entryName], { encoding: 'utf8' });
|
|
115
|
+
if (result.status !== 0) {
|
|
116
|
+
throw new Error(`extract ${entryName} failed\n${result.stdout}\n${result.stderr}`);
|
|
117
|
+
}
|
|
118
|
+
return result.stdout;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function assert(condition, message) {
|
|
122
|
+
if (!condition) {
|
|
123
|
+
throw new Error(message);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function waitForOutput(getText, pattern, timeoutMs = 30000) {
|
|
128
|
+
const startedAt = Date.now();
|
|
129
|
+
while (Date.now() - startedAt <= timeoutMs) {
|
|
130
|
+
if (pattern.test(getText())) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`timeout waiting for output: ${String(pattern)}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function retry(action, timeoutMs = 30000, intervalMs = 1000) {
|
|
139
|
+
const startedAt = Date.now();
|
|
140
|
+
let lastError = null;
|
|
141
|
+
while (Date.now() - startedAt <= timeoutMs) {
|
|
142
|
+
try {
|
|
143
|
+
return await action();
|
|
144
|
+
} catch (error) {
|
|
145
|
+
lastError = error;
|
|
146
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
throw lastError || new Error('retry timeout');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function verifyReleaseLayout(projectDir, expectedPackage, expectedSubdomain) {
|
|
153
|
+
const lpkPath = path.join(projectDir, 'release.lpk');
|
|
154
|
+
await runCli(projectDir, ['project', 'release', '-o', lpkPath]);
|
|
155
|
+
const manifestText = extractTarEntry(lpkPath, 'manifest.yml');
|
|
156
|
+
const packageText = extractTarEntry(lpkPath, 'package.yml');
|
|
157
|
+
assert(!manifestText.includes('injects:'), 'release manifest must not contain injects');
|
|
158
|
+
assert(!manifestText.includes('name:'), 'release manifest must not contain static name');
|
|
159
|
+
assert(!manifestText.includes('description:'), 'release manifest must not contain static description');
|
|
160
|
+
assert(packageText.includes(`package: ${expectedPackage}`), 'package.yml missing package id');
|
|
161
|
+
assert(manifestText.includes(`subdomain: ${expectedSubdomain}`), 'release manifest missing subdomain');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function smokeNotReadyPage({ url, title, requireText = [], username, password, screenshot }) {
|
|
165
|
+
await runSmoke([
|
|
166
|
+
'--mode=not-ready',
|
|
167
|
+
`--url=${url}`,
|
|
168
|
+
`--username=${username}`,
|
|
169
|
+
`--password=${password}`,
|
|
170
|
+
`--title=${title}`,
|
|
171
|
+
`--require-text=${requireText.join('||')}`,
|
|
172
|
+
`--screenshot=${screenshot}`,
|
|
173
|
+
]);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function smokeReadyPage({ url, title, requireText, forbidText, username, password, screenshot }) {
|
|
177
|
+
await runSmoke([
|
|
178
|
+
'--mode=ready',
|
|
179
|
+
`--url=${url}`,
|
|
180
|
+
`--username=${username}`,
|
|
181
|
+
`--password=${password}`,
|
|
182
|
+
`--title=${title}`,
|
|
183
|
+
`--require-text=${requireText.join('||')}`,
|
|
184
|
+
`--forbid-text=${forbidText.join('||')}`,
|
|
185
|
+
`--screenshot=${screenshot}`,
|
|
186
|
+
]);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function uninstallPackage(pkgId) {
|
|
190
|
+
await capture(process.execPath, [cliEntrypoint, 'lpk', 'uninstall', pkgId]);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function runHelloVue(projectDir, appName, boxName, username, password) {
|
|
194
|
+
const pkgId = `cloud.lazycat.app.${appName}.dev`;
|
|
195
|
+
const url = `https://${appName}.${boxName}.heiyu.space`;
|
|
196
|
+
try {
|
|
197
|
+
await verifyReleaseLayout(projectDir, `cloud.lazycat.app.${appName}`, appName);
|
|
198
|
+
await runCli(projectDir, ['project', 'deploy']);
|
|
199
|
+
await runCli(projectDir, ['project', 'start', '--restart']);
|
|
200
|
+
await runCli(projectDir, ['project', 'exec', '--tty=false', 'pwd']);
|
|
201
|
+
await smokeNotReadyPage({
|
|
202
|
+
url,
|
|
203
|
+
title: 'Frontend Dev',
|
|
204
|
+
username,
|
|
205
|
+
password,
|
|
206
|
+
screenshot: path.join(projectDir, 'hello-vue-not-ready.png'),
|
|
207
|
+
});
|
|
208
|
+
const vite = spawnStreaming('npm', ['run', 'dev'], { cwd: projectDir });
|
|
209
|
+
try {
|
|
210
|
+
await waitForOutput(vite.getStdout, /ready in|Local:\s+http:\/\/localhost:3000\//, 30000);
|
|
211
|
+
await retry(async () => {
|
|
212
|
+
await smokeReadyPage({
|
|
213
|
+
url,
|
|
214
|
+
title: 'Vite + Vue + TS',
|
|
215
|
+
requireText: ['Welcome to Lazycat Microserver', 'Open Developer Docs', 'Fast local iteration'],
|
|
216
|
+
forbidText: ['Frontend dev server is not ready', 'Dev machine is offline', 'Dev machine is not linked yet'],
|
|
217
|
+
username,
|
|
218
|
+
password,
|
|
219
|
+
screenshot: path.join(projectDir, 'hello-vue-ready.png'),
|
|
220
|
+
});
|
|
221
|
+
}, 30000, 1000);
|
|
222
|
+
} finally {
|
|
223
|
+
vite.child.kill('SIGINT');
|
|
224
|
+
}
|
|
225
|
+
} finally {
|
|
226
|
+
await uninstallPackage(pkgId).catch(() => {});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function runTodolistGolang(projectDir, appName, boxName, username, password) {
|
|
231
|
+
const pkgId = `cloud.lazycat.app.${appName}.dev`;
|
|
232
|
+
const url = `https://${appName}.${boxName}.heiyu.space`;
|
|
233
|
+
try {
|
|
234
|
+
await verifyReleaseLayout(projectDir, `cloud.lazycat.app.${appName}`, appName);
|
|
235
|
+
await runCli(projectDir, ['project', 'deploy']);
|
|
236
|
+
await runCli(projectDir, ['project', 'start', '--restart']);
|
|
237
|
+
const execResult = await runCli(projectDir, ['project', 'exec', '--tty=false', 'pwd']);
|
|
238
|
+
assert(execResult.stdout.includes('/lzcapp/cache/project-mirror'), 'project exec should enter project mirror workdir');
|
|
239
|
+
await smokeNotReadyPage({
|
|
240
|
+
url,
|
|
241
|
+
title: 'Backend Dev',
|
|
242
|
+
requireText: ['Backend dev service is not ready', 'project sync --watch', 'project exec /bin/sh', 'Expected local port: 3000'],
|
|
243
|
+
username,
|
|
244
|
+
password,
|
|
245
|
+
screenshot: path.join(projectDir, 'todolist-golang-not-ready.png'),
|
|
246
|
+
});
|
|
247
|
+
} finally {
|
|
248
|
+
await uninstallPackage(pkgId).catch(() => {});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function runTodolistPython(projectDir, appName, boxName, username, password) {
|
|
253
|
+
const pkgId = `cloud.lazycat.app.${appName}.dev`;
|
|
254
|
+
const url = `https://${appName}.${boxName}.heiyu.space`;
|
|
255
|
+
try {
|
|
256
|
+
await verifyReleaseLayout(projectDir, `cloud.lazycat.app.${appName}`, appName);
|
|
257
|
+
await runCli(projectDir, ['project', 'deploy']);
|
|
258
|
+
await runCli(projectDir, ['project', 'start', '--restart']);
|
|
259
|
+
const execResult = await runCli(projectDir, ['project', 'exec', '--tty=false', 'pwd']);
|
|
260
|
+
assert(execResult.stdout.includes('/lzcapp/cache/project-mirror'), 'project exec should enter project mirror workdir');
|
|
261
|
+
await retry(async () => {
|
|
262
|
+
await smokeReadyPage({
|
|
263
|
+
url,
|
|
264
|
+
title: 'Lazycat Python Todo Template',
|
|
265
|
+
requireText: ['Todo demo powered by Flask API', 'Open Developer Docs', 'Todo quick demo'],
|
|
266
|
+
forbidText: ['Python dev service is not ready'],
|
|
267
|
+
username,
|
|
268
|
+
password,
|
|
269
|
+
screenshot: path.join(projectDir, 'todolist-python-ready.png'),
|
|
270
|
+
});
|
|
271
|
+
}, 45000, 1000);
|
|
272
|
+
} finally {
|
|
273
|
+
await uninstallPackage(pkgId).catch(() => {});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function main() {
|
|
278
|
+
const template = getArg('template', 'hello-vue');
|
|
279
|
+
const username = getArg('username', 'c');
|
|
280
|
+
const password = getArg('password', 'ccc123');
|
|
281
|
+
const keep = hasFlag('keep');
|
|
282
|
+
const boxName = normalizeBoxName(getArg('box')) || await readDefaultBoxName();
|
|
283
|
+
const workspace = mkdtempSync(path.join(os.tmpdir(), `lzc-cli-smoke-${template}-`));
|
|
284
|
+
const appName = `${template}-smoke-${Date.now().toString(36)}`.replace(/[^a-z0-9-]/g, '-');
|
|
285
|
+
console.log(`workspace=${workspace}`);
|
|
286
|
+
console.log(`box=${boxName}`);
|
|
287
|
+
console.log(`app=${appName}`);
|
|
288
|
+
try {
|
|
289
|
+
await runCli(workspace, ['project', 'create', appName, '--template', template], { stdinText: '\n' });
|
|
290
|
+
const projectDir = path.join(workspace, appName);
|
|
291
|
+
if (template === 'hello-vue') {
|
|
292
|
+
await runHelloVue(projectDir, appName, boxName, username, password);
|
|
293
|
+
} else if (template === 'todolist-golang') {
|
|
294
|
+
await runTodolistGolang(projectDir, appName, boxName, username, password);
|
|
295
|
+
} else if (template === 'todolist-python') {
|
|
296
|
+
await runTodolistPython(projectDir, appName, boxName, username, password);
|
|
297
|
+
} else {
|
|
298
|
+
throw new Error(`unsupported template: ${template}`);
|
|
299
|
+
}
|
|
300
|
+
console.log(JSON.stringify({ ok: true, template, workspace, boxName, appName }, null, 2));
|
|
301
|
+
} finally {
|
|
302
|
+
if (!keep) {
|
|
303
|
+
rmSync(workspace, { recursive: true, force: true });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
main().catch((error) => {
|
|
309
|
+
console.error(error?.stack || String(error));
|
|
310
|
+
process.exit(1);
|
|
311
|
+
});
|
package/template/_lpk/README.md
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
|
|
2
1
|
# 懒猫云应用
|
|
3
2
|
|
|
4
3
|
## 第一次部署(先看到结果)
|
|
5
|
-
```
|
|
6
|
-
npm install
|
|
4
|
+
```
|
|
7
5
|
lzc-cli project deploy
|
|
8
6
|
lzc-cli project info
|
|
9
7
|
```
|
|
10
8
|
|
|
9
|
+
默认情况下,project 命令在存在 `lzc-build.dev.yml` 时会优先使用它。
|
|
10
|
+
每个命令都会打印实际使用的 `Build config`。
|
|
11
|
+
如果要操作 `lzc-build.yml`,请显式加上 `--release`。
|
|
12
|
+
如果是前端模板,`project deploy` 会按 `buildscript` 自动安装依赖并完成构建,不需要额外先执行 `npm install`。
|
|
13
|
+
|
|
11
14
|
## 构建
|
|
12
15
|
```
|
|
13
16
|
lzc-cli project build -o you-awesome.lpk
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
name: ${name} # app名称
|
|
2
|
-
package: ${package} # app的唯一标识符
|
|
3
|
-
version: 0.0.1 # app的版本
|
|
4
|
-
description: # app描述
|
|
5
|
-
|
|
6
|
-
license: https://choosealicense.com/licenses/mit/
|
|
7
|
-
homepage: # 项目主页或文档地址
|
|
8
|
-
author: # app author
|
|
9
|
-
|
|
10
1
|
# application 是默认前台容器,对应固定 service 名 app
|
|
11
2
|
application:
|
|
12
3
|
subdomain: ${subdomain} # 默认访问子域名前缀
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
application:
|
|
2
|
+
subdomain: ${subdomain} # 默认访问子域名前缀
|
|
3
|
+
#@build if env.DEV_MODE=1
|
|
4
|
+
injects:
|
|
5
|
+
- id: frontend-dev-proxy
|
|
6
|
+
on: request
|
|
7
|
+
auth_required: false
|
|
8
|
+
when:
|
|
9
|
+
- "/*"
|
|
10
|
+
do:
|
|
11
|
+
- src: |
|
|
12
|
+
const devPort = 3000;
|
|
13
|
+
#@build include ./manifest.dev.page.js
|
|
14
|
+
|
|
15
|
+
if (!ctx.dev.id) {
|
|
16
|
+
ctx.response.send(200, renderDevPage("not_linked", devPort));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const via = ctx.net.via.client(ctx.dev.id);
|
|
21
|
+
if (!ctx.dev.online()) {
|
|
22
|
+
ctx.response.send(200, renderDevPage("offline", devPort));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!ctx.net.reachable("tcp", "127.0.0.1", devPort, via)) {
|
|
27
|
+
ctx.response.send(200, renderDevPage("not_ready", devPort));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
ctx.proxy.to("http://127.0.0.1:" + String(devPort), {
|
|
32
|
+
via: via,
|
|
33
|
+
use_target_host: true,
|
|
34
|
+
});
|
|
35
|
+
#@build else
|
|
36
|
+
routes:
|
|
37
|
+
- /=file:///lzcapp/pkg/content/dist
|
|
38
|
+
#@build end
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
name: ${name} # app名称
|
|
2
|
-
package: ${package} # app的唯一标识符
|
|
3
|
-
version: 0.0.1 # app的版本
|
|
4
|
-
description: # app描述
|
|
5
|
-
|
|
6
|
-
license: https://choosealicense.com/licenses/mit/
|
|
7
|
-
homepage: # 出现bug时候提交反馈的地方
|
|
8
|
-
author: # app author
|
|
9
|
-
|
|
10
1
|
# application 作为默认前台容器运行,对应固定 service 名 app
|
|
11
2
|
application:
|
|
12
3
|
subdomain: ${subdomain} # 默认访问子域名前缀
|
|
@@ -1,16 +1,30 @@
|
|
|
1
|
-
name: ${name} # app名称
|
|
2
|
-
package: ${package} # app的唯一标识符
|
|
3
|
-
version: 0.0.1 # app的版本
|
|
4
|
-
description: # app描述
|
|
5
|
-
|
|
6
|
-
license: https://choosealicense.com/licenses/mit/
|
|
7
|
-
homepage: # 出现bug时候提交反馈的地方
|
|
8
|
-
author: # app author
|
|
9
|
-
|
|
10
1
|
application:
|
|
11
2
|
subdomain: ${subdomain} # 默认访问子域名前缀
|
|
12
3
|
image: embed:app-runtime # 引用 lzc-build.yml 里的 images.app-runtime
|
|
4
|
+
#@build if env.DEV_MODE=1
|
|
5
|
+
injects:
|
|
6
|
+
- id: backend-dev-proxy
|
|
7
|
+
on: request
|
|
8
|
+
auth_required: false
|
|
9
|
+
when:
|
|
10
|
+
- "/*"
|
|
11
|
+
do:
|
|
12
|
+
- src: |
|
|
13
|
+
const backendPort = 3000;
|
|
14
|
+
const backendURL = "http://127.0.0.1:" + String(backendPort);
|
|
15
|
+
#@build include ./manifest.dev.page.js
|
|
16
|
+
|
|
17
|
+
if (!ctx.net.reachable("tcp", "127.0.0.1", backendPort)) {
|
|
18
|
+
ctx.response.send(200, renderDevPage(backendPort));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
ctx.proxy.to(backendURL, {
|
|
23
|
+
use_target_host: true,
|
|
24
|
+
});
|
|
25
|
+
#@build else
|
|
13
26
|
routes:
|
|
14
27
|
# exec:// 路由会执行 /app/run.sh,并把请求转发到 3000 端口
|
|
15
28
|
# 适合「一个容器同时承载前端+后端」的入门场景
|
|
16
29
|
- /=exec://3000,/app/run.sh
|
|
30
|
+
#@build end
|
|
@@ -1,15 +1,29 @@
|
|
|
1
|
-
name: ${name} # app名称
|
|
2
|
-
package: ${package} # app的唯一标识符
|
|
3
|
-
version: 0.0.1 # app的版本
|
|
4
|
-
description: # app描述
|
|
5
|
-
|
|
6
|
-
license: https://choosealicense.com/licenses/mit/
|
|
7
|
-
homepage: # 出现bug时候提交反馈的地方
|
|
8
|
-
author: # app author
|
|
9
|
-
|
|
10
1
|
application:
|
|
11
2
|
subdomain: ${subdomain} # 默认访问子域名前缀
|
|
12
3
|
image: embed:app-runtime # 引用 lzc-build.yml 里的 images.app-runtime
|
|
4
|
+
#@build if env.DEV_MODE=1
|
|
5
|
+
injects:
|
|
6
|
+
- id: backend-dev-proxy
|
|
7
|
+
on: request
|
|
8
|
+
auth_required: false
|
|
9
|
+
when:
|
|
10
|
+
- "/*"
|
|
11
|
+
do:
|
|
12
|
+
- src: |
|
|
13
|
+
const backendPort = 8080;
|
|
14
|
+
const backendURL = "http://127.0.0.1:" + String(backendPort);
|
|
15
|
+
#@build include ./manifest.dev.page.js
|
|
16
|
+
|
|
17
|
+
if (!ctx.net.reachable("tcp", "127.0.0.1", backendPort)) {
|
|
18
|
+
ctx.response.send(200, renderDevPage(backendPort));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
ctx.proxy.to(backendURL, {
|
|
23
|
+
use_target_host: true,
|
|
24
|
+
});
|
|
25
|
+
#@build else
|
|
13
26
|
routes:
|
|
14
27
|
# exec:// 路由会执行 /app/run.sh,并把请求转发到 8080 端口
|
|
15
28
|
- /=exec://8080,/app/run.sh
|
|
29
|
+
#@build end
|
|
@@ -1,15 +1,37 @@
|
|
|
1
|
-
name: ${name} # app名称
|
|
2
|
-
package: ${package} # app的唯一标识符
|
|
3
|
-
version: 0.0.1 # app的版本
|
|
4
|
-
description: # app描述
|
|
5
|
-
|
|
6
|
-
license: https://choosealicense.com/licenses/mit/
|
|
7
|
-
homepage: # 出现bug时候提交反馈的地方
|
|
8
|
-
author: # app author
|
|
9
|
-
|
|
10
1
|
application:
|
|
11
2
|
subdomain: ${subdomain} # 默认访问子域名前缀
|
|
3
|
+
#@build if env.DEV_MODE=1
|
|
4
|
+
injects:
|
|
5
|
+
- id: backend-dev-proxy
|
|
6
|
+
on: request
|
|
7
|
+
auth_required: false
|
|
8
|
+
when:
|
|
9
|
+
- "/*"
|
|
10
|
+
do:
|
|
11
|
+
- src: |
|
|
12
|
+
const backendHost = "backend";
|
|
13
|
+
const backendPort = 3000;
|
|
14
|
+
const backendURL = "http://" + backendHost + ":" + String(backendPort);
|
|
15
|
+
#@build include ./manifest.dev.page.js
|
|
16
|
+
|
|
17
|
+
if (!ctx.net.reachable("tcp", backendHost, backendPort)) {
|
|
18
|
+
ctx.response.send(200, renderDevPage(backendPort));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
ctx.proxy.to(backendURL, {
|
|
23
|
+
use_target_host: true,
|
|
24
|
+
});
|
|
25
|
+
#@build else
|
|
12
26
|
image: embed:app-runtime # 引用 lzc-build.yml 里的 images.app-runtime
|
|
13
27
|
routes:
|
|
14
28
|
# exec:// 路由会执行 /app/run.sh,并把请求转发到 3000 端口
|
|
15
29
|
- /=exec://3000,/app/run.sh
|
|
30
|
+
#@build end
|
|
31
|
+
|
|
32
|
+
#@build if env.DEV_MODE=1
|
|
33
|
+
services:
|
|
34
|
+
backend:
|
|
35
|
+
image: embed:app-runtime
|
|
36
|
+
command: /app/run.sh
|
|
37
|
+
#@build end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
application:
|
|
2
|
+
subdomain: ${subdomain} # 默认访问子域名前缀
|
|
3
|
+
#@build if env.DEV_MODE=1
|
|
4
|
+
injects:
|
|
5
|
+
- id: frontend-dev-proxy
|
|
6
|
+
on: request
|
|
7
|
+
auth_required: false
|
|
8
|
+
when:
|
|
9
|
+
- "/*"
|
|
10
|
+
do:
|
|
11
|
+
- src: |
|
|
12
|
+
const devPort = 3000;
|
|
13
|
+
#@build include ./manifest.dev.page.js
|
|
14
|
+
|
|
15
|
+
if (!ctx.dev.id) {
|
|
16
|
+
ctx.response.send(200, renderDevPage("not_linked", devPort));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const via = ctx.net.via.client(ctx.dev.id);
|
|
21
|
+
if (!ctx.dev.online()) {
|
|
22
|
+
ctx.response.send(200, renderDevPage("offline", devPort));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!ctx.net.reachable("tcp", "127.0.0.1", devPort, via)) {
|
|
27
|
+
ctx.response.send(200, renderDevPage("not_ready", devPort));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
ctx.proxy.to("http://127.0.0.1:" + String(devPort), {
|
|
32
|
+
via: via,
|
|
33
|
+
use_target_host: true,
|
|
34
|
+
});
|
|
35
|
+
#@build else
|
|
36
|
+
routes:
|
|
37
|
+
- /=file:///lzcapp/pkg/content/dist
|
|
38
|
+
#@build end
|