refactor: extract seed provision scripts to dedicated directory

Move bootstrap, default, and inform scripts from inline template
strings in lib/init.ts to standalone files under seed/, loaded via
an esbuild text-loader plugin. Add type-checking (provisions.d.ts,
tsconfig.json) and ESLint config for the seed scripts. Set tsconfig
module to Preserve to support import attributes.
This commit is contained in:
Zaid Humaimidi committed 2026-03-13 13:48:45 -07:00
1 parent f39e2ce7cf
commit d445aa03cf
10 files changed
+236 -56

No files matched your search

+14 -1
View File
@@ -92,6 +92,19 @@ const assetsPlugin = {
}, },
} as esbuild.Plugin; } as esbuild.Plugin;
const seedPlugin = {
name: "seed",
setup(build) {
build.onLoad({ filter: /\/seed\// }, (args) => {
if (args.with?.["type"] !== "text") return undefined;
let contents = fs.readFileSync(args.path, "utf8");
// Strip TypeScript directives that are only needed for type-checking
contents = contents.replace(/^\s*\/\/\s*@ts-.*\n/gm, "");
return { contents, loader: "text" };
});
},
} as esbuild.Plugin;
const packageDotJsonPlugin = { const packageDotJsonPlugin = {
name: "packageDotJson", name: "packageDotJson",
setup(build) { setup(build) {
@@ -295,7 +308,7 @@ async function generateBackendJs(): Promise<void> {
banner: { js: "#!/usr/bin/env node" }, banner: { js: "#!/usr/bin/env node" },
entryPoints: services.map((s) => `bin/${s}.ts`), entryPoints: services.map((s) => `bin/${s}.ts`),
outdir: path.join(OUTPUT_DIR, "bin"), outdir: path.join(OUTPUT_DIR, "bin"),
plugins: [packageDotJsonPlugin, assetsPlugin], plugins: [packageDotJsonPlugin, assetsPlugin, seedPlugin],
}); });
for (const bin of services) { for (const bin of services) {
+2 -2
View File
@@ -5,7 +5,7 @@ const execPromise = promisify(exec);
async function runEslint(): Promise<string> { async function runEslint(): Promise<string> {
const CMD = const CMD =
"eslint 'bin/*.ts' 'lib/**/*.ts' 'ui/**/*.ts' 'test/**/*.ts' 'build/**/*.ts'"; "eslint 'bin/*.ts' 'lib/**/*.ts' 'ui/**/*.ts' 'test/**/*.ts' 'build/**/*.ts' 'seed/*'";
const env = { const env = {
...(process.stdout.isTTY && { FORCE_COLOR: "1" }), ...(process.stdout.isTTY && { FORCE_COLOR: "1" }),
...process.env, ...process.env,
@@ -21,7 +21,7 @@ async function runEslint(): Promise<string> {
} }
async function runTsc(): Promise<string> { async function runTsc(): Promise<string> {
const CMD = "tsc --noEmit"; const CMD = "tsc --noEmit && tsc -p seed/tsconfig.json";
const env = { const env = {
...(process.stdout.isTTY && { FORCE_COLOR: "1" }), ...(process.stdout.isTTY && { FORCE_COLOR: "1" }),
...process.env, ...process.env,
+23
View File
@@ -51,4 +51,27 @@ export default defineConfig(
}, },
}, },
}, },
{
files: ["seed/*.js"],
languageOptions: {
parserOptions: {
project: null,
},
globals: {
declare: "readonly",
clear: "readonly",
commit: "readonly",
ext: "readonly",
log: "readonly",
args: "readonly",
},
},
rules: {
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/no-misused-promises": "off",
"@typescript-eslint/no-shadow": "off",
"no-shadow": ["error", { allow: ["err", "total"] }],
},
},
); );
+3 -52
View File
@@ -9,6 +9,9 @@ import {
putUser, putUser,
} from "./ui/db.ts"; } from "./ui/db.ts";
import { del } from "./cache.ts"; import { del } from "./cache.ts";
import BOOTSTRAP_SCRIPT from "../seed/bootstrap.js" with { type: "text" };
import DEFAULT_SCRIPT from "../seed/default.js" with { type: "text" };
import INFORM_SCRIPT from "../seed/inform.js" with { type: "text" };
interface Status { interface Status {
users: boolean; users: boolean;
@@ -19,58 +22,6 @@ interface Status {
overview: boolean; overview: boolean;
} }
const BOOTSTRAP_SCRIPT = `
const now = Date.now();
// Clear cached data model to force a refresh
clear("Device", now);
clear("InternetGatewayDevice", now);
`.trim();
const DEFAULT_SCRIPT = `
const hourly = Date.now(3600000);
// Refresh basic parameters hourly
declare("InternetGatewayDevice.DeviceInfo.HardwareVersion", {path: hourly, value: hourly});
declare("InternetGatewayDevice.DeviceInfo.SoftwareVersion", {path: hourly, value: hourly});
declare("InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.MACAddress", {path: hourly, value: hourly});
declare("InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.ExternalIPAddress", {path: hourly, value: hourly});
declare("InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.SSID", {path: hourly, value: hourly});
// Don't refresh password field periodically because CPEs always report blank passowrds for security reasons
declare("InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.KeyPassphrase", {path: hourly, value: 1});
declare("InternetGatewayDevice.LANDevice.*.Hosts.Host.*.HostName", {path: hourly, value: hourly});
declare("InternetGatewayDevice.LANDevice.*.Hosts.Host.*.IPAddress", {path: hourly, value: hourly});
declare("InternetGatewayDevice.LANDevice.*.Hosts.Host.*.MACAddress", {path: hourly, value: hourly});
`.trim();
const INFORM_SCRIPT = `
// Device ID as user name
const username = declare("DeviceID.ID", {value: 1}).value[0]
// Password will be fixed for a given device because Math.random() is seeded with device ID by default.
const password = Math.trunc(Math.random() * Number.MAX_SAFE_INTEGER).toString(36);
const informInterval = 300;
// Refresh values daily
const daily = Date.now(86400000);
// Unique inform offset per device for better load distribution
const informTime = daily % 86400000;
declare("InternetGatewayDevice.ManagementServer.ConnectionRequestUsername", {value: daily}, {value: username});
declare("InternetGatewayDevice.ManagementServer.ConnectionRequestPassword", {value: daily}, {value: password});
declare("InternetGatewayDevice.ManagementServer.PeriodicInformEnable", {value: daily}, {value: true});
declare("InternetGatewayDevice.ManagementServer.PeriodicInformInterval", {value: daily}, {value: informInterval});
declare("InternetGatewayDevice.ManagementServer.PeriodicInformTime", {value: daily}, {value: informTime});
declare("Device.ManagementServer.ConnectionRequestUsername", {value: daily}, {value: username});
declare("Device.ManagementServer.ConnectionRequestPassword", {value: daily}, {value: password});
declare("Device.ManagementServer.PeriodicInformEnable", {value: daily}, {value: true});
declare("Device.ManagementServer.PeriodicInformInterval", {value: daily}, {value: informInterval});
declare("Device.ManagementServer.PeriodicInformTime", {value: daily}, {value: informTime});
`.trim();
export async function getStatus(): Promise<Status> { export async function getStatus(): Promise<Status> {
const [configSnapshot, presetCount] = await Promise.all([ const [configSnapshot, presetCount] = await Promise.all([
getRevision(), getRevision(),
+5
View File
@@ -0,0 +1,5 @@
const now = Date.now();
// Clear cached data model to force a refresh
clear("Device", now);
clear("InternetGatewayDevice", now);
+40
View File
@@ -0,0 +1,40 @@
const hourly = Date.now(3600000);
// Refresh basic parameters hourly
declare("InternetGatewayDevice.DeviceInfo.HardwareVersion", {
path: hourly,
value: hourly,
});
declare("InternetGatewayDevice.DeviceInfo.SoftwareVersion", {
path: hourly,
value: hourly,
});
declare(
"InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.MACAddress",
{ path: hourly, value: hourly },
);
declare(
"InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.ExternalIPAddress",
{ path: hourly, value: hourly },
);
declare("InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.SSID", {
path: hourly,
value: hourly,
});
// Don't refresh password field periodically because CPEs always report blank passowrds for security reasons
declare("InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.KeyPassphrase", {
path: hourly,
value: 1,
});
declare("InternetGatewayDevice.LANDevice.*.Hosts.Host.*.HostName", {
path: hourly,
value: hourly,
});
declare("InternetGatewayDevice.LANDevice.*.Hosts.Host.*.IPAddress", {
path: hourly,
value: hourly,
});
declare("InternetGatewayDevice.LANDevice.*.Hosts.Host.*.MACAddress", {
path: hourly,
value: hourly,
});
+67
View File
@@ -0,0 +1,67 @@
// Device ID as user name
const username = declare("DeviceID.ID", { value: 1 }).value[0];
// Password will be fixed for a given device because Math.random() is seeded with device ID by default.
const password = Math.trunc(Math.random() * Number.MAX_SAFE_INTEGER).toString(
36,
);
const informInterval = 300;
// Refresh values daily
const daily = Date.now(86400000);
// Unique inform offset per device for better load distribution
const informTime = daily % 86400000;
declare(
"InternetGatewayDevice.ManagementServer.ConnectionRequestUsername",
{ value: daily },
{ value: username },
);
declare(
"InternetGatewayDevice.ManagementServer.ConnectionRequestPassword",
{ value: daily },
{ value: password },
);
declare(
"InternetGatewayDevice.ManagementServer.PeriodicInformEnable",
{ value: daily },
{ value: true },
);
declare(
"InternetGatewayDevice.ManagementServer.PeriodicInformInterval",
{ value: daily },
{ value: informInterval },
);
declare(
"InternetGatewayDevice.ManagementServer.PeriodicInformTime",
{ value: daily },
{ value: informTime },
);
declare(
"Device.ManagementServer.ConnectionRequestUsername",
{ value: daily },
{ value: username },
);
declare(
"Device.ManagementServer.ConnectionRequestPassword",
{ value: daily },
{ value: password },
);
declare(
"Device.ManagementServer.PeriodicInformEnable",
{ value: daily },
{ value: true },
);
declare(
"Device.ManagementServer.PeriodicInformInterval",
{ value: daily },
{ value: informInterval },
);
declare(
"Device.ManagementServer.PeriodicInformTime",
{ value: daily },
{ value: informTime },
);
+71
View File
@@ -0,0 +1,71 @@
interface Timestamps {
path?: number;
object?: number;
writable?: number;
value?: number;
notification?: number;
accessList?: number;
}
interface Values {
path?: number | [number, number];
object?: boolean;
writable?: boolean;
value?: string | number | boolean | [string | number | boolean, string?];
notification?: number;
accessList?: string[];
}
interface ParameterWrapper extends Iterable<ParameterWrapper> {
readonly path: string | undefined;
readonly size: number | undefined;
readonly object: 0 | 1 | undefined;
readonly writable: 0 | 1 | undefined;
readonly value: [string | number | boolean, string] | undefined;
readonly notification: number | undefined;
readonly accessList: string[] | undefined;
}
declare function declare(
path: string,
timestamps?: Timestamps | null,
values?: Values | null,
): ParameterWrapper;
declare function clear(
path: string,
timestamp: number,
attributes?: Timestamps,
): void;
declare function commit(): void;
declare function ext(...args: unknown[]): unknown;
declare function log(msg: string, meta?: Record<string, unknown>): void;
declare const args: unknown[];
interface DateConstructor {
new (): Date;
new (
year: number,
monthIndex?: number,
day?: number,
hours?: number,
minutes?: number,
seconds?: number,
milliseconds?: number,
): Date;
now(intervalOrCron?: number | string, variance?: number): number;
parse(dateString: string): number;
UTC(
year: number,
monthIndex?: number,
day?: number,
hours?: number,
minutes?: number,
seconds?: number,
milliseconds?: number,
): number;
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"noEmit": true,
"target": "ES2022",
"types": []
},
"include": ["./*.js", "./*.d.ts"]
}
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": ["ES2022", "dom"], "lib": ["ES2022", "dom"],
"module": "ES2022", "module": "Preserve",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"target": "ES2022", "target": "ES2022",
"resolveJsonModule": true, "resolveJsonModule": true,