@madhanmaaz/udane 0.0.0-stage → 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/app.js +95 -0
- package/config/udane.config.js +47 -0
- package/constants/index.constant.js +4 -0
- package/helpers/directory.helper.js +74 -0
- package/helpers/getConfig.helper.js +187 -0
- package/helpers/ignore.helper.js +47 -0
- package/helpers/templateEngine.helper.js +10 -0
- package/lib/bin.js +125 -0
- package/lib/udane.js +134 -0
- package/license +21 -0
- package/middleware/inject.middleware.js +42 -0
- package/package.json +42 -5
- package/public/css/base/global.css +10 -0
- package/public/css/base/root.css +136 -0
- package/public/css/components/input.css +41 -0
- package/public/css/pages/listing.css +88 -0
- package/public/favicon.ico +0 -0
- package/public/fonts/Roboto-Medium.ttf +0 -0
- package/public/images/file.png +0 -0
- package/public/images/folder.png +0 -0
- package/public/images/logo.png +0 -0
- package/public/js/client.js +56 -0
- package/readme.md +124 -0
- package/views/error.tark +35 -0
- package/views/listing.tark +102 -0
- package/README.md +0 -3
package/app.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const path = require("node:path");
|
|
2
|
+
const http = require("node:http");
|
|
3
|
+
|
|
4
|
+
const fastify = require("fastify");
|
|
5
|
+
const tarkine = require("tarkine");
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
listDirectory,
|
|
9
|
+
resolveDirectory,
|
|
10
|
+
} = require("./helpers/directory.helper");
|
|
11
|
+
const templateEngine = require("./helpers/templateEngine.helper");
|
|
12
|
+
const { PUBLIC_DIR_PREFIX } = require("./constants/index.constant");
|
|
13
|
+
|
|
14
|
+
const noStore = {
|
|
15
|
+
cacheControl: false,
|
|
16
|
+
setHeaders(reply) {
|
|
17
|
+
reply.header("cache-control", "no-store");
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
module.exports = function buildApp(appConfig = {}, fastifyOptions = {}) {
|
|
22
|
+
const app = fastify(fastifyOptions);
|
|
23
|
+
|
|
24
|
+
if (appConfig.cors) {
|
|
25
|
+
app.addHook("onRequest", async (request, reply) => {
|
|
26
|
+
reply.header("access-control-allow-origin", "*");
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// plugins
|
|
31
|
+
app.decorateReply("render", tarkine.adapters.fastify(templateEngine));
|
|
32
|
+
|
|
33
|
+
// inject script
|
|
34
|
+
app.addHook("onSend", require("./middleware/inject.middleware"));
|
|
35
|
+
|
|
36
|
+
// udane's own assets. This is the first static instance, so it owns the reply decorator.
|
|
37
|
+
app.register(require("@fastify/static"), {
|
|
38
|
+
root: path.join(__dirname, "public"),
|
|
39
|
+
prefix: `/${PUBLIC_DIR_PREFIX}/`,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// extra folders from appConfig.mount
|
|
43
|
+
for (const [prefix, root] of Object.entries(appConfig.mount)) {
|
|
44
|
+
app.register(require("@fastify/static"), {
|
|
45
|
+
root,
|
|
46
|
+
prefix: `${prefix}/`,
|
|
47
|
+
decorateReply: false,
|
|
48
|
+
...noStore,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// the project being served
|
|
53
|
+
app.register(require("@fastify/static"), {
|
|
54
|
+
root: appConfig.root,
|
|
55
|
+
index: appConfig.index,
|
|
56
|
+
decorateReply: false,
|
|
57
|
+
...noStore,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
app.setErrorHandler((error, request, reply) => {
|
|
61
|
+
const statusCode = error.statusCode || 500;
|
|
62
|
+
const title = http.STATUS_CODES[statusCode];
|
|
63
|
+
|
|
64
|
+
reply.render("/error", {
|
|
65
|
+
PUBLIC_DIR_PREFIX,
|
|
66
|
+
statusCode,
|
|
67
|
+
title,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
app.setNotFoundHandler(async (request, reply) => {
|
|
72
|
+
const { pathname } = new URL(request.url, "http://localhost");
|
|
73
|
+
|
|
74
|
+
if (request.method === "GET") {
|
|
75
|
+
const folder = resolveDirectory(pathname, appConfig);
|
|
76
|
+
|
|
77
|
+
if (folder) {
|
|
78
|
+
const listing = await listDirectory(folder, pathname);
|
|
79
|
+
|
|
80
|
+
return reply.render("listing", {
|
|
81
|
+
PUBLIC_DIR_PREFIX,
|
|
82
|
+
listing,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return reply.render("/error", {
|
|
88
|
+
statusCode: 404,
|
|
89
|
+
title: "Not Found",
|
|
90
|
+
PUBLIC_DIR_PREFIX,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return app;
|
|
95
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// config/udane.config.js
|
|
2
|
+
// Default configuration for udane.
|
|
3
|
+
// Priority (lowest to highest): these defaults < udane.config.js in the user's cwd < CLI flags.
|
|
4
|
+
// Every option is listed here so this file doubles as the reference.
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
// "0.0.0.0" exposes the server to the local network (phone testing, other devices).
|
|
8
|
+
// Use "127.0.0.1" to keep it private to this machine.
|
|
9
|
+
host: "0.0.0.0",
|
|
10
|
+
|
|
11
|
+
// Port to listen on. If it is busy, udane tries the next free port.
|
|
12
|
+
port: 5300,
|
|
13
|
+
|
|
14
|
+
// Folder to serve, relative to the current working directory.
|
|
15
|
+
root: ".",
|
|
16
|
+
|
|
17
|
+
// Extra folders served under a URL prefix. Empty by default, because with
|
|
18
|
+
// host "0.0.0.0" everything mounted here is visible to the whole network.
|
|
19
|
+
// Keys are URL prefixes, values are folders ("~" is expanded to the home directory).
|
|
20
|
+
// Example: { "/downloads": "~/Downloads" }
|
|
21
|
+
mount: {},
|
|
22
|
+
|
|
23
|
+
// Entry files tried for a directory, in order.
|
|
24
|
+
index: ["index.html"],
|
|
25
|
+
|
|
26
|
+
// Send "Access-Control-Allow-Origin: *" on every response.
|
|
27
|
+
cors: false,
|
|
28
|
+
|
|
29
|
+
// Swap changed CSS in place without a full page reload.
|
|
30
|
+
cssHotSwap: true,
|
|
31
|
+
|
|
32
|
+
// Milliseconds to wait after the last file change before notifying the browser.
|
|
33
|
+
// Prevents several reloads when an editor writes many files at once.
|
|
34
|
+
debounce: 100,
|
|
35
|
+
|
|
36
|
+
// Paths to watch, relative to root. Empty array means watch the whole root.
|
|
37
|
+
watch: [],
|
|
38
|
+
|
|
39
|
+
// Paths and patterns the watcher ignores.
|
|
40
|
+
ignore: ["node_modules", ".git", ".DS_Store", "*.log"],
|
|
41
|
+
|
|
42
|
+
// Print one line per request.
|
|
43
|
+
logRequests: true,
|
|
44
|
+
|
|
45
|
+
// Print a line when a watched file changes.
|
|
46
|
+
logChanges: true,
|
|
47
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
|
|
4
|
+
// Maps a URL path to a real folder inside a mount or root. Returns null if it is not one.
|
|
5
|
+
function resolveDirectory(pathname, config) {
|
|
6
|
+
let decoded;
|
|
7
|
+
try {
|
|
8
|
+
decoded = decodeURIComponent(pathname);
|
|
9
|
+
} catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// longest mount prefix first, root last
|
|
14
|
+
const targets = [
|
|
15
|
+
...Object.entries(config.mount)
|
|
16
|
+
.map(([prefix, dir]) => ({ prefix, dir }))
|
|
17
|
+
.sort((a, b) => b.prefix.length - a.prefix.length),
|
|
18
|
+
{ prefix: "", dir: config.root },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
for (const { prefix, dir } of targets) {
|
|
22
|
+
if (prefix && decoded !== prefix && !decoded.startsWith(`${prefix}/`))
|
|
23
|
+
continue;
|
|
24
|
+
|
|
25
|
+
const full = path.resolve(dir, `.${decoded.slice(prefix.length)}`);
|
|
26
|
+
const inside = path.relative(dir, full);
|
|
27
|
+
|
|
28
|
+
// blocks "../" traversal outside the served folder
|
|
29
|
+
if (inside.startsWith("..") || path.isAbsolute(inside)) continue;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
if (fs.statSync(full).isDirectory()) return full;
|
|
33
|
+
} catch {
|
|
34
|
+
// not found in this target, try the next one
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function listDirectory(full, pathname) {
|
|
42
|
+
const base = pathname.endsWith("/") ? pathname : `${pathname}/`;
|
|
43
|
+
const items = await fs.promises.readdir(full, { withFileTypes: true });
|
|
44
|
+
|
|
45
|
+
const entries = items
|
|
46
|
+
.filter((item) => !item.name.startsWith("."))
|
|
47
|
+
.map((item) => {
|
|
48
|
+
const isDirectory = item.isDirectory();
|
|
49
|
+
return {
|
|
50
|
+
name: isDirectory ? `${item.name}/` : item.name,
|
|
51
|
+
href:
|
|
52
|
+
base +
|
|
53
|
+
encodeURIComponent(item.name) +
|
|
54
|
+
(isDirectory ? "/" : ""),
|
|
55
|
+
isDirectory,
|
|
56
|
+
};
|
|
57
|
+
})
|
|
58
|
+
.sort(
|
|
59
|
+
(a, b) =>
|
|
60
|
+
Number(b.isDirectory) - Number(a.isDirectory) ||
|
|
61
|
+
a.name.localeCompare(b.name),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
parent: base === "/" ? null : base.replace(/[^/]+\/$/, ""),
|
|
66
|
+
base,
|
|
67
|
+
entries,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
resolveDirectory,
|
|
73
|
+
listDirectory,
|
|
74
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
const process = require("node:process");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const os = require("node:os");
|
|
5
|
+
|
|
6
|
+
const { z } = require("zod");
|
|
7
|
+
|
|
8
|
+
const defaults = require("../config/udane.config");
|
|
9
|
+
const { PUBLIC_DIR_PREFIX } = require("../constants/index.constant");
|
|
10
|
+
|
|
11
|
+
const NESTED = ["mount"];
|
|
12
|
+
const CONFIG_NAME = "udane.config.js";
|
|
13
|
+
const RESERVED_PREFIX = `/${PUBLIC_DIR_PREFIX}`;
|
|
14
|
+
|
|
15
|
+
const configSchema = z
|
|
16
|
+
.object({
|
|
17
|
+
host: z.string(),
|
|
18
|
+
port: z.number().int().min(1).max(65535),
|
|
19
|
+
root: z.string(),
|
|
20
|
+
mount: z.record(z.string().startsWith("/"), z.string()),
|
|
21
|
+
index: z.array(z.string()),
|
|
22
|
+
cors: z.boolean(),
|
|
23
|
+
cssHotSwap: z.boolean(),
|
|
24
|
+
debounce: z.number().int().min(0),
|
|
25
|
+
watch: z.array(z.string()),
|
|
26
|
+
ignore: z.array(z.string()),
|
|
27
|
+
logRequests: z.boolean(),
|
|
28
|
+
logChanges: z.boolean(),
|
|
29
|
+
})
|
|
30
|
+
.strict();
|
|
31
|
+
|
|
32
|
+
function isPlainObject(value) {
|
|
33
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Drops undefined values so an unset CLI flag never overwrites a real value.
|
|
37
|
+
function clean(obj) {
|
|
38
|
+
return Object.fromEntries(
|
|
39
|
+
Object.entries(obj).filter(([, value]) => value !== undefined),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Looks for udane.config.js inside a folder. Returns { path, config } or null.
|
|
44
|
+
function loadConfigFile(dir) {
|
|
45
|
+
const file = path.join(dir, CONFIG_NAME);
|
|
46
|
+
if (!fs.existsSync(file)) return null;
|
|
47
|
+
|
|
48
|
+
let config;
|
|
49
|
+
try {
|
|
50
|
+
config = require(file);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
throw new Error(`Failed to load ${file}: ${err.message}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!isPlainObject(config)) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`${file} must export an object (module.exports = { ... })`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { path: file, config };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function mergeConfigs(...sources) {
|
|
65
|
+
const result = {};
|
|
66
|
+
|
|
67
|
+
for (const source of sources) {
|
|
68
|
+
for (const [key, value] of Object.entries(source)) {
|
|
69
|
+
result[key] =
|
|
70
|
+
NESTED.includes(key) && isPlainObject(value)
|
|
71
|
+
? { ...result[key], ...value }
|
|
72
|
+
: value;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatIssues(error, files) {
|
|
80
|
+
const where = files.length ? ` (config: ${files.join(", ")})` : "";
|
|
81
|
+
const lines = error.issues.map((issue) => {
|
|
82
|
+
const key =
|
|
83
|
+
issue.code === "unrecognized_keys"
|
|
84
|
+
? issue.keys.join(", ")
|
|
85
|
+
: issue.path.join(".") || "(root)";
|
|
86
|
+
return ` ${key}: ${issue.message}`;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return `Invalid config${where}\n${lines.join("\n")}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Expands "~" (Node does not) and resolves to an absolute path.
|
|
93
|
+
function expandPath(target, cwd) {
|
|
94
|
+
let value = target;
|
|
95
|
+
|
|
96
|
+
if (value === "~") {
|
|
97
|
+
value = os.homedir();
|
|
98
|
+
} else if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
99
|
+
value = path.join(os.homedir(), value.slice(2));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return path.resolve(cwd, value);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function assertFolder(folder, label) {
|
|
106
|
+
let stat;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
stat = fs.statSync(folder);
|
|
110
|
+
} catch {
|
|
111
|
+
throw new Error(`${label} not found: ${folder}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!stat.isDirectory()) {
|
|
115
|
+
throw new Error(`${label} is not a folder: ${folder}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizeMount(mount, cwd) {
|
|
120
|
+
const result = {};
|
|
121
|
+
|
|
122
|
+
for (const [prefix, target] of Object.entries(mount)) {
|
|
123
|
+
const key = prefix.replace(/\/+$/, "");
|
|
124
|
+
|
|
125
|
+
if (!key) {
|
|
126
|
+
throw new Error(`mount: "/" is not allowed as a prefix`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (key === RESERVED_PREFIX || key.startsWith(`${RESERVED_PREFIX}/`)) {
|
|
130
|
+
throw new Error(`mount: "${prefix}" is reserved by udane`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const folder = expandPath(target, cwd);
|
|
134
|
+
assertFolder(folder, `mount "${prefix}"`);
|
|
135
|
+
result[key] = folder;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalize(config, cwd) {
|
|
142
|
+
const root = expandPath(config.root, cwd);
|
|
143
|
+
assertFolder(root, "root");
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
...config,
|
|
147
|
+
root,
|
|
148
|
+
mount: normalizeMount(config.mount, cwd),
|
|
149
|
+
// Empty list means watch the whole root.
|
|
150
|
+
watch: config.watch.length
|
|
151
|
+
? config.watch.map((item) => path.resolve(root, item))
|
|
152
|
+
: [root],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Priority (lowest to highest):
|
|
157
|
+
// defaults < ~/.config/udane.config.js < udane.config.js in cwd < options (CLI flags)
|
|
158
|
+
module.exports = function getConfig(options = {}) {
|
|
159
|
+
const cwd = process.cwd();
|
|
160
|
+
|
|
161
|
+
const files = [
|
|
162
|
+
loadConfigFile(path.join(os.homedir(), ".config")),
|
|
163
|
+
loadConfigFile(cwd),
|
|
164
|
+
].filter(Boolean);
|
|
165
|
+
|
|
166
|
+
const merged = mergeConfigs(
|
|
167
|
+
defaults,
|
|
168
|
+
...files.map((file) => file.config),
|
|
169
|
+
clean(options),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const result = configSchema.safeParse(merged);
|
|
173
|
+
|
|
174
|
+
if (!result.success) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
formatIssues(
|
|
177
|
+
result.error,
|
|
178
|
+
files.map((file) => file.path),
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return Object.freeze({
|
|
184
|
+
...normalize(result.data, cwd),
|
|
185
|
+
configFiles: files.map((file) => file.path),
|
|
186
|
+
});
|
|
187
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Builds the `ignored` function for chokidar from config.ignore.
|
|
2
|
+
// - a pattern without "/" matches any file or folder name ("node_modules", "*.log", ".git")
|
|
3
|
+
// - a pattern with "/" matches a path fragment ("dist/cache")
|
|
4
|
+
// - "*" matches any characters, "?" matches one character
|
|
5
|
+
// Matching ignores the part of the path above the watched root, so serving a
|
|
6
|
+
// project that lives inside a folder like "node_modules" still works.
|
|
7
|
+
|
|
8
|
+
const escape = (text) => text.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
9
|
+
|
|
10
|
+
const toRegex = (pattern) =>
|
|
11
|
+
new RegExp(`^${escape(pattern).replace(/\*/g, ".*").replace(/\?/g, ".")}$`);
|
|
12
|
+
|
|
13
|
+
module.exports = function createIgnore(patterns, roots) {
|
|
14
|
+
const matchers = patterns.map((pattern) => {
|
|
15
|
+
const clean = pattern
|
|
16
|
+
.replace(/\\/g, "/")
|
|
17
|
+
.replace(/^\.?\//, "")
|
|
18
|
+
.replace(/\/+$/, "");
|
|
19
|
+
|
|
20
|
+
return { clean, hasSlash: clean.includes("/"), regex: toRegex(clean) };
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const bases = roots.map((root) =>
|
|
24
|
+
root.replace(/\\/g, "/").replace(/\/+$/, ""),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return (filepath) => {
|
|
28
|
+
let normalized = filepath.replace(/\\/g, "/");
|
|
29
|
+
|
|
30
|
+
const base = bases.find(
|
|
31
|
+
(item) => normalized === item || normalized.startsWith(`${item}/`),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (base) {
|
|
35
|
+
normalized = normalized.slice(base.length);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
39
|
+
|
|
40
|
+
return matchers.some(({ clean, hasSlash, regex }) =>
|
|
41
|
+
hasSlash
|
|
42
|
+
? normalized.endsWith(`/${clean}`) ||
|
|
43
|
+
normalized.includes(`/${clean}/`)
|
|
44
|
+
: segments.some((segment) => regex.test(segment)),
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
};
|
package/lib/bin.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const process = require("node:process");
|
|
4
|
+
const { parseArgs } = require("node:util");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const fs = require("node:fs");
|
|
7
|
+
|
|
8
|
+
const udane = require("./udane");
|
|
9
|
+
const pkg = require("../package.json");
|
|
10
|
+
|
|
11
|
+
const HELP = `
|
|
12
|
+
udane v${pkg.version}
|
|
13
|
+
Live-reload development server.
|
|
14
|
+
|
|
15
|
+
Usage
|
|
16
|
+
udane [root] [options]
|
|
17
|
+
udane init [--force]
|
|
18
|
+
|
|
19
|
+
Commands
|
|
20
|
+
init Copy the default config file into the current folder
|
|
21
|
+
|
|
22
|
+
Options
|
|
23
|
+
-p, --port <number> Port to listen on
|
|
24
|
+
-H, --host <address> Interface to bind (0.0.0.0 = network, 127.0.0.1 = local only)
|
|
25
|
+
--cors Send Access-Control-Allow-Origin: *
|
|
26
|
+
--debounce <ms> Wait time after the last file change
|
|
27
|
+
--watch <path> Path to watch (repeatable)
|
|
28
|
+
--ignore <pattern> Pattern to ignore (repeatable)
|
|
29
|
+
-f, --force Overwrite an existing config file (init only)
|
|
30
|
+
-h, --help Show this help
|
|
31
|
+
-v, --version Show the version
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
function readArgs(argv) {
|
|
35
|
+
const { values, positionals } = parseArgs({
|
|
36
|
+
args: argv,
|
|
37
|
+
allowPositionals: true,
|
|
38
|
+
options: {
|
|
39
|
+
port: { type: "string", short: "p" },
|
|
40
|
+
host: { type: "string", short: "H" },
|
|
41
|
+
cors: { type: "boolean" },
|
|
42
|
+
debounce: { type: "string" },
|
|
43
|
+
watch: { type: "string", multiple: true },
|
|
44
|
+
ignore: { type: "string", multiple: true },
|
|
45
|
+
force: { type: "boolean", short: "f" },
|
|
46
|
+
help: { type: "boolean", short: "h" },
|
|
47
|
+
version: { type: "boolean", short: "v" },
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const number = (value) => (value === undefined ? undefined : Number(value));
|
|
52
|
+
const command = positionals[0] === "init" ? "init" : undefined;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
command,
|
|
56
|
+
force: values.force,
|
|
57
|
+
help: values.help,
|
|
58
|
+
version: values.version,
|
|
59
|
+
|
|
60
|
+
// undefined values are dropped by getConfig, so unset flags never override config
|
|
61
|
+
options: {
|
|
62
|
+
root: command ? undefined : positionals[0],
|
|
63
|
+
port: number(values.port),
|
|
64
|
+
host: values.host,
|
|
65
|
+
cors: values.cors,
|
|
66
|
+
debounce: number(values.debounce),
|
|
67
|
+
watch: values.watch,
|
|
68
|
+
ignore: values.ignore,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function init(force) {
|
|
74
|
+
const source = path.join(__dirname, "..", "config", "udane.config.js");
|
|
75
|
+
const target = path.join(process.cwd(), "udane.config.js");
|
|
76
|
+
|
|
77
|
+
if (fs.existsSync(target) && !force) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
"udane.config.js already exists (use --force to overwrite)",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fs.copyFileSync(source, target);
|
|
84
|
+
console.log(`[+] Created ${target}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function main() {
|
|
88
|
+
const { command, force, help, version, options } = readArgs(
|
|
89
|
+
process.argv.slice(2),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (help) return console.log(HELP);
|
|
93
|
+
if (version) return console.log(pkg.version);
|
|
94
|
+
if (command === "init") return init(force);
|
|
95
|
+
|
|
96
|
+
const server = await udane(options);
|
|
97
|
+
|
|
98
|
+
if (server.port !== server.requestedPort) {
|
|
99
|
+
console.log(
|
|
100
|
+
`[+] Port ${server.requestedPort} is busy, using ${server.port}`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log(
|
|
105
|
+
`\n[+] udane v${pkg.version} by https://madhanmaaz.netlify.app`,
|
|
106
|
+
);
|
|
107
|
+
console.log(`[+] Root: ${server.config.root}\n`);
|
|
108
|
+
|
|
109
|
+
let closing = false;
|
|
110
|
+
const shutdown = async () => {
|
|
111
|
+
if (closing) return;
|
|
112
|
+
closing = true;
|
|
113
|
+
await server.close();
|
|
114
|
+
process.exit(0);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
process.on("SIGINT", shutdown);
|
|
118
|
+
process.on("SIGTERM", shutdown);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
main().catch((error) => {
|
|
122
|
+
// console.log(error);
|
|
123
|
+
console.error(`[-] ${error.message}`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
});
|