@antislop/zero-lucid-generator 1.0.0 → 1.1.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/README.md +20 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +55 -41
- package/package.json +12 -6
package/README.md
CHANGED
|
@@ -60,10 +60,29 @@ import { schema, zql } from './zero-schema.gen.js'
|
|
|
60
60
|
|---|---|---|---|
|
|
61
61
|
| `modelsSourcePath` | `string` | **required** | Path to your models directory, relative to the config file |
|
|
62
62
|
| `output` | `string` | `zero-schema.gen.ts` | Output file path, relative to the config file |
|
|
63
|
-
| `
|
|
63
|
+
| `format` | `boolean` | `false` | Format the output with [oxfmt](https://oxc.rs/docs/guide/usage/formatter) (must be installed). Respects your `.oxfmtrc.json`. |
|
|
64
64
|
| `excludeModels` | `LucidModel[]` | `[]` | Models to exclude from the generated schema |
|
|
65
65
|
| `columnTypes` | `Record<string, Record<string, string>>` | `{}` | Override the inferred Zero type for specific columns |
|
|
66
66
|
|
|
67
|
+
### Formatting with oxfmt
|
|
68
|
+
|
|
69
|
+
Set `format: true` to have the generated file automatically formatted on every run:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const config: Config = {
|
|
73
|
+
modelsSourcePath: './app/models',
|
|
74
|
+
format: true,
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Install oxfmt in your project:
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
npm add -D oxfmt
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The formatter picks up your `.oxfmtrc.json` / `oxfmt.config.ts` automatically, so the output will always match what your own format scripts produce.
|
|
85
|
+
|
|
67
86
|
---
|
|
68
87
|
|
|
69
88
|
## CLI options
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,8 @@ type ConfigInput = {
|
|
|
9
9
|
*/
|
|
10
10
|
excludeModels?: (abstract new (...args: any[]) => any)[];
|
|
11
11
|
output?: string;
|
|
12
|
-
|
|
12
|
+
/** Format the output file with oxfmt. Requires oxfmt to be installed. */
|
|
13
|
+
format?: boolean;
|
|
13
14
|
columnTypes?: Record<string, Record<string, string>>;
|
|
14
15
|
};
|
|
15
16
|
|
package/dist/index.js
CHANGED
|
@@ -17,14 +17,14 @@ async function getDefaultExportFromModulePath(modulePath) {
|
|
|
17
17
|
try {
|
|
18
18
|
await fs.access(modulePath);
|
|
19
19
|
} catch {
|
|
20
|
-
throw new Error(`
|
|
20
|
+
throw new Error(`Module not found at ${modulePath}`);
|
|
21
21
|
}
|
|
22
22
|
const { tsImport } = await import("tsx/esm/api");
|
|
23
23
|
const moduleUrl = url.pathToFileURL(modulePath).href;
|
|
24
24
|
const module = await tsImport(moduleUrl, { parentURL: import.meta.url });
|
|
25
25
|
const defaultExport = module.default;
|
|
26
26
|
if (!defaultExport) {
|
|
27
|
-
throw new Error(`
|
|
27
|
+
throw new Error(`Module at ${modulePath} does not have a default export`);
|
|
28
28
|
}
|
|
29
29
|
return defaultExport;
|
|
30
30
|
}
|
|
@@ -49,17 +49,17 @@ var Config = class {
|
|
|
49
49
|
this.excludeModels = configInput.excludeModels ?? [];
|
|
50
50
|
this.outputFilePath = path.resolve(configDir, configInput.output ?? DEFAULT_OUTPUT_FILE_PATH);
|
|
51
51
|
this.tsconfigPath = tsconfigPath ?? path.resolve(configDir, "tsconfig.json");
|
|
52
|
-
this.formatOutputFile = configInput.
|
|
52
|
+
this.formatOutputFile = configInput.format ?? false;
|
|
53
53
|
this.columnTypes = configInput.columnTypes ?? {};
|
|
54
54
|
}
|
|
55
55
|
async verify() {
|
|
56
56
|
if (!this.modelsSourcePath) {
|
|
57
|
-
throw new Error("
|
|
57
|
+
throw new Error("modelsSourcePath is required");
|
|
58
58
|
}
|
|
59
59
|
await this.loadModels();
|
|
60
60
|
if (this.models.length === 0) {
|
|
61
61
|
throw new Error(
|
|
62
|
-
`
|
|
62
|
+
`No Lucid models found in modelsSourcePath. Each model file must have a default export that extends BaseModel.`
|
|
63
63
|
);
|
|
64
64
|
}
|
|
65
65
|
this.verifyColumnTypes();
|
|
@@ -71,7 +71,7 @@ var Config = class {
|
|
|
71
71
|
fileNames = await fs2.readdir(modelsSourceAbsPath);
|
|
72
72
|
} catch (e) {
|
|
73
73
|
throw new Error(
|
|
74
|
-
`
|
|
74
|
+
`Could not read modelsSourcePath at ${modelsSourceAbsPath}. Does the directory exist?Error: ${e}`
|
|
75
75
|
);
|
|
76
76
|
}
|
|
77
77
|
const tsFileNames = fileNames.filter((f) => f.endsWith(".ts") && !f.endsWith(".d.ts"));
|
|
@@ -84,7 +84,7 @@ var Config = class {
|
|
|
84
84
|
path.join(modelsSourceAbsPath, fileName)
|
|
85
85
|
);
|
|
86
86
|
} catch (err) {
|
|
87
|
-
console.warn(`
|
|
87
|
+
console.warn(`Failed to import ${fileName}, skipping: ${String(err)}`);
|
|
88
88
|
return null;
|
|
89
89
|
}
|
|
90
90
|
if (!this.isLucidModel(defaultExport)) {
|
|
@@ -106,14 +106,14 @@ var Config = class {
|
|
|
106
106
|
for (const [modelName, columnOverrides] of Object.entries(this.columnTypes ?? {})) {
|
|
107
107
|
if (!modelNames.has(modelName)) {
|
|
108
108
|
throw new Error(
|
|
109
|
-
`
|
|
109
|
+
`columnTypes has unknown model "${modelName}". Known models: ${[...modelNames].join(", ")}`
|
|
110
110
|
);
|
|
111
111
|
}
|
|
112
112
|
for (const [attributeName, typeString] of Object.entries(columnOverrides)) {
|
|
113
113
|
const baseTypeName = typeString.match(/^([a-z]+)/)?.[1];
|
|
114
114
|
if (!baseTypeName || !validZeroTypes.includes(baseTypeName)) {
|
|
115
115
|
throw new Error(
|
|
116
|
-
`
|
|
116
|
+
`columnTypes override for ${modelName}.${attributeName} has unrecognised type "${typeString}". Expected one of: ${validZeroTypes.join(", ")}`
|
|
117
117
|
);
|
|
118
118
|
}
|
|
119
119
|
}
|
|
@@ -137,8 +137,8 @@ var ConfigLoader = class {
|
|
|
137
137
|
defaultExport = await getDefaultExportFromModulePath(absoluteConfigPath);
|
|
138
138
|
} catch (err) {
|
|
139
139
|
throw new Error(
|
|
140
|
-
`
|
|
141
|
-
Error: ${String(err)}`
|
|
140
|
+
`Failed to import config at ${absoluteConfigPath}
|
|
141
|
+
Error: ${err instanceof Error ? err.message : String(err)}`
|
|
142
142
|
);
|
|
143
143
|
}
|
|
144
144
|
const config = new Config(defaultExport, path.dirname(absoluteConfigPath), opts.tsconfigPath);
|
|
@@ -190,12 +190,12 @@ var SchemaTransformer = class {
|
|
|
190
190
|
});
|
|
191
191
|
if (!modelSourceFile) {
|
|
192
192
|
throw new Error(
|
|
193
|
-
`
|
|
193
|
+
`Could not find source file for model ${model.name} in ${this.project.getSourceFiles().map((sourceFile) => sourceFile.getFilePath()).join(", ")}`
|
|
194
194
|
);
|
|
195
195
|
}
|
|
196
196
|
const classDeclaration = modelSourceFile.getClass(model.name);
|
|
197
197
|
if (!classDeclaration) {
|
|
198
|
-
throw new Error(`
|
|
198
|
+
throw new Error(`Could not find class declaration for model ${model.name}`);
|
|
199
199
|
}
|
|
200
200
|
let currentClass = classDeclaration;
|
|
201
201
|
while (currentClass) {
|
|
@@ -226,7 +226,7 @@ var SchemaTransformer = class {
|
|
|
226
226
|
zeroType = tsTypeToZeroType(tsType);
|
|
227
227
|
} else {
|
|
228
228
|
console.warn(
|
|
229
|
-
`
|
|
229
|
+
`Could not infer Zero type for ${model.name}.${attributeName}. Falling back to json(). Add a columnTypes override if this is wrong.`
|
|
230
230
|
);
|
|
231
231
|
zeroType = "json()";
|
|
232
232
|
}
|
|
@@ -247,13 +247,13 @@ var SchemaTransformer = class {
|
|
|
247
247
|
relation.boot();
|
|
248
248
|
} catch (err) {
|
|
249
249
|
throw new Error(
|
|
250
|
-
`
|
|
250
|
+
`Failed to boot relation "${relationName}" on ${model.name}: ${String(err)}`
|
|
251
251
|
);
|
|
252
252
|
}
|
|
253
253
|
const relatedModel = relation.relatedModel();
|
|
254
254
|
if (!allModels.some((m) => m.name === relatedModel.name)) {
|
|
255
255
|
console.info(
|
|
256
|
-
`
|
|
256
|
+
`Skipping relation "${relationName}" on ${model.name} \u2014 ${relatedModel.name} is not included.`
|
|
257
257
|
);
|
|
258
258
|
continue;
|
|
259
259
|
}
|
|
@@ -285,7 +285,7 @@ var SchemaTransformer = class {
|
|
|
285
285
|
case "manyToMany": {
|
|
286
286
|
if (!relation.pivotTable || !relation.pivotForeignKey || !relation.pivotRelatedForeignKey) {
|
|
287
287
|
throw new Error(
|
|
288
|
-
`
|
|
288
|
+
`manyToMany relation "${relationName}" on ${model.name} \u2014 pivot table fields could not be resolved after boot().`
|
|
289
289
|
);
|
|
290
290
|
}
|
|
291
291
|
relationships[relationName] = {
|
|
@@ -310,7 +310,7 @@ var SchemaTransformer = class {
|
|
|
310
310
|
const throughRel = relation;
|
|
311
311
|
if (!throughRel.throughModel) {
|
|
312
312
|
throw new Error(
|
|
313
|
-
`
|
|
313
|
+
`hasManyThrough relation "${relationName}" on ${model.name} \u2014 throughModel could not be resolved after boot().`
|
|
314
314
|
);
|
|
315
315
|
}
|
|
316
316
|
const throughModel = throughRel.throughModel();
|
|
@@ -352,33 +352,47 @@ function tsTypeToZeroType(type) {
|
|
|
352
352
|
import fs3 from "fs";
|
|
353
353
|
|
|
354
354
|
// src/formatter.ts
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
return null;
|
|
367
|
-
}
|
|
355
|
+
import { spawnSync } from "child_process";
|
|
356
|
+
import { createRequire } from "module";
|
|
357
|
+
import path2 from "path";
|
|
358
|
+
function resolveOxfmtBin() {
|
|
359
|
+
try {
|
|
360
|
+
const req = createRequire(process.cwd() + "/package.json");
|
|
361
|
+
const pkgMain = req.resolve("oxfmt");
|
|
362
|
+
const pkgRoot = path2.join(pkgMain, "..", "..");
|
|
363
|
+
return path2.join(pkgRoot, "bin", "oxfmt");
|
|
364
|
+
} catch {
|
|
365
|
+
return null;
|
|
368
366
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
367
|
+
}
|
|
368
|
+
var Formatter = class {
|
|
369
|
+
/**
|
|
370
|
+
* Formats `code` by piping it through the oxfmt CLI with `--stdin-filepath`
|
|
371
|
+
* set to the output file's absolute path. This lets oxfmt walk up the
|
|
372
|
+
* directory tree and discover `.oxfmtrc.json` / `oxfmt.config.ts` in the
|
|
373
|
+
* user's project.
|
|
374
|
+
*/
|
|
375
|
+
static async format(code, outputFilePath) {
|
|
376
|
+
const bin = resolveOxfmtBin();
|
|
377
|
+
if (!bin) {
|
|
378
|
+
console.warn("lucid-zero: oxfmt not found \u2014 skipping formatting");
|
|
373
379
|
return code;
|
|
374
380
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
381
|
+
const result = spawnSync(
|
|
382
|
+
process.execPath,
|
|
383
|
+
[bin, `--stdin-filepath=${outputFilePath}`],
|
|
384
|
+
{
|
|
385
|
+
input: code,
|
|
386
|
+
encoding: "utf8",
|
|
387
|
+
cwd: process.cwd()
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
if (result.status !== 0) {
|
|
391
|
+
console.warn(`lucid-zero: oxfmt formatting failed \u2014 skipping
|
|
392
|
+
${result.stderr ?? ""}`);
|
|
380
393
|
return code;
|
|
381
394
|
}
|
|
395
|
+
return result.stdout;
|
|
382
396
|
}
|
|
383
397
|
};
|
|
384
398
|
|
|
@@ -395,7 +409,7 @@ var CodeGenerator = class {
|
|
|
395
409
|
async generateToOutputFile() {
|
|
396
410
|
let code = this.generate();
|
|
397
411
|
if (this.config.formatOutputFile) {
|
|
398
|
-
code = await Formatter.format(code);
|
|
412
|
+
code = await Formatter.format(code, this.config.outputFilePath);
|
|
399
413
|
}
|
|
400
414
|
fs3.writeFileSync(this.config.outputFilePath, code);
|
|
401
415
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antislop/zero-lucid-generator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Generate Zero schemas from Lucid ORM models",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -19,13 +19,20 @@
|
|
|
19
19
|
"check-types": "tsc --noEmit",
|
|
20
20
|
"lint": "eslint src tests",
|
|
21
21
|
"lint:fix": "eslint src tests --fix",
|
|
22
|
-
"format": "
|
|
23
|
-
"format:check": "
|
|
22
|
+
"format": "oxfmt \"src/**/*.ts\" \"tests/**/*.ts\"",
|
|
23
|
+
"format:check": "oxfmt --check \"src/**/*.ts\" \"tests/**/*.ts\""
|
|
24
24
|
},
|
|
25
25
|
"bin": {
|
|
26
26
|
"lucid-zero": "./dist/index.js"
|
|
27
27
|
},
|
|
28
|
-
"keywords": [
|
|
28
|
+
"keywords": [
|
|
29
|
+
"zero",
|
|
30
|
+
"lucid",
|
|
31
|
+
"adonisjs",
|
|
32
|
+
"schema",
|
|
33
|
+
"generator",
|
|
34
|
+
"rocicorp"
|
|
35
|
+
],
|
|
29
36
|
"author": "antislop",
|
|
30
37
|
"license": "ISC",
|
|
31
38
|
"publishConfig": {
|
|
@@ -43,8 +50,7 @@
|
|
|
43
50
|
"@typescript-eslint/parser": "^8.61.0",
|
|
44
51
|
"@vitest/coverage-v8": "^4.1.8",
|
|
45
52
|
"eslint": "^10.4.1",
|
|
46
|
-
"
|
|
47
|
-
"prettier": "^3.8.4",
|
|
53
|
+
"oxfmt": "^0.54.0",
|
|
48
54
|
"tsup": "^8.5.1",
|
|
49
55
|
"tsx": "^4.22.4",
|
|
50
56
|
"typescript": "^6.0.3",
|