@iyulab/m3l 0.1.3 → 0.4.1

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.3",
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.4.1",
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.4.1"
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,249 +0,0 @@
1
- # @iyulab/m3l
2
-
3
- M3L (Meta Model Markup Language) parser and CLI — parse `.m3l.md` files into a structured JSON AST.
4
-
5
- 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.
6
-
7
- ## Install
8
-
9
- ```bash
10
- npm install @iyulab/m3l
11
- ```
12
-
13
- ## Quick Start
14
-
15
- ### As a Library
16
-
17
- ```typescript
18
- import { parse, parseString, validateFiles } from '@iyulab/m3l';
19
-
20
- // Parse a file or directory
21
- const ast = await parse('./models');
22
-
23
- // Parse a string
24
- const ast2 = parseString(`
25
- ## User
26
- - name: string(100) @not_null
27
- - email: string(320)? @unique
28
-
29
- ## UserRole ::enum
30
- - admin: "Administrator"
31
- - user: "Regular User"
32
- `);
33
-
34
- console.log(ast2.models); // [{ name: 'User', fields: [...], ... }]
35
- console.log(ast2.enums); // [{ name: 'UserRole', values: [...], ... }]
36
-
37
- // Validate with diagnostics
38
- const { ast: validated, errors, warnings } = await validateFiles('./models');
39
- ```
40
-
41
- ### As a CLI
42
-
43
- ```bash
44
- # Parse and output JSON AST
45
- npx m3l parse ./models
46
-
47
- # Parse a single file
48
- npx m3l parse ./models/user.m3l.md -o ast.json
49
-
50
- # Validate
51
- npx m3l validate ./models
52
-
53
- # Validate with strict style checks and JSON output
54
- npx m3l validate ./models --strict --format json
55
- ```
56
-
57
- ## M3L Syntax
58
-
59
- ```markdown
60
- # Library System
61
-
62
- ## Author
63
- - name(Author Name): string(100) @not_null @idx
64
- - bio(Biography): text?
65
- - birth_date: date?
66
-
67
- ### Rollup
68
- - book_count: integer @rollup(BookAuthor.author_id, count)
69
-
70
- > Stores information about book authors.
71
-
72
- ## BookStatus ::enum "Status of a book"
73
- - available: "Available"
74
- - borrowed: "Borrowed"
75
- - reserved: "Reserved"
76
-
77
- ## Book : BaseModel
78
- - title: string(200) @not_null @idx
79
- - isbn: string(20) @unique
80
- - status: enum = "available"
81
- - available: "Available"
82
- - borrowed: "Borrowed"
83
- - publisher_id: identifier @fk(Publisher.id)
84
-
85
- ### Lookup
86
- - publisher_name: string @lookup(publisher_id.name)
87
-
88
- ### Computed
89
- - is_available: boolean @computed("status = 'available' AND quantity > 0")
90
-
91
- ## OverdueLoans ::view @materialized
92
- > Currently overdue book loans.
93
- ### Source
94
- - from: Loan
95
- - where: "due_date < now() AND status = 'ongoing'"
96
- - order_by: due_date asc
97
- - borrower_name: string @lookup(member_id.name)
98
- ```
99
-
100
- **Key syntax elements:**
101
-
102
- | Syntax | Meaning |
103
- |--------|---------|
104
- | `# Title` | Document title or namespace (`# Namespace: domain.example`) |
105
- | `## Name` | Model definition |
106
- | `## Name : Parent` | Model with inheritance |
107
- | `## Name ::enum` | Enum definition |
108
- | `## Name ::interface` | Interface definition |
109
- | `## Name ::view` | Derived view |
110
- | `- field: type` | Field definition |
111
- | `- field: type?` | Nullable field |
112
- | `- field: type[]` | Array field |
113
- | `- field: type?[]` | Array of nullable items |
114
- | `- field: type = val` | Field with default value |
115
- | `@attr` / `@attr(args)` | Attribute (constraint, index, etc.) |
116
- | `` `[FrameworkAttr]` `` | Custom framework attribute |
117
- | `### Lookup` / `### Rollup` / `### Computed` | Kind section for derived fields |
118
- | `### Section` | Named section (Indexes, Relations, Metadata, etc.) |
119
- | `> text` | Model/element description |
120
- | `"text"` | Inline description on field |
121
-
122
- ## AST Output
123
-
124
- The parser produces an `M3LAST` object:
125
-
126
- ```typescript
127
- interface M3LAST {
128
- parserVersion: string; // Parser package version (semver)
129
- astVersion: string; // AST schema version
130
- project: { name?: string; version?: string };
131
- sources: string[];
132
- models: ModelNode[];
133
- enums: EnumNode[];
134
- interfaces: ModelNode[];
135
- views: ModelNode[];
136
- errors: Diagnostic[];
137
- warnings: Diagnostic[];
138
- }
139
- ```
140
-
141
- ### Key AST types
142
-
143
- ```typescript
144
- interface FieldNode {
145
- name: string;
146
- label?: string;
147
- type?: string;
148
- params?: (string | number)[];
149
- generic_params?: string[]; // map<K,V> → ["K", "V"]
150
- nullable: boolean;
151
- array: boolean;
152
- arrayItemNullable: boolean; // string?[] → true
153
- kind: 'stored' | 'computed' | 'lookup' | 'rollup';
154
- default_value?: string;
155
- description?: string;
156
- attributes: FieldAttribute[];
157
- framework_attrs?: CustomAttribute[];
158
- lookup?: { path: string };
159
- rollup?: { target: string; fk: string; aggregate: string; field?: string; where?: string };
160
- computed?: { expression: string };
161
- enum_values?: EnumValue[];
162
- fields?: FieldNode[]; // sub-fields for object type
163
- loc: SourceLocation;
164
- }
165
-
166
- interface ModelNode {
167
- name: string;
168
- type: 'model' | 'enum' | 'interface' | 'view';
169
- inherits: string[];
170
- attributes: FieldAttribute[]; // model-level attributes (@public, etc.)
171
- fields: FieldNode[];
172
- sections: { indexes; relations; behaviors; metadata; [key: string]: unknown };
173
- // ...
174
- }
175
- ```
176
-
177
- ## Validation
178
-
179
- The validator checks for semantic errors and style warnings:
180
-
181
- **Errors:**
182
- | Code | Description |
183
- |------|-------------|
184
- | M3L-E001 | Rollup FK field missing `@reference` |
185
- | M3L-E002 | Lookup FK field missing `@reference` |
186
- | M3L-E004 | View references non-existent model |
187
- | M3L-E006 | Duplicate field name within model |
188
-
189
- **Warnings (--strict):**
190
- | Code | Description |
191
- |------|-------------|
192
- | M3L-W001 | Field line exceeds 80 characters |
193
- | M3L-W002 | Object nesting exceeds 3 levels |
194
- | M3L-W004 | Lookup chain exceeds 3 hops |
195
-
196
- ## Multi-file Projects
197
-
198
- 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.
199
-
200
- Optionally, create an `m3l.config.yaml`:
201
-
202
- ```yaml
203
- name: my-project
204
- version: 1.0.0
205
- sources:
206
- - "models/**/*.m3l.md"
207
- - "shared/*.m3l"
208
- ```
209
-
210
- ## API Reference
211
-
212
- ### `parse(inputPath: string): Promise<M3LAST>`
213
-
214
- Parse M3L files from a file or directory into a merged AST.
215
-
216
- ### `parseString(content: string, filename?: string): M3LAST`
217
-
218
- Parse an M3L content string into an AST.
219
-
220
- ### `validateFiles(inputPath: string, options?): Promise<{ ast, errors, warnings }>`
221
-
222
- Parse and validate M3L files. Options: `{ strict?: boolean }`.
223
-
224
- ### Lower-level API
225
-
226
- ```typescript
227
- import { lex, parseTokens, resolve, validate } from '@iyulab/m3l';
228
-
229
- const tokens = lex(content, 'file.m3l.md');
230
- const parsed = parseTokens(tokens, 'file.m3l.md');
231
- const ast = resolve([parsed]);
232
- const diagnostics = validate(ast, { strict: true });
233
- ```
234
-
235
- ## Compatibility
236
-
237
- This TypeScript parser and the [C# parser](../csharp/) produce equivalent AST structures. Both share the same conformance test suite.
238
-
239
- | | TypeScript | C# |
240
- |---|---|---|
241
- | Package | `@iyulab/m3l` | `M3LParser` |
242
- | Runtime | Node.js 20+ | .NET 8.0+ |
243
- | AST Version | 1.0 | 1.0 |
244
-
245
- Version is managed centrally via the root `VERSION` file.
246
-
247
- ## License
248
-
249
- 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;