@hatchingpoint/point 0.0.12 → 0.0.14

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.
@@ -11,6 +11,7 @@ import type {
11
11
  PointCoreTypeExpression,
12
12
  PointCoreValueDeclaration,
13
13
  PointSourceSpan,
14
+ PointSemanticViewControls,
14
15
  } from "../core/ast.ts";
15
16
  import type {
16
17
  PointSemanticBinding,
@@ -35,6 +36,8 @@ import type {
35
36
  PointSemanticTypeExpression,
36
37
  PointSemanticUseDeclaration,
37
38
  PointSemanticViewDeclaration,
39
+ PointSemanticViewStatement,
40
+ PointSemanticPageDeclaration,
38
41
  PointSemanticWorkflowDeclaration,
39
42
  PointSemanticWorkflowStatement,
40
43
  PointSemanticActionDeclaration,
@@ -104,6 +107,7 @@ function buildCallableMap(
104
107
  declaration.kind === "action" ||
105
108
  declaration.kind === "policy" ||
106
109
  declaration.kind === "view" ||
110
+ declaration.kind === "page" ||
107
111
  declaration.kind === "route" ||
108
112
  declaration.kind === "workflow" ||
109
113
  declaration.kind === "command"
@@ -119,6 +123,7 @@ function defaultOutputName(declaration: PointSemanticDeclaration): string {
119
123
  if (declaration.kind === "label") return "label";
120
124
  if (declaration.kind === "policy") return "policy";
121
125
  if (declaration.kind === "view") return "view";
126
+ if (declaration.kind === "page") return "page";
122
127
  if (declaration.kind === "route") return "route";
123
128
  if ("output" in declaration) return toIdentifier(declaration.output.name);
124
129
  return "result";
@@ -152,6 +157,8 @@ function desugarDeclaration(
152
157
  return [desugarPolicy(declaration, records, callables)];
153
158
  case "view":
154
159
  return [desugarView(declaration, records, callables)];
160
+ case "page":
161
+ return [desugarPage(declaration, records, callables)];
155
162
  case "route":
156
163
  return [desugarRoute(declaration, records, callables)];
157
164
  case "workflow":
@@ -296,13 +303,85 @@ function desugarView(
296
303
  const outputType: PointCoreTypeExpression = { kind: "typeRef", name: "Text", args: [] };
297
304
  const { params, bindings } = collectBindings(declaration.inputs, declaration.output);
298
305
  const ctx: DesugarContext = { records, callables, bindings, outputName: "page", outputType };
306
+ const renderStatements = declaration.body.filter(
307
+ (statement): statement is Extract<PointSemanticViewStatement, { kind: "render" | "whenRender" }> =>
308
+ statement.kind === "render" || statement.kind === "whenRender",
309
+ );
310
+ const metadata = semanticDeclarationMetadata(declaration);
311
+ const viewControls = buildViewControls(declaration, ctx);
312
+ if (viewControls) metadata.viewControls = viewControls;
299
313
  return {
300
314
  kind: "function",
301
315
  name: semanticFunctionName(declaration.name, "view", "view"),
302
316
  params,
303
317
  returnType: outputType,
304
- body: desugarViewBody(declaration.body, ctx),
305
- semantic: semanticDeclarationMetadata(declaration),
318
+ body: desugarViewBody(renderStatements, ctx),
319
+ semantic: metadata,
320
+ span: declaration.span,
321
+ };
322
+ }
323
+
324
+ function buildViewControls(
325
+ declaration: PointSemanticViewDeclaration,
326
+ ctx: DesugarContext,
327
+ ): PointSemanticViewControls | undefined {
328
+ const bindStatements = declaration.body.filter(
329
+ (statement): statement is Extract<PointSemanticViewStatement, { kind: "bindCheckbox" }> => statement.kind === "bindCheckbox",
330
+ );
331
+ if (bindStatements.length === 0) return undefined;
332
+
333
+ const onChangeCall = declaration.body.find(
334
+ (statement): statement is Extract<PointSemanticViewStatement, { kind: "onChangeCall" }> => statement.kind === "onChangeCall",
335
+ );
336
+ const handlerInput = declaration.inputs.find((input) => input.type.name === "Handler" && input.type.args.length === 1);
337
+ const callbackLabel = onChangeCall?.callback ?? handlerInput?.label;
338
+ if (!callbackLabel) {
339
+ throw new Error(`View ${declaration.name} with bind checkbox requires input Handler T or on change call`);
340
+ }
341
+ const changeCallback = toIdentifier(callbackLabel);
342
+
343
+ return {
344
+ changeCallback,
345
+ checkboxes: bindStatements.map((statement) => {
346
+ const target = desugarExpression(statement.target, ctx);
347
+ if (target.kind !== "property") {
348
+ throw new Error(`bind checkbox target must be a record field access`);
349
+ }
350
+ if (target.target.kind !== "identifier") {
351
+ throw new Error(`bind checkbox target must start with an input record`);
352
+ }
353
+ return {
354
+ label: statement.label,
355
+ target,
356
+ recordParam: target.target.name,
357
+ fieldName: target.name,
358
+ };
359
+ }),
360
+ };
361
+ }
362
+
363
+ function desugarPage(
364
+ declaration: PointSemanticPageDeclaration,
365
+ records: Map<string, Map<string, string>>,
366
+ callables: Map<string, string>,
367
+ ): PointCoreFunctionDeclaration {
368
+ const outputType: PointCoreTypeExpression = { kind: "typeRef", name: "Text", args: [] };
369
+ const pageType: PointCoreTypeExpression = { kind: "typeRef", name: "Page", args: [] };
370
+ const { params, bindings } = collectBindings(declaration.inputs, { name: "page", type: pageType });
371
+ const ctx: DesugarContext = { records, callables, bindings, outputName: "page", outputType };
372
+ const metadata = semanticDeclarationMetadata(declaration);
373
+ metadata.pageLayout = {
374
+ title: desugarExpression(declaration.title, ctx),
375
+ description: declaration.description ? desugarExpression(declaration.description, ctx) : undefined,
376
+ main: desugarExpression(declaration.main, ctx),
377
+ };
378
+ return {
379
+ kind: "function",
380
+ name: semanticFunctionName(declaration.name, "page", "page"),
381
+ params,
382
+ returnType: outputType,
383
+ body: [{ kind: "return", value: metadata.pageLayout.main, span: declaration.span }],
384
+ semantic: metadata,
306
385
  span: declaration.span,
307
386
  };
308
387
  }
@@ -390,7 +469,7 @@ function desugarParameter(binding: PointSemanticBinding): PointCoreParameter {
390
469
  }
391
470
 
392
471
  function desugarType(type: PointSemanticTypeExpression): PointCoreTypeExpression {
393
- if (type.name === "List" || type.name === "Maybe" || type.name === "Or") {
472
+ if (type.name === "List" || type.name === "Maybe" || type.name === "Or" || type.name === "Handler") {
394
473
  return { kind: "typeRef", name: type.name, args: type.args.map(desugarType) };
395
474
  }
396
475
  const primitives = new Set(["Text", "Int", "Float", "Bool", "Void", "Error", "Page"]);
@@ -32,6 +32,14 @@ export function parseSemanticTypeExpression(source: string): PointSemanticTypeEx
32
32
  if (maybeMatch) {
33
33
  return { kind: "typeRef", name: "Maybe", args: [parseSemanticTypeExpression(maybeMatch[1] ?? "")] };
34
34
  }
35
+ const handlerMatch = trimmed.match(/^Handler\s+(.+)$/);
36
+ if (handlerMatch) {
37
+ return { kind: "typeRef", name: "Handler", args: [parseSemanticTypeExpression(handlerMatch[1] ?? "")] };
38
+ }
39
+ const handlerGenericMatch = trimmed.match(/^Handler<(.+)>$/);
40
+ if (handlerGenericMatch) {
41
+ return { kind: "typeRef", name: "Handler", args: [parseSemanticTypeExpression(handlerGenericMatch[1] ?? "")] };
42
+ }
35
43
  return { kind: "typeRef", name: trimmed, args: [] };
36
44
  }
37
45
 
@@ -83,6 +83,14 @@ function formatDeclaration(declaration: PointSemanticDeclaration): string[] {
83
83
  ...formatViewOutput(declaration.output),
84
84
  ...declaration.body.map((statement) => ` ${formatViewStatement(statement)}`),
85
85
  ];
86
+ case "page":
87
+ return [
88
+ `page ${declaration.name}`,
89
+ ...formatInputs(declaration.inputs),
90
+ ` title ${formatExpression(declaration.title)}`,
91
+ ...(declaration.description ? [` description ${formatExpression(declaration.description)}`] : []),
92
+ ` main render ${formatExpression(declaration.main)}`,
93
+ ];
86
94
  case "route":
87
95
  return [
88
96
  `route ${declaration.name}`,
@@ -176,6 +184,8 @@ function formatPolicyStatement(statement: PointSemanticPolicyStatement): string
176
184
 
177
185
  function formatViewStatement(statement: PointSemanticViewStatement): string {
178
186
  if (statement.kind === "whenRender") return `when ${formatExpression(statement.condition)} render ${formatExpression(statement.value)}`;
187
+ if (statement.kind === "bindCheckbox") return `bind checkbox "${statement.label}" to ${formatExpression(statement.target)}`;
188
+ if (statement.kind === "onChangeCall") return `on change call ${statement.callback}`;
179
189
  return `render ${formatExpression(statement.value)}`;
180
190
  }
181
191
 
@@ -30,6 +30,9 @@ export function semanticDeclarationMetadata(declaration: PointSemanticDeclaratio
30
30
  effects: [],
31
31
  };
32
32
  }
33
+ if (declaration.kind === "page") {
34
+ return { kind: "page", name: declaration.name, outputName: "page", effects: [] };
35
+ }
33
36
  if (declaration.kind === "route") {
34
37
  return { kind: "route", name: declaration.name, outputName: declaration.output.name, effects: [] };
35
38
  }
@@ -11,7 +11,7 @@ export function toIdentifier(label: string): string {
11
11
  export function semanticFunctionName(
12
12
  label: string,
13
13
  outputName: string,
14
- kind: "calculation" | "rule" | "label" | "action" | "policy" | "view" | "route" | "workflow" | "command",
14
+ kind: "calculation" | "rule" | "label" | "action" | "policy" | "view" | "page" | "route" | "workflow" | "command",
15
15
  ): string {
16
16
  const base = toIdentifier(label);
17
17
  const suffix =
@@ -21,7 +21,9 @@ export function semanticFunctionName(
21
21
  ? "Policy"
22
22
  : kind === "view"
23
23
  ? "View"
24
- : kind === "route"
24
+ : kind === "page"
25
+ ? "Page"
26
+ : kind === "route"
25
27
  ? "Route"
26
28
  : kind === "workflow"
27
29
  ? "Workflow"
@@ -25,6 +25,7 @@ import type {
25
25
  PointSemanticUseDeclaration,
26
26
  PointSemanticViewDeclaration,
27
27
  PointSemanticViewStatement,
28
+ PointSemanticPageDeclaration,
28
29
  PointSemanticWorkflowDeclaration,
29
30
  PointSemanticWorkflowStatement,
30
31
  PointSemanticBinding,
@@ -129,6 +130,12 @@ export function parseSemanticSource(source: string): PointSemanticProgram {
129
130
  index = parsed.next;
130
131
  continue;
131
132
  }
133
+ if (trimmed.startsWith("page ")) {
134
+ const parsed = parsePage(lines, index, source, records, callables);
135
+ declarations.push(parsed.declaration);
136
+ index = parsed.next;
137
+ continue;
138
+ }
132
139
  if (trimmed.startsWith("route ")) {
133
140
  const parsed = parseRoute(lines, index, source, records, callables);
134
141
  declarations.push(parsed.declaration);
@@ -586,6 +593,24 @@ function parseView(
586
593
  });
587
594
  continue;
588
595
  }
596
+ const bindCheckbox = line.match(/^bind checkbox "(.+)" to (.+)$/);
597
+ if (bindCheckbox) {
598
+ statements.push({
599
+ kind: "bindCheckbox",
600
+ label: bindCheckbox[1] ?? "",
601
+ target: parseLineExpression(bindCheckbox[2] ?? "", context, source, lineNumber),
602
+ span: lineSpan(source, lineNumber),
603
+ });
604
+ continue;
605
+ }
606
+ if (line.startsWith("on change call ")) {
607
+ statements.push({
608
+ kind: "onChangeCall",
609
+ callback: line.slice("on change call ".length).trim(),
610
+ span: lineSpan(source, lineNumber),
611
+ });
612
+ continue;
613
+ }
589
614
  throw new Error(`Unknown view statement: ${line}`);
590
615
  }
591
616
 
@@ -595,6 +620,57 @@ function parseView(
595
620
  };
596
621
  }
597
622
 
623
+ function parsePage(
624
+ lines: string[],
625
+ start: number,
626
+ source: string,
627
+ records: Map<string, Map<string, string>>,
628
+ callables: string[],
629
+ ): { declaration: PointSemanticPageDeclaration; next: number } {
630
+ const name = (lines[start] ?? "").trim().slice("page ".length).trim();
631
+ const body = collectSemanticBody(lines, start + 1);
632
+ const inputs: PointSemanticBinding[] = [];
633
+ const paramTypes = new Map<string, string>();
634
+ const bindings: string[] = [];
635
+ let title: PointSemanticPageDeclaration["title"] | undefined;
636
+ let description: PointSemanticPageDeclaration["description"];
637
+ let main: PointSemanticPageDeclaration["main"] | undefined;
638
+
639
+ for (let lineIndex = 0; lineIndex < body.lines.length; lineIndex += 1) {
640
+ const line = body.lines[lineIndex] ?? "";
641
+ const lineNumber = body.lineNumbers[lineIndex] ?? start + 2;
642
+ if (line.startsWith("input ")) {
643
+ const binding = parseInputBinding(line.slice("input ".length), source, lineNumber);
644
+ inputs.push(binding);
645
+ paramTypes.set(binding.label, typeLabel(binding.type));
646
+ bindings.push(binding.label);
647
+ continue;
648
+ }
649
+ const context = buildExpressionContext({ bindings, paramTypes, recordFields: records, callables });
650
+ if (line.startsWith("title ")) {
651
+ title = parseLineExpression(line.slice("title ".length), context, source, lineNumber);
652
+ continue;
653
+ }
654
+ if (line.startsWith("description ")) {
655
+ description = parseLineExpression(line.slice("description ".length), context, source, lineNumber);
656
+ continue;
657
+ }
658
+ if (line.startsWith("main render ")) {
659
+ main = parseLineExpression(line.slice("main render ".length), context, source, lineNumber);
660
+ continue;
661
+ }
662
+ throw new Error(`Unknown page statement: ${line}`);
663
+ }
664
+
665
+ if (!title) throw new Error(`Page ${name} requires a title`);
666
+ if (!main) throw new Error(`Page ${name} requires main render`);
667
+
668
+ return {
669
+ declaration: { kind: "page", name, inputs, title, description, main, span: lineSpan(source, start + 1) },
670
+ next: body.next,
671
+ };
672
+ }
673
+
598
674
  function parseRoute(
599
675
  lines: string[],
600
676
  start: number,
@@ -893,7 +969,7 @@ function collectSemanticBody(lines: string[], start: number): SemanticBody {
893
969
  }
894
970
 
895
971
  function isSemanticTopLevel(line: string): boolean {
896
- return /^(module|use|record|calculation|rule|label|external|action|policy|view|route|workflow|command)\s+/.test(line);
972
+ return /^(module|use|record|calculation|rule|label|external|action|policy|view|page|route|workflow|command)\s+/.test(line);
897
973
  }
898
974
 
899
975
  function isLoopBoundary(line: string): boolean {
@@ -914,7 +990,10 @@ function typeLabel(type: { kind: "typeRef"; name: string; args: unknown[] }): st
914
990
  if (type.name === "Maybe" && type.args[0]) {
915
991
  return `Maybe<${typeLabel(type.args[0] as { kind: "typeRef"; name: string; args: unknown[] })}>`;
916
992
  }
917
- const primitives = new Set(["Text", "Int", "Float", "Bool", "Void", "Maybe", "Or", "Error", "Page"]);
993
+ if (type.name === "Handler" && type.args[0]) {
994
+ return `Handler ${typeLabel(type.args[0] as { kind: "typeRef"; name: string; args: unknown[] })}`;
995
+ }
996
+ const primitives = new Set(["Text", "Int", "Float", "Bool", "Void", "Maybe", "Or", "Error", "Page", "Handler"]);
918
997
  if (primitives.has(type.name)) return type.name;
919
998
  return toPascalCase(type.name);
920
999
  }
package/src/std/env.ts ADDED
@@ -0,0 +1,4 @@
1
+ export function envGet(name: string): string | null {
2
+ const value = process.env[name];
3
+ return value === undefined ? null : value;
4
+ }
package/src/std/fs.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+
3
+ type PointStdError = { message: string };
4
+
5
+ export function readFile(path: string): string | PointStdError {
6
+ try {
7
+ return readFileSync(path, "utf8");
8
+ } catch (error) {
9
+ return { message: error instanceof Error ? error.message : String(error) };
10
+ }
11
+ }
12
+
13
+ export function writeFile(path: string, contents: string): void | PointStdError {
14
+ try {
15
+ writeFileSync(path, contents, "utf8");
16
+ } catch (error) {
17
+ return { message: error instanceof Error ? error.message : String(error) };
18
+ }
19
+ }
@@ -0,0 +1,30 @@
1
+ type PointStdError = { message: string };
2
+
3
+ async function readResponseText(response: Response): Promise<string | PointStdError> {
4
+ if (!response.ok) {
5
+ return { message: `HTTP ${response.status}: ${response.statusText}` };
6
+ }
7
+ return response.text();
8
+ }
9
+
10
+ export async function httpGet(url: string): Promise<string | PointStdError> {
11
+ try {
12
+ return await readResponseText(await fetch(url));
13
+ } catch (error) {
14
+ return { message: error instanceof Error ? error.message : String(error) };
15
+ }
16
+ }
17
+
18
+ export async function httpPost(url: string, body: string): Promise<string | PointStdError> {
19
+ try {
20
+ return await readResponseText(
21
+ await fetch(url, {
22
+ method: "POST",
23
+ headers: { "Content-Type": "text/plain; charset=utf-8" },
24
+ body,
25
+ }),
26
+ );
27
+ } catch (error) {
28
+ return { message: error instanceof Error ? error.message : String(error) };
29
+ }
30
+ }
@@ -0,0 +1,13 @@
1
+ export type PointStdError = { message: string };
2
+
3
+ export function jsonParse(value: string): string | PointStdError {
4
+ try {
5
+ return JSON.stringify(JSON.parse(value));
6
+ } catch (error) {
7
+ return { message: error instanceof Error ? error.message : String(error) };
8
+ }
9
+ }
10
+
11
+ export function jsonStringify(value: string): string {
12
+ return JSON.stringify(JSON.parse(value));
13
+ }
@@ -0,0 +1,15 @@
1
+ export function textLength(value: string): number {
2
+ return value.length;
3
+ }
4
+
5
+ export function textContains(value: string, search: string): boolean {
6
+ return value.includes(search);
7
+ }
8
+
9
+ export function textSplit(value: string, separator: string): string[] {
10
+ return value.split(separator);
11
+ }
12
+
13
+ export function textTrim(value: string): string {
14
+ return value.trim();
15
+ }
@@ -0,0 +1,15 @@
1
+ export function now(): string {
2
+ return new Date().toISOString();
3
+ }
4
+
5
+ export async function sleep(ms: number): Promise<void> {
6
+ await Bun.sleep(ms);
7
+ }
8
+
9
+ export function formatTime(value: string): string {
10
+ const date = new Date(value);
11
+ if (Number.isNaN(date.getTime())) {
12
+ return value;
13
+ }
14
+ return date.toUTCString();
15
+ }