@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hatchingpoint/point",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Point language compiler and CLI.",
@@ -24,7 +24,13 @@
24
24
  "exports": {
25
25
  ".": "./src/index.ts",
26
26
  "./cli": "./src/cli.ts",
27
- "./core": "./src/core/index.ts"
27
+ "./core": "./src/core/index.ts",
28
+ "./std/json": "./src/std/json.ts",
29
+ "./std/http": "./src/std/http.ts",
30
+ "./std/text": "./src/std/text.ts",
31
+ "./std/time": "./src/std/time.ts",
32
+ "./std/fs": "./src/std/fs.ts",
33
+ "./std/env": "./src/std/env.ts"
28
34
  },
29
35
  "publishConfig": {
30
36
  "access": "public",
package/src/cli.ts CHANGED
File without changes
package/src/core/ast.ts CHANGED
@@ -85,11 +85,31 @@ export interface PointSemanticProgramMetadata {
85
85
  source: "semantic";
86
86
  }
87
87
 
88
+ export interface PointSemanticPageLayout {
89
+ title: PointCoreExpression;
90
+ description?: PointCoreExpression;
91
+ main: PointCoreExpression;
92
+ }
93
+
94
+ export interface PointSemanticViewCheckboxBinding {
95
+ label: string;
96
+ target: PointCoreExpression;
97
+ recordParam: string;
98
+ fieldName: string;
99
+ }
100
+
101
+ export interface PointSemanticViewControls {
102
+ changeCallback: string;
103
+ checkboxes: PointSemanticViewCheckboxBinding[];
104
+ }
105
+
88
106
  export interface PointSemanticDeclarationMetadata {
89
- kind: "record" | "calculation" | "rule" | "label" | "external" | "action" | "policy" | "view" | "route" | "workflow" | "command";
107
+ kind: "record" | "calculation" | "rule" | "label" | "external" | "action" | "policy" | "view" | "page" | "route" | "workflow" | "command";
90
108
  name: string;
91
109
  outputName?: string;
92
110
  effects?: string[];
111
+ pageLayout?: PointSemanticPageLayout;
112
+ viewControls?: PointSemanticViewControls;
93
113
  }
94
114
 
95
115
  export interface PointCoreTypeExpression {
package/src/core/check.ts CHANGED
@@ -28,7 +28,7 @@ type DiagnosticMetadata = Partial<Pick<PointCoreDiagnostic, "expected" | "actual
28
28
  type ScopeEntry = { type: PointCoreTypeExpression; mutable: boolean };
29
29
  type Scope = Map<string, ScopeEntry>;
30
30
 
31
- const PRIMITIVE_TYPES = new Set(["Text", "Int", "Float", "Bool", "Void", "List", "Maybe", "Error", "Or"]);
31
+ const PRIMITIVE_TYPES = new Set(["Text", "Int", "Float", "Bool", "Void", "List", "Maybe", "Error", "Or", "Page", "Handler"]);
32
32
 
33
33
  export function checkPointCore(program: PointCoreProgram): PointCoreDiagnostic[] {
34
34
  const checker = new CoreChecker(program);
@@ -125,6 +125,7 @@ class CoreChecker {
125
125
  }
126
126
  return;
127
127
  }
128
+ if (fn.semantic?.kind === "page" || fn.semantic?.kind === "view") return;
128
129
  this.checkExpressionAssignable(statement.value, fn.returnType, `fn.${fn.name}.return`, locals);
129
130
  return;
130
131
  }
@@ -531,7 +532,14 @@ class CoreChecker {
531
532
  repair: "Use syntax such as User or Error.",
532
533
  });
533
534
  }
