Core language implementation for URLSpec, built with Langium
@urlspec/language is the core package that implements the URLSpec domain-specific language. It provides parsing, validation, and resolution capabilities for .urlspec files, built on the powerful Langium framework.
- Parse URLSpec files: Convert
.urlspectext into structured AST - Type-safe resolution: Transform AST into developer-friendly resolved structures
- Validation: Catch syntax and semantic errors
- Pretty printing: Generate formatted
.urlspectext from AST - Langium services: Expose language services for IDE integration
# npm
npm install @urlspec/language
# yarn
yarn add @urlspec/language
# pnpm
pnpm add @urlspec/languageimport { parse } from '@urlspec/language';
const urlspecContent = `
page list = /jobs {
category?: string;
}
`;
const document = await parse(urlspecContent);
// Access the Langium AST
console.log(document.parseResult.value.pages[0].name); // "list"The resolve() function provides a higher-level API with merged global parameters and resolved type references:
import { resolve } from '@urlspec/language';
const spec = resolve(urlspecContent);
// Easier to work with resolved structure
console.log(spec.pages[0].name); // "list"
console.log(spec.pages[0].path); // "/jobs"
console.log(spec.pages[0].parameters);
// Includes both page-specific and global parametersThe package exports comprehensive TypeScript types for the resolved structure:
import type {
ResolvedURLSpec,
ResolvedPage,
ResolvedParameter,
ResolvedType,
ResolvedPathSegment,
} from '@urlspec/language';
function processSpec(spec: ResolvedURLSpec) {
spec.pages.forEach((page: ResolvedPage) => {
console.log(`Page: ${page.name}`);
console.log(`Path: ${page.path}`);
page.pathSegments.forEach((segment: ResolvedPathSegment) => {
if (segment.type === 'parameter') {
console.log(` Dynamic param: ${segment.value}`);
}
});
page.parameters.forEach((param: ResolvedParameter) => {
console.log(` Param: ${param.name} (${param.optional ? 'optional' : 'required'}, ${param.source})`);
});
});
}Parses URLSpec text into a Langium document with AST.
Parameters:
input- URLSpec source code as a string
Returns:
- Promise resolving to a
URLSpecDocumentcontaining the Langium AST
Example:
const document = await parse(`
page home = /home {
query?: string;
}
`);
const ast = document.parseResult.value;
console.log(ast.pages[0].name); // "home"
console.log(ast.pages[0].parameters[0].name); // "query"Resolves a parsed URLSpec document into a developer-friendly structure with:
- Global parameters merged into each page
- Type references resolved to actual types
- Path segments parsed and categorized
Parameters:
doc- A Langium document returned byparse()
Returns:
ResolvedURLSpecobject with fully resolved structure
Example:
const doc = await parse(`
param category = "electronics" | "clothing" | "food";
global {
utm_source?: string;
}
page products = /products {
cat: category;
}
`);
const spec = resolve(doc);
console.log(spec.paramTypes[0]);
// { name: 'category', type: { kind: 'union', values: ['electronics', 'clothing', 'food'] } }
const productsPage = spec.pages[0];
const catParam = productsPage.parameters.find(p => p.name === 'cat');
console.log(catParam?.type);
// Resolved to actual union type, not a reference
const utmParam = productsPage.parameters.find(p => p.name === 'utm_source');
console.log(utmParam?.source); // "global"Converts a Langium AST back to formatted URLSpec text.
Parameters:
urlSpec- The Langium AST model to print
Returns:
- Formatted URLSpec source code as a string
Example:
import { parse, print } from '@urlspec/language';
const document = await parse('page home = /home { query?: string; }');
const ast = document.parseResult.value;
const formatted = print(ast);
console.log(formatted);
// Output:
// page home = /home {
// query?: string;
// }Creates Langium language services for URLSpec. Used primarily for language server and IDE integration.
Parameters:
context- Optional Langium module context
Returns:
URLSpecServicesobject with Langium services
Top-level resolved structure representing an entire URLSpec document.
interface ResolvedURLSpec {
paramTypes: ResolvedParamType[];
pages: ResolvedPage[];
global?: ResolvedParameter[];
}Represents a single page definition with resolved parameters.
interface ResolvedPage {
name: string;
path: string;
pathSegments: ResolvedPathSegment[];
parameters: ResolvedParameter[];
description?: string;
}Represents a query or path parameter with its type and optionality.
interface ResolvedParameter {
name: string;
optional: boolean;
type: ResolvedType;
source: "global" | "page";
description?: string;
}Union type representing all possible parameter types.
type ResolvedType =
| { kind: 'string' }
| { kind: 'literal'; value: string }
| { kind: 'union'; values: string[] };Represents a segment of a URL path.
interface ResolvedPathSegment {
type: "static" | "parameter";
value: string;
}@urlspec/language provides two APIs for different use cases:
Use when you need:
- Direct access to Langium AST
- Building IDE tools or language servers
- Low-level manipulation of syntax tree
- Custom validation or transformation
Example:
const doc = await parse(input);
const ast = doc.parseResult.value;
// Direct AST access
ast.pages.forEach(page => {
console.log(page.$type); // Langium type
console.log(page.name);
});Use when you need:
- Simple, flattened data structure
- Type references resolved to actual types
- Global parameters automatically merged
- Building application features
Example:
const doc = await parse(input);
const spec = resolve(doc);
// Easy access to resolved data
spec.pages[0].parameters.forEach(param => {
// Type is already resolved, no need to lookup references
console.log(param.type);
});import { parse } from '@urlspec/language';
const document = await parse(invalidInput);
if (document.parseResult.lexerErrors.length > 0) {
console.error('Lexer errors:', document.parseResult.lexerErrors);
}
if (document.parseResult.parserErrors.length > 0) {
console.error('Parser errors:', document.parseResult.parserErrors);
}const doc = await parse(`
page article = /blog/:category/:article_id {
category: string;
article_id: string;
}
`);
const spec = resolve(doc);
const articlePage = spec.pages[0];
articlePage.pathSegments.forEach(segment => {
if (segment.type === 'static') {
console.log(`Static: ${segment.value}`);
} else if (segment.type === 'parameter') {
console.log(`Dynamic: ${segment.value}`);
// Get the parameter details
const param = articlePage.parameters.find(p => p.name === segment.value);
console.log(` Optional: ${param?.optional}`);
}
});URLSpec is defined using Langium grammar. Key syntax elements:
param status = "active" | "inactive" | "pending";
param sortOrder = "asc" | "desc";
global {
utm_source?: string;
debug?: "true" | "false";
}
// Static path
page home = /;
// Dynamic path with parameters
page user = /users/:user_id {
user_id: string;
tab?: "posts" | "comments" | "likes";
}
// Multiple dynamic segments
page post = /blog/:category/:post_id {
category: string;
post_id: string;
preview?: "true" | "false";
}
// String type
string
// String literal
"active"
// Union of literals
"small" | "medium" | "large"
// Type reference
sortOrder // References a param type
# Generate Langium parser and build
yarn build
# Just generate Langium artifacts
yarn langium:generate# Run tests
yarn test
# Watch mode
yarn test:watchThe Langium grammar is defined in:
packages/language/src/urlspec.langium
Contributions are welcome! Please see the root repository README for contribution guidelines.
Modifying Grammar:
- Edit
src/urlspec.langium - Run
yarn langium:generate - Update TypeScript code as needed
- Run tests:
yarn test
Adding Validation:
- Add validation logic in
src/validator.ts(if exists) - Update tests
- Update documentation
- @urlspec/builder - Programmatic API to build URLSpec files
- urlspec-vscode-extension - VS Code extension
MIT License - see LICENSE for details