@izumisy-tailor/omakase-modules 0.1.0 → 0.3.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.
@@ -0,0 +1,91 @@
1
+ import type { TailorDBType } from "@tailor-platform/sdk";
2
+
3
+ type ModuleBuilderProps<C extends Record<string, unknown>> = {
4
+ config: C;
5
+ };
6
+
7
+ export type ConfiguredModule<
8
+ C extends Record<string, unknown> = Record<string, unknown>
9
+ > = {
10
+ packageName: string;
11
+ moduleProps: ModuleBuilderProps<C>;
12
+ };
13
+
14
+ export type DefinedModule<
15
+ C extends Record<string, unknown>,
16
+ Tables extends Record<string, unknown> = Record<string, unknown>
17
+ > = {
18
+ packageName: string;
19
+ configure: (props: ModuleBuilderProps<C>) => ConfiguredModule<C>;
20
+ /**
21
+ * @internal Type-only hook so that table shapes flow through helper utilities.
22
+ */
23
+ readonly __tablesBrand?: Tables;
24
+ };
25
+
26
+ export const defineModule = <
27
+ C extends Record<string, unknown>,
28
+ Tables extends Record<string, unknown> = Record<string, unknown>
29
+ >(baseProps: {
30
+ /**
31
+ * The package name of the module.
32
+ *
33
+ * This is required to be the same as the name field in package.json
34
+ */
35
+ packageName: string;
36
+ }): DefinedModule<C, Tables> => {
37
+ return {
38
+ packageName: baseProps.packageName,
39
+ configure: (props) => {
40
+ return {
41
+ packageName: baseProps.packageName,
42
+ moduleProps: props,
43
+ };
44
+ },
45
+ };
46
+ };
47
+
48
+ export type ModuleDependency<T extends DefinedModule<any, any>> =
49
+ T extends DefinedModule<infer C, infer Tables>
50
+ ? ConfiguredModule<C> & {
51
+ readonly __tablesBrand?: Tables;
52
+ }
53
+ : never;
54
+
55
+ // ============================================================================
56
+ // Utility Types for Module Development
57
+ // ============================================================================
58
+
59
+ /**
60
+ * Derives a tables type from a tableNames array.
61
+ * Use this to avoid manually defining a separate Tables type.
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * export const tableNames = ["product", "category"] as const;
66
+ * type MyTables = TablesFromNames<typeof tableNames>;
67
+ * // Result: { product: TailorDBType; category: TailorDBType }
68
+ * ```
69
+ */
70
+ export type TablesFromNames<T extends readonly string[]> = {
71
+ [K in T[number]]: TailorDBType;
72
+ };
73
+
74
+ /**
75
+ * Context passed to table builder functions.
76
+ * Provides access to the module's configuration.
77
+ *
78
+ * @example
79
+ * ```typescript
80
+ * export const buildProductTable = (
81
+ * { config }: ModuleFactoryContext<ModuleConfig>,
82
+ * deps: { category: TailorDBType }
83
+ * ) => {
84
+ * const prefix = config.dataModel?.product?.docNumberPrefix ?? "PROD";
85
+ * return db.type("Product", { ... });
86
+ * };
87
+ * ```
88
+ */
89
+ export type ModuleFactoryContext<C extends Record<string, unknown>> = {
90
+ config: C;
91
+ };
@@ -0,0 +1,9 @@
1
+ export {
2
+ defineModule,
3
+ type ConfiguredModule,
4
+ type DefinedModule,
5
+ type ModuleDependency,
6
+ type TablesFromNames,
7
+ type ModuleFactoryContext,
8
+ } from "./helpers";
9
+ export { withModuleConfiguration } from "./register";
@@ -0,0 +1,50 @@
1
+ import type { LoadedModules } from "../config/module-loader";
2
+ import type { DefinedModule } from "./helpers";
3
+
4
+ type ModuleFactoryContext<C extends Record<string, unknown>> = {
5
+ config: C;
6
+ };
7
+
8
+ /**
9
+ * Define module exports that depend on configuration from loadModules.
10
+ *
11
+ * This function returns a factory function that takes LoadedModules as input
12
+ * and produces the configured exports. The wrapper files generated by
13
+ * getModulesReference will call this factory with the app's loadModules result.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * // In module's tailordb/index.ts
18
+ * export default withModuleConfiguration(moduleDef, (context) => {
19
+ * const inventory = buildInventoryTable(context);
20
+ * return { inventory };
21
+ * });
22
+ *
23
+ * // In module's executors that need dependency module tables:
24
+ * export default withModuleConfiguration(moduleDef, async (context, loadedModules) => {
25
+ * const { productVariant } = await loadedModules.getTables(commerceModuleTables);
26
+ * return createExecutor({ ... });
27
+ * });
28
+ *
29
+ * // The wrapper file will call it like:
30
+ * // import factory from "module/backend/tailordb";
31
+ * // import modules from "../../modules";
32
+ * // export default await factory(modules);
33
+ * ```
34
+ */
35
+ export const withModuleConfiguration = <
36
+ C extends Record<string, unknown>,
37
+ Tables extends Record<string, unknown>,
38
+ Result
39
+ >(
40
+ module: DefinedModule<C, Tables>,
41
+ factory: (
42
+ context: ModuleFactoryContext<C>,
43
+ loadedModules: LoadedModules
44
+ ) => Result | Promise<Result>
45
+ ): ((loadedModules: LoadedModules) => Promise<Result>) => {
46
+ return async (loadedModules: LoadedModules) => {
47
+ const moduleState = loadedModules.loadConfig<C>(module);
48
+ return await factory(moduleState, loadedModules);
49
+ };
50
+ };
@@ -0,0 +1 @@
1
+ export { loadModules, ModuleLoader, type LoadedModules } from "./module-loader";
@@ -0,0 +1,93 @@
1
+ import { ConfiguredModule } from "../builder/helpers";
2
+
3
+ /**
4
+ * Module loader
5
+ * Builder for registering modules within loadModules
6
+ */
7
+ export class ModuleLoader {
8
+ private modules: Array<ConfiguredModule<any>> = [];
9
+
10
+ /**
11
+ * Add a module to the loader
12
+ * @param module Configured module
13
+ * @returns The added module (can be used as a dependency for other modules)
14
+ */
15
+ add<C extends Record<string, unknown>>(
16
+ module: ConfiguredModule<C>
17
+ ): ConfiguredModule<C> {
18
+ this.modules.push(module);
19
+ return module;
20
+ }
21
+
22
+ /**
23
+ * Get all registered modules
24
+ * @internal
25
+ */
26
+ _getModules(): Array<ConfiguredModule<any>> {
27
+ return this.modules;
28
+ }
29
+ }
30
+
31
+ export type LoadedModules = {
32
+ loadedModules: Record<string, ConfiguredModule<any>>;
33
+ loadConfig: <C extends Record<string, unknown>>(module: {
34
+ packageName: string;
35
+ }) => {
36
+ config: C;
37
+ };
38
+ /**
39
+ * Get the tables from a dependency module by calling its factory function.
40
+ * This is used when an executor or resolver needs to reference tables from another module.
41
+ *
42
+ * @param factory The factory function exported by the dependency module's tailordb/index.ts
43
+ * @returns The tables created by the factory
44
+ */
45
+ getTables: <T>(
46
+ factory: (loadedModules: LoadedModules) => Promise<T>
47
+ ) => Promise<T>;
48
+ };
49
+
50
+ /**
51
+ * Load modules with configuration
52
+ * @param configurator Function that receives a loader, registers modules, and returns the loader
53
+ * @returns Loaded modules and loadConfig function
54
+ */
55
+ export const loadModules = (
56
+ configurator: (loader: ModuleLoader) => ModuleLoader
57
+ ): LoadedModules => {
58
+ const emptyLoader = new ModuleLoader();
59
+ const modules = configurator(emptyLoader)._getModules();
60
+
61
+ const loadedModules = modules.reduce<Record<string, ConfiguredModule<any>>>(
62
+ (acc, module) => {
63
+ acc[module.packageName] = module;
64
+ return acc;
65
+ },
66
+ {}
67
+ );
68
+
69
+ const loadedModulesResult: LoadedModules = {
70
+ loadedModules,
71
+ loadConfig: <C extends Record<string, unknown>>(module: {
72
+ packageName: string;
73
+ }) => {
74
+ const loadedModule = loadedModules[module.packageName];
75
+ if (!loadedModule) {
76
+ throw new Error(
77
+ `Module "${module.packageName}" has not been configured. Ensure it is added via loadModules.`
78
+ );
79
+ }
80
+
81
+ return {
82
+ config: loadedModule.moduleProps.config as C,
83
+ };
84
+ },
85
+ getTables: async <T>(
86
+ factory: (loadedModules: LoadedModules) => Promise<T>
87
+ ): Promise<T> => {
88
+ return factory(loadedModulesResult);
89
+ },
90
+ };
91
+
92
+ return loadedModulesResult;
93
+ };
@@ -0,0 +1 @@
1
+ export { getModulesReference } from "./paths";
@@ -0,0 +1,49 @@
1
+ import path from "node:path";
2
+ import { LoadedModules } from "../module-loader";
3
+ import { generateWrapperFiles, OMAKASE_WRAPPER_DIR } from "./wrapper/generator";
4
+
5
+ export type GetModulesReferenceOptions = {
6
+ /**
7
+ * Base path for the application (defaults to process.cwd())
8
+ */
9
+ basePath?: string;
10
+ /**
11
+ * Whether to suppress log output (defaults to false)
12
+ */
13
+ silent?: boolean;
14
+ };
15
+
16
+ export const getModulesReference = async (
17
+ loadedModules: LoadedModules,
18
+ options: GetModulesReferenceOptions = {}
19
+ ) => {
20
+ const { basePath = process.cwd(), silent = false } = options;
21
+
22
+ // Log loaded modules information
23
+ const modulePackageNames = Object.keys(loadedModules.loadedModules);
24
+ if (!silent) {
25
+ console.log(`[omakase] Loaded ${modulePackageNames.length} module(s):\n`);
26
+ for (const name of modulePackageNames) {
27
+ console.log(` * ${name}`);
28
+ }
29
+ console.log("");
30
+ }
31
+
32
+ // Generate wrapper files and return paths to them
33
+ const wrapperPaths = await generateWrapperFiles(loadedModules, basePath);
34
+
35
+ return {
36
+ tailordb:
37
+ wrapperPaths.tailordb.length > 0
38
+ ? [path.join(OMAKASE_WRAPPER_DIR, "*", "tailordb", "*.ts")]
39
+ : [],
40
+ resolver:
41
+ wrapperPaths.resolver.length > 0
42
+ ? [path.join(OMAKASE_WRAPPER_DIR, "*", "resolvers", "*.ts")]
43
+ : [],
44
+ executor:
45
+ wrapperPaths.executor.length > 0
46
+ ? [path.join(OMAKASE_WRAPPER_DIR, "*", "executors", "*.ts")]
47
+ : [],
48
+ };
49
+ };
@@ -0,0 +1,141 @@
1
+ import dedent from "dedent";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ /**
6
+ * Path alias for importing modules config.
7
+ * App's tsconfig.json should have: "@omakase-modules/config": ["./modules"]
8
+ */
9
+ export const MODULES_IMPORT_PATH = "@omakase-modules/config";
10
+
11
+ /**
12
+ * Category types for wrapper generation
13
+ */
14
+ export type Category = "tailordb" | "resolvers" | "executors";
15
+
16
+ /**
17
+ * Abstract base class for category-specific wrapper generation
18
+ */
19
+ export abstract class WrapperStrategy {
20
+ constructor(
21
+ protected readonly packageName: string,
22
+ protected readonly basePath: string
23
+ ) {}
24
+
25
+ /**
26
+ * The category this strategy handles
27
+ */
28
+ abstract readonly category: Category;
29
+
30
+ /**
31
+ * Filter files to process for this category
32
+ */
33
+ abstract filterFiles(files: string[]): string[];
34
+
35
+ /**
36
+ * Get the module export path
37
+ */
38
+ abstract getExportPath(fileName: string): string;
39
+
40
+ /**
41
+ * Generate the export statements using the factory result.
42
+ * Default implementation exports the result as default export.
43
+ */
44
+ protected generateExports(): Promise<string> {
45
+ return Promise.resolve("export default result;");
46
+ }
47
+
48
+ /**
49
+ * Generate wrapper file content using template method
50
+ *
51
+ * The common header, imports, and factory call are included here.
52
+ */
53
+ async generateContent(moduleExportPath: string): Promise<string> {
54
+ const exports = await this.generateExports();
55
+
56
+ return dedent`
57
+ /**
58
+ * Auto-generated wrapper file by @izumisy-tailor/omakase-modules
59
+ * DO NOT EDIT THIS FILE MANUALLY
60
+ *
61
+ * This file calls the module's factory function with the app's loadModules result,
62
+ * ensuring proper configuration injection and avoiding tree-shaking issues.
63
+ */
64
+
65
+ // Import the loadModules result from the app's modules.ts
66
+ import modules from "${MODULES_IMPORT_PATH}";
67
+
68
+ // Import the factory function from the module
69
+ import createFactory from "${moduleExportPath}";
70
+
71
+ // Call the factory with loadModules result
72
+ const result = await createFactory(modules);
73
+
74
+ ${exports}
75
+ `;
76
+ }
77
+
78
+ /**
79
+ * Generate wrapper files for this category
80
+ */
81
+ async generateFiles(wrapperDir: string): Promise<string[]> {
82
+ const sourcePath = path.join(
83
+ this.basePath,
84
+ "node_modules",
85
+ this.packageName,
86
+ "src",
87
+ this.category
88
+ );
89
+
90
+ // Check if the source directory exists
91
+ if (!(await this.exists(sourcePath))) {
92
+ return [];
93
+ }
94
+
95
+ const allFiles = await fs.readdir(sourcePath);
96
+ const files = this.filterFiles(allFiles);
97
+ const wrapperPaths: string[] = [];
98
+
99
+ for (const file of files) {
100
+ const wrapperPath = await this.generateFile(wrapperDir, file);
101
+ wrapperPaths.push(wrapperPath);
102
+ }
103
+
104
+ return wrapperPaths;
105
+ }
106
+
107
+ /**
108
+ * Generate a single wrapper file for this category
109
+ */
110
+ private async generateFile(
111
+ wrapperDir: string,
112
+ fileName: string
113
+ ): Promise<string> {
114
+ const categoryWrapperDir = path.join(
115
+ wrapperDir,
116
+ this.packageName,
117
+ this.category
118
+ );
119
+ await fs.mkdir(categoryWrapperDir, { recursive: true });
120
+
121
+ const wrapperFilePath = path.join(categoryWrapperDir, fileName);
122
+ const moduleExportPath = this.getExportPath(fileName);
123
+ const content = await this.generateContent(moduleExportPath);
124
+
125
+ await fs.writeFile(wrapperFilePath, content, "utf-8");
126
+
127
+ return wrapperFilePath;
128
+ }
129
+
130
+ /**
131
+ * Check if a path exists
132
+ */
133
+ private async exists(filePath: string): Promise<boolean> {
134
+ try {
135
+ await fs.access(filePath);
136
+ return true;
137
+ } catch {
138
+ return false;
139
+ }
140
+ }
141
+ }
@@ -0,0 +1,52 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { LoadedModules } from "../../module-loader";
4
+ import { createStrategy } from "./strategies";
5
+
6
+ /**
7
+ * Directory where wrapper files are generated
8
+ */
9
+ export const OMAKASE_WRAPPER_DIR = ".tailor-sdk/.omakase";
10
+
11
+ /**
12
+ * Generates wrapper files for each module's tailordb, executor, and resolver files.
13
+ * These wrappers import the factory functions from modules and call them with
14
+ * the loadModules result from the app's modules.ts.
15
+ */
16
+ export const generateWrapperFiles = async (
17
+ loadedModules: LoadedModules,
18
+ basePath: string = process.cwd()
19
+ ) => {
20
+ const wrapperDir = path.join(basePath, OMAKASE_WRAPPER_DIR);
21
+ const modulePackageNames = Object.keys(loadedModules.loadedModules);
22
+
23
+ // Clean up existing wrapper directory
24
+ try {
25
+ await fs.access(wrapperDir);
26
+ await fs.rm(wrapperDir, { recursive: true });
27
+ } catch {
28
+ // Directory doesn't exist, no need to clean up
29
+ }
30
+
31
+ const result = {
32
+ tailordb: [] as string[],
33
+ resolver: [] as string[],
34
+ executor: [] as string[],
35
+ };
36
+
37
+ for (const packageName of modulePackageNames) {
38
+ const strategyFor = createStrategy(packageName, basePath);
39
+
40
+ result.tailordb.push(
41
+ ...(await strategyFor("tailordb").generateFiles(wrapperDir))
42
+ );
43
+ result.resolver.push(
44
+ ...(await strategyFor("resolvers").generateFiles(wrapperDir))
45
+ );
46
+ result.executor.push(
47
+ ...(await strategyFor("executors").generateFiles(wrapperDir))
48
+ );
49
+ }
50
+
51
+ return result;
52
+ };
@@ -0,0 +1,102 @@
1
+ import { createRequire } from "node:module";
2
+ import path from "node:path";
3
+ import { Category, WrapperStrategy } from "./base";
4
+
5
+ /**
6
+ * Strategy for tailordb category
7
+ */
8
+ class TailorDBStrategy extends WrapperStrategy {
9
+ readonly category = "tailordb" as const;
10
+ filterFiles(files: string[]) {
11
+ return files.filter((file) => file === "index.ts");
12
+ }
13
+
14
+ getExportPath() {
15
+ return `${this.packageName}/backend/tailordb`;
16
+ }
17
+
18
+ protected async generateExports() {
19
+ const tableNames = await this.importTableNames();
20
+ return tableNames
21
+ .map((name) => `export const ${name}Table = result.${name};`)
22
+ .join("\n");
23
+ }
24
+
25
+ /**
26
+ * Import tableNames from a module's tailordb/index.ts using dynamic import.
27
+ * Uses createRequire to leverage Node.js module resolution from the app's context.
28
+ */
29
+ private async importTableNames(): Promise<readonly string[]> {
30
+ try {
31
+ const appRequire = createRequire(
32
+ path.join(this.basePath, "package.json")
33
+ );
34
+ const modulePath = appRequire.resolve(
35
+ `${this.packageName}/backend/tailordb`
36
+ );
37
+ const module = await import(`file://${modulePath}`);
38
+ if (!module.tableNames) {
39
+ console.warn(
40
+ `[warn] tableNames not found in ${this.packageName}/backend/tailordb. Expected: export const tableNames = [...] as const;`
41
+ );
42
+ return [];
43
+ }
44
+ return module.tableNames;
45
+ } catch (error) {
46
+ console.warn(
47
+ `[warn] Failed to import ${this.packageName}/backend/tailordb:`,
48
+ error
49
+ );
50
+ return [];
51
+ }
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Strategy for resolvers category
57
+ */
58
+ class ResolversStrategy extends WrapperStrategy {
59
+ readonly category = "resolvers" as const;
60
+
61
+ filterFiles(files: string[]) {
62
+ return files.filter((file) => file.endsWith(".ts") && file !== "index.ts");
63
+ }
64
+
65
+ getExportPath(fileName: string) {
66
+ const baseName = fileName.replace(/\.ts$/, "");
67
+ return `${this.packageName}/backend/resolvers/${baseName}`;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Strategy for executors category
73
+ */
74
+ class ExecutorsStrategy extends WrapperStrategy {
75
+ readonly category = "executors" as const;
76
+
77
+ filterFiles(files: string[]) {
78
+ return files.filter((file) => file.endsWith(".ts") && file !== "index.ts");
79
+ }
80
+
81
+ getExportPath(fileName: string) {
82
+ const baseName = fileName.replace(/\.ts$/, "");
83
+ return `${this.packageName}/backend/executors/${baseName}`;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Factory to create strategy instance for each category.
89
+ * Returns a function that takes a category and returns a strategy.
90
+ */
91
+ export const createStrategy =
92
+ (packageName: string, basePath: string) =>
93
+ (category: Category): WrapperStrategy => {
94
+ switch (category) {
95
+ case "tailordb":
96
+ return new TailorDBStrategy(packageName, basePath);
97
+ case "resolvers":
98
+ return new ResolversStrategy(packageName, basePath);
99
+ case "executors":
100
+ return new ExecutorsStrategy(packageName, basePath);
101
+ }
102
+ };
@@ -1,3 +0,0 @@
1
- import { i as defineModule, n as DefinedModule, r as ModuleDependency, t as ConfiguredModule } from "../helpers-CNjRbrYN.mjs";
2
- import { t as withModuleConfiguration } from "../index-BoAL29Di.mjs";
3
- export { ConfiguredModule, DefinedModule, ModuleDependency, defineModule, withModuleConfiguration };
@@ -1,35 +0,0 @@
1
- //#region src/builder/helpers.ts
2
- const defineModule = (baseProps) => {
3
- return {
4
- packageName: baseProps.packageName,
5
- configure: (props) => {
6
- return {
7
- packageName: baseProps.packageName,
8
- moduleProps: props
9
- };
10
- }
11
- };
12
- };
13
-
14
- //#endregion
15
- //#region src/builder/register.ts
16
- /**
17
- * Load a module's configuration, define something.
18
- *
19
- * THis is a low-level utility composed by a specific builder function.
20
- */
21
- const withModuleConfiguration = async (module, factory) => {
22
- /**
23
- * This import intentionally uses a module path instead of a package path
24
- * to let the app override the implementation via tsconfig paths.
25
- *
26
- * For more details, see `moduleConfigLoader` in `./src/stub-loader/interface.ts`.
27
- *
28
- * Dynamic import is also important here to avoid "cannot acess before initialization" error.
29
- */
30
- const { default: configLoader } = await import("@izumisy-tailor/omakase-modules/config/loader");
31
- return await factory(await configLoader.loadConfig(module));
32
- };
33
-
34
- //#endregion
35
- export { defineModule, withModuleConfiguration };
@@ -1,2 +0,0 @@
1
- import { n as ModuleLoader, r as loadModules, t as LoadedModules } from "../module-loader-B4sA1i0A.mjs";
2
- export { type LoadedModules, ModuleLoader, loadModules };