@frigear-nu/module-kit 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=4.0.0"
6
6
  },
7
- "version": "0.0.4",
7
+ "version": "0.0.5",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "unknown"
@@ -3,6 +3,7 @@ export type ParsedTableExport = {
3
3
  tableCall?: {
4
4
  callee: string;
5
5
  tableName: string;
6
+ extraArgs?: string[];
6
7
  };
7
8
  /** Column (or spread) entries keyed by property name, in source order. Values are the raw `key: value` source text. */
8
9
  properties: Map<string, string>;
@@ -21,3 +22,10 @@ export type ParsedTableExport = {
21
22
  * when `exportName` is `'default'`
22
23
  */
23
24
  export declare const parseRegularTableExport: (filePath: string, exportName: string) => ParsedTableExport | undefined;
25
+ /**
26
+ * Lists the names of every named export in a "regular" drizzle schema file that looks like a table (or columns)
27
+ * export - i.e. every export that `parseRegularTableExport` would be able to parse. Used by `registerDatabaseTable`
28
+ * to auto-discover table names when none are explicitly provided. Does not include the `default` export, since it
29
+ * has no name to register a table under.
30
+ */
31
+ export declare const listRegularTableExportNames: (filePath: string) => string[];
@@ -1,8 +1,10 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import ts from "typescript";
3
- export const parseRegularTableExport = (filePath, exportName) => {
3
+ const parseSource = (filePath) => {
4
4
  const text = readFileSync(filePath, "utf8");
5
- const source = ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
5
+ return ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
6
+ };
7
+ const collectImports = (source) => {
6
8
  const imports = /* @__PURE__ */ new Map();
7
9
  for (const statement of source.statements) {
8
10
  if (ts.isImportDeclaration(statement) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && ts.isStringLiteralLike(statement.moduleSpecifier)) {
@@ -11,43 +13,64 @@ export const parseRegularTableExport = (filePath, exportName) => {
11
13
  imports.set(statement.moduleSpecifier.text, specifiers);
12
14
  }
13
15
  }
14
- const toResult = (initializer) => {
15
- let objectLiteral;
16
- let tableCall;
17
- if (ts.isCallExpression(initializer)) {
18
- const args = initializer.arguments;
19
- if (args.length >= 2 && ts.isStringLiteralLike(args[0]) && ts.isObjectLiteralExpression(args[1])) {
20
- tableCall = { callee: initializer.expression.getText(), tableName: args[0].text };
21
- objectLiteral = args[1];
22
- } else if (args.length === 1 && ts.isObjectLiteralExpression(args[0])) {
23
- objectLiteral = args[0];
24
- }
25
- } else if (ts.isObjectLiteralExpression(initializer)) {
26
- objectLiteral = initializer;
16
+ return imports;
17
+ };
18
+ const toTableResult = (initializer, imports) => {
19
+ let objectLiteral;
20
+ let tableCall;
21
+ if (ts.isCallExpression(initializer)) {
22
+ const args = initializer.arguments;
23
+ if (args.length >= 2 && ts.isStringLiteralLike(args[0]) && ts.isObjectLiteralExpression(args[1])) {
24
+ const extraArgs = args.length > 2 ? args.slice(2).map((arg) => arg.getText()) : void 0;
25
+ tableCall = { callee: initializer.expression.getText(), tableName: args[0].text, extraArgs };
26
+ objectLiteral = args[1];
27
+ } else if (args.length === 1 && ts.isObjectLiteralExpression(args[0])) {
28
+ objectLiteral = args[0];
27
29
  }
28
- if (!objectLiteral) return void 0;
29
- const properties = /* @__PURE__ */ new Map();
30
- for (const property of objectLiteral.properties) {
31
- if (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) {
32
- properties.set(property.name.getText(), property.getText());
33
- } else if (ts.isSpreadAssignment(property)) {
34
- properties.set(`__spread_${properties.size}`, property.getText());
35
- }
30
+ } else if (ts.isObjectLiteralExpression(initializer)) {
31
+ objectLiteral = initializer;
32
+ }
33
+ if (!objectLiteral) return void 0;
34
+ const properties = /* @__PURE__ */ new Map();
35
+ for (const property of objectLiteral.properties) {
36
+ if (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) {
37
+ properties.set(property.name.getText(), property.getText());
38
+ } else if (ts.isSpreadAssignment(property)) {
39
+ properties.set(`__spread_${properties.size}`, property.getText());
36
40
  }
37
- return { tableCall, properties, imports };
38
- };
41
+ }
42
+ return { tableCall, properties, imports };
43
+ };
44
+ export const parseRegularTableExport = (filePath, exportName) => {
45
+ const source = parseSource(filePath);
46
+ const imports = collectImports(source);
39
47
  for (const statement of source.statements) {
40
48
  if (ts.isVariableStatement(statement) && statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
41
49
  for (const declaration of statement.declarationList.declarations) {
42
50
  if (ts.isIdentifier(declaration.name) && declaration.name.text === exportName && declaration.initializer) {
43
- const result = toResult(declaration.initializer);
51
+ const result = toTableResult(declaration.initializer, imports);
44
52
  if (result) return result;
45
53
  }
46
54
  }
47
55
  } else if (exportName === "default" && ts.isExportAssignment(statement) && !statement.isExportEquals) {
48
- const result = toResult(statement.expression);
56
+ const result = toTableResult(statement.expression, imports);
49
57
  if (result) return result;
50
58
  }
51
59
  }
52
60
  return void 0;
53
61
  };
62
+ export const listRegularTableExportNames = (filePath) => {
63
+ const source = parseSource(filePath);
64
+ const imports = collectImports(source);
65
+ const names = [];
66
+ for (const statement of source.statements) {
67
+ if (ts.isVariableStatement(statement) && statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
68
+ for (const declaration of statement.declarationList.declarations) {
69
+ if (ts.isIdentifier(declaration.name) && declaration.initializer && toTableResult(declaration.initializer, imports)) {
70
+ names.push(declaration.name.text);
71
+ }
72
+ }
73
+ }
74
+ }
75
+ return names;
76
+ };
@@ -42,12 +42,22 @@ export declare const defineExtendableTable: <T>(table: TableDefinerFn<T>) => (pr
42
42
  */
43
43
  export declare const onDatabaseSchemaExtend: (nuxt: Nuxt, extender: DbExtenderFn) => Promise<() => void>;
44
44
  type DatabaseTableOptions = {
45
- name: string;
45
+ /**
46
+ * The name of the table's export (e.g. `users`). Pass an array of names (e.g. `['users', 'userPasskeys']`) to
47
+ * register several tables that are exported from the same `schema` file in one go - they're generated into a
48
+ * single file so that tables can reference each other (e.g. a foreign key `.references(() => users.id)`). An
49
+ * array of names (or omitting `name` entirely) is only supported when `factory` is omitted.
50
+ *
51
+ * When omitted, every table-like export in `schema` (and its extension files) is registered automatically - this
52
+ * is equivalent to passing an array of every such export's name.
53
+ */
54
+ name?: string | string[];
46
55
  /**
47
56
  * The exported factory function name to call with merged extension columns (e.g. `defineUsersTable`), as produced
48
57
  * by `defineExtendableTable`. When omitted, `schema` (and every registered/layer extension file) is treated as a
49
58
  * "regular" drizzle file - e.g. `export const users = sqliteTable('users', { ... })` - and its columns are merged
50
- * directly, without requiring any helper wrapper.
59
+ * directly, without requiring any helper wrapper. Only supported when `name` is a single string, since the
60
+ * factory is called once with a single table's columns.
51
61
  */
52
62
  factory?: string;
53
63
  resolver: ReturnType<typeof createResolver>;
@@ -71,6 +81,10 @@ export declare const extendDatabaseTable: (nuxt: Nuxt, options: TableExtensionOp
71
81
  * table by importing the named `factory` from `schema`, merging in any columns registered
72
82
  * through `extendDatabaseTable` and any Nuxt-layer extension files, then calling the factory with
73
83
  * `{ name, columns }`.
84
+ *
85
+ * When `name` is an array, or omitted entirely, every (named or auto-discovered) table is generated into a single
86
+ * file - this lets tables declared in the same `schema` file reference each other directly (e.g. a foreign key
87
+ * `.references(() => users.id)` from a `userPasskeys` table), since they share the same module scope.
74
88
  */
75
89
  export declare const registerDatabaseTable: (nuxt: Nuxt, options: DatabaseTableOptions) => Promise<() => void>;
76
90
  export {};
@@ -1,6 +1,6 @@
1
1
  import { addTemplate, createResolver } from "@nuxt/kit";
2
2
  import { existsSync } from "node:fs";
3
- import { parseRegularTableExport } from "./regular-table.js";
3
+ import { listRegularTableExportNames, parseRegularTableExport } from "./regular-table.js";
4
4
  export const defineTableColumns = (columns) => {
5
5
  return columns;
6
6
  };
@@ -22,73 +22,102 @@ export const extendDatabaseTable = (nuxt, options) => {
22
22
  };
23
23
  export const registerDatabaseTable = (nuxt, options) => {
24
24
  const { name, factory, resolver, schema, override, extensionFile } = options;
25
+ if (factory && (name === void 0 || Array.isArray(name))) {
26
+ throw new Error(
27
+ '[module-kit] registerDatabaseTable requires a single "name" (not an array, and not omitted) when "factory" is set. Omit "factory" to register multiple (or auto-discovered) tables from the same regular drizzle schema file.'
28
+ );
29
+ }
25
30
  return onDatabaseSchemaExtend(nuxt, async ({ dialect, paths }) => {
26
31
  if (override && override !== "default") {
27
32
  paths.push(await resolver.resolvePath(override, { cwd: nuxt.options.rootDir }));
28
33
  return;
29
34
  }
30
- const registered = [];
31
- await nuxt.callHook("module-kit:db:table:extend", { name, dialect, paths: registered });
32
- const extensions = registered.map((file) => ({ file, legacy: true }));
35
+ const schemaPath = await resolver.resolvePath(schema(dialect));
36
+ const names = name === void 0 ? listRegularTableExportNames(schemaPath) : Array.isArray(name) ? name : [name];
37
+ if (names.length === 0) {
38
+ throw new Error(
39
+ `[module-kit] Could not find any table exports in "${schemaPath}" to register. "schema" must be a regular drizzle file, e.g. \`export const users = sqliteTable('users', { ... })\`.`
40
+ );
41
+ }
42
+ const extensionsByName = /* @__PURE__ */ new Map();
43
+ for (const tableName2 of names) {
44
+ const registered = [];
45
+ await nuxt.callHook("module-kit:db:table:extend", { name: tableName2, dialect, paths: registered });
46
+ extensionsByName.set(tableName2, registered.map((file) => ({ file, legacy: true })));
47
+ }
33
48
  const layerRoots = [...new Set(nuxt.options._layers?.map((layer) => layer.config.rootDir).reverse() ?? [])];
34
49
  for (const root of [...layerRoots.filter((root2) => root2 !== nuxt.options.rootDir), nuxt.options.rootDir]) {
35
50
  const rootResolver = createResolver(root);
36
51
  const extensionPath = await rootResolver.resolvePath(extensionFile?.(dialect) ?? `server/db/extend.${dialect}.ts`);
37
52
  if (!extensionFile) {
38
- const legacyPath = await rootResolver.resolvePath(`server/db/${name}.extend.${dialect}`);
39
- if (existsSync(legacyPath)) extensions.push({ file: legacyPath, legacy: true });
53
+ for (const tableName2 of names) {
54
+ const legacyPath = await rootResolver.resolvePath(`server/db/${tableName2}.extend.${dialect}`);
55
+ if (existsSync(legacyPath)) extensionsByName.get(tableName2).push({ file: legacyPath, legacy: true });
56
+ }
57
+ }
58
+ if (existsSync(extensionPath)) {
59
+ for (const tableName2 of names) extensionsByName.get(tableName2).push({ file: extensionPath, legacy: false });
40
60
  }
41
- if (existsSync(extensionPath)) extensions.push({ file: extensionPath, legacy: false });
42
61
  }
43
62
  if (!factory) {
44
- const schemaPath = await resolver.resolvePath(schema(dialect));
45
- const template2 = buildRegularTableTemplate({ name, dialect, schemaPath, extensionFiles: extensions.map(({ file }) => file) });
63
+ const template2 = buildRegularTableTemplate({ names, dialect, schemaPath, extensionsByName });
46
64
  paths.push(template2.dst);
47
65
  return;
48
66
  }
67
+ const tableName = names[0];
68
+ const extensions = extensionsByName.get(tableName);
49
69
  const template = addTemplate({
50
- filename: `${name}/schema.${dialect}.ts`,
70
+ filename: `${tableName}/schema.${dialect}.ts`,
51
71
  write: true,
52
72
  getContents: () => `
53
73
  import { ${factory} } from ${JSON.stringify(resolver.resolve(schema(dialect)))}
54
74
  ${extensions.map(({ file }, index) => `import * as extra${index} from ${JSON.stringify(file)}`).join("\n")}
55
- export const ${name} = ${factory}({ name: ${JSON.stringify(name)}, columns: { ${extensions.map(({ legacy }, index) => legacy ? `...(extra${index}[${JSON.stringify(name)} as keyof typeof extra${index}] ?? Object.entries(extra${index}).find(([key]) => key === 'default')?.[1] ?? {})` : `...(extra${index}[${JSON.stringify(name)} as keyof typeof extra${index}] ?? {})`).join(", ")} } })
75
+ export const ${tableName} = ${factory}({ name: ${JSON.stringify(tableName)}, columns: { ${extensions.map(({ legacy }, index) => legacy ? `...(extra${index}[${JSON.stringify(tableName)} as keyof typeof extra${index}] ?? Object.entries(extra${index}).find(([key]) => key === 'default')?.[1] ?? {})` : `...(extra${index}[${JSON.stringify(tableName)} as keyof typeof extra${index}] ?? {})`).join(", ")} } })
56
76
  `
57
77
  });
58
78
  paths.push(template.dst);
59
79
  });
60
80
  };
61
81
  const buildRegularTableTemplate = (options) => {
62
- const { name, dialect, schemaPath, extensionFiles } = options;
63
- const base = parseRegularTableExport(schemaPath, name);
64
- if (!base?.tableCall) {
65
- throw new Error(
66
- `[module-kit] Could not find a table export named "${name}" in "${schemaPath}". When "factory" is omitted, "schema" must be a regular drizzle file, e.g. \`export const ${name} = sqliteTable(${JSON.stringify(name)}, { ... })\`.`
67
- );
68
- }
69
- const properties = new Map(base.properties);
70
- const importsByModule = new Map([...base.imports].map(([module, specifiers]) => [module, new Set(specifiers)]));
71
- for (const file of extensionFiles) {
72
- const parsed = parseRegularTableExport(file, name);
73
- if (!parsed) continue;
74
- for (const [key, value] of parsed.properties) {
75
- properties.delete(key);
76
- properties.set(key, value);
82
+ const { names, dialect, schemaPath, extensionsByName } = options;
83
+ const importsByModule = /* @__PURE__ */ new Map();
84
+ const tables = names.map((name) => {
85
+ const base = parseRegularTableExport(schemaPath, name);
86
+ if (!base?.tableCall) {
87
+ throw new Error(
88
+ `[module-kit] Could not find a table export named "${name}" in "${schemaPath}". When "factory" is omitted, "schema" must be a regular drizzle file, e.g. \`export const ${name} = sqliteTable(${JSON.stringify(name)}, { ... })\`.`
89
+ );
77
90
  }
78
- for (const [module, specifiers] of parsed.imports) {
91
+ const properties = new Map(base.properties);
92
+ for (const [module, specifiers] of base.imports) {
79
93
  const set = importsByModule.get(module) ?? /* @__PURE__ */ new Set();
80
94
  for (const specifier of specifiers) set.add(specifier);
81
95
  importsByModule.set(module, set);
82
96
  }
83
- }
97
+ for (const { file } of extensionsByName.get(name) ?? []) {
98
+ const parsed = parseRegularTableExport(file, name);
99
+ if (!parsed) continue;
100
+ for (const [key, value] of parsed.properties) {
101
+ properties.delete(key);
102
+ properties.set(key, value);
103
+ }
104
+ for (const [module, specifiers] of parsed.imports) {
105
+ const set = importsByModule.get(module) ?? /* @__PURE__ */ new Set();
106
+ for (const specifier of specifiers) set.add(specifier);
107
+ importsByModule.set(module, set);
108
+ }
109
+ }
110
+ return { name, tableCall: base.tableCall, properties };
111
+ });
84
112
  return addTemplate({
85
- filename: `${name}/schema.${dialect}.ts`,
113
+ filename: `${names.join("+")}/schema.${dialect}.ts`,
86
114
  write: true,
87
115
  getContents: () => `
88
116
  ${[...importsByModule].map(([module, specifiers]) => `import { ${[...specifiers].join(", ")} } from ${JSON.stringify(module)}`).join("\n")}
89
- export const ${name} = ${base.tableCall.callee}(${JSON.stringify(base.tableCall.tableName)}, {
117
+ ${tables.map(({ name, tableCall, properties }) => `
118
+ export const ${name} = ${tableCall.callee}(${JSON.stringify(tableCall.tableName)}, {
90
119
  ${[...properties.values()].map((property) => ` ${property},`).join("\n")}
91
- })
92
- `
120
+ }${tableCall.extraArgs?.length ? `, ${tableCall.extraArgs.join(", ")}` : ""})
121
+ `).join("")}`
93
122
  });
94
123
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frigear-nu/module-kit",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Frigear Module Kit",