localssl-cli 0.1.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/README.md +25 -0
- package/bin/localssl.js +81 -0
- package/package.json +37 -0
- package/src/bootstrap.js +175 -0
- package/src/certgen.js +66 -0
- package/src/ci.js +51 -0
- package/src/frameworks/cra.js +42 -0
- package/src/frameworks/detect.js +23 -0
- package/src/frameworks/express.js +17 -0
- package/src/frameworks/index.js +43 -0
- package/src/frameworks/nextjs.js +35 -0
- package/src/frameworks/vite.js +30 -0
- package/src/frameworks/webpack.js +30 -0
- package/src/gitignore.js +30 -0
- package/src/index.js +16 -0
- package/src/init.js +10 -0
- package/src/mobile.js +53 -0
- package/src/remove.js +34 -0
- package/src/renew.js +11 -0
- package/src/status.js +53 -0
- package/src/team.js +110 -0
- package/src/trust/chromium.js +106 -0
- package/src/trust/firefox.js +69 -0
- package/src/trust/linux.js +30 -0
- package/src/trust/macos.js +25 -0
- package/src/trust/windows.js +21 -0
- package/src/trust.js +9 -0
- package/src/use.js +40 -0
- package/src/utils.js +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# localssl
|
|
2
|
+
|
|
3
|
+
One-command local HTTPS for development teams.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i -D localssl
|
|
9
|
+
npx localssl
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Commands
|
|
13
|
+
|
|
14
|
+
- `localssl` / `localssl use` - initialize + create project cert + configure framework
|
|
15
|
+
- `localssl init` - initialize machine CA and trust stores
|
|
16
|
+
- `localssl status` - check expiry and team sync status
|
|
17
|
+
- `localssl renew` - renew project cert
|
|
18
|
+
- `localssl trust` - import team public CAs from `localssl.json`
|
|
19
|
+
- `localssl qr` - serve CA certificate + print QR for phone install
|
|
20
|
+
- `localssl ci` - ephemeral cert setup for CI (`CI=true`)
|
|
21
|
+
- `localssl remove` - uninstall trust and local files
|
|
22
|
+
|
|
23
|
+
## Team sharing
|
|
24
|
+
|
|
25
|
+
`localssl.json` is safe to commit. It stores only public CA certificates.
|
package/bin/localssl.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { Command } = require('commander');
|
|
3
|
+
const { initCommand } = require('../src/init');
|
|
4
|
+
const { useProject } = require('../src/use');
|
|
5
|
+
const { runQrServer } = require('../src/mobile');
|
|
6
|
+
const { runCiSetup } = require('../src/ci');
|
|
7
|
+
const { status } = require('../src/status');
|
|
8
|
+
const { renew } = require('../src/renew');
|
|
9
|
+
const { removeAll } = require('../src/remove');
|
|
10
|
+
const { trustTeam } = require('../src/trust');
|
|
11
|
+
const { isCI, err } = require('../src/utils');
|
|
12
|
+
|
|
13
|
+
const program = new Command();
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.name('localssl')
|
|
17
|
+
.description('One-command local HTTPS setup')
|
|
18
|
+
.version('0.1.0');
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.command('init')
|
|
22
|
+
.description('Initialize machine CA and trust stores')
|
|
23
|
+
.action(runGuard(initCommand));
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.command('use')
|
|
27
|
+
.description('Generate per-project certificate and configure framework')
|
|
28
|
+
.argument('[hosts...]', 'additional hosts')
|
|
29
|
+
.action(runGuard(async (hosts) => useProject(hosts || [])));
|
|
30
|
+
|
|
31
|
+
program
|
|
32
|
+
.command('status')
|
|
33
|
+
.description('Show cert status and expiry')
|
|
34
|
+
.action(runGuard(status));
|
|
35
|
+
|
|
36
|
+
program
|
|
37
|
+
.command('renew')
|
|
38
|
+
.description('Regenerate project certificate')
|
|
39
|
+
.action(runGuard(renew));
|
|
40
|
+
|
|
41
|
+
program
|
|
42
|
+
.command('qr')
|
|
43
|
+
.description('Start mobile CA QR installer server')
|
|
44
|
+
.action(runGuard(runQrServer));
|
|
45
|
+
|
|
46
|
+
program
|
|
47
|
+
.command('ci')
|
|
48
|
+
.description('Setup ephemeral HTTPS for CI')
|
|
49
|
+
.action(runGuard(async () => {
|
|
50
|
+
if (!isCI()) {
|
|
51
|
+
throw new Error('CI mode expected CI=true environment variable.');
|
|
52
|
+
}
|
|
53
|
+
await runCiSetup();
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
program
|
|
57
|
+
.command('trust')
|
|
58
|
+
.description('Trust teammate public CAs from localssl.json')
|
|
59
|
+
.action(runGuard(trustTeam));
|
|
60
|
+
|
|
61
|
+
program
|
|
62
|
+
.command('remove')
|
|
63
|
+
.description('Uninstall localssl from machine and project')
|
|
64
|
+
.action(runGuard(removeAll));
|
|
65
|
+
|
|
66
|
+
program.action(runGuard(async () => {
|
|
67
|
+
await useProject([]);
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
program.parseAsync(process.argv);
|
|
71
|
+
|
|
72
|
+
function runGuard(fn) {
|
|
73
|
+
return async (...args) => {
|
|
74
|
+
try {
|
|
75
|
+
await fn(...args);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
err(error.message || 'localssl failed');
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "localssl-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "One-command local HTTPS setup for teams",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"localssl": "bin/localssl.js"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"start": "node bin/localssl.js",
|
|
15
|
+
"test": "node -e \"console.log('No tests yet')\""
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"https",
|
|
19
|
+
"localhost",
|
|
20
|
+
"mkcert",
|
|
21
|
+
"developer-experience"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"files": [
|
|
25
|
+
"bin",
|
|
26
|
+
"src",
|
|
27
|
+
"README.md",
|
|
28
|
+
"package.json"
|
|
29
|
+
],
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"chalk": "^4.1.2",
|
|
32
|
+
"commander": "^12.1.0",
|
|
33
|
+
"detect-port": "^2.1.0",
|
|
34
|
+
"fs-extra": "^11.2.0",
|
|
35
|
+
"qrcode-terminal": "^0.12.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/bootstrap.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const { execFile } = require('child_process');
|
|
6
|
+
const { LOCALSSL_HOME, LOCALSSL_CA_PUBLIC, step, warn } = require('./utils');
|
|
7
|
+
const { trustCertificate: trustMac } = require('./trust/macos');
|
|
8
|
+
const { trustCertificate: trustWindows } = require('./trust/windows');
|
|
9
|
+
const { trustCertificate: trustLinux } = require('./trust/linux');
|
|
10
|
+
const { trustInFirefox } = require('./trust/firefox');
|
|
11
|
+
const { trustInChromium } = require('./trust/chromium');
|
|
12
|
+
|
|
13
|
+
const MKCERT_VERSION = 'v1.4.4';
|
|
14
|
+
|
|
15
|
+
function getMkcertBinaryPath() {
|
|
16
|
+
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
17
|
+
return path.join(LOCALSSL_HOME, `mkcert${ext}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getMkcertAssetName() {
|
|
21
|
+
const platform = process.platform;
|
|
22
|
+
const arch = os.arch();
|
|
23
|
+
|
|
24
|
+
if (platform === 'win32') {
|
|
25
|
+
if (arch === 'x64') return 'mkcert-v1.4.4-windows-amd64.exe';
|
|
26
|
+
if (arch === 'arm64') return 'mkcert-v1.4.4-windows-arm64.exe';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (platform === 'darwin') {
|
|
30
|
+
if (arch === 'x64') return 'mkcert-v1.4.4-darwin-amd64';
|
|
31
|
+
if (arch === 'arm64') return 'mkcert-v1.4.4-darwin-arm64';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (platform === 'linux') {
|
|
35
|
+
if (arch === 'x64') return 'mkcert-v1.4.4-linux-amd64';
|
|
36
|
+
if (arch === 'arm64') return 'mkcert-v1.4.4-linux-arm64';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
throw new Error(`Unsupported platform/arch: ${platform}/${arch}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function downloadFile(url, outputPath) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const request = https.get(url, (response) => {
|
|
45
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
46
|
+
downloadFile(response.headers.location, outputPath).then(resolve).catch(reject);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (response.statusCode !== 200) {
|
|
51
|
+
reject(new Error(`mkcert download failed: ${response.statusCode}`));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const file = fs.createWriteStream(outputPath, { mode: 0o755 });
|
|
56
|
+
response.pipe(file);
|
|
57
|
+
file.on('finish', () => file.close(resolve));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
request.on('error', reject);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function ensureMkcert() {
|
|
65
|
+
await fs.ensureDir(LOCALSSL_HOME);
|
|
66
|
+
const mkcertPath = getMkcertBinaryPath();
|
|
67
|
+
|
|
68
|
+
if (await fs.pathExists(mkcertPath)) {
|
|
69
|
+
return mkcertPath;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const asset = getMkcertAssetName();
|
|
73
|
+
const url = `https://github.com/FiloSottile/mkcert/releases/download/${MKCERT_VERSION}/${asset}`;
|
|
74
|
+
await downloadFile(url, mkcertPath);
|
|
75
|
+
|
|
76
|
+
if (process.platform !== 'win32') {
|
|
77
|
+
await fs.chmod(mkcertPath, 0o755);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return mkcertPath;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function execMkcert(mkcertPath, args) {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
execFile(mkcertPath, args, { env: { ...process.env, CAROOT: LOCALSSL_HOME } }, (error, stdout, stderr) => {
|
|
86
|
+
if (error) {
|
|
87
|
+
reject(new Error(stderr || error.message));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
resolve(stdout);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function trustSystem(certPath) {
|
|
96
|
+
if (process.platform === 'darwin') {
|
|
97
|
+
await trustMac(certPath);
|
|
98
|
+
return 'macOS system store';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (process.platform === 'win32') {
|
|
102
|
+
await trustWindows(certPath);
|
|
103
|
+
return 'Windows Root store';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
await trustLinux(certPath);
|
|
107
|
+
return 'Linux system store';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function configureNodeExtraCACerts() {
|
|
111
|
+
const value = LOCALSSL_CA_PUBLIC;
|
|
112
|
+
|
|
113
|
+
if (process.platform === 'win32') {
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
execFile('setx', ['NODE_EXTRA_CA_CERTS', value], { windowsHide: true }, () => resolve('setx NODE_EXTRA_CA_CERTS'));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const shellProfile = process.platform === 'darwin' ? path.join(os.homedir(), '.zprofile') : path.join(os.homedir(), '.profile');
|
|
120
|
+
const exportLine = `export NODE_EXTRA_CA_CERTS=\"${value}\"`;
|
|
121
|
+
|
|
122
|
+
let current = '';
|
|
123
|
+
if (await fs.pathExists(shellProfile)) {
|
|
124
|
+
current = await fs.readFile(shellProfile, 'utf8');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!current.includes('NODE_EXTRA_CA_CERTS')) {
|
|
128
|
+
await fs.appendFile(shellProfile, `\n${exportLine}\n`);
|
|
129
|
+
return `updated ${path.basename(shellProfile)}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return `${path.basename(shellProfile)} already configured`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function initMachine({ quiet = false } = {}) {
|
|
136
|
+
await fs.ensureDir(LOCALSSL_HOME);
|
|
137
|
+
const mkcertPath = await ensureMkcert();
|
|
138
|
+
|
|
139
|
+
const hasCA = await fs.pathExists(LOCALSSL_CA_PUBLIC);
|
|
140
|
+
if (hasCA) {
|
|
141
|
+
if (!quiet) {
|
|
142
|
+
step(1, 1, 'Machine CA setup', 'skip', '(already configured)');
|
|
143
|
+
}
|
|
144
|
+
return { mkcertPath, initialized: false };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await execMkcert(mkcertPath, ['-install']);
|
|
148
|
+
const systemResult = await trustSystem(LOCALSSL_CA_PUBLIC);
|
|
149
|
+
const firefoxResult = await trustInFirefox(LOCALSSL_CA_PUBLIC);
|
|
150
|
+
const chromiumResult = await trustInChromium(LOCALSSL_CA_PUBLIC);
|
|
151
|
+
const nodeResult = await configureNodeExtraCACerts();
|
|
152
|
+
|
|
153
|
+
if (!quiet) {
|
|
154
|
+
const firefoxText = firefoxResult.trusted ? `+ Firefox (${firefoxResult.reason})` : `+ Firefox skipped (${firefoxResult.reason})`;
|
|
155
|
+
const chromiumText = chromiumResult.trusted ? `+ Chrome/Edge (${chromiumResult.reason})` : `+ Chrome/Edge skipped (${chromiumResult.reason})`;
|
|
156
|
+
step(1, 1, 'Installing local CA', 'ok', `(${systemResult} ${firefoxText} ${chromiumText}; ${nodeResult})`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!firefoxResult.trusted) {
|
|
160
|
+
warn(`Firefox trust skipped: ${firefoxResult.reason}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!chromiumResult.trusted) {
|
|
164
|
+
warn(`Chrome/Edge NSS trust skipped: ${chromiumResult.reason}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { mkcertPath, initialized: true };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = {
|
|
171
|
+
initMachine,
|
|
172
|
+
ensureMkcert,
|
|
173
|
+
execMkcert,
|
|
174
|
+
getMkcertBinaryPath
|
|
175
|
+
};
|
package/src/certgen.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const { PROJECT_DIR, PROJECT_LOCALSSL_DIR, PROJECT_CERT, PROJECT_KEY } = require('./utils');
|
|
4
|
+
const { execMkcert } = require('./bootstrap');
|
|
5
|
+
|
|
6
|
+
function parseHostsFromUrl(value) {
|
|
7
|
+
try {
|
|
8
|
+
const normalized = value.startsWith('http') ? value : `https://${value}`;
|
|
9
|
+
const host = new URL(normalized).hostname;
|
|
10
|
+
return host ? [host] : [];
|
|
11
|
+
} catch {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function detectHostsFromProject() {
|
|
17
|
+
const hosts = new Set(['localhost', '127.0.0.1', '::1']);
|
|
18
|
+
|
|
19
|
+
const packageJsonPath = path.join(PROJECT_DIR, 'package.json');
|
|
20
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
21
|
+
const pkg = await fs.readJson(packageJsonPath).catch(() => ({}));
|
|
22
|
+
if (typeof pkg.proxy === 'string') {
|
|
23
|
+
parseHostsFromUrl(pkg.proxy).forEach((host) => hosts.add(host));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const file of ['.env', '.env.local']) {
|
|
28
|
+
const envPath = path.join(PROJECT_DIR, file);
|
|
29
|
+
if (!(await fs.pathExists(envPath))) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const content = await fs.readFile(envPath, 'utf8');
|
|
34
|
+
const lines = content.split(/\r?\n/);
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
const [key, raw] = line.split('=');
|
|
37
|
+
if (!key || !raw) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const value = raw.trim().replace(/^['"]|['"]$/g, '');
|
|
42
|
+
if (/VITE_DEV_SERVER_HOST|NEXT_PUBLIC_URL|APP_URL|HOST|DEV_URL/i.test(key)) {
|
|
43
|
+
parseHostsFromUrl(value).forEach((host) => hosts.add(host));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return [...hosts];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function generateProjectCert({ mkcertPath, hosts, force = false }) {
|
|
52
|
+
await fs.ensureDir(PROJECT_LOCALSSL_DIR);
|
|
53
|
+
|
|
54
|
+
const certExists = (await fs.pathExists(PROJECT_CERT)) && (await fs.pathExists(PROJECT_KEY));
|
|
55
|
+
if (certExists && !force) {
|
|
56
|
+
return { generated: false, certPath: PROJECT_CERT, keyPath: PROJECT_KEY };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await execMkcert(mkcertPath, ['-cert-file', PROJECT_CERT, '-key-file', PROJECT_KEY, ...hosts]);
|
|
60
|
+
return { generated: true, certPath: PROJECT_CERT, keyPath: PROJECT_KEY };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
detectHostsFromProject,
|
|
65
|
+
generateProjectCert
|
|
66
|
+
};
|
package/src/ci.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const { execFile } = require('child_process');
|
|
5
|
+
const { ensureMkcert } = require('./bootstrap');
|
|
6
|
+
|
|
7
|
+
function execWithEnv(file, args, env) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
execFile(file, args, { env }, (error, stdout, stderr) => {
|
|
10
|
+
if (error) {
|
|
11
|
+
reject(new Error(stderr || error.message));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
resolve(stdout);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function runCiSetup() {
|
|
20
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'localssl-'));
|
|
21
|
+
const certDir = path.join(tempRoot, 'certs');
|
|
22
|
+
const cert = path.join(certDir, 'cert.pem');
|
|
23
|
+
const key = path.join(certDir, 'key.pem');
|
|
24
|
+
const ca = path.join(tempRoot, 'rootCA.pem');
|
|
25
|
+
await fs.ensureDir(certDir);
|
|
26
|
+
|
|
27
|
+
const mkcertPath = await ensureMkcert();
|
|
28
|
+
const env = { ...process.env, CAROOT: tempRoot };
|
|
29
|
+
await execWithEnv(mkcertPath, ['-install'], env);
|
|
30
|
+
await execWithEnv(mkcertPath, ['-cert-file', cert, '-key-file', key, 'localhost', '127.0.0.1', '::1'], env);
|
|
31
|
+
|
|
32
|
+
const vars = {
|
|
33
|
+
NODE_EXTRA_CA_CERTS: ca,
|
|
34
|
+
SSL_CERT_FILE: ca,
|
|
35
|
+
REQUESTS_CA_BUNDLE: ca,
|
|
36
|
+
LOCALSSL_CERT_FILE: cert,
|
|
37
|
+
LOCALSSL_KEY_FILE: key
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (process.env.GITHUB_ENV) {
|
|
41
|
+
const lines = Object.entries(vars).map(([k, v]) => `${k}=${v}`).join('\n') + '\n';
|
|
42
|
+
await fs.appendFile(process.env.GITHUB_ENV, lines);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log(' CI HTTPS ready ✓');
|
|
46
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
47
|
+
console.log(` export ${k}=${v}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { runCiSetup };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const { PROJECT_DIR } = require('../utils');
|
|
4
|
+
|
|
5
|
+
async function configureCra(certPath, keyPath) {
|
|
6
|
+
const envLocalPath = path.join(PROJECT_DIR, '.env.local');
|
|
7
|
+
const vars = {
|
|
8
|
+
HTTPS: 'true',
|
|
9
|
+
SSL_CRT_FILE: certPath,
|
|
10
|
+
SSL_KEY_FILE: keyPath
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let current = '';
|
|
14
|
+
if (await fs.pathExists(envLocalPath)) {
|
|
15
|
+
current = await fs.readFile(envLocalPath, 'utf8');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let changed = false;
|
|
19
|
+
let updated = current;
|
|
20
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
21
|
+
const line = `${key}=${value}`;
|
|
22
|
+
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
23
|
+
if (regex.test(updated)) {
|
|
24
|
+
if (!updated.includes(line)) {
|
|
25
|
+
updated = updated.replace(regex, line);
|
|
26
|
+
changed = true;
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
updated += `${updated.endsWith('\n') || !updated ? '' : '\n'}${line}\n`;
|
|
30
|
+
changed = true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!changed) {
|
|
35
|
+
return { updated: false, details: 'already configured' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await fs.writeFile(envLocalPath, updated);
|
|
39
|
+
return { updated: true, details: '.env.local updated' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { configureCra };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const { PROJECT_DIR } = require('../utils');
|
|
4
|
+
|
|
5
|
+
async function detectFramework() {
|
|
6
|
+
const packageJsonPath = path.join(PROJECT_DIR, 'package.json');
|
|
7
|
+
if (!(await fs.pathExists(packageJsonPath))) {
|
|
8
|
+
return 'generic';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const pkg = await fs.readJson(packageJsonPath).catch(() => ({}));
|
|
12
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
13
|
+
|
|
14
|
+
if (deps.vite) return 'vite';
|
|
15
|
+
if (deps.next) return 'nextjs';
|
|
16
|
+
if (deps['react-scripts']) return 'cra';
|
|
17
|
+
if (deps.express) return 'express';
|
|
18
|
+
if (deps['webpack-dev-server']) return 'webpack';
|
|
19
|
+
|
|
20
|
+
return 'generic';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { detectFramework };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const { PROJECT_DIR } = require('../utils');
|
|
4
|
+
|
|
5
|
+
async function configureExpress(certPath, keyPath) {
|
|
6
|
+
const helperPath = path.join(PROJECT_DIR, 'localssl.js');
|
|
7
|
+
if (await fs.pathExists(helperPath)) {
|
|
8
|
+
return { updated: false, details: 'localssl.js already exists' };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const content = `const fs = require('fs');\n\nconst httpsOptions = {\n cert: fs.readFileSync('${certPath.replace(/\\/g, '/')}'),\n key: fs.readFileSync('${keyPath.replace(/\\/g, '/')}')\n};\n\nmodule.exports = { httpsOptions };\n`;
|
|
12
|
+
|
|
13
|
+
await fs.writeFile(helperPath, content);
|
|
14
|
+
return { updated: true, details: 'localssl.js created' };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = { configureExpress };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const { detectFramework } = require('./detect');
|
|
2
|
+
const { configureVite } = require('./vite');
|
|
3
|
+
const { configureNext } = require('./nextjs');
|
|
4
|
+
const { configureCra } = require('./cra');
|
|
5
|
+
const { configureExpress } = require('./express');
|
|
6
|
+
const { configureWebpack } = require('./webpack');
|
|
7
|
+
|
|
8
|
+
async function configureFramework(certPath, keyPath) {
|
|
9
|
+
const framework = await detectFramework();
|
|
10
|
+
|
|
11
|
+
if (framework === 'vite') {
|
|
12
|
+
const result = await configureVite(certPath, keyPath);
|
|
13
|
+
return { framework: 'Vite', ...result };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (framework === 'nextjs') {
|
|
17
|
+
const result = await configureNext(certPath, keyPath);
|
|
18
|
+
return { framework: 'Next.js', ...result };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (framework === 'cra') {
|
|
22
|
+
const result = await configureCra(certPath, keyPath);
|
|
23
|
+
return { framework: 'Create React App', ...result };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (framework === 'express') {
|
|
27
|
+
const result = await configureExpress(certPath, keyPath);
|
|
28
|
+
return { framework: 'Express', ...result };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (framework === 'webpack') {
|
|
32
|
+
const result = await configureWebpack(certPath, keyPath);
|
|
33
|
+
return { framework: 'Webpack Dev Server', ...result };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
framework: 'Generic',
|
|
38
|
+
updated: false,
|
|
39
|
+
details: `Use cert: ${certPath} and key: ${keyPath}`
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { configureFramework };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const { PROJECT_DIR } = require('../utils');
|
|
4
|
+
|
|
5
|
+
function ensureHttpsFlag(script, certPath, keyPath) {
|
|
6
|
+
if (!script || script.includes('--experimental-https')) {
|
|
7
|
+
return script;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const cert = certPath.replace(/\\/g, '/');
|
|
11
|
+
const key = keyPath.replace(/\\/g, '/');
|
|
12
|
+
return `${script} --experimental-https --experimental-https-key ${key} --experimental-https-cert ${cert}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function configureNext(certPath, keyPath) {
|
|
16
|
+
const packageJsonPath = path.join(PROJECT_DIR, 'package.json');
|
|
17
|
+
if (!(await fs.pathExists(packageJsonPath))) {
|
|
18
|
+
return { updated: false, details: 'package.json not found' };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const pkg = await fs.readJson(packageJsonPath);
|
|
22
|
+
pkg.scripts = pkg.scripts || {};
|
|
23
|
+
const before = pkg.scripts.dev || 'next dev';
|
|
24
|
+
const nextScript = ensureHttpsFlag(before, certPath, keyPath);
|
|
25
|
+
|
|
26
|
+
if (nextScript === before) {
|
|
27
|
+
return { updated: false, details: 'already configured' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
pkg.scripts.dev = nextScript;
|
|
31
|
+
await fs.writeJson(packageJsonPath, pkg, { spaces: 2 });
|
|
32
|
+
return { updated: true, details: 'package.json scripts updated' };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { configureNext };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const { PROJECT_DIR } = require('../utils');
|
|
4
|
+
|
|
5
|
+
async function configureVite(certPath, keyPath) {
|
|
6
|
+
const tsPath = path.join(PROJECT_DIR, 'vite.config.ts');
|
|
7
|
+
const jsPath = path.join(PROJECT_DIR, 'vite.config.js');
|
|
8
|
+
const configPath = (await fs.pathExists(tsPath)) ? tsPath : jsPath;
|
|
9
|
+
|
|
10
|
+
if (!configPath || !(await fs.pathExists(configPath))) {
|
|
11
|
+
const content = `import { defineConfig } from 'vite';\nimport fs from 'fs';\n\nexport default defineConfig({\n server: {\n https: {\n cert: fs.readFileSync('${certPath.replace(/\\/g, '/')}'),\n key: fs.readFileSync('${keyPath.replace(/\\/g, '/')}')\n }\n }\n});\n`;
|
|
12
|
+
await fs.writeFile(tsPath, content);
|
|
13
|
+
return { updated: true, details: 'vite.config.ts created' };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const existing = await fs.readFile(configPath, 'utf8');
|
|
17
|
+
if (/server\s*:\s*{[\s\S]*https\s*:/m.test(existing) || /https\s*:\s*true/m.test(existing)) {
|
|
18
|
+
return { updated: false, details: 'already configured' };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const injection = `\n\n// localssl\nimport fs from 'fs';\n\nconst localsslHttps = {\n cert: fs.readFileSync('${certPath.replace(/\\/g, '/')}'),\n key: fs.readFileSync('${keyPath.replace(/\\/g, '/')}')\n};\n`;
|
|
22
|
+
|
|
23
|
+
const updated = existing.replace(/defineConfig\s*\(\s*{/, `defineConfig({\n server: { https: localsslHttps },`);
|
|
24
|
+
const finalText = updated === existing ? `${existing}${injection}` : `${injection}${updated}`;
|
|
25
|
+
await fs.writeFile(configPath, finalText);
|
|
26
|
+
|
|
27
|
+
return { updated: true, details: path.basename(configPath) + ' updated' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { configureVite };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const { PROJECT_DIR } = require('../utils');
|
|
4
|
+
|
|
5
|
+
async function configureWebpack(certPath, keyPath) {
|
|
6
|
+
const configPath = path.join(PROJECT_DIR, 'webpack.config.js');
|
|
7
|
+
if (!(await fs.pathExists(configPath))) {
|
|
8
|
+
return { updated: false, details: 'webpack.config.js not found' };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const existing = await fs.readFile(configPath, 'utf8');
|
|
12
|
+
if (/devServer\s*:\s*{[\s\S]*https\s*:/m.test(existing)) {
|
|
13
|
+
return { updated: false, details: 'already configured' };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const injection = `\nconst fs = require('fs');\n`;
|
|
17
|
+
const httpsBlock = `https: { cert: fs.readFileSync('${certPath.replace(/\\/g, '/')}'), key: fs.readFileSync('${keyPath.replace(/\\/g, '/')}') },`;
|
|
18
|
+
|
|
19
|
+
let updated = existing;
|
|
20
|
+
if (!updated.includes("const fs = require('fs');")) {
|
|
21
|
+
updated = `${injection}${updated}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
updated = updated.replace(/devServer\s*:\s*{/, `devServer: {\n ${httpsBlock}`);
|
|
25
|
+
await fs.writeFile(configPath, updated);
|
|
26
|
+
|
|
27
|
+
return { updated: true, details: 'webpack.config.js updated' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { configureWebpack };
|