534
- if (type.name !== "List" && type.name !== "Maybe" && type.name !== "Or" && type.args.length > 0 && !this.typeDeclarations.has(String(type.name))) {
535
+ if (type.name === "Handler" && type.args.length !== 1) {
536
+ this.push("invalid-type-arity", "Handler requires one type argument", path, type.span, {
537
+ expected: "Handler T",
538
+ actual: formatType(type),
539
+ repair: "Use Handler Listing Signals or another record type.",
540
+ });
541
+ }
542
+ if (type.name !== "List" && type.name !== "Maybe" && type.name !== "Or" && type.name !== "Handler" && type.args.length > 0 && !this.typeDeclarations.has(String(type.name))) {
535
543
  this.push("invalid-type-arity", `${type.name} does not accept type arguments`, path, type.span, {
536
544
  expected: String(type.name),
537
545
  actual: formatType(type),
package/src/core/cli.ts CHANGED
@@ -6,11 +6,13 @@ import { createPointCoreIndex, createPointCoreRepairPlan, explainPointCoreRef }
6
6
  import { createSemanticIndex, explainSemanticRef, mapPublicDiagnostics } from "../semantic/context.ts";
7
7
  import { emitPointCoreTypeScript } from "./emit-typescript.ts";
8
8
  import { emitPointCoreJavaScript } from "./emit-javascript.ts";
9
- import { emitPointCorePython } from "./emit-python.ts";
9
+ import { emitPointCorePython, isPureLogicProgram } from "./emit-python.ts";
10
+ import { canBundleRunInMemory, executeBundledEntry } from "./run-bridge.ts";
10
11
  import { formatPointSource } from "./format.ts";
11
12
  import { isCacheHit, isIncrementalEnabled, readBuildCache, recordCacheEntry, writeBuildCache } from "./incremental.ts";
12
13
  import { parsePointSource } from "./parser.ts";
13
14
  import { runCheckDocs } from "./check-docs.ts";
15
+ import { addPointDependency, modulePathFromLock, POINT_LOCK, POINT_MANIFEST, readPointLock } from "./packages.ts";
14
16
  import { runPointLspServer } from "../lsp/server.ts";
15
17
 
16
18
  const DEFAULT_INPUT = "examples/math.point";
@@ -22,7 +24,17 @@ const DEFAULT_PATTERNS = ["examples/**/*.point", "std/**/*.point", "compiler/**/
22
24
  const GENERATED_DIR = "generated";
23
25
 
24
26
  export async function main() {
25
- const [, , command = "check", input = DEFAULT_INPUT, output = DEFAULT_OUTPUT] = Bun.argv;
27
+ const command = Bun.argv[2] ?? "check";
28
+ const tail = Bun.argv.slice(3);
29
+ let input = tail[0] ?? DEFAULT_INPUT;
30
+ let output = tail[1] ?? DEFAULT_OUTPUT;
31
+ let runFlags: Record<string, boolean | undefined> | undefined;
32
+ if (command === "run") {
33
+ const parsed = parseCliFlags(tail);
34
+ runFlags = parsed.flags;
35
+ input = parsed.positional[0] ?? DEFAULT_INPUT;
36
+ output = parsed.positional[1] ?? DEFAULT_OUTPUT;
37
+ }
26
38
  if (command.endsWith("-all")) {
27
39
  await runProjectCommand(command);
28
40
  return;
@@ -43,6 +55,18 @@ export async function main() {
43
55
  return;
44
56
  }
45
57
 
58
+ if (command === "add") {
59
+ const dependencyName = input;
60
+ const spec = output;
61
+ if (!dependencyName || !spec) {
62
+ throw new Error("Usage: point add <name> <spec> (spec: workspace:<path> | file:<path> | npm:<package>)");
63
+ }
64
+ const { manifest, lock } = await addPointDependency(dependencyName, spec);
65
+ console.log(`Point add updated ${POINT_MANIFEST} and ${POINT_LOCK}: ${dependencyName} -> ${spec}`);
66
+ console.log(JSON.stringify({ name: manifest.name, dependencies: manifest.dependencies, lockPackages: Object.keys(lock.packages) }, null, 2));
67
+ return;
68
+ }
69
+
46
70
  const inputPath = resolve(process.cwd(), input);
47
71
  const source = await Bun.file(inputPath).text();
48
72
  const program = parsePointSource(source);
@@ -163,16 +187,17 @@ export async function main() {
163
187
  console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
164
188
  process.exit(1);
165
189
  }
166
- const runOutput = resolve(tmpdir(), `point-run-${Date.now()}.js`);
167
- await Bun.write(runOutput, emitPointCoreJavaScript(program));
168
190
  let entryName: string | null = null;
169
191
  try {
170
- const mod = await import(pathToFileUrl(runOutput));
171
192
  entryName = findRunEntryName(program);
172
193
  if (!entryName) throw new Error("No zero-argument entrypoint found. Define an action or calculation with no inputs.");
173
- const entry = mod[entryName];
174
- if (typeof entry !== "function") throw new Error(`Entrypoint ${entryName} was not exported.`);
175
- const value = await entry();
194
+ const useBundle = runFlags?.bundle === true || (runFlags?.bundle !== false && canBundleRunInMemory(program));
195
+ if (runFlags?.bundle === true && !canBundleRunInMemory(program)) {
196
+ throw new Error("Cannot use --bundle: module has imports, externals, or non-pure logic (views, routes, workflows, commands).");
197
+ }
198
+ const value = useBundle
199
+ ? await executeBundledEntry(program, entryName)
200
+ : await executeTempModuleRun(program, entryName);
176
201
  if (value !== undefined) console.log(typeof value === "string" ? value : JSON.stringify(value));
177
202
  } catch (error) {
178
203
  console.error(`Runtime error in ${runtimeSourceLocation(program, input, entryName)}: ${error instanceof Error ? error.message : String(error)}`);
@@ -198,8 +223,9 @@ export async function main() {
198
223
  async function runProjectCommand(command: string) {
199
224
  const inputs = await discoverInputs();
200
225
  if (inputs.length === 0) throw new Error(`No Point core files matched ${DEFAULT_PATTERNS.join(", ")}`);
201
- const results = await Promise.all(inputs.map((input) => loadCoreFile(input)));
202
- const graph = createModuleGraph(results);
226
+ const lock = await readPointLock();
227
+ const results = await Promise.all(inputs.map((input) => loadCoreFile(input, lock)));
228
+ const graph = createModuleGraph(results, lock);
203
229
  const orderedResults = orderByDependencies(results, graph);
204
230
 
205
231
  if (command === "fmt-all") {
@@ -298,6 +324,26 @@ async function runProjectCommand(command: string) {
298
324
  return;
299
325
  }
300
326
 
327
+ if (command === "build-py-all") {
328
+ const diagnostics = orderedResults.flatMap((result) =>
329
+ checkPointCore(programWithDependencyDeclarations(result, graph)).map((diagnostic) => ({ ...diagnostic, file: result.input })),
330
+ );
331
+ if (diagnostics.length > 0) {
332
+ console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
333
+ process.exit(1);
334
+ }
335
+ const pureLogicResults = orderedResults.filter((result) => isPureLogicProgram(result.program));
336
+ for (const result of pureLogicResults) {
337
+ const output = pyOutputFor(result.input);
338
+ const outputPath = resolve(process.cwd(), output);
339
+ await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
340
+ await Bun.write(outputPath, emitPointCorePython(programWithTypeScriptImports(result, graph)));
341
+ }
342
+ const skipped = results.length - pureLogicResults.length;
343
+ console.log(`Point core Python build wrote ${pureLogicResults.length} files${skipped ? ` (${skipped} skipped)` : ""}`);
344
+ return;
345
+ }
346
+
301
347
  if (command === "test-all") {
302
348
  const diagnostics = orderedResults.flatMap((result) =>
303
349
  checkPointCore(programWithDependencyDeclarations(result, graph)).map((diagnostic) => ({ ...diagnostic, file: result.input })),
@@ -400,6 +446,26 @@ function pathToFileUrl(path: string): string {
400
446
  return `file://${path.replaceAll("\\", "/")}`;
401
447
  }
402
448
 
449
+ async function executeTempModuleRun(program: PointCoreProgram, entryName: string): Promise<unknown> {
450
+ const runOutput = resolve(tmpdir(), `point-run-${Date.now()}.js`);
451
+ await Bun.write(runOutput, emitPointCoreJavaScript(program));
452
+ const mod = await import(pathToFileUrl(runOutput));
453
+ const entry = mod[entryName];
454
+ if (typeof entry !== "function") throw new Error(`Entrypoint ${entryName} was not exported.`);
455
+ return await entry();
456
+ }
457
+
458
+ function parseCliFlags(args: string[]): { flags: Record<string, boolean | undefined>; positional: string[] } {
459
+ const flags: Record<string, boolean | undefined> = {};
460
+ const positional: string[] = [];
461
+ for (const arg of args) {
462
+ if (arg === "--bundle") flags.bundle = true;
463
+ else if (arg === "--no-bundle") flags.bundle = false;
464
+ else positional.push(arg);
465
+ }
466
+ return { flags, positional };
467
+ }
468
+
403
469
  export function findRunEntryName(program: PointCoreProgram): string | null {
404
470
  const zeroArgFunctions = program.declarations.filter((declaration) => declaration.kind === "function" && declaration.params.length === 0);
405
471
  const preferred =
@@ -409,9 +475,9 @@ export function findRunEntryName(program: PointCoreProgram): string | null {
409
475
  return preferred?.name ?? null;
410
476
  }
411
477
 
412
- async function loadCoreFile(input: string) {
478
+ async function loadCoreFile(input: string, lock: Awaited<ReturnType<typeof readPointLock>>) {
413
479
  const source = await Bun.file(resolve(process.cwd(), input)).text();
414
- return { input, source, program: parsePointSource(source), uses: parseUseDeclarations(source, input) };
480
+ return { input, source, program: parsePointSource(source), uses: parseUseDeclarations(source, input, lock) };
415
481
  }
416
482
 
417
483
  type CoreFile = Awaited<ReturnType<typeof loadCoreFile>>;
@@ -423,15 +489,15 @@ interface UseDeclaration {
423
489
  input: string;
424
490
  }
425
491
 
426
- function parseUseDeclarations(source: string, input: string): UseDeclaration[] {
492
+ function parseUseDeclarations(source: string, input: string, lock: Awaited<ReturnType<typeof readPointLock>>): UseDeclaration[] {
427
493
  return source
428
494
  .split(/\r?\n/)
429
495
  .map((line) => line.trim().match(/^use\s+([A-Za-z][A-Za-z0-9]*(?:\.[A-Za-z][A-Za-z0-9]*)*)(?:\s+from\s+"([^"]+)")?$/))
430
496
  .filter((match): match is RegExpMatchArray => Boolean(match))
431
- .map((match) => ({ moduleName: match[1]!, from: match[2] ?? stdPathFor(match[1]!), input }));
497
+ .map((match) => ({ moduleName: match[1]!, from: match[2] ?? modulePathFromLock(lock, match[1]!), input }));
432
498
  }
433
499
 
434
- function createModuleGraph(results: CoreFile[]): ModuleGraph {
500
+ function createModuleGraph(results: CoreFile[], lock: Awaited<ReturnType<typeof readPointLock>>): ModuleGraph {
435
501
  const byInput = new Map(results.map((result) => [normalizeInput(result.input), result]));
436
502
  const graph: ModuleGraph = new Map();
437
503
  for (const result of results) {
@@ -490,14 +556,12 @@ function publicDeclarations(program: PointCoreProgram): Array<Extract<PointCoreD
490
556
  }
491
557
 
492
558
  function resolveDependencyInput(input: string, from: string): string {
493
- if (from.startsWith("std/")) return from;
494
- const base = dirname(resolve(process.cwd(), input));
495
- return resolve(base, from).replace(resolve(process.cwd()), "").replace(/^[/\\]/, "");
496
- }
497
-
498
- function stdPathFor(moduleName: string): string {
499
- if (!moduleName.startsWith("std.")) throw new Error(`Use declarations without from must target std modules: ${moduleName}`);
500
- return `${moduleName.replace(/^std\./, "std/").replaceAll(".", "/")}.point`;
559
+ const normalized = from.replaceAll("\\", "/");
560
+ if (normalized.startsWith("./") || normalized.startsWith("../")) {
561
+ const base = dirname(resolve(process.cwd(), input));
562
+ return resolve(base, from).replace(resolve(process.cwd()), "").replace(/^[/\\]/, "");
563
+ }
564
+ return normalized;
501
565
  }
502
566
 
503
567
  function normalizeInput(input: string): string {
@@ -27,6 +27,7 @@ export type PointCoreSymbolKind =
27
27
  | "action"
28
28
  | "policy"
29
29
  | "view"
30
+ | "page"
30
31
  | "route"
31
32
  | "workflow"
32
33
  | "command";
@@ -325,6 +326,7 @@ function relatedRefsFor(symbol: PointCoreSymbol, index: PointCoreIndex): string[
325
326
  symbol.kind === "action" ||
326
327
  symbol.kind === "policy" ||
327
328
  symbol.kind === "view" ||
329
+ symbol.kind === "page" ||
328
330
  symbol.kind === "route" ||
329
331
  symbol.kind === "workflow" ||
330
332
  symbol.kind === "command"
@@ -351,6 +353,7 @@ function summaryFor(symbol: PointCoreSymbol): string {
351
353
  if (symbol.kind === "action") return `Semantic action ${symbol.name} returns ${symbol.type}; effects: ${(symbol.effects ?? []).join(", ") || "none"}.`;
352
354
  if (symbol.kind === "policy") return `Semantic policy ${symbol.name} returns ${symbol.type}.`;
353
355
  if (symbol.kind === "view") return `Semantic view ${symbol.name} returns React JSX.`;
356
+ if (symbol.kind === "page") return `Semantic page ${symbol.name} returns a Next.js page shell as React JSX.`;
354
357
  if (symbol.kind === "route") return `Semantic route ${symbol.name} returns ${symbol.type}.`;
355
358
  if (symbol.kind === "workflow") return `Semantic workflow ${symbol.name} returns ${symbol.type}.`;
356
359
  if (symbol.kind === "command") return `Semantic command ${symbol.name} returns ${symbol.type}.`;
@@ -52,9 +52,13 @@ function emitDeclaration(declaration: PointCoreDeclaration): string[] {
52
52
 
53
53
  function emitFunction(declaration: PointCoreFunctionDeclaration): string[] {
54
54
  const asyncPrefix = declaration.semantic?.kind === "action" || declaration.semantic?.kind === "workflow" || declaration.semantic?.kind === "command" ? "async " : "";
55
+ const bodyLines =
56
+ declaration.semantic?.kind === "page" && declaration.semantic.pageLayout
57
+ ? [`return ${emitExpression(declaration.semantic.pageLayout.main)};`]
58
+ : declaration.body.flatMap((statement) => emitStatement(statement, declaration.semantic?.kind));
55
59
  return [
56
60
  `export ${asyncPrefix}function ${declaration.name}(${declaration.params.map(emitParam).join(", ")}) {`,
57
- ...indentLines(declaration.body.flatMap((statement) => emitStatement(statement, declaration.semantic?.kind))),
61
+ ...indentLines(bodyLines),
58
62
  "}",
59
63
  ];
60
64
  }
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  PointCoreDeclaration,
3
3
  PointCoreExpression,
4
+ PointCoreExternalDeclaration,
4
5
  PointCoreFunctionDeclaration,
5
6
  PointCoreParameter,
6
7
  PointCorePrimitiveType,
@@ -16,7 +17,12 @@ const BINARY_OPERATORS: Record<string, string> = {
16
17
  or: "or",
17
18
  };
18
19
 
19
- const UNSUPPORTED_SEMANTIC_KINDS = new Set(["view", "route", "action", "workflow", "command"]);
20
+ const UNSUPPORTED_SEMANTIC_KINDS = new Set(["view", "page", "route", "workflow", "command"]);
21
+
22
+ /** True when every declaration is emit-able as Python (no views, routes, workflows, or commands). */
23
+ export function isPureLogicProgram(program: PointCoreProgram): boolean {
24
+ return !program.declarations.some((declaration) => declaration.semantic && UNSUPPORTED_SEMANTIC_KINDS.has(declaration.semantic.kind));
25
+ }
20
26
 
21
27
  /** Emit Python from a core AST program (pure logic modules). Production path: parsePointSource → check → emit. */
22
28
  export function emitPointCorePython(program: PointCoreProgram): string {
@@ -42,11 +48,7 @@ function emitDeclaration(declaration: PointCoreDeclaration): string[] {
42
48
  const moduleName = declaration.from.replace(/^\.\//, "").replace(/-/g, "_");
43
49
  return [`from ${toPythonModuleName(moduleName)} import ${declaration.names.join(", ")}`];
44
50
  }
45
- if (declaration.kind === "external") {
46
- const moduleName = declaration.from.replace(/^\.\//, "").replace(/-/g, "_");
47
- const imported = declaration.importName ?? declaration.name;
48
- return [`from ${toPythonModuleName(moduleName)} import ${imported} as ${declaration.name}`];
49
- }
51
+ if (declaration.kind === "external") return emitExternal(declaration);
50
52
  if (declaration.kind === "type") return emitType(declaration);
51
53
  if (declaration.kind === "value") return [emitValue(declaration)];
52
54
  if (declaration.semantic && UNSUPPORTED_SEMANTIC_KINDS.has(declaration.semantic.kind)) {
@@ -62,6 +64,19 @@ function emitType(declaration: PointCoreTypeDeclaration): string[] {
62
64
  ];
63
65
  }
64
66
 
67
+ function emitExternal(declaration: PointCoreExternalDeclaration): string[] {
68
+ if (declaration.from === "node:fs" && declaration.importName === "readFileSync") {
69
+ return [
70
+ `def ${declaration.name}(${declaration.params.map(emitParam).join(", ")}) -> ${emitTypeExpression(declaration.returnType)}:`,
71
+ " from pathlib import Path",
72
+ " return Path(path).read_text()",
73
+ ];
74
+ }
75
+ const moduleName = declaration.from.replace(/^\.\//, "").replace(/-/g, "_");
76
+ const imported = declaration.importName ?? declaration.name;
77
+ return [`from ${toPythonModuleName(moduleName)} import ${imported} as ${declaration.name}`];
78
+ }
79
+
65
80
  function emitFunction(declaration: PointCoreFunctionDeclaration): string[] {
66
81
  const asyncPrefix = declaration.semantic?.kind === "action" || declaration.semantic?.kind === "workflow" || declaration.semantic?.kind === "command" ? "async " : "";
67
82
  return [
@@ -79,7 +94,7 @@ function emitReturnType(declaration: PointCoreFunctionDeclaration): string {
79
94
 
80
95
  function emitStatement(statement: PointCoreStatement, semanticKind?: string): string[] {
81
96
  if (statement.kind === "return") {
82
- if (semanticKind === "view") return ["# Point: view return values are not supported in Python emit yet", "return \"\""];
97
+ if (semanticKind === "view" || semanticKind === "page") return ["# Point: view/page return values are not supported in Python emit yet", "return \"\""];
83
98
  return [statement.value ? `return ${emitExpression(statement.value)}` : "return"];
84
99
  }
85
100
  if (statement.kind === "value") return [emitValue(statement)];
@@ -9,6 +9,9 @@ import type {
9
9
  PointCoreTypeDeclaration,
10
10
  PointCoreTypeExpression,
11
11
  PointCoreValueDeclaration,
12
+ PointSemanticPageLayout,
13
+ PointSemanticViewCheckboxBinding,
14
+ PointSemanticViewControls,
12
15
  } from "./ast.ts";
13
16
 
14
17
  const BINARY_OPERATORS: Record<string, string> = {
@@ -54,18 +57,86 @@ function emitFunction(declaration: PointCoreFunctionDeclaration): string[] {
54
57
  const returnType =
55
58
  declaration.semantic?.kind === "action" || declaration.semantic?.kind === "workflow" || declaration.semantic?.kind === "command"
56
59
  ? `Promise<${emitTypeExpression(declaration.returnType)}>`
57
- : declaration.semantic?.kind === "view"
60
+ : declaration.semantic?.kind === "view" || declaration.semantic?.kind === "page"
58
61
  ? "JSX.Element"
59
62
  : declaration.semantic?.kind === "route"
60
63
  ? "Response | string"
61
64
  : emitTypeExpression(declaration.returnType);
65
+ const viewControls = declaration.semantic?.viewControls;
66
+ const bodyLines =
67
+ declaration.semantic?.kind === "page" && declaration.semantic.pageLayout
68
+ ? emitPageBody(declaration.semantic.pageLayout)
69
+ : declaration.semantic?.kind === "view" && viewControls
70
+ ? emitControlledViewBody(declaration.body, viewControls)
71
+ : declaration.body.flatMap((statement) => emitStatement(statement, declaration.semantic?.kind));
62
72
  return [
63
73
  `export ${asyncPrefix}function ${declaration.name}(${declaration.params.map(emitParam).join(", ")}): ${returnType} {`,
64
- ...indentLines(declaration.body.flatMap((statement) => emitStatement(statement, declaration.semantic?.kind))),
74
+ ...indentLines(bodyLines),
65
75
  "}",
66
76
  ];
67
77
  }
68
78
 
79
+ function emitControlledViewBody(body: PointCoreStatement[], controls: PointSemanticViewControls): string[] {
80
+ const checkboxLines = controls.checkboxes.map((binding) => emitCheckboxControl(binding, controls.changeCallback));
81
+ const contentJsx = emitViewContentExpression(body);
82
+ return ["return (", " <>", ...indentLines(checkboxLines), ` ${contentJsx}`, " </>", ");"];
83
+ }
84
+
85
+ function emitCheckboxControl(binding: PointSemanticViewCheckboxBinding, changeCallback: string): string {
86
+ const checked = emitExpression(binding.target);
87
+ return `<label><input type="checkbox" checked={${checked}} onChange={(e) => ${changeCallback}({ ...${binding.recordParam}, ${binding.fieldName}: e.target.checked })} />${escapeJsxText(binding.label)}</label>`;
88
+ }
89
+
90
+ function emitViewContentExpression(body: PointCoreStatement[]): string {
91
+ let expression = "null";
92
+ for (let index = body.length - 1; index >= 0; index -= 1) {
93
+ const statement = body[index];
94
+ if (!statement) continue;
95
+ if (statement.kind === "return" && statement.value) {
96
+ expression = emitViewRenderFragment(statement.value);
97
+ continue;
98
+ }
99
+ if (statement.kind === "if" && statement.thenBody.length === 1 && statement.thenBody[0]?.kind === "return" && statement.thenBody[0].value) {
100
+ const thenValue = emitViewRenderFragment(statement.thenBody[0].value);
101
+ expression = `${emitCondition(statement.condition)} ? ${thenValue} : ${expression}`;
102
+ }
103
+ }
104
+ return `{${expression}}`;
105
+ }
106
+
107
+ function emitViewRenderFragment(expression: PointCoreExpression): string {
108
+ if (expression.kind === "literal" && typeof expression.value === "string") {
109
+ return `<>${escapeJsxText(expression.value)}</>`;
110
+ }
111
+ return `<>{${emitExpression(expression)}}</>`;
112
+ }
113
+
114
+ function emitPageBody(layout: PointSemanticPageLayout): string[] {
115
+ const title = emitJsxChild(layout.title);
116
+ const description = layout.description ? `\n <p className="point-page-description">${emitJsxChild(layout.description)}</p>` : "";
117
+ const main = emitJsxChild(layout.main, true);
118
+ return [
119
+ `return (`,
120
+ ` <main className="point-page">`,
121
+ ` <header className="point-page-header">`,
122
+ ` <h1>${title}</h1>${description}`,
123
+ ` </header>`,
124
+ ` <section className="point-page-main">${main}</section>`,
125
+ ` </main>`,
126
+ `);`,
127
+ ];
128
+ }
129
+
130
+ function emitJsxChild(expression: PointCoreExpression, allowComponent = false): string {
131
+ if (expression.kind === "literal" && typeof expression.value === "string") {
132
+ return escapeJsxText(expression.value);
133
+ }
134
+ if (allowComponent) {
135
+ return `{${emitExpression(expression)}}`;
136
+ }
137
+ return `{${emitExpression(expression)}}`;
138
+ }
139
+
69
140
  function emitStatement(statement: PointCoreStatement, semanticKind?: string): string[] {
70
141
  if (statement.kind === "return") {
71
142
  if (semanticKind === "view" && statement.value) {
@@ -114,10 +185,20 @@ function emitValue(declaration: PointCoreValueDeclaration, exported: boolean): s
114
185
  }
115
186
 
116
187
  function emitParam(param: PointCoreParameter): string {
117
- return `${param.name}: ${emitTypeExpression(param.type)}`;
188
+ return `${param.name}: ${emitParamType(param.type)}`;
189
+ }
190
+
191
+ function emitParamType(type: PointCoreTypeExpression): string {
192
+ if (type.name === "Handler" && type.args.length === 1) {
193
+ return `(value: ${emitTypeExpression(type.args[0]!)}) => void`;
194
+ }
195
+ return emitTypeExpression(type);
118
196
  }
119
197
 
120
198
  function emitTypeExpression(type: PointCoreTypeExpression): string {
199
+ if (type.name === "Handler" && type.args.length === 1) {
200
+ return `(value: ${emitTypeExpression(type.args[0]!)}) => void`;
201
+ }
121
202
  if (type.name === "List") return `Array<${type.args[0] ? emitTypeExpression(type.args[0]) : "unknown"}>`;
122
203
  if (type.name === "Maybe") return `${type.args[0] ? emitTypeExpression(type.args[0]) : "unknown"} | null`;
123
204
  if (type.name === "Or") return type.args.map(emitTypeExpression).join(" | ");
package/src/core/index.ts CHANGED
@@ -7,6 +7,7 @@ export * from "./emit-typescript.ts";
7
7
  export * from "./emit-javascript.ts";
8
8
  export * from "./emit-python.ts";
9
9
  export * from "./incremental.ts";
10
+ export * from "./packages.ts";
10
11
  export * from "./serialize.ts";
11
12
  export * from "../semantic/index.ts";
12
13
  export * from "./format.ts";