metal-orm 1.0.49 → 1.0.51
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/index.cjs +151 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +151 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/generate-entities/cli.mjs +101 -0
- package/scripts/generate-entities/render.mjs +130 -17
- package/scripts/inflection/compound.mjs +72 -0
- package/scripts/inflection/en.mjs +26 -0
- package/scripts/inflection/index.mjs +29 -0
- package/scripts/inflection/pt-br.mjs +391 -0
- package/scripts/naming-strategy.mjs +27 -63
- package/scripts/pt-pluralizer.mjs +19 -0
- package/src/core/ddl/introspect/mssql.ts +74 -2
- package/src/core/ddl/introspect/postgres.ts +69 -39
- package/src/core/ddl/introspect/sqlite.ts +69 -5
- package/src/schema/column-types.ts +14 -9
package/package.json
CHANGED
|
@@ -1,8 +1,107 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import process from 'node:process';
|
|
2
3
|
import path from 'node:path';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
3
5
|
import { parseArgs as parseCliArgs } from 'node:util';
|
|
4
6
|
|
|
5
7
|
const DIALECTS = new Set(['postgres', 'mysql', 'sqlite', 'mssql']);
|
|
8
|
+
const NODE_NEXT_MODULE_RESOLUTIONS = new Set(['node16', 'nodenext']);
|
|
9
|
+
|
|
10
|
+
const TS_CONFIG_BASE_NAMES = ['tsconfig.json', 'tsconfig.base.json', 'tsconfig.app.json', 'tsconfig.build.json'];
|
|
11
|
+
const nodeRequire = createRequire(import.meta.url);
|
|
12
|
+
|
|
13
|
+
const normalizeCompilerOption = value => (typeof value === 'string' ? value.trim().toLowerCase() : '');
|
|
14
|
+
|
|
15
|
+
const hasNodeNextModuleResolution = compilerOptions => {
|
|
16
|
+
if (!compilerOptions || typeof compilerOptions !== 'object') return false;
|
|
17
|
+
const moduleResolution = normalizeCompilerOption(compilerOptions.moduleResolution);
|
|
18
|
+
const moduleOption = normalizeCompilerOption(compilerOptions.module);
|
|
19
|
+
return (
|
|
20
|
+
NODE_NEXT_MODULE_RESOLUTIONS.has(moduleResolution) || NODE_NEXT_MODULE_RESOLUTIONS.has(moduleOption)
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const resolveExtendsPath = (extendsValue, baseDir) => {
|
|
25
|
+
if (!extendsValue || typeof extendsValue !== 'string') return undefined;
|
|
26
|
+
const candidatePaths = [];
|
|
27
|
+
const normalizedValue = extendsValue.trim();
|
|
28
|
+
candidatePaths.push(path.resolve(baseDir, normalizedValue));
|
|
29
|
+
if (!path.extname(normalizedValue)) {
|
|
30
|
+
candidatePaths.push(`${path.resolve(baseDir, normalizedValue)}.json`);
|
|
31
|
+
}
|
|
32
|
+
for (const candidate of candidatePaths) {
|
|
33
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
return nodeRequire.resolve(normalizedValue, { paths: [baseDir] });
|
|
37
|
+
} catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const inspectTsConfig = (configPath, visited) => {
|
|
43
|
+
if (!configPath) return false;
|
|
44
|
+
const normalized = path.resolve(configPath);
|
|
45
|
+
if (visited.has(normalized)) return false;
|
|
46
|
+
if (!fs.existsSync(normalized)) return false;
|
|
47
|
+
visited.add(normalized);
|
|
48
|
+
let raw;
|
|
49
|
+
try {
|
|
50
|
+
raw = fs.readFileSync(normalized, 'utf8');
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = JSON.parse(raw);
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (hasNodeNextModuleResolution(parsed.compilerOptions)) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (parsed.extends) {
|
|
64
|
+
const extended = resolveExtendsPath(parsed.extends, path.dirname(normalized));
|
|
65
|
+
if (extended && inspectTsConfig(extended, visited)) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const discoverTsConfigPaths = cwd => {
|
|
73
|
+
const candidates = new Set();
|
|
74
|
+
for (const name of TS_CONFIG_BASE_NAMES) {
|
|
75
|
+
const fullPath = path.join(cwd, name);
|
|
76
|
+
if (fs.existsSync(fullPath)) {
|
|
77
|
+
candidates.add(fullPath);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const entries = fs.readdirSync(cwd);
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const lower = entry.toLowerCase();
|
|
84
|
+
if (lower.startsWith('tsconfig') && lower.endsWith('.json')) {
|
|
85
|
+
candidates.add(path.join(cwd, entry));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// ignore readdir errors
|
|
90
|
+
}
|
|
91
|
+
return Array.from(candidates);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const shouldUseJsImportExtensions = cwd => {
|
|
95
|
+
const paths = discoverTsConfigPaths(cwd);
|
|
96
|
+
if (!paths.length) return false;
|
|
97
|
+
const visited = new Set();
|
|
98
|
+
for (const configPath of paths) {
|
|
99
|
+
if (inspectTsConfig(configPath, visited)) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
};
|
|
6
105
|
|
|
7
106
|
export const parseOptions = (argv = process.argv.slice(2), env = process.env, cwd = process.cwd()) => {
|
|
8
107
|
const parser = {
|
|
@@ -52,6 +151,8 @@ export const parseOptions = (argv = process.argv.slice(2), env = process.env, cw
|
|
|
52
151
|
dryRun: Boolean(values['dry-run'])
|
|
53
152
|
};
|
|
54
153
|
|
|
154
|
+
opts.useJsImportExtensions = shouldUseJsImportExtensions(cwd);
|
|
155
|
+
|
|
55
156
|
if (!DIALECTS.has(opts.dialect)) {
|
|
56
157
|
throw new Error(`Unsupported dialect "${opts.dialect}". Supported: ${Array.from(DIALECTS).join(', ')}`);
|
|
57
158
|
}
|
|
@@ -4,11 +4,47 @@ import { buildSchemaMetadata } from './schema.mjs';
|
|
|
4
4
|
|
|
5
5
|
const escapeJsString = value => value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
6
6
|
|
|
7
|
+
const formatJsDoc = comment => {
|
|
8
|
+
if (!comment) return null;
|
|
9
|
+
const normalized = comment.replace(/\r\n?/g, '\n').trim();
|
|
10
|
+
if (!normalized) return null;
|
|
11
|
+
const lines = normalized.split('\n').map(line => line.replace(/\*\//g, '*\\/'));
|
|
12
|
+
return ['/**', ...lines.map(line => ` * ${line}`), ' */'].join('\n');
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const appendJsDoc = (lines, comment, indent = '') => {
|
|
16
|
+
const doc = formatJsDoc(comment);
|
|
17
|
+
if (!doc) return;
|
|
18
|
+
doc.split('\n').forEach(line => lines.push(`${indent}${line}`));
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const normalizeReferenceTable = (refTable, sourceSchema, defaultSchema) => {
|
|
22
|
+
if (!refTable || typeof refTable !== 'string') return refTable;
|
|
23
|
+
const parts = refTable.split('.');
|
|
24
|
+
if (parts.length === 1) return refTable;
|
|
25
|
+
|
|
26
|
+
if (parts.length === 2) {
|
|
27
|
+
const [schema, table] = parts;
|
|
28
|
+
return schema === sourceSchema || (defaultSchema && schema === defaultSchema) ? table : refTable;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (parts.length === 3) {
|
|
32
|
+
const [db, schema, table] = parts;
|
|
33
|
+
if (schema === sourceSchema || (defaultSchema && schema === defaultSchema)) {
|
|
34
|
+
return `${db}.${table}`;
|
|
35
|
+
}
|
|
36
|
+
return refTable;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return refTable;
|
|
40
|
+
};
|
|
41
|
+
|
|
7
42
|
const parseColumnType = colTypeRaw => {
|
|
8
43
|
const type = (colTypeRaw || '').toLowerCase();
|
|
9
44
|
const lengthMatch = type.match(/\((\d+)(?:\s*,\s*(\d+))?\)/);
|
|
10
45
|
const length = lengthMatch ? Number(lengthMatch[1]) : undefined;
|
|
11
46
|
const scale = lengthMatch && lengthMatch[2] ? Number(lengthMatch[2]) : undefined;
|
|
47
|
+
const isMaxLength = /\(max\)/.test(type);
|
|
12
48
|
|
|
13
49
|
const base = type.replace(/\(.*\)/, '');
|
|
14
50
|
|
|
@@ -18,7 +54,8 @@ const parseColumnType = colTypeRaw => {
|
|
|
18
54
|
if (base.includes('uuid') || base.includes('uniqueidentifier')) return { factory: 'col.uuid()', ts: 'string' };
|
|
19
55
|
if (base === 'date') return { factory: 'col.date<Date>()', ts: 'Date' };
|
|
20
56
|
if (base.includes('datetime') || base === 'time') return { factory: 'col.datetime<Date>()', ts: 'Date' };
|
|
21
|
-
if (base.includes('
|
|
57
|
+
if (base.includes('text') || (isMaxLength && base.includes('char'))) return { factory: 'col.text()', ts: 'string' };
|
|
58
|
+
if (base.includes('char')) {
|
|
22
59
|
const lenArg = length ? `${length}` : '255';
|
|
23
60
|
return { factory: `col.varchar(${lenArg})`, ts: 'string' };
|
|
24
61
|
}
|
|
@@ -64,7 +101,65 @@ const normalizeDefault = value => {
|
|
|
64
101
|
return { kind: 'value', code: JSON.stringify(value) };
|
|
65
102
|
};
|
|
66
103
|
|
|
67
|
-
const
|
|
104
|
+
const formatDefaultValueForDoc = value => {
|
|
105
|
+
const normalized = normalizeDefault(value);
|
|
106
|
+
if (!normalized) return null;
|
|
107
|
+
const code = normalized.code;
|
|
108
|
+
if (typeof code !== 'string') {
|
|
109
|
+
return `${code}`;
|
|
110
|
+
}
|
|
111
|
+
if (normalized.kind === 'raw') {
|
|
112
|
+
const trimmed = code.trim();
|
|
113
|
+
if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
114
|
+
return trimmed.slice(1, -1);
|
|
115
|
+
}
|
|
116
|
+
return trimmed;
|
|
117
|
+
}
|
|
118
|
+
return code;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const buildColumnRemarks = column => {
|
|
122
|
+
const notes = [];
|
|
123
|
+
if (column.autoIncrement) {
|
|
124
|
+
notes.push('Auto-increment identity column');
|
|
125
|
+
}
|
|
126
|
+
if (column.generated) {
|
|
127
|
+
const verb = column.generated === 'always' ? 'Generated always' : 'Generated by default';
|
|
128
|
+
notes.push(`${verb} column`);
|
|
129
|
+
}
|
|
130
|
+
if (column.check) {
|
|
131
|
+
notes.push(`Check constraint: ${column.check}`);
|
|
132
|
+
}
|
|
133
|
+
if (column.references) {
|
|
134
|
+
const ref = column.references;
|
|
135
|
+
const actions = [];
|
|
136
|
+
if (ref.onDelete) actions.push(`on delete ${ref.onDelete}`);
|
|
137
|
+
if (ref.onUpdate) actions.push(`on update ${ref.onUpdate}`);
|
|
138
|
+
const actionSuffix = actions.length ? ` (${actions.join(', ')})` : '';
|
|
139
|
+
notes.push(`References ${ref.table}.${ref.column}${actionSuffix}`);
|
|
140
|
+
}
|
|
141
|
+
if (!notes.length) return null;
|
|
142
|
+
return notes.join('; ');
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const buildColumnDoc = column => {
|
|
146
|
+
const entries = [];
|
|
147
|
+
if (column.comment) {
|
|
148
|
+
entries.push(column.comment);
|
|
149
|
+
}
|
|
150
|
+
const defaultValue = formatDefaultValueForDoc(column.default);
|
|
151
|
+
if (defaultValue) {
|
|
152
|
+
entries.push(`@defaultValue ${defaultValue}`);
|
|
153
|
+
}
|
|
154
|
+
const remarks = buildColumnRemarks(column);
|
|
155
|
+
if (remarks) {
|
|
156
|
+
entries.push(`@remarks ${remarks}`);
|
|
157
|
+
}
|
|
158
|
+
if (!entries.length) return null;
|
|
159
|
+
return entries.join('\n');
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const renderColumnExpression = (column, tablePk, tableSchema, defaultSchema) => {
|
|
68
163
|
const base = parseColumnType(column.type);
|
|
69
164
|
let expr = base.factory;
|
|
70
165
|
|
|
@@ -88,8 +183,9 @@ const renderColumnExpression = (column, tablePk) => {
|
|
|
88
183
|
}
|
|
89
184
|
}
|
|
90
185
|
if (column.references) {
|
|
186
|
+
const refTable = normalizeReferenceTable(column.references.table, tableSchema, defaultSchema);
|
|
91
187
|
const refParts = [
|
|
92
|
-
`table: '${escapeJsString(
|
|
188
|
+
`table: '${escapeJsString(refTable)}'`,
|
|
93
189
|
`column: '${escapeJsString(column.references.column)}'`
|
|
94
190
|
];
|
|
95
191
|
if (column.references.onDelete) refParts.push(`onDelete: '${escapeJsString(column.references.onDelete)}'`);
|
|
@@ -106,7 +202,8 @@ const renderColumnExpression = (column, tablePk) => {
|
|
|
106
202
|
decorator,
|
|
107
203
|
expr,
|
|
108
204
|
tsType,
|
|
109
|
-
optional
|
|
205
|
+
optional,
|
|
206
|
+
comment: buildColumnDoc(column)
|
|
110
207
|
};
|
|
111
208
|
};
|
|
112
209
|
|
|
@@ -126,16 +223,20 @@ const METAL_ORM_IMPORT_ORDER = [
|
|
|
126
223
|
'getTableDefFromEntity'
|
|
127
224
|
];
|
|
128
225
|
|
|
129
|
-
const renderEntityClassLines = ({ table, className, naming, relations, resolveClassName }) => {
|
|
226
|
+
const renderEntityClassLines = ({ table, className, naming, relations, resolveClassName, defaultSchema }) => {
|
|
130
227
|
const lines = [];
|
|
131
228
|
const derivedDefault = naming.defaultTableNameFromClass(className);
|
|
132
229
|
const needsTableNameOption = table.name !== derivedDefault;
|
|
133
230
|
const entityOpts = needsTableNameOption ? `{ tableName: '${escapeJsString(table.name)}' }` : '';
|
|
231
|
+
if (table.comment) {
|
|
232
|
+
appendJsDoc(lines, table.comment);
|
|
233
|
+
}
|
|
134
234
|
lines.push(`@Entity(${entityOpts})`);
|
|
135
235
|
lines.push(`export class ${className} {`);
|
|
136
236
|
|
|
137
237
|
for (const col of table.columns) {
|
|
138
|
-
const rendered = renderColumnExpression(col, table.primaryKey);
|
|
238
|
+
const rendered = renderColumnExpression(col, table.primaryKey, table.schema, defaultSchema);
|
|
239
|
+
appendJsDoc(lines, rendered.comment, ' ');
|
|
139
240
|
lines.push(` @${rendered.decorator}(${rendered.expr})`);
|
|
140
241
|
lines.push(` ${col.name}${rendered.optional ? '?:' : '!:'} ${rendered.tsType};`);
|
|
141
242
|
lines.push('');
|
|
@@ -188,7 +289,7 @@ const renderEntityClassLines = ({ table, className, naming, relations, resolveCl
|
|
|
188
289
|
return lines;
|
|
189
290
|
};
|
|
190
291
|
|
|
191
|
-
const computeTableUsage = (table, relations) => {
|
|
292
|
+
const computeTableUsage = (table, relations, defaultSchema) => {
|
|
192
293
|
const usage = {
|
|
193
294
|
needsCol: false,
|
|
194
295
|
needsEntity: true,
|
|
@@ -205,7 +306,7 @@ const computeTableUsage = (table, relations) => {
|
|
|
205
306
|
|
|
206
307
|
for (const col of table.columns) {
|
|
207
308
|
usage.needsCol = true;
|
|
208
|
-
const rendered = renderColumnExpression(col, table.primaryKey);
|
|
309
|
+
const rendered = renderColumnExpression(col, table.primaryKey, table.schema, defaultSchema);
|
|
209
310
|
if (rendered.decorator === 'PrimaryKey') {
|
|
210
311
|
usage.needsPrimaryKeyDecorator = true;
|
|
211
312
|
} else {
|
|
@@ -257,11 +358,16 @@ const buildMetalOrmImportStatement = names => {
|
|
|
257
358
|
return `import { ${ordered.join(', ')} } from 'metal-orm';`;
|
|
258
359
|
};
|
|
259
360
|
|
|
260
|
-
const
|
|
361
|
+
const appendJsExtension = (specifier, useExtension) => (useExtension ? `${specifier}.js` : specifier);
|
|
362
|
+
|
|
363
|
+
const getRelativeModuleSpecifier = (from, to, extension = '') => {
|
|
261
364
|
const rel = path.relative(path.dirname(from), to).replace(/\\/g, '/');
|
|
262
|
-
if (!rel)
|
|
365
|
+
if (!rel) {
|
|
366
|
+
return './';
|
|
367
|
+
}
|
|
263
368
|
const withoutExt = rel.replace(/\.ts$/i, '');
|
|
264
|
-
|
|
369
|
+
const normalized = withoutExt.startsWith('.') ? withoutExt : `./${withoutExt}`;
|
|
370
|
+
return `${normalized}${extension}`;
|
|
265
371
|
};
|
|
266
372
|
|
|
267
373
|
export const renderEntityFile = (schema, options) => {
|
|
@@ -285,7 +391,7 @@ export const renderEntityFile = (schema, options) => {
|
|
|
285
391
|
|
|
286
392
|
for (const table of tables) {
|
|
287
393
|
const rels = relations.get(table.name) || [];
|
|
288
|
-
const tableUsage = computeTableUsage(table, rels);
|
|
394
|
+
const tableUsage = computeTableUsage(table, rels, options.schema);
|
|
289
395
|
aggregateUsage.needsCol ||= tableUsage.needsCol;
|
|
290
396
|
aggregateUsage.needsColumnDecorator ||= tableUsage.needsColumnDecorator;
|
|
291
397
|
aggregateUsage.needsPrimaryKeyDecorator ||= tableUsage.needsPrimaryKeyDecorator;
|
|
@@ -318,7 +424,8 @@ export const renderEntityFile = (schema, options) => {
|
|
|
318
424
|
className,
|
|
319
425
|
naming,
|
|
320
426
|
relations: relations.get(table.name) || [],
|
|
321
|
-
resolveClassName: metadata.resolveClassName
|
|
427
|
+
resolveClassName: metadata.resolveClassName,
|
|
428
|
+
defaultSchema: options.schema
|
|
322
429
|
});
|
|
323
430
|
lines.push(...classLines);
|
|
324
431
|
}
|
|
@@ -345,7 +452,7 @@ export const renderSplitEntityFiles = (schema, options) => {
|
|
|
345
452
|
for (const table of metadata.tables) {
|
|
346
453
|
const className = metadata.classNames.get(table.name);
|
|
347
454
|
const relations = metadata.relations.get(table.name) || [];
|
|
348
|
-
const usage = computeTableUsage(table, relations);
|
|
455
|
+
const usage = computeTableUsage(table, relations, options.schema);
|
|
349
456
|
const metalImportNames = getMetalOrmImportNamesFromUsage(usage);
|
|
350
457
|
const metalImportStatement = buildMetalOrmImportStatement(metalImportNames);
|
|
351
458
|
|
|
@@ -368,7 +475,8 @@ export const renderSplitEntityFiles = (schema, options) => {
|
|
|
368
475
|
importLines.push(metalImportStatement);
|
|
369
476
|
}
|
|
370
477
|
for (const targetClass of Array.from(relationImports).sort()) {
|
|
371
|
-
|
|
478
|
+
const specifier = appendJsExtension(`./${targetClass}`, options.useJsImportExtensions);
|
|
479
|
+
importLines.push(`import { ${targetClass} } from '${specifier}';`);
|
|
372
480
|
}
|
|
373
481
|
|
|
374
482
|
const lines = [
|
|
@@ -384,7 +492,8 @@ export const renderSplitEntityFiles = (schema, options) => {
|
|
|
384
492
|
className,
|
|
385
493
|
naming,
|
|
386
494
|
relations,
|
|
387
|
-
resolveClassName: metadata.resolveClassName
|
|
495
|
+
resolveClassName: metadata.resolveClassName,
|
|
496
|
+
defaultSchema: options.schema
|
|
388
497
|
});
|
|
389
498
|
lines.push(...classLines);
|
|
390
499
|
|
|
@@ -404,7 +513,11 @@ export const renderSplitIndexFile = (metadata, options) => {
|
|
|
404
513
|
for (const table of metadata.tables) {
|
|
405
514
|
const className = metadata.classNames.get(table.name);
|
|
406
515
|
const filePath = path.join(options.outDir, `${className}.ts`);
|
|
407
|
-
const moduleSpecifier = getRelativeModuleSpecifier(
|
|
516
|
+
const moduleSpecifier = getRelativeModuleSpecifier(
|
|
517
|
+
options.out,
|
|
518
|
+
filePath,
|
|
519
|
+
options.useJsImportExtensions ? '.js' : ''
|
|
520
|
+
);
|
|
408
521
|
importLines.push(`import { ${className} } from '${moduleSpecifier}';`);
|
|
409
522
|
exportedClasses.push(className);
|
|
410
523
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const stripDiacritics = value =>
|
|
2
|
+
String(value || '')
|
|
3
|
+
.normalize('NFD')
|
|
4
|
+
.replace(/[\u0300-\u036f]/g, '');
|
|
5
|
+
|
|
6
|
+
export const normalizeLookup = value => stripDiacritics(value).toLowerCase().trim();
|
|
7
|
+
|
|
8
|
+
export const detectTextFormat = text => {
|
|
9
|
+
if (/^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$/.test(text)) return 'SCREAMING_SNAKE';
|
|
10
|
+
if (/^[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(text)) return 'snake_case';
|
|
11
|
+
if (/^[A-Z][a-zA-Z0-9]*$/.test(text) && /[a-z]/.test(text)) return 'PascalCase';
|
|
12
|
+
if (/^[a-z][a-zA-Z0-9]*$/.test(text) && /[A-Z]/.test(text)) return 'camelCase';
|
|
13
|
+
return 'normal';
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const splitIntoWords = (text, format) => {
|
|
17
|
+
switch (format) {
|
|
18
|
+
case 'camelCase':
|
|
19
|
+
case 'PascalCase':
|
|
20
|
+
return text
|
|
21
|
+
.replace(/([a-z])([A-Z])/g, '$1\0$2')
|
|
22
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1\0$2')
|
|
23
|
+
.split('\0')
|
|
24
|
+
.map(p => p.toLowerCase());
|
|
25
|
+
case 'snake_case':
|
|
26
|
+
return text.toLowerCase().split('_');
|
|
27
|
+
case 'SCREAMING_SNAKE':
|
|
28
|
+
return text.toLowerCase().split('_');
|
|
29
|
+
default:
|
|
30
|
+
return text.toLowerCase().split(/\s+/);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const capitalize = value => (value ? value.charAt(0).toUpperCase() + value.slice(1).toLowerCase() : '');
|
|
35
|
+
|
|
36
|
+
export const rebuildFromWords = (words, format, originalText) => {
|
|
37
|
+
switch (format) {
|
|
38
|
+
case 'camelCase':
|
|
39
|
+
return words.map((w, i) => (i === 0 ? w : capitalize(w))).join('');
|
|
40
|
+
case 'PascalCase':
|
|
41
|
+
return words.map(capitalize).join('');
|
|
42
|
+
case 'snake_case':
|
|
43
|
+
return words.join('_');
|
|
44
|
+
case 'SCREAMING_SNAKE':
|
|
45
|
+
return words.map(w => w.toUpperCase()).join('_');
|
|
46
|
+
default: {
|
|
47
|
+
const result = words.join(' ');
|
|
48
|
+
return originalText && originalText[0] === originalText[0].toUpperCase() ? capitalize(result) : result;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const applyToCompoundHead = (term, { connectors, transformWord } = {}) => {
|
|
54
|
+
if (!term || !String(term).trim()) return '';
|
|
55
|
+
const original = String(term).trim();
|
|
56
|
+
const format = detectTextFormat(original);
|
|
57
|
+
const words = splitIntoWords(original, format);
|
|
58
|
+
|
|
59
|
+
let transformed = false;
|
|
60
|
+
const out = words.map(word => {
|
|
61
|
+
const normalized = normalizeLookup(word);
|
|
62
|
+
if (connectors?.has?.(normalized)) return word;
|
|
63
|
+
if (!transformed) {
|
|
64
|
+
transformed = true;
|
|
65
|
+
return transformWord ? transformWord(word) : word;
|
|
66
|
+
}
|
|
67
|
+
return word;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return rebuildFromWords(out, format, original);
|
|
71
|
+
};
|
|
72
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const EN_DEFAULT_IRREGULARS = {};
|
|
2
|
+
|
|
3
|
+
export const pluralizeWordEn = word => {
|
|
4
|
+
const lower = String(word || '').toLowerCase();
|
|
5
|
+
if (!lower) return '';
|
|
6
|
+
if (lower.endsWith('y')) return `${lower.slice(0, -1)}ies`;
|
|
7
|
+
if (lower.endsWith('s')) return `${lower}es`;
|
|
8
|
+
return `${lower}s`;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const singularizeWordEn = word => {
|
|
12
|
+
const lower = String(word || '').toLowerCase();
|
|
13
|
+
if (!lower) return '';
|
|
14
|
+
if (lower.endsWith('ies')) return `${lower.slice(0, -3)}y`;
|
|
15
|
+
if (lower.endsWith('ses')) return lower.slice(0, -2);
|
|
16
|
+
if (lower.endsWith('s')) return lower.slice(0, -1);
|
|
17
|
+
return lower;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const createEnglishInflector = () => ({
|
|
21
|
+
locale: 'en',
|
|
22
|
+
defaultIrregulars: EN_DEFAULT_IRREGULARS,
|
|
23
|
+
pluralizeWord: pluralizeWordEn,
|
|
24
|
+
singularizeWord: singularizeWordEn
|
|
25
|
+
});
|
|
26
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createEnglishInflector } from './en.mjs';
|
|
2
|
+
import { createPtBrInflector } from './pt-br.mjs';
|
|
3
|
+
|
|
4
|
+
const INFLECTOR_FACTORIES = new Map();
|
|
5
|
+
|
|
6
|
+
export const registerInflector = (localePrefix, factory) => {
|
|
7
|
+
const key = String(localePrefix || '').toLowerCase();
|
|
8
|
+
if (!key) throw new Error('localePrefix is required');
|
|
9
|
+
if (typeof factory !== 'function') throw new Error('factory must be a function that returns an inflector');
|
|
10
|
+
INFLECTOR_FACTORIES.set(key, factory);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
registerInflector('en', createEnglishInflector);
|
|
14
|
+
registerInflector('pt', createPtBrInflector);
|
|
15
|
+
|
|
16
|
+
export const resolveInflector = locale => {
|
|
17
|
+
const normalized = String(locale || 'en').toLowerCase();
|
|
18
|
+
let bestFactory = undefined;
|
|
19
|
+
let bestPrefixLength = 0;
|
|
20
|
+
for (const [prefix, factory] of INFLECTOR_FACTORIES) {
|
|
21
|
+
if (!normalized.startsWith(prefix)) continue;
|
|
22
|
+
if (prefix.length > bestPrefixLength) {
|
|
23
|
+
bestPrefixLength = prefix.length;
|
|
24
|
+
bestFactory = factory;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (bestFactory) return bestFactory();
|
|
28
|
+
return createEnglishInflector();
|
|
29
|
+
};
|