From c24bc9bd9bfe55a6d098aa81ebd4ec8816e4f3ac Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 10 Oct 2025 20:33:45 +0100 Subject: [PATCH] changes --- packages/cli/src/commands/init.ts | 16 +- .../lexicons/com/test/example/profile.json | 21 + .../expected/lexicons/com/test/post.json | 26 - .../scenarios/basic/expected/typelex/main.tsp | 10 +- .../com/myservice/example/profile.json | 21 + .../expected/lexicons/com/myservice/post.json | 26 - .../nested-init/expected/typelex/main.tsp | 10 +- .../expected1/app/typelex/main.tsp | 10 +- .../lexicons/com/myapp/example/profile.json | 21 + .../expected1/lexicons/com/myapp/post.json | 26 - .../expected2/app/typelex/main.tsp | 9 +- .../myapp/{post.json => example/profile.json} | 18 +- .../test/scenarios/parent-lexicons/test.ts | 26 +- .../lexicons/com/myapp/example/profile.json | 21 + .../expected1/lexicons/com/myapp/post.json | 26 - .../expected1/typelex/main.tsp | 10 +- .../scenarios/with-external-lexicons/test.ts | 6 + .../website/src/components/CodeBlock.astro | 14 + .../src/components/ComparisonBlock.astro | 57 +++ packages/website/src/layouts/BaseLayout.astro | 172 +++++++ packages/website/src/layouts/DocsLayout.astro | 445 ++++++++++++++++++ packages/website/src/pages/index.astro | 114 +---- 22 files changed, 870 insertions(+), 235 deletions(-) create mode 100644 packages/cli/test/scenarios/basic/expected/lexicons/com/test/example/profile.json delete mode 100644 packages/cli/test/scenarios/basic/expected/lexicons/com/test/post.json create mode 100644 packages/cli/test/scenarios/nested-init/expected/lexicons/com/myservice/example/profile.json delete mode 100644 packages/cli/test/scenarios/nested-init/expected/lexicons/com/myservice/post.json create mode 100644 packages/cli/test/scenarios/parent-lexicons/expected1/lexicons/com/myapp/example/profile.json delete mode 100644 packages/cli/test/scenarios/parent-lexicons/expected1/lexicons/com/myapp/post.json rename packages/cli/test/scenarios/parent-lexicons/expected2/lexicons/com/myapp/{post.json => example/profile.json} (50%) create mode 100644 packages/cli/test/scenarios/with-external-lexicons/expected1/lexicons/com/myapp/example/profile.json delete mode 100644 packages/cli/test/scenarios/with-external-lexicons/expected1/lexicons/com/myapp/post.json create mode 100644 packages/website/src/components/CodeBlock.astro create mode 100644 packages/website/src/components/ComparisonBlock.astro create mode 100644 packages/website/src/layouts/BaseLayout.astro create mode 100644 packages/website/src/layouts/DocsLayout.astro diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 211a7df..daf52ff 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -3,6 +3,7 @@ import { mkdir, writeFile, readFile, access, stat } from "fs/promises"; import { spawn } from "child_process"; import { createInterface } from "readline"; import pc from "picocolors"; +import { generateExternalsFile } from "../utils/externals-generator.js"; function gradientText(text: string): string { const colors = [ @@ -31,11 +32,13 @@ function createMainTemplate(namespace: string): string { return `import "@typelex/emitter"; import "./externals.tsp"; -namespace ${namespace}.post { - @rec("tid") +namespace ${namespace}.example.profile { + /** My profile. */ + @rec("literal:self") model Main { - @required text: string; - @required createdAt: datetime; + /** Free-form profile description.*/ + @maxGraphemes(256) + description?: string; } } `; @@ -266,8 +269,9 @@ export async function initSetup(): Promise { console.log(`${pc.green("✓")} Created ${pc.cyan("typelex/main.tsp")}`); } - // Always create/overwrite externals.tsp - await writeFile(externalsTspPath, EXTERNALS_TSP_TEMPLATE, "utf-8"); + // Generate externals.tsp with any existing external lexicons + const outDir = lexiconsDir || "./lexicons"; + await generateExternalsFile(namespace, cwd, outDir); console.log(`${pc.green("✓")} Created ${pc.cyan("typelex/externals.tsp")}`); // Add build script to package.json diff --git a/packages/cli/test/scenarios/basic/expected/lexicons/com/test/example/profile.json b/packages/cli/test/scenarios/basic/expected/lexicons/com/test/example/profile.json new file mode 100644 index 0000000..a586b89 --- /dev/null +++ b/packages/cli/test/scenarios/basic/expected/lexicons/com/test/example/profile.json @@ -0,0 +1,21 @@ +{ + "lexicon": 1, + "id": "com.test.example.profile", + "defs": { + "main": { + "type": "record", + "key": "literal:self", + "record": { + "type": "object", + "properties": { + "description": { + "type": "string", + "maxGraphemes": 256, + "description": "Free-form profile description." + } + } + }, + "description": "My profile." + } + } +} diff --git a/packages/cli/test/scenarios/basic/expected/lexicons/com/test/post.json b/packages/cli/test/scenarios/basic/expected/lexicons/com/test/post.json deleted file mode 100644 index c923458..0000000 --- a/packages/cli/test/scenarios/basic/expected/lexicons/com/test/post.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "lexicon": 1, - "id": "com.test.post", - "defs": { - "main": { - "type": "record", - "key": "tid", - "record": { - "type": "object", - "properties": { - "text": { - "type": "string" - }, - "createdAt": { - "type": "string", - "format": "datetime" - } - }, - "required": [ - "text", - "createdAt" - ] - } - } - } -} diff --git a/packages/cli/test/scenarios/basic/expected/typelex/main.tsp b/packages/cli/test/scenarios/basic/expected/typelex/main.tsp index f004e39..d03c6e2 100644 --- a/packages/cli/test/scenarios/basic/expected/typelex/main.tsp +++ b/packages/cli/test/scenarios/basic/expected/typelex/main.tsp @@ -1,10 +1,12 @@ import "@typelex/emitter"; import "./externals.tsp"; -namespace com.test.post { - @rec("tid") +namespace com.test.example.profile { + /** My profile. */ + @rec("literal:self") model Main { - @required text: string; - @required createdAt: datetime; + /** Free-form profile description.*/ + @maxGraphemes(256) + description?: string; } } diff --git a/packages/cli/test/scenarios/nested-init/expected/lexicons/com/myservice/example/profile.json b/packages/cli/test/scenarios/nested-init/expected/lexicons/com/myservice/example/profile.json new file mode 100644 index 0000000..107620f --- /dev/null +++ b/packages/cli/test/scenarios/nested-init/expected/lexicons/com/myservice/example/profile.json @@ -0,0 +1,21 @@ +{ + "lexicon": 1, + "id": "com.myservice.example.profile", + "defs": { + "main": { + "type": "record", + "key": "literal:self", + "record": { + "type": "object", + "properties": { + "description": { + "type": "string", + "maxGraphemes": 256, + "description": "Free-form profile description." + } + } + }, + "description": "My profile." + } + } +} diff --git a/packages/cli/test/scenarios/nested-init/expected/lexicons/com/myservice/post.json b/packages/cli/test/scenarios/nested-init/expected/lexicons/com/myservice/post.json deleted file mode 100644 index 1475d2c..0000000 --- a/packages/cli/test/scenarios/nested-init/expected/lexicons/com/myservice/post.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "lexicon": 1, - "id": "com.myservice.post", - "defs": { - "main": { - "type": "record", - "key": "tid", - "record": { - "type": "object", - "properties": { - "text": { - "type": "string" - }, - "createdAt": { - "type": "string", - "format": "datetime" - } - }, - "required": [ - "text", - "createdAt" - ] - } - } - } -} diff --git a/packages/cli/test/scenarios/nested-init/expected/typelex/main.tsp b/packages/cli/test/scenarios/nested-init/expected/typelex/main.tsp index 75fe571..a8ce25f 100644 --- a/packages/cli/test/scenarios/nested-init/expected/typelex/main.tsp +++ b/packages/cli/test/scenarios/nested-init/expected/typelex/main.tsp @@ -1,10 +1,12 @@ import "@typelex/emitter"; import "./externals.tsp"; -namespace com.myservice.post { - @rec("tid") +namespace com.myservice.example.profile { + /** My profile. */ + @rec("literal:self") model Main { - @required text: string; - @required createdAt: datetime; + /** Free-form profile description.*/ + @maxGraphemes(256) + description?: string; } } diff --git a/packages/cli/test/scenarios/parent-lexicons/expected1/app/typelex/main.tsp b/packages/cli/test/scenarios/parent-lexicons/expected1/app/typelex/main.tsp index 204e86d..1454194 100644 --- a/packages/cli/test/scenarios/parent-lexicons/expected1/app/typelex/main.tsp +++ b/packages/cli/test/scenarios/parent-lexicons/expected1/app/typelex/main.tsp @@ -1,10 +1,12 @@ import "@typelex/emitter"; import "./externals.tsp"; -namespace com.myapp.post { - @rec("tid") +namespace com.myapp.example.profile { + /** My profile. */ + @rec("literal:self") model Main { - @required text: string; - @required createdAt: datetime; + /** Free-form profile description.*/ + @maxGraphemes(256) + description?: string; } } diff --git a/packages/cli/test/scenarios/parent-lexicons/expected1/lexicons/com/myapp/example/profile.json b/packages/cli/test/scenarios/parent-lexicons/expected1/lexicons/com/myapp/example/profile.json new file mode 100644 index 0000000..c1b8233 --- /dev/null +++ b/packages/cli/test/scenarios/parent-lexicons/expected1/lexicons/com/myapp/example/profile.json @@ -0,0 +1,21 @@ +{ + "lexicon": 1, + "id": "com.myapp.example.profile", + "defs": { + "main": { + "type": "record", + "key": "literal:self", + "record": { + "type": "object", + "properties": { + "description": { + "type": "string", + "maxGraphemes": 256, + "description": "Free-form profile description." + } + } + }, + "description": "My profile." + } + } +} diff --git a/packages/cli/test/scenarios/parent-lexicons/expected1/lexicons/com/myapp/post.json b/packages/cli/test/scenarios/parent-lexicons/expected1/lexicons/com/myapp/post.json deleted file mode 100644 index 8277df3..0000000 --- a/packages/cli/test/scenarios/parent-lexicons/expected1/lexicons/com/myapp/post.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "lexicon": 1, - "id": "com.myapp.post", - "defs": { - "main": { - "type": "record", - "key": "tid", - "record": { - "type": "object", - "properties": { - "text": { - "type": "string" - }, - "createdAt": { - "type": "string", - "format": "datetime" - } - }, - "required": [ - "text", - "createdAt" - ] - } - } - } -} diff --git a/packages/cli/test/scenarios/parent-lexicons/expected2/app/typelex/main.tsp b/packages/cli/test/scenarios/parent-lexicons/expected2/app/typelex/main.tsp index 63ab2b1..e6ae4ef 100644 --- a/packages/cli/test/scenarios/parent-lexicons/expected2/app/typelex/main.tsp +++ b/packages/cli/test/scenarios/parent-lexicons/expected2/app/typelex/main.tsp @@ -1,10 +1,13 @@ import "@typelex/emitter"; import "./externals.tsp"; -namespace com.myapp.post { - @rec("tid") +namespace com.myapp.example.profile { + /** My profile. */ + @rec("literal:self") model Main { - @required text: string; + /** Free-form profile description.*/ + @maxGraphemes(256) + description?: string; labels?: com.atproto.label.defs.SelfLabels; } } diff --git a/packages/cli/test/scenarios/parent-lexicons/expected2/lexicons/com/myapp/post.json b/packages/cli/test/scenarios/parent-lexicons/expected2/lexicons/com/myapp/example/profile.json similarity index 50% rename from packages/cli/test/scenarios/parent-lexicons/expected2/lexicons/com/myapp/post.json rename to packages/cli/test/scenarios/parent-lexicons/expected2/lexicons/com/myapp/example/profile.json index 19d593a..db34337 100644 --- a/packages/cli/test/scenarios/parent-lexicons/expected2/lexicons/com/myapp/post.json +++ b/packages/cli/test/scenarios/parent-lexicons/expected2/lexicons/com/myapp/example/profile.json @@ -1,25 +1,25 @@ { "lexicon": 1, - "id": "com.myapp.post", + "id": "com.myapp.example.profile", "defs": { "main": { "type": "record", - "key": "tid", + "key": "literal:self", "record": { "type": "object", "properties": { - "text": { - "type": "string" + "description": { + "type": "string", + "maxGraphemes": 256, + "description": "Free-form profile description." }, "labels": { "type": "ref", "ref": "com.atproto.label.defs#selfLabels" } - }, - "required": [ - "text" - ] - } + } + }, + "description": "My profile." } } } diff --git a/packages/cli/test/scenarios/parent-lexicons/test.ts b/packages/cli/test/scenarios/parent-lexicons/test.ts index 18d44e3..bc5cc4e 100644 --- a/packages/cli/test/scenarios/parent-lexicons/test.ts +++ b/packages/cli/test/scenarios/parent-lexicons/test.ts @@ -5,22 +5,36 @@ export async function run(project) { await project.init("com.myapp.*", { cwd: appDir }); + // Verify init generated externals.tsp with existing external lexicons (before build) + const externals = await project.readFile("app/typelex/externals.tsp"); + if (!externals.includes("com.atproto.label.defs")) { + throw new Error( + "externals.tsp should contain external lexicons after init", + ); + } + // Verify init created a working project with default main.tsp await project.runBuildScript({ cwd: appDir }); await project.compareTo("expected1"); - // Edit main.tsp to add a post schema with labels (simulates user editing the file) - await project.writeFile("app/typelex/main.tsp", `import "@typelex/emitter"; + // Edit main.tsp to add labels (simulates user editing the file) + await project.writeFile( + "app/typelex/main.tsp", + `import "@typelex/emitter"; import "./externals.tsp"; -namespace com.myapp.post { - @rec("tid") +namespace com.myapp.example.profile { + /** My profile. */ + @rec("literal:self") model Main { - @required text: string; + /** Free-form profile description.*/ + @maxGraphemes(256) + description?: string; labels?: com.atproto.label.defs.SelfLabels; } } -`); +`, + ); await project.runBuildScript({ cwd: appDir }); await project.compareTo("expected2"); diff --git a/packages/cli/test/scenarios/with-external-lexicons/expected1/lexicons/com/myapp/example/profile.json b/packages/cli/test/scenarios/with-external-lexicons/expected1/lexicons/com/myapp/example/profile.json new file mode 100644 index 0000000..c1b8233 --- /dev/null +++ b/packages/cli/test/scenarios/with-external-lexicons/expected1/lexicons/com/myapp/example/profile.json @@ -0,0 +1,21 @@ +{ + "lexicon": 1, + "id": "com.myapp.example.profile", + "defs": { + "main": { + "type": "record", + "key": "literal:self", + "record": { + "type": "object", + "properties": { + "description": { + "type": "string", + "maxGraphemes": 256, + "description": "Free-form profile description." + } + } + }, + "description": "My profile." + } + } +} diff --git a/packages/cli/test/scenarios/with-external-lexicons/expected1/lexicons/com/myapp/post.json b/packages/cli/test/scenarios/with-external-lexicons/expected1/lexicons/com/myapp/post.json deleted file mode 100644 index 8277df3..0000000 --- a/packages/cli/test/scenarios/with-external-lexicons/expected1/lexicons/com/myapp/post.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "lexicon": 1, - "id": "com.myapp.post", - "defs": { - "main": { - "type": "record", - "key": "tid", - "record": { - "type": "object", - "properties": { - "text": { - "type": "string" - }, - "createdAt": { - "type": "string", - "format": "datetime" - } - }, - "required": [ - "text", - "createdAt" - ] - } - } - } -} diff --git a/packages/cli/test/scenarios/with-external-lexicons/expected1/typelex/main.tsp b/packages/cli/test/scenarios/with-external-lexicons/expected1/typelex/main.tsp index 204e86d..1454194 100644 --- a/packages/cli/test/scenarios/with-external-lexicons/expected1/typelex/main.tsp +++ b/packages/cli/test/scenarios/with-external-lexicons/expected1/typelex/main.tsp @@ -1,10 +1,12 @@ import "@typelex/emitter"; import "./externals.tsp"; -namespace com.myapp.post { - @rec("tid") +namespace com.myapp.example.profile { + /** My profile. */ + @rec("literal:self") model Main { - @required text: string; - @required createdAt: datetime; + /** Free-form profile description.*/ + @maxGraphemes(256) + description?: string; } } diff --git a/packages/cli/test/scenarios/with-external-lexicons/test.ts b/packages/cli/test/scenarios/with-external-lexicons/test.ts index 4d5972f..1861f97 100644 --- a/packages/cli/test/scenarios/with-external-lexicons/test.ts +++ b/packages/cli/test/scenarios/with-external-lexicons/test.ts @@ -1,6 +1,12 @@ export async function run(project) { await project.init("com.myapp.*"); + // Verify init generated externals.tsp with existing external lexicons (before build) + const externals = await project.readFile("typelex/externals.tsp"); + if (!externals.includes("com.atproto.label.defs")) { + throw new Error("externals.tsp should contain external lexicons after init"); + } + // Verify init created a working project with default main.tsp await project.runBuildScript(); await project.compareTo("expected1"); diff --git a/packages/website/src/components/CodeBlock.astro b/packages/website/src/components/CodeBlock.astro new file mode 100644 index 0000000..89877f6 --- /dev/null +++ b/packages/website/src/components/CodeBlock.astro @@ -0,0 +1,14 @@ +--- +import { highlightCode } from '../utils/shiki'; + +interface Props { + lang: 'typespec' | 'json' | 'bash'; + code?: string; +} + +const { lang, code } = Astro.props; +const codeContent = code || await Astro.slots.render('default'); +const highlighted = await highlightCode(codeContent.trim(), lang); +--- + +
diff --git a/packages/website/src/components/ComparisonBlock.astro b/packages/website/src/components/ComparisonBlock.astro
new file mode 100644
index 0000000..79c6488
--- /dev/null
+++ b/packages/website/src/components/ComparisonBlock.astro
@@ -0,0 +1,57 @@
+---
+import { highlightCode } from '../utils/shiki';
+import { compileToJson } from '../utils/compile';
+import { createPlaygroundUrl } from '../utils/playground-url';
+import stringify from 'json-stringify-pretty-compact';
+import { mkdtempSync, writeFileSync, rmSync } from 'fs';
+import { join } from 'path';
+import { tmpdir } from 'os';
+
+interface Props {
+  code: string;
+}
+
+const { code } = Astro.props;
+
+// Create temporary file for compilation
+const tmpDir = mkdtempSync(join(tmpdir(), 'typelex-'));
+const tmpFile = join(tmpDir, 'example.tsp');
+writeFileSync(tmpFile, code);
+
+let lexiconJson: string;
+let lexicon: string;
+
+try {
+  lexiconJson = await compileToJson(tmpFile);
+  lexicon = stringify(JSON.parse(lexiconJson), { maxLength: 80 });
+} finally {
+  rmSync(tmpDir, { recursive: true, force: true });
+}
+
+const typelexHtml = await highlightCode(code, 'typespec');
+const lexiconHtml = await highlightCode(lexicon, 'json');
+const playgroundUrl = createPlaygroundUrl(code);
+---
+
+
+
+
+

+ Typelex + + + +

+
+
+
+

+ Lexicon +

+
+
+
+
diff --git a/packages/website/src/layouts/BaseLayout.astro b/packages/website/src/layouts/BaseLayout.astro new file mode 100644 index 0000000..6e2ca4f --- /dev/null +++ b/packages/website/src/layouts/BaseLayout.astro @@ -0,0 +1,172 @@ +--- +interface Props { + title: string; + description?: string; + transparentNav?: boolean; +} + +const { + title, + description = "An experimental TypeSpec syntax for AT Protocol Lexicons. Write Lexicons in a more readable syntax using TypeSpec.", + transparentNav = false +} = Astro.props; +--- + + + + + + + + + {title} + + + + + + + + + + + + + + + + + + + + + + {transparentNav && ( + + )} + + + + diff --git a/packages/website/src/layouts/DocsLayout.astro b/packages/website/src/layouts/DocsLayout.astro new file mode 100644 index 0000000..a22f82d --- /dev/null +++ b/packages/website/src/layouts/DocsLayout.astro @@ -0,0 +1,445 @@ +--- +import BaseLayout from './BaseLayout.astro'; + +interface Props { + title: string; +} + +const { title } = Astro.props; +--- + + +
+ + +
+
+

{title}

+ +
+
+
+ + +
+ + diff --git a/packages/website/src/pages/index.astro b/packages/website/src/pages/index.astro index 8081aa8..603dbef 100644 --- a/packages/website/src/pages/index.astro +++ b/packages/website/src/pages/index.astro @@ -1,4 +1,5 @@ --- +import BaseLayout from '../layouts/BaseLayout.astro'; import { highlightCode } from '../utils/shiki'; import { compileToJson } from '../utils/compile'; import { createPlaygroundUrl } from '../utils/playground-url'; @@ -124,32 +125,8 @@ const highlighted = await Promise.all( ); --- - - - - - - - - typelex – An experimental TypeSpec syntax for Lexicon - - - - - - - - - - - - - - - - - -
+ +

typelex

An experimental TypeSpec syntax for Lexicon

@@ -234,7 +211,7 @@ namespace app.bsky.actor.profile { @@ -282,29 +259,31 @@ namespace app.bsky.actor.profile {
0

Try the playground

-

Experiment with typelex in your browser before installing.

Open Playground +

Experiment with typelex in your browser before installing.

1
-

Install packages

-
+

Add typelex to your app

+
+

This will add a few things to your package.json, and create a typelex/ folder.

2
-

Create typelex/main.tsp

+

Write your lexicons in typelex/main.tsp

-

Or grab any example Lexicon from the Playground.

+

Your app's lexicons go here. They may reference any external ones from lexicons/.

3
-

Create tspconfig.yaml

-
+

Compile your lexicons

+
+

Your app’s compiled lexicons will appear in lexicons/ alongside any external ones.

4
-
-

Add a build script to package.json

-
-
-
- -
-
5
-
-

Generate Lexicon files

-
-

Lexicon files will be generated in the output-dir from your tspconfig.yaml config.

-
-
- -
-
6

Set up VS Code

Install the TypeSpec for VS Code extension for syntax highlighting and IntelliSense.

@@ -377,9 +332,9 @@ options:
-
7
+
5
-

Read the docs

+

Learn more

Check out the documentation to learn more.

@@ -392,9 +347,9 @@ options:

This is my personal hobby project and is not affiliated with AT or endorsed by anyone.

Who knows if this is a good idea?

- + - - - + +