@freestyle-sh/with-dev-server 0.1.1
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 +49 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +179 -0
- package/package.json +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# @freestyle-sh/with-nodejs
|
|
2
|
+
|
|
3
|
+
Node.js runtime via [NVM](https://github.com/nvm-sh/nvm) for [Freestyle](https://freestyle.sh) VMs.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @freestyle-sh/with-nodejs freestyle-sandboxes
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { freestyle, VmSpec } from "freestyle-sandboxes";
|
|
15
|
+
import { VmDevServer } from "../src/index";
|
|
16
|
+
|
|
17
|
+
const TEMPLATE_REPO = "https://github.com/freestyle-sh/freestyle-next";
|
|
18
|
+
|
|
19
|
+
const { repoId } = await freestyle.git.repos.create({
|
|
20
|
+
source: {
|
|
21
|
+
url: TEMPLATE_REPO,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const domain = `${repoId}.style.dev`;
|
|
26
|
+
|
|
27
|
+
const { vm } = await freestyle.vms.create({
|
|
28
|
+
snapshot: new VmSpec({
|
|
29
|
+
with: {
|
|
30
|
+
devServer: new VmDevServer({
|
|
31
|
+
workdir: "/repo",
|
|
32
|
+
templateRepo: TEMPLATE_REPO,
|
|
33
|
+
}),
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
with: {
|
|
37
|
+
devServer: new VmDevServer({
|
|
38
|
+
repo: repoId,
|
|
39
|
+
}),
|
|
40
|
+
},
|
|
41
|
+
domains: [
|
|
42
|
+
{
|
|
43
|
+
domain: domain,
|
|
44
|
+
vmPort: 3000,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { VmSpec, VmWithInstance, VmWith } from 'freestyle-sandboxes';
|
|
2
|
+
|
|
3
|
+
declare const createSnapshotSpec: (templateRepo: string) => VmSpec;
|
|
4
|
+
declare class VmDevServerInstance extends VmWithInstance {
|
|
5
|
+
options: {
|
|
6
|
+
workdir: string;
|
|
7
|
+
};
|
|
8
|
+
constructor(options: {
|
|
9
|
+
workdir: string;
|
|
10
|
+
});
|
|
11
|
+
private shellEscape;
|
|
12
|
+
private buildJournalctlCommand;
|
|
13
|
+
getLogs(options?: {
|
|
14
|
+
unit?: string;
|
|
15
|
+
lines?: number;
|
|
16
|
+
since?: string;
|
|
17
|
+
}): Promise<{
|
|
18
|
+
statusCode?: number | undefined | null;
|
|
19
|
+
stdout?: string | undefined | null;
|
|
20
|
+
stderr?: string | undefined | null;
|
|
21
|
+
}>;
|
|
22
|
+
streamLogs(options?: {
|
|
23
|
+
unit?: string;
|
|
24
|
+
lines?: number;
|
|
25
|
+
since?: string;
|
|
26
|
+
pollIntervalMs?: number;
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
}): AsyncGenerator<string>;
|
|
29
|
+
restart(): Promise<{
|
|
30
|
+
statusCode?: number | undefined | null;
|
|
31
|
+
stdout?: string | undefined | null;
|
|
32
|
+
stderr?: string | undefined | null;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
declare class VmDevServer extends VmWith<VmDevServerInstance> {
|
|
36
|
+
templateRepo?: string;
|
|
37
|
+
repo?: string;
|
|
38
|
+
workdir: string;
|
|
39
|
+
createInstance(): VmDevServerInstance;
|
|
40
|
+
constructor(options: {
|
|
41
|
+
templateRepo?: string;
|
|
42
|
+
repo?: string;
|
|
43
|
+
workdir?: string;
|
|
44
|
+
});
|
|
45
|
+
configureSnapshotSpec(spec: VmSpec): VmSpec | Promise<VmSpec>;
|
|
46
|
+
configureSpec(spec: VmSpec): VmSpec | Promise<VmSpec>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { VmDevServer, VmDevServerInstance, createSnapshotSpec };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { VmNodeJs } from '@freestyle-sh/with-nodejs';
|
|
2
|
+
import { VmSpec, VmWithInstance, VmWith } from 'freestyle-sandboxes';
|
|
3
|
+
|
|
4
|
+
const createSnapshotSpec = (templateRepo) => {
|
|
5
|
+
const newSpec = new VmSpec({
|
|
6
|
+
with: {
|
|
7
|
+
nodejs: new VmNodeJs({})
|
|
8
|
+
},
|
|
9
|
+
git: {
|
|
10
|
+
repos: [
|
|
11
|
+
{
|
|
12
|
+
repo: templateRepo,
|
|
13
|
+
path: "/repo"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
systemd: {
|
|
18
|
+
services: [
|
|
19
|
+
{
|
|
20
|
+
name: "npm-install",
|
|
21
|
+
bash: "npm install",
|
|
22
|
+
mode: "oneshot",
|
|
23
|
+
workdir: "/repo"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "npm-dev",
|
|
27
|
+
bash: "npm run dev",
|
|
28
|
+
after: ["npm-install"],
|
|
29
|
+
requires: ["npm-install"],
|
|
30
|
+
workdir: "/repo"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "curl-test",
|
|
34
|
+
bash: `
|
|
35
|
+
set -e
|
|
36
|
+
timeout 10 bash -c 'while ! curl http://localhost:3000; do
|
|
37
|
+
echo "Retrying..."
|
|
38
|
+
sleep 1
|
|
39
|
+
done'
|
|
40
|
+
`,
|
|
41
|
+
mode: "oneshot",
|
|
42
|
+
after: ["npm-dev"],
|
|
43
|
+
requires: ["npm-dev"],
|
|
44
|
+
workdir: "/repo",
|
|
45
|
+
timeoutSec: 10
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return newSpec;
|
|
51
|
+
};
|
|
52
|
+
class VmDevServerInstance extends VmWithInstance {
|
|
53
|
+
constructor(options) {
|
|
54
|
+
super();
|
|
55
|
+
this.options = options;
|
|
56
|
+
}
|
|
57
|
+
shellEscape(value) {
|
|
58
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
59
|
+
}
|
|
60
|
+
buildJournalctlCommand(options) {
|
|
61
|
+
const unit = options.unit ?? "npm-dev";
|
|
62
|
+
const parts = ["journalctl", "-u", this.shellEscape(unit), "--no-pager"];
|
|
63
|
+
if (options.output) {
|
|
64
|
+
parts.push("-o", this.shellEscape(options.output));
|
|
65
|
+
}
|
|
66
|
+
if (options.lines !== void 0) {
|
|
67
|
+
parts.push("-n", String(options.lines));
|
|
68
|
+
}
|
|
69
|
+
if (options.since) {
|
|
70
|
+
parts.push("--since", this.shellEscape(options.since));
|
|
71
|
+
}
|
|
72
|
+
if (options.afterCursor) {
|
|
73
|
+
parts.push("--after-cursor", this.shellEscape(options.afterCursor));
|
|
74
|
+
}
|
|
75
|
+
if (options.showCursor) {
|
|
76
|
+
parts.push("--show-cursor");
|
|
77
|
+
}
|
|
78
|
+
return parts.join(" ");
|
|
79
|
+
}
|
|
80
|
+
async getLogs(options) {
|
|
81
|
+
const command = this.buildJournalctlCommand({
|
|
82
|
+
unit: options?.unit,
|
|
83
|
+
lines: options?.lines ?? 200,
|
|
84
|
+
since: options?.since
|
|
85
|
+
});
|
|
86
|
+
return await this.vm.exec({ command });
|
|
87
|
+
}
|
|
88
|
+
async *streamLogs(options) {
|
|
89
|
+
const pollIntervalMs = options?.pollIntervalMs ?? 1e3;
|
|
90
|
+
let cursor = void 0;
|
|
91
|
+
let first = true;
|
|
92
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
93
|
+
while (true) {
|
|
94
|
+
if (options?.signal?.aborted) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const command = this.buildJournalctlCommand({
|
|
98
|
+
unit: options?.unit,
|
|
99
|
+
lines: first ? options?.lines ?? 200 : void 0,
|
|
100
|
+
since: first ? options?.since : void 0,
|
|
101
|
+
afterCursor: first ? void 0 : cursor,
|
|
102
|
+
showCursor: true,
|
|
103
|
+
output: "cat"
|
|
104
|
+
});
|
|
105
|
+
const result = await this.vm.exec({ command });
|
|
106
|
+
if (result.statusCode && result.statusCode !== 0) {
|
|
107
|
+
const error = result.stderr ?? "Failed to read logs";
|
|
108
|
+
throw new Error(error);
|
|
109
|
+
}
|
|
110
|
+
const output = result.stdout ?? "";
|
|
111
|
+
let nextCursor = cursor;
|
|
112
|
+
for (const line of output.split(/\r?\n/)) {
|
|
113
|
+
if (!line) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const cursorMatch = line.match(/^-- cursor: (.+)$/);
|
|
117
|
+
if (cursorMatch) {
|
|
118
|
+
nextCursor = cursorMatch[1].trim();
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (line.startsWith("-- No entries")) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
yield line;
|
|
125
|
+
}
|
|
126
|
+
cursor = nextCursor;
|
|
127
|
+
first = false;
|
|
128
|
+
await sleep(pollIntervalMs);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async restart() {
|
|
132
|
+
return await this.vm.exec("systemctl restart npm-dev");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
class VmDevServer extends VmWith {
|
|
136
|
+
templateRepo;
|
|
137
|
+
repo;
|
|
138
|
+
workdir = "/repo";
|
|
139
|
+
createInstance() {
|
|
140
|
+
return new VmDevServerInstance({
|
|
141
|
+
workdir: this.workdir
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
constructor(options) {
|
|
145
|
+
super();
|
|
146
|
+
this.templateRepo = options.templateRepo;
|
|
147
|
+
this.repo = options.repo;
|
|
148
|
+
this.workdir = options.workdir ?? "/repo";
|
|
149
|
+
}
|
|
150
|
+
configureSnapshotSpec(spec) {
|
|
151
|
+
if (this.templateRepo) {
|
|
152
|
+
const composed = this.composeSpecs(
|
|
153
|
+
spec,
|
|
154
|
+
createSnapshotSpec(this.templateRepo)
|
|
155
|
+
);
|
|
156
|
+
return composed;
|
|
157
|
+
}
|
|
158
|
+
return spec;
|
|
159
|
+
}
|
|
160
|
+
configureSpec(spec) {
|
|
161
|
+
if (this.repo) {
|
|
162
|
+
const newSpec = new VmSpec({
|
|
163
|
+
git: {
|
|
164
|
+
repos: [
|
|
165
|
+
{
|
|
166
|
+
repo: this.repo,
|
|
167
|
+
path: this.workdir
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
return this.composeSpecs(spec, newSpec);
|
|
173
|
+
} else {
|
|
174
|
+
return spec;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export { VmDevServer, VmDevServerInstance, createSnapshotSpec };
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@freestyle-sh/with-dev-server",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"freestyle-sandboxes": "^0.1.24",
|
|
7
|
+
"@freestyle-sh/with-nodejs": "^0.2.3"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"source": "./src/index.ts",
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "pkgroll"
|
|
24
|
+
}
|
|
25
|
+
}
|