@xrmforge/devkit 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/dist/index.d.ts +133 -0
- package/dist/index.js +199 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @xrmforge/devkit - Build Configuration
|
|
3
|
+
*
|
|
4
|
+
* Types and validation for the `build` section in xrmforge.config.json.
|
|
5
|
+
*/
|
|
6
|
+
/** A single build entry (one WebResource) */
|
|
7
|
+
interface BuildEntry {
|
|
8
|
+
/** Relative path to the TypeScript source file */
|
|
9
|
+
input: string;
|
|
10
|
+
/** Global namespace for D365 form event binding (e.g. "Contoso.Account") */
|
|
11
|
+
namespace: string;
|
|
12
|
+
/** Optional output filename relative to outDir (defaults to entry key + ".js") */
|
|
13
|
+
out?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Build configuration for WebResource bundling */
|
|
16
|
+
interface BuildConfig {
|
|
17
|
+
/** Bundler to use (currently only "esbuild") */
|
|
18
|
+
bundler?: 'esbuild';
|
|
19
|
+
/** Named build entries: key = entry name, value = entry config */
|
|
20
|
+
entries: Record<string, BuildEntry>;
|
|
21
|
+
/** Output directory for built bundles (default: "./dist") */
|
|
22
|
+
outDir?: string;
|
|
23
|
+
/** JavaScript target version (default: "es2020") */
|
|
24
|
+
target?: string;
|
|
25
|
+
/** Generate source maps (default: true) */
|
|
26
|
+
sourcemap?: boolean;
|
|
27
|
+
/** Minify output (default: false) */
|
|
28
|
+
minify?: boolean;
|
|
29
|
+
/** Additional modules to exclude from bundling */
|
|
30
|
+
external?: string[];
|
|
31
|
+
}
|
|
32
|
+
/** Fully resolved build config with all defaults applied */
|
|
33
|
+
interface ResolvedBuildConfig {
|
|
34
|
+
bundler: 'esbuild';
|
|
35
|
+
entries: Record<string, BuildEntry>;
|
|
36
|
+
outDir: string;
|
|
37
|
+
target: string;
|
|
38
|
+
sourcemap: boolean;
|
|
39
|
+
minify: boolean;
|
|
40
|
+
external: string[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Validate a raw build config object.
|
|
44
|
+
* Throws BuildError with CONFIG_INVALID if any required field is missing or invalid.
|
|
45
|
+
*/
|
|
46
|
+
declare function validateBuildConfig(raw: unknown): BuildConfig;
|
|
47
|
+
/**
|
|
48
|
+
* Apply default values to a validated build config.
|
|
49
|
+
*/
|
|
50
|
+
declare function resolveBuildConfig(config: BuildConfig): ResolvedBuildConfig;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @xrmforge/devkit - Build Result Types
|
|
54
|
+
*/
|
|
55
|
+
/** Result of building a single entry */
|
|
56
|
+
interface BuildResultEntry {
|
|
57
|
+
/** Entry name from the config */
|
|
58
|
+
name: string;
|
|
59
|
+
/** Absolute path of the output file */
|
|
60
|
+
outFile: string;
|
|
61
|
+
/** File size in bytes */
|
|
62
|
+
sizeBytes: number;
|
|
63
|
+
/** Build duration in milliseconds */
|
|
64
|
+
durationMs: number;
|
|
65
|
+
}
|
|
66
|
+
/** Overall result of building all entries */
|
|
67
|
+
interface BuildResult {
|
|
68
|
+
/** Per-entry results */
|
|
69
|
+
entries: BuildResultEntry[];
|
|
70
|
+
/** Total build duration in milliseconds */
|
|
71
|
+
totalDurationMs: number;
|
|
72
|
+
/** Build errors (entry name + message) */
|
|
73
|
+
errors: string[];
|
|
74
|
+
/** Build warnings (entry name + message) */
|
|
75
|
+
warnings: string[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @xrmforge/devkit - esbuild Builder
|
|
80
|
+
*
|
|
81
|
+
* Builds D365 WebResources as IIFE bundles with named globals.
|
|
82
|
+
* Abstracts esbuild so users never write esbuild config.
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Build all entries defined in the config as IIFE bundles.
|
|
87
|
+
*
|
|
88
|
+
* @param config - Validated build configuration
|
|
89
|
+
* @param cwd - Working directory for resolving relative paths (defaults to process.cwd())
|
|
90
|
+
* @returns Build result with per-entry details
|
|
91
|
+
*/
|
|
92
|
+
declare function build(config: BuildConfig, cwd?: string): Promise<BuildResult>;
|
|
93
|
+
/**
|
|
94
|
+
* Start watch mode for all entries.
|
|
95
|
+
* Returns a dispose function to stop watching.
|
|
96
|
+
*
|
|
97
|
+
* @param config - Validated build configuration
|
|
98
|
+
* @param options - Watch options
|
|
99
|
+
* @returns Object with dispose() to stop watching
|
|
100
|
+
*/
|
|
101
|
+
declare function watch(config: BuildConfig, options?: {
|
|
102
|
+
cwd?: string;
|
|
103
|
+
onRebuild?: (result: BuildResult) => void;
|
|
104
|
+
}): Promise<{
|
|
105
|
+
dispose: () => Promise<void>;
|
|
106
|
+
}>;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @xrmforge/devkit - Build Error Types
|
|
110
|
+
*
|
|
111
|
+
* Structured error types for build operations.
|
|
112
|
+
*/
|
|
113
|
+
declare enum BuildErrorCode {
|
|
114
|
+
/** Build configuration is invalid or missing required fields */
|
|
115
|
+
CONFIG_INVALID = "BUILD_6001",
|
|
116
|
+
/** Entry point file not found on disk */
|
|
117
|
+
ENTRY_NOT_FOUND = "BUILD_6002",
|
|
118
|
+
/** esbuild compilation failed (syntax errors, missing imports) */
|
|
119
|
+
BUILD_FAILED = "BUILD_6003",
|
|
120
|
+
/** Error in watch mode */
|
|
121
|
+
WATCH_ERROR = "BUILD_6004"
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Error class for build operations.
|
|
125
|
+
* Carries a machine-readable code and optional context for debugging.
|
|
126
|
+
*/
|
|
127
|
+
declare class BuildError extends Error {
|
|
128
|
+
readonly code: BuildErrorCode;
|
|
129
|
+
readonly context: Record<string, unknown>;
|
|
130
|
+
constructor(code: BuildErrorCode, message: string, context?: Record<string, unknown>);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export { type BuildConfig, type BuildEntry, BuildError, BuildErrorCode, type BuildResult, type BuildResultEntry, type ResolvedBuildConfig, build, resolveBuildConfig, validateBuildConfig, watch };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var BuildErrorCode = /* @__PURE__ */ ((BuildErrorCode2) => {
|
|
3
|
+
BuildErrorCode2["CONFIG_INVALID"] = "BUILD_6001";
|
|
4
|
+
BuildErrorCode2["ENTRY_NOT_FOUND"] = "BUILD_6002";
|
|
5
|
+
BuildErrorCode2["BUILD_FAILED"] = "BUILD_6003";
|
|
6
|
+
BuildErrorCode2["WATCH_ERROR"] = "BUILD_6004";
|
|
7
|
+
return BuildErrorCode2;
|
|
8
|
+
})(BuildErrorCode || {});
|
|
9
|
+
var BuildError = class _BuildError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
context;
|
|
12
|
+
constructor(code, message, context2 = {}) {
|
|
13
|
+
super(`[${code}] ${message}`);
|
|
14
|
+
this.name = "BuildError";
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.context = context2;
|
|
17
|
+
if (Error.captureStackTrace) {
|
|
18
|
+
Error.captureStackTrace(this, _BuildError);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/config.ts
|
|
24
|
+
function validateBuildConfig(raw) {
|
|
25
|
+
if (!raw || typeof raw !== "object") {
|
|
26
|
+
throw new BuildError(
|
|
27
|
+
"BUILD_6001" /* CONFIG_INVALID */,
|
|
28
|
+
"Build configuration must be an object."
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const config = raw;
|
|
32
|
+
if (!config["entries"] || typeof config["entries"] !== "object" || Array.isArray(config["entries"])) {
|
|
33
|
+
throw new BuildError(
|
|
34
|
+
"BUILD_6001" /* CONFIG_INVALID */,
|
|
35
|
+
'Build configuration requires an "entries" object with at least one entry.'
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const entries = config["entries"];
|
|
39
|
+
const entryNames = Object.keys(entries);
|
|
40
|
+
if (entryNames.length === 0) {
|
|
41
|
+
throw new BuildError(
|
|
42
|
+
"BUILD_6001" /* CONFIG_INVALID */,
|
|
43
|
+
'Build configuration requires at least one entry in "entries".'
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
for (const name of entryNames) {
|
|
47
|
+
const entry = entries[name];
|
|
48
|
+
if (!entry || typeof entry !== "object") {
|
|
49
|
+
throw new BuildError(
|
|
50
|
+
"BUILD_6001" /* CONFIG_INVALID */,
|
|
51
|
+
`Entry "${name}" must be an object with "input" and "namespace".`,
|
|
52
|
+
{ entry: name }
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (!entry["input"] || typeof entry["input"] !== "string") {
|
|
56
|
+
throw new BuildError(
|
|
57
|
+
"BUILD_6001" /* CONFIG_INVALID */,
|
|
58
|
+
`Entry "${name}" requires an "input" field (path to .ts source file).`,
|
|
59
|
+
{ entry: name }
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (!entry["namespace"] || typeof entry["namespace"] !== "string") {
|
|
63
|
+
throw new BuildError(
|
|
64
|
+
"BUILD_6001" /* CONFIG_INVALID */,
|
|
65
|
+
`Entry "${name}" requires a "namespace" field (e.g. "Contoso.Account").`,
|
|
66
|
+
{ entry: name }
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (config["bundler"] !== void 0 && config["bundler"] !== "esbuild") {
|
|
71
|
+
throw new BuildError(
|
|
72
|
+
"BUILD_6001" /* CONFIG_INVALID */,
|
|
73
|
+
`Unsupported bundler: "${String(config["bundler"])}". Currently only "esbuild" is supported.`,
|
|
74
|
+
{ bundler: config["bundler"] }
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return config;
|
|
78
|
+
}
|
|
79
|
+
function resolveBuildConfig(config) {
|
|
80
|
+
return {
|
|
81
|
+
bundler: config.bundler ?? "esbuild",
|
|
82
|
+
entries: config.entries,
|
|
83
|
+
outDir: config.outDir ?? "./dist",
|
|
84
|
+
target: config.target ?? "es2020",
|
|
85
|
+
sourcemap: config.sourcemap ?? true,
|
|
86
|
+
minify: config.minify ?? false,
|
|
87
|
+
external: config.external ?? []
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/builder/esbuild-builder.ts
|
|
92
|
+
import * as esbuild from "esbuild";
|
|
93
|
+
import { stat, mkdir } from "fs/promises";
|
|
94
|
+
import { resolve, dirname } from "path";
|
|
95
|
+
async function build2(config, cwd) {
|
|
96
|
+
const startTime = Date.now();
|
|
97
|
+
const resolved = resolveBuildConfig(config);
|
|
98
|
+
const basedir = cwd ?? process.cwd();
|
|
99
|
+
const outDir = resolve(basedir, resolved.outDir);
|
|
100
|
+
await mkdir(outDir, { recursive: true });
|
|
101
|
+
const entryNames = Object.keys(resolved.entries);
|
|
102
|
+
const results = [];
|
|
103
|
+
const errors = [];
|
|
104
|
+
const warnings = [];
|
|
105
|
+
const settled = await Promise.allSettled(
|
|
106
|
+
entryNames.map(async (name) => {
|
|
107
|
+
const entry = resolved.entries[name];
|
|
108
|
+
const entryStart = Date.now();
|
|
109
|
+
const outFile = resolve(outDir, entry.out ?? `${name}.js`);
|
|
110
|
+
await mkdir(dirname(outFile), { recursive: true });
|
|
111
|
+
const buildOptions = {
|
|
112
|
+
entryPoints: [resolve(basedir, entry.input)],
|
|
113
|
+
bundle: true,
|
|
114
|
+
format: "iife",
|
|
115
|
+
globalName: entry.namespace,
|
|
116
|
+
outfile: outFile,
|
|
117
|
+
target: [resolved.target],
|
|
118
|
+
minify: resolved.minify,
|
|
119
|
+
sourcemap: resolved.sourcemap,
|
|
120
|
+
treeShaking: true,
|
|
121
|
+
logLevel: "silent",
|
|
122
|
+
external: resolved.external
|
|
123
|
+
};
|
|
124
|
+
const result = await esbuild.build(buildOptions);
|
|
125
|
+
for (const w of result.warnings) {
|
|
126
|
+
warnings.push(`[${name}] ${w.text}`);
|
|
127
|
+
}
|
|
128
|
+
const stats = await stat(outFile);
|
|
129
|
+
return {
|
|
130
|
+
name,
|
|
131
|
+
outFile,
|
|
132
|
+
sizeBytes: stats.size,
|
|
133
|
+
durationMs: Date.now() - entryStart
|
|
134
|
+
};
|
|
135
|
+
})
|
|
136
|
+
);
|
|
137
|
+
for (let i = 0; i < settled.length; i++) {
|
|
138
|
+
const outcome = settled[i];
|
|
139
|
+
const name = entryNames[i];
|
|
140
|
+
if (outcome.status === "fulfilled") {
|
|
141
|
+
results.push(outcome.value);
|
|
142
|
+
} else {
|
|
143
|
+
const errorMsg = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
|
|
144
|
+
if (errorMsg.includes("Could not resolve") || errorMsg.includes("ENOENT")) {
|
|
145
|
+
errors.push(`[${name}] ${new BuildError("BUILD_6002" /* ENTRY_NOT_FOUND */, errorMsg, { entry: name }).message}`);
|
|
146
|
+
} else {
|
|
147
|
+
errors.push(`[${name}] ${new BuildError("BUILD_6003" /* BUILD_FAILED */, errorMsg, { entry: name }).message}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
entries: results,
|
|
153
|
+
totalDurationMs: Date.now() - startTime,
|
|
154
|
+
errors,
|
|
155
|
+
warnings
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
async function watch(config, options) {
|
|
159
|
+
const resolved = resolveBuildConfig(config);
|
|
160
|
+
const basedir = options?.cwd ?? process.cwd();
|
|
161
|
+
const outDir = resolve(basedir, resolved.outDir);
|
|
162
|
+
await mkdir(outDir, { recursive: true });
|
|
163
|
+
const contexts = [];
|
|
164
|
+
for (const [name, entry] of Object.entries(resolved.entries)) {
|
|
165
|
+
const outFile = resolve(outDir, entry.out ?? `${name}.js`);
|
|
166
|
+
await mkdir(dirname(outFile), { recursive: true });
|
|
167
|
+
const ctx = await esbuild.context({
|
|
168
|
+
entryPoints: [resolve(basedir, entry.input)],
|
|
169
|
+
bundle: true,
|
|
170
|
+
format: "iife",
|
|
171
|
+
globalName: entry.namespace,
|
|
172
|
+
outfile: outFile,
|
|
173
|
+
target: [resolved.target],
|
|
174
|
+
minify: resolved.minify,
|
|
175
|
+
sourcemap: resolved.sourcemap,
|
|
176
|
+
treeShaking: true,
|
|
177
|
+
logLevel: "silent",
|
|
178
|
+
external: resolved.external
|
|
179
|
+
});
|
|
180
|
+
contexts.push(ctx);
|
|
181
|
+
await ctx.watch();
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
dispose: async () => {
|
|
185
|
+
for (const ctx of contexts) {
|
|
186
|
+
await ctx.dispose();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
export {
|
|
192
|
+
BuildError,
|
|
193
|
+
BuildErrorCode,
|
|
194
|
+
build2 as build,
|
|
195
|
+
resolveBuildConfig,
|
|
196
|
+
validateBuildConfig,
|
|
197
|
+
watch
|
|
198
|
+
};
|
|
199
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/config.ts","../src/builder/esbuild-builder.ts"],"sourcesContent":["/**\n * @xrmforge/devkit - Build Error Types\n *\n * Structured error types for build operations.\n */\n\nexport enum BuildErrorCode {\n /** Build configuration is invalid or missing required fields */\n CONFIG_INVALID = 'BUILD_6001',\n /** Entry point file not found on disk */\n ENTRY_NOT_FOUND = 'BUILD_6002',\n /** esbuild compilation failed (syntax errors, missing imports) */\n BUILD_FAILED = 'BUILD_6003',\n /** Error in watch mode */\n WATCH_ERROR = 'BUILD_6004',\n}\n\n/**\n * Error class for build operations.\n * Carries a machine-readable code and optional context for debugging.\n */\nexport class BuildError extends Error {\n public readonly code: BuildErrorCode;\n public readonly context: Record<string, unknown>;\n\n constructor(code: BuildErrorCode, message: string, context: Record<string, unknown> = {}) {\n super(`[${code}] ${message}`);\n this.name = 'BuildError';\n this.code = code;\n this.context = context;\n\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, BuildError);\n }\n }\n}\n","/**\n * @xrmforge/devkit - Build Configuration\n *\n * Types and validation for the `build` section in xrmforge.config.json.\n */\n\nimport { BuildError, BuildErrorCode } from './errors.js';\n\n/** A single build entry (one WebResource) */\nexport interface BuildEntry {\n /** Relative path to the TypeScript source file */\n input: string;\n /** Global namespace for D365 form event binding (e.g. \"Contoso.Account\") */\n namespace: string;\n /** Optional output filename relative to outDir (defaults to entry key + \".js\") */\n out?: string;\n}\n\n/** Build configuration for WebResource bundling */\nexport interface BuildConfig {\n /** Bundler to use (currently only \"esbuild\") */\n bundler?: 'esbuild';\n /** Named build entries: key = entry name, value = entry config */\n entries: Record<string, BuildEntry>;\n /** Output directory for built bundles (default: \"./dist\") */\n outDir?: string;\n /** JavaScript target version (default: \"es2020\") */\n target?: string;\n /** Generate source maps (default: true) */\n sourcemap?: boolean;\n /** Minify output (default: false) */\n minify?: boolean;\n /** Additional modules to exclude from bundling */\n external?: string[];\n}\n\n/** Fully resolved build config with all defaults applied */\nexport interface ResolvedBuildConfig {\n bundler: 'esbuild';\n entries: Record<string, BuildEntry>;\n outDir: string;\n target: string;\n sourcemap: boolean;\n minify: boolean;\n external: string[];\n}\n\n/**\n * Validate a raw build config object.\n * Throws BuildError with CONFIG_INVALID if any required field is missing or invalid.\n */\nexport function validateBuildConfig(raw: unknown): BuildConfig {\n if (!raw || typeof raw !== 'object') {\n throw new BuildError(\n BuildErrorCode.CONFIG_INVALID,\n 'Build configuration must be an object.',\n );\n }\n\n const config = raw as Record<string, unknown>;\n\n // entries: required, non-empty object\n if (!config['entries'] || typeof config['entries'] !== 'object' || Array.isArray(config['entries'])) {\n throw new BuildError(\n BuildErrorCode.CONFIG_INVALID,\n 'Build configuration requires an \"entries\" object with at least one entry.',\n );\n }\n\n const entries = config['entries'] as Record<string, unknown>;\n const entryNames = Object.keys(entries);\n\n if (entryNames.length === 0) {\n throw new BuildError(\n BuildErrorCode.CONFIG_INVALID,\n 'Build configuration requires at least one entry in \"entries\".',\n );\n }\n\n for (const name of entryNames) {\n const entry = entries[name] as Record<string, unknown> | undefined;\n\n if (!entry || typeof entry !== 'object') {\n throw new BuildError(\n BuildErrorCode.CONFIG_INVALID,\n `Entry \"${name}\" must be an object with \"input\" and \"namespace\".`,\n { entry: name },\n );\n }\n\n if (!entry['input'] || typeof entry['input'] !== 'string') {\n throw new BuildError(\n BuildErrorCode.CONFIG_INVALID,\n `Entry \"${name}\" requires an \"input\" field (path to .ts source file).`,\n { entry: name },\n );\n }\n\n if (!entry['namespace'] || typeof entry['namespace'] !== 'string') {\n throw new BuildError(\n BuildErrorCode.CONFIG_INVALID,\n `Entry \"${name}\" requires a \"namespace\" field (e.g. \"Contoso.Account\").`,\n { entry: name },\n );\n }\n }\n\n // bundler: optional, must be \"esbuild\" if set\n if (config['bundler'] !== undefined && config['bundler'] !== 'esbuild') {\n throw new BuildError(\n BuildErrorCode.CONFIG_INVALID,\n `Unsupported bundler: \"${String(config['bundler'])}\". Currently only \"esbuild\" is supported.`,\n { bundler: config['bundler'] },\n );\n }\n\n return config as unknown as BuildConfig;\n}\n\n/**\n * Apply default values to a validated build config.\n */\nexport function resolveBuildConfig(config: BuildConfig): ResolvedBuildConfig {\n return {\n bundler: config.bundler ?? 'esbuild',\n entries: config.entries,\n outDir: config.outDir ?? './dist',\n target: config.target ?? 'es2020',\n sourcemap: config.sourcemap ?? true,\n minify: config.minify ?? false,\n external: config.external ?? [],\n };\n}\n","/**\n * @xrmforge/devkit - esbuild Builder\n *\n * Builds D365 WebResources as IIFE bundles with named globals.\n * Abstracts esbuild so users never write esbuild config.\n */\n\nimport * as esbuild from 'esbuild';\nimport { stat, mkdir } from 'node:fs/promises';\nimport { resolve, dirname } from 'node:path';\nimport type { BuildConfig } from '../config.js';\nimport { resolveBuildConfig } from '../config.js';\nimport { BuildError, BuildErrorCode } from '../errors.js';\nimport type { BuildResult, BuildResultEntry } from './types.js';\n\n/**\n * Build all entries defined in the config as IIFE bundles.\n *\n * @param config - Validated build configuration\n * @param cwd - Working directory for resolving relative paths (defaults to process.cwd())\n * @returns Build result with per-entry details\n */\nexport async function build(config: BuildConfig, cwd?: string): Promise<BuildResult> {\n const startTime = Date.now();\n const resolved = resolveBuildConfig(config);\n const basedir = cwd ?? process.cwd();\n const outDir = resolve(basedir, resolved.outDir);\n\n // Ensure output directory exists\n await mkdir(outDir, { recursive: true });\n\n const entryNames = Object.keys(resolved.entries);\n const results: BuildResultEntry[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n\n // Build all entries in parallel\n const settled = await Promise.allSettled(\n entryNames.map(async (name) => {\n const entry = resolved.entries[name]!;\n const entryStart = Date.now();\n const outFile = resolve(outDir, entry.out ?? `${name}.js`);\n\n // Ensure subdirectory exists for custom out paths\n await mkdir(dirname(outFile), { recursive: true });\n\n const buildOptions: esbuild.BuildOptions = {\n entryPoints: [resolve(basedir, entry.input)],\n bundle: true,\n format: 'iife',\n globalName: entry.namespace,\n outfile: outFile,\n target: [resolved.target],\n minify: resolved.minify,\n sourcemap: resolved.sourcemap,\n treeShaking: true,\n logLevel: 'silent',\n external: resolved.external,\n };\n\n const result = await esbuild.build(buildOptions);\n\n // Collect esbuild warnings\n for (const w of result.warnings) {\n warnings.push(`[${name}] ${w.text}`);\n }\n\n // Get output file size\n const stats = await stat(outFile);\n\n return {\n name,\n outFile,\n sizeBytes: stats.size,\n durationMs: Date.now() - entryStart,\n } satisfies BuildResultEntry;\n }),\n );\n\n for (let i = 0; i < settled.length; i++) {\n const outcome = settled[i]!;\n const name = entryNames[i]!;\n\n if (outcome.status === 'fulfilled') {\n results.push(outcome.value);\n } else {\n const errorMsg = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);\n // Distinguish \"file not found\" from other build errors\n if (errorMsg.includes('Could not resolve') || errorMsg.includes('ENOENT')) {\n errors.push(`[${name}] ${new BuildError(BuildErrorCode.ENTRY_NOT_FOUND, errorMsg, { entry: name }).message}`);\n } else {\n errors.push(`[${name}] ${new BuildError(BuildErrorCode.BUILD_FAILED, errorMsg, { entry: name }).message}`);\n }\n }\n }\n\n return {\n entries: results,\n totalDurationMs: Date.now() - startTime,\n errors,\n warnings,\n };\n}\n\n/**\n * Start watch mode for all entries.\n * Returns a dispose function to stop watching.\n *\n * @param config - Validated build configuration\n * @param options - Watch options\n * @returns Object with dispose() to stop watching\n */\nexport async function watch(\n config: BuildConfig,\n options?: {\n cwd?: string;\n onRebuild?: (result: BuildResult) => void;\n },\n): Promise<{ dispose: () => Promise<void> }> {\n const resolved = resolveBuildConfig(config);\n const basedir = options?.cwd ?? process.cwd();\n const outDir = resolve(basedir, resolved.outDir);\n\n await mkdir(outDir, { recursive: true });\n\n const contexts: esbuild.BuildContext[] = [];\n\n for (const [name, entry] of Object.entries(resolved.entries)) {\n const outFile = resolve(outDir, entry.out ?? `${name}.js`);\n await mkdir(dirname(outFile), { recursive: true });\n\n const ctx = await esbuild.context({\n entryPoints: [resolve(basedir, entry.input)],\n bundle: true,\n format: 'iife',\n globalName: entry.namespace,\n outfile: outFile,\n target: [resolved.target],\n minify: resolved.minify,\n sourcemap: resolved.sourcemap,\n treeShaking: true,\n logLevel: 'silent',\n external: resolved.external,\n });\n\n contexts.push(ctx);\n await ctx.watch();\n }\n\n return {\n dispose: async () => {\n for (const ctx of contexts) {\n await ctx.dispose();\n }\n },\n };\n}\n"],"mappings":"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","names":["BuildErrorCode","context","build"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xrmforge/devkit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Build orchestration and project tooling for Dynamics 365 WebResources",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dynamics-365",
|
|
7
|
+
"typescript",
|
|
8
|
+
"xrm",
|
|
9
|
+
"dataverse",
|
|
10
|
+
"esbuild",
|
|
11
|
+
"web-resources",
|
|
12
|
+
"iife"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "XrmForge Contributors",
|
|
16
|
+
"homepage": "https://github.com/juergenbeck/XrmForge/tree/main/packages/devkit",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/juergenbeck/XrmForge.git",
|
|
20
|
+
"directory": "packages/devkit"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"main": "./dist/index.js",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"dev": "tsup --watch",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"test:watch": "vitest",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"lint": "eslint src/",
|
|
42
|
+
"clean": "rm -rf dist"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"esbuild": "^0.25.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.0.0",
|
|
49
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
50
|
+
"tsup": "^8.3.0",
|
|
51
|
+
"typescript": "^5.7.0",
|
|
52
|
+
"vitest": "^3.0.0"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=20.0.0"
|
|
56
|
+
}
|
|
57
|
+
}
|