@xrmforge/devkit 0.1.0 → 0.2.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/dist/index.d.ts CHANGED
@@ -105,6 +105,46 @@ declare function watch(config: BuildConfig, options?: {
105
105
  dispose: () => Promise<void>;
106
106
  }>;
107
107
 
108
+ /**
109
+ * @xrmforge/devkit - Scaffold Types
110
+ */
111
+ /** Configuration for project scaffolding */
112
+ interface ScaffoldConfig {
113
+ /** Absolute path to the target directory */
114
+ targetDir: string;
115
+ /** Project name for package.json (e.g. "my-d365-scripts") */
116
+ projectName: string;
117
+ /** Publisher prefix for D365 WebResources (e.g. "contoso") */
118
+ prefix: string;
119
+ /** Base namespace for form scripts (e.g. "Contoso") */
120
+ namespace: string;
121
+ }
122
+ /** Result of scaffolding a project */
123
+ interface ScaffoldResult {
124
+ /** Relative paths of all created files */
125
+ filesCreated: string[];
126
+ /** Warnings (e.g. directory not empty) */
127
+ warnings: string[];
128
+ }
129
+
130
+ /**
131
+ * @xrmforge/devkit - Project Scaffolding
132
+ *
133
+ * Generates a complete D365 form scripting project from templates.
134
+ */
135
+
136
+ /**
137
+ * Scaffold a new D365 form scripting project.
138
+ *
139
+ * Creates a complete project structure with package.json, tsconfig,
140
+ * xrmforge.config.json, example form script, and test file.
141
+ *
142
+ * @param config - Scaffold configuration
143
+ * @returns List of created files and any warnings
144
+ * @throws {BuildError} if target directory is not empty (unless files are only dotfiles)
145
+ */
146
+ declare function scaffoldProject(config: ScaffoldConfig): Promise<ScaffoldResult>;
147
+
108
148
  /**
109
149
  * @xrmforge/devkit - Build Error Types
110
150
  *
@@ -130,4 +170,4 @@ declare class BuildError extends Error {
130
170
  constructor(code: BuildErrorCode, message: string, context?: Record<string, unknown>);
131
171
  }
132
172
 
133
- export { type BuildConfig, type BuildEntry, BuildError, BuildErrorCode, type BuildResult, type BuildResultEntry, type ResolvedBuildConfig, build, resolveBuildConfig, validateBuildConfig, watch };
173
+ export { type BuildConfig, type BuildEntry, BuildError, BuildErrorCode, type BuildResult, type BuildResultEntry, type ResolvedBuildConfig, type ScaffoldConfig, type ScaffoldResult, build, resolveBuildConfig, scaffoldProject, validateBuildConfig, watch };
package/dist/index.js CHANGED
@@ -188,11 +188,299 @@ async function watch(config, options) {
188
188
  }
189
189
  };
190
190
  }
191
+
192
+ // src/scaffold/scaffold.ts
193
+ import { mkdir as mkdir2, writeFile, readdir } from "fs/promises";
194
+ import { join } from "path";
195
+ async function scaffoldProject(config) {
196
+ const { targetDir } = config;
197
+ const filesCreated = [];
198
+ const warnings = [];
199
+ await mkdir2(targetDir, { recursive: true });
200
+ const existing = await readdir(targetDir);
201
+ const nonDotFiles = existing.filter((f) => !f.startsWith(".") && f !== "node_modules");
202
+ if (nonDotFiles.length > 0) {
203
+ throw new BuildError(
204
+ "BUILD_6001" /* CONFIG_INVALID */,
205
+ `Target directory is not empty: ${targetDir}
206
+ Found: ${nonDotFiles.slice(0, 5).join(", ")}${nonDotFiles.length > 5 ? "..." : ""}
207
+ Use an empty directory or remove existing files first.`,
208
+ { targetDir, existingFiles: nonDotFiles }
209
+ );
210
+ }
211
+ const dirs = [
212
+ "src/forms",
213
+ "typings",
214
+ "tests/forms"
215
+ ];
216
+ for (const dir of dirs) {
217
+ await mkdir2(join(targetDir, dir), { recursive: true });
218
+ }
219
+ const templates = generateTemplates(config);
220
+ for (const [relativePath, content] of templates) {
221
+ const absolutePath = join(targetDir, relativePath);
222
+ await mkdir2(join(absolutePath, ".."), { recursive: true });
223
+ await writeFile(absolutePath, content, "utf-8");
224
+ filesCreated.push(relativePath);
225
+ }
226
+ return { filesCreated, warnings };
227
+ }
228
+ function generateTemplates(config) {
229
+ const { projectName, prefix, namespace } = config;
230
+ const lowerPrefix = prefix.toLowerCase();
231
+ return [
232
+ ["package.json", generatePackageJson(projectName)],
233
+ ["tsconfig.json", generateTsConfig()],
234
+ ["xrmforge.config.json", generateXrmForgeConfig(lowerPrefix, namespace)],
235
+ ["vitest.config.ts", generateVitestConfig()],
236
+ [".gitignore", generateGitIgnore()],
237
+ ["src/forms/example-form.ts", generateExampleForm(namespace)],
238
+ ["typings/.gitkeep", ""],
239
+ ["tests/forms/example-form.test.ts", generateExampleTest(namespace)],
240
+ [".github/workflows/ci.yml", generateGitHubActionsCI()],
241
+ ["azure-pipelines.yml", generateAzureDevOpsPipeline()]
242
+ ];
243
+ }
244
+ function generatePackageJson(projectName) {
245
+ const pkg = {
246
+ name: projectName,
247
+ version: "0.1.0",
248
+ private: true,
249
+ type: "module",
250
+ scripts: {
251
+ generate: "xrmforge generate",
252
+ typecheck: "tsc --noEmit",
253
+ build: "xrmforge build",
254
+ watch: "xrmforge build --watch",
255
+ test: "vitest run",
256
+ "test:watch": "vitest"
257
+ },
258
+ devDependencies: {
259
+ "@types/xrm": "^9.0.90",
260
+ "@xrmforge/cli": "^0.3.0",
261
+ "@xrmforge/testing": "^0.1.0",
262
+ "@xrmforge/formhelpers": "^0.1.0",
263
+ typescript: "^5.7.0",
264
+ vitest: "^3.0.0"
265
+ }
266
+ };
267
+ return JSON.stringify(pkg, null, 2) + "\n";
268
+ }
269
+ function generateTsConfig() {
270
+ const config = {
271
+ compilerOptions: {
272
+ target: "ES2020",
273
+ module: "ESNext",
274
+ moduleResolution: "bundler",
275
+ lib: ["ES2020", "DOM"],
276
+ types: ["xrm"],
277
+ strict: true,
278
+ noEmit: true,
279
+ skipLibCheck: false,
280
+ esModuleInterop: true
281
+ },
282
+ include: [
283
+ "src/**/*.ts",
284
+ "typings/**/*.d.ts",
285
+ "typings/**/*.ts"
286
+ ]
287
+ };
288
+ return JSON.stringify(config, null, 2) + "\n";
289
+ }
290
+ function generateXrmForgeConfig(prefix, namespace) {
291
+ const config = {
292
+ build: {
293
+ outDir: `./dist/${prefix}_/JS`,
294
+ target: "es2020",
295
+ sourcemap: true,
296
+ minify: true,
297
+ entries: {
298
+ example_form: {
299
+ input: "./src/forms/example-form.ts",
300
+ namespace: `${namespace}.Example`,
301
+ out: "Example/OnLoad.js"
302
+ }
303
+ }
304
+ }
305
+ };
306
+ return JSON.stringify(config, null, 2) + "\n";
307
+ }
308
+ function generateVitestConfig() {
309
+ return `import { defineConfig } from 'vitest/config';
310
+
311
+ export default defineConfig({
312
+ test: {
313
+ globals: false,
314
+ include: ['tests/**/*.test.ts'],
315
+ },
316
+ });
317
+ `;
318
+ }
319
+ function generateGitIgnore() {
320
+ return `# Dependencies
321
+ node_modules/
322
+
323
+ # Build output
324
+ dist/
325
+
326
+ # XrmForge cache
327
+ .xrmforge/
328
+
329
+ # IDE
330
+ .vscode/settings.json
331
+ .idea/
332
+
333
+ # OS
334
+ .DS_Store
335
+ Thumbs.db
336
+
337
+ # Logs
338
+ *.log
339
+ `;
340
+ }
341
+ function generateExampleForm(namespace) {
342
+ return `/**
343
+ * Example Form Script for Dynamics 365.
344
+ *
345
+ * Register in D365 as: ${namespace}.Example.onLoad
346
+ *
347
+ * Replace this with your actual form logic.
348
+ */
349
+
350
+ /**
351
+ * Called when the form loads.
352
+ */
353
+ export function onLoad(executionContext: Xrm.Events.EventContext): void {
354
+ const formContext = executionContext.getFormContext();
355
+
356
+ // Example: show a notification on the form
357
+ formContext.ui.setFormNotification(
358
+ 'Form loaded successfully',
359
+ 'INFO',
360
+ 'example-notification',
361
+ );
362
+
363
+ // Example: read a field value
364
+ const nameAttr = formContext.getAttribute('name');
365
+ if (nameAttr) {
366
+ const value = nameAttr.getValue();
367
+ console.log('Name field value:', value);
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Called when the form is saved.
373
+ */
374
+ export function onSave(executionContext: Xrm.Events.EventContext): void {
375
+ const formContext = executionContext.getFormContext();
376
+
377
+ // Clear the notification on save
378
+ formContext.ui.clearFormNotification('example-notification');
379
+ }
380
+ `;
381
+ }
382
+ function generateExampleTest(namespace) {
383
+ return `import { describe, it, expect } from 'vitest';
384
+
385
+ /**
386
+ * Example test for the form script.
387
+ *
388
+ * Uses @xrmforge/testing for type-safe mocking once you have
389
+ * generated types. For now, this is a placeholder.
390
+ */
391
+ describe('${namespace}.Example', () => {
392
+ it('should export onLoad function', async () => {
393
+ const mod = await import('../../src/forms/example-form.js');
394
+ expect(typeof mod.onLoad).toBe('function');
395
+ });
396
+
397
+ it('should export onSave function', async () => {
398
+ const mod = await import('../../src/forms/example-form.js');
399
+ expect(typeof mod.onSave).toBe('function');
400
+ });
401
+ });
402
+ `;
403
+ }
404
+ function generateGitHubActionsCI() {
405
+ return `name: CI
406
+
407
+ on:
408
+ push:
409
+ branches: [main]
410
+ pull_request:
411
+ branches: [main]
412
+
413
+ jobs:
414
+ build:
415
+ runs-on: ubuntu-latest
416
+
417
+ steps:
418
+ - uses: actions/checkout@v4
419
+
420
+ - uses: actions/setup-node@v4
421
+ with:
422
+ node-version: 20
423
+
424
+ - run: npm ci
425
+
426
+ - name: Generate types from Dataverse
427
+ run: npx xrmforge generate --from-config
428
+ env:
429
+ XRMFORGE_CLIENT_ID: \${{ secrets.XRMFORGE_CLIENT_ID }}
430
+ XRMFORGE_CLIENT_SECRET: \${{ secrets.XRMFORGE_CLIENT_SECRET }}
431
+ XRMFORGE_TENANT_ID: \${{ secrets.XRMFORGE_TENANT_ID }}
432
+
433
+ - name: Type check
434
+ run: npx tsc --noEmit
435
+
436
+ - name: Test
437
+ run: npx vitest run
438
+
439
+ - name: Build WebResources
440
+ run: npx xrmforge build
441
+ `;
442
+ }
443
+ function generateAzureDevOpsPipeline() {
444
+ return `trigger:
445
+ branches:
446
+ include:
447
+ - main
448
+
449
+ pool:
450
+ vmImage: 'ubuntu-latest'
451
+
452
+ steps:
453
+ - task: NodeTool@0
454
+ inputs:
455
+ versionSpec: '20.x'
456
+ displayName: 'Install Node.js'
457
+
458
+ - script: npm ci
459
+ displayName: 'Install dependencies'
460
+
461
+ - script: npx xrmforge generate --from-config
462
+ displayName: 'Generate types from Dataverse'
463
+ env:
464
+ XRMFORGE_CLIENT_ID: $(XRMFORGE_CLIENT_ID)
465
+ XRMFORGE_CLIENT_SECRET: $(XRMFORGE_CLIENT_SECRET)
466
+ XRMFORGE_TENANT_ID: $(XRMFORGE_TENANT_ID)
467
+
468
+ - script: npx tsc --noEmit
469
+ displayName: 'Type check'
470
+
471
+ - script: npx vitest run
472
+ displayName: 'Test'
473
+
474
+ - script: npx xrmforge build
475
+ displayName: 'Build WebResources'
476
+ `;
477
+ }
191
478
  export {
192
479
  BuildError,
193
480
  BuildErrorCode,
194
481
  build2 as build,
195
482
  resolveBuildConfig,
483
+ scaffoldProject,
196
484
  validateBuildConfig,
197
485
  watch
198
486
  };
