@iyulab/m3l 0.1.4 → 0.5.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/index.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Parse a single M3L file and return the AST as JSON.
3
+ *
4
+ * @param content - M3L markdown text
5
+ * @param filename - Source filename for error reporting
6
+ * @returns JSON string with `{ success: boolean, data?: AST, error?: string }`
7
+ */
8
+ export function parse(content: string, filename: string): string;
9
+
10
+ /**
11
+ * Parse multiple M3L files and return the merged AST as JSON.
12
+ *
13
+ * @param filesJson - JSON array of `{ content: string, filename: string }` objects
14
+ * @returns JSON string with `{ success: boolean, data?: AST, error?: string }`
15
+ */
16
+ export function parseMulti(filesJson: string): string;
17
+
18
+ /**
19
+ * Validate M3L content and return diagnostics as JSON.
20
+ *
21
+ * @param content - M3L markdown text
22
+ * @param optionsJson - JSON options `{ strict?: boolean, filename?: string }`
23
+ * @returns JSON string with `{ success: boolean, data?: ValidateResult, error?: string }`
24
+ */
25
+ export function validate(content: string, optionsJson: string): string;
package/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @iyulab/m3l — M3L parser (Rust native via NAPI)
3
+ *
4
+ * Thin wrapper that re-exports the native NAPI addon.
5
+ * All parsing is performed by the Rust m3l-core library.
6
+ */
7
+
8
+ const { parse, parseMulti, validate } = require('@iyulab/m3l-napi');
9
+
10
+ module.exports.parse = parse;
11
+ module.exports.parseMulti = parseMulti;
12
+ module.exports.validate = validate;
package/package.json CHANGED
@@ -1,34 +1,29 @@
1
1
  {
2
2
  "name": "@iyulab/m3l",
3
- "version": "0.1.4",
4
- "description": "M3L parser and CLI tool parse .m3l.md files into JSON AST",
5
- "type": "module",
6
- "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
8
- "bin": {
9
- "m3l": "dist/cli.js"
10
- },
11
- "scripts": {
12
- "build": "tsc",
13
- "test": "vitest run",
14
- "test:watch": "vitest",
15
- "lint": "tsc --noEmit"
16
- },
3
+ "version": "0.5.0",
4
+ "description": "M3L parser Markdown-based schema definition language (.m3l.md JSON AST)",
5
+ "license": "MIT",
6
+ "main": "index.js",
7
+ "types": "index.d.ts",
8
+ "files": [
9
+ "index.js",
10
+ "index.d.ts"
11
+ ],
17
12
  "dependencies": {
18
- "commander": "^13.0.0",
19
- "yaml": "^2.7.0",
20
- "fast-glob": "^3.3.0"
21
- },
22
- "devDependencies": {
23
- "typescript": "^5.7.0",
24
- "vitest": "^3.0.0",
25
- "@types/node": "^22.0.0"
13
+ "@iyulab/m3l-napi": "0.5.0"
26
14
  },
27
15
  "engines": {
28
- "node": ">=20"
16
+ "node": ">= 18"
29
17
  },
30
- "files": [
31
- "dist"
32
- ],
33
- "license": "MIT"
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/iyulab/m3l"
21
+ },
22
+ "keywords": [
23
+ "m3l",
24
+ "markdown",
25
+ "schema",
26
+ "parser",
27
+ "ast"
28
+ ]
34
29
  }
