@xrmforge/devkit 0.5.0 → 0.5.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.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/templates/AGENT.md +16 -12
- package/dist/templates/example-form.ts +2 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -286,8 +286,8 @@ function generatePackageJson(projectName) {
|
|
|
286
286
|
},
|
|
287
287
|
devDependencies: {
|
|
288
288
|
"@types/xrm": "^9.0.90",
|
|
289
|
-
"@xrmforge/cli": "^0.3
|
|
290
|
-
"@xrmforge/testing": "^0.
|
|
289
|
+
"@xrmforge/cli": "^0.4.3",
|
|
290
|
+
"@xrmforge/testing": "^0.2.0",
|
|
291
291
|
"@xrmforge/helpers": "^0.1.0",
|
|
292
292
|
typescript: "^5.7.0",
|
|
293
293
|
vitest: "^3.0.0"
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/errors.ts","../src/config.ts","../src/builder/esbuild-builder.ts","../src/scaffold/scaffold.ts","../src/scaffold/template-loader.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, access } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { ScaffoldConfig, ScaffoldResult } from './types.js';\nimport { BuildError, BuildErrorCode } from '../errors.js';\nimport { loadTemplate } from './template-loader.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 && !config.force) {\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 --force to scaffold anyway (existing files will be skipped).`,\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 = await generateTemplates(config);\n\n for (const [relativePath, content] of templates) {\n const absolutePath = join(targetDir, relativePath);\n await mkdir(join(absolutePath, '..'), { recursive: true });\n\n // In force mode: skip files that already exist\n if (config.force) {\n try {\n await access(absolutePath);\n warnings.push(`Skipped ${relativePath} (already exists)`);\n continue;\n } catch {\n // File doesn't exist, proceed with write\n }\n }\n\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 */\nasync function generateTemplates(config: ScaffoldConfig): Promise<Array<[string, string]>> {\n const { projectName, prefix, namespace } = config;\n const lowerPrefix = prefix.toLowerCase();\n const namespaceVars = { namespace };\n\n return [\n ['package.json', generatePackageJson(projectName)],\n ['tsconfig.json', generateTsConfig()],\n ['xrmforge.config.json', generateXrmForgeConfig(lowerPrefix, namespace)],\n ['vitest.config.ts', await loadTemplate('vitest.config.ts')],\n ['.gitignore', await loadTemplate('gitignore')],\n ['AGENT.md', await loadTemplate('AGENT.md')],\n ['src/forms/example-form.ts', await loadTemplate('example-form.ts', namespaceVars)],\n ['typings/.gitkeep', ''],\n ['tests/forms/example-form.test.ts', await loadTemplate('example-form.test.ts', namespaceVars)],\n ['.github/workflows/ci.yml', await loadTemplate('github-actions-ci.yml')],\n ['azure-pipelines.yml', await loadTemplate('azure-pipelines.yml')],\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/helpers': '^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","/**\n * Template loader for scaffold templates.\n *\n * Reads template files from the templates/ directory relative to this module.\n * Supports {{placeholder}} variable substitution.\n */\n\nimport { readFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n/** Path to the templates directory (relative to compiled output or source). */\nconst TEMPLATES_DIR = join(__dirname, 'templates');\n\n/**\n * Load a template file by name and optionally substitute variables.\n *\n * Variables in the template use the `{{key}}` syntax.\n *\n * @param name - Template filename (e.g. 'AGENT.md', 'example-form.ts')\n * @param vars - Optional key-value pairs for placeholder substitution\n * @returns Template content with variables replaced\n */\nexport async function loadTemplate(\n name: string,\n vars?: Record<string, string>,\n): Promise<string> {\n const content = await readFile(join(TEMPLATES_DIR, name), 'utf-8');\n\n if (!vars || Object.keys(vars).length === 0) {\n return content;\n }\n\n return Object.entries(vars).reduce(\n (result, [key, value]) => result.replaceAll(`{{${key}}}`, value),\n content,\n );\n}\n"],"mappings":"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","names":["BuildErrorCode","context","build","mkdir","join","dirname","mkdir","join"]}
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/config.ts","../src/builder/esbuild-builder.ts","../src/scaffold/scaffold.ts","../src/scaffold/template-loader.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, access } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { ScaffoldConfig, ScaffoldResult } from './types.js';\nimport { BuildError, BuildErrorCode } from '../errors.js';\nimport { loadTemplate } from './template-loader.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 && !config.force) {\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 --force to scaffold anyway (existing files will be skipped).`,\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 = await generateTemplates(config);\n\n for (const [relativePath, content] of templates) {\n const absolutePath = join(targetDir, relativePath);\n await mkdir(join(absolutePath, '..'), { recursive: true });\n\n // In force mode: skip files that already exist\n if (config.force) {\n try {\n await access(absolutePath);\n warnings.push(`Skipped ${relativePath} (already exists)`);\n continue;\n } catch {\n // File doesn't exist, proceed with write\n }\n }\n\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 */\nasync function generateTemplates(config: ScaffoldConfig): Promise<Array<[string, string]>> {\n const { projectName, prefix, namespace } = config;\n const lowerPrefix = prefix.toLowerCase();\n const namespaceVars = { namespace };\n\n return [\n ['package.json', generatePackageJson(projectName)],\n ['tsconfig.json', generateTsConfig()],\n ['xrmforge.config.json', generateXrmForgeConfig(lowerPrefix, namespace)],\n ['vitest.config.ts', await loadTemplate('vitest.config.ts')],\n ['.gitignore', await loadTemplate('gitignore')],\n ['AGENT.md', await loadTemplate('AGENT.md')],\n ['src/forms/example-form.ts', await loadTemplate('example-form.ts', namespaceVars)],\n ['typings/.gitkeep', ''],\n ['tests/forms/example-form.test.ts', await loadTemplate('example-form.test.ts', namespaceVars)],\n ['.github/workflows/ci.yml', await loadTemplate('github-actions-ci.yml')],\n ['azure-pipelines.yml', await loadTemplate('azure-pipelines.yml')],\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.4.3',\n '@xrmforge/testing': '^0.2.0',\n '@xrmforge/helpers': '^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","/**\n * Template loader for scaffold templates.\n *\n * Reads template files from the templates/ directory relative to this module.\n * Supports {{placeholder}} variable substitution.\n */\n\nimport { readFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n/** Path to the templates directory (relative to compiled output or source). */\nconst TEMPLATES_DIR = join(__dirname, 'templates');\n\n/**\n * Load a template file by name and optionally substitute variables.\n *\n * Variables in the template use the `{{key}}` syntax.\n *\n * @param name - Template filename (e.g. 'AGENT.md', 'example-form.ts')\n * @param vars - Optional key-value pairs for placeholder substitution\n * @returns Template content with variables replaced\n */\nexport async function loadTemplate(\n name: string,\n vars?: Record<string, string>,\n): Promise<string> {\n const content = await readFile(join(TEMPLATES_DIR, name), 'utf-8');\n\n if (!vars || Object.keys(vars).length === 0) {\n return content;\n }\n\n return Object.entries(vars).reduce(\n (result, [key, value]) => result.replaceAll(`{{${key}}}`, value),\n content,\n );\n}\n"],"mappings":"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","names":["BuildErrorCode","context","build","mkdir","join","dirname","mkdir","join"]}
|
package/dist/templates/AGENT.md
CHANGED
|
@@ -10,13 +10,15 @@ This file helps AI coding assistants write optimal Dynamics 365 form scripts.
|
|
|
10
10
|
- `@xrmforge/devkit` - esbuild IIFE bundles via xrmforge build
|
|
11
11
|
- `@xrmforge/eslint-plugin` - D365-specific ESLint rules
|
|
12
12
|
|
|
13
|
-
## Generated Types (
|
|
13
|
+
## Generated Types (generated/ directory)
|
|
14
14
|
|
|
15
15
|
Run `xrmforge generate` to create:
|
|
16
|
-
- `
|
|
17
|
-
- `
|
|
18
|
-
- `
|
|
19
|
-
- `
|
|
16
|
+
- `generated/forms/{entity}.ts` - Form interface + Fields/Tabs/Sections/Subgrids enums
|
|
17
|
+
- `generated/optionsets/{entity}.ts` - OptionSet const enums
|
|
18
|
+
- `generated/entities/{entity}.ts` - Entity interface
|
|
19
|
+
- `generated/fields/{entity}.ts` - Entity Fields enum for type-safe $select queries
|
|
20
|
+
- `generated/entity-names.ts` - EntityNames const enum
|
|
21
|
+
- `generated/index.ts` - Barrel file with `export * from` re-exports
|
|
20
22
|
|
|
21
23
|
## Rules: Always
|
|
22
24
|
|
|
@@ -59,7 +61,8 @@ Run `xrmforge generate` to create:
|
|
|
59
61
|
```typescript
|
|
60
62
|
// BEFORE: formContext.getAttribute("name").getValue()
|
|
61
63
|
// AFTER:
|
|
62
|
-
import { AccountMainFormFieldsEnum as Fields } from '../
|
|
64
|
+
import { AccountMainFormFieldsEnum as Fields } from '../generated/forms/account.js';
|
|
65
|
+
import type { AccountMainForm } from '../generated/forms/account.js';
|
|
63
66
|
const form = ctx.getFormContext() as AccountMainForm;
|
|
64
67
|
form.getAttribute(Fields.AccountName).getValue(); // StringAttribute, typed
|
|
65
68
|
```
|
|
@@ -68,14 +71,14 @@ form.getAttribute(Fields.AccountName).getValue(); // StringAttribute, typed
|
|
|
68
71
|
```typescript
|
|
69
72
|
// BEFORE: if (status.getValue() === 595300002) { ... }
|
|
70
73
|
// AFTER:
|
|
71
|
-
import { StatusCode } from '../
|
|
74
|
+
import { StatusCode } from '../generated/optionsets/invoice.js';
|
|
72
75
|
if (status.getValue() === StatusCode.Gebucht) { ... }
|
|
73
76
|
```
|
|
74
77
|
|
|
75
78
|
### Testing
|
|
76
79
|
```typescript
|
|
77
80
|
import { createFormMock } from '@xrmforge/testing';
|
|
78
|
-
const mock = createFormMock<AccountMainForm
|
|
81
|
+
const mock = createFormMock<AccountMainForm>({
|
|
79
82
|
name: 'Test', statuscode: 0
|
|
80
83
|
});
|
|
81
84
|
onLoad(mock.executionContext);
|
|
@@ -87,7 +90,7 @@ expect(mock.formContext.getControl('revenue').getVisible()).toBe(true);
|
|
|
87
90
|
```
|
|
88
91
|
src/forms/{entity}-form.ts - Form scripts (one per entity)
|
|
89
92
|
src/shared/{name}.ts - Shared utilities
|
|
90
|
-
|
|
93
|
+
generated/ - Generated types (do not edit manually)
|
|
91
94
|
tests/forms/{entity}.test.ts - Tests
|
|
92
95
|
xrmforge.config.json - Build config
|
|
93
96
|
```
|
|
@@ -113,12 +116,12 @@ When you see these patterns in legacy code, apply the XrmForge replacement:
|
|
|
113
116
|
|
|
114
117
|
When you find magic numbers like `getValue() === 105710002` in legacy code:
|
|
115
118
|
1. Search the file for ALL numeric comparisons with getValue()
|
|
116
|
-
2. Create a const enum in
|
|
119
|
+
2. Create a const enum in generated/optionsets/ with descriptive names
|
|
117
120
|
3. Import and use the enum instead of the number
|
|
118
121
|
|
|
119
122
|
Example:
|
|
120
123
|
```typescript
|
|
121
|
-
//
|
|
124
|
+
// generated/optionsets/invoice.ts
|
|
122
125
|
export const enum InvoiceStatusCode {
|
|
123
126
|
Neu = 1,
|
|
124
127
|
Versendet = 105710000,
|
|
@@ -127,7 +130,7 @@ export const enum InvoiceStatusCode {
|
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
// In the form script:
|
|
130
|
-
import { InvoiceStatusCode } from '../../
|
|
133
|
+
import { InvoiceStatusCode } from '../../generated/optionsets/invoice.js';
|
|
131
134
|
if (status.getValue() === InvoiceStatusCode.Gebucht) { ... }
|
|
132
135
|
```
|
|
133
136
|
|
|
@@ -178,6 +181,7 @@ When creating manual typings without `xrmforge generate`:
|
|
|
178
181
|
5. **openFile()** requires `fileSize` property in FileDetails.
|
|
179
182
|
|
|
180
183
|
6. **const enum in .d.ts files** cannot be imported at runtime by test frameworks.
|
|
184
|
+
Since v0.8.0, XrmForge generates `.ts` files, so this is no longer an issue.
|
|
181
185
|
For manual typings, use regular `enum` in `.ts` files (not `.d.ts`).
|
|
182
186
|
|
|
183
187
|
## Full Migration Guide
|
|
@@ -20,6 +20,8 @@ export function onLoad(executionContext: Xrm.Events.EventContext): void {
|
|
|
20
20
|
);
|
|
21
21
|
|
|
22
22
|
// Example: read a field value
|
|
23
|
+
// TODO: Replace with generated Fields enum after running 'xrmforge generate'
|
|
24
|
+
// Example: formContext.getAttribute(Fields.Name)
|
|
23
25
|
const nameAttr = formContext.getAttribute('name');
|
|
24
26
|
if (nameAttr) {
|
|
25
27
|
const value = nameAttr.getValue();
|