+4
CHANGELOG.md
+4
CHANGELOG.md
+3
-1
packages/cli/src/commands/init.ts
+3
-1
packages/cli/src/commands/init.ts
···
4
4
import { createInterface } from "readline";
5
5
import pc from "picocolors";
6
6
import { generateExternalsFile } from "../utils/externals-generator.js";
7
+
import { escapeTypeSpecKeywords } from "../utils/escape-keywords.js";
7
8
8
9
function gradientText(text: string): string {
9
10
const colors = [
···
29
30
}
30
31
31
32
function createMainTemplate(namespace: string): string {
33
+
const escapedNamespace = escapeTypeSpecKeywords(namespace);
32
34
return `import "@typelex/emitter";
33
35
import "./externals.tsp";
34
36
35
-
namespace ${namespace}.example.profile {
37
+
namespace ${escapedNamespace}.example.profile {
36
38
/** My profile. */
37
39
@rec("literal:self")
38
40
model Main {
+28
packages/cli/src/utils/escape-keywords.ts
+28
packages/cli/src/utils/escape-keywords.ts
···
1
+
/**
2
+
* Complete list of TypeSpec reserved keywords (67 total)
3
+
* Source: @typespec/compiler/src/core/scanner.ts
4
+
*/
5
+
const TYPESPEC_KEYWORDS = new Set([
6
+
// Active keywords
7
+
"import", "model", "scalar", "namespace", "using", "op", "enum", "alias",
8
+
"is", "interface", "union", "projection", "else", "if", "dec", "fn",
9
+
"const", "init", "extern", "extends", "true", "false", "return", "void",
10
+
"never", "unknown", "valueof", "typeof",
11
+
// Reserved keywords
12
+
"statemachine", "macro", "package", "metadata", "env", "arg", "declare",
13
+
"array", "struct", "record", "module", "mod", "sym", "context", "prop",
14
+
"property", "scenario", "pub", "sub", "typeref", "trait", "this", "self",
15
+
"super", "keyof", "with", "implements", "impl", "satisfies", "flag", "auto",
16
+
"partial", "private", "public", "protected", "internal", "sealed", "local",
17
+
"async"
18
+
]);
19
+
20
+
/**
21
+
* Escape TypeSpec reserved keywords in a namespace identifier
22
+
* Example: "pub.leaflet.example" -> "`pub`.leaflet.example"
23
+
*/
24
+
export function escapeTypeSpecKeywords(nsid: string): string {
25
+
return nsid.split('.').map(part =>
26
+
TYPESPEC_KEYWORDS.has(part) ? `\`${part}\`` : part
27
+
).join('.');
28
+
}
+3
-2
packages/cli/src/utils/externals-generator.ts
+3
-2
packages/cli/src/utils/externals-generator.ts
···
1
1
import { resolve } from "path";
2
2
import { writeFile, mkdir } from "fs/promises";
3
3
import { findExternalLexicons, LexiconDoc, isTokenDef, isModelDef } from "./lexicon.js";
4
+
import { escapeTypeSpecKeywords } from "./escape-keywords.js";
4
5
5
6
/**
6
7
* Convert camelCase to PascalCase
···
38
39
39
40
for (const [nsid, lexicon] of sortedNamespaces) {
40
41
lines.push("@external");
41
-
// Escape reserved keywords in namespace (like 'record')
42
-
const escapedNsid = nsid.replace(/\b(record|union|enum|interface|namespace|model|op|import|using|extends|is|scalar|alias|if|else|return|void|never|unknown|any|true|false|null)\b/g, '`$1`');
42
+
// Escape reserved keywords in namespace
43
+
const escapedNsid = escapeTypeSpecKeywords(nsid);
43
44
lines.push(`namespace ${escapedNsid} {`);
44
45
45
46
// Sort definitions for consistent output
+14
packages/cli/test/scenarios/reserved-keywords/expected/lexicons/app/bsky/feed/post/record.json
+14
packages/cli/test/scenarios/reserved-keywords/expected/lexicons/app/bsky/feed/post/record.json
+14
packages/cli/test/scenarios/reserved-keywords/expected/lexicons/com/atproto/server/defs.json
+14
packages/cli/test/scenarios/reserved-keywords/expected/lexicons/com/atproto/server/defs.json
+21
packages/cli/test/scenarios/reserved-keywords/expected/lexicons/pub/leaflet/example/profile.json
+21
packages/cli/test/scenarios/reserved-keywords/expected/lexicons/pub/leaflet/example/profile.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "pub.leaflet.example.profile",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"key": "literal:self",
8
+
"record": {
9
+
"type": "object",
10
+
"properties": {
11
+
"description": {
12
+
"type": "string",
13
+
"maxGraphemes": 256,
14
+
"description": "Free-form profile description."
15
+
}
16
+
}
17
+
},
18
+
"description": "My profile."
19
+
}
20
+
}
21
+
}
+7
packages/cli/test/scenarios/reserved-keywords/expected/package.json
+7
packages/cli/test/scenarios/reserved-keywords/expected/package.json
+14
packages/cli/test/scenarios/reserved-keywords/expected/typelex/externals.tsp
+14
packages/cli/test/scenarios/reserved-keywords/expected/typelex/externals.tsp
···
1
+
import "@typelex/emitter";
2
+
3
+
// Generated by typelex from ./lexicons (excluding pub.leaflet.*)
4
+
// This file is auto-generated. Do not edit manually.
5
+
6
+
@external
7
+
namespace app.bsky.feed.post.`record` {
8
+
model Main { }
9
+
}
10
+
11
+
@external
12
+
namespace com.atproto.server.defs {
13
+
model InviteCode { }
14
+
}
+12
packages/cli/test/scenarios/reserved-keywords/expected/typelex/main.tsp
+12
packages/cli/test/scenarios/reserved-keywords/expected/typelex/main.tsp
+14
packages/cli/test/scenarios/reserved-keywords/project/lexicons/app/bsky/feed/post/record.json
+14
packages/cli/test/scenarios/reserved-keywords/project/lexicons/app/bsky/feed/post/record.json
+14
packages/cli/test/scenarios/reserved-keywords/project/lexicons/com/atproto/server/defs.json
+14
packages/cli/test/scenarios/reserved-keywords/project/lexicons/com/atproto/server/defs.json
+4
packages/cli/test/scenarios/reserved-keywords/project/package.json
+4
packages/cli/test/scenarios/reserved-keywords/project/package.json
+10
packages/cli/test/scenarios/reserved-keywords/test.ts
+10
packages/cli/test/scenarios/reserved-keywords/test.ts