package/dist/index.js.map CHANGED
@@ -1 +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"]}
1
+ {"version":3,"sources":["../src/errors.ts","../src/config.ts","../src/builder/esbuild-builder.ts","../src/scaffold/scaffold.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","/**\n * @xrmforge/devkit - Project Scaffolding\n *\n * Generates a complete D365 form scripting project from templates.\n */\n\nimport { mkdir, writeFile, readdir } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { ScaffoldConfig, ScaffoldResult } from './types.js';\nimport { BuildError, BuildErrorCode } from '../errors.js';\n\n/**\n * Scaffold a new D365 form scripting project.\n *\n * Creates a complete project structure with package.json, tsconfig,\n * xrmforge.config.json, example form script, and test file.\n *\n * @param config - Scaffold configuration\n * @returns List of created files and any warnings\n * @throws {BuildError} if target directory is not empty (unless files are only dotfiles)\n */\nexport async function scaffoldProject(config: ScaffoldConfig): Promise<ScaffoldResult> {\n const { targetDir } = config;\n const filesCreated: string[] = [];\n const warnings: string[] = [];\n\n // Ensure target directory exists\n await mkdir(targetDir, { recursive: true });\n\n // Check if directory is empty (ignore dotfiles and node_modules)\n const existing = await readdir(targetDir);\n const nonDotFiles = existing.filter((f) => !f.startsWith('.') && f !== 'node_modules');\n if (nonDotFiles.length > 0) {\n throw new BuildError(\n BuildErrorCode.CONFIG_INVALID,\n `Target directory is not empty: ${targetDir}\\n` +\n `Found: ${nonDotFiles.slice(0, 5).join(', ')}${nonDotFiles.length > 5 ? '...' : ''}\\n` +\n `Use an empty directory or remove existing files first.`,\n { targetDir, existingFiles: nonDotFiles },\n );\n }\n\n // Create directory structure\n const dirs = [\n 'src/forms',\n 'typings',\n 'tests/forms',\n ];\n\n for (const dir of dirs) {\n await mkdir(join(targetDir, dir), { recursive: true });\n }\n\n // Generate and write all template files\n const templates = generateTemplates(config);\n\n for (const [relativePath, content] of templates) {\n const absolutePath = join(targetDir, relativePath);\n await mkdir(join(absolutePath, '..'), { recursive: true });\n await writeFile(absolutePath, content, 'utf-8');\n filesCreated.push(relativePath);\n }\n\n return { filesCreated, warnings };\n}\n\n/**\n * Generate all template file contents.\n * Returns an array of [relativePath, content] tuples.\n */\nfunction generateTemplates(config: ScaffoldConfig): Array<[string, string]> {\n const { projectName, prefix, namespace } = config;\n const lowerPrefix = prefix.toLowerCase();\n\n return [\n ['package.json', generatePackageJson(projectName)],\n ['tsconfig.json', generateTsConfig()],\n ['xrmforge.config.json', generateXrmForgeConfig(lowerPrefix, namespace)],\n ['vitest.config.ts', generateVitestConfig()],\n ['.gitignore', generateGitIgnore()],\n ['src/forms/example-form.ts', generateExampleForm(namespace)],\n ['typings/.gitkeep', ''],\n ['tests/forms/example-form.test.ts', generateExampleTest(namespace)],\n ['.github/workflows/ci.yml', generateGitHubActionsCI()],\n ['azure-pipelines.yml', generateAzureDevOpsPipeline()],\n ];\n}\n\nfunction generatePackageJson(projectName: string): string {\n const pkg = {\n name: projectName,\n version: '0.1.0',\n private: true,\n type: 'module',\n scripts: {\n generate: 'xrmforge generate',\n typecheck: 'tsc --noEmit',\n build: 'xrmforge build',\n watch: 'xrmforge build --watch',\n test: 'vitest run',\n 'test:watch': 'vitest',\n },\n devDependencies: {\n '@types/xrm': '^9.0.90',\n '@xrmforge/cli': '^0.3.0',\n '@xrmforge/testing': '^0.1.0',\n '@xrmforge/formhelpers': '^0.1.0',\n typescript: '^5.7.0',\n vitest: '^3.0.0',\n },\n };\n return JSON.stringify(pkg, null, 2) + '\\n';\n}\n\nfunction generateTsConfig(): string {\n const config = {\n compilerOptions: {\n target: 'ES2020',\n module: 'ESNext',\n moduleResolution: 'bundler',\n lib: ['ES2020', 'DOM'],\n types: ['xrm'],\n strict: true,\n noEmit: true,\n skipLibCheck: false,\n esModuleInterop: true,\n },\n include: [\n 'src/**/*.ts',\n 'typings/**/*.d.ts',\n 'typings/**/*.ts',\n ],\n };\n return JSON.stringify(config, null, 2) + '\\n';\n}\n\nfunction generateXrmForgeConfig(prefix: string, namespace: string): string {\n const config = {\n build: {\n outDir: `./dist/${prefix}_/JS`,\n target: 'es2020',\n sourcemap: true,\n minify: true,\n entries: {\n example_form: {\n input: './src/forms/example-form.ts',\n namespace: `${namespace}.Example`,\n out: 'Example/OnLoad.js',\n },\n },\n },\n };\n return JSON.stringify(config, null, 2) + '\\n';\n}\n\nfunction generateVitestConfig(): string {\n return `import { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n test: {\n globals: false,\n include: ['tests/**/*.test.ts'],\n },\n});\n`;\n}\n\nfunction generateGitIgnore(): string {\n return `# Dependencies\nnode_modules/\n\n# Build output\ndist/\n\n# XrmForge cache\n.xrmforge/\n\n# IDE\n.vscode/settings.json\n.idea/\n\n# OS\n.DS_Store\nThumbs.db\n\n# Logs\n*.log\n`;\n}\n\nfunction generateExampleForm(namespace: string): string {\n return `/**\n * Example Form Script for Dynamics 365.\n *\n * Register in D365 as: ${namespace}.Example.onLoad\n *\n * Replace this with your actual form logic.\n */\n\n/**\n * Called when the form loads.\n */\nexport function onLoad(executionContext: Xrm.Events.EventContext): void {\n const formContext = executionContext.getFormContext();\n\n // Example: show a notification on the form\n formContext.ui.setFormNotification(\n 'Form loaded successfully',\n 'INFO',\n 'example-notification',\n );\n\n // Example: read a field value\n const nameAttr = formContext.getAttribute('name');\n if (nameAttr) {\n const value = nameAttr.getValue();\n console.log('Name field value:', value);\n }\n}\n\n/**\n * Called when the form is saved.\n */\nexport function onSave(executionContext: Xrm.Events.EventContext): void {\n const formContext = executionContext.getFormContext();\n\n // Clear the notification on save\n formContext.ui.clearFormNotification('example-notification');\n}\n`;\n}\n\nfunction generateExampleTest(namespace: string): string {\n return `import { describe, it, expect } from 'vitest';\n\n/**\n * Example test for the form script.\n *\n * Uses @xrmforge/testing for type-safe mocking once you have\n * generated types. For now, this is a placeholder.\n */\ndescribe('${namespace}.Example', () => {\n it('should export onLoad function', async () => {\n const mod = await import('../../src/forms/example-form.js');\n expect(typeof mod.onLoad).toBe('function');\n });\n\n it('should export onSave function', async () => {\n const mod = await import('../../src/forms/example-form.js');\n expect(typeof mod.onSave).toBe('function');\n });\n});\n`;\n}\n\nfunction generateGitHubActionsCI(): string {\n return `name: CI\n\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\njobs:\n build:\n runs-on: ubuntu-latest\n\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-node@v4\n with:\n node-version: 20\n\n - run: npm ci\n\n - name: Generate types from Dataverse\n run: npx xrmforge generate --from-config\n env:\n XRMFORGE_CLIENT_ID: \\${{ secrets.XRMFORGE_CLIENT_ID }}\n XRMFORGE_CLIENT_SECRET: \\${{ secrets.XRMFORGE_CLIENT_SECRET }}\n XRMFORGE_TENANT_ID: \\${{ secrets.XRMFORGE_TENANT_ID }}\n\n - name: Type check\n run: npx tsc --noEmit\n\n - name: Test\n run: npx vitest run\n\n - name: Build WebResources\n run: npx xrmforge build\n`;\n}\n\nfunction generateAzureDevOpsPipeline(): string {\n return `trigger:\n branches:\n include:\n - main\n\npool:\n vmImage: 'ubuntu-latest'\n\nsteps:\n - task: NodeTool@0\n inputs:\n versionSpec: '20.x'\n displayName: 'Install Node.js'\n\n - script: npm ci\n displayName: 'Install dependencies'\n\n - script: npx xrmforge generate --from-config\n displayName: 'Generate types from Dataverse'\n env:\n XRMFORGE_CLIENT_ID: \\$(XRMFORGE_CLIENT_ID)\n XRMFORGE_CLIENT_SECRET: \\$(XRMFORGE_CLIENT_SECRET)\n XRMFORGE_TENANT_ID: \\$(XRMFORGE_TENANT_ID)\n\n - script: npx tsc --noEmit\n displayName: 'Type check'\n\n - script: npx vitest run\n displayName: 'Test'\n\n - script: npx xrmforge build\n displayName: 'Build WebResources'\n`;\n}\n"],"mappings":"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","names":["BuildErrorCode","context","build","mkdir","mkdir"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xrmforge/devkit",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Build orchestration and project tooling for Dynamics 365 WebResources",
5
5
  "keywords": [
6
6
  "dynamics-365",