@backtickjs/core 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jean-François Bisson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,79 @@
1
- # Temporary Holding Version
1
+ # @backtickjs/core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The `cs` tag and the types around it. A client script, written `` cs`…` `` inside a server component, is code that runs on the client; values from the server cross into it as splices written `$name`, type-checked on both sides. Every Backtick app imports `cs` from here, whatever its UI framework.
4
+
5
+ `cs` is compiled at build time, so a project also needs a build plugin (`@backtickjs/node-plugin` or `@backtickjs/bun-plugin`), an adapter for its framework (`@backtickjs/react`, `@backtickjs/react-native` or `@backtickjs/solid-js`) and `@backtickjs/tsc` for type checking. Called without the compiler, `cs` throws. `npx create-backtick-app@latest` sets all of this up.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install @backtickjs/core
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ A server component reads data on the server and returns a client script. A client component is a script written as an arrow function, used as a `<$Name>` tag:
16
+
17
+ ```tsx
18
+ import { cs } from "@backtickjs/core";
19
+ import { useState } from "@backtickjs/react";
20
+
21
+ // A client component: it runs on the client.
22
+ const Counter = cs`(props: { start: number }) => {
23
+ const [count, setCount] = $useState(props.start);
24
+ return <button onClick={() => setCount(count + 1)}>{count}</button>;
25
+ }`;
26
+
27
+ // A server component: it runs on your server for every request.
28
+ export async function Home() {
29
+ const node = process.version;
30
+ return cs`(
31
+ <main>
32
+ <p>Assembled with Node {$node}.</p>
33
+ <$Counter start={3} />
34
+ </main>
35
+ )`;
36
+ }
37
+ ```
38
+
39
+ `$node` and `$Counter` are splices: `node` is a server variable, written into the bundle as data; `Counter` is a script. `$useState` is a splice too, of an export the client provides. A server component is drawn inside a script through a splice: `{${<Footer />}}`.
40
+
41
+ What can be spliced is `Spliceable`: strings, numbers, booleans, `null`, `undefined`, plain arrays and objects of those, and scripts. Host functions and class instances can't be spliced; a function the client calls is written as a script, `` cs`(n: number) => …` ``.
42
+
43
+ To use a package the client provides beyond what an adapter covers, describe its export with `createImport`. `from` is the specifier the bundle imports, and `version` the semver range of its package that the export works with:
44
+
45
+ ```ts
46
+ import { createImport } from "@backtickjs/core";
47
+
48
+ const greet = createImport<() => string>({
49
+ name: "greet",
50
+ from: "my-native-module",
51
+ version: "^1.0.0",
52
+ });
53
+ // In a script: cs`<p>{$greet()}</p>`
54
+ ```
55
+
56
+ The client must provide that package, and the server must list it in the bundler's `packageVersions` at a version in that range.
57
+
58
+ ## API
59
+
60
+ - `cs`: the tag for client scripts. Compiled at build time; throws if called uncompiled.
61
+ - `Client<T>`: the type of a script that stands for a `T` on the client.
62
+ - `Spliceable`: what a server may splice into a script.
63
+ - `Spliced<T>`: what a spliceable value becomes on the client (`Client<U>` becomes `U`; arrays and objects map member by member).
64
+ - `createImport<T>({ name, from, version })`: an export of a module the client provides, as a value a script can splice.
65
+ - `isClientImport`, `ClientImport`: its check and type.
66
+ - `createJsxElement`, `isJsxElement`, `JsxElement`, `JsxElementOf`, `JsxElementTypeOf`: what a server-side JSX tag evaluates to before bundling. Used by adapters' JSX runtimes.
67
+ - `create`, `isClientScript`, `ClientScript`, `ClientModule`, `Param`: the compiled form of a script, written by the compiler and read by the bundler. Not for application code.
68
+
69
+ ## Related
70
+
71
+ - [`@backtickjs/bundler`](https://github.com/backtickjs/backtick/tree/main/packages/bundler): runs server components and writes the bundle
72
+ - [`@backtickjs/react`](https://github.com/backtickjs/backtick/tree/main/packages/react), [`@backtickjs/react-native`](https://github.com/backtickjs/backtick/tree/main/packages/react-native), [`@backtickjs/solid-js`](https://github.com/backtickjs/backtick/tree/main/packages/solid-js): framework adapters
73
+ - [`@backtickjs/node-plugin`](https://github.com/backtickjs/backtick/tree/main/packages/node-plugin), [`@backtickjs/bun-plugin`](https://github.com/backtickjs/backtick/tree/main/packages/bun-plugin): compile scripts as your server loads them
74
+ - [`@backtickjs/tsc`](https://github.com/backtickjs/backtick/tree/main/packages/tsc): type-checks scripts and their splices
75
+ - [Documentation](https://backtickjs.com/docs)
76
+
77
+ ## License
78
+
79
+ MIT
@@ -0,0 +1,5 @@
1
+ declare const ClientBrand: unique symbol;
2
+ export interface Client<T> {
3
+ readonly [ClientBrand]: T;
4
+ }
5
+ export {};
package/dist/Client.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=Client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Client.js","sourceRoot":"","sources":["../src/Client.ts"],"names":[],"mappings":""}
@@ -0,0 +1,17 @@
1
+ import type { Client } from "./Client.js";
2
+ /**
3
+ * An export of a module the client provides, held as a value: what a script
4
+ * splices to use it, and what a bundle imports it by.
5
+ */
6
+ export interface ClientImport<T> extends Client<T> {
7
+ readonly "@backtickjs": "ClientImport";
8
+ readonly name: string;
9
+ readonly from: string;
10
+ readonly version: string;
11
+ }
12
+ export declare function isClientImport(value: unknown): value is ClientImport<unknown>;
13
+ export declare function createImport<T>({ name, from, version, }: {
14
+ name: string;
15
+ from: string;
16
+ version: string;
17
+ }): Client<T>;
@@ -0,0 +1,17 @@
1
+ export function isClientImport(value) {
2
+ return (typeof value === "object" &&
3
+ value !== null &&
4
+ "@backtickjs" in value &&
5
+ value["@backtickjs"] === "ClientImport");
6
+ }
7
+ // `T` is what the module's own declarations say the export is; nothing here
8
+ // can check the two agree, and the module the client resolves is what does.
9
+ export function createImport({ name, from, version, }) {
10
+ return {
11
+ "@backtickjs": "ClientImport",
12
+ name,
13
+ from,
14
+ version,
15
+ };
16
+ }
17
+ //# sourceMappingURL=ClientImport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClientImport.js","sourceRoot":"","sources":["../src/ClientImport.ts"],"names":[],"mappings":"AAeA,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,aAAa,IAAI,KAAK;QACtB,KAAK,CAAC,aAAa,CAAC,KAAK,cAAc,CACxC,CAAC;AACJ,CAAC;AAED,4EAA4E;AAC5E,4EAA4E;AAC5E,MAAM,UAAU,YAAY,CAAI,EAC9B,IAAI,EACJ,IAAI,EACJ,OAAO,GAKR;IACC,OAAO;QACL,aAAa,EAAE,cAAc;QAC7B,IAAI;QACJ,IAAI;QACJ,OAAO;KACsB,CAAC;AAClC,CAAC"}
@@ -0,0 +1,22 @@
1
+ import type { Spliceable } from "./Spliceable.js";
2
+ export type Param = {
3
+ kind: "splice";
4
+ bindings: readonly string[];
5
+ } | {
6
+ kind: "capture";
7
+ key: string;
8
+ };
9
+ export interface ClientModule {
10
+ readonly id: string;
11
+ readonly code: string;
12
+ readonly map: string;
13
+ readonly dependencies: readonly string[];
14
+ readonly params: readonly Param[];
15
+ }
16
+ export interface ClientScript {
17
+ readonly "@backtickjs": "ClientScript";
18
+ readonly module: ClientModule;
19
+ readonly args: readonly Spliceable[];
20
+ }
21
+ export declare function isClientScript(value: unknown): value is ClientScript;
22
+ export declare function create(module: ClientModule, args: readonly Spliceable[]): ClientScript;
@@ -0,0 +1,10 @@
1
+ export function isClientScript(value) {
2
+ return (typeof value === "object" &&
3
+ value !== null &&
4
+ "@backtickjs" in value &&
5
+ value["@backtickjs"] === "ClientScript");
6
+ }
7
+ export function create(module, args) {
8
+ return { "@backtickjs": "ClientScript", module, args };
9
+ }
10
+ //# sourceMappingURL=ClientScript.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClientScript.js","sourceRoot":"","sources":["../src/ClientScript.ts"],"names":[],"mappings":"AAmCA,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,aAAa,IAAI,KAAK;QACtB,KAAK,CAAC,aAAa,CAAC,KAAK,cAAc,CACxC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,MAAM,CACpB,MAAoB,EACpB,IAA2B;IAE3B,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AACzD,CAAC"}
@@ -0,0 +1,28 @@
1
+ import type { Client } from "./Client.js";
2
+ /**
3
+ * What a JSX tag evaluates to on the server, before bundling resolves it: the
4
+ * tag and its props, which are the server's own. What a drawing is, is the
5
+ * adapter's; this is what the bundler expands. A client value, of the type its
6
+ * adapter names (Solid's: its element), so a host may hand one over wherever
7
+ * a drawing may stand.
8
+ */
9
+ export interface JsxElement extends Client<unknown> {
10
+ readonly "@backtickjs": "JsxElement";
11
+ readonly type: string | ((props: never) => unknown);
12
+ readonly props: unknown;
13
+ }
14
+ export declare function isJsxElement(value: unknown): value is JsxElement;
15
+ export declare function createJsxElement(type: string | ((props: never) => unknown), props: unknown): JsxElement;
16
+ /**
17
+ * A JSX expression: in a script, what the framework draws, so it fits the
18
+ * framework's slots; on the host, a client value standing for it, so it
19
+ * splices.
20
+ */
21
+ export type JsxElementOf<T> = T & Client<T>;
22
+ /**
23
+ * What a tag may name: an intrinsic element; a component as a script sees it,
24
+ * answering what the framework draws (a client component, or a server
25
+ * component lowered); or a server component on the host, answering a script
26
+ * that draws, or nothing.
27
+ */
28
+ export type JsxElementTypeOf<T> = string | ((props: never) => T) | ((props: never) => Client<T> | null | Promise<Client<T> | null>);
@@ -0,0 +1,10 @@
1
+ export function isJsxElement(value) {
2
+ return (typeof value === "object" &&
3
+ value !== null &&
4
+ "@backtickjs" in value &&
5
+ value["@backtickjs"] === "JsxElement");
6
+ }
7
+ export function createJsxElement(type, props) {
8
+ return { "@backtickjs": "JsxElement", type, props };
9
+ }
10
+ //# sourceMappingURL=JsxElement.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JsxElement.js","sourceRoot":"","sources":["../src/JsxElement.ts"],"names":[],"mappings":"AAeA,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,aAAa,IAAI,KAAK;QACtB,KAAK,CAAC,aAAa,CAAC,KAAK,YAAY,CACtC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,IAA0C,EAC1C,KAAc;IAEd,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,IAAI,EAAE,KAAK,EAAgB,CAAC;AACpE,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { Client } from "./Client.js";
2
+ /**
3
+ * What the host may splice: the value written out, a script standing in for
4
+ * it, or a container mixing the two. Not a host function, which is host code:
5
+ * a client function is a script, cs`(n: number) => …`. An index signature rather than a mapped
6
+ * type, so it can be named in a `satisfies` without a type to walk.
7
+ */
8
+ export type Spliceable = Client<unknown> | null | undefined | number | boolean | string | readonly Spliceable[] | {
9
+ readonly [key: string]: Spliceable;
10
+ };
11
+ export type Spliced<T> = T extends Client<infer U> ? U : T extends readonly (infer Item extends Spliceable)[] ? Spliced<Item>[] : T extends {
12
+ readonly [key: string]: Spliceable;
13
+ } ? {
14
+ -readonly [K in keyof T]: Spliced<T[K]>;
15
+ } : T;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=Spliceable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Spliceable.js","sourceRoot":"","sources":["../src/Spliceable.ts"],"names":[],"mappings":""}
package/dist/cs.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import type { Client } from "./Client.js";
2
+ import { create } from "./ClientScript.js";
3
+ import type { Spliceable } from "./Spliceable.js";
4
+ import type { Spliced } from "./Spliceable.js";
5
+ declare function lift<T>(_: T): Client<T>;
6
+ declare function splice<T extends [T] extends [Spliceable] ? unknown : Spliceable>(_: T): unknown extends Spliced<T> ? any : Spliced<T>;
7
+ /**
8
+ * The globals a project declares — its libs, `@types`, a `declare global` —
9
+ * as an ordinary object type: reading one it doesn't declare is an error
10
+ * however strict the project is, where `typeof globalThis` itself lets it
11
+ * through as `any` without `noImplicitAny`.
12
+ */
13
+ export type GlobalThis = {
14
+ [Key in keyof typeof globalThis]: (typeof globalThis)[Key];
15
+ };
16
+ export declare const cs: ((_strings: TemplateStringsArray, ..._values: unknown[]) => Client<unknown>) & {
17
+ create: typeof create;
18
+ lift: typeof lift;
19
+ splice: typeof splice;
20
+ } & {
21
+ readonly globalThis: GlobalThis;
22
+ };
23
+ export {};
package/dist/cs.js ADDED
@@ -0,0 +1,27 @@
1
+ import { create } from "./ClientScript.js";
2
+ // The root of a script
3
+ function lift(_) {
4
+ throw new Error("Don't call `cs.lift` directly; it's used to generate virtual " +
5
+ "code for the typechecker. Write code using cs`...` instead.");
6
+ }
7
+ // A constraint that is `unknown` for a value that splices, rather than
8
+ // `Spliceable` itself or an intersection with it: either of those keeps a
9
+ // literal a literal instead of widening it. What can't be spliced is refused
10
+ // once, where it is written, and is `any` after: refused, `T` falls back to
11
+ // `Spliceable`, which splices to `unknown`.
12
+ function splice(_) {
13
+ throw new Error("Don't call `cs.splice` directly; it's used to generate virtual " +
14
+ "code for the typechecker. Write code using cs`...` instead.");
15
+ }
16
+ // What hangs off the tag.
17
+ const members = {
18
+ create,
19
+ lift,
20
+ splice,
21
+ };
22
+ // `globalThis`, which a name the script didn't bind is read off. In the type
23
+ // and not in the object: nothing runs the virtual code it is written in.
24
+ export const cs = Object.assign((_strings, ..._values) => {
25
+ throw new Error("`cs` was not compiled. Is @backtickjs set up for this project?");
26
+ }, members);
27
+ //# sourceMappingURL=cs.js.map
package/dist/cs.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cs.js","sourceRoot":"","sources":["../src/cs.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAI3C,uBAAuB;AACvB,SAAS,IAAI,CAAI,CAAI;IACnB,MAAM,IAAI,KAAK,CACb,+DAA+D;QAC7D,6DAA6D,CAChE,CAAC;AACJ,CAAC;AAED,uEAAuE;AACvE,0EAA0E;AAC1E,6EAA6E;AAC7E,4EAA4E;AAC5E,4CAA4C;AAC5C,SAAS,MAAM,CACb,CAAI;IAEJ,MAAM,IAAI,KAAK,CACb,iEAAiE;QAC/D,6DAA6D,CAChE,CAAC;AACJ,CAAC;AAED,0BAA0B;AAC1B,MAAM,OAAO,GAAG;IACd,MAAM;IACN,IAAI;IACJ,MAAM;CACP,CAAC;AAYF,6EAA6E;AAC7E,yEAAyE;AACzE,MAAM,CAAC,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAC7B,CAAC,QAA8B,EAAE,GAAG,OAAkB,EAAmB,EAAE;IACzE,MAAM,IAAI,KAAK,CACb,gEAAgE,CACjE,CAAC;AACJ,CAAC,EACD,OAEC,CACF,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Backtick: the `cs` tag, what a host may hand a client, and what a template
3
+ * compiles to.
4
+ */
5
+ export { cs } from "./cs.js";
6
+ export type { Client } from "./Client.js";
7
+ export type { Spliceable, Spliced } from "./Spliceable.js";
8
+ export { createImport, isClientImport, type ClientImport, } from "./ClientImport.js";
9
+ export { createJsxElement, isJsxElement, type JsxElement, type JsxElementOf, type JsxElementTypeOf, } from "./JsxElement.js";
10
+ export { create, type ClientModule, type ClientScript, isClientScript, type Param, } from "./ClientScript.js";
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Backtick: the `cs` tag, what a host may hand a client, and what a template
3
+ * compiles to.
4
+ */
5
+ // The tag, and how a host language spells a script standing in for a value.
6
+ export { cs } from "./cs.js";
7
+ export { createImport, isClientImport, } from "./ClientImport.js";
8
+ // A drawing as a host builds one: what an adapter's JSX runtime makes, and
9
+ // what the bundler expands.
10
+ export { createJsxElement, isJsxElement, } from "./JsxElement.js";
11
+ // What a template compiles to: the compiler writes one and the bundler reads
12
+ // it, so the shape is neither end's.
13
+ export { create, isClientScript, } from "./ClientScript.js";
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,4EAA4E;AAC5E,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAM7B,OAAO,EACL,YAAY,EACZ,cAAc,GAEf,MAAM,mBAAmB,CAAC;AAE3B,2EAA2E;AAC3E,4BAA4B;AAC5B,OAAO,EACL,gBAAgB,EAChB,YAAY,GAIb,MAAM,iBAAiB,CAAC;AAEzB,6EAA6E;AAC7E,qCAAqC;AACrC,OAAO,EACL,MAAM,EAGN,cAAc,GAEf,MAAM,mBAAmB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
2
  "name": "@backtickjs/core",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "description": "The cs tag for Backtick client scripts, and the types for the values that cross from server to client",
5
+ "keywords": [
6
+ "backtick",
7
+ "react-native",
8
+ "server-driven-ui",
9
+ "server-components",
10
+ "jsx",
11
+ "typescript",
12
+ "tagged-template"
13
+ ],
14
+ "homepage": "https://backtickjs.com",
15
+ "bugs": {
16
+ "url": "https://github.com/backtickjs/backtick/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/backtickjs/backtick.git",
21
+ "directory": "packages/core"
22
+ },
23
+ "license": "MIT",
24
+ "author": "Jean-François Bisson <jf@backtickjs.com>",
25
+ "type": "module",
26
+ "main": "./dist/index.js",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ }
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "!dist/._cache",
36
+ "src"
37
+ ],
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "peerDependencies": {
42
+ "typescript": "^6.0.3"
43
+ },
44
+ "devDependencies": {
45
+ "prettier": "^3.6.2",
46
+ "typescript": "^6.0.3"
47
+ },
48
+ "scripts": {
49
+ "build": "tsc -b",
50
+ "dev": "tsc -b --watch",
51
+ "format": "prettier --write --ignore-unknown .",
52
+ "format:check": "prettier --check --ignore-unknown .",
53
+ "clean": "rm -rf node_modules dist .turbo",
54
+ "test": "node --test \"test/**/*.test.ts\""
55
+ }
6
56
  }
package/src/Client.ts ADDED
@@ -0,0 +1,4 @@
1
+ declare const ClientBrand: unique symbol;
2
+ export interface Client<T> {
3
+ readonly [ClientBrand]: T;
4
+ }
@@ -0,0 +1,42 @@
1
+ import type { Client } from "./Client.js";
2
+
3
+ /**
4
+ * An export of a module the client provides, held as a value: what a script
5
+ * splices to use it, and what a bundle imports it by.
6
+ */
7
+ export interface ClientImport<T> extends Client<T> {
8
+ readonly "@backtickjs": "ClientImport";
9
+ readonly name: string;
10
+ // the specifier the client resolves
11
+ readonly from: string;
12
+ // the versions of `from`'s package it works with, a semver range
13
+ readonly version: string;
14
+ }
15
+
16
+ export function isClientImport(value: unknown): value is ClientImport<unknown> {
17
+ return (
18
+ typeof value === "object" &&
19
+ value !== null &&
20
+ "@backtickjs" in value &&
21
+ value["@backtickjs"] === "ClientImport"
22
+ );
23
+ }
24
+
25
+ // `T` is what the module's own declarations say the export is; nothing here
26
+ // can check the two agree, and the module the client resolves is what does.
27
+ export function createImport<T>({
28
+ name,
29
+ from,
30
+ version,
31
+ }: {
32
+ name: string;
33
+ from: string;
34
+ version: string;
35
+ }): Client<T> {
36
+ return {
37
+ "@backtickjs": "ClientImport",
38
+ name,
39
+ from,
40
+ version,
41
+ } as unknown as ClientImport<T>;
42
+ }
@@ -0,0 +1,50 @@
1
+ import type { Spliceable } from "./Spliceable.js";
2
+
3
+ // One parameter of a script's function. Splices come first, in the order the
4
+ // script reads them, then captures.
5
+ export type Param =
6
+ // a host value, passed as a function the script calls with `bindings`
7
+ // wherever it reads it, a tag (`<$Card>`) among them
8
+ | { kind: "splice"; bindings: readonly string[] }
9
+ // a variable of the enclosing script
10
+ | { kind: "capture"; key: string };
11
+
12
+ // The compiled code of one `cs`, shared by every script it creates.
13
+ export interface ClientModule {
14
+ // `<fileHash>:<line>:<column>` of the `cs`. The hash keeps ids from two
15
+ // files apart: they match only when the files are identical.
16
+ readonly id: string;
17
+ // A module-table entry, `(module, exports, require) => { … }`, whose default
18
+ // export is the script's function.
19
+ readonly code: string;
20
+ // Source map into the host file, without its content.
21
+ readonly map: string;
22
+ // The modules `code` requires.
23
+ readonly dependencies: readonly string[];
24
+ // `params[i]` is the function's `$splice<i>` or `$capture<i>`
25
+ readonly params: readonly Param[];
26
+ }
27
+
28
+ // One run of a `cs`: its module, and the values that run passes it.
29
+ export interface ClientScript {
30
+ readonly "@backtickjs": "ClientScript";
31
+ readonly module: ClientModule;
32
+ // one per splice, in parameter order
33
+ readonly args: readonly Spliceable[];
34
+ }
35
+
36
+ export function isClientScript(value: unknown): value is ClientScript {
37
+ return (
38
+ typeof value === "object" &&
39
+ value !== null &&
40
+ "@backtickjs" in value &&
41
+ value["@backtickjs"] === "ClientScript"
42
+ );
43
+ }
44
+
45
+ export function create(
46
+ module: ClientModule,
47
+ args: readonly Spliceable[],
48
+ ): ClientScript {
49
+ return { "@backtickjs": "ClientScript", module, args };
50
+ }
@@ -0,0 +1,53 @@
1
+ import type { Client } from "./Client.js";
2
+
3
+ /**
4
+ * What a JSX tag evaluates to on the server, before bundling resolves it: the
5
+ * tag and its props, which are the server's own. What a drawing is, is the
6
+ * adapter's; this is what the bundler expands. A client value, of the type its
7
+ * adapter names (Solid's: its element), so a host may hand one over wherever
8
+ * a drawing may stand.
9
+ */
10
+ export interface JsxElement extends Client<unknown> {
11
+ readonly "@backtickjs": "JsxElement";
12
+ readonly type: string | ((props: never) => unknown);
13
+ readonly props: unknown;
14
+ }
15
+
16
+ export function isJsxElement(value: unknown): value is JsxElement {
17
+ return (
18
+ typeof value === "object" &&
19
+ value !== null &&
20
+ "@backtickjs" in value &&
21
+ value["@backtickjs"] === "JsxElement"
22
+ );
23
+ }
24
+
25
+ export function createJsxElement(
26
+ type: string | ((props: never) => unknown),
27
+ props: unknown,
28
+ ): JsxElement {
29
+ return { "@backtickjs": "JsxElement", type, props } as JsxElement;
30
+ }
31
+
32
+ // What an adapter's JSX namespace types its `Element` and `ElementType` as,
33
+ // given what its framework draws (Solid's `JSX.Element`, React's `ReactNode`).
34
+ // One namespace types both a host's JSX and a script's, so each type answers
35
+ // for both sides.
36
+
37
+ /**
38
+ * A JSX expression: in a script, what the framework draws, so it fits the
39
+ * framework's slots; on the host, a client value standing for it, so it
40
+ * splices.
41
+ */
42
+ export type JsxElementOf<T> = T & Client<T>;
43
+
44
+ /**
45
+ * What a tag may name: an intrinsic element; a component as a script sees it,
46
+ * answering what the framework draws (a client component, or a server
47
+ * component lowered); or a server component on the host, answering a script
48
+ * that draws, or nothing.
49
+ */
50
+ export type JsxElementTypeOf<T> =
51
+ | string
52
+ | ((props: never) => T)
53
+ | ((props: never) => Client<T> | null | Promise<Client<T> | null>);
@@ -0,0 +1,31 @@
1
+ import type { Client } from "./Client.js";
2
+
3
+ /**
4
+ * What the host may splice: the value written out, a script standing in for
5
+ * it, or a container mixing the two. Not a host function, which is host code:
6
+ * a client function is a script, cs`(n: number) => …`. An index signature rather than a mapped
7
+ * type, so it can be named in a `satisfies` without a type to walk.
8
+ */
9
+ export type Spliceable =
10
+ | Client<unknown>
11
+ | null
12
+ | undefined
13
+ | number
14
+ | boolean
15
+ | string
16
+ | readonly Spliceable[]
17
+ | { readonly [key: string]: Spliceable };
18
+
19
+ // What a spliceable becomes on the client:
20
+ // Client<U> -> U
21
+ // T[] -> Spliced<T>[]
22
+ // { k: T } -> { k: Spliced<T> }
23
+ // primitives -> unchanged
24
+ export type Spliced<T> =
25
+ T extends Client<infer U>
26
+ ? U
27
+ : T extends readonly (infer Item extends Spliceable)[]
28
+ ? Spliced<Item>[]
29
+ : T extends { readonly [key: string]: Spliceable }
30
+ ? { -readonly [K in keyof T]: Spliced<T[K]> }
31
+ : T;
package/src/cs.ts ADDED
@@ -0,0 +1,56 @@
1
+ import type { Client } from "./Client.js";
2
+ import { create } from "./ClientScript.js";
3
+ import type { Spliceable } from "./Spliceable.js";
4
+ import type { Spliced } from "./Spliceable.js";
5
+
6
+ // The root of a script
7
+ function lift<T>(_: T): Client<T> {
8
+ throw new Error(
9
+ "Don't call `cs.lift` directly; it's used to generate virtual " +
10
+ "code for the typechecker. Write code using cs`...` instead.",
11
+ );
12
+ }
13
+
14
+ // A constraint that is `unknown` for a value that splices, rather than
15
+ // `Spliceable` itself or an intersection with it: either of those keeps a
16
+ // literal a literal instead of widening it. What can't be spliced is refused
17
+ // once, where it is written, and is `any` after: refused, `T` falls back to
18
+ // `Spliceable`, which splices to `unknown`.
19
+ function splice<T extends [T] extends [Spliceable] ? unknown : Spliceable>(
20
+ _: T,
21
+ ): unknown extends Spliced<T> ? any : Spliced<T> {
22
+ throw new Error(
23
+ "Don't call `cs.splice` directly; it's used to generate virtual " +
24
+ "code for the typechecker. Write code using cs`...` instead.",
25
+ );
26
+ }
27
+
28
+ // What hangs off the tag.
29
+ const members = {
30
+ create,
31
+ lift,
32
+ splice,
33
+ };
34
+
35
+ /**
36
+ * The globals a project declares — its libs, `@types`, a `declare global` —
37
+ * as an ordinary object type: reading one it doesn't declare is an error
38
+ * however strict the project is, where `typeof globalThis` itself lets it
39
+ * through as `any` without `noImplicitAny`.
40
+ */
41
+ export type GlobalThis = {
42
+ [Key in keyof typeof globalThis]: (typeof globalThis)[Key];
43
+ };
44
+
45
+ // `globalThis`, which a name the script didn't bind is read off. In the type
46
+ // and not in the object: nothing runs the virtual code it is written in.
47
+ export const cs = Object.assign(
48
+ (_strings: TemplateStringsArray, ..._values: unknown[]): Client<unknown> => {
49
+ throw new Error(
50
+ "`cs` was not compiled. Is @backtickjs set up for this project?",
51
+ );
52
+ },
53
+ members as typeof members & {
54
+ readonly globalThis: GlobalThis;
55
+ },
56
+ );
package/src/index.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Backtick: the `cs` tag, what a host may hand a client, and what a template
3
+ * compiles to.
4
+ */
5
+
6
+ // The tag, and how a host language spells a script standing in for a value.
7
+ export { cs } from "./cs.js";
8
+ export type { Client } from "./Client.js";
9
+
10
+ // What a host may hand a client: data, a script, or an export of a module the
11
+ // client provides.
12
+ export type { Spliceable, Spliced } from "./Spliceable.js";
13
+ export {
14
+ createImport,
15
+ isClientImport,
16
+ type ClientImport,
17
+ } from "./ClientImport.js";
18
+
19
+ // A drawing as a host builds one: what an adapter's JSX runtime makes, and
20
+ // what the bundler expands.
21
+ export {
22
+ createJsxElement,
23
+ isJsxElement,
24
+ type JsxElement,
25
+ type JsxElementOf,
26
+ type JsxElementTypeOf,
27
+ } from "./JsxElement.js";
28
+
29
+ // What a template compiles to: the compiler writes one and the bundler reads
30
+ // it, so the shape is neither end's.
31
+ export {
32
+ create,
33
+ type ClientModule,
34
+ type ClientScript,
35
+ isClientScript,
36
+ type Param,
37
+ } from "./ClientScript.js";