@open-mercato/shared 0.5.1-develop.2699.f8b50c8046 → 0.5.1-develop.2709.b6bdd776ac
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/.turbo/turbo-build.log +1 -1
- package/dist/lib/bootstrap/dynamicLoader.js +20 -4
- package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
- package/dist/lib/bootstrap/generatedCacheRecovery.js +121 -0
- package/dist/lib/bootstrap/generatedCacheRecovery.js.map +7 -0
- package/dist/lib/commands/registry.js +10 -0
- package/dist/lib/commands/registry.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/package.json +6 -1
- package/src/lib/bootstrap/__tests__/generatedCacheRecovery.test.ts +90 -0
- package/src/lib/bootstrap/dynamicLoader.ts +23 -6
- package/src/lib/bootstrap/generatedCacheRecovery.ts +174 -0
- package/src/lib/commands/__tests__/registry.test.ts +47 -0
- package/src/lib/commands/registry.ts +10 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:shared] found
|
|
1
|
+
[build:shared] found 202 entry points
|
|
2
2
|
[build:shared] built successfully
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { findAppRoot } from "./appResolver.js";
|
|
2
2
|
import { registerEntityIds } from "../encryption/entityIds.js";
|
|
3
|
+
import {
|
|
4
|
+
ensureMikroOrmV7GeneratedCacheCompatibility,
|
|
5
|
+
recoverMikroOrmV7GeneratedCacheFromImportError
|
|
6
|
+
} from "./generatedCacheRecovery.js";
|
|
3
7
|
import path from "node:path";
|
|
4
8
|
import fs from "node:fs";
|
|
5
9
|
import { pathToFileURL } from "node:url";
|
|
6
|
-
async function compileAndImport(tsPath) {
|
|
10
|
+
async function compileAndImport(tsPath, allowRecovery = true) {
|
|
7
11
|
const jsPath = tsPath.replace(/\.ts$/, ".mjs");
|
|
12
|
+
const appRoot = path.dirname(path.dirname(path.dirname(tsPath)));
|
|
8
13
|
const tsExists = fs.existsSync(tsPath);
|
|
9
14
|
const jsExists = fs.existsSync(jsPath);
|
|
10
15
|
if (!tsExists) {
|
|
@@ -13,7 +18,6 @@ async function compileAndImport(tsPath) {
|
|
|
13
18
|
const needsCompile = !jsExists || fs.statSync(tsPath).mtimeMs > fs.statSync(jsPath).mtimeMs;
|
|
14
19
|
if (needsCompile) {
|
|
15
20
|
const esbuild = await import("esbuild");
|
|
16
|
-
const appRoot = path.dirname(path.dirname(path.dirname(tsPath)));
|
|
17
21
|
const aliasPlugin = {
|
|
18
22
|
name: "alias-resolver",
|
|
19
23
|
setup(build) {
|
|
@@ -55,8 +59,19 @@ async function compileAndImport(tsPath) {
|
|
|
55
59
|
loader: { ".json": "json" }
|
|
56
60
|
});
|
|
57
61
|
}
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
try {
|
|
63
|
+
const fileUrl = `${pathToFileURL(jsPath).href}?mtime=${fs.statSync(jsPath).mtimeMs}`;
|
|
64
|
+
return import(fileUrl);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (!allowRecovery) {
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
const recovered = recoverMikroOrmV7GeneratedCacheFromImportError(appRoot, error);
|
|
70
|
+
if (!recovered.applied) {
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
return compileAndImport(tsPath, false);
|
|
74
|
+
}
|
|
60
75
|
}
|
|
61
76
|
async function loadBootstrapData(appRoot) {
|
|
62
77
|
const resolved = appRoot ? {
|
|
@@ -70,6 +85,7 @@ async function loadBootstrapData(appRoot) {
|
|
|
70
85
|
);
|
|
71
86
|
}
|
|
72
87
|
const { generatedDir } = resolved;
|
|
88
|
+
ensureMikroOrmV7GeneratedCacheCompatibility(resolved.appDir);
|
|
73
89
|
const entityIdsModule = await compileAndImport(path.join(generatedDir, "entities.ids.generated.ts"));
|
|
74
90
|
registerEntityIds(entityIdsModule.E);
|
|
75
91
|
const [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/bootstrap/dynamicLoader.ts"],
|
|
4
|
-
"sourcesContent": ["import type { BootstrapData } from './types'\nimport { findAppRoot, type AppRoot } from './appResolver'\nimport { registerEntityIds } from '../encryption/entityIds'\nimport path from 'node:path'\nimport fs from 'node:fs'\nimport { pathToFileURL } from 'node:url'\n\n/**\n * Compile a TypeScript file to JavaScript using esbuild bundler.\n * This bundles the file and all its dependencies, handling JSON imports properly.\n * The compiled file is written next to the source file with a .mjs extension.\n */\nasync function compileAndImport(tsPath: string): Promise<Record<string, unknown>> {\n const jsPath = tsPath.replace(/\\.ts$/, '.mjs')\n\n // Check if we need to recompile (source newer than compiled)\n const tsExists = fs.existsSync(tsPath)\n const jsExists = fs.existsSync(jsPath)\n\n if (!tsExists) {\n throw new Error(`Generated file not found: ${tsPath}`)\n }\n\n const needsCompile = !jsExists ||\n fs.statSync(tsPath).mtimeMs > fs.statSync(jsPath).mtimeMs\n\n if (needsCompile) {\n // Dynamically import esbuild only when needed\n const esbuild = await import('esbuild')\n\n //
|
|
5
|
-
"mappings": "AACA,SAAS,mBAAiC;AAC1C,SAAS,yBAAyB;AAClC,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,SAAS,qBAAqB;AAO9B,eAAe,iBAAiB,
|
|
4
|
+
"sourcesContent": ["import type { BootstrapData } from './types'\nimport { findAppRoot, type AppRoot } from './appResolver'\nimport { registerEntityIds } from '../encryption/entityIds'\nimport {\n ensureMikroOrmV7GeneratedCacheCompatibility,\n recoverMikroOrmV7GeneratedCacheFromImportError,\n} from './generatedCacheRecovery'\nimport path from 'node:path'\nimport fs from 'node:fs'\nimport { pathToFileURL } from 'node:url'\n\n/**\n * Compile a TypeScript file to JavaScript using esbuild bundler.\n * This bundles the file and all its dependencies, handling JSON imports properly.\n * The compiled file is written next to the source file with a .mjs extension.\n */\nasync function compileAndImport(tsPath: string, allowRecovery: boolean = true): Promise<Record<string, unknown>> {\n const jsPath = tsPath.replace(/\\.ts$/, '.mjs')\n const appRoot = path.dirname(path.dirname(path.dirname(tsPath)))\n\n // Check if we need to recompile (source newer than compiled)\n const tsExists = fs.existsSync(tsPath)\n const jsExists = fs.existsSync(jsPath)\n\n if (!tsExists) {\n throw new Error(`Generated file not found: ${tsPath}`)\n }\n\n const needsCompile = !jsExists ||\n fs.statSync(tsPath).mtimeMs > fs.statSync(jsPath).mtimeMs\n\n if (needsCompile) {\n // Dynamically import esbuild only when needed\n const esbuild = await import('esbuild')\n\n // Plugin to resolve @/ alias to app root (works for @app modules)\n const aliasPlugin: import('esbuild').Plugin = {\n name: 'alias-resolver',\n setup(build) {\n // Resolve @/ alias to app root\n build.onResolve({ filter: /^@\\// }, (args) => {\n const resolved = path.join(appRoot, args.path.slice(2))\n // Try with .ts extension if base path doesn't exist\n if (!fs.existsSync(resolved) && fs.existsSync(resolved + '.ts')) {\n return { path: resolved + '.ts' }\n }\n // Also check for /index.ts if it's a directory\n if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory() && fs.existsSync(path.join(resolved, 'index.ts'))) {\n return { path: path.join(resolved, 'index.ts') }\n }\n return { path: resolved }\n })\n },\n }\n\n // Plugin to mark non-JSON package imports as external\n const externalNonJsonPlugin: import('esbuild').Plugin = {\n name: 'external-non-json',\n setup(build) {\n // Mark all package imports as external EXCEPT JSON files\n // Filter matches paths that don't start with . or / (package imports like @open-mercato/shared)\n build.onResolve({ filter: /^[^./]/ }, (args) => {\n // Skip Windows absolute paths (e.g., C:\\...) - they're local files, not packages\n if (/^[a-zA-Z]:/.test(args.path)) {\n return null // Let esbuild handle it\n }\n // If it's a JSON file, let esbuild bundle it\n if (args.path.endsWith('.json')) {\n return null // Let esbuild handle it\n }\n // Otherwise mark as external\n return { path: args.path, external: true }\n })\n },\n }\n\n // Use esbuild.build with bundling to handle JSON imports\n await esbuild.build({\n entryPoints: [tsPath],\n outfile: jsPath,\n bundle: true,\n format: 'esm',\n platform: 'node',\n target: 'node18',\n plugins: [aliasPlugin, externalNonJsonPlugin],\n // Allow JSON imports\n loader: { '.json': 'json' },\n })\n }\n\n // Import the compiled JavaScript\n try {\n const fileUrl = `${pathToFileURL(jsPath).href}?mtime=${fs.statSync(jsPath).mtimeMs}`\n return import(fileUrl)\n } catch (error) {\n if (!allowRecovery) {\n throw error\n }\n\n const recovered = recoverMikroOrmV7GeneratedCacheFromImportError(appRoot, error)\n if (!recovered.applied) {\n throw error\n }\n\n return compileAndImport(tsPath, false)\n }\n}\n\n\n/**\n * Dynamically load bootstrap data from a resolved app directory.\n *\n * IMPORTANT: This only works in unbundled contexts (CLI, tsx).\n * Do NOT use this in Next.js bundled code - use static imports instead.\n *\n * For CLI context, we skip loading modules.generated.ts which has Next.js dependencies.\n * CLI commands are discovered separately via the CLI module system.\n *\n * @param appRoot - Optional explicit app root path. If not provided, will search from cwd.\n * @returns The loaded bootstrap data\n * @throws Error if app root cannot be found or generated files are missing\n */\nexport async function loadBootstrapData(appRoot?: string): Promise<BootstrapData> {\n const resolved: AppRoot | null = appRoot\n ? {\n generatedDir: path.join(appRoot, '.mercato', 'generated'),\n appDir: appRoot,\n mercatoDir: path.join(appRoot, '.mercato'),\n }\n : findAppRoot()\n\n if (!resolved) {\n throw new Error(\n 'Could not find app root with .mercato/generated directory. ' +\n 'Make sure you run this command from within a Next.js app directory, ' +\n 'or run \"yarn mercato generate\" first to create the generated files.',\n )\n }\n\n const { generatedDir } = resolved\n\n ensureMikroOrmV7GeneratedCacheCompatibility(resolved.appDir)\n\n // IMPORTANT: Load entity IDs FIRST and register them before loading modules.\n // This is because modules (e.g., ce.ts files) use E.xxx.xxx at module scope,\n // and they need entity IDs to be available when they're imported.\n const entityIdsModule = await compileAndImport(path.join(generatedDir, 'entities.ids.generated.ts'))\n registerEntityIds(entityIdsModule.E as BootstrapData['entityIds'])\n\n // Now load the rest of the generated files.\n // modules.cli.generated.ts excludes Next.js-dependent code (routes, APIs, widgets)\n const [\n modulesModule,\n entitiesModule,\n diModule,\n searchModule,\n ] = await Promise.all([\n compileAndImport(path.join(generatedDir, 'modules.cli.generated.ts')),\n compileAndImport(path.join(generatedDir, 'entities.generated.ts')),\n compileAndImport(path.join(generatedDir, 'di.generated.ts')),\n compileAndImport(path.join(generatedDir, 'search.generated.ts')).catch(() => ({ searchModuleConfigs: [] })),\n ])\n\n return {\n modules: modulesModule.modules as BootstrapData['modules'],\n entities: entitiesModule.entities as BootstrapData['entities'],\n diRegistrars: diModule.diRegistrars as BootstrapData['diRegistrars'],\n entityIds: entityIdsModule.E as BootstrapData['entityIds'],\n // Search configs are needed by workers for indexing\n searchModuleConfigs: (searchModule.searchModuleConfigs ?? []) as BootstrapData['searchModuleConfigs'],\n // Empty UI-related data - not needed for CLI\n dashboardWidgetEntries: [],\n injectionWidgetEntries: [],\n injectionTables: [],\n interceptorEntries: [],\n componentOverrideEntries: [],\n }\n}\n\n/**\n * Create and execute bootstrap in CLI context.\n *\n * This is a convenience function that finds the app root, loads the generated\n * data dynamically, and runs bootstrap. Use this in CLI entry points.\n *\n * Returns the loaded bootstrap data so the CLI can register modules directly\n * (avoids module resolution issues when importing @open-mercato/cli/mercato).\n *\n * @param appRoot - Optional explicit app root path\n * @returns The loaded bootstrap data (modules, entities, etc.)\n */\nexport async function bootstrapFromAppRoot(appRoot?: string): Promise<BootstrapData> {\n const { createBootstrap, waitForAsyncRegistration } = await import('./factory.js')\n const data = await loadBootstrapData(appRoot)\n const bootstrap = createBootstrap(data)\n bootstrap()\n // In CLI context, wait for async registrations (UI widgets, search configs, etc.)\n await waitForAsyncRegistration()\n\n return data\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const DECORATOR_EXPORT_NAMES = [
|
|
4
|
+
"Entity",
|
|
5
|
+
"PrimaryKey",
|
|
6
|
+
"Property",
|
|
7
|
+
"ManyToOne",
|
|
8
|
+
"OneToMany",
|
|
9
|
+
"OneToOne",
|
|
10
|
+
"ManyToMany",
|
|
11
|
+
"Enum",
|
|
12
|
+
"Index",
|
|
13
|
+
"Unique",
|
|
14
|
+
"Embeddable",
|
|
15
|
+
"Embedded",
|
|
16
|
+
"Formula"
|
|
17
|
+
];
|
|
18
|
+
const RECOVERY_VERSION = "mikro-orm-v7-generated-cache-recovery-v1";
|
|
19
|
+
const RECOVERY_MARKER_FILE = ".mikro-orm-v7-cache-recovery.json";
|
|
20
|
+
const GENERATED_DIR_SEGMENTS = [".mercato", "generated"];
|
|
21
|
+
const staleDecoratorImportPattern = new RegExp(
|
|
22
|
+
String.raw`import\s*\{[^}]*\b(?:${DECORATOR_EXPORT_NAMES.join("|")})\b[^}]*\}\s*from\s*['"]@mikro-orm/core['"]`,
|
|
23
|
+
"m"
|
|
24
|
+
);
|
|
25
|
+
const decoratorExportErrorPattern = /@mikro-orm\/core' does not provide an export named '(?:Entity|PrimaryKey|Property|ManyToOne|OneToMany|OneToOne|ManyToMany|Enum|Index|Unique|Embeddable|Embedded|Formula)'/;
|
|
26
|
+
function getGeneratedDir(appRoot) {
|
|
27
|
+
return path.join(appRoot, ...GENERATED_DIR_SEGMENTS);
|
|
28
|
+
}
|
|
29
|
+
function getRecoveryMarkerPath(appRoot) {
|
|
30
|
+
return path.join(getGeneratedDir(appRoot), RECOVERY_MARKER_FILE);
|
|
31
|
+
}
|
|
32
|
+
function walkFiles(dirPath) {
|
|
33
|
+
if (!fs.existsSync(dirPath)) return [];
|
|
34
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
35
|
+
const files = [];
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
const absolutePath = path.join(dirPath, entry.name);
|
|
38
|
+
if (entry.isDirectory()) {
|
|
39
|
+
files.push(...walkFiles(absolutePath));
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (entry.isFile()) {
|
|
43
|
+
files.push(absolutePath);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return files;
|
|
47
|
+
}
|
|
48
|
+
function listGeneratedCacheFiles(appRoot) {
|
|
49
|
+
return walkFiles(getGeneratedDir(appRoot)).filter((filePath) => filePath.endsWith(".mjs")).sort();
|
|
50
|
+
}
|
|
51
|
+
function findStaleGeneratedCacheFiles(appRoot) {
|
|
52
|
+
const generatedFiles = listGeneratedCacheFiles(appRoot);
|
|
53
|
+
return generatedFiles.filter((filePath) => {
|
|
54
|
+
const source = fs.readFileSync(filePath, "utf8");
|
|
55
|
+
return staleDecoratorImportPattern.test(source);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function writeRecoveryMarker(appRoot, marker) {
|
|
59
|
+
const markerPath = getRecoveryMarkerPath(appRoot);
|
|
60
|
+
fs.writeFileSync(markerPath, JSON.stringify(marker, null, 2));
|
|
61
|
+
return markerPath;
|
|
62
|
+
}
|
|
63
|
+
function logRecoveryMessage(logger, reason) {
|
|
64
|
+
const header = reason === "runtime-import-error" ? "\u26A0\uFE0F Open Mercato detected a stale generated cache while bootstrapping the app." : "\u26A0\uFE0F Open Mercato detected stale generated cache from the MikroORM v7 migration.";
|
|
65
|
+
logger.warn("");
|
|
66
|
+
logger.warn(header);
|
|
67
|
+
logger.warn("\u{1F4D8} Open Mercato migrated MikroORM to version 7. Please review UPGRADE_NOTES.md and the `migrate-mikro-orm` skill if your code still imports decorators from `@mikro-orm/core`.");
|
|
68
|
+
logger.warn("\u{1F9F9} Cleaning generated compilation cache and recompiling generated code now...");
|
|
69
|
+
logger.warn("");
|
|
70
|
+
}
|
|
71
|
+
function deleteGeneratedCacheFiles(filePaths) {
|
|
72
|
+
const deletedFiles = [];
|
|
73
|
+
for (const filePath of filePaths) {
|
|
74
|
+
if (!fs.existsSync(filePath)) continue;
|
|
75
|
+
fs.rmSync(filePath, { force: true });
|
|
76
|
+
deletedFiles.push(filePath);
|
|
77
|
+
}
|
|
78
|
+
return deletedFiles;
|
|
79
|
+
}
|
|
80
|
+
function applyGeneratedCacheRecovery(appRoot, staleFiles, reason, logger) {
|
|
81
|
+
if (staleFiles.length === 0) {
|
|
82
|
+
return { applied: false, deletedFiles: [], markerPath: null };
|
|
83
|
+
}
|
|
84
|
+
logRecoveryMessage(logger, reason);
|
|
85
|
+
const generatedCacheFiles = listGeneratedCacheFiles(appRoot);
|
|
86
|
+
const deletedFiles = deleteGeneratedCacheFiles(generatedCacheFiles);
|
|
87
|
+
const markerPath = writeRecoveryMarker(appRoot, {
|
|
88
|
+
version: RECOVERY_VERSION,
|
|
89
|
+
reason,
|
|
90
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
91
|
+
deletedFiles
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
applied: true,
|
|
95
|
+
deletedFiles,
|
|
96
|
+
markerPath
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function shouldRecoverMikroOrmV7GeneratedCache(error) {
|
|
100
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
101
|
+
return decoratorExportErrorPattern.test(message);
|
|
102
|
+
}
|
|
103
|
+
function ensureMikroOrmV7GeneratedCacheCompatibility(appRoot, options = {}) {
|
|
104
|
+
const logger = options.logger ?? console;
|
|
105
|
+
const staleFiles = findStaleGeneratedCacheFiles(appRoot);
|
|
106
|
+
return applyGeneratedCacheRecovery(appRoot, staleFiles, "stale-generated-cache-scan", logger);
|
|
107
|
+
}
|
|
108
|
+
function recoverMikroOrmV7GeneratedCacheFromImportError(appRoot, error, options = {}) {
|
|
109
|
+
if (!shouldRecoverMikroOrmV7GeneratedCache(error)) {
|
|
110
|
+
return { applied: false, deletedFiles: [], markerPath: null };
|
|
111
|
+
}
|
|
112
|
+
const logger = options.logger ?? console;
|
|
113
|
+
const staleFiles = findStaleGeneratedCacheFiles(appRoot);
|
|
114
|
+
return applyGeneratedCacheRecovery(appRoot, staleFiles, "runtime-import-error", logger);
|
|
115
|
+
}
|
|
116
|
+
export {
|
|
117
|
+
ensureMikroOrmV7GeneratedCacheCompatibility,
|
|
118
|
+
recoverMikroOrmV7GeneratedCacheFromImportError,
|
|
119
|
+
shouldRecoverMikroOrmV7GeneratedCache
|
|
120
|
+
};
|
|
121
|
+
//# sourceMappingURL=generatedCacheRecovery.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/bootstrap/generatedCacheRecovery.ts"],
|
|
4
|
+
"sourcesContent": ["import fs from 'node:fs'\nimport path from 'node:path'\n\nconst DECORATOR_EXPORT_NAMES = [\n 'Entity',\n 'PrimaryKey',\n 'Property',\n 'ManyToOne',\n 'OneToMany',\n 'OneToOne',\n 'ManyToMany',\n 'Enum',\n 'Index',\n 'Unique',\n 'Embeddable',\n 'Embedded',\n 'Formula',\n] as const\n\nconst RECOVERY_VERSION = 'mikro-orm-v7-generated-cache-recovery-v1'\nconst RECOVERY_MARKER_FILE = '.mikro-orm-v7-cache-recovery.json'\nconst GENERATED_DIR_SEGMENTS = ['.mercato', 'generated'] as const\n\nconst staleDecoratorImportPattern = new RegExp(\n String.raw`import\\s*\\{[^}]*\\b(?:${DECORATOR_EXPORT_NAMES.join('|')})\\b[^}]*\\}\\s*from\\s*['\"]@mikro-orm/core['\"]`,\n 'm',\n)\n\nconst decoratorExportErrorPattern = /@mikro-orm\\/core' does not provide an export named '(?:Entity|PrimaryKey|Property|ManyToOne|OneToMany|OneToOne|ManyToMany|Enum|Index|Unique|Embeddable|Embedded|Formula)'/\n\ntype RecoveryLogger = {\n warn: (message: string) => void\n}\n\ntype RecoveryReason = 'stale-generated-cache-scan' | 'runtime-import-error'\n\ntype RecoveryMarker = {\n version: string\n reason: RecoveryReason\n createdAt: string\n deletedFiles: string[]\n}\n\nexport type GeneratedCacheRecoveryResult = {\n applied: boolean\n deletedFiles: string[]\n markerPath: string | null\n}\n\nfunction getGeneratedDir(appRoot: string): string {\n return path.join(appRoot, ...GENERATED_DIR_SEGMENTS)\n}\n\nfunction getRecoveryMarkerPath(appRoot: string): string {\n return path.join(getGeneratedDir(appRoot), RECOVERY_MARKER_FILE)\n}\n\nfunction walkFiles(dirPath: string): string[] {\n if (!fs.existsSync(dirPath)) return []\n\n const entries = fs.readdirSync(dirPath, { withFileTypes: true })\n const files: string[] = []\n for (const entry of entries) {\n const absolutePath = path.join(dirPath, entry.name)\n if (entry.isDirectory()) {\n files.push(...walkFiles(absolutePath))\n continue\n }\n if (entry.isFile()) {\n files.push(absolutePath)\n }\n }\n return files\n}\n\nfunction listGeneratedCacheFiles(appRoot: string): string[] {\n return walkFiles(getGeneratedDir(appRoot))\n .filter((filePath) => filePath.endsWith('.mjs'))\n .sort()\n}\n\nfunction findStaleGeneratedCacheFiles(appRoot: string): string[] {\n const generatedFiles = listGeneratedCacheFiles(appRoot)\n return generatedFiles.filter((filePath) => {\n const source = fs.readFileSync(filePath, 'utf8')\n return staleDecoratorImportPattern.test(source)\n })\n}\n\nfunction writeRecoveryMarker(appRoot: string, marker: RecoveryMarker): string {\n const markerPath = getRecoveryMarkerPath(appRoot)\n fs.writeFileSync(markerPath, JSON.stringify(marker, null, 2))\n return markerPath\n}\n\nfunction logRecoveryMessage(logger: RecoveryLogger, reason: RecoveryReason): void {\n const header =\n reason === 'runtime-import-error'\n ? '\u26A0\uFE0F Open Mercato detected a stale generated cache while bootstrapping the app.'\n : '\u26A0\uFE0F Open Mercato detected stale generated cache from the MikroORM v7 migration.'\n\n logger.warn('')\n logger.warn(header)\n logger.warn('\uD83D\uDCD8 Open Mercato migrated MikroORM to version 7. Please review UPGRADE_NOTES.md and the `migrate-mikro-orm` skill if your code still imports decorators from `@mikro-orm/core`.')\n logger.warn('\uD83E\uDDF9 Cleaning generated compilation cache and recompiling generated code now...')\n logger.warn('')\n}\n\nfunction deleteGeneratedCacheFiles(filePaths: string[]): string[] {\n const deletedFiles: string[] = []\n for (const filePath of filePaths) {\n if (!fs.existsSync(filePath)) continue\n fs.rmSync(filePath, { force: true })\n deletedFiles.push(filePath)\n }\n return deletedFiles\n}\n\nfunction applyGeneratedCacheRecovery(\n appRoot: string,\n staleFiles: string[],\n reason: RecoveryReason,\n logger: RecoveryLogger,\n): GeneratedCacheRecoveryResult {\n if (staleFiles.length === 0) {\n return { applied: false, deletedFiles: [], markerPath: null }\n }\n\n logRecoveryMessage(logger, reason)\n\n const generatedCacheFiles = listGeneratedCacheFiles(appRoot)\n const deletedFiles = deleteGeneratedCacheFiles(generatedCacheFiles)\n const markerPath = writeRecoveryMarker(appRoot, {\n version: RECOVERY_VERSION,\n reason,\n createdAt: new Date().toISOString(),\n deletedFiles,\n })\n\n return {\n applied: true,\n deletedFiles,\n markerPath,\n }\n}\n\nexport function shouldRecoverMikroOrmV7GeneratedCache(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error)\n return decoratorExportErrorPattern.test(message)\n}\n\nexport function ensureMikroOrmV7GeneratedCacheCompatibility(\n appRoot: string,\n options: { logger?: RecoveryLogger } = {},\n): GeneratedCacheRecoveryResult {\n const logger = options.logger ?? console\n const staleFiles = findStaleGeneratedCacheFiles(appRoot)\n return applyGeneratedCacheRecovery(appRoot, staleFiles, 'stale-generated-cache-scan', logger)\n}\n\nexport function recoverMikroOrmV7GeneratedCacheFromImportError(\n appRoot: string,\n error: unknown,\n options: { logger?: RecoveryLogger } = {},\n): GeneratedCacheRecoveryResult {\n if (!shouldRecoverMikroOrmV7GeneratedCache(error)) {\n return { applied: false, deletedFiles: [], markerPath: null }\n }\n\n const logger = options.logger ?? console\n const staleFiles = findStaleGeneratedCacheFiles(appRoot)\n return applyGeneratedCacheRecovery(appRoot, staleFiles, 'runtime-import-error', logger)\n}\n\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
class CommandRegistry {
|
|
2
2
|
constructor() {
|
|
3
3
|
this.handlers = /* @__PURE__ */ new Map();
|
|
4
|
+
this.didWarnAboutDevelopmentReregistration = false;
|
|
4
5
|
}
|
|
5
6
|
register(handler) {
|
|
6
7
|
if (!handler?.id) throw new Error("Command handler must define an id");
|
|
7
8
|
if (this.handlers.has(handler.id)) {
|
|
9
|
+
if (process.env.NODE_ENV === "development") {
|
|
10
|
+
if (!this.didWarnAboutDevelopmentReregistration) {
|
|
11
|
+
console.debug("[Bootstrap] Commands re-registered (this may occur during HMR)");
|
|
12
|
+
this.didWarnAboutDevelopmentReregistration = true;
|
|
13
|
+
}
|
|
14
|
+
this.handlers.set(handler.id, handler);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
8
17
|
throw new Error(`Duplicate command registration for id ${handler.id}`);
|
|
9
18
|
}
|
|
10
19
|
this.handlers.set(handler.id, handler);
|
|
@@ -26,6 +35,7 @@ class CommandRegistry {
|
|
|
26
35
|
}
|
|
27
36
|
clear() {
|
|
28
37
|
this.handlers.clear();
|
|
38
|
+
this.didWarnAboutDevelopmentReregistration = false;
|
|
29
39
|
}
|
|
30
40
|
}
|
|
31
41
|
const commandRegistry = new CommandRegistry();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/commands/registry.ts"],
|
|
4
|
-
"sourcesContent": ["import type { CommandHandler } from './types'\n\nclass CommandRegistry {\n private handlers = new Map<string, CommandHandler>()\n\n register(handler: CommandHandler) {\n if (!handler?.id) throw new Error('Command handler must define an id')\n if (this.handlers.has(handler.id)) {\n throw new Error(`Duplicate command registration for id ${handler.id}`)\n }\n this.handlers.set(handler.id, handler)\n }\n\n unregister(id: string) {\n this.handlers.delete(id)\n }\n\n get<TInput = unknown, TResult = unknown>(id: string): CommandHandler<TInput, TResult> | null {\n return (this.handlers.get(id) as CommandHandler<TInput, TResult> | undefined) ?? null\n }\n\n has(id: string): boolean {\n return this.handlers.has(id)\n }\n\n /**\n * List all registered command handler IDs.\n */\n list(): string[] {\n return Array.from(this.handlers.keys())\n }\n\n clear() {\n this.handlers.clear()\n }\n}\n\nexport const commandRegistry = new CommandRegistry()\n\nexport function registerCommand(handler: CommandHandler) {\n commandRegistry.register(handler)\n}\n\nexport function unregisterCommand(id: string) {\n commandRegistry.unregister(id)\n}\n"],
|
|
5
|
-
"mappings": "AAEA,MAAM,gBAAgB;AAAA,EAAtB;AACE,SAAQ,WAAW,oBAAI,IAA4B;AAAA;AAAA,
|
|
4
|
+
"sourcesContent": ["import type { CommandHandler } from './types'\n\nclass CommandRegistry {\n private handlers = new Map<string, CommandHandler>()\n private didWarnAboutDevelopmentReregistration = false\n\n register(handler: CommandHandler) {\n if (!handler?.id) throw new Error('Command handler must define an id')\n if (this.handlers.has(handler.id)) {\n if (process.env.NODE_ENV === 'development') {\n if (!this.didWarnAboutDevelopmentReregistration) {\n console.debug('[Bootstrap] Commands re-registered (this may occur during HMR)')\n this.didWarnAboutDevelopmentReregistration = true\n }\n this.handlers.set(handler.id, handler)\n return\n }\n throw new Error(`Duplicate command registration for id ${handler.id}`)\n }\n this.handlers.set(handler.id, handler)\n }\n\n unregister(id: string) {\n this.handlers.delete(id)\n }\n\n get<TInput = unknown, TResult = unknown>(id: string): CommandHandler<TInput, TResult> | null {\n return (this.handlers.get(id) as CommandHandler<TInput, TResult> | undefined) ?? null\n }\n\n has(id: string): boolean {\n return this.handlers.has(id)\n }\n\n /**\n * List all registered command handler IDs.\n */\n list(): string[] {\n return Array.from(this.handlers.keys())\n }\n\n clear() {\n this.handlers.clear()\n this.didWarnAboutDevelopmentReregistration = false\n }\n}\n\nexport const commandRegistry = new CommandRegistry()\n\nexport function registerCommand(handler: CommandHandler) {\n commandRegistry.register(handler)\n}\n\nexport function unregisterCommand(id: string) {\n commandRegistry.unregister(id)\n}\n"],
|
|
5
|
+
"mappings": "AAEA,MAAM,gBAAgB;AAAA,EAAtB;AACE,SAAQ,WAAW,oBAAI,IAA4B;AACnD,SAAQ,wCAAwC;AAAA;AAAA,EAEhD,SAAS,SAAyB;AAChC,QAAI,CAAC,SAAS,GAAI,OAAM,IAAI,MAAM,mCAAmC;AACrE,QAAI,KAAK,SAAS,IAAI,QAAQ,EAAE,GAAG;AACjC,UAAI,QAAQ,IAAI,aAAa,eAAe;AAC1C,YAAI,CAAC,KAAK,uCAAuC;AAC/C,kBAAQ,MAAM,gEAAgE;AAC9E,eAAK,wCAAwC;AAAA,QAC/C;AACA,aAAK,SAAS,IAAI,QAAQ,IAAI,OAAO;AACrC;AAAA,MACF;AACA,YAAM,IAAI,MAAM,yCAAyC,QAAQ,EAAE,EAAE;AAAA,IACvE;AACA,SAAK,SAAS,IAAI,QAAQ,IAAI,OAAO;AAAA,EACvC;AAAA,EAEA,WAAW,IAAY;AACrB,SAAK,SAAS,OAAO,EAAE;AAAA,EACzB;AAAA,EAEA,IAAyC,IAAoD;AAC3F,WAAQ,KAAK,SAAS,IAAI,EAAE,KAAqD;AAAA,EACnF;AAAA,EAEA,IAAI,IAAqB;AACvB,WAAO,KAAK,SAAS,IAAI,EAAE;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA,EAKA,OAAiB;AACf,WAAO,MAAM,KAAK,KAAK,SAAS,KAAK,CAAC;AAAA,EACxC;AAAA,EAEA,QAAQ;AACN,SAAK,SAAS,MAAM;AACpB,SAAK,wCAAwC;AAAA,EAC/C;AACF;AAEO,MAAM,kBAAkB,IAAI,gBAAgB;AAE5C,SAAS,gBAAgB,SAAyB;AACvD,kBAAgB,SAAS,OAAO;AAClC;AAEO,SAAS,kBAAkB,IAAY;AAC5C,kBAAgB,WAAW,EAAE;AAC/B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/lib/version.js
CHANGED
package/dist/lib/version.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/version.ts"],
|
|
4
|
-
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.5.1-develop.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.5.1-develop.2709.b6bdd776ac'\nexport const appVersion = APP_VERSION\n"],
|
|
5
5
|
"mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/shared",
|
|
3
|
-
"version": "0.5.1-develop.
|
|
3
|
+
"version": "0.5.1-develop.2709.b6bdd776ac",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -89,7 +89,12 @@
|
|
|
89
89
|
}
|
|
90
90
|
},
|
|
91
91
|
"dependencies": {
|
|
92
|
+
"@mikro-orm/core": "^7.0.10",
|
|
93
|
+
"@mikro-orm/decorators": "^7.0.10",
|
|
94
|
+
"@mikro-orm/postgresql": "^7.0.10",
|
|
95
|
+
"dotenv": "^17.4.2",
|
|
92
96
|
"rate-limiter-flexible": "^11.0.1",
|
|
97
|
+
"reflect-metadata": "^0.2.2",
|
|
93
98
|
"sanitize-html": "^2.17.2"
|
|
94
99
|
},
|
|
95
100
|
"devDependencies": {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ensureMikroOrmV7GeneratedCacheCompatibility,
|
|
7
|
+
recoverMikroOrmV7GeneratedCacheFromImportError,
|
|
8
|
+
shouldRecoverMikroOrmV7GeneratedCache,
|
|
9
|
+
} from '../generatedCacheRecovery'
|
|
10
|
+
|
|
11
|
+
function createAppRoot(tempDir: string): { appRoot: string; generatedDir: string } {
|
|
12
|
+
const appRoot = path.join(tempDir, 'apps', 'mercato')
|
|
13
|
+
const generatedDir = path.join(appRoot, '.mercato', 'generated')
|
|
14
|
+
fs.mkdirSync(generatedDir, { recursive: true })
|
|
15
|
+
return { appRoot, generatedDir }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('generatedCacheRecovery', () => {
|
|
19
|
+
let tempDir: string
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'open-mercato-generated-cache-recovery-'))
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('removes stale generated .mjs bundles and writes a marker file', () => {
|
|
30
|
+
const { appRoot, generatedDir } = createAppRoot(tempDir)
|
|
31
|
+
const stalePath = path.join(generatedDir, 'entities.generated.mjs')
|
|
32
|
+
const companionPath = path.join(generatedDir, 'modules.cli.generated.mjs')
|
|
33
|
+
const warnings: string[] = []
|
|
34
|
+
|
|
35
|
+
fs.writeFileSync(stalePath, 'import { Entity, PrimaryKey, Property } from "@mikro-orm/core";\n')
|
|
36
|
+
fs.writeFileSync(companionPath, 'export const modules = []\n')
|
|
37
|
+
|
|
38
|
+
const result = ensureMikroOrmV7GeneratedCacheCompatibility(appRoot, {
|
|
39
|
+
logger: { warn: (message) => warnings.push(message) },
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
expect(result.applied).toBe(true)
|
|
43
|
+
expect(fs.existsSync(stalePath)).toBe(false)
|
|
44
|
+
expect(fs.existsSync(companionPath)).toBe(false)
|
|
45
|
+
expect(result.markerPath).not.toBeNull()
|
|
46
|
+
expect(result.deletedFiles).toHaveLength(2)
|
|
47
|
+
expect(result.deletedFiles).toEqual(expect.arrayContaining([companionPath, stalePath]))
|
|
48
|
+
expect(fs.existsSync(result.markerPath!)).toBe(true)
|
|
49
|
+
expect(warnings.some((message) => message.includes('MikroORM to version 7'))).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('does nothing when the generated cache is already compatible', () => {
|
|
53
|
+
const { appRoot, generatedDir } = createAppRoot(tempDir)
|
|
54
|
+
const cleanPath = path.join(generatedDir, 'entities.generated.mjs')
|
|
55
|
+
|
|
56
|
+
fs.writeFileSync(cleanPath, 'import { Entity } from "@mikro-orm/decorators/legacy";\n')
|
|
57
|
+
|
|
58
|
+
const result = ensureMikroOrmV7GeneratedCacheCompatibility(appRoot)
|
|
59
|
+
|
|
60
|
+
expect(result.applied).toBe(false)
|
|
61
|
+
expect(result.deletedFiles).toEqual([])
|
|
62
|
+
expect(result.markerPath).toBeNull()
|
|
63
|
+
expect(fs.existsSync(cleanPath)).toBe(true)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('retries recovery when the import error matches the MikroORM decorator export failure', () => {
|
|
67
|
+
const { appRoot, generatedDir } = createAppRoot(tempDir)
|
|
68
|
+
const stalePath = path.join(generatedDir, 'entities.generated.mjs')
|
|
69
|
+
|
|
70
|
+
fs.writeFileSync(stalePath, 'import { Entity, PrimaryKey, Property } from "@mikro-orm/core";\n')
|
|
71
|
+
|
|
72
|
+
const result = recoverMikroOrmV7GeneratedCacheFromImportError(
|
|
73
|
+
appRoot,
|
|
74
|
+
new SyntaxError("The requested module '@mikro-orm/core' does not provide an export named 'Entity'"),
|
|
75
|
+
{ logger: { warn: () => undefined } },
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
expect(result.applied).toBe(true)
|
|
79
|
+
expect(fs.existsSync(stalePath)).toBe(false)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('recognizes the MikroORM v7 decorator export error signature', () => {
|
|
83
|
+
expect(
|
|
84
|
+
shouldRecoverMikroOrmV7GeneratedCache(
|
|
85
|
+
new Error("The requested module '@mikro-orm/core' does not provide an export named 'Entity'"),
|
|
86
|
+
),
|
|
87
|
+
).toBe(true)
|
|
88
|
+
expect(shouldRecoverMikroOrmV7GeneratedCache(new Error('Some other bootstrap error'))).toBe(false)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { BootstrapData } from './types'
|
|
2
2
|
import { findAppRoot, type AppRoot } from './appResolver'
|
|
3
3
|
import { registerEntityIds } from '../encryption/entityIds'
|
|
4
|
+
import {
|
|
5
|
+
ensureMikroOrmV7GeneratedCacheCompatibility,
|
|
6
|
+
recoverMikroOrmV7GeneratedCacheFromImportError,
|
|
7
|
+
} from './generatedCacheRecovery'
|
|
4
8
|
import path from 'node:path'
|
|
5
9
|
import fs from 'node:fs'
|
|
6
10
|
import { pathToFileURL } from 'node:url'
|
|
@@ -10,8 +14,9 @@ import { pathToFileURL } from 'node:url'
|
|
|
10
14
|
* This bundles the file and all its dependencies, handling JSON imports properly.
|
|
11
15
|
* The compiled file is written next to the source file with a .mjs extension.
|
|
12
16
|
*/
|
|
13
|
-
async function compileAndImport(tsPath: string): Promise<Record<string, unknown>> {
|
|
17
|
+
async function compileAndImport(tsPath: string, allowRecovery: boolean = true): Promise<Record<string, unknown>> {
|
|
14
18
|
const jsPath = tsPath.replace(/\.ts$/, '.mjs')
|
|
19
|
+
const appRoot = path.dirname(path.dirname(path.dirname(tsPath)))
|
|
15
20
|
|
|
16
21
|
// Check if we need to recompile (source newer than compiled)
|
|
17
22
|
const tsExists = fs.existsSync(tsPath)
|
|
@@ -28,9 +33,6 @@ async function compileAndImport(tsPath: string): Promise<Record<string, unknown>
|
|
|
28
33
|
// Dynamically import esbuild only when needed
|
|
29
34
|
const esbuild = await import('esbuild')
|
|
30
35
|
|
|
31
|
-
// The app root is 2 levels up from .mercato/generated/
|
|
32
|
-
const appRoot = path.dirname(path.dirname(path.dirname(tsPath)))
|
|
33
|
-
|
|
34
36
|
// Plugin to resolve @/ alias to app root (works for @app modules)
|
|
35
37
|
const aliasPlugin: import('esbuild').Plugin = {
|
|
36
38
|
name: 'alias-resolver',
|
|
@@ -87,8 +89,21 @@ async function compileAndImport(tsPath: string): Promise<Record<string, unknown>
|
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
// Import the compiled JavaScript
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
try {
|
|
93
|
+
const fileUrl = `${pathToFileURL(jsPath).href}?mtime=${fs.statSync(jsPath).mtimeMs}`
|
|
94
|
+
return import(fileUrl)
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (!allowRecovery) {
|
|
97
|
+
throw error
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const recovered = recoverMikroOrmV7GeneratedCacheFromImportError(appRoot, error)
|
|
101
|
+
if (!recovered.applied) {
|
|
102
|
+
throw error
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return compileAndImport(tsPath, false)
|
|
106
|
+
}
|
|
92
107
|
}
|
|
93
108
|
|
|
94
109
|
|
|
@@ -124,6 +139,8 @@ export async function loadBootstrapData(appRoot?: string): Promise<BootstrapData
|
|
|
124
139
|
|
|
125
140
|
const { generatedDir } = resolved
|
|
126
141
|
|
|
142
|
+
ensureMikroOrmV7GeneratedCacheCompatibility(resolved.appDir)
|
|
143
|
+
|
|
127
144
|
// IMPORTANT: Load entity IDs FIRST and register them before loading modules.
|
|
128
145
|
// This is because modules (e.g., ce.ts files) use E.xxx.xxx at module scope,
|
|
129
146
|
// and they need entity IDs to be available when they're imported.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
const DECORATOR_EXPORT_NAMES = [
|
|
5
|
+
'Entity',
|
|
6
|
+
'PrimaryKey',
|
|
7
|
+
'Property',
|
|
8
|
+
'ManyToOne',
|
|
9
|
+
'OneToMany',
|
|
10
|
+
'OneToOne',
|
|
11
|
+
'ManyToMany',
|
|
12
|
+
'Enum',
|
|
13
|
+
'Index',
|
|
14
|
+
'Unique',
|
|
15
|
+
'Embeddable',
|
|
16
|
+
'Embedded',
|
|
17
|
+
'Formula',
|
|
18
|
+
] as const
|
|
19
|
+
|
|
20
|
+
const RECOVERY_VERSION = 'mikro-orm-v7-generated-cache-recovery-v1'
|
|
21
|
+
const RECOVERY_MARKER_FILE = '.mikro-orm-v7-cache-recovery.json'
|
|
22
|
+
const GENERATED_DIR_SEGMENTS = ['.mercato', 'generated'] as const
|
|
23
|
+
|
|
24
|
+
const staleDecoratorImportPattern = new RegExp(
|
|
25
|
+
String.raw`import\s*\{[^}]*\b(?:${DECORATOR_EXPORT_NAMES.join('|')})\b[^}]*\}\s*from\s*['"]@mikro-orm/core['"]`,
|
|
26
|
+
'm',
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const decoratorExportErrorPattern = /@mikro-orm\/core' does not provide an export named '(?:Entity|PrimaryKey|Property|ManyToOne|OneToMany|OneToOne|ManyToMany|Enum|Index|Unique|Embeddable|Embedded|Formula)'/
|
|
30
|
+
|
|
31
|
+
type RecoveryLogger = {
|
|
32
|
+
warn: (message: string) => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type RecoveryReason = 'stale-generated-cache-scan' | 'runtime-import-error'
|
|
36
|
+
|
|
37
|
+
type RecoveryMarker = {
|
|
38
|
+
version: string
|
|
39
|
+
reason: RecoveryReason
|
|
40
|
+
createdAt: string
|
|
41
|
+
deletedFiles: string[]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type GeneratedCacheRecoveryResult = {
|
|
45
|
+
applied: boolean
|
|
46
|
+
deletedFiles: string[]
|
|
47
|
+
markerPath: string | null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getGeneratedDir(appRoot: string): string {
|
|
51
|
+
return path.join(appRoot, ...GENERATED_DIR_SEGMENTS)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getRecoveryMarkerPath(appRoot: string): string {
|
|
55
|
+
return path.join(getGeneratedDir(appRoot), RECOVERY_MARKER_FILE)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function walkFiles(dirPath: string): string[] {
|
|
59
|
+
if (!fs.existsSync(dirPath)) return []
|
|
60
|
+
|
|
61
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true })
|
|
62
|
+
const files: string[] = []
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
const absolutePath = path.join(dirPath, entry.name)
|
|
65
|
+
if (entry.isDirectory()) {
|
|
66
|
+
files.push(...walkFiles(absolutePath))
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
if (entry.isFile()) {
|
|
70
|
+
files.push(absolutePath)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return files
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function listGeneratedCacheFiles(appRoot: string): string[] {
|
|
77
|
+
return walkFiles(getGeneratedDir(appRoot))
|
|
78
|
+
.filter((filePath) => filePath.endsWith('.mjs'))
|
|
79
|
+
.sort()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function findStaleGeneratedCacheFiles(appRoot: string): string[] {
|
|
83
|
+
const generatedFiles = listGeneratedCacheFiles(appRoot)
|
|
84
|
+
return generatedFiles.filter((filePath) => {
|
|
85
|
+
const source = fs.readFileSync(filePath, 'utf8')
|
|
86
|
+
return staleDecoratorImportPattern.test(source)
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function writeRecoveryMarker(appRoot: string, marker: RecoveryMarker): string {
|
|
91
|
+
const markerPath = getRecoveryMarkerPath(appRoot)
|
|
92
|
+
fs.writeFileSync(markerPath, JSON.stringify(marker, null, 2))
|
|
93
|
+
return markerPath
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function logRecoveryMessage(logger: RecoveryLogger, reason: RecoveryReason): void {
|
|
97
|
+
const header =
|
|
98
|
+
reason === 'runtime-import-error'
|
|
99
|
+
? '⚠️ Open Mercato detected a stale generated cache while bootstrapping the app.'
|
|
100
|
+
: '⚠️ Open Mercato detected stale generated cache from the MikroORM v7 migration.'
|
|
101
|
+
|
|
102
|
+
logger.warn('')
|
|
103
|
+
logger.warn(header)
|
|
104
|
+
logger.warn('📘 Open Mercato migrated MikroORM to version 7. Please review UPGRADE_NOTES.md and the `migrate-mikro-orm` skill if your code still imports decorators from `@mikro-orm/core`.')
|
|
105
|
+
logger.warn('🧹 Cleaning generated compilation cache and recompiling generated code now...')
|
|
106
|
+
logger.warn('')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function deleteGeneratedCacheFiles(filePaths: string[]): string[] {
|
|
110
|
+
const deletedFiles: string[] = []
|
|
111
|
+
for (const filePath of filePaths) {
|
|
112
|
+
if (!fs.existsSync(filePath)) continue
|
|
113
|
+
fs.rmSync(filePath, { force: true })
|
|
114
|
+
deletedFiles.push(filePath)
|
|
115
|
+
}
|
|
116
|
+
return deletedFiles
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function applyGeneratedCacheRecovery(
|
|
120
|
+
appRoot: string,
|
|
121
|
+
staleFiles: string[],
|
|
122
|
+
reason: RecoveryReason,
|
|
123
|
+
logger: RecoveryLogger,
|
|
124
|
+
): GeneratedCacheRecoveryResult {
|
|
125
|
+
if (staleFiles.length === 0) {
|
|
126
|
+
return { applied: false, deletedFiles: [], markerPath: null }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
logRecoveryMessage(logger, reason)
|
|
130
|
+
|
|
131
|
+
const generatedCacheFiles = listGeneratedCacheFiles(appRoot)
|
|
132
|
+
const deletedFiles = deleteGeneratedCacheFiles(generatedCacheFiles)
|
|
133
|
+
const markerPath = writeRecoveryMarker(appRoot, {
|
|
134
|
+
version: RECOVERY_VERSION,
|
|
135
|
+
reason,
|
|
136
|
+
createdAt: new Date().toISOString(),
|
|
137
|
+
deletedFiles,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
applied: true,
|
|
142
|
+
deletedFiles,
|
|
143
|
+
markerPath,
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function shouldRecoverMikroOrmV7GeneratedCache(error: unknown): boolean {
|
|
148
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
149
|
+
return decoratorExportErrorPattern.test(message)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function ensureMikroOrmV7GeneratedCacheCompatibility(
|
|
153
|
+
appRoot: string,
|
|
154
|
+
options: { logger?: RecoveryLogger } = {},
|
|
155
|
+
): GeneratedCacheRecoveryResult {
|
|
156
|
+
const logger = options.logger ?? console
|
|
157
|
+
const staleFiles = findStaleGeneratedCacheFiles(appRoot)
|
|
158
|
+
return applyGeneratedCacheRecovery(appRoot, staleFiles, 'stale-generated-cache-scan', logger)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function recoverMikroOrmV7GeneratedCacheFromImportError(
|
|
162
|
+
appRoot: string,
|
|
163
|
+
error: unknown,
|
|
164
|
+
options: { logger?: RecoveryLogger } = {},
|
|
165
|
+
): GeneratedCacheRecoveryResult {
|
|
166
|
+
if (!shouldRecoverMikroOrmV7GeneratedCache(error)) {
|
|
167
|
+
return { applied: false, deletedFiles: [], markerPath: null }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const logger = options.logger ?? console
|
|
171
|
+
const staleFiles = findStaleGeneratedCacheFiles(appRoot)
|
|
172
|
+
return applyGeneratedCacheRecovery(appRoot, staleFiles, 'runtime-import-error', logger)
|
|
173
|
+
}
|
|
174
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { commandRegistry, registerCommand } from '@open-mercato/shared/lib/commands'
|
|
2
|
+
|
|
3
|
+
describe('command registry registration', () => {
|
|
4
|
+
const originalNodeEnv = process.env.NODE_ENV
|
|
5
|
+
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
commandRegistry.clear()
|
|
8
|
+
process.env.NODE_ENV = originalNodeEnv
|
|
9
|
+
jest.restoreAllMocks()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('throws on duplicate command ids outside development', () => {
|
|
13
|
+
process.env.NODE_ENV = 'test'
|
|
14
|
+
|
|
15
|
+
registerCommand({
|
|
16
|
+
id: 'test.command.duplicate',
|
|
17
|
+
execute: jest.fn(),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
expect(() =>
|
|
21
|
+
registerCommand({
|
|
22
|
+
id: 'test.command.duplicate',
|
|
23
|
+
execute: jest.fn(),
|
|
24
|
+
})
|
|
25
|
+
).toThrow('Duplicate command registration for id test.command.duplicate')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('overwrites duplicate command ids in development to tolerate HMR re-evaluation', () => {
|
|
29
|
+
process.env.NODE_ENV = 'development'
|
|
30
|
+
const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {})
|
|
31
|
+
const firstExecute = jest.fn(async () => 'first')
|
|
32
|
+
const secondExecute = jest.fn(async () => 'second')
|
|
33
|
+
|
|
34
|
+
registerCommand({
|
|
35
|
+
id: 'test.command.hmr',
|
|
36
|
+
execute: firstExecute,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
registerCommand({
|
|
40
|
+
id: 'test.command.hmr',
|
|
41
|
+
execute: secondExecute,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
expect(commandRegistry.get('test.command.hmr')?.execute).toBe(secondExecute)
|
|
45
|
+
expect(debugSpy).toHaveBeenCalledWith('[Bootstrap] Commands re-registered (this may occur during HMR)')
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -2,10 +2,19 @@ import type { CommandHandler } from './types'
|
|
|
2
2
|
|
|
3
3
|
class CommandRegistry {
|
|
4
4
|
private handlers = new Map<string, CommandHandler>()
|
|
5
|
+
private didWarnAboutDevelopmentReregistration = false
|
|
5
6
|
|
|
6
7
|
register(handler: CommandHandler) {
|
|
7
8
|
if (!handler?.id) throw new Error('Command handler must define an id')
|
|
8
9
|
if (this.handlers.has(handler.id)) {
|
|
10
|
+
if (process.env.NODE_ENV === 'development') {
|
|
11
|
+
if (!this.didWarnAboutDevelopmentReregistration) {
|
|
12
|
+
console.debug('[Bootstrap] Commands re-registered (this may occur during HMR)')
|
|
13
|
+
this.didWarnAboutDevelopmentReregistration = true
|
|
14
|
+
}
|
|
15
|
+
this.handlers.set(handler.id, handler)
|
|
16
|
+
return
|
|
17
|
+
}
|
|
9
18
|
throw new Error(`Duplicate command registration for id ${handler.id}`)
|
|
10
19
|
}
|
|
11
20
|
this.handlers.set(handler.id, handler)
|
|
@@ -32,6 +41,7 @@ class CommandRegistry {
|
|
|
32
41
|
|
|
33
42
|
clear() {
|
|
34
43
|
this.handlers.clear()
|
|
44
|
+
this.didWarnAboutDevelopmentReregistration = false
|
|
35
45
|
}
|
|
36
46
|
}
|
|
37
47
|
|