@warlock.js/core 5.6.0 → 5.8.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/CHANGELOG.md +36 -0
- package/esm/benchmark/profiler.d.mts.map +1 -1
- package/esm/benchmark/profiler.mjs +4 -3
- package/esm/benchmark/profiler.mjs.map +1 -1
- package/esm/cli/cli-commands.manager.mjs +12 -9
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/index.mjs +2 -0
- package/esm/cli/commands/doctor/checks/index.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/jwt-secret.check.mjs +55 -0
- package/esm/cli/commands/doctor/checks/jwt-secret.check.mjs.map +1 -0
- package/esm/cli/commands/generate/generators/controller.generator.mjs +4 -4
- package/esm/cli/commands/generate/generators/controller.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/migration.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/model.generator.mjs +4 -4
- package/esm/cli/commands/generate/generators/model.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/module.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/repository.generator.mjs +4 -4
- package/esm/cli/commands/generate/generators/repository.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/resource.generator.mjs +4 -4
- package/esm/cli/commands/generate/generators/resource.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/service.generator.mjs +4 -4
- package/esm/cli/commands/generate/generators/service.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/templates/stubs.mjs +6 -3
- package/esm/cli/commands/generate/templates/stubs.mjs.map +1 -1
- package/esm/cli/commands/routes/format-routes-table.mjs +3 -3
- package/esm/cli/commands/routes/format-routes-table.mjs.map +1 -1
- package/esm/cli/parse-cli-args.mjs +1 -0
- package/esm/cli/parse-cli-args.mjs.map +1 -1
- package/esm/cli/string-similarity.mjs +13 -4
- package/esm/cli/string-similarity.mjs.map +1 -1
- package/esm/commands/cli-command.d.mts.map +1 -1
- package/esm/commands/cli-command.mjs +5 -2
- package/esm/commands/cli-command.mjs.map +1 -1
- package/esm/config/config-loader.mjs +3 -1
- package/esm/config/config-loader.mjs.map +1 -1
- package/esm/dev-server/dependency-graph.d.mts.map +1 -1
- package/esm/dev-server/dependency-graph.mjs +2 -1
- package/esm/dev-server/dependency-graph.mjs.map +1 -1
- package/esm/dev-server/dev-logger.mjs +7 -5
- package/esm/dev-server/dev-logger.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +2 -1
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/parse-imports.mjs +22 -8
- package/esm/dev-server/parse-imports.mjs.map +1 -1
- package/esm/encryption/encrypt.mjs +1 -0
- package/esm/encryption/encrypt.mjs.map +1 -1
- package/esm/generations/add-command.action.mjs +8 -2
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/generations/features/shared/relocate-conflicting-home-route.mjs +6 -1
- package/esm/generations/features/shared/relocate-conflicting-home-route.mjs.map +1 -1
- package/esm/generations/stubs.mjs +6 -6
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/boot-port-preflight.d.mts +25 -1
- package/esm/http/boot-port-preflight.d.mts.map +1 -1
- package/esm/http/boot-port-preflight.mjs +51 -2
- package/esm/http/boot-port-preflight.mjs.map +1 -1
- package/esm/http/index.d.mts +1 -1
- package/esm/http/index.mjs +1 -1
- package/esm/http/middleware/maintenance.middleware.mjs +1 -2
- package/esm/http/middleware/maintenance.middleware.mjs.map +1 -1
- package/esm/http/middleware/utils/cidr-match.d.mts.map +1 -1
- package/esm/http/middleware/utils/cidr-match.mjs +1 -0
- package/esm/http/middleware/utils/cidr-match.mjs.map +1 -1
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +14 -7
- package/esm/http/request.mjs.map +1 -1
- package/esm/index.d.mts +2 -2
- package/esm/index.mjs +2 -2
- package/esm/repositories/adapters/cascade/filter-applicator.mjs +4 -2
- package/esm/repositories/adapters/cascade/filter-applicator.mjs.map +1 -1
- package/esm/router/positional-handler-diagnostics.d.mts.map +1 -1
- package/esm/router/positional-handler-diagnostics.mjs +9 -3
- package/esm/router/positional-handler-diagnostics.mjs.map +1 -1
- package/esm/router/route-registry.mjs +1 -1
- package/esm/router/route-registry.mjs.map +1 -1
- package/esm/storage/scoped-storage.d.mts.map +1 -1
- package/esm/storage/scoped-storage.mjs +1 -0
- package/esm/storage/scoped-storage.mjs.map +1 -1
- package/esm/storage/storage.d.mts.map +1 -1
- package/esm/storage/storage.mjs +6 -2
- package/esm/storage/storage.mjs.map +1 -1
- package/esm/storage/utils/safe-fetch.d.mts.map +1 -1
- package/esm/storage/utils/safe-fetch.mjs +6 -1
- package/esm/storage/utils/safe-fetch.mjs.map +1 -1
- package/esm/utils/load-environment.mjs +4 -1
- package/esm/utils/load-environment.mjs.map +1 -1
- package/esm/utils/recorded-environment-overrides.mjs +41 -0
- package/esm/utils/recorded-environment-overrides.mjs.map +1 -0
- package/esm/utils/version-compare.d.mts.map +1 -1
- package/esm/utils/version-compare.mjs +12 -4
- package/esm/utils/version-compare.mjs.map +1 -1
- package/esm/vite/lower-stage3-decorators.mjs +1 -0
- package/esm/vite/lower-stage3-decorators.mjs.map +1 -1
- package/package.json +12 -12
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse-imports.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/parse-imports.ts"],"sourcesContent":["import { fileExistsAsync, directoryExistsAsync } from \"@warlock.js/fs\";\nimport { type ImportSpecifier, parse } from \"es-module-lexer\";\nimport path from \"node:path\";\nimport { Path } from \"../utils/normalized-path\";\nimport { tsconfigManager } from \"./tsconfig-manager\";\n\n/**\n * Detect if a file contains only type definitions (no runtime code)\n *\n * A file is considered type-only if ALL its exports are:\n * - interface declarations\n * - type alias declarations\n * - export type { ... } statements\n * - export type { ... } from \"...\" statements\n *\n * Files with any of these are NOT type-only:\n * - export const/let/var\n * - export function\n * - export class\n * - export default (non-type)\n * - export { ... } (without type keyword)\n * - export * from (without type keyword)\n *\n * @param source - The source code to analyze\n * @returns true if the file exports only types\n */\nexport function isTypeOnlyFile(source: string): boolean {\n // Remove comments to avoid false positives\n const withoutComments = source\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\") // block comments\n .replace(/\\/\\/.*$/gm, \"\"); // line comments\n\n // Remove string literals to avoid false positives\n const withoutStrings = withoutComments\n .replace(/'(?:[^'\\\\]|\\\\.)*'/g, \"''\")\n .replace(/\"(?:[^\"\\\\]|\\\\.)*\"/g, '\"\"')\n .replace(/`(?:[^`\\\\]|\\\\.)*`/g, \"``\");\n\n // Patterns for type-only exports (these are safe)\n const typeOnlyPatterns = [\n /export\\s+type\\s+\\{[^}]*\\}/g, // export type { Foo, Bar }\n /export\\s+type\\s+\\{[^}]*\\}\\s+from\\s+['\"]/g, // export type { Foo } from \"...\"\n /export\\s+interface\\s+\\w+/g, // export interface Foo\n /export\\s+type\\s+\\w+\\s*=/g, // export type Foo =\n ];\n\n // Remove all type-only exports from consideration\n let remaining = withoutStrings;\n for (const pattern of typeOnlyPatterns) {\n remaining = remaining.replace(pattern, \"\");\n }\n\n // Patterns for runtime exports (these make the file NOT type-only)\n const runtimeExportPatterns = [\n /export\\s+(?:const|let|var)\\s+\\w+/g, // export const/let/var foo\n /export\\s+function\\s+\\w+/g, // export function foo\n /export\\s+async\\s+function\\s+\\w+/g, // export async function foo\n /export\\s+class\\s+\\w+/g, // export class Foo\n /export\\s+enum\\s+\\w+/g, // export enum Foo (enums have runtime value)\n /export\\s+default\\s+(?!type\\s)/g, // export default (not type)\n /export\\s+\\{[^}]*\\}(?!\\s+from)/g, // export { foo } (local re-export without type)\n /export\\s+\\{[^}]*\\}\\s+from\\s+['\"][^'\"]+['\"]/g, // export { foo } from (without type)\n /export\\s+\\*\\s+from\\s+['\"][^'\"]+['\"]/g, // export * from (re-exports everything)\n /export\\s+\\*\\s+as\\s+\\w+/g, // export * as namespace\n ];\n\n for (const pattern of runtimeExportPatterns) {\n if (pattern.test(remaining)) {\n // Reset regex lastIndex for subsequent tests\n pattern.lastIndex = 0;\n\n // Special case: check if export { } contains only type exports\n if (pattern.source.includes(\"export\\\\s+\\\\{\")) {\n const matches = remaining.match(/export\\s+\\{([^}]*)\\}(?:\\s+from\\s+['\"][^'\"]+['\"])?/g);\n if (matches) {\n for (const match of matches) {\n // Skip if it's already a type export\n if (/^export\\s+type\\s+/.test(match)) continue;\n\n // Extract the specifiers\n const specifiersMatch = match.match(/export\\s+\\{([^}]*)\\}/);\n if (specifiersMatch) {\n const specifiers = specifiersMatch[1];\n const items = specifiers\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n\n // If any specifier is NOT prefixed with \"type \", this is a runtime export\n const hasRuntimeSpecifier = items.some((item) => !item.startsWith(\"type \"));\n if (hasRuntimeSpecifier) {\n return false;\n }\n }\n }\n }\n continue;\n }\n\n return false;\n }\n }\n\n // If we get here, no runtime exports were found\n // But we should also verify the file has at least some type exports\n // (an empty file or file with only imports is not really \"type-only\")\n const hasTypeExports = typeOnlyPatterns.some((pattern) => {\n pattern.lastIndex = 0;\n return pattern.test(withoutStrings);\n });\n\n return hasTypeExports;\n}\n\n/**\n * Check if an import statement is a pure `import type` (the keyword sits\n * immediately after `import`, covering `import type { X }`, `import type Foo`,\n * and `import type * as Ns`).\n */\nfunction isTypeOnlyImport(line: string): boolean {\n const trimmed = line.trim();\n\n return trimmed.startsWith(\"import type \") || !!trimmed.match(/^import\\s+type\\s+[\\{\\*]/);\n}\n\n/**\n * Decide whether an `import ... from \"m\"` statement contributes any runtime\n * binding. Returns false for pure `import type` and for destructured imports\n * where every specifier is prefixed with the `type` keyword.\n */\nfunction hasRuntimeImports(line: string): boolean {\n const trimmed = line.trim();\n\n if (isTypeOnlyImport(trimmed)) {\n return false;\n }\n\n const specifiersMatch = trimmed.match(/import\\s+(?:type\\s+)?\\{([^}]+)\\}/);\n\n if (!specifiersMatch) {\n return true;\n }\n\n const items = specifiersMatch[1]\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n\n return !items.every((item) => /^type\\s+\\w+/.test(item));\n}\n\n/**\n * Decide whether an `export ... from \"m\"` statement re-exports only types.\n * Covers `export type { X } from`, `export type * from`, and\n * `export { type X, type Y } from`.\n */\nfunction isExportTypeOnlyStatement(line: string): boolean {\n const trimmed = line.trim();\n\n if (/^export\\s+type\\s+/.test(trimmed)) {\n return true;\n }\n\n const specifiersMatch = trimmed.match(/export\\s+\\{([^}]+)\\}/);\n\n if (!specifiersMatch) {\n return false;\n }\n\n const items = specifiersMatch[1]\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n\n return items.every((item) => /^type\\s+\\w+/.test(item));\n}\n\n/**\n * Extract import paths using regex (more reliable for TypeScript).\n * Each entry carries an `isTypeOnly` flag: true when every occurrence of the\n * path in the source is a type-only statement (no runtime binding).\n * A single runtime occurrence makes the path runtime.\n */\nfunction extractImportPathsWithRegex(\n source: string,\n): Array<{ path: string; originalLine: string; isTypeOnly: boolean }> {\n const imports: Array<{ path: string; originalLine: string; isTypeOnly: boolean }> = [];\n const seenPaths = new Map<string, number>();\n\n /**\n * Record an import path with its type-only flag.\n * If the same path appears in multiple statements, the edge is type-only\n * iff every occurrence is type-only — a single runtime statement makes it runtime.\n */\n const record = (importPath: string, originalLine: string, isTypeOnly: boolean): void => {\n if (!importPath) {\n return;\n }\n\n const existingIndex = seenPaths.get(importPath);\n\n if (existingIndex === undefined) {\n seenPaths.set(importPath, imports.length);\n imports.push({ path: importPath, originalLine, isTypeOnly });\n\n return;\n }\n\n if (!isTypeOnly) {\n imports[existingIndex].isTypeOnly = false;\n }\n };\n\n // Pattern 1: Standard ES module imports (handles multiline)\n // Matches: import { ... } from \"path\", import Foo from \"path\", import Foo, { ... } from \"path\"\n const importRegex =\n /import\\s+(?:type\\s+)?(\\{[\\s\\S]*?\\}|\\*\\s+as\\s+\\w+|\\w+(?:\\s*,\\s*\\{[\\s\\S]*?\\})?)\\s+from\\s+['\"]([^'\"]+)['\"]/g;\n\n let match;\n\n while ((match = importRegex.exec(source)) !== null) {\n const fullMatch = match[0];\n const importPath = match[2];\n const isTypeOnly = !hasRuntimeImports(fullMatch);\n\n record(importPath, fullMatch, isTypeOnly);\n }\n\n // Pattern 1b: Side-effect imports - import \"path\" — always runtime\n const sideEffectRegex = /import\\s+['\"]([^'\"]+)['\"]/g;\n\n while ((match = sideEffectRegex.exec(source)) !== null) {\n record(match[1], match[0], false);\n }\n\n // Pattern 2: Dynamic imports - import(\"path\") — always runtime\n const dynamicImportPattern = /import\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g;\n\n while ((match = dynamicImportPattern.exec(source)) !== null) {\n record(match[1], match[0], false);\n }\n\n // Pattern 3: Export from - export ... from \"path\"\n const exportFromPattern = /export\\s+(?:\\{[^}]*\\}|\\*|\\w+)\\s+from\\s+['\"]([^'\"]+)['\"]/g;\n\n while ((match = exportFromPattern.exec(source)) !== null) {\n const fullMatch = match[0];\n\n record(match[1], fullMatch, isExportTypeOnlyStatement(fullMatch));\n }\n\n return imports;\n}\n\n/**\n * Metadata for a single resolved import edge.\n * `isTypeOnly` is true iff every import/export statement that references this\n * path is type-only (pure `import type`, `export type`, or destructured lists\n * where every specifier carries the `type` keyword). A single runtime\n * occurrence flips the flag to false — that's what matters for cycle detection.\n */\nexport type ResolvedImport = {\n absolutePath: string;\n isTypeOnly: boolean;\n};\n\n/**\n * Parse import/export statements in a TS/JS source file and return the\n * resolved dependency edges keyed by the original import path. Includes\n * type-only edges (flagged) so downstream consumers can distinguish runtime\n * cycles from type-only ones.\n *\n * @example\n * const importMap = await parseImports(source, \"/abs/path/to/file.ts\");\n * for (const [importPath, { absolutePath, isTypeOnly }] of importMap) {\n * // ...\n * }\n */\nexport async function parseImports(\n source: string,\n filePath: string,\n): Promise<Map<string, ResolvedImport>> {\n try {\n // Skip .d.ts files - they're type declarations, not runtime code\n if (filePath.endsWith(\".d.ts\")) {\n return new Map();\n }\n\n // Try es-module-lexer first (faster and more accurate for simple cases)\n try {\n const [imports] = await parse(source);\n\n if (imports && imports.length > 0) {\n return await resolveImports(imports as ImportSpecifier[], source, filePath);\n }\n } catch (lexerError) {\n // es-module-lexer failed, fall back to regex-based extraction\n // This is common with TypeScript files that have complex syntax\n }\n\n // Fallback: Use regex-based extraction (more forgiving with TypeScript)\n const regexImports = extractImportPathsWithRegex(source);\n const resolvedImports = new Map<string, ResolvedImport>();\n\n for (const { path: importPath, isTypeOnly } of regexImports) {\n if (isNodeBuiltin(importPath)) {\n continue;\n }\n\n if (!importPath.startsWith(\".\") && !tsconfigManager.isAlias(importPath)) {\n continue;\n }\n\n let resolvedPath: string | null = null;\n\n if (tsconfigManager.isAlias(importPath)) {\n resolvedPath = await resolveAliasImport(importPath);\n } else if (importPath.startsWith(\".\")) {\n resolvedPath = await resolveRelativeImport(importPath, filePath);\n }\n\n if (resolvedPath) {\n mergeResolvedImport(resolvedImports, importPath, resolvedPath, isTypeOnly);\n }\n }\n\n return resolvedImports;\n } catch (error) {\n console.error(`Error parsing imports for ${filePath}:`, error);\n\n return new Map();\n }\n}\n\n/**\n * Merge a resolved import into the output map, preserving the type-only rule:\n * a path is type-only iff every statement that references it is type-only.\n */\nfunction mergeResolvedImport(\n target: Map<string, ResolvedImport>,\n importPath: string,\n absolutePath: string,\n isTypeOnly: boolean,\n): void {\n const existing = target.get(importPath);\n\n if (!existing) {\n target.set(importPath, { absolutePath, isTypeOnly });\n\n return;\n }\n\n if (!isTypeOnly) {\n existing.isTypeOnly = false;\n }\n}\n\n/**\n * Resolve imports from `es-module-lexer` output. For each specifier, slice the\n * raw statement (`ss..se`) and re-run the type-only heuristic so we can flag\n * the edge correctly — the lexer itself does not distinguish `import type`.\n */\nasync function resolveImports(\n imports: ImportSpecifier[],\n source: string,\n filePath: string,\n): Promise<Map<string, ResolvedImport>> {\n const resolvedImports = new Map<string, ResolvedImport>();\n\n for (const imp of imports) {\n const importPath = imp.n;\n\n if (!importPath) {\n continue;\n }\n\n if (isNodeBuiltin(importPath)) {\n continue;\n }\n\n if (!importPath.startsWith(\".\") && !tsconfigManager.isAlias(importPath)) {\n continue;\n }\n\n let resolvedPath: string | null = null;\n\n if (tsconfigManager.isAlias(importPath)) {\n resolvedPath = await resolveAliasImport(importPath);\n } else if (importPath.startsWith(\".\")) {\n resolvedPath = await resolveRelativeImport(importPath, filePath);\n }\n\n if (!resolvedPath) {\n continue;\n }\n\n const isTypeOnly = isLexerStatementTypeOnly(imp, source);\n\n mergeResolvedImport(resolvedImports, importPath, resolvedPath, isTypeOnly);\n }\n\n return resolvedImports;\n}\n\n/**\n * Classify a single lexer-reported statement as type-only or runtime.\n * Dynamic imports (`import(...)`) are always runtime. For static\n * imports/export-from we slice the raw source and apply the same heuristic\n * the regex fallback uses.\n */\nfunction isLexerStatementTypeOnly(imp: ImportSpecifier, source: string): boolean {\n // Dynamic imports always execute — they cannot be type-only.\n // es-module-lexer reports dynamic with a.n === undefined typically, but\n // we guard by checking the statement prefix too.\n const statementStart = imp.ss;\n const statementEnd = imp.se;\n\n if (\n typeof statementStart !== \"number\" ||\n typeof statementEnd !== \"number\" ||\n statementEnd <= statementStart\n ) {\n return false;\n }\n\n const statement = source.slice(statementStart, statementEnd);\n const trimmed = statement.trim();\n\n if (trimmed.startsWith(\"import(\") || /^\\bimport\\s*\\(/.test(trimmed)) {\n return false;\n }\n\n if (trimmed.startsWith(\"export\")) {\n return isExportTypeOnlyStatement(statement);\n }\n\n return !hasRuntimeImports(statement);\n}\n\n/**\n * Resolve alias imports to actual file paths with extensions\n * Example: app/users/services/get-users.service -> /absolute/path/to/src/app/users/services/get-users.service.ts\n */\nasync function resolveAliasImport(importPath: string): Promise<string | null> {\n // Use tsconfig manager to resolve the alias to an absolute path\n const resolvedBase = tsconfigManager.resolveAliasToAbsolute(importPath);\n\n if (!resolvedBase) return null;\n\n // Try to resolve with extensions\n const resolvedPath = await tryResolveWithExtensions(resolvedBase);\n\n return resolvedPath;\n}\n\n/**\n * Resolve relative imports to actual file paths\n * Example: ./../services/get-user.service -> /absolute/path/to/services/get-user.service.ts\n */\nasync function resolveRelativeImport(\n importPath: string,\n currentFilePath: string,\n): Promise<string | null> {\n const dir = path.dirname(currentFilePath);\n // Use path.resolve to handle .. and . properly, then normalize to forward slashes\n const resolvedBase = Path.normalize(path.resolve(dir, importPath));\n\n // Try to resolve with extensions\n const resolvedPath = await tryResolveWithExtensions(resolvedBase);\n\n return resolvedPath;\n}\n\n/**\n * Try to resolve a file path by checking different extensions\n * TypeScript/JavaScript files can be imported without extensions\n *\n * @TODO: For better performance, we need to check the files in files orchestrator\n * instead of using the file system as we will be fetching all project files anyway.\n */\n// Cache for file existence checks to avoid redundant filesystem calls\nconst fileExistsCache = new Map<string, boolean>();\n\n/**\n * Clear the file exists cache\n * Should be called when new files are created to ensure fresh lookups\n */\nexport function clearFileExistsCache(): void {\n fileExistsCache.clear();\n}\n\nasync function cachedFileExists(filePath: string): Promise<boolean> {\n if (fileExistsCache.has(filePath)) {\n return fileExistsCache.get(filePath)!;\n }\n const exists = (await fileExistsAsync(filePath)) as boolean;\n fileExistsCache.set(filePath, exists);\n return exists;\n}\n\nasync function tryResolveWithExtensions(basePath: string): Promise<string | null> {\n // Normalize the base path first (handle Windows paths)\n const normalizedBase = Path.normalize(basePath);\n\n // List of extensions to try, in order of preference\n const extensions = [\".ts\", \".tsx\", \".js\", \".jsx\", \".mjs\", \".cjs\"];\n const validExtensions = new Set(extensions);\n\n // If the path already has a VALID code file extension, check if it exists\n const ext = path.extname(normalizedBase);\n if (ext && validExtensions.has(ext)) {\n if (await cachedFileExists(normalizedBase)) {\n return normalizedBase;\n }\n // If explicit extension doesn't exist, return null\n return null;\n }\n\n // Try all extensions in parallel for better performance\n const pathsToCheck = extensions.map((extension) => normalizedBase + extension);\n const results = await Promise.all(\n pathsToCheck.map(async (p) => ({ path: p, exists: await cachedFileExists(p) })),\n );\n\n // Return the first one that exists (in order of preference)\n for (const result of results) {\n if (result.exists) {\n return result.path;\n }\n }\n\n // Try index files in directory\n if (await directoryExistsAsync(normalizedBase)) {\n const indexPaths = extensions.map((extension) =>\n Path.join(normalizedBase, `index${extension}`),\n );\n const indexResults = await Promise.all(\n indexPaths.map(async (p) => ({ path: p, exists: await cachedFileExists(p) })),\n );\n\n for (const result of indexResults) {\n if (result.exists) {\n return result.path;\n }\n }\n }\n\n return null;\n}\n\n/**\n * Check if import is a Node.js built-in module\n */\nfunction isNodeBuiltin(importPath: string): boolean {\n const builtins = [\n \"fs\",\n \"path\",\n \"http\",\n \"https\",\n \"crypto\",\n \"stream\",\n \"util\",\n \"events\",\n \"buffer\",\n \"child_process\",\n \"os\",\n \"url\",\n \"querystring\",\n \"zlib\",\n \"net\",\n \"tls\",\n \"dns\",\n \"dgram\",\n \"cluster\",\n \"worker_threads\",\n \"perf_hooks\",\n \"async_hooks\",\n \"timers\",\n \"readline\",\n \"repl\",\n \"vm\",\n \"assert\",\n \"console\",\n \"process\",\n \"v8\",\n ];\n\n // Check for node: prefix or direct builtin name\n if (importPath.startsWith(\"node:\")) return true;\n\n const moduleName = importPath.split(\"/\")[0];\n return builtins.includes(moduleName);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"parse-imports.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/parse-imports.ts"],"sourcesContent":["import { fileExistsAsync, directoryExistsAsync } from \"@warlock.js/fs\";\nimport { type ImportSpecifier, parse } from \"es-module-lexer\";\nimport path from \"node:path\";\nimport { Path } from \"../utils/normalized-path\";\nimport { tsconfigManager } from \"./tsconfig-manager\";\n\n/**\n * Detect if a file contains only type definitions (no runtime code)\n *\n * A file is considered type-only if ALL its exports are:\n * - interface declarations\n * - type alias declarations\n * - export type { ... } statements\n * - export type { ... } from \"...\" statements\n *\n * Files with any of these are NOT type-only:\n * - export const/let/var\n * - export function\n * - export class\n * - export default (non-type)\n * - export { ... } (without type keyword)\n * - export * from (without type keyword)\n *\n * @param source - The source code to analyze\n * @returns true if the file exports only types\n */\nexport function isTypeOnlyFile(source: string): boolean {\n // Remove comments to avoid false positives\n const withoutComments = source\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\") // block comments\n .replace(/\\/\\/.*$/gm, \"\"); // line comments\n\n // Remove string literals to avoid false positives\n const withoutStrings = withoutComments\n .replace(/'(?:[^'\\\\]|\\\\.)*'/g, \"''\")\n .replace(/\"(?:[^\"\\\\]|\\\\.)*\"/g, '\"\"')\n .replace(/`(?:[^`\\\\]|\\\\.)*`/g, \"``\");\n\n // Patterns for type-only exports (these are safe)\n const typeOnlyPatterns = [\n /export\\s+type\\s+\\{[^}]*\\}/g, // export type { Foo, Bar }\n /export\\s+type\\s+\\{[^}]*\\}\\s+from\\s+['\"]/g, // export type { Foo } from \"...\"\n /export\\s+interface\\s+\\w+/g, // export interface Foo\n /export\\s+type\\s+\\w+\\s*=/g, // export type Foo =\n ];\n\n // Remove all type-only exports from consideration\n let remaining = withoutStrings;\n for (const pattern of typeOnlyPatterns) {\n remaining = remaining.replace(pattern, \"\");\n }\n\n // Patterns for runtime exports (these make the file NOT type-only)\n const runtimeExportPatterns = [\n /export\\s+(?:const|let|var)\\s+\\w+/g, // export const/let/var foo\n /export\\s+function\\s+\\w+/g, // export function foo\n /export\\s+async\\s+function\\s+\\w+/g, // export async function foo\n /export\\s+class\\s+\\w+/g, // export class Foo\n /export\\s+enum\\s+\\w+/g, // export enum Foo (enums have runtime value)\n /export\\s+default\\s+(?!type\\s)/g, // export default (not type)\n /export\\s+\\{[^}]*\\}(?!\\s+from)/g, // export { foo } (local re-export without type)\n /export\\s+\\{[^}]*\\}\\s+from\\s+['\"][^'\"]+['\"]/g, // export { foo } from (without type)\n /export\\s+\\*\\s+from\\s+['\"][^'\"]+['\"]/g, // export * from (re-exports everything)\n /export\\s+\\*\\s+as\\s+\\w+/g, // export * as namespace\n ];\n\n for (const pattern of runtimeExportPatterns) {\n if (pattern.test(remaining)) {\n // Reset regex lastIndex for subsequent tests\n pattern.lastIndex = 0;\n\n // Special case: check if export { } contains only type exports\n if (pattern.source.includes(\"export\\\\s+\\\\{\")) {\n const matches = remaining.match(/export\\s+\\{([^}]*)\\}(?:\\s+from\\s+['\"][^'\"]+['\"])?/g);\n if (matches) {\n for (const match of matches) {\n // Skip if it's already a type export\n if (/^export\\s+type\\s+/.test(match)) continue;\n\n // Extract the specifiers\n const specifiersMatch = match.match(/export\\s+\\{([^}]*)\\}/);\n if (specifiersMatch) {\n // Capture group 1 exists whenever the pattern matched. `?? \"\"`\n // yields an empty specifier list, which `some()` below reads as\n // \"no runtime specifier\" — the same answer an empty `{}` gives.\n const specifiers = specifiersMatch[1] ?? \"\";\n const items = specifiers\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n\n // If any specifier is NOT prefixed with \"type \", this is a runtime export\n const hasRuntimeSpecifier = items.some((item) => !item.startsWith(\"type \"));\n if (hasRuntimeSpecifier) {\n return false;\n }\n }\n }\n }\n continue;\n }\n\n return false;\n }\n }\n\n // If we get here, no runtime exports were found\n // But we should also verify the file has at least some type exports\n // (an empty file or file with only imports is not really \"type-only\")\n const hasTypeExports = typeOnlyPatterns.some((pattern) => {\n pattern.lastIndex = 0;\n return pattern.test(withoutStrings);\n });\n\n return hasTypeExports;\n}\n\n/**\n * Check if an import statement is a pure `import type` (the keyword sits\n * immediately after `import`, covering `import type { X }`, `import type Foo`,\n * and `import type * as Ns`).\n */\nfunction isTypeOnlyImport(line: string): boolean {\n const trimmed = line.trim();\n\n return trimmed.startsWith(\"import type \") || !!trimmed.match(/^import\\s+type\\s+[\\{\\*]/);\n}\n\n/**\n * Decide whether an `import ... from \"m\"` statement contributes any runtime\n * binding. Returns false for pure `import type` and for destructured imports\n * where every specifier is prefixed with the `type` keyword.\n */\nfunction hasRuntimeImports(line: string): boolean {\n const trimmed = line.trim();\n\n if (isTypeOnlyImport(trimmed)) {\n return false;\n }\n\n const specifiersMatch = trimmed.match(/import\\s+(?:type\\s+)?\\{([^}]+)\\}/);\n\n if (!specifiersMatch) {\n return true;\n }\n\n const items = (specifiersMatch[1] ?? \"\")\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n\n return !items.every((item) => /^type\\s+\\w+/.test(item));\n}\n\n/**\n * Decide whether an `export ... from \"m\"` statement re-exports only types.\n * Covers `export type { X } from`, `export type * from`, and\n * `export { type X, type Y } from`.\n */\nfunction isExportTypeOnlyStatement(line: string): boolean {\n const trimmed = line.trim();\n\n if (/^export\\s+type\\s+/.test(trimmed)) {\n return true;\n }\n\n const specifiersMatch = trimmed.match(/export\\s+\\{([^}]+)\\}/);\n\n if (!specifiersMatch) {\n return false;\n }\n\n const items = (specifiersMatch[1] ?? \"\")\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n\n return items.every((item) => /^type\\s+\\w+/.test(item));\n}\n\n/**\n * Extract import paths using regex (more reliable for TypeScript).\n * Each entry carries an `isTypeOnly` flag: true when every occurrence of the\n * path in the source is a type-only statement (no runtime binding).\n * A single runtime occurrence makes the path runtime.\n */\nfunction extractImportPathsWithRegex(\n source: string,\n): Array<{ path: string; originalLine: string; isTypeOnly: boolean }> {\n const imports: Array<{ path: string; originalLine: string; isTypeOnly: boolean }> = [];\n const seenPaths = new Map<string, number>();\n\n /**\n * Record an import path with its type-only flag.\n * If the same path appears in multiple statements, the edge is type-only\n * iff every occurrence is type-only — a single runtime statement makes it runtime.\n */\n const record = (importPath: string, originalLine: string, isTypeOnly: boolean): void => {\n if (!importPath) {\n return;\n }\n\n const existingIndex = seenPaths.get(importPath);\n\n if (existingIndex === undefined) {\n seenPaths.set(importPath, imports.length);\n imports.push({ path: importPath, originalLine, isTypeOnly });\n\n return;\n }\n\n if (!isTypeOnly) {\n const existing = imports[existingIndex];\n\n // `existingIndex` came from findIndex on this same array, so it is in\n // range; the compiler does not carry that here.\n if (existing) existing.isTypeOnly = false;\n }\n };\n\n // Pattern 1: Standard ES module imports (handles multiline)\n // Matches: import { ... } from \"path\", import Foo from \"path\", import Foo, { ... } from \"path\"\n const importRegex =\n /import\\s+(?:type\\s+)?(\\{[\\s\\S]*?\\}|\\*\\s+as\\s+\\w+|\\w+(?:\\s*,\\s*\\{[\\s\\S]*?\\})?)\\s+from\\s+['\"]([^'\"]+)['\"]/g;\n\n let match;\n\n while ((match = importRegex.exec(source)) !== null) {\n const fullMatch = match[0];\n const importPath = match[2];\n\n // Each pattern here captures its path in a group, so a match always carries\n // one. Skipping rather than recording keeps a malformed match out of the\n // dependency graph entirely — recording `undefined` would add an import\n // edge to a module named \"undefined\", which the dev server would then\n // watch, fail to resolve, and report against the wrong file.\n if (importPath === undefined) continue;\n\n const isTypeOnly = !hasRuntimeImports(fullMatch);\n\n record(importPath, fullMatch, isTypeOnly);\n }\n\n // Pattern 1b: Side-effect imports - import \"path\" — always runtime\n const sideEffectRegex = /import\\s+['\"]([^'\"]+)['\"]/g;\n\n while ((match = sideEffectRegex.exec(source)) !== null) {\n const sideEffectPath = match[1];\n\n if (sideEffectPath === undefined) continue;\n\n record(sideEffectPath, match[0], false);\n }\n\n // Pattern 2: Dynamic imports - import(\"path\") — always runtime\n const dynamicImportPattern = /import\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g;\n\n while ((match = dynamicImportPattern.exec(source)) !== null) {\n const dynamicPath = match[1];\n\n if (dynamicPath === undefined) continue;\n\n record(dynamicPath, match[0], false);\n }\n\n // Pattern 3: Export from - export ... from \"path\"\n const exportFromPattern = /export\\s+(?:\\{[^}]*\\}|\\*|\\w+)\\s+from\\s+['\"]([^'\"]+)['\"]/g;\n\n while ((match = exportFromPattern.exec(source)) !== null) {\n const fullMatch = match[0];\n const exportPath = match[1];\n\n if (exportPath === undefined) continue;\n\n record(exportPath, fullMatch, isExportTypeOnlyStatement(fullMatch));\n }\n\n return imports;\n}\n\n/**\n * Metadata for a single resolved import edge.\n * `isTypeOnly` is true iff every import/export statement that references this\n * path is type-only (pure `import type`, `export type`, or destructured lists\n * where every specifier carries the `type` keyword). A single runtime\n * occurrence flips the flag to false — that's what matters for cycle detection.\n */\nexport type ResolvedImport = {\n absolutePath: string;\n isTypeOnly: boolean;\n};\n\n/**\n * Parse import/export statements in a TS/JS source file and return the\n * resolved dependency edges keyed by the original import path. Includes\n * type-only edges (flagged) so downstream consumers can distinguish runtime\n * cycles from type-only ones.\n *\n * @example\n * const importMap = await parseImports(source, \"/abs/path/to/file.ts\");\n * for (const [importPath, { absolutePath, isTypeOnly }] of importMap) {\n * // ...\n * }\n */\nexport async function parseImports(\n source: string,\n filePath: string,\n): Promise<Map<string, ResolvedImport>> {\n try {\n // Skip .d.ts files - they're type declarations, not runtime code\n if (filePath.endsWith(\".d.ts\")) {\n return new Map();\n }\n\n // Try es-module-lexer first (faster and more accurate for simple cases)\n try {\n const [imports] = await parse(source);\n\n if (imports && imports.length > 0) {\n return await resolveImports(imports as ImportSpecifier[], source, filePath);\n }\n } catch (lexerError) {\n // es-module-lexer failed, fall back to regex-based extraction\n // This is common with TypeScript files that have complex syntax\n }\n\n // Fallback: Use regex-based extraction (more forgiving with TypeScript)\n const regexImports = extractImportPathsWithRegex(source);\n const resolvedImports = new Map<string, ResolvedImport>();\n\n for (const { path: importPath, isTypeOnly } of regexImports) {\n if (isNodeBuiltin(importPath)) {\n continue;\n }\n\n if (!importPath.startsWith(\".\") && !tsconfigManager.isAlias(importPath)) {\n continue;\n }\n\n let resolvedPath: string | null = null;\n\n if (tsconfigManager.isAlias(importPath)) {\n resolvedPath = await resolveAliasImport(importPath);\n } else if (importPath.startsWith(\".\")) {\n resolvedPath = await resolveRelativeImport(importPath, filePath);\n }\n\n if (resolvedPath) {\n mergeResolvedImport(resolvedImports, importPath, resolvedPath, isTypeOnly);\n }\n }\n\n return resolvedImports;\n } catch (error) {\n console.error(`Error parsing imports for ${filePath}:`, error);\n\n return new Map();\n }\n}\n\n/**\n * Merge a resolved import into the output map, preserving the type-only rule:\n * a path is type-only iff every statement that references it is type-only.\n */\nfunction mergeResolvedImport(\n target: Map<string, ResolvedImport>,\n importPath: string,\n absolutePath: string,\n isTypeOnly: boolean,\n): void {\n const existing = target.get(importPath);\n\n if (!existing) {\n target.set(importPath, { absolutePath, isTypeOnly });\n\n return;\n }\n\n if (!isTypeOnly) {\n existing.isTypeOnly = false;\n }\n}\n\n/**\n * Resolve imports from `es-module-lexer` output. For each specifier, slice the\n * raw statement (`ss..se`) and re-run the type-only heuristic so we can flag\n * the edge correctly — the lexer itself does not distinguish `import type`.\n */\nasync function resolveImports(\n imports: ImportSpecifier[],\n source: string,\n filePath: string,\n): Promise<Map<string, ResolvedImport>> {\n const resolvedImports = new Map<string, ResolvedImport>();\n\n for (const imp of imports) {\n const importPath = imp.n;\n\n if (!importPath) {\n continue;\n }\n\n if (isNodeBuiltin(importPath)) {\n continue;\n }\n\n if (!importPath.startsWith(\".\") && !tsconfigManager.isAlias(importPath)) {\n continue;\n }\n\n let resolvedPath: string | null = null;\n\n if (tsconfigManager.isAlias(importPath)) {\n resolvedPath = await resolveAliasImport(importPath);\n } else if (importPath.startsWith(\".\")) {\n resolvedPath = await resolveRelativeImport(importPath, filePath);\n }\n\n if (!resolvedPath) {\n continue;\n }\n\n const isTypeOnly = isLexerStatementTypeOnly(imp, source);\n\n mergeResolvedImport(resolvedImports, importPath, resolvedPath, isTypeOnly);\n }\n\n return resolvedImports;\n}\n\n/**\n * Classify a single lexer-reported statement as type-only or runtime.\n * Dynamic imports (`import(...)`) are always runtime. For static\n * imports/export-from we slice the raw source and apply the same heuristic\n * the regex fallback uses.\n */\nfunction isLexerStatementTypeOnly(imp: ImportSpecifier, source: string): boolean {\n // Dynamic imports always execute — they cannot be type-only.\n // es-module-lexer reports dynamic with a.n === undefined typically, but\n // we guard by checking the statement prefix too.\n const statementStart = imp.ss;\n const statementEnd = imp.se;\n\n if (\n typeof statementStart !== \"number\" ||\n typeof statementEnd !== \"number\" ||\n statementEnd <= statementStart\n ) {\n return false;\n }\n\n const statement = source.slice(statementStart, statementEnd);\n const trimmed = statement.trim();\n\n if (trimmed.startsWith(\"import(\") || /^\\bimport\\s*\\(/.test(trimmed)) {\n return false;\n }\n\n if (trimmed.startsWith(\"export\")) {\n return isExportTypeOnlyStatement(statement);\n }\n\n return !hasRuntimeImports(statement);\n}\n\n/**\n * Resolve alias imports to actual file paths with extensions\n * Example: app/users/services/get-users.service -> /absolute/path/to/src/app/users/services/get-users.service.ts\n */\nasync function resolveAliasImport(importPath: string): Promise<string | null> {\n // Use tsconfig manager to resolve the alias to an absolute path\n const resolvedBase = tsconfigManager.resolveAliasToAbsolute(importPath);\n\n if (!resolvedBase) return null;\n\n // Try to resolve with extensions\n const resolvedPath = await tryResolveWithExtensions(resolvedBase);\n\n return resolvedPath;\n}\n\n/**\n * Resolve relative imports to actual file paths\n * Example: ./../services/get-user.service -> /absolute/path/to/services/get-user.service.ts\n */\nasync function resolveRelativeImport(\n importPath: string,\n currentFilePath: string,\n): Promise<string | null> {\n const dir = path.dirname(currentFilePath);\n // Use path.resolve to handle .. and . properly, then normalize to forward slashes\n const resolvedBase = Path.normalize(path.resolve(dir, importPath));\n\n // Try to resolve with extensions\n const resolvedPath = await tryResolveWithExtensions(resolvedBase);\n\n return resolvedPath;\n}\n\n/**\n * Try to resolve a file path by checking different extensions\n * TypeScript/JavaScript files can be imported without extensions\n *\n * @TODO: For better performance, we need to check the files in files orchestrator\n * instead of using the file system as we will be fetching all project files anyway.\n */\n// Cache for file existence checks to avoid redundant filesystem calls\nconst fileExistsCache = new Map<string, boolean>();\n\n/**\n * Clear the file exists cache\n * Should be called when new files are created to ensure fresh lookups\n */\nexport function clearFileExistsCache(): void {\n fileExistsCache.clear();\n}\n\nasync function cachedFileExists(filePath: string): Promise<boolean> {\n if (fileExistsCache.has(filePath)) {\n return fileExistsCache.get(filePath)!;\n }\n const exists = (await fileExistsAsync(filePath)) as boolean;\n fileExistsCache.set(filePath, exists);\n return exists;\n}\n\nasync function tryResolveWithExtensions(basePath: string): Promise<string | null> {\n // Normalize the base path first (handle Windows paths)\n const normalizedBase = Path.normalize(basePath);\n\n // List of extensions to try, in order of preference\n const extensions = [\".ts\", \".tsx\", \".js\", \".jsx\", \".mjs\", \".cjs\"];\n const validExtensions = new Set(extensions);\n\n // If the path already has a VALID code file extension, check if it exists\n const ext = path.extname(normalizedBase);\n if (ext && validExtensions.has(ext)) {\n if (await cachedFileExists(normalizedBase)) {\n return normalizedBase;\n }\n // If explicit extension doesn't exist, return null\n return null;\n }\n\n // Try all extensions in parallel for better performance\n const pathsToCheck = extensions.map((extension) => normalizedBase + extension);\n const results = await Promise.all(\n pathsToCheck.map(async (p) => ({ path: p, exists: await cachedFileExists(p) })),\n );\n\n // Return the first one that exists (in order of preference)\n for (const result of results) {\n if (result.exists) {\n return result.path;\n }\n }\n\n // Try index files in directory\n if (await directoryExistsAsync(normalizedBase)) {\n const indexPaths = extensions.map((extension) =>\n Path.join(normalizedBase, `index${extension}`),\n );\n const indexResults = await Promise.all(\n indexPaths.map(async (p) => ({ path: p, exists: await cachedFileExists(p) })),\n );\n\n for (const result of indexResults) {\n if (result.exists) {\n return result.path;\n }\n }\n }\n\n return null;\n}\n\n/**\n * Check if import is a Node.js built-in module\n */\nfunction isNodeBuiltin(importPath: string): boolean {\n const builtins = [\n \"fs\",\n \"path\",\n \"http\",\n \"https\",\n \"crypto\",\n \"stream\",\n \"util\",\n \"events\",\n \"buffer\",\n \"child_process\",\n \"os\",\n \"url\",\n \"querystring\",\n \"zlib\",\n \"net\",\n \"tls\",\n \"dns\",\n \"dgram\",\n \"cluster\",\n \"worker_threads\",\n \"perf_hooks\",\n \"async_hooks\",\n \"timers\",\n \"readline\",\n \"repl\",\n \"vm\",\n \"assert\",\n \"console\",\n \"process\",\n \"v8\",\n ];\n\n // Check for node: prefix or direct builtin name\n if (importPath.startsWith(\"node:\")) return true;\n\n // `split` always yields a first element. Falling back to the whole path\n // keeps the builtin check answering about a real name rather than undefined.\n const moduleName = importPath.split(\"/\")[0] ?? importPath;\n\n return builtins.includes(moduleName);\n}\n"],"mappings":"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"}
|
|
@@ -66,6 +66,7 @@ function decrypt(cipherText) {
|
|
|
66
66
|
const parts = cipherText.split(":");
|
|
67
67
|
if (parts.length !== 3) throw new Error("Invalid encrypted format. Expected iv:ciphertext:authTag");
|
|
68
68
|
const [ivHex, encryptedHex, authTagHex] = parts;
|
|
69
|
+
if (ivHex === void 0 || encryptedHex === void 0 || authTagHex === void 0) throw new Error("Invalid encrypted format. Expected iv:ciphertext:authTag");
|
|
69
70
|
const decipher = crypto.createDecipheriv(algorithm, keyBuffer, Buffer.from(ivHex, "hex"));
|
|
70
71
|
decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
|
|
71
72
|
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"encrypt.mjs","names":[],"sources":["../../../../../../../core/src/encryption/encrypt.ts"],"sourcesContent":["import crypto from \"crypto\";\nimport { config } from \"../config\";\nimport { type EncryptionConfigurations } from \"./types\";\n\n/**\n * Default encryption algorithm.\n */\nconst DEFAULT_ALGORITHM = \"aes-256-gcm\";\n\n/**\n * Get the encryption key from config, validated as a 32-byte hex string.\n */\nfunction getEncryptionKey(): Buffer {\n const key = config.key<EncryptionConfigurations[\"key\"]>(\"encryption.key\");\n\n if (!key) {\n throw new Error(\n \"Missing encryption key. Set 'encryption.key' in your config (64 hex characters = 32 bytes).\",\n );\n }\n\n const buffer = Buffer.from(key, \"hex\");\n\n if (buffer.length !== 32) {\n throw new Error(\n `Encryption key must be exactly 32 bytes (64 hex characters). Got ${buffer.length} bytes for key ${key}.`,\n );\n }\n\n return buffer;\n}\n\n/**\n * Get the configured encryption algorithm, defaults to aes-256-gcm.\n */\nfunction getAlgorithm(): string {\n return config.key<EncryptionConfigurations[\"algorithm\"]>(\n \"encryption.algorithm\",\n DEFAULT_ALGORITHM,\n )!;\n}\n\n/**\n * Encrypts a plaintext string using AES-256-GCM (or the configured algorithm).\n * Returns a combined string in the format `iv:ciphertext:authTag`.\n *\n * @example\n * import { encrypt } from \"@warlock.js/core\";\n *\n * const encrypted = encrypt(\"sk-proj-12345\");\n * // => \"a1b2c3...:d4e5f6...:g7h8i9...\"\n */\nexport function encrypt(plainText: string): string {\n if (!plainText) return plainText;\n\n const keyBuffer = getEncryptionKey();\n const algorithm = getAlgorithm();\n\n try {\n // IV must be unique per encryption. 16 bytes is standard for AES.\n const iv = crypto.randomBytes(16);\n const cipher = crypto.createCipheriv(algorithm, keyBuffer, iv);\n\n let encrypted = cipher.update(plainText, \"utf8\", \"hex\");\n encrypted += cipher.final(\"hex\");\n\n // Auth tag validates ciphertext integrity (tamper detection)\n const authTag = (cipher as crypto.CipherGCM).getAuthTag().toString(\"hex\");\n\n return `${iv.toString(\"hex\")}:${encrypted}:${authTag}`;\n } catch (error) {\n throw new Error(`Encryption failed: ${(error as Error).message}`);\n }\n}\n\n/**\n * Decrypts a string previously encrypted with {@link encrypt}.\n * Expects the format `iv:ciphertext:authTag`.\n *\n * @example\n * import { decrypt } from \"@warlock.js/core\";\n *\n * const original = decrypt(encryptedString);\n * // => \"sk-proj-12345\"\n */\nexport function decrypt(cipherText: string): string {\n if (!cipherText) return cipherText;\n\n const keyBuffer = getEncryptionKey();\n const algorithm = getAlgorithm();\n\n try {\n const parts = cipherText.split(\":\");\n\n if (parts.length !== 3) {\n throw new Error(\"Invalid encrypted format. Expected iv:ciphertext:authTag\");\n }\n\n const [ivHex, encryptedHex, authTagHex] = parts;\n\n const decipher = crypto.createDecipheriv(algorithm, keyBuffer, Buffer.from(ivHex, \"hex\"));\n\n (decipher as crypto.DecipherGCM).setAuthTag(Buffer.from(authTagHex, \"hex\"));\n\n let decrypted = decipher.update(encryptedHex, \"hex\", \"utf8\");\n // .final() throws if auth tag is invalid (data tampered)\n decrypted += decipher.final(\"utf8\");\n\n return decrypted;\n } catch (error) {\n throw new Error(`Decryption failed: ${(error as Error).message}`);\n }\n}\n"],"mappings":";;;;;;;;AAOA,MAAM,oBAAoB;;;;AAK1B,SAAS,mBAA2B;CAClC,MAAM,MAAM,OAAO,IAAqC,gBAAgB;CAExE,IAAI,CAAC,KACH,MAAM,IAAI,MACR,6FACF;CAGF,MAAM,SAAS,OAAO,KAAK,KAAK,KAAK;CAErC,IAAI,OAAO,WAAW,IACpB,MAAM,IAAI,MACR,oEAAoE,OAAO,OAAO,iBAAiB,IAAI,EACzG;CAGF,OAAO;AACT;;;;AAKA,SAAS,eAAuB;CAC9B,OAAO,OAAO,IACZ,wBACA,iBACF;AACF;;;;;;;;;;;AAYA,SAAgB,QAAQ,WAA2B;CACjD,IAAI,CAAC,WAAW,OAAO;CAEvB,MAAM,YAAY,iBAAiB;CACnC,MAAM,YAAY,aAAa;CAE/B,IAAI;EAEF,MAAM,KAAK,OAAO,YAAY,EAAE;EAChC,MAAM,SAAS,OAAO,eAAe,WAAW,WAAW,EAAE;EAE7D,IAAI,YAAY,OAAO,OAAO,WAAW,QAAQ,KAAK;EACtD,aAAa,OAAO,MAAM,KAAK;EAG/B,MAAM,UAAW,OAA4B,WAAW,CAAC,CAAC,SAAS,KAAK;EAExE,OAAO,GAAG,GAAG,SAAS,KAAK,EAAE,GAAG,UAAU,GAAG;CAC/C,SAAS,OAAO;EACd,MAAM,IAAI,MAAM,sBAAuB,MAAgB,SAAS;CAClE;AACF;;;;;;;;;;;AAYA,SAAgB,QAAQ,YAA4B;CAClD,IAAI,CAAC,YAAY,OAAO;CAExB,MAAM,YAAY,iBAAiB;CACnC,MAAM,YAAY,aAAa;CAE/B,IAAI;EACF,MAAM,QAAQ,WAAW,MAAM,GAAG;EAElC,IAAI,MAAM,WAAW,GACnB,MAAM,IAAI,MAAM,0DAA0D;EAG5E,MAAM,CAAC,OAAO,cAAc,cAAc;EAE1C,MAAM,WAAW,OAAO,iBAAiB,WAAW,WAAW,OAAO,KAAK,OAAO,KAAK,CAAC;EAExF,AAAC,SAAgC,WAAW,OAAO,KAAK,YAAY,KAAK,CAAC;EAE1E,IAAI,YAAY,SAAS,OAAO,cAAc,OAAO,MAAM;EAE3D,aAAa,SAAS,MAAM,MAAM;EAElC,OAAO;CACT,SAAS,OAAO;EACd,MAAM,IAAI,MAAM,sBAAuB,MAAgB,SAAS;CAClE;AACF"}
|
|
1
|
+
{"version":3,"file":"encrypt.mjs","names":[],"sources":["../../../../../../../core/src/encryption/encrypt.ts"],"sourcesContent":["import crypto from \"crypto\";\nimport { config } from \"../config\";\nimport { type EncryptionConfigurations } from \"./types\";\n\n/**\n * Default encryption algorithm.\n */\nconst DEFAULT_ALGORITHM = \"aes-256-gcm\";\n\n/**\n * Get the encryption key from config, validated as a 32-byte hex string.\n */\nfunction getEncryptionKey(): Buffer {\n const key = config.key<EncryptionConfigurations[\"key\"]>(\"encryption.key\");\n\n if (!key) {\n throw new Error(\n \"Missing encryption key. Set 'encryption.key' in your config (64 hex characters = 32 bytes).\",\n );\n }\n\n const buffer = Buffer.from(key, \"hex\");\n\n if (buffer.length !== 32) {\n throw new Error(\n `Encryption key must be exactly 32 bytes (64 hex characters). Got ${buffer.length} bytes for key ${key}.`,\n );\n }\n\n return buffer;\n}\n\n/**\n * Get the configured encryption algorithm, defaults to aes-256-gcm.\n */\nfunction getAlgorithm(): string {\n return config.key<EncryptionConfigurations[\"algorithm\"]>(\n \"encryption.algorithm\",\n DEFAULT_ALGORITHM,\n )!;\n}\n\n/**\n * Encrypts a plaintext string using AES-256-GCM (or the configured algorithm).\n * Returns a combined string in the format `iv:ciphertext:authTag`.\n *\n * @example\n * import { encrypt } from \"@warlock.js/core\";\n *\n * const encrypted = encrypt(\"sk-proj-12345\");\n * // => \"a1b2c3...:d4e5f6...:g7h8i9...\"\n */\nexport function encrypt(plainText: string): string {\n if (!plainText) return plainText;\n\n const keyBuffer = getEncryptionKey();\n const algorithm = getAlgorithm();\n\n try {\n // IV must be unique per encryption. 16 bytes is standard for AES.\n const iv = crypto.randomBytes(16);\n const cipher = crypto.createCipheriv(algorithm, keyBuffer, iv);\n\n let encrypted = cipher.update(plainText, \"utf8\", \"hex\");\n encrypted += cipher.final(\"hex\");\n\n // Auth tag validates ciphertext integrity (tamper detection)\n const authTag = (cipher as crypto.CipherGCM).getAuthTag().toString(\"hex\");\n\n return `${iv.toString(\"hex\")}:${encrypted}:${authTag}`;\n } catch (error) {\n throw new Error(`Encryption failed: ${(error as Error).message}`);\n }\n}\n\n/**\n * Decrypts a string previously encrypted with {@link encrypt}.\n * Expects the format `iv:ciphertext:authTag`.\n *\n * @example\n * import { decrypt } from \"@warlock.js/core\";\n *\n * const original = decrypt(encryptedString);\n * // => \"sk-proj-12345\"\n */\nexport function decrypt(cipherText: string): string {\n if (!cipherText) return cipherText;\n\n const keyBuffer = getEncryptionKey();\n const algorithm = getAlgorithm();\n\n try {\n const parts = cipherText.split(\":\");\n\n if (parts.length !== 3) {\n throw new Error(\"Invalid encrypted format. Expected iv:ciphertext:authTag\");\n }\n\n const [ivHex, encryptedHex, authTagHex] = parts;\n\n if (ivHex === undefined || encryptedHex === undefined || authTagHex === undefined) {\n throw new Error(\"Invalid encrypted format. Expected iv:ciphertext:authTag\");\n }\n\n const decipher = crypto.createDecipheriv(algorithm, keyBuffer, Buffer.from(ivHex, \"hex\"));\n\n (decipher as crypto.DecipherGCM).setAuthTag(Buffer.from(authTagHex, \"hex\"));\n\n let decrypted = decipher.update(encryptedHex, \"hex\", \"utf8\");\n // .final() throws if auth tag is invalid (data tampered)\n decrypted += decipher.final(\"utf8\");\n\n return decrypted;\n } catch (error) {\n throw new Error(`Decryption failed: ${(error as Error).message}`);\n }\n}\n"],"mappings":";;;;;;;;AAOA,MAAM,oBAAoB;;;;AAK1B,SAAS,mBAA2B;CAClC,MAAM,MAAM,OAAO,IAAqC,gBAAgB;CAExE,IAAI,CAAC,KACH,MAAM,IAAI,MACR,6FACF;CAGF,MAAM,SAAS,OAAO,KAAK,KAAK,KAAK;CAErC,IAAI,OAAO,WAAW,IACpB,MAAM,IAAI,MACR,oEAAoE,OAAO,OAAO,iBAAiB,IAAI,EACzG;CAGF,OAAO;AACT;;;;AAKA,SAAS,eAAuB;CAC9B,OAAO,OAAO,IACZ,wBACA,iBACF;AACF;;;;;;;;;;;AAYA,SAAgB,QAAQ,WAA2B;CACjD,IAAI,CAAC,WAAW,OAAO;CAEvB,MAAM,YAAY,iBAAiB;CACnC,MAAM,YAAY,aAAa;CAE/B,IAAI;EAEF,MAAM,KAAK,OAAO,YAAY,EAAE;EAChC,MAAM,SAAS,OAAO,eAAe,WAAW,WAAW,EAAE;EAE7D,IAAI,YAAY,OAAO,OAAO,WAAW,QAAQ,KAAK;EACtD,aAAa,OAAO,MAAM,KAAK;EAG/B,MAAM,UAAW,OAA4B,WAAW,CAAC,CAAC,SAAS,KAAK;EAExE,OAAO,GAAG,GAAG,SAAS,KAAK,EAAE,GAAG,UAAU,GAAG;CAC/C,SAAS,OAAO;EACd,MAAM,IAAI,MAAM,sBAAuB,MAAgB,SAAS;CAClE;AACF;;;;;;;;;;;AAYA,SAAgB,QAAQ,YAA4B;CAClD,IAAI,CAAC,YAAY,OAAO;CAExB,MAAM,YAAY,iBAAiB;CACnC,MAAM,YAAY,aAAa;CAE/B,IAAI;EACF,MAAM,QAAQ,WAAW,MAAM,GAAG;EAElC,IAAI,MAAM,WAAW,GACnB,MAAM,IAAI,MAAM,0DAA0D;EAG5E,MAAM,CAAC,OAAO,cAAc,cAAc;EAE1C,IAAI,UAAU,UAAa,iBAAiB,UAAa,eAAe,QACtE,MAAM,IAAI,MAAM,0DAA0D;EAG5E,MAAM,WAAW,OAAO,iBAAiB,WAAW,WAAW,OAAO,KAAK,OAAO,KAAK,CAAC;EAExF,AAAC,SAAgC,WAAW,OAAO,KAAK,YAAY,KAAK,CAAC;EAE1E,IAAI,YAAY,SAAS,OAAO,cAAc,OAAO,MAAM;EAE3D,aAAa,SAAS,MAAM,MAAM;EAElC,OAAO;CACT,SAAS,OAAO;EACd,MAAM,IAAI,MAAM,sBAAuB,MAAgB,SAAS;CAClE;AACF"}
|
|
@@ -26,6 +26,7 @@ function resolveFeatures(features, visited = /* @__PURE__ */ new Set()) {
|
|
|
26
26
|
if (visited.has(feature)) continue;
|
|
27
27
|
visited.add(feature);
|
|
28
28
|
const def = featuresMap[feature];
|
|
29
|
+
if (def === void 0) continue;
|
|
29
30
|
if (def.requires?.length) resolved.push(...resolveFeatures(def.requires, visited));
|
|
30
31
|
resolved.push(feature);
|
|
31
32
|
}
|
|
@@ -36,7 +37,11 @@ async function addCommandAction(options) {
|
|
|
36
37
|
const { packageManager, list, noInstall } = options.options;
|
|
37
38
|
if (list) {
|
|
38
39
|
console.log("Available Features:");
|
|
39
|
-
for (const feature of allowedFeatures)
|
|
40
|
+
for (const feature of allowedFeatures) {
|
|
41
|
+
const definition = featuresMap[feature];
|
|
42
|
+
if (definition === void 0) continue;
|
|
43
|
+
console.log(`- ${colors.yellowBright(feature)}: ${colors.green(definition.description)}`);
|
|
44
|
+
}
|
|
40
45
|
process.exit(0);
|
|
41
46
|
}
|
|
42
47
|
validateFeatures(features);
|
|
@@ -47,6 +52,7 @@ async function addCommandAction(options) {
|
|
|
47
52
|
const scripts = {};
|
|
48
53
|
for (const feature of resolvedFeatures) {
|
|
49
54
|
const featurePackages = featuresMap[feature];
|
|
55
|
+
if (featurePackages === void 0) continue;
|
|
50
56
|
Object.assign(dependencies, featurePackages.dependencies);
|
|
51
57
|
if (featurePackages.devDependencies) Object.assign(devDependencies, featurePackages.devDependencies);
|
|
52
58
|
if (featurePackages.ejectConfig) ejectConfigs[featurePackages.ejectConfig.name] = featurePackages.ejectConfig;
|
|
@@ -77,7 +83,7 @@ async function addCommandAction(options) {
|
|
|
77
83
|
}
|
|
78
84
|
for (const feature of resolvedFeatures) {
|
|
79
85
|
const featurePackages = featuresMap[feature];
|
|
80
|
-
if (featurePackages
|
|
86
|
+
if (featurePackages?.onExecuting) await featurePackages.onExecuting(options);
|
|
81
87
|
}
|
|
82
88
|
if (Object.keys(scripts).length > 0) {
|
|
83
89
|
console.log(`Adding scripts ${colors.magenta(Object.keys(scripts).join(", "))}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add-command.action.mjs","names":[],"sources":["../../../../../../../core/src/generations/add-command.action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport { fileExistsAsync, getJsonFileAsync, putFileAsync, putJsonFileAsync } from \"@warlock.js/fs\";\r\nimport { execSync } from \"node:child_process\";\r\nimport type { CommandActionData } from \"../commands/types\";\r\nimport {\r\n detectPackageManager,\r\n getAddCommand,\r\n getExactAddCommand,\r\n type PackageManager,\r\n} from \"../updater/package-manager\";\r\nimport { rootPath, srcPath } from \"../utils\";\r\nimport { getWarlockVersion } from \"../utils/framework-vesion\";\r\nimport { featuresMap } from \"./features\";\r\n\r\nexport { featuresMap };\r\nexport type { FeatureDefinition } from \"./features\";\r\n\r\n/**\r\n * The parts of a project `package.json` this action reads or writes. Deliberately\r\n * partial — it describes what we touch, not the whole manifest.\r\n */\r\ntype ProjectPackageJson = {\r\n dependencies?: Record<string, string>;\r\n devDependencies?: Record<string, string>;\r\n scripts?: Record<string, string>;\r\n};\r\n\r\nexport const allowedFeatures = Object.keys(featuresMap);\r\n\r\nfunction isWarlockPackage(packageName: string): boolean {\r\n return packageName.startsWith(\"@warlock.js/\");\r\n}\r\n\r\n/**\r\n * Resolve the internal feature-map placeholder to the version of Core that is\r\n * actually executing the command. Every @warlock.js package is lockstep, so\r\n * Core is the single source of truth; non-Warlock dependencies are untouched.\r\n */\r\nexport function resolveWarlockDependencyVersions(\r\n dependencies: Record<string, string>,\r\n frameworkVersion: string,\r\n): void {\r\n for (const dependency of Object.keys(dependencies)) {\r\n if (isWarlockPackage(dependency)) {\r\n dependencies[dependency] = frameworkVersion;\r\n }\r\n }\r\n}\r\n\r\nfunction resolveFeatures(features: string[], visited = new Set<string>()): string[] {\r\n const resolved: string[] = [];\r\n\r\n for (const feature of features) {\r\n if (visited.has(feature)) continue;\r\n visited.add(feature);\r\n\r\n const def = featuresMap[feature];\r\n\r\n if (def.requires?.length) {\r\n resolved.push(...resolveFeatures(def.requires, visited));\r\n }\r\n\r\n resolved.push(feature);\r\n }\r\n\r\n return resolved;\r\n}\r\n\r\nexport async function addCommandAction(options: CommandActionData) {\r\n const features = options.args;\r\n const { packageManager, list, noInstall } = options.options;\r\n\r\n if (list) {\r\n console.log(\"Available Features:\");\r\n\r\n for (const feature of allowedFeatures) {\r\n console.log(\r\n `- ${colors.yellowBright(feature)}: ${colors.green(featuresMap[feature].description)}`,\r\n );\r\n }\r\n\r\n process.exit(0);\r\n }\r\n\r\n validateFeatures(features);\r\n\r\n const resolvedFeatures = resolveFeatures(features);\r\n\r\n const dependencies: Record<string, string> = {};\r\n const devDependencies: Record<string, string> = {};\r\n const ejectConfigs: Record<string, { content: string; name: string }> = {};\r\n const scripts: Record<string, string> = {};\r\n\r\n for (const feature of resolvedFeatures) {\r\n const featurePackages = featuresMap[feature as keyof typeof featuresMap];\r\n Object.assign(dependencies, featurePackages.dependencies);\r\n if (featurePackages.devDependencies) {\r\n Object.assign(devDependencies, featurePackages.devDependencies);\r\n }\r\n\r\n if (featurePackages.ejectConfig) {\r\n ejectConfigs[featurePackages.ejectConfig.name] = featurePackages.ejectConfig;\r\n }\r\n\r\n if (featurePackages.script) {\r\n Object.assign(scripts, featurePackages.script);\r\n }\r\n }\r\n\r\n // Pin every @warlock.js/* feature package to the INSTALLED framework version so\r\n // a scaffolded project's features match its core version instead of drifting to\r\n // the feature map's static range.\r\n const frameworkVersion = await getWarlockVersion();\r\n resolveWarlockDependencyVersions(dependencies, frameworkVersion);\r\n\r\n const currentPackageJson = await getJsonFileAsync<ProjectPackageJson>(rootPath(\"package.json\"));\r\n\r\n // Fresh templates may omit one of the maps — guard before reading.\r\n currentPackageJson.dependencies = currentPackageJson.dependencies ?? {};\r\n currentPackageJson.devDependencies = currentPackageJson.devDependencies ?? {};\r\n\r\n // Skip anything already present so we never downgrade an existing pin.\r\n for (const dependency of Object.keys(dependencies)) {\r\n if (currentPackageJson.dependencies[dependency]) {\r\n console.log(`${colors.yellowBright(dependency)} is already installed, skipping...`);\r\n delete dependencies[dependency];\r\n }\r\n }\r\n\r\n for (const devDependency of Object.keys(devDependencies)) {\r\n if (currentPackageJson.devDependencies[devDependency]) {\r\n console.log(`${colors.yellowBright(devDependency)} is already installed, skipping...`);\r\n delete devDependencies[devDependency];\r\n }\r\n }\r\n\r\n if (noInstall) {\r\n await recordDependencies(dependencies, devDependencies);\r\n } else {\r\n await installDependencies(\r\n packageManager as PackageManager | undefined,\r\n dependencies,\r\n devDependencies,\r\n );\r\n }\r\n\r\n for (const [name, config] of Object.entries(ejectConfigs)) {\r\n if (await fileExistsAsync(srcPath(`config/${name}.ts`))) {\r\n console.log(`${colors.yellowBright(name)} config already exists, skipping...`);\r\n continue;\r\n }\r\n\r\n console.log(`Creating ${colors.magenta(name)} config...`);\r\n\r\n await putFileAsync(srcPath(`config/${name}.ts`), config.content);\r\n\r\n console.log(`${colors.green(name)} config created successfully`);\r\n }\r\n\r\n // now loop again over features to execute onExecuting\r\n for (const feature of resolvedFeatures) {\r\n const featurePackages = featuresMap[feature as keyof typeof featuresMap];\r\n if (featurePackages.onExecuting) {\r\n await featurePackages.onExecuting(options);\r\n }\r\n }\r\n\r\n if (Object.keys(scripts).length > 0) {\r\n console.log(`Adding scripts ${colors.magenta(Object.keys(scripts).join(\", \"))}`);\r\n const packageJsonPath = rootPath(\"package.json\");\r\n const packageJson = await getJsonFileAsync<ProjectPackageJson>(packageJsonPath);\r\n packageJson.scripts = { ...(packageJson.scripts ?? {}), ...scripts };\r\n await putJsonFileAsync(packageJsonPath, packageJson);\r\n\r\n console.log(`Scripts added successfully ${colors.green(Object.keys(scripts).join(\", \"))}`);\r\n }\r\n}\r\n\r\n/**\r\n * Install the resolved dependency sets through the project's package manager.\r\n * Runs two passes (prod then dev) so each lands in the correct section.\r\n */\r\nexport async function installDependencies(\r\n packageManager: PackageManager | undefined,\r\n dependencies: Record<string, string>,\r\n devDependencies: Record<string, string>,\r\n) {\r\n // `--package-manager` is optional; without it, fall back to the lockfile.\r\n const resolvedPackageManager = packageManager ?? (await detectPackageManager());\r\n const packageManagerCommand = getAddCommand(resolvedPackageManager);\r\n const exactPackageManagerCommand = getExactAddCommand(resolvedPackageManager);\r\n\r\n const installDependencySet = (dependencySet: Record<string, string>, development: boolean) => {\r\n const regularSpecs: string[] = [];\r\n const exactWarlockSpecs: string[] = [];\r\n\r\n for (const [name, version] of Object.entries(dependencySet)) {\r\n const spec = `${name}@${version}`;\r\n (isWarlockPackage(name) ? exactWarlockSpecs : regularSpecs).push(spec);\r\n }\r\n\r\n const developmentFlag = development ? \" -D\" : \"\";\r\n const runInstall = (command: string, specs: string[]) => {\r\n if (specs.length === 0) return;\r\n\r\n execSync(`${command} ${specs.join(\" \")}${developmentFlag}`, {\r\n cwd: process.cwd(),\r\n stdio: \"inherit\",\r\n });\r\n };\r\n\r\n runInstall(packageManagerCommand, regularSpecs);\r\n runInstall(exactPackageManagerCommand, exactWarlockSpecs);\r\n };\r\n\r\n if (Object.keys(dependencies).length > 0) {\r\n console.log(`Installing dependencies ${colors.magenta(Object.keys(dependencies).join(\", \"))}`);\r\n\r\n installDependencySet(dependencies, false);\r\n\r\n console.log(\r\n `Dependencies installed successfully ${colors.green(Object.keys(dependencies).join(\", \"))}`,\r\n );\r\n }\r\n\r\n if (Object.keys(devDependencies).length > 0) {\r\n console.log(\r\n `Installing dev dependencies ${colors.magenta(Object.keys(devDependencies).join(\", \"))}`,\r\n );\r\n\r\n installDependencySet(devDependencies, true);\r\n\r\n console.log(\r\n `Dev dependencies installed successfully ${colors.green(Object.keys(devDependencies).join(\", \"))}`,\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Write the resolved dependency sets into package.json without installing.\r\n * Used by `--no-install` so a scaffolder can batch every feature into one\r\n * install pass after the command returns. Versions come from the feature map.\r\n */\r\nasync function recordDependencies(\r\n dependencies: Record<string, string>,\r\n devDependencies: Record<string, string>,\r\n) {\r\n if (Object.keys(dependencies).length === 0 && Object.keys(devDependencies).length === 0) {\r\n return;\r\n }\r\n\r\n const packageJsonPath = rootPath(\"package.json\");\r\n const packageJson = await getJsonFileAsync<ProjectPackageJson>(packageJsonPath);\r\n\r\n packageJson.dependencies = packageJson.dependencies ?? {};\r\n packageJson.devDependencies = packageJson.devDependencies ?? {};\r\n\r\n Object.assign(packageJson.dependencies, dependencies);\r\n Object.assign(packageJson.devDependencies, devDependencies);\r\n\r\n await putJsonFileAsync(packageJsonPath, packageJson);\r\n\r\n const recorded = [...Object.keys(dependencies), ...Object.keys(devDependencies)];\r\n\r\n console.log(\r\n `Recorded ${colors.green(recorded.join(\", \"))} in package.json (install skipped via --no-install)`,\r\n );\r\n}\r\n\r\nfunction validateFeatures(features: string[]) {\r\n for (const feature of features) {\r\n if (!allowedFeatures.includes(feature)) {\r\n console.log(\r\n `Feature ${colors.redBright(feature)} is not allowed, allowed features are: ${colors.green(allowedFeatures.join(\", \"))}`,\r\n );\r\n process.exit(1);\r\n }\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"add-command.action.mjs","names":[],"sources":["../../../../../../../core/src/generations/add-command.action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport { fileExistsAsync, getJsonFileAsync, putFileAsync, putJsonFileAsync } from \"@warlock.js/fs\";\r\nimport { execSync } from \"node:child_process\";\r\nimport type { CommandActionData } from \"../commands/types\";\r\nimport {\r\n detectPackageManager,\r\n getAddCommand,\r\n getExactAddCommand,\r\n type PackageManager,\r\n} from \"../updater/package-manager\";\r\nimport { rootPath, srcPath } from \"../utils\";\r\nimport { getWarlockVersion } from \"../utils/framework-vesion\";\r\nimport { featuresMap } from \"./features\";\r\n\r\nexport { featuresMap };\r\nexport type { FeatureDefinition } from \"./features\";\r\n\r\n/**\r\n * The parts of a project `package.json` this action reads or writes. Deliberately\r\n * partial — it describes what we touch, not the whole manifest.\r\n */\r\ntype ProjectPackageJson = {\r\n dependencies?: Record<string, string>;\r\n devDependencies?: Record<string, string>;\r\n scripts?: Record<string, string>;\r\n};\r\n\r\nexport const allowedFeatures = Object.keys(featuresMap);\r\n\r\nfunction isWarlockPackage(packageName: string): boolean {\r\n return packageName.startsWith(\"@warlock.js/\");\r\n}\r\n\r\n/**\r\n * Resolve the internal feature-map placeholder to the version of Core that is\r\n * actually executing the command. Every @warlock.js package is lockstep, so\r\n * Core is the single source of truth; non-Warlock dependencies are untouched.\r\n */\r\nexport function resolveWarlockDependencyVersions(\r\n dependencies: Record<string, string>,\r\n frameworkVersion: string,\r\n): void {\r\n for (const dependency of Object.keys(dependencies)) {\r\n if (isWarlockPackage(dependency)) {\r\n dependencies[dependency] = frameworkVersion;\r\n }\r\n }\r\n}\r\n\r\nfunction resolveFeatures(features: string[], visited = new Set<string>()): string[] {\r\n const resolved: string[] = [];\r\n\r\n for (const feature of features) {\r\n if (visited.has(feature)) continue;\r\n visited.add(feature);\r\n\r\n const def = featuresMap[feature];\r\n\r\n // `addCommandAction` runs `validateFeatures()` before calling this, and the\r\n // recursive calls below pass `requires` entries that come out of the map\r\n // itself — so every lookup resolves. Skipping an unresolvable one keeps a\r\n // typo from crashing the command with `Cannot read properties of\r\n // undefined`, which names neither the feature nor the fact that it was the\r\n // ARGUMENT that was wrong.\r\n if (def === undefined) continue;\r\n\r\n if (def.requires?.length) {\r\n resolved.push(...resolveFeatures(def.requires, visited));\r\n }\r\n\r\n resolved.push(feature);\r\n }\r\n\r\n return resolved;\r\n}\r\n\r\nexport async function addCommandAction(options: CommandActionData) {\r\n const features = options.args;\r\n const { packageManager, list, noInstall } = options.options;\r\n\r\n if (list) {\r\n console.log(\"Available Features:\");\r\n\r\n for (const feature of allowedFeatures) {\r\n const definition = featuresMap[feature];\r\n\r\n // `allowedFeatures` is derived from this same map, so this resolves.\r\n if (definition === undefined) continue;\r\n\r\n console.log(\r\n `- ${colors.yellowBright(feature)}: ${colors.green(definition.description)}`,\r\n );\r\n }\r\n\r\n process.exit(0);\r\n }\r\n\r\n validateFeatures(features);\r\n\r\n const resolvedFeatures = resolveFeatures(features);\r\n\r\n const dependencies: Record<string, string> = {};\r\n const devDependencies: Record<string, string> = {};\r\n const ejectConfigs: Record<string, { content: string; name: string }> = {};\r\n const scripts: Record<string, string> = {};\r\n\r\n for (const feature of resolvedFeatures) {\r\n const featurePackages = featuresMap[feature as keyof typeof featuresMap];\r\n\r\n // Validated upstream; see the note in resolveFeatures above.\r\n if (featurePackages === undefined) continue;\r\n\r\n Object.assign(dependencies, featurePackages.dependencies);\r\n if (featurePackages.devDependencies) {\r\n Object.assign(devDependencies, featurePackages.devDependencies);\r\n }\r\n\r\n if (featurePackages.ejectConfig) {\r\n ejectConfigs[featurePackages.ejectConfig.name] = featurePackages.ejectConfig;\r\n }\r\n\r\n if (featurePackages.script) {\r\n Object.assign(scripts, featurePackages.script);\r\n }\r\n }\r\n\r\n // Pin every @warlock.js/* feature package to the INSTALLED framework version so\r\n // a scaffolded project's features match its core version instead of drifting to\r\n // the feature map's static range.\r\n const frameworkVersion = await getWarlockVersion();\r\n resolveWarlockDependencyVersions(dependencies, frameworkVersion);\r\n\r\n const currentPackageJson = await getJsonFileAsync<ProjectPackageJson>(rootPath(\"package.json\"));\r\n\r\n // Fresh templates may omit one of the maps — guard before reading.\r\n currentPackageJson.dependencies = currentPackageJson.dependencies ?? {};\r\n currentPackageJson.devDependencies = currentPackageJson.devDependencies ?? {};\r\n\r\n // Skip anything already present so we never downgrade an existing pin.\r\n for (const dependency of Object.keys(dependencies)) {\r\n if (currentPackageJson.dependencies[dependency]) {\r\n console.log(`${colors.yellowBright(dependency)} is already installed, skipping...`);\r\n delete dependencies[dependency];\r\n }\r\n }\r\n\r\n for (const devDependency of Object.keys(devDependencies)) {\r\n if (currentPackageJson.devDependencies[devDependency]) {\r\n console.log(`${colors.yellowBright(devDependency)} is already installed, skipping...`);\r\n delete devDependencies[devDependency];\r\n }\r\n }\r\n\r\n if (noInstall) {\r\n await recordDependencies(dependencies, devDependencies);\r\n } else {\r\n await installDependencies(\r\n packageManager as PackageManager | undefined,\r\n dependencies,\r\n devDependencies,\r\n );\r\n }\r\n\r\n for (const [name, config] of Object.entries(ejectConfigs)) {\r\n if (await fileExistsAsync(srcPath(`config/${name}.ts`))) {\r\n console.log(`${colors.yellowBright(name)} config already exists, skipping...`);\r\n continue;\r\n }\r\n\r\n console.log(`Creating ${colors.magenta(name)} config...`);\r\n\r\n await putFileAsync(srcPath(`config/${name}.ts`), config.content);\r\n\r\n console.log(`${colors.green(name)} config created successfully`);\r\n }\r\n\r\n // now loop again over features to execute onExecuting\r\n for (const feature of resolvedFeatures) {\r\n const featurePackages = featuresMap[feature as keyof typeof featuresMap];\r\n\r\n if (featurePackages?.onExecuting) {\r\n await featurePackages.onExecuting(options);\r\n }\r\n }\r\n\r\n if (Object.keys(scripts).length > 0) {\r\n console.log(`Adding scripts ${colors.magenta(Object.keys(scripts).join(\", \"))}`);\r\n const packageJsonPath = rootPath(\"package.json\");\r\n const packageJson = await getJsonFileAsync<ProjectPackageJson>(packageJsonPath);\r\n packageJson.scripts = { ...(packageJson.scripts ?? {}), ...scripts };\r\n await putJsonFileAsync(packageJsonPath, packageJson);\r\n\r\n console.log(`Scripts added successfully ${colors.green(Object.keys(scripts).join(\", \"))}`);\r\n }\r\n}\r\n\r\n/**\r\n * Install the resolved dependency sets through the project's package manager.\r\n * Runs two passes (prod then dev) so each lands in the correct section.\r\n */\r\nexport async function installDependencies(\r\n packageManager: PackageManager | undefined,\r\n dependencies: Record<string, string>,\r\n devDependencies: Record<string, string>,\r\n) {\r\n // `--package-manager` is optional; without it, fall back to the lockfile.\r\n const resolvedPackageManager = packageManager ?? (await detectPackageManager());\r\n const packageManagerCommand = getAddCommand(resolvedPackageManager);\r\n const exactPackageManagerCommand = getExactAddCommand(resolvedPackageManager);\r\n\r\n const installDependencySet = (dependencySet: Record<string, string>, development: boolean) => {\r\n const regularSpecs: string[] = [];\r\n const exactWarlockSpecs: string[] = [];\r\n\r\n for (const [name, version] of Object.entries(dependencySet)) {\r\n const spec = `${name}@${version}`;\r\n (isWarlockPackage(name) ? exactWarlockSpecs : regularSpecs).push(spec);\r\n }\r\n\r\n const developmentFlag = development ? \" -D\" : \"\";\r\n const runInstall = (command: string, specs: string[]) => {\r\n if (specs.length === 0) return;\r\n\r\n execSync(`${command} ${specs.join(\" \")}${developmentFlag}`, {\r\n cwd: process.cwd(),\r\n stdio: \"inherit\",\r\n });\r\n };\r\n\r\n runInstall(packageManagerCommand, regularSpecs);\r\n runInstall(exactPackageManagerCommand, exactWarlockSpecs);\r\n };\r\n\r\n if (Object.keys(dependencies).length > 0) {\r\n console.log(`Installing dependencies ${colors.magenta(Object.keys(dependencies).join(\", \"))}`);\r\n\r\n installDependencySet(dependencies, false);\r\n\r\n console.log(\r\n `Dependencies installed successfully ${colors.green(Object.keys(dependencies).join(\", \"))}`,\r\n );\r\n }\r\n\r\n if (Object.keys(devDependencies).length > 0) {\r\n console.log(\r\n `Installing dev dependencies ${colors.magenta(Object.keys(devDependencies).join(\", \"))}`,\r\n );\r\n\r\n installDependencySet(devDependencies, true);\r\n\r\n console.log(\r\n `Dev dependencies installed successfully ${colors.green(Object.keys(devDependencies).join(\", \"))}`,\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Write the resolved dependency sets into package.json without installing.\r\n * Used by `--no-install` so a scaffolder can batch every feature into one\r\n * install pass after the command returns. Versions come from the feature map.\r\n */\r\nasync function recordDependencies(\r\n dependencies: Record<string, string>,\r\n devDependencies: Record<string, string>,\r\n) {\r\n if (Object.keys(dependencies).length === 0 && Object.keys(devDependencies).length === 0) {\r\n return;\r\n }\r\n\r\n const packageJsonPath = rootPath(\"package.json\");\r\n const packageJson = await getJsonFileAsync<ProjectPackageJson>(packageJsonPath);\r\n\r\n packageJson.dependencies = packageJson.dependencies ?? {};\r\n packageJson.devDependencies = packageJson.devDependencies ?? {};\r\n\r\n Object.assign(packageJson.dependencies, dependencies);\r\n Object.assign(packageJson.devDependencies, devDependencies);\r\n\r\n await putJsonFileAsync(packageJsonPath, packageJson);\r\n\r\n const recorded = [...Object.keys(dependencies), ...Object.keys(devDependencies)];\r\n\r\n console.log(\r\n `Recorded ${colors.green(recorded.join(\", \"))} in package.json (install skipped via --no-install)`,\r\n );\r\n}\r\n\r\nfunction validateFeatures(features: string[]) {\r\n for (const feature of features) {\r\n if (!allowedFeatures.includes(feature)) {\r\n console.log(\r\n `Feature ${colors.redBright(feature)} is not allowed, allowed features are: ${colors.green(allowedFeatures.join(\", \"))}`,\r\n );\r\n process.exit(1);\r\n }\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;AA2BA,MAAa,kBAAkB,OAAO,KAAK,WAAW;AAEtD,SAAS,iBAAiB,aAA8B;CACtD,OAAO,YAAY,WAAW,cAAc;AAC9C;;;;;;AAOA,SAAgB,iCACd,cACA,kBACM;CACN,KAAK,MAAM,cAAc,OAAO,KAAK,YAAY,GAC/C,IAAI,iBAAiB,UAAU,GAC7B,aAAa,cAAc;AAGjC;AAEA,SAAS,gBAAgB,UAAoB,0BAAU,IAAI,IAAY,GAAa;CAClF,MAAM,WAAqB,CAAC;CAE5B,KAAK,MAAM,WAAW,UAAU;EAC9B,IAAI,QAAQ,IAAI,OAAO,GAAG;EAC1B,QAAQ,IAAI,OAAO;EAEnB,MAAM,MAAM,YAAY;EAQxB,IAAI,QAAQ,QAAW;EAEvB,IAAI,IAAI,UAAU,QAChB,SAAS,KAAK,GAAG,gBAAgB,IAAI,UAAU,OAAO,CAAC;EAGzD,SAAS,KAAK,OAAO;CACvB;CAEA,OAAO;AACT;AAEA,eAAsB,iBAAiB,SAA4B;CACjE,MAAM,WAAW,QAAQ;CACzB,MAAM,EAAE,gBAAgB,MAAM,cAAc,QAAQ;CAEpD,IAAI,MAAM;EACR,QAAQ,IAAI,qBAAqB;EAEjC,KAAK,MAAM,WAAW,iBAAiB;GACrC,MAAM,aAAa,YAAY;GAG/B,IAAI,eAAe,QAAW;GAE9B,QAAQ,IACN,KAAK,OAAO,aAAa,OAAO,EAAE,IAAI,OAAO,MAAM,WAAW,WAAW,GAC3E;EACF;EAEA,QAAQ,KAAK,CAAC;CAChB;CAEA,iBAAiB,QAAQ;CAEzB,MAAM,mBAAmB,gBAAgB,QAAQ;CAEjD,MAAM,eAAuC,CAAC;CAC9C,MAAM,kBAA0C,CAAC;CACjD,MAAM,eAAkE,CAAC;CACzE,MAAM,UAAkC,CAAC;CAEzC,KAAK,MAAM,WAAW,kBAAkB;EACtC,MAAM,kBAAkB,YAAY;EAGpC,IAAI,oBAAoB,QAAW;EAEnC,OAAO,OAAO,cAAc,gBAAgB,YAAY;EACxD,IAAI,gBAAgB,iBAClB,OAAO,OAAO,iBAAiB,gBAAgB,eAAe;EAGhE,IAAI,gBAAgB,aAClB,aAAa,gBAAgB,YAAY,QAAQ,gBAAgB;EAGnE,IAAI,gBAAgB,QAClB,OAAO,OAAO,SAAS,gBAAgB,MAAM;CAEjD;CAMA,iCAAiC,cAAc,MADhB,kBAAkB,CACc;CAE/D,MAAM,qBAAqB,MAAM,iBAAqC,SAAS,cAAc,CAAC;CAG9F,mBAAmB,eAAe,mBAAmB,gBAAgB,CAAC;CACtE,mBAAmB,kBAAkB,mBAAmB,mBAAmB,CAAC;CAG5E,KAAK,MAAM,cAAc,OAAO,KAAK,YAAY,GAC/C,IAAI,mBAAmB,aAAa,aAAa;EAC/C,QAAQ,IAAI,GAAG,OAAO,aAAa,UAAU,EAAE,mCAAmC;EAClF,OAAO,aAAa;CACtB;CAGF,KAAK,MAAM,iBAAiB,OAAO,KAAK,eAAe,GACrD,IAAI,mBAAmB,gBAAgB,gBAAgB;EACrD,QAAQ,IAAI,GAAG,OAAO,aAAa,aAAa,EAAE,mCAAmC;EACrF,OAAO,gBAAgB;CACzB;CAGF,IAAI,WACF,MAAM,mBAAmB,cAAc,eAAe;MAEtD,MAAM,oBACJ,gBACA,cACA,eACF;CAGF,KAAK,MAAM,CAAC,MAAM,WAAW,OAAO,QAAQ,YAAY,GAAG;EACzD,IAAI,MAAM,gBAAgB,QAAQ,UAAU,KAAK,IAAI,CAAC,GAAG;GACvD,QAAQ,IAAI,GAAG,OAAO,aAAa,IAAI,EAAE,oCAAoC;GAC7E;EACF;EAEA,QAAQ,IAAI,YAAY,OAAO,QAAQ,IAAI,EAAE,WAAW;EAExD,MAAM,aAAa,QAAQ,UAAU,KAAK,IAAI,GAAG,OAAO,OAAO;EAE/D,QAAQ,IAAI,GAAG,OAAO,MAAM,IAAI,EAAE,6BAA6B;CACjE;CAGA,KAAK,MAAM,WAAW,kBAAkB;EACtC,MAAM,kBAAkB,YAAY;EAEpC,IAAI,iBAAiB,aACnB,MAAM,gBAAgB,YAAY,OAAO;CAE7C;CAEA,IAAI,OAAO,KAAK,OAAO,CAAC,CAAC,SAAS,GAAG;EACnC,QAAQ,IAAI,kBAAkB,OAAO,QAAQ,OAAO,KAAK,OAAO,CAAC,CAAC,KAAK,IAAI,CAAC,GAAG;EAC/E,MAAM,kBAAkB,SAAS,cAAc;EAC/C,MAAM,cAAc,MAAM,iBAAqC,eAAe;EAC9E,YAAY,UAAU;GAAE,GAAI,YAAY,WAAW,CAAC;GAAI,GAAG;EAAQ;EACnE,MAAM,iBAAiB,iBAAiB,WAAW;EAEnD,QAAQ,IAAI,8BAA8B,OAAO,MAAM,OAAO,KAAK,OAAO,CAAC,CAAC,KAAK,IAAI,CAAC,GAAG;CAC3F;AACF;;;;;AAMA,eAAsB,oBACpB,gBACA,cACA,iBACA;CAEA,MAAM,yBAAyB,kBAAmB,MAAM,qBAAqB;CAC7E,MAAM,wBAAwB,cAAc,sBAAsB;CAClE,MAAM,6BAA6B,mBAAmB,sBAAsB;CAE5E,MAAM,wBAAwB,eAAuC,gBAAyB;EAC5F,MAAM,eAAyB,CAAC;EAChC,MAAM,oBAA8B,CAAC;EAErC,KAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,aAAa,GAAG;GAC3D,MAAM,OAAO,GAAG,KAAK,GAAG;GACxB,CAAC,iBAAiB,IAAI,IAAI,oBAAoB,aAAY,CAAE,KAAK,IAAI;EACvE;EAEA,MAAM,kBAAkB,cAAc,QAAQ;EAC9C,MAAM,cAAc,SAAiB,UAAoB;GACvD,IAAI,MAAM,WAAW,GAAG;GAExB,SAAS,GAAG,QAAQ,GAAG,MAAM,KAAK,GAAG,IAAI,mBAAmB;IAC1D,KAAK,QAAQ,IAAI;IACjB,OAAO;GACT,CAAC;EACH;EAEA,WAAW,uBAAuB,YAAY;EAC9C,WAAW,4BAA4B,iBAAiB;CAC1D;CAEA,IAAI,OAAO,KAAK,YAAY,CAAC,CAAC,SAAS,GAAG;EACxC,QAAQ,IAAI,2BAA2B,OAAO,QAAQ,OAAO,KAAK,YAAY,CAAC,CAAC,KAAK,IAAI,CAAC,GAAG;EAE7F,qBAAqB,cAAc,KAAK;EAExC,QAAQ,IACN,uCAAuC,OAAO,MAAM,OAAO,KAAK,YAAY,CAAC,CAAC,KAAK,IAAI,CAAC,GAC1F;CACF;CAEA,IAAI,OAAO,KAAK,eAAe,CAAC,CAAC,SAAS,GAAG;EAC3C,QAAQ,IACN,+BAA+B,OAAO,QAAQ,OAAO,KAAK,eAAe,CAAC,CAAC,KAAK,IAAI,CAAC,GACvF;EAEA,qBAAqB,iBAAiB,IAAI;EAE1C,QAAQ,IACN,2CAA2C,OAAO,MAAM,OAAO,KAAK,eAAe,CAAC,CAAC,KAAK,IAAI,CAAC,GACjG;CACF;AACF;;;;;;AAOA,eAAe,mBACb,cACA,iBACA;CACA,IAAI,OAAO,KAAK,YAAY,CAAC,CAAC,WAAW,KAAK,OAAO,KAAK,eAAe,CAAC,CAAC,WAAW,GACpF;CAGF,MAAM,kBAAkB,SAAS,cAAc;CAC/C,MAAM,cAAc,MAAM,iBAAqC,eAAe;CAE9E,YAAY,eAAe,YAAY,gBAAgB,CAAC;CACxD,YAAY,kBAAkB,YAAY,mBAAmB,CAAC;CAE9D,OAAO,OAAO,YAAY,cAAc,YAAY;CACpD,OAAO,OAAO,YAAY,iBAAiB,eAAe;CAE1D,MAAM,iBAAiB,iBAAiB,WAAW;CAEnD,MAAM,WAAW,CAAC,GAAG,OAAO,KAAK,YAAY,GAAG,GAAG,OAAO,KAAK,eAAe,CAAC;CAE/E,QAAQ,IACN,YAAY,OAAO,MAAM,SAAS,KAAK,IAAI,CAAC,EAAE,oDAChD;AACF;AAEA,SAAS,iBAAiB,UAAoB;CAC5C,KAAK,MAAM,WAAW,UACpB,IAAI,CAAC,gBAAgB,SAAS,OAAO,GAAG;EACtC,QAAQ,IACN,WAAW,OAAO,UAAU,OAAO,EAAE,yCAAyC,OAAO,MAAM,gBAAgB,KAAK,IAAI,CAAC,GACvH;EACA,QAAQ,KAAK,CAAC;CAChB;AAEJ"}
|
|
@@ -50,7 +50,12 @@ function resolveHomeRouteCollision(files) {
|
|
|
50
50
|
},
|
|
51
51
|
rewrites: []
|
|
52
52
|
};
|
|
53
|
-
const
|
|
53
|
+
const claimant = claimants[0];
|
|
54
|
+
if (claimant === void 0) return {
|
|
55
|
+
collision: { outcome: "absent" },
|
|
56
|
+
rewrites: []
|
|
57
|
+
};
|
|
58
|
+
const { file, matches } = claimant;
|
|
54
59
|
if (matches.length > 1) return {
|
|
55
60
|
collision: {
|
|
56
61
|
outcome: "conflict",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relocate-conflicting-home-route.mjs","names":[],"sources":["../../../../../../../../../core/src/generations/features/shared/relocate-conflicting-home-route.ts"],"sourcesContent":["import { getFileAsync, putFileAsync } from \"@warlock.js/fs\";\nimport glob from \"fast-glob\";\nimport { srcPath } from \"../../../utils\";\n\n/**\n * A TOP-LEVEL `router.get(\"/\", ...)` — anchored at column 0 on purpose.\n *\n * Routes nested in a `router.group({ prefix: \"/x\" }, ...)` are indented by every\n * formatter this codebase runs, and their real path is `/x`, not `/`. Anchoring\n * is what keeps the notifications feature's own `router.get(\"/\", ...)` (inside\n * the `/notifications` group) from reading as a homepage collision.\n *\n * Only the path literal is captured. The handler — a bare identifier in the\n * template, but possibly an inline arrow spanning lines — is never matched, so\n * the rewrite below cannot damage it.\n */\nconst TOP_LEVEL_ROOT_GET = /^router\\s*\\.\\s*get\\(\\s*([\"'`])\\/\\1/gm;\n\n/**\n * Whether `/welcome` is already spoken for, so relocating onto it would trade\n * one duplicate-route 500 for another.\n */\nconst TOP_LEVEL_WELCOME_GET = /^router\\s*\\.\\s*get\\(\\s*([\"'`])\\/welcome\\1/m;\n\nexport type HomeRouteCollision =\n /** Nothing under `src/app` claims a top-level `/` — write the page as normal. */\n | { outcome: \"absent\" }\n /** The one file that claimed `/` was moved to `/welcome`; the page is safe to write. */\n | { outcome: \"relocated\"; relativePath: string }\n /** Something claims `/` that we will not rewrite. The page is NOT written. */\n | { outcome: \"conflict\"; reason: string }\n /** We tried to relocate and could not. The page is NOT written. */\n | { outcome: \"failed\"; reason: string };\n\n/** One routes source file, as read from disk (or handed to the pure resolver in a test). */\nexport type RoutesFileSource = {\n relativePath: string;\n source: string;\n};\n\n/** What {@link resolveHomeRouteCollision} decided, plus the sources it rewrote. */\nexport type HomeRouteResolution = {\n collision: HomeRouteCollision;\n rewrites: RoutesFileSource[];\n};\n\n/**\n * Decide what to do about `src/app/**\\/routes.{ts,tsx}` claiming `/`, and\n * produce the rewritten source when exactly one file does.\n *\n * Pure by design: no filesystem access here, so this is the part a test can\n * exercise directly without touching disk. {@link relocateConflictingHomeRoute}\n * is the thin I/O wrapper — it reads the files, calls this, and writes back\n * whatever this returns in `rewrites`.\n *\n * @param files Every `routes.{ts,tsx}` file under `src/app`, with its current text.\n * @returns The collision outcome, and the (possibly empty) list of files to write back.\n */\nexport function resolveHomeRouteCollision(files: RoutesFileSource[]): HomeRouteResolution {\n const claimants = files\n .map((file) => ({ file, matches: file.source.match(TOP_LEVEL_ROOT_GET) ?? [] }))\n .filter(({ matches }) => matches.length > 0);\n\n if (claimants.length === 0) {\n return { collision: { outcome: \"absent\" }, rewrites: [] };\n }\n\n if (claimants.length > 1) {\n const names = claimants.map(({ file }) => file.relativePath).join(\", \");\n\n return {\n collision: {\n outcome: \"conflict\",\n reason: `multiple files declare a top-level GET \"/\" (${names})`,\n },\n rewrites: [],\n };\n }\n\n const [{ file, matches }
|
|
1
|
+
{"version":3,"file":"relocate-conflicting-home-route.mjs","names":[],"sources":["../../../../../../../../../core/src/generations/features/shared/relocate-conflicting-home-route.ts"],"sourcesContent":["import { getFileAsync, putFileAsync } from \"@warlock.js/fs\";\nimport glob from \"fast-glob\";\nimport { srcPath } from \"../../../utils\";\n\n/**\n * A TOP-LEVEL `router.get(\"/\", ...)` — anchored at column 0 on purpose.\n *\n * Routes nested in a `router.group({ prefix: \"/x\" }, ...)` are indented by every\n * formatter this codebase runs, and their real path is `/x`, not `/`. Anchoring\n * is what keeps the notifications feature's own `router.get(\"/\", ...)` (inside\n * the `/notifications` group) from reading as a homepage collision.\n *\n * Only the path literal is captured. The handler — a bare identifier in the\n * template, but possibly an inline arrow spanning lines — is never matched, so\n * the rewrite below cannot damage it.\n */\nconst TOP_LEVEL_ROOT_GET = /^router\\s*\\.\\s*get\\(\\s*([\"'`])\\/\\1/gm;\n\n/**\n * Whether `/welcome` is already spoken for, so relocating onto it would trade\n * one duplicate-route 500 for another.\n */\nconst TOP_LEVEL_WELCOME_GET = /^router\\s*\\.\\s*get\\(\\s*([\"'`])\\/welcome\\1/m;\n\nexport type HomeRouteCollision =\n /** Nothing under `src/app` claims a top-level `/` — write the page as normal. */\n | { outcome: \"absent\" }\n /** The one file that claimed `/` was moved to `/welcome`; the page is safe to write. */\n | { outcome: \"relocated\"; relativePath: string }\n /** Something claims `/` that we will not rewrite. The page is NOT written. */\n | { outcome: \"conflict\"; reason: string }\n /** We tried to relocate and could not. The page is NOT written. */\n | { outcome: \"failed\"; reason: string };\n\n/** One routes source file, as read from disk (or handed to the pure resolver in a test). */\nexport type RoutesFileSource = {\n relativePath: string;\n source: string;\n};\n\n/** What {@link resolveHomeRouteCollision} decided, plus the sources it rewrote. */\nexport type HomeRouteResolution = {\n collision: HomeRouteCollision;\n rewrites: RoutesFileSource[];\n};\n\n/**\n * Decide what to do about `src/app/**\\/routes.{ts,tsx}` claiming `/`, and\n * produce the rewritten source when exactly one file does.\n *\n * Pure by design: no filesystem access here, so this is the part a test can\n * exercise directly without touching disk. {@link relocateConflictingHomeRoute}\n * is the thin I/O wrapper — it reads the files, calls this, and writes back\n * whatever this returns in `rewrites`.\n *\n * @param files Every `routes.{ts,tsx}` file under `src/app`, with its current text.\n * @returns The collision outcome, and the (possibly empty) list of files to write back.\n */\nexport function resolveHomeRouteCollision(files: RoutesFileSource[]): HomeRouteResolution {\n const claimants = files\n .map((file) => ({ file, matches: file.source.match(TOP_LEVEL_ROOT_GET) ?? [] }))\n .filter(({ matches }) => matches.length > 0);\n\n if (claimants.length === 0) {\n return { collision: { outcome: \"absent\" }, rewrites: [] };\n }\n\n if (claimants.length > 1) {\n const names = claimants.map(({ file }) => file.relativePath).join(\", \");\n\n return {\n collision: {\n outcome: \"conflict\",\n reason: `multiple files declare a top-level GET \"/\" (${names})`,\n },\n rewrites: [],\n };\n }\n\n const claimant = claimants[0];\n\n // The two guards above establish exactly one claimant, but the compiler does\n // not narrow a destructured element from a length check. Reporting \"absent\"\n // is the same answer the empty case gives, and the honest one: with no\n // claimant there is no conflicting home route to relocate.\n if (claimant === undefined) {\n return { collision: { outcome: \"absent\" }, rewrites: [] };\n }\n\n const { file, matches } = claimant;\n\n if (matches.length > 1) {\n return {\n collision: {\n outcome: \"conflict\",\n reason: `${file.relativePath} declares ${matches.length} top-level GET \"/\" routes`,\n },\n rewrites: [],\n };\n }\n\n if (TOP_LEVEL_WELCOME_GET.test(file.source)) {\n return {\n collision: {\n outcome: \"conflict\",\n reason: `${file.relativePath} already declares GET \"/welcome\", so the usual relocation target is taken`,\n },\n rewrites: [],\n };\n }\n\n const next = file.source.replace(TOP_LEVEL_ROOT_GET, (match: string, quote: string) =>\n match.replace(`${quote}/${quote}`, `${quote}/welcome${quote}`),\n );\n\n if (next === file.source) {\n return {\n collision: {\n outcome: \"conflict\",\n reason: `${file.relativePath}'s GET \"/\" route could not be rewritten`,\n },\n rewrites: [],\n };\n }\n\n return {\n collision: { outcome: \"relocated\", relativePath: file.relativePath },\n rewrites: [{ relativePath: file.relativePath, source: next }],\n };\n}\n\n/**\n * Make room for a page that declares `route.path = \"/\"`.\n *\n * The project template registers `router.get(\"/\", homePageController)` and the\n * page stub declares `route.path = \"/\"`. Fastify rejects the second registration\n * (`Method 'GET' already declared for route '/'`) and the homepage 500s at\n * request time — so `warlock add web` cannot just write the page and hope.\n *\n * This used to check exactly one hardcoded path, `src/app/shared/routes.ts`.\n * That guard never fired on a real scaffold: the template registers the home\n * route at `src/app/home/routes.ts`, `fileExistsAsync` on the wrong path always\n * failed, and every call silently returned `absent` — the collision-avoidance\n * logic below existed and was correct, but was pointed at a file that does not\n * exist, so it never ran. It now scans every `src/app/**\\/routes.{ts,tsx}`\n * instead of trusting one filename, so renaming the template's routes file\n * again cannot reintroduce the same silent no-op.\n *\n * Of the three ways out, this RELOCATES the JSON route to `/welcome` rather than\n * deleting it or refusing to scaffold:\n *\n * - Deleting the controller is what the scaffolder's own `react` feature does,\n * but it may do that: it owns the file it is deleting, seconds after writing\n * it. `warlock add web` runs against a project a human has been living in, and\n * silently unlinking their code is not a thing an `add` command gets to do.\n * - Writing the page anyway and printing a warning ships a project whose\n * homepage 500s. A warning above a broken app is still a broken app.\n * - Relocating keeps BOTH surfaces working: the React homepage takes `/`, the\n * JSON welcome answers at `/welcome`, and no line of user code disappears.\n *\n * Only the exact top-level shape is rewritten, and only the path literal inside\n * it. Anything else that claims `/` is reported and left completely alone — we\n * do not guess at code we cannot recognise.\n */\nexport async function relocateConflictingHomeRoute(): Promise<HomeRouteCollision> {\n const relativePaths = await glob(\"**/routes.{ts,tsx}\", {\n cwd: srcPath(\"app\"),\n absolute: false,\n });\n\n if (relativePaths.length === 0) {\n return { outcome: \"absent\" };\n }\n\n let files: RoutesFileSource[];\n\n try {\n files = await Promise.all(\n relativePaths.map(async (relativePath) => ({\n relativePath: `app/${relativePath}`,\n source: await getFileAsync(srcPath(\"app\", relativePath)),\n })),\n );\n } catch (error) {\n return {\n outcome: \"failed\",\n reason: `could not be read (${(error as Error).message})`,\n };\n }\n\n const { collision, rewrites } = resolveHomeRouteCollision(files);\n\n if (rewrites.length === 0) {\n return collision;\n }\n\n try {\n await Promise.all(\n rewrites.map((file) => putFileAsync(srcPath(file.relativePath), file.source)),\n );\n } catch (error) {\n return {\n outcome: \"failed\",\n reason: `could not be written (${(error as Error).message})`,\n };\n }\n\n return collision;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAgBA,MAAM,qBAAqB;;;;;AAM3B,MAAM,wBAAwB;;;;;;;;;;;;;AAoC9B,SAAgB,0BAA0B,OAAgD;CACxF,MAAM,YAAY,MACf,KAAK,UAAU;EAAE;EAAM,SAAS,KAAK,OAAO,MAAM,kBAAkB,KAAK,CAAC;CAAE,EAAE,CAAC,CAC/E,QAAQ,EAAE,cAAc,QAAQ,SAAS,CAAC;CAE7C,IAAI,UAAU,WAAW,GACvB,OAAO;EAAE,WAAW,EAAE,SAAS,SAAS;EAAG,UAAU,CAAC;CAAE;CAG1D,IAAI,UAAU,SAAS,GAGrB,OAAO;EACL,WAAW;GACT,SAAS;GACT,QAAQ,+CALE,UAAU,KAAK,EAAE,WAAW,KAAK,YAAY,CAAC,CAAC,KAAK,IAKH,EAAE;EAC/D;EACA,UAAU,CAAC;CACb;CAGF,MAAM,WAAW,UAAU;CAM3B,IAAI,aAAa,QACf,OAAO;EAAE,WAAW,EAAE,SAAS,SAAS;EAAG,UAAU,CAAC;CAAE;CAG1D,MAAM,EAAE,MAAM,YAAY;CAE1B,IAAI,QAAQ,SAAS,GACnB,OAAO;EACL,WAAW;GACT,SAAS;GACT,QAAQ,GAAG,KAAK,aAAa,YAAY,QAAQ,OAAO;EAC1D;EACA,UAAU,CAAC;CACb;CAGF,IAAI,sBAAsB,KAAK,KAAK,MAAM,GACxC,OAAO;EACL,WAAW;GACT,SAAS;GACT,QAAQ,GAAG,KAAK,aAAa;EAC/B;EACA,UAAU,CAAC;CACb;CAGF,MAAM,OAAO,KAAK,OAAO,QAAQ,qBAAqB,OAAe,UACnE,MAAM,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,UAAU,OAAO,CAC/D;CAEA,IAAI,SAAS,KAAK,QAChB,OAAO;EACL,WAAW;GACT,SAAS;GACT,QAAQ,GAAG,KAAK,aAAa;EAC/B;EACA,UAAU,CAAC;CACb;CAGF,OAAO;EACL,WAAW;GAAE,SAAS;GAAa,cAAc,KAAK;EAAa;EACnE,UAAU,CAAC;GAAE,cAAc,KAAK;GAAc,QAAQ;EAAK,CAAC;CAC9D;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,eAAsB,+BAA4D;CAChF,MAAM,gBAAgB,MAAM,KAAK,sBAAsB;EACrD,KAAK,QAAQ,KAAK;EAClB,UAAU;CACZ,CAAC;CAED,IAAI,cAAc,WAAW,GAC3B,OAAO,EAAE,SAAS,SAAS;CAG7B,IAAI;CAEJ,IAAI;EACF,QAAQ,MAAM,QAAQ,IACpB,cAAc,IAAI,OAAO,kBAAkB;GACzC,cAAc,OAAO;GACrB,QAAQ,MAAM,aAAa,QAAQ,OAAO,YAAY,CAAC;EACzD,EAAE,CACJ;CACF,SAAS,OAAO;EACd,OAAO;GACL,SAAS;GACT,QAAQ,sBAAuB,MAAgB,QAAQ;EACzD;CACF;CAEA,MAAM,EAAE,WAAW,aAAa,0BAA0B,KAAK;CAE/D,IAAI,SAAS,WAAW,GACtB,OAAO;CAGT,IAAI;EACF,MAAM,QAAQ,IACZ,SAAS,KAAK,SAAS,aAAa,QAAQ,KAAK,YAAY,GAAG,KAAK,MAAM,CAAC,CAC9E;CACF,SAAS,OAAO;EACd,OAAO;GACL,SAAS;GACT,QAAQ,yBAA0B,MAAgB,QAAQ;EAC5D;CACF;CAEA,OAAO;AACT"}
|
|
@@ -648,9 +648,9 @@ const webContactControllerStub = `import { type Request, type RequestHandler } f
|
|
|
648
648
|
import { type Infer, v } from "@warlock.js/seal";
|
|
649
649
|
|
|
650
650
|
export const contactSchema = v.object({
|
|
651
|
-
name: v.string().min(2)
|
|
652
|
-
email: v.email()
|
|
653
|
-
message: v.string().min(10)
|
|
651
|
+
name: v.string().min(2),
|
|
652
|
+
email: v.email(),
|
|
653
|
+
message: v.string().min(10),
|
|
654
654
|
});
|
|
655
655
|
|
|
656
656
|
export type ContactSchema = Infer.Output<typeof contactSchema>;
|
|
@@ -743,9 +743,9 @@ export const route = { path: "/", name: "index" } as const;
|
|
|
743
743
|
export const metadata = { title: "Home" };
|
|
744
744
|
|
|
745
745
|
const contactSchema = v.object({
|
|
746
|
-
name: v.string().min(2)
|
|
747
|
-
email: v.email()
|
|
748
|
-
message: v.string().min(10)
|
|
746
|
+
name: v.string().min(2),
|
|
747
|
+
email: v.email(),
|
|
748
|
+
message: v.string().min(10),
|
|
749
749
|
});
|
|
750
750
|
|
|
751
751
|
function TextInput({ label, ...controlProps }: FormControlProps & { label: string }) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stubs.mjs","names":[],"sources":["../../../../../../../core/src/generations/stubs.ts"],"sourcesContent":["export const accessConfigStub = `import { type AccessConfigurations } from \"@warlock.js/access\";\r\nimport { DatabaseAccessResolver } from \"app/access/services/access-resolver\";\r\n\r\n/**\r\n * Authorization configuration — read by @warlock.js/access on boot.\r\n *\r\n * The resolver is the one required piece: it tells the engine how to read a\r\n * user's roles + permissions. The ejected DatabaseAccessResolver reads roles\r\n * from the user_roles table and maps them through the roles catalog table (so\r\n * roles + their permissions are managed at runtime, in the DB).\r\n *\r\n * For a fixed, code-defined catalog with no tables, swap in DefaultAccessResolver:\r\n * import { DefaultAccessResolver } from \"@warlock.js/access\";\r\n * resolver: new DefaultAccessResolver({ admin: [\"*\"], editor: [\"orders.*\"] }),\r\n *\r\n * Multi-tenant? Add a \\`resolveTenant()\\` to the resolver to read the active\r\n * tenant from the request; checks then scope to it automatically.\r\n */\r\nconst access: AccessConfigurations = {\r\n resolver: new DatabaseAccessResolver(),\r\n\r\n // Cache resolved permission sets (default \"10m\").\r\n // cache: { ttl: \"10m\" },\r\n};\r\n\r\nexport default access;\r\n`;\r\n\r\nexport const aiConfigStub = `import type { AIConfig } from \"@warlock.js/ai\";\r\n\r\n// >>> warlock:ai-packages (auto-managed) >>>\r\n// Satellite packages augment the \"ai\" object on import — e.g. ai.workspace,\r\n// ai.tools / ai.mcp, and panoptic's ai.config({ panoptic }) wiring. The command\r\n// \"warlock add ai-workspace | ai-tools | ai-panoptic\" adds the matching\r\n// side-effect import below; keep them so the augmentation + runtime registration\r\n// load before the ai connector applies this config.\r\n// <<< warlock:ai-packages <<<\r\n\r\n/**\r\n * AI configuration — applied on boot by the ai connector, which calls\r\n * ai.config(...) with the object below. Cross-cutting defaults live here\r\n * (shared cache / snapshot stores, observability); per-call options always win.\r\n *\r\n * Wire a default model from a provider you installed, e.g.:\r\n * import { OpenAISDK } from \"@warlock.js/ai-openai\";\r\n * const openai = OpenAISDK({ apiKey: env(\"OPENAI_API_KEY\") });\r\n * // then pass openai.model({ name: \"gpt-4o-mini\" }) into your agents.\r\n */\r\nconst ai: Partial<AIConfig> = {\r\n // Default cache driver for cache-backed AI features (semantic cache, rag / memory vector stores).\r\n // defaultStore: cache.driver(\"redis\", { client }),\r\n\r\n // Observability — requires \"warlock add ai-panoptic\". Exporters + the local dashboard.\r\n // panoptic: { exporters: [], dashboard: false, observeAll: false },\r\n};\r\n\r\nexport default ai;\r\n`;\r\n\r\nexport const accessRoleModelStub = `import { Model, RegisterModel } from \"@warlock.js/cascade\";\r\nimport { type Infer, v } from \"@warlock.js/seal\";\r\n\r\n/**\r\n * Validation schema for the roles catalog — mirrors the migration columns\r\n * (snake_case). Each row is a role name plus the permission strings it grants;\r\n * wildcards work (\"orders.*\", \"*\"). The DatabaseAccessResolver maps a user's\r\n * assigned role names through this table to their effective permissions.\r\n */\r\nexport const roleSchema = v.object({\r\n name: v.string(),\r\n permissions: v.array(v.string()).default([]),\r\n});\r\n\r\nexport type RoleSchema = Infer<typeof roleSchema>;\r\n\r\n/**\r\n * The roles catalog — role name → the permissions it grants. Managed at runtime\r\n * (admins add roles + edit their permissions), unlike a fixed code map. Read by\r\n * DatabaseAccessResolver.resolvePermissions to expand a user's roles to permissions.\r\n */\r\n@RegisterModel()\r\nexport class Role extends Model<RoleSchema> {\r\n public static table = \"roles\";\r\n\r\n public static schema = roleSchema;\r\n\r\n /** The permission strings this role grants. */\r\n public get permissions(): string[] {\r\n return this.get<string[]>(\"permissions\", []);\r\n }\r\n}\r\n`;\r\n\r\nexport const accessRoleModelIndexStub = `export * from \"./role.model\";\r\n`;\r\n\r\nexport const accessRoleMigrationStub = `import { arrayText, Migration, text } from \"@warlock.js/cascade\";\r\nimport { Role } from \"../role.model\";\r\n\r\n/**\r\n * Roles catalog table. \\`name\\` is unique (one row per role); \\`permissions\\` is a\r\n * text array of the permission strings the role grants.\r\n */\r\nexport default Migration.create(Role, {\r\n name: text().notNullable().unique(),\r\n permissions: arrayText().nullable(),\r\n});\r\n`;\r\n\r\nexport const accessUserRoleModelStub = `import { access } from \"@warlock.js/access\";\r\nimport type { Auth } from \"@warlock.js/auth\";\r\nimport { Model, RegisterModel } from \"@warlock.js/cascade\";\r\nimport { type Infer, v } from \"@warlock.js/seal\";\r\n\r\n/**\r\n * Validation schema for a role assignment — mirrors the migration columns\r\n * (snake_case). \\`tenant\\` is nullable: a null tenant is a GLOBAL assignment.\r\n */\r\nexport const userRoleSchema = v.object({\r\n user_id: v.string(),\r\n user_type: v.string(),\r\n role: v.string(),\r\n tenant: v.string().optional(),\r\n});\r\n\r\nexport type UserRoleSchema = Infer<typeof userRoleSchema>;\r\n\r\n/**\r\n * The role-assignment table — which roles a user holds, optionally per tenant.\r\n * Read by DatabaseAccessResolver.resolveRoles; mutated via the statics below.\r\n * \\`assign\\` / \\`revoke\\` flush the cached permission set automatically, so callers\r\n * never need to call \\`access.flush(user, tenant)\\` themselves.\r\n */\r\n@RegisterModel()\r\nexport class UserRole extends Model<UserRoleSchema> {\r\n public static table = \"user_roles\";\r\n\r\n public static schema = userRoleSchema;\r\n\r\n /**\r\n * Role names assigned to the user in the given tenant.\r\n *\r\n * An unresolved tenant (\\`undefined\\`) scopes to GLOBAL roles only — the rows\r\n * stored with no tenant (\\`null\\`) — never the union across every tenant. The\r\n * union would be a privilege-escalation: a user who is \\`owner\\` in one tenant\r\n * must not be treated as \\`owner\\` everywhere just because a check didn't carry\r\n * a tenant. This mirrors how \\`assign(user, role)\\` stores a global row.\r\n */\r\n public static async rolesFor(user: Auth, tenant?: string): Promise<string[]> {\r\n const rows = await this.query()\r\n .where({\r\n user_id: user.id,\r\n user_type: user.userType,\r\n tenant: tenant ?? null,\r\n })\r\n .get();\r\n\r\n // De-dupe so a duplicate row (a concurrent assign that slipped past the\r\n // existence check) can't distort the resolved set.\r\n return [...new Set(rows.map((row) => row.get(\"role\") as string))];\r\n }\r\n\r\n /**\r\n * Assign a role to the user. No-op if the assignment already exists.\r\n * Flushes the user's cached permission set automatically.\r\n */\r\n public static async assign(user: Auth, role: string, tenant?: string): Promise<void> {\r\n const existing = await this.first({\r\n user_id: user.id,\r\n user_type: user.userType,\r\n role,\r\n tenant: tenant ?? null,\r\n });\r\n\r\n if (existing) return;\r\n\r\n await this.create({\r\n user_id: user.id,\r\n user_type: user.userType,\r\n role,\r\n tenant,\r\n });\r\n\r\n await access.flush(user, tenant);\r\n }\r\n\r\n /**\r\n * Remove a role assignment from the user.\r\n * Flushes the user's cached permission set automatically.\r\n */\r\n public static async revoke(user: Auth, role: string, tenant?: string): Promise<void> {\r\n await this.delete({\r\n user_id: user.id,\r\n user_type: user.userType,\r\n role,\r\n tenant: tenant ?? null,\r\n });\r\n\r\n await access.flush(user, tenant);\r\n }\r\n}\r\n`;\r\n\r\nexport const accessUserRoleModelIndexStub = `export * from \"./user-role.model\";\r\n`;\r\n\r\nexport const accessUserRoleMigrationStub = `import { Migration, text, uuid } from \"@warlock.js/cascade\";\r\nimport { UserRole } from \"../user-role.model\";\r\n\r\n/**\r\n * Role-assignment table. \\`user_id\\` is a UUID — override this migration if your\r\n * user ids are integers. The composite index powers the per-user (per-tenant)\r\n * lookup the resolver runs on every check.\r\n */\r\nexport default Migration.create(\r\n UserRole,\r\n {\r\n user_id: uuid().notNullable().index(),\r\n user_type: text().notNullable(),\r\n role: text().notNullable().index(),\r\n tenant: text().nullable().index(),\r\n },\r\n {\r\n index: [{ columns: [\"user_id\", \"user_type\", \"tenant\"] }],\r\n },\r\n);\r\n`;\r\n\r\nexport const accessResolverStub = `import type { AccessResolver } from \"@warlock.js/access\";\r\nimport type { Auth } from \"@warlock.js/auth\";\r\nimport { Role } from \"app/access/models/role\";\r\nimport { UserRole } from \"app/access/models/user-role\";\r\n\r\n/**\r\n * The app's access adapter — connects @warlock.js/access to the ejected role\r\n * tables. Roles come from the user_roles assignment table; permissions are\r\n * expanded by mapping those role names through the roles catalog table. Both\r\n * are managed at runtime (in the DB), so admins can add roles + edit their\r\n * permissions without a deploy.\r\n *\r\n * The engine owns the hard parts (wildcard matching, caching, fail-closed); this\r\n * resolver only fetches — keep it dumb, never cache inside it.\r\n */\r\nexport class DatabaseAccessResolver implements AccessResolver {\r\n /** The role names this user holds (powers \\`hasRole\\` / \\`hasAnyRole\\`). */\r\n public async resolveRoles(user: Auth, tenant?: string): Promise<string[]> {\r\n return UserRole.rolesFor(user, tenant);\r\n }\r\n\r\n /** The effective permission strings this user has (powers \\`can\\` / \\`authorize\\`). */\r\n public async resolvePermissions(user: Auth, tenant?: string): Promise<string[]> {\r\n const names = await this.resolveRoles(user, tenant);\r\n\r\n if (names.length === 0) return [];\r\n\r\n const roles = await Role.query().whereIn(\"name\", names).get();\r\n\r\n // Flatten + de-dupe so two roles granting the same permission yield one entry.\r\n return [...new Set(roles.flatMap((role) => role.permissions))];\r\n }\r\n\r\n /**\r\n * Optional. Resolve the ambient tenant when a check doesn't pass one\r\n * explicitly — derive it from the authenticated user (safer than reading\r\n * client request input, which a caller could spoof). Uncomment + adapt for a\r\n * multi-tenant app (single-tenant apps leave this off and return undefined).\r\n */\r\n // public resolveTenant(user: Auth): string | undefined {\r\n // return user.get(\"organization_id\");\r\n // }\r\n}\r\n`;\r\n\r\nexport const socketConfigStub = `import type { SocketOptions } from \"@warlock.js/core\";\r\n\r\n/**\r\n * Socket.IO configuration — read by the framework's socket connector\r\n * on boot. When the HTTP server is running the socket server attaches\r\n * to it; otherwise it listens on its own configured port.\r\n *\r\n * Remove this file to disable the socket server entirely.\r\n */\r\nexport default {\r\n options: {\r\n cors: {\r\n origin: \"*\",\r\n },\r\n },\r\n} as SocketOptions;\r\n`;\r\n\r\nexport const communicatorsConfigStub = `import { env } from \"@warlock.js/core\";\r\nimport type { BrokerConfigurations, RabbitMQClientOptions } from \"@warlock.js/herald\";\r\n\r\nconst heraldConfigurations: BrokerConfigurations<RabbitMQClientOptions> = {\r\n driver: \"rabbitmq\",\r\n name: \"default\",\r\n isDefault: true,\r\n\r\n // ============================================================================\r\n // Connection Settings\r\n // ============================================================================\r\n\r\n host: env(\"RABBITMQ_HOST\", \"localhost\"),\r\n port: env(\"RABBITMQ_PORT\", 5672),\r\n username: env(\"RABBITMQ_USERNAME\", \"guest\"),\r\n password: env(\"RABBITMQ_PASSWORD\", \"guest\"),\r\n vhost: env(\"RABBITMQ_VHOST\", \"/\"),\r\n\r\n // Or use connection URI (takes precedence over host/port)\r\n // uri: env(\"RABBITMQ_URL\"),\r\n\r\n // ============================================================================\r\n // Connection Options\r\n // ============================================================================\r\n\r\n /** Heartbeat interval in seconds */\r\n heartbeat: 60,\r\n\r\n /** Connection timeout in milliseconds */\r\n connectionTimeout: 10000,\r\n\r\n /** Enable automatic reconnection on disconnect */\r\n reconnect: true,\r\n\r\n /** Delay between reconnection attempts in milliseconds */\r\n reconnectDelay: 5_000,\r\n\r\n // ============================================================================\r\n // Consumer Options\r\n // ============================================================================\r\n\r\n /** Default prefetch count (number of unacknowledged messages per consumer) */\r\n prefetch: 10,\r\n\r\n // ============================================================================\r\n // Client Options (Native amqplib options)\r\n // ============================================================================\r\n // These options are passed directly to amqplib.connect()\r\n // for low-level configuration like frame size, TLS, socket options, etc.\r\n // ============================================================================\r\n clientOptions: {\r\n // Frame max size in bytes (0 = no limit)\r\n // frameMax: 0,\r\n\r\n // Channel max (0 = unlimited)\r\n // channelMax: 0,\r\n\r\n // Socket options\r\n socket: {\r\n // Enable TCP keep-alive\r\n keepAlive: true,\r\n\r\n // Disable Nagle's algorithm for lower latency\r\n noDelay: true,\r\n\r\n // Socket timeout (in addition to heartbeat)\r\n // timeout: 30000,\r\n },\r\n\r\n // TLS/SSL options (uncomment for secure connections)\r\n // socket: {\r\n // ca: fs.readFileSync('/path/to/ca.pem'),\r\n // cert: fs.readFileSync('/path/to/cert.pem'),\r\n // key: fs.readFileSync('/path/to/key.pem'),\r\n // rejectUnauthorized: true,\r\n // },\r\n },\r\n};\r\n\r\nexport default heraldConfigurations;\r\n`;\r\n\r\nexport const notificationsConfigStub = `import { type NotificationConfig, inApp, mailChannel } from \"@warlock.js/notifications\";\r\nimport { Notification } from \"app/notifications/notification.model\";\r\n\r\n/**\r\n * Notifications configuration. Auto-loaded from src/config on boot — the\r\n * framework's notifications connector reads this default export and hands it to\r\n * setNotificationConfig, so this file stays declarative (no side-effect call).\r\n *\r\n * Each channel is payload-typed, so notify.mail(...) / notify.database(...)\r\n * and defineNotification are type-checked against the registry.\r\n *\r\n * Channels enabled here:\r\n * - mail wraps @warlock.js/core sendMail; route is notifiable.email.\r\n * The \"from\" address defaults to config/mail.ts; override per\r\n * channel with mailChannel({ from: \"no-reply@yourapp.com\" }).\r\n * - database in-app store backed by the Notification model. The \"inApp\"\r\n * facade exposes the recipient-scoped read API: listUnread,\r\n * countUnread, markAsRead, dismiss, ...\r\n *\r\n * Async delivery (.queue()) is OPTIONAL: run \"npx warlock add herald\",\r\n * import { heraldQueue } from \"@warlock.js/notifications\", and uncomment the\r\n * queue line below.\r\n */\r\nconst config: NotificationConfig = {\r\n channels: {\r\n mail: mailChannel(),\r\n database: inApp.configure({ model: Notification }),\r\n },\r\n\r\n // Async queue — requires @warlock.js/herald (npx warlock add herald):\r\n // queue: heraldQueue(),\r\n};\r\n\r\nexport default config;\r\n`;\r\n\r\nexport const notificationModelStub = `import { RegisterModel } from \"@warlock.js/cascade\";\r\nimport { DatabaseNotification, type NotificationColumnMap } from \"@warlock.js/notifications\";\r\nimport { v } from \"@warlock.js/seal\";\r\n\r\n/**\r\n * Validation schema for the notifications table — mirrors the migration\r\n * columns (snake_case). Cascade validates + casts every write against it:\r\n * nullable columns use .nullish() (may be absent or null), and payload is\r\n * free-form JSON. Keep this in sync with the migration + columnMap when you\r\n * add or rename columns.\r\n */\r\nconst notificationSchema = v.object({\r\n user_id: v.string(),\r\n type: v.string(),\r\n title: v.string(),\r\n body: v.string().nullish(),\r\n payload: v.record(v.any()).nullish(),\r\n read_at: v.date().nullish(),\r\n idempotency_key: v.string().nullish(),\r\n});\r\n\r\n/**\r\n * In-app notification model.\r\n *\r\n * Extends the package's DatabaseNotification base, which provides the stable\r\n * accessors (recipientId, tenantId, isRead, readAt, markRead) — all derived\r\n * from the columnMap below. The read/write API lives on the inApp facade\r\n * (configured in config/notifications.ts); you rarely touch this class directly.\r\n */\r\n@RegisterModel()\r\nexport class Notification extends DatabaseNotification {\r\n public static table = \"notifications\";\r\n public static schema = notificationSchema;\r\n\r\n /**\r\n * Maps the in-app store's roles to your columns. This default is\r\n * single-tenant + read_at-only. Add tenant: \"organization_id\" for\r\n * multi-tenant; use isRead: \"is_read\" (instead of, or alongside, readAt) to\r\n * track a boolean read flag. The migration + accessors all follow this map.\r\n */\r\n public static columnMap: NotificationColumnMap = { readAt: \"read_at\" };\r\n}\r\n`;\r\n\r\nexport const notificationMigrationStub = `import { Migration } from \"@warlock.js/cascade\";\r\nimport { notificationColumns } from \"@warlock.js/notifications\";\r\nimport { Notification } from \"../notification.model\";\r\n\r\n/**\r\n * Notifications table.\r\n *\r\n * Columns come from notificationColumns(Notification) — the recipient / tenant\r\n * / read-state names follow the model's columnMap; type / title / body /\r\n * payload / idempotency_key are fixed. Spread it to add your own columns\r\n * (remember to mirror them in the model schema):\r\n *\r\n * import { uuid } from \"@warlock.js/cascade\";\r\n *\r\n * export default Migration.create(Notification, {\r\n * ...notificationColumns(Notification),\r\n * // category_id: uuid().index().nullable(),\r\n * });\r\n */\r\nexport default Migration.create(Notification, notificationColumns(Notification));\r\n`;\r\n\r\nexport const notificationControllersStub = `import { type RequestHandler } from \"@warlock.js/core\";\r\nimport { inApp, type Id } from \"@warlock.js/notifications\";\r\n\r\n/**\r\n * The authenticated user's notification HTTP surface — thin wrappers over the\r\n * recipient-scoped \\`inApp\\` facade (a foreign id can never touch another user's\r\n * rows). Notifications are produced by domain events, never over HTTP, so there\r\n * is no create. Trim or split these as your app grows.\r\n */\r\n\r\n/**\r\n * Read \\`id\\` off \\`request.user\\` without assuming this app's \\`RequestUser\\`\r\n * augmentation declares it — \\`RequestUser\\` is empty by default (see\r\n * \\`@warlock.js/core\\`'s \\`RequestUser\\` docs), so a narrow runtime read survives\r\n * any augmentation shape instead of assuming \\`.id\\` exists at the type level.\r\n * \\`inApp\\` only ever needs the id (it reduces a \\`Notifiable\\` to one via\r\n * \\`recipient.id\\` internally), so reading it here — rather than forwarding\r\n * \\`request.user\\` itself — also skips a needless \\`Notifiable\\` cast.\r\n */\r\nfunction recipientId(user: unknown): Id {\r\n if (user && typeof user === \"object\" && \"id\" in user) {\r\n const id = (user as { id?: unknown }).id;\r\n\r\n if (typeof id === \"string\" || typeof id === \"number\") return id;\r\n }\r\n\r\n throw new Error(\"Authenticated request is missing a usable user id\");\r\n}\r\n\r\n/** GET /notifications — list, most recent first (page / limit / type / unread via query). */\r\nexport const listNotificationsController: RequestHandler = async ({ request, response }) => {\r\n const { data, pagination } = await inApp.list(recipientId(request.user), request.all());\r\n\r\n return response.success({ notifications: data, pagination });\r\n};\r\n\r\nlistNotificationsController.description = \"List notifications\";\r\n\r\n/** GET /notifications/unread-count — drives the bell badge. */\r\nexport const unreadNotificationsCountController: RequestHandler = async ({\r\n request,\r\n response,\r\n}) => {\r\n const count = await inApp.countUnread(recipientId(request.user));\r\n\r\n return response.success({ count });\r\n};\r\n\r\nunreadNotificationsCountController.description = \"Unread notifications count\";\r\n\r\n/** PATCH /notifications/:id/read — mark one read, return the updated row. */\r\nexport const markNotificationReadController: RequestHandler = async ({ request, response }) => {\r\n const id = request.input(\"id\");\r\n const userId = recipientId(request.user);\r\n\r\n await inApp.markAsRead(userId, id);\r\n const notification = await inApp.find(userId, id);\r\n\r\n return response.success({ notification });\r\n};\r\n\r\nmarkNotificationReadController.description = \"Mark notification read\";\r\n\r\n/** PATCH /notifications/read-all — mark every unread one read. */\r\nexport const markAllNotificationsReadController: RequestHandler = async ({\r\n request,\r\n response,\r\n}) => {\r\n const count = await inApp.markAsRead(recipientId(request.user));\r\n\r\n return response.success({ count });\r\n};\r\n\r\nmarkAllNotificationsReadController.description = \"Mark all notifications read\";\r\n\r\n/** DELETE /notifications — dismiss all for the user. */\r\nexport const clearNotificationsController: RequestHandler = async ({ request, response }) => {\r\n await inApp.dismiss(recipientId(request.user));\r\n\r\n return response.noContent();\r\n};\r\n\r\nclearNotificationsController.description = \"Clear notifications\";\r\n\r\n/** DELETE /notifications/:id — dismiss one. */\r\nexport const deleteNotificationController: RequestHandler = async ({ request, response }) => {\r\n await inApp.dismiss(recipientId(request.user), request.input(\"id\"));\r\n\r\n return response.noContent();\r\n};\r\n\r\ndeleteNotificationController.description = \"Delete notification\";\r\n`;\r\n\r\nexport const notificationRoutesStub = `import { authMiddleware } from \"@warlock.js/auth\";\r\nimport { router } from \"@warlock.js/core\";\r\nimport {\r\n clearNotificationsController,\r\n deleteNotificationController,\r\n listNotificationsController,\r\n markAllNotificationsReadController,\r\n markNotificationReadController,\r\n unreadNotificationsCountController,\r\n} from \"./controllers/notifications.controller\";\r\n\r\n/**\r\n * Notification routes — the authenticated user's read + dismiss surface.\r\n *\r\n * Notifications are produced by domain events (never created over HTTP), so\r\n * there is no POST. Every route is gated by \\`authMiddleware\\` and recipient-\r\n * scoped by \\`inApp\\` (a foreign id touches zero rows). Delete any endpoint you\r\n * don't need; if your app reads notifications over sockets/GraphQL instead,\r\n * delete this file + the controllers entirely.\r\n */\r\nrouter.group({ prefix: \"/notifications\", middleware: [authMiddleware([])] }, () => {\r\n router.get(\"/\", listNotificationsController);\r\n router.get(\"/unread-count\", unreadNotificationsCountController);\r\n router.patch(\"/read-all\", markAllNotificationsReadController);\r\n router.patch(\"/:id/read\", markNotificationReadController);\r\n router.delete(\"/\", clearNotificationsController);\r\n router.delete(\"/:id\", deleteNotificationController);\r\n});\r\n`;\r\n\r\n/**\r\n * `src/web/root.tsx` — the application root for the SSR page layer.\r\n *\r\n * Deliberately minimal. The framework ships a default root, so this exists to\r\n * give you a place to start rather than because anything requires it. The\r\n * reference app (`v5/app/src/web/root.tsx`) is where to look for the fuller\r\n * shape: middleware, an app-level loader, locales, an ErrorBoundary.\r\n */\r\nexport const webRootStub = `import type { AppProps } from \"@warlock.js/web\";\r\nimport { Head, Scripts } from \"@warlock.js/web\";\r\n\r\n/**\r\n * The application root.\r\n *\r\n * NOT async, and it receives no request/response: it renders on the server and\r\n * again in the browser during hydration, where neither exists.\r\n */\r\nexport default function App({ children }: AppProps) {\r\n return (\r\n <html lang=\"en\">\r\n <head>\r\n {/*\r\n Placement only. The framework injects the page's \\`metadata\\`, the\r\n stylesheet and preload tags for this route, and the canonical links\r\n into <head> by default — <Head /> just says WHERE they land.\r\n\r\n Do not add a <title> here: the page's \\`metadata\\` owns it, and a root\r\n that emits one too produces two.\r\n */}\r\n <Head />\r\n <link rel=\"icon\" href=\"data:,\" />\r\n </head>\r\n <body>\r\n {/*\r\n REQUIRED — this is the hydration mount point, not a styling wrapper.\r\n\r\n The browser runtime looks up \\`#root\\` and hydrates that element only.\r\n Remove this div, or rename the id, and the page still renders from the\r\n server but never becomes interactive: the runtime throws in the console\r\n and nothing on screen changes.\r\n\r\n Wrap it in your own markup freely, and put anything that must live\r\n outside the hydrated tree (a static footer, a portal target) outside\r\n it — just keep an element with \\`id=\"root\"\\` around {children}.\r\n */}\r\n <div id=\"root\">{children}</div>\r\n {/*\r\n The hydration payload and module tags. Written explicitly because\r\n placement occasionally matters — a CSP nonce, or ordering against\r\n your own scripts.\r\n */}\r\n <Scripts />\r\n </body>\r\n </html>\r\n );\r\n}\r\n`;\r\n\r\n/**\r\n * `src/app/contact/controllers/contact.controller.ts` — a real API endpoint\r\n * for the Web starter's contact form. It intentionally has no persistence\r\n * dependency: replace the acknowledgement with a mail/job/database action.\r\n */\r\nexport const webContactControllerStub = `import { type Request, type RequestHandler } from \"@warlock.js/core\";\r\nimport { type Infer, v } from \"@warlock.js/seal\";\r\n\r\nexport const contactSchema = v.object({\r\n name: v.string().min(2).required(),\r\n email: v.email().required(),\r\n message: v.string().min(10).required(),\r\n});\r\n\r\nexport type ContactSchema = Infer.Output<typeof contactSchema>;\r\n\r\n/** POST /api/contact — validates the starter contact form. */\r\nexport const contactController: RequestHandler<Request<ContactSchema>> = async ({\r\n request,\r\n response,\r\n}) => {\r\n const contact = request.validated();\r\n\r\n // Replace this with delivery/persistence for your app. Keeping the accepted\r\n // payload visible makes the endpoint useful while remaining side-effect free.\r\n return response.success({\r\n message: \"Thanks, \" + contact.name + \". Your message has been received.\",\r\n });\r\n};\r\n\r\ncontactController.validation = { schema: contactSchema };\r\n`;\r\n\r\n/** `src/app/contact/routes.ts` — discovered by the standard app route loader. */\r\nexport const webContactRoutesStub = `import { router } from \"@warlock.js/core\";\r\nimport { contactController } from \"./controllers/contact.controller\";\r\n\r\nrouter.post(\"/api/contact\", contactController);\r\n`;\r\n\r\n/**\r\n * `src/web/index.register.ts` — universal static setup for the starter page.\r\n *\r\n * The page re-exports this stable binding so Warlock's `register()` lifecycle\r\n * still sees it in both realms without making React Fast Refresh treat every\r\n * JSX edit as an incompatible function-export replacement.\r\n */\r\nexport const webHomeRegisterStub = `import { extend } from \"@mongez/localization\";\r\n\r\nexport function register() {\r\n extend(\"en\", {\r\n starter: {\r\n title: \"Your Warlock app is running.\",\r\n introduction: \"This page is rendered on the server and hydrated in the browser.\",\r\n language: \"العربية\",\r\n contact: \"Send a message\",\r\n name: \"Name\",\r\n email: \"Email\",\r\n message: \"Message\",\r\n submit: \"Send message\",\r\n sent: \"Thanks — your message has been received.\",\r\n },\r\n });\r\n extend(\"ar\", {\r\n starter: {\r\n title: \"تطبيق Warlock يعمل الآن.\",\r\n introduction: \"تُعرض هذه الصفحة على الخادم ثم تُفعَّل في المتصفح.\",\r\n language: \"English\",\r\n contact: \"أرسل رسالة\",\r\n name: \"الاسم\",\r\n email: \"البريد الإلكتروني\",\r\n message: \"الرسالة\",\r\n submit: \"إرسال الرسالة\",\r\n sent: \"شكرًا — تم استلام رسالتك.\",\r\n },\r\n });\r\n}\r\n`;\r\n\r\n/**\r\n * `src/web/index.page.tsx` — one page, so \\`warlock dev\\` has something to serve\r\n * the moment this finishes.\r\n */\r\nexport const webHomePageStub = `import { http } from \"@mongez/http\";\r\nimport { setCurrentLocaleCode } from \"@mongez/localization\";\r\nimport { Form, useFormControl, type FormControlProps } from \"@mongez/react-form\";\r\nimport { transX } from \"@mongez/react-localization\";\r\nimport { v } from \"@warlock.js/seal\";\r\nimport { Link, type PageProps } from \"@warlock.js/web\";\r\nimport { useState } from \"react\";\r\n\r\nexport { register } from \"./index.register\";\r\n\r\n/**\r\n * A page route is an ordinary Warlock route whose handler renders React\r\n * instead of returning JSON.\r\n *\r\n * The URL and stable hydration name are the ones this file DECLARES below.\r\n * This page answers \\`GET \"/\"\\` because \\`route.path = \"/\"\\`, not because of\r\n * where the file lives. A page file with\r\n * no \\`route\\` export is REFUSED by both the dev server and the build.\r\n */\r\nexport const route = { path: \"/\", name: \"index\" } as const;\r\n\r\nexport const metadata = { title: \"Home\" };\r\n\r\nconst contactSchema = v.object({\r\n name: v.string().min(2).required(),\r\n email: v.email().required(),\r\n message: v.string().min(10).required(),\r\n});\r\n\r\nfunction TextInput({ label, ...controlProps }: FormControlProps & { label: string }) {\r\n const { error, getErrorProps, getInputProps } = useFormControl(controlProps);\r\n\r\n return (\r\n <div className=\"wk-field\">\r\n <label htmlFor={controlProps.name}>{label}</label>\r\n <input {...getInputProps()} />\r\n {error && <p {...getErrorProps()}>{error}</p>}\r\n </div>\r\n );\r\n}\r\n\r\n/**\r\n * Add a \\`loader\\` export to fetch data on the server, and it arrives here as\r\n * \\`data\\`, typed:\r\n *\r\n * export const loader = (async () => ({ items: await itemsRepository.all() }));\r\n * export default function HomePage({ data }: PageProps<typeof loader>) { ... }\r\n */\r\nexport default function HomePage(_props: PageProps) {\r\n // Live state. If the button below does nothing, the page rendered on the\r\n // server but never hydrated — the runtime never mounted at \\`#root\\`. This is\r\n // deliberately here so that failure is impossible to miss.\r\n const [count, setCount] = useState(0);\r\n const [locale, setLocale] = useState<\"en\" | \"ar\">(\"en\");\r\n const [submitted, setSubmitted] = useState(false);\r\n const [submitError, setSubmitError] = useState<string | null>(null);\r\n\r\n const toggleLocale = () => {\r\n const nextLocale = locale === \"en\" ? \"ar\" : \"en\";\r\n setCurrentLocaleCode(nextLocale);\r\n setLocale(nextLocale);\r\n };\r\n\r\n return (\r\n <>\r\n {/*\r\n Self-contained, dependency-free styling: plain CSS, system fonts, and\r\n CSS custom properties, scoped to this page. No CSS framework, no utility\r\n classes, no external stylesheet — this page looks the same whether or\r\n not \\`warlock add tailwind\\` has ever been run.\r\n */}\r\n <style>{\\`\r\n .wk-home {\r\n --wk-fg: #0f172a;\r\n --wk-muted: #64748b;\r\n --wk-accent: #4f46e5;\r\n --wk-border: #e2e8f0;\r\n font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\r\n color: var(--wk-fg);\r\n max-width: 42rem;\r\n margin: 4rem auto;\r\n padding: 0 1.5rem;\r\n line-height: 1.6;\r\n }\r\n .wk-home h1 { font-size: 2.25rem; margin: 0 0 0.5rem; }\r\n .wk-home p { color: var(--wk-muted); margin: 0 0 1.5rem; }\r\n .wk-home code {\r\n font-family: ui-monospace, \"SFMono-Regular\", Menlo, monospace;\r\n background: #f1f5f9;\r\n padding: 0.1rem 0.35rem;\r\n border-radius: 0.25rem;\r\n }\r\n .wk-check {\r\n border: 1px solid var(--wk-border);\r\n border-radius: 0.75rem;\r\n padding: 1.25rem 1.5rem;\r\n margin: 2rem 0;\r\n }\r\n .wk-check strong { display: block; font-size: 1.5rem; }\r\n .wk-check button {\r\n font: inherit;\r\n cursor: pointer;\r\n background: var(--wk-accent);\r\n color: #fff;\r\n border: 0;\r\n border-radius: 0.5rem;\r\n padding: 0.5rem 1rem;\r\n margin-top: 0.75rem;\r\n }\r\n .wk-links { display: flex; gap: 1.25rem; font-size: 0.95rem; }\r\n .wk-links a { color: var(--wk-accent); text-decoration: none; }\r\n .wk-links a:hover { text-decoration: underline; }\r\n .wk-language { margin-left: auto; }\r\n .wk-contact { margin-top: 2rem; }\r\n .wk-field { display: grid; gap: 0.35rem; margin: 0.8rem 0; }\r\n .wk-field input, .wk-field textarea { font: inherit; padding: 0.55rem; }\r\n .wk-field p, .wk-submit-error { color: #b91c1c; margin: 0; }\r\n .wk-success { color: #047857; }\r\n \\`}</style>\r\n\r\n <main className=\"wk-home\" dir={locale === \"ar\" ? \"rtl\" : \"ltr\"}>\r\n <nav className=\"wk-links\" aria-label=\"Starter links\">\r\n <a href=\"https://warlock.js.org\" target=\"_blank\" rel=\"noreferrer\">\r\n Docs\r\n </a>\r\n <Link href=\"/\" aria-current=\"page\">\r\n Home\r\n </Link>\r\n <button\r\n className=\"wk-language\"\r\n type=\"button\"\r\n aria-pressed={locale === \"ar\"}\r\n onClick={toggleLocale}\r\n >\r\n {transX(\"starter.language\")}\r\n </button>\r\n </nav>\r\n\r\n <h1>{transX(\"starter.title\")}</h1>\r\n <p>{transX(\"starter.introduction\")}</p>\r\n\r\n <section className=\"wk-check\">\r\n <label>If this number goes up when you click, React is hydrated:</label>\r\n <strong>{count}</strong>\r\n <button type=\"button\" onClick={() => setCount((c) => c + 1)}>\r\n Count up\r\n </button>\r\n </section>\r\n\r\n <section className=\"wk-contact\" aria-labelledby=\"contact-heading\">\r\n <h2 id=\"contact-heading\">{transX(\"starter.contact\")}</h2>\r\n <Form<typeof contactSchema>\r\n id=\"contact-form\"\r\n schema={contactSchema}\r\n onSubmit={async ({ form, values }) => {\r\n setSubmitted(false);\r\n setSubmitError(null);\r\n const result = await http.post<{ message: string }>(\"/api/contact\", values);\r\n\r\n if (result.error) {\r\n if (result.error.isValidationError) {\r\n const body = result.error.body as {\r\n errors?: Array<{ input: string; error: string }>;\r\n message?: string;\r\n };\r\n form.setErrors(\r\n Object.fromEntries(\r\n (body.errors ?? []).map(({ input, error }) => [input, error]),\r\n ),\r\n );\r\n setSubmitError(body.message ?? \"Please correct the highlighted fields.\");\r\n } else {\r\n setSubmitError(\"Your message could not be sent. Please try again.\");\r\n }\r\n return;\r\n }\r\n\r\n setSubmitted(true);\r\n form.reset();\r\n }}\r\n >\r\n <TextInput name=\"name\" label={transX(\"starter.name\")} autoComplete=\"name\" />\r\n <TextInput\r\n name=\"email\"\r\n label={transX(\"starter.email\")}\r\n type=\"email\"\r\n autoComplete=\"email\"\r\n />\r\n <ContactMessage />\r\n <button type=\"submit\">{transX(\"starter.submit\")}</button>\r\n {submitError && (\r\n <p className=\"wk-submit-error\" role=\"alert\">\r\n {submitError}\r\n </p>\r\n )}\r\n {submitted && (\r\n <p className=\"wk-success\" role=\"status\">\r\n {transX(\"starter.sent\")}\r\n </p>\r\n )}\r\n </Form>\r\n </section>\r\n </main>\r\n </>\r\n );\r\n}\r\n\r\nfunction ContactMessage() {\r\n const { error, getErrorProps, getInputProps } = useFormControl({ name: \"message\" });\r\n\r\n return (\r\n <div className=\"wk-field\">\r\n <label htmlFor=\"message\">{transX(\"starter.message\")}</label>\r\n <textarea {...getInputProps()} rows={5} />\r\n {error && <p {...getErrorProps()}>{error}</p>}\r\n </div>\r\n );\r\n}\r\n`;\r\n"],"mappings":";AAAA,MAAa,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BhC,MAAa,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+B5B,MAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCnC,MAAa,2BAA2B;;AAGxC,MAAa,0BAA0B;;;;;;;;;;;;AAavC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8FvC,MAAa,+BAA+B;;AAG5C,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;AAsB3C,MAAa,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6ClC,MAAa,mBAAmB;;;;;;;;;;;;;;;;;AAkBhC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkFvC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCvC,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CrC,MAAa,4BAA4B;;;;;;;;;;;;;;;;;;;;;AAsBzC,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8F3C,MAAa,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCtC,MAAa,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuD3B,MAAa,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BxC,MAAa,uBAAuB;;;;;;;;;;;;AAapC,MAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCnC,MAAa,kBAAkB"}
|
|
1
|
+
{"version":3,"file":"stubs.mjs","names":[],"sources":["../../../../../../../core/src/generations/stubs.ts"],"sourcesContent":["export const accessConfigStub = `import { type AccessConfigurations } from \"@warlock.js/access\";\r\nimport { DatabaseAccessResolver } from \"app/access/services/access-resolver\";\r\n\r\n/**\r\n * Authorization configuration — read by @warlock.js/access on boot.\r\n *\r\n * The resolver is the one required piece: it tells the engine how to read a\r\n * user's roles + permissions. The ejected DatabaseAccessResolver reads roles\r\n * from the user_roles table and maps them through the roles catalog table (so\r\n * roles + their permissions are managed at runtime, in the DB).\r\n *\r\n * For a fixed, code-defined catalog with no tables, swap in DefaultAccessResolver:\r\n * import { DefaultAccessResolver } from \"@warlock.js/access\";\r\n * resolver: new DefaultAccessResolver({ admin: [\"*\"], editor: [\"orders.*\"] }),\r\n *\r\n * Multi-tenant? Add a \\`resolveTenant()\\` to the resolver to read the active\r\n * tenant from the request; checks then scope to it automatically.\r\n */\r\nconst access: AccessConfigurations = {\r\n resolver: new DatabaseAccessResolver(),\r\n\r\n // Cache resolved permission sets (default \"10m\").\r\n // cache: { ttl: \"10m\" },\r\n};\r\n\r\nexport default access;\r\n`;\r\n\r\nexport const aiConfigStub = `import type { AIConfig } from \"@warlock.js/ai\";\r\n\r\n// >>> warlock:ai-packages (auto-managed) >>>\r\n// Satellite packages augment the \"ai\" object on import — e.g. ai.workspace,\r\n// ai.tools / ai.mcp, and panoptic's ai.config({ panoptic }) wiring. The command\r\n// \"warlock add ai-workspace | ai-tools | ai-panoptic\" adds the matching\r\n// side-effect import below; keep them so the augmentation + runtime registration\r\n// load before the ai connector applies this config.\r\n// <<< warlock:ai-packages <<<\r\n\r\n/**\r\n * AI configuration — applied on boot by the ai connector, which calls\r\n * ai.config(...) with the object below. Cross-cutting defaults live here\r\n * (shared cache / snapshot stores, observability); per-call options always win.\r\n *\r\n * Wire a default model from a provider you installed, e.g.:\r\n * import { OpenAISDK } from \"@warlock.js/ai-openai\";\r\n * const openai = OpenAISDK({ apiKey: env(\"OPENAI_API_KEY\") });\r\n * // then pass openai.model({ name: \"gpt-4o-mini\" }) into your agents.\r\n */\r\nconst ai: Partial<AIConfig> = {\r\n // Default cache driver for cache-backed AI features (semantic cache, rag / memory vector stores).\r\n // defaultStore: cache.driver(\"redis\", { client }),\r\n\r\n // Observability — requires \"warlock add ai-panoptic\". Exporters + the local dashboard.\r\n // panoptic: { exporters: [], dashboard: false, observeAll: false },\r\n};\r\n\r\nexport default ai;\r\n`;\r\n\r\nexport const accessRoleModelStub = `import { Model, RegisterModel } from \"@warlock.js/cascade\";\r\nimport { type Infer, v } from \"@warlock.js/seal\";\r\n\r\n/**\r\n * Validation schema for the roles catalog — mirrors the migration columns\r\n * (snake_case). Each row is a role name plus the permission strings it grants;\r\n * wildcards work (\"orders.*\", \"*\"). The DatabaseAccessResolver maps a user's\r\n * assigned role names through this table to their effective permissions.\r\n */\r\nexport const roleSchema = v.object({\r\n name: v.string(),\r\n permissions: v.array(v.string()).default([]),\r\n});\r\n\r\nexport type RoleSchema = Infer<typeof roleSchema>;\r\n\r\n/**\r\n * The roles catalog — role name → the permissions it grants. Managed at runtime\r\n * (admins add roles + edit their permissions), unlike a fixed code map. Read by\r\n * DatabaseAccessResolver.resolvePermissions to expand a user's roles to permissions.\r\n */\r\n@RegisterModel()\r\nexport class Role extends Model<RoleSchema> {\r\n public static table = \"roles\";\r\n\r\n public static schema = roleSchema;\r\n\r\n /** The permission strings this role grants. */\r\n public get permissions(): string[] {\r\n return this.get<string[]>(\"permissions\", []);\r\n }\r\n}\r\n`;\r\n\r\nexport const accessRoleModelIndexStub = `export * from \"./role.model\";\r\n`;\r\n\r\nexport const accessRoleMigrationStub = `import { arrayText, Migration, text } from \"@warlock.js/cascade\";\r\nimport { Role } from \"../role.model\";\r\n\r\n/**\r\n * Roles catalog table. \\`name\\` is unique (one row per role); \\`permissions\\` is a\r\n * text array of the permission strings the role grants.\r\n */\r\nexport default Migration.create(Role, {\r\n name: text().notNullable().unique(),\r\n permissions: arrayText().nullable(),\r\n});\r\n`;\r\n\r\nexport const accessUserRoleModelStub = `import { access } from \"@warlock.js/access\";\r\nimport type { Auth } from \"@warlock.js/auth\";\r\nimport { Model, RegisterModel } from \"@warlock.js/cascade\";\r\nimport { type Infer, v } from \"@warlock.js/seal\";\r\n\r\n/**\r\n * Validation schema for a role assignment — mirrors the migration columns\r\n * (snake_case). \\`tenant\\` is nullable: a null tenant is a GLOBAL assignment.\r\n */\r\nexport const userRoleSchema = v.object({\r\n user_id: v.string(),\r\n user_type: v.string(),\r\n role: v.string(),\r\n tenant: v.string().optional(),\r\n});\r\n\r\nexport type UserRoleSchema = Infer<typeof userRoleSchema>;\r\n\r\n/**\r\n * The role-assignment table — which roles a user holds, optionally per tenant.\r\n * Read by DatabaseAccessResolver.resolveRoles; mutated via the statics below.\r\n * \\`assign\\` / \\`revoke\\` flush the cached permission set automatically, so callers\r\n * never need to call \\`access.flush(user, tenant)\\` themselves.\r\n */\r\n@RegisterModel()\r\nexport class UserRole extends Model<UserRoleSchema> {\r\n public static table = \"user_roles\";\r\n\r\n public static schema = userRoleSchema;\r\n\r\n /**\r\n * Role names assigned to the user in the given tenant.\r\n *\r\n * An unresolved tenant (\\`undefined\\`) scopes to GLOBAL roles only — the rows\r\n * stored with no tenant (\\`null\\`) — never the union across every tenant. The\r\n * union would be a privilege-escalation: a user who is \\`owner\\` in one tenant\r\n * must not be treated as \\`owner\\` everywhere just because a check didn't carry\r\n * a tenant. This mirrors how \\`assign(user, role)\\` stores a global row.\r\n */\r\n public static async rolesFor(user: Auth, tenant?: string): Promise<string[]> {\r\n const rows = await this.query()\r\n .where({\r\n user_id: user.id,\r\n user_type: user.userType,\r\n tenant: tenant ?? null,\r\n })\r\n .get();\r\n\r\n // De-dupe so a duplicate row (a concurrent assign that slipped past the\r\n // existence check) can't distort the resolved set.\r\n return [...new Set(rows.map((row) => row.get(\"role\") as string))];\r\n }\r\n\r\n /**\r\n * Assign a role to the user. No-op if the assignment already exists.\r\n * Flushes the user's cached permission set automatically.\r\n */\r\n public static async assign(user: Auth, role: string, tenant?: string): Promise<void> {\r\n const existing = await this.first({\r\n user_id: user.id,\r\n user_type: user.userType,\r\n role,\r\n tenant: tenant ?? null,\r\n });\r\n\r\n if (existing) return;\r\n\r\n await this.create({\r\n user_id: user.id,\r\n user_type: user.userType,\r\n role,\r\n tenant,\r\n });\r\n\r\n await access.flush(user, tenant);\r\n }\r\n\r\n /**\r\n * Remove a role assignment from the user.\r\n * Flushes the user's cached permission set automatically.\r\n */\r\n public static async revoke(user: Auth, role: string, tenant?: string): Promise<void> {\r\n await this.delete({\r\n user_id: user.id,\r\n user_type: user.userType,\r\n role,\r\n tenant: tenant ?? null,\r\n });\r\n\r\n await access.flush(user, tenant);\r\n }\r\n}\r\n`;\r\n\r\nexport const accessUserRoleModelIndexStub = `export * from \"./user-role.model\";\r\n`;\r\n\r\nexport const accessUserRoleMigrationStub = `import { Migration, text, uuid } from \"@warlock.js/cascade\";\r\nimport { UserRole } from \"../user-role.model\";\r\n\r\n/**\r\n * Role-assignment table. \\`user_id\\` is a UUID — override this migration if your\r\n * user ids are integers. The composite index powers the per-user (per-tenant)\r\n * lookup the resolver runs on every check.\r\n */\r\nexport default Migration.create(\r\n UserRole,\r\n {\r\n user_id: uuid().notNullable().index(),\r\n user_type: text().notNullable(),\r\n role: text().notNullable().index(),\r\n tenant: text().nullable().index(),\r\n },\r\n {\r\n index: [{ columns: [\"user_id\", \"user_type\", \"tenant\"] }],\r\n },\r\n);\r\n`;\r\n\r\nexport const accessResolverStub = `import type { AccessResolver } from \"@warlock.js/access\";\r\nimport type { Auth } from \"@warlock.js/auth\";\r\nimport { Role } from \"app/access/models/role\";\r\nimport { UserRole } from \"app/access/models/user-role\";\r\n\r\n/**\r\n * The app's access adapter — connects @warlock.js/access to the ejected role\r\n * tables. Roles come from the user_roles assignment table; permissions are\r\n * expanded by mapping those role names through the roles catalog table. Both\r\n * are managed at runtime (in the DB), so admins can add roles + edit their\r\n * permissions without a deploy.\r\n *\r\n * The engine owns the hard parts (wildcard matching, caching, fail-closed); this\r\n * resolver only fetches — keep it dumb, never cache inside it.\r\n */\r\nexport class DatabaseAccessResolver implements AccessResolver {\r\n /** The role names this user holds (powers \\`hasRole\\` / \\`hasAnyRole\\`). */\r\n public async resolveRoles(user: Auth, tenant?: string): Promise<string[]> {\r\n return UserRole.rolesFor(user, tenant);\r\n }\r\n\r\n /** The effective permission strings this user has (powers \\`can\\` / \\`authorize\\`). */\r\n public async resolvePermissions(user: Auth, tenant?: string): Promise<string[]> {\r\n const names = await this.resolveRoles(user, tenant);\r\n\r\n if (names.length === 0) return [];\r\n\r\n const roles = await Role.query().whereIn(\"name\", names).get();\r\n\r\n // Flatten + de-dupe so two roles granting the same permission yield one entry.\r\n return [...new Set(roles.flatMap((role) => role.permissions))];\r\n }\r\n\r\n /**\r\n * Optional. Resolve the ambient tenant when a check doesn't pass one\r\n * explicitly — derive it from the authenticated user (safer than reading\r\n * client request input, which a caller could spoof). Uncomment + adapt for a\r\n * multi-tenant app (single-tenant apps leave this off and return undefined).\r\n */\r\n // public resolveTenant(user: Auth): string | undefined {\r\n // return user.get(\"organization_id\");\r\n // }\r\n}\r\n`;\r\n\r\nexport const socketConfigStub = `import type { SocketOptions } from \"@warlock.js/core\";\r\n\r\n/**\r\n * Socket.IO configuration — read by the framework's socket connector\r\n * on boot. When the HTTP server is running the socket server attaches\r\n * to it; otherwise it listens on its own configured port.\r\n *\r\n * Remove this file to disable the socket server entirely.\r\n */\r\nexport default {\r\n options: {\r\n cors: {\r\n origin: \"*\",\r\n },\r\n },\r\n} as SocketOptions;\r\n`;\r\n\r\nexport const communicatorsConfigStub = `import { env } from \"@warlock.js/core\";\r\nimport type { BrokerConfigurations, RabbitMQClientOptions } from \"@warlock.js/herald\";\r\n\r\nconst heraldConfigurations: BrokerConfigurations<RabbitMQClientOptions> = {\r\n driver: \"rabbitmq\",\r\n name: \"default\",\r\n isDefault: true,\r\n\r\n // ============================================================================\r\n // Connection Settings\r\n // ============================================================================\r\n\r\n host: env(\"RABBITMQ_HOST\", \"localhost\"),\r\n port: env(\"RABBITMQ_PORT\", 5672),\r\n username: env(\"RABBITMQ_USERNAME\", \"guest\"),\r\n password: env(\"RABBITMQ_PASSWORD\", \"guest\"),\r\n vhost: env(\"RABBITMQ_VHOST\", \"/\"),\r\n\r\n // Or use connection URI (takes precedence over host/port)\r\n // uri: env(\"RABBITMQ_URL\"),\r\n\r\n // ============================================================================\r\n // Connection Options\r\n // ============================================================================\r\n\r\n /** Heartbeat interval in seconds */\r\n heartbeat: 60,\r\n\r\n /** Connection timeout in milliseconds */\r\n connectionTimeout: 10000,\r\n\r\n /** Enable automatic reconnection on disconnect */\r\n reconnect: true,\r\n\r\n /** Delay between reconnection attempts in milliseconds */\r\n reconnectDelay: 5_000,\r\n\r\n // ============================================================================\r\n // Consumer Options\r\n // ============================================================================\r\n\r\n /** Default prefetch count (number of unacknowledged messages per consumer) */\r\n prefetch: 10,\r\n\r\n // ============================================================================\r\n // Client Options (Native amqplib options)\r\n // ============================================================================\r\n // These options are passed directly to amqplib.connect()\r\n // for low-level configuration like frame size, TLS, socket options, etc.\r\n // ============================================================================\r\n clientOptions: {\r\n // Frame max size in bytes (0 = no limit)\r\n // frameMax: 0,\r\n\r\n // Channel max (0 = unlimited)\r\n // channelMax: 0,\r\n\r\n // Socket options\r\n socket: {\r\n // Enable TCP keep-alive\r\n keepAlive: true,\r\n\r\n // Disable Nagle's algorithm for lower latency\r\n noDelay: true,\r\n\r\n // Socket timeout (in addition to heartbeat)\r\n // timeout: 30000,\r\n },\r\n\r\n // TLS/SSL options (uncomment for secure connections)\r\n // socket: {\r\n // ca: fs.readFileSync('/path/to/ca.pem'),\r\n // cert: fs.readFileSync('/path/to/cert.pem'),\r\n // key: fs.readFileSync('/path/to/key.pem'),\r\n // rejectUnauthorized: true,\r\n // },\r\n },\r\n};\r\n\r\nexport default heraldConfigurations;\r\n`;\r\n\r\nexport const notificationsConfigStub = `import { type NotificationConfig, inApp, mailChannel } from \"@warlock.js/notifications\";\r\nimport { Notification } from \"app/notifications/notification.model\";\r\n\r\n/**\r\n * Notifications configuration. Auto-loaded from src/config on boot — the\r\n * framework's notifications connector reads this default export and hands it to\r\n * setNotificationConfig, so this file stays declarative (no side-effect call).\r\n *\r\n * Each channel is payload-typed, so notify.mail(...) / notify.database(...)\r\n * and defineNotification are type-checked against the registry.\r\n *\r\n * Channels enabled here:\r\n * - mail wraps @warlock.js/core sendMail; route is notifiable.email.\r\n * The \"from\" address defaults to config/mail.ts; override per\r\n * channel with mailChannel({ from: \"no-reply@yourapp.com\" }).\r\n * - database in-app store backed by the Notification model. The \"inApp\"\r\n * facade exposes the recipient-scoped read API: listUnread,\r\n * countUnread, markAsRead, dismiss, ...\r\n *\r\n * Async delivery (.queue()) is OPTIONAL: run \"npx warlock add herald\",\r\n * import { heraldQueue } from \"@warlock.js/notifications\", and uncomment the\r\n * queue line below.\r\n */\r\nconst config: NotificationConfig = {\r\n channels: {\r\n mail: mailChannel(),\r\n database: inApp.configure({ model: Notification }),\r\n },\r\n\r\n // Async queue — requires @warlock.js/herald (npx warlock add herald):\r\n // queue: heraldQueue(),\r\n};\r\n\r\nexport default config;\r\n`;\r\n\r\nexport const notificationModelStub = `import { RegisterModel } from \"@warlock.js/cascade\";\r\nimport { DatabaseNotification, type NotificationColumnMap } from \"@warlock.js/notifications\";\r\nimport { v } from \"@warlock.js/seal\";\r\n\r\n/**\r\n * Validation schema for the notifications table — mirrors the migration\r\n * columns (snake_case). Cascade validates + casts every write against it:\r\n * nullable columns use .nullish() (may be absent or null), and payload is\r\n * free-form JSON. Keep this in sync with the migration + columnMap when you\r\n * add or rename columns.\r\n */\r\nconst notificationSchema = v.object({\r\n user_id: v.string(),\r\n type: v.string(),\r\n title: v.string(),\r\n body: v.string().nullish(),\r\n payload: v.record(v.any()).nullish(),\r\n read_at: v.date().nullish(),\r\n idempotency_key: v.string().nullish(),\r\n});\r\n\r\n/**\r\n * In-app notification model.\r\n *\r\n * Extends the package's DatabaseNotification base, which provides the stable\r\n * accessors (recipientId, tenantId, isRead, readAt, markRead) — all derived\r\n * from the columnMap below. The read/write API lives on the inApp facade\r\n * (configured in config/notifications.ts); you rarely touch this class directly.\r\n */\r\n@RegisterModel()\r\nexport class Notification extends DatabaseNotification {\r\n public static table = \"notifications\";\r\n public static schema = notificationSchema;\r\n\r\n /**\r\n * Maps the in-app store's roles to your columns. This default is\r\n * single-tenant + read_at-only. Add tenant: \"organization_id\" for\r\n * multi-tenant; use isRead: \"is_read\" (instead of, or alongside, readAt) to\r\n * track a boolean read flag. The migration + accessors all follow this map.\r\n */\r\n public static columnMap: NotificationColumnMap = { readAt: \"read_at\" };\r\n}\r\n`;\r\n\r\nexport const notificationMigrationStub = `import { Migration } from \"@warlock.js/cascade\";\r\nimport { notificationColumns } from \"@warlock.js/notifications\";\r\nimport { Notification } from \"../notification.model\";\r\n\r\n/**\r\n * Notifications table.\r\n *\r\n * Columns come from notificationColumns(Notification) — the recipient / tenant\r\n * / read-state names follow the model's columnMap; type / title / body /\r\n * payload / idempotency_key are fixed. Spread it to add your own columns\r\n * (remember to mirror them in the model schema):\r\n *\r\n * import { uuid } from \"@warlock.js/cascade\";\r\n *\r\n * export default Migration.create(Notification, {\r\n * ...notificationColumns(Notification),\r\n * // category_id: uuid().index().nullable(),\r\n * });\r\n */\r\nexport default Migration.create(Notification, notificationColumns(Notification));\r\n`;\r\n\r\nexport const notificationControllersStub = `import { type RequestHandler } from \"@warlock.js/core\";\r\nimport { inApp, type Id } from \"@warlock.js/notifications\";\r\n\r\n/**\r\n * The authenticated user's notification HTTP surface — thin wrappers over the\r\n * recipient-scoped \\`inApp\\` facade (a foreign id can never touch another user's\r\n * rows). Notifications are produced by domain events, never over HTTP, so there\r\n * is no create. Trim or split these as your app grows.\r\n */\r\n\r\n/**\r\n * Read \\`id\\` off \\`request.user\\` without assuming this app's \\`RequestUser\\`\r\n * augmentation declares it — \\`RequestUser\\` is empty by default (see\r\n * \\`@warlock.js/core\\`'s \\`RequestUser\\` docs), so a narrow runtime read survives\r\n * any augmentation shape instead of assuming \\`.id\\` exists at the type level.\r\n * \\`inApp\\` only ever needs the id (it reduces a \\`Notifiable\\` to one via\r\n * \\`recipient.id\\` internally), so reading it here — rather than forwarding\r\n * \\`request.user\\` itself — also skips a needless \\`Notifiable\\` cast.\r\n */\r\nfunction recipientId(user: unknown): Id {\r\n if (user && typeof user === \"object\" && \"id\" in user) {\r\n const id = (user as { id?: unknown }).id;\r\n\r\n if (typeof id === \"string\" || typeof id === \"number\") return id;\r\n }\r\n\r\n throw new Error(\"Authenticated request is missing a usable user id\");\r\n}\r\n\r\n/** GET /notifications — list, most recent first (page / limit / type / unread via query). */\r\nexport const listNotificationsController: RequestHandler = async ({ request, response }) => {\r\n const { data, pagination } = await inApp.list(recipientId(request.user), request.all());\r\n\r\n return response.success({ notifications: data, pagination });\r\n};\r\n\r\nlistNotificationsController.description = \"List notifications\";\r\n\r\n/** GET /notifications/unread-count — drives the bell badge. */\r\nexport const unreadNotificationsCountController: RequestHandler = async ({\r\n request,\r\n response,\r\n}) => {\r\n const count = await inApp.countUnread(recipientId(request.user));\r\n\r\n return response.success({ count });\r\n};\r\n\r\nunreadNotificationsCountController.description = \"Unread notifications count\";\r\n\r\n/** PATCH /notifications/:id/read — mark one read, return the updated row. */\r\nexport const markNotificationReadController: RequestHandler = async ({ request, response }) => {\r\n const id = request.input(\"id\");\r\n const userId = recipientId(request.user);\r\n\r\n await inApp.markAsRead(userId, id);\r\n const notification = await inApp.find(userId, id);\r\n\r\n return response.success({ notification });\r\n};\r\n\r\nmarkNotificationReadController.description = \"Mark notification read\";\r\n\r\n/** PATCH /notifications/read-all — mark every unread one read. */\r\nexport const markAllNotificationsReadController: RequestHandler = async ({\r\n request,\r\n response,\r\n}) => {\r\n const count = await inApp.markAsRead(recipientId(request.user));\r\n\r\n return response.success({ count });\r\n};\r\n\r\nmarkAllNotificationsReadController.description = \"Mark all notifications read\";\r\n\r\n/** DELETE /notifications — dismiss all for the user. */\r\nexport const clearNotificationsController: RequestHandler = async ({ request, response }) => {\r\n await inApp.dismiss(recipientId(request.user));\r\n\r\n return response.noContent();\r\n};\r\n\r\nclearNotificationsController.description = \"Clear notifications\";\r\n\r\n/** DELETE /notifications/:id — dismiss one. */\r\nexport const deleteNotificationController: RequestHandler = async ({ request, response }) => {\r\n await inApp.dismiss(recipientId(request.user), request.input(\"id\"));\r\n\r\n return response.noContent();\r\n};\r\n\r\ndeleteNotificationController.description = \"Delete notification\";\r\n`;\r\n\r\nexport const notificationRoutesStub = `import { authMiddleware } from \"@warlock.js/auth\";\r\nimport { router } from \"@warlock.js/core\";\r\nimport {\r\n clearNotificationsController,\r\n deleteNotificationController,\r\n listNotificationsController,\r\n markAllNotificationsReadController,\r\n markNotificationReadController,\r\n unreadNotificationsCountController,\r\n} from \"./controllers/notifications.controller\";\r\n\r\n/**\r\n * Notification routes — the authenticated user's read + dismiss surface.\r\n *\r\n * Notifications are produced by domain events (never created over HTTP), so\r\n * there is no POST. Every route is gated by \\`authMiddleware\\` and recipient-\r\n * scoped by \\`inApp\\` (a foreign id touches zero rows). Delete any endpoint you\r\n * don't need; if your app reads notifications over sockets/GraphQL instead,\r\n * delete this file + the controllers entirely.\r\n */\r\nrouter.group({ prefix: \"/notifications\", middleware: [authMiddleware([])] }, () => {\r\n router.get(\"/\", listNotificationsController);\r\n router.get(\"/unread-count\", unreadNotificationsCountController);\r\n router.patch(\"/read-all\", markAllNotificationsReadController);\r\n router.patch(\"/:id/read\", markNotificationReadController);\r\n router.delete(\"/\", clearNotificationsController);\r\n router.delete(\"/:id\", deleteNotificationController);\r\n});\r\n`;\r\n\r\n/**\r\n * `src/web/root.tsx` — the application root for the SSR page layer.\r\n *\r\n * Deliberately minimal. The framework ships a default root, so this exists to\r\n * give you a place to start rather than because anything requires it. The\r\n * reference app (`v5/app/src/web/root.tsx`) is where to look for the fuller\r\n * shape: middleware, an app-level loader, locales, an ErrorBoundary.\r\n */\r\nexport const webRootStub = `import type { AppProps } from \"@warlock.js/web\";\r\nimport { Head, Scripts } from \"@warlock.js/web\";\r\n\r\n/**\r\n * The application root.\r\n *\r\n * NOT async, and it receives no request/response: it renders on the server and\r\n * again in the browser during hydration, where neither exists.\r\n */\r\nexport default function App({ children }: AppProps) {\r\n return (\r\n <html lang=\"en\">\r\n <head>\r\n {/*\r\n Placement only. The framework injects the page's \\`metadata\\`, the\r\n stylesheet and preload tags for this route, and the canonical links\r\n into <head> by default — <Head /> just says WHERE they land.\r\n\r\n Do not add a <title> here: the page's \\`metadata\\` owns it, and a root\r\n that emits one too produces two.\r\n */}\r\n <Head />\r\n <link rel=\"icon\" href=\"data:,\" />\r\n </head>\r\n <body>\r\n {/*\r\n REQUIRED — this is the hydration mount point, not a styling wrapper.\r\n\r\n The browser runtime looks up \\`#root\\` and hydrates that element only.\r\n Remove this div, or rename the id, and the page still renders from the\r\n server but never becomes interactive: the runtime throws in the console\r\n and nothing on screen changes.\r\n\r\n Wrap it in your own markup freely, and put anything that must live\r\n outside the hydrated tree (a static footer, a portal target) outside\r\n it — just keep an element with \\`id=\"root\"\\` around {children}.\r\n */}\r\n <div id=\"root\">{children}</div>\r\n {/*\r\n The hydration payload and module tags. Written explicitly because\r\n placement occasionally matters — a CSP nonce, or ordering against\r\n your own scripts.\r\n */}\r\n <Scripts />\r\n </body>\r\n </html>\r\n );\r\n}\r\n`;\r\n\r\n/**\r\n * `src/app/contact/controllers/contact.controller.ts` — a real API endpoint\r\n * for the Web starter's contact form. It intentionally has no persistence\r\n * dependency: replace the acknowledgement with a mail/job/database action.\r\n */\r\nexport const webContactControllerStub = `import { type Request, type RequestHandler } from \"@warlock.js/core\";\r\nimport { type Infer, v } from \"@warlock.js/seal\";\r\n\r\nexport const contactSchema = v.object({\r\n name: v.string().min(2),\r\n email: v.email(),\r\n message: v.string().min(10),\r\n});\r\n\r\nexport type ContactSchema = Infer.Output<typeof contactSchema>;\r\n\r\n/** POST /api/contact — validates the starter contact form. */\r\nexport const contactController: RequestHandler<Request<ContactSchema>> = async ({\r\n request,\r\n response,\r\n}) => {\r\n const contact = request.validated();\r\n\r\n // Replace this with delivery/persistence for your app. Keeping the accepted\r\n // payload visible makes the endpoint useful while remaining side-effect free.\r\n return response.success({\r\n message: \"Thanks, \" + contact.name + \". Your message has been received.\",\r\n });\r\n};\r\n\r\ncontactController.validation = { schema: contactSchema };\r\n`;\r\n\r\n/** `src/app/contact/routes.ts` — discovered by the standard app route loader. */\r\nexport const webContactRoutesStub = `import { router } from \"@warlock.js/core\";\r\nimport { contactController } from \"./controllers/contact.controller\";\r\n\r\nrouter.post(\"/api/contact\", contactController);\r\n`;\r\n\r\n/**\r\n * `src/web/index.register.ts` — universal static setup for the starter page.\r\n *\r\n * The page re-exports this stable binding so Warlock's `register()` lifecycle\r\n * still sees it in both realms without making React Fast Refresh treat every\r\n * JSX edit as an incompatible function-export replacement.\r\n */\r\nexport const webHomeRegisterStub = `import { extend } from \"@mongez/localization\";\r\n\r\nexport function register() {\r\n extend(\"en\", {\r\n starter: {\r\n title: \"Your Warlock app is running.\",\r\n introduction: \"This page is rendered on the server and hydrated in the browser.\",\r\n language: \"العربية\",\r\n contact: \"Send a message\",\r\n name: \"Name\",\r\n email: \"Email\",\r\n message: \"Message\",\r\n submit: \"Send message\",\r\n sent: \"Thanks — your message has been received.\",\r\n },\r\n });\r\n extend(\"ar\", {\r\n starter: {\r\n title: \"تطبيق Warlock يعمل الآن.\",\r\n introduction: \"تُعرض هذه الصفحة على الخادم ثم تُفعَّل في المتصفح.\",\r\n language: \"English\",\r\n contact: \"أرسل رسالة\",\r\n name: \"الاسم\",\r\n email: \"البريد الإلكتروني\",\r\n message: \"الرسالة\",\r\n submit: \"إرسال الرسالة\",\r\n sent: \"شكرًا — تم استلام رسالتك.\",\r\n },\r\n });\r\n}\r\n`;\r\n\r\n/**\r\n * `src/web/index.page.tsx` — one page, so \\`warlock dev\\` has something to serve\r\n * the moment this finishes.\r\n */\r\nexport const webHomePageStub = `import { http } from \"@mongez/http\";\r\nimport { setCurrentLocaleCode } from \"@mongez/localization\";\r\nimport { Form, useFormControl, type FormControlProps } from \"@mongez/react-form\";\r\nimport { transX } from \"@mongez/react-localization\";\r\nimport { v } from \"@warlock.js/seal\";\r\nimport { Link, type PageProps } from \"@warlock.js/web\";\r\nimport { useState } from \"react\";\r\n\r\nexport { register } from \"./index.register\";\r\n\r\n/**\r\n * A page route is an ordinary Warlock route whose handler renders React\r\n * instead of returning JSON.\r\n *\r\n * The URL and stable hydration name are the ones this file DECLARES below.\r\n * This page answers \\`GET \"/\"\\` because \\`route.path = \"/\"\\`, not because of\r\n * where the file lives. A page file with\r\n * no \\`route\\` export is REFUSED by both the dev server and the build.\r\n */\r\nexport const route = { path: \"/\", name: \"index\" } as const;\r\n\r\nexport const metadata = { title: \"Home\" };\r\n\r\nconst contactSchema = v.object({\r\n name: v.string().min(2),\r\n email: v.email(),\r\n message: v.string().min(10),\r\n});\r\n\r\nfunction TextInput({ label, ...controlProps }: FormControlProps & { label: string }) {\r\n const { error, getErrorProps, getInputProps } = useFormControl(controlProps);\r\n\r\n return (\r\n <div className=\"wk-field\">\r\n <label htmlFor={controlProps.name}>{label}</label>\r\n <input {...getInputProps()} />\r\n {error && <p {...getErrorProps()}>{error}</p>}\r\n </div>\r\n );\r\n}\r\n\r\n/**\r\n * Add a \\`loader\\` export to fetch data on the server, and it arrives here as\r\n * \\`data\\`, typed:\r\n *\r\n * export const loader = (async () => ({ items: await itemsRepository.all() }));\r\n * export default function HomePage({ data }: PageProps<typeof loader>) { ... }\r\n */\r\nexport default function HomePage(_props: PageProps) {\r\n // Live state. If the button below does nothing, the page rendered on the\r\n // server but never hydrated — the runtime never mounted at \\`#root\\`. This is\r\n // deliberately here so that failure is impossible to miss.\r\n const [count, setCount] = useState(0);\r\n const [locale, setLocale] = useState<\"en\" | \"ar\">(\"en\");\r\n const [submitted, setSubmitted] = useState(false);\r\n const [submitError, setSubmitError] = useState<string | null>(null);\r\n\r\n const toggleLocale = () => {\r\n const nextLocale = locale === \"en\" ? \"ar\" : \"en\";\r\n setCurrentLocaleCode(nextLocale);\r\n setLocale(nextLocale);\r\n };\r\n\r\n return (\r\n <>\r\n {/*\r\n Self-contained, dependency-free styling: plain CSS, system fonts, and\r\n CSS custom properties, scoped to this page. No CSS framework, no utility\r\n classes, no external stylesheet — this page looks the same whether or\r\n not \\`warlock add tailwind\\` has ever been run.\r\n */}\r\n <style>{\\`\r\n .wk-home {\r\n --wk-fg: #0f172a;\r\n --wk-muted: #64748b;\r\n --wk-accent: #4f46e5;\r\n --wk-border: #e2e8f0;\r\n font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\r\n color: var(--wk-fg);\r\n max-width: 42rem;\r\n margin: 4rem auto;\r\n padding: 0 1.5rem;\r\n line-height: 1.6;\r\n }\r\n .wk-home h1 { font-size: 2.25rem; margin: 0 0 0.5rem; }\r\n .wk-home p { color: var(--wk-muted); margin: 0 0 1.5rem; }\r\n .wk-home code {\r\n font-family: ui-monospace, \"SFMono-Regular\", Menlo, monospace;\r\n background: #f1f5f9;\r\n padding: 0.1rem 0.35rem;\r\n border-radius: 0.25rem;\r\n }\r\n .wk-check {\r\n border: 1px solid var(--wk-border);\r\n border-radius: 0.75rem;\r\n padding: 1.25rem 1.5rem;\r\n margin: 2rem 0;\r\n }\r\n .wk-check strong { display: block; font-size: 1.5rem; }\r\n .wk-check button {\r\n font: inherit;\r\n cursor: pointer;\r\n background: var(--wk-accent);\r\n color: #fff;\r\n border: 0;\r\n border-radius: 0.5rem;\r\n padding: 0.5rem 1rem;\r\n margin-top: 0.75rem;\r\n }\r\n .wk-links { display: flex; gap: 1.25rem; font-size: 0.95rem; }\r\n .wk-links a { color: var(--wk-accent); text-decoration: none; }\r\n .wk-links a:hover { text-decoration: underline; }\r\n .wk-language { margin-left: auto; }\r\n .wk-contact { margin-top: 2rem; }\r\n .wk-field { display: grid; gap: 0.35rem; margin: 0.8rem 0; }\r\n .wk-field input, .wk-field textarea { font: inherit; padding: 0.55rem; }\r\n .wk-field p, .wk-submit-error { color: #b91c1c; margin: 0; }\r\n .wk-success { color: #047857; }\r\n \\`}</style>\r\n\r\n <main className=\"wk-home\" dir={locale === \"ar\" ? \"rtl\" : \"ltr\"}>\r\n <nav className=\"wk-links\" aria-label=\"Starter links\">\r\n <a href=\"https://warlock.js.org\" target=\"_blank\" rel=\"noreferrer\">\r\n Docs\r\n </a>\r\n <Link href=\"/\" aria-current=\"page\">\r\n Home\r\n </Link>\r\n <button\r\n className=\"wk-language\"\r\n type=\"button\"\r\n aria-pressed={locale === \"ar\"}\r\n onClick={toggleLocale}\r\n >\r\n {transX(\"starter.language\")}\r\n </button>\r\n </nav>\r\n\r\n <h1>{transX(\"starter.title\")}</h1>\r\n <p>{transX(\"starter.introduction\")}</p>\r\n\r\n <section className=\"wk-check\">\r\n <label>If this number goes up when you click, React is hydrated:</label>\r\n <strong>{count}</strong>\r\n <button type=\"button\" onClick={() => setCount((c) => c + 1)}>\r\n Count up\r\n </button>\r\n </section>\r\n\r\n <section className=\"wk-contact\" aria-labelledby=\"contact-heading\">\r\n <h2 id=\"contact-heading\">{transX(\"starter.contact\")}</h2>\r\n <Form<typeof contactSchema>\r\n id=\"contact-form\"\r\n schema={contactSchema}\r\n onSubmit={async ({ form, values }) => {\r\n setSubmitted(false);\r\n setSubmitError(null);\r\n const result = await http.post<{ message: string }>(\"/api/contact\", values);\r\n\r\n if (result.error) {\r\n if (result.error.isValidationError) {\r\n const body = result.error.body as {\r\n errors?: Array<{ input: string; error: string }>;\r\n message?: string;\r\n };\r\n form.setErrors(\r\n Object.fromEntries(\r\n (body.errors ?? []).map(({ input, error }) => [input, error]),\r\n ),\r\n );\r\n setSubmitError(body.message ?? \"Please correct the highlighted fields.\");\r\n } else {\r\n setSubmitError(\"Your message could not be sent. Please try again.\");\r\n }\r\n return;\r\n }\r\n\r\n setSubmitted(true);\r\n form.reset();\r\n }}\r\n >\r\n <TextInput name=\"name\" label={transX(\"starter.name\")} autoComplete=\"name\" />\r\n <TextInput\r\n name=\"email\"\r\n label={transX(\"starter.email\")}\r\n type=\"email\"\r\n autoComplete=\"email\"\r\n />\r\n <ContactMessage />\r\n <button type=\"submit\">{transX(\"starter.submit\")}</button>\r\n {submitError && (\r\n <p className=\"wk-submit-error\" role=\"alert\">\r\n {submitError}\r\n </p>\r\n )}\r\n {submitted && (\r\n <p className=\"wk-success\" role=\"status\">\r\n {transX(\"starter.sent\")}\r\n </p>\r\n )}\r\n </Form>\r\n </section>\r\n </main>\r\n </>\r\n );\r\n}\r\n\r\nfunction ContactMessage() {\r\n const { error, getErrorProps, getInputProps } = useFormControl({ name: \"message\" });\r\n\r\n return (\r\n <div className=\"wk-field\">\r\n <label htmlFor=\"message\">{transX(\"starter.message\")}</label>\r\n <textarea {...getInputProps()} rows={5} />\r\n {error && <p {...getErrorProps()}>{error}</p>}\r\n </div>\r\n );\r\n}\r\n`;\r\n"],"mappings":";AAAA,MAAa,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BhC,MAAa,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+B5B,MAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCnC,MAAa,2BAA2B;;AAGxC,MAAa,0BAA0B;;;;;;;;;;;;AAavC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8FvC,MAAa,+BAA+B;;AAG5C,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;AAsB3C,MAAa,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6ClC,MAAa,mBAAmB;;;;;;;;;;;;;;;;;AAkBhC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkFvC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCvC,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CrC,MAAa,4BAA4B;;;;;;;;;;;;;;;;;;;;;AAsBzC,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8F3C,MAAa,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCtC,MAAa,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuD3B,MAAa,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BxC,MAAa,uBAAuB;;;;;;;;;;;;AAapC,MAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCnC,MAAa,kBAAkB"}
|