package/README.md DELETED
@@ -1,305 +0,0 @@
1
- # @iyulab/m3l
2
-
3
- [![npm](https://img.shields.io/npm/v/@iyulab/m3l)](https://www.npmjs.com/package/@iyulab/m3l)
4
- [![TypeScript CI](https://github.com/iyulab/m3l/actions/workflows/parser-publish.yml/badge.svg)](https://github.com/iyulab/m3l/actions/workflows/parser-publish.yml)
5
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](../../LICENSE)
6
-
7
- M3L (Meta Model Markup Language) parser and CLI — parse `.m3l.md` / `.m3l` files into a structured JSON AST.
8
-
9
- M3L is a Markdown-based data modeling language. You write data models in readable Markdown, and this parser converts them into a machine-processable AST with full validation.
10
-
11
- ## Install
12
-
13
- ```bash
14
- npm install @iyulab/m3l
15
- ```
16
-
17
- ## Quick Start
18
-
19
- ### As a Library
20
-
21
- ```typescript
22
- import { parse, parseString, validateFiles } from '@iyulab/m3l';
23
-
24
- // Parse a file or directory
25
- const ast = await parse('./models');
26
-
27
- // Parse a string
28
- const ast2 = parseString(`
29
- ## User
30
- - name: string(100) @not_null
31
- - email: string(320)? @unique
32
-
33
- ## UserRole ::enum
34
- - admin: "Administrator"
35
- - user: "Regular User"
36
- `);
37
-
38
- console.log(ast2.models); // [{ name: 'User', fields: [...], ... }]
39
- console.log(ast2.enums); // [{ name: 'UserRole', values: [...], ... }]
40
-
41
- // Validate with diagnostics
42
- const { ast: validated, errors, warnings } = await validateFiles('./models');
43
- ```
44
-
45
- ### As a CLI
46
-
47
- ```bash
48
- # Parse and output JSON AST
49
- npx m3l parse ./models
50
-
51
- # Parse a single file
52
- npx m3l parse ./models/user.m3l.md -o ast.json
53
-
54
- # Validate
55
- npx m3l validate ./models
56
-
57
- # Validate with strict style checks and JSON output
58
- npx m3l validate ./models --strict --format json
59
- ```
60
-
61
- ## M3L Syntax
62
-
63
- ```markdown
64
- # Library System
65
-
66
- ## Author
67
- - name(Author Name): string(100) @not_null @idx
68
- - bio(Biography): text?
69
- - birth_date: date?
70
-
71
- ### Rollup
72
- - book_count: integer @rollup(BookAuthor.author_id, count)
73
-
74
- > Stores information about book authors.
75
-
76
- ## BookStatus ::enum "Status of a book"
77
- - available: "Available"
78
- - borrowed: "Borrowed"
79
- - reserved: "Reserved"
80
-
81
- ## Book : BaseModel
82
- - title: string(200) @not_null @idx
83
- - isbn: string(20) @unique
84
- - status: enum = "available"
85
- - available: "Available"
86
- - borrowed: "Borrowed"
87
- - publisher_id: identifier @fk(Publisher.id)
88
-
89
- ### Lookup
90
- - publisher_name: string @lookup(publisher_id.name)
91
-
92
- ### Computed
93
- - is_available: boolean @computed("status = 'available' AND quantity > 0")
94
-
95
- ## OverdueLoans ::view @materialized
96
- > Currently overdue book loans.
97
- ### Source
98
- - from: Loan
99
- - where: "due_date < now() AND status = 'ongoing'"
100
- - order_by: due_date asc
101
- - borrower_name: string @lookup(member_id.name)
102
- ```
103
-
104
- **Key syntax elements:**
105
-
106
- | Syntax | Meaning |
107
- |--------|---------|
108
- | `# Title` | Document title or namespace (`# Namespace: domain.example`) |
109
- | `## Name` | Model definition |
110
- | `## Name : Parent` | Model with inheritance |
111
- | `## Name ::enum` | Enum definition |
112
- | `## Name ::interface` | Interface definition |
113
- | `## Name ::view` | Derived view |
114
- | `## @name ::attribute` | Attribute registry entry |
115
- | `- field: type` | Field definition |
116
- | `- field: type?` | Nullable field |
117
- | `- field: type[]` | Array field |
118
- | `- field: type?[]` | Array of nullable items |
119
- | `- field: type = val` | Field with default value |
120
- | `@attr` / `@attr(args)` | Attribute (constraint, index, etc.) |
121
- | `` `[FrameworkAttr]` `` | Custom framework attribute |
122
- | `### Lookup` / `### Rollup` / `### Computed` | Kind section for derived fields |
123
- | `### Section` | Named section (Indexes, Relations, Metadata, etc.) |
124
- | `> text` | Model/element description |
125
- | `"text"` | Inline description on field |
126
-
127
- ## AST Output
128
-
129
- The parser produces an `M3LAST` object:
130
-
131
- ```typescript
132
- interface M3LAST {
133
- parserVersion: string; // Parser package version (semver)
134
- astVersion: string; // AST schema version
135
- project: { name?: string; version?: string };
136
- sources: string[];
137
- models: ModelNode[];
138
- enums: EnumNode[];
139
- interfaces: ModelNode[];
140
- views: ModelNode[];
141
- attributeRegistry: AttributeRegistryEntry[];
142
- errors: Diagnostic[];
143
- warnings: Diagnostic[];
144
- }
145
- ```
146
-
147
- ### Key AST types
148
-
149
- ```typescript
150
- interface FieldNode {
151
- name: string;
152
- label?: string;
153
- type?: string;
154
- params?: (string | number)[];
155
- generic_params?: string[]; // map<K,V> -> ["K", "V"]
156
- nullable: boolean;
157
- array: boolean;
158
- arrayItemNullable: boolean; // string?[] -> true
159
- kind: 'stored' | 'computed' | 'lookup' | 'rollup';
160
- default_value?: string;
161
- description?: string;
162
- attributes: FieldAttribute[];
163
- framework_attrs?: CustomAttribute[];
164
- lookup?: { path: string };
165
- rollup?: { target: string; fk: string; aggregate: string; field?: string; where?: string };
166
- computed?: { expression: string };
167
- enum_values?: EnumValue[];
168
- fields?: FieldNode[]; // sub-fields for object type
169
- loc: SourceLocation;
170
- }
171
-
172
- interface FieldAttribute {
173
- name: string;
174
- args?: (string | number | boolean)[];
175
- cascade?: string;
176
- isStandard?: boolean; // true for M3L standard attributes
177
- isRegistered?: boolean; // true for attributes in the registry
178
- }
179
-
180
- interface CustomAttribute {
181
- content: string; // e.g. "MaxLength(100)"
182
- raw: string; // e.g. "[MaxLength(100)]"
183
- parsed?: { // structured parse result
184
- name: string;
185
- arguments: (string | number | boolean)[];
186
- };
187
- }
188
-
189
- interface AttributeRegistryEntry {
190
- name: string;
191
- description?: string;
192
- target: ('field' | 'model')[];
193
- type: string;
194
- range?: [number, number];
195
- required: boolean;
196
- defaultValue?: string | number | boolean;
197
- }
198
- ```
199
-
200
- ## Attribute Registry
201
-
202
- Define custom attributes with validation metadata using `::attribute`:
203
-
204
- ```markdown
205
- ## @pii ::attribute
206
- > Personal identifiable information marker
207
- - target: [field]
208
- - type: boolean
209
- - default: false
210
-
211
- ## @audit_level ::attribute
212
- > Audit compliance level
213
- - target: [field, model]
214
- - type: integer
215
- - range: [1, 5]
216
- - default: 1
217
- ```
218
-
219
- Attributes are classified into 3 tiers:
220
-
221
- | Tier | `isStandard` | `isRegistered` | Example |
222
- |------|-------------|----------------|---------|
223
- | Standard | `true` | — | `@primary`, `@unique`, `@reference` |
224
- | Registered | — | `true` | `@pii`, `@audit_level` (defined via `::attribute`) |
225
- | Unregistered | — | — | `@some_unknown_attr` |
226
-
227
- ## Validation
228
-
229
- The validator checks for semantic errors and style warnings:
230
-
231
- **Errors:**
232
- | Code | Description |
233
- |------|-------------|
234
- | M3L-E001 | Rollup FK field missing `@reference` |
235
- | M3L-E002 | Lookup FK field missing `@reference` |
236
- | M3L-E004 | View references non-existent model |
237
- | M3L-E005 | Duplicate model/enum name |
238
- | M3L-E006 | Duplicate field name within model |
239
- | M3L-E007 | Unresolved parent in inheritance |
240
-
241
- **Warnings (--strict):**
242
- | Code | Description |
243
- |------|-------------|
244
- | M3L-W001 | Field line exceeds 80 characters |
245
- | M3L-W002 | Object nesting exceeds 3 levels |
246
- | M3L-W004 | Lookup chain exceeds 3 hops |
247
-
248
- ## Multi-file Projects
249
-
250
- Place `.m3l.md` or `.m3l` files in a directory and parse the directory path. The resolver automatically merges all files, resolves inheritance, and detects cross-file references.
251
-
252
- Optionally, create an `m3l.config.yaml`:
253
-
254
- ```yaml
255
- name: my-project
256
- version: 1.0.0
257
- sources:
258
- - "models/**/*.m3l.md"
259
- - "shared/*.m3l"
260
- ```
261
-
262
- ## API Reference
263
-
264
- ### `parse(inputPath: string): Promise<M3LAST>`
265
-
266
- Parse M3L files from a file or directory into a merged AST.
267
-
268
- ### `parseString(content: string, filename?: string): M3LAST`
269
-
270
- Parse an M3L content string into an AST.
271
-
272
- ### `validateFiles(inputPath: string, options?): Promise<{ ast, errors, warnings }>`
273
-
274
- Parse and validate M3L files. Options: `{ strict?: boolean }`.
275
-
276
- ### `STANDARD_ATTRIBUTES: Set<string>`
277
-
278
- The set of 27 M3L standard attribute names (e.g. `primary`, `unique`, `reference`, `computed`, ...).
279
-
280
- ### Lower-level API
281
-
282
- ```typescript
283
- import { lex, parseTokens, resolve, validate } from '@iyulab/m3l';
284
-
285
- const tokens = lex(content, 'file.m3l.md');
286
- const parsed = parseTokens(tokens, 'file.m3l.md');
287
- const ast = resolve([parsed]);
288
- const diagnostics = validate(ast, { strict: true });
289
- ```
290
-
291
- ## Compatibility
292
-
293
- This TypeScript parser and the [C# parser](../csharp/) produce equivalent AST structures. Both share the same conformance test suite.
294
-
295
- | | TypeScript | C# |
296
- |---|---|---|
297
- | Package | `@iyulab/m3l` | `M3LParser` |
298
- | Runtime | Node.js 20+ | .NET 8.0+ |
299
- | AST Version | 1.0 | 1.0 |
300
-
301
- Version is managed centrally via the root `VERSION` file.
302
-
303
- ## License
304
-
305
- MIT
package/dist/cli.d.ts DELETED
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env node
2
- import type { ValidateOptions } from './types.js';
3
- /**
4
- * Run parse command — also used for testing.
5
- */
6
- export declare function runParse(inputPath: string, outputFile?: string): Promise<string>;
7
- /**
8
- * Run validate command — also used for testing.
9
- */
10
- export declare function runValidate(inputPath: string, options?: ValidateOptions, format?: string): Promise<string>;
11
- /**
12
- * Run CLI with args — for testing.
13
- */
14
- export declare function runCLI(args: string[]): Promise<string>;
package/dist/cli.js DELETED
@@ -1,150 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import { writeFileSync } from 'fs';
4
- import { resolve as resolvePath } from 'path';
5
- import { readM3LFiles, readProjectConfig } from './reader.js';
6
- import { parseString as parseFileContent } from './parser.js';
7
- import { resolve } from './resolver.js';
8
- import { validate } from './validator.js';
9
- const program = new Command()
10
- .name('m3l')
11
- .description('M3L parser and validator — parse .m3l.md files into JSON AST')
12
- .version('0.1.0');
13
- program
14
- .command('parse [path]')
15
- .description('Parse M3L files and output JSON AST')
16
- .option('-o, --output <file>', 'Write output to file instead of stdout')
17
- .action(async (inputPath, options) => {
18
- try {
19
- const output = await runParse(inputPath || '.', options?.output);
20
- if (!options?.output) {
21
- process.stdout.write(output + '\n');
22
- }
23
- }
24
- catch (err) {
25
- process.stderr.write(`Error: ${err.message}\n`);
26
- process.exit(1);
27
- }
28
- });
29
- program
30
- .command('validate [path]')
31
- .description('Validate M3L files')
32
- .option('--strict', 'Enable strict style guidelines')
33
- .option('--format <format>', 'Output format: human (default) or json', 'human')
34
- .action(async (inputPath, options) => {
35
- try {
36
- const output = await runValidate(inputPath || '.', { strict: options?.strict }, options?.format || 'human');
37
- process.stdout.write(output + '\n');
38
- // Exit with error code if there are errors
39
- if (output.includes('"errors":') || output.match(/\d+ error/)) {
40
- const errorCount = extractErrorCount(output, options?.format || 'human');
41
- if (errorCount > 0)
42
- process.exit(1);
43
- }
44
- }
45
- catch (err) {
46
- process.stderr.write(`Error: ${err.message}\n`);
47
- process.exit(1);
48
- }
49
- });
50
- // Entry point for direct execution
51
- const args = process.argv.slice(2);
52
- if (args.length > 0) {
53
- program.parse();
54
- }
55
- /**
56
- * Run parse command — also used for testing.
57
- */
58
- export async function runParse(inputPath, outputFile) {
59
- const ast = await buildAST(inputPath);
60
- const json = JSON.stringify(ast, null, 2);
61
- if (outputFile) {
62
- writeFileSync(resolvePath(outputFile), json, 'utf-8');
63
- return `Written to ${outputFile}`;
64
- }
65
- return json;
66
- }
67
- /**
68
- * Run validate command — also used for testing.
69
- */
70
- export async function runValidate(inputPath, options = {}, format = 'human') {
71
- const ast = await buildAST(inputPath);
72
- const result = validate(ast, options);
73
- if (format === 'json') {
74
- return JSON.stringify({
75
- diagnostics: [...result.errors, ...result.warnings],
76
- summary: {
77
- errors: result.errors.length,
78
- warnings: result.warnings.length,
79
- files: ast.sources.length,
80
- },
81
- }, null, 2);
82
- }
83
- // Human-readable format
84
- return formatHumanDiagnostics(result.errors, result.warnings, ast.sources.length);
85
- }
86
- /**
87
- * Run CLI with args — for testing.
88
- */
89
- export async function runCLI(args) {
90
- const cmd = args[0];
91
- const rest = args.slice(1);
92
- if (cmd === 'parse') {
93
- const inputPath = rest.find(a => !a.startsWith('-')) || '.';
94
- const outputIdx = rest.indexOf('-o');
95
- const outputFile = outputIdx >= 0 ? rest[outputIdx + 1] : undefined;
96
- const outputIdx2 = rest.indexOf('--output');
97
- const outputFile2 = outputIdx2 >= 0 ? rest[outputIdx2 + 1] : undefined;
98
- return runParse(inputPath, outputFile || outputFile2);
99
- }
100
- if (cmd === 'validate') {
101
- const inputPath = rest.find(a => !a.startsWith('-')) || '.';
102
- const strict = rest.includes('--strict');
103
- const formatIdx = rest.indexOf('--format');
104
- const format = formatIdx >= 0 ? rest[formatIdx + 1] : 'human';
105
- return runValidate(inputPath, { strict }, format);
106
- }
107
- throw new Error(`Unknown command: ${cmd}`);
108
- }
109
- // --- Internal ---
110
- async function buildAST(inputPath) {
111
- const resolved = resolvePath(inputPath);
112
- const files = await readM3LFiles(resolved);
113
- if (files.length === 0) {
114
- throw new Error(`No .m3l.md files found at: ${inputPath}`);
115
- }
116
- const parsedFiles = files.map(f => parseFileContent(f.content, f.path));
117
- // Try to read project config
118
- let projectInfo;
119
- try {
120
- const config = await readProjectConfig(resolved);
121
- if (config)
122
- projectInfo = config;
123
- }
124
- catch {
125
- // No config — that's fine
126
- }
127
- return resolve(parsedFiles, projectInfo || undefined);
128
- }
129
- function formatHumanDiagnostics(errors, warnings, fileCount) {
130
- const lines = [];
131
- for (const d of [...errors, ...warnings]) {
132
- const severity = d.severity === 'error' ? 'error' : 'warning';
133
- lines.push(`${d.file}:${d.line}:${d.col} ${severity}[${d.code}]: ${d.message}`);
134
- }
135
- lines.push(`${errors.length} error${errors.length !== 1 ? 's' : ''}, ${warnings.length} warning${warnings.length !== 1 ? 's' : ''} in ${fileCount} file${fileCount !== 1 ? 's' : ''}.`);
136
- return lines.join('\n');
137
- }
138
- function extractErrorCount(output, format) {
139
- if (format === 'json') {
140
- try {
141
- const parsed = JSON.parse(output);
142
- return parsed.summary?.errors || 0;
143
- }
144
- catch {
145
- return 0;
146
- }
147
- }
148
- const match = output.match(/(\d+) error/);
149
- return match ? parseInt(match[1], 10) : 0;
150
- }
package/dist/index.d.ts DELETED
@@ -1,23 +0,0 @@
1
- export { lex } from './lexer.js';
2
- export { parseTokens, parseString as parseFileString, STANDARD_ATTRIBUTES } from './parser.js';
3
- export { resolve, AST_VERSION, PARSER_VERSION } from './resolver.js';
4
- export { validate } from './validator.js';
5
- export { readM3LFiles, readM3LString, readProjectConfig } from './reader.js';
6
- export type * from './types.js';
7
- import type { M3LAST, ValidateOptions } from './types.js';
8
- /**
9
- * High-level API: Parse M3L files from a path (file or directory) into a merged AST.
10
- */
11
- export declare function parse(inputPath: string): Promise<M3LAST>;
12
- /**
13
- * High-level API: Parse M3L content string into AST.
14
- */
15
- export declare function parseString(content: string, filename?: string): M3LAST;
16
- /**
17
- * High-level API: Validate M3L files from a path.
18
- */
19
- export declare function validateFiles(inputPath: string, options?: ValidateOptions): Promise<{
20
- ast: M3LAST;
21
- errors: import('./types.js').Diagnostic[];
22
- warnings: import('./types.js').Diagnostic[];
23
- }>;
package/dist/index.js DELETED
@@ -1,46 +0,0 @@
1
- export { lex } from './lexer.js';
2
- export { parseTokens, parseString as parseFileString, STANDARD_ATTRIBUTES } from './parser.js';
3
- export { resolve, AST_VERSION, PARSER_VERSION } from './resolver.js';
4
- export { validate } from './validator.js';
5
- export { readM3LFiles, readM3LString, readProjectConfig } from './reader.js';
6
- import { readM3LFiles, readProjectConfig } from './reader.js';
7
- import { parseString as parseFileContent } from './parser.js';
8
- import { resolve } from './resolver.js';
9
- import { validate as validateAST } from './validator.js';
10
- import { resolve as resolvePath } from 'path';
11
- /**
12
- * High-level API: Parse M3L files from a path (file or directory) into a merged AST.
13
- */
14
- export async function parse(inputPath) {
15
- const resolved = resolvePath(inputPath);
16
- const files = await readM3LFiles(resolved);
17
- if (files.length === 0) {
18
- throw new Error(`No .m3l.md or .m3l files found at: ${inputPath}`);
19
- }
20
- const parsedFiles = files.map(f => parseFileContent(f.content, f.path));
21
- let projectInfo;
22
- try {
23
- const config = await readProjectConfig(resolved);
24
- if (config)
25
- projectInfo = config;
26
- }
27
- catch {
28
- // No config
29
- }
30
- return resolve(parsedFiles, projectInfo || undefined);
31
- }
32
- /**
33
- * High-level API: Parse M3L content string into AST.
34
- */
35
- export function parseString(content, filename = 'inline.m3l.md') {
36
- const parsed = parseFileContent(content, filename);
37
- return resolve([parsed]);
38
- }
39
- /**
40
- * High-level API: Validate M3L files from a path.
41
- */
42
- export async function validateFiles(inputPath, options) {
43
- const ast = await parse(inputPath);
44
- const result = validateAST(ast, options);
45
- return { ast, ...result };
46
- }
package/dist/lexer.d.ts DELETED
@@ -1,6 +0,0 @@
1
- import type { Token } from './types.js';
2
- /**
3
- * Tokenize M3L markdown content into a sequence of tokens.
4
- */
5
- export declare function lex(content: string, file: string): Token[];
6
- export declare function parseTypeAndAttrs(rest: string, data: Record<string, unknown>): void;