@hatchingpoint/point 0.0.13 → 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.13",
3
+ "version": "0.0.14",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Point language compiler and CLI.",
@@ -26,7 +26,11 @@
26
26
  "./cli": "./src/cli.ts",
27
27
  "./core": "./src/core/index.ts",
28
28
  "./std/json": "./src/std/json.ts",
29
- "./std/http": "./src/std/http.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"
30
34
  },
31
35
  "publishConfig": {
32
36
  "access": "public",
package/src/core/cli.ts CHANGED
@@ -7,6 +7,7 @@ import { createSemanticIndex, explainSemanticRef, mapPublicDiagnostics } from ".
7
7
  import { emitPointCoreTypeScript } from "./emit-typescript.ts";
8
8
  import { emitPointCoreJavaScript } from "./emit-javascript.ts";
9
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";
@@ -23,7 +24,17 @@ const DEFAULT_PATTERNS = ["examples/**/*.point", "std/**/*.point", "compiler/**/
23
24
  const GENERATED_DIR = "generated";
24
25
 
25
26
  export async function main() {
26
- 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
+ }
27
38
  if (command.endsWith("-all")) {
28
39
  await runProjectCommand(command);
29
40
  return;
@@ -176,16 +187,17 @@ export async function main() {
176
187
  console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
177
188
  process.exit(1);
178
189
  }
179
- const runOutput = resolve(tmpdir(), `point-run-${Date.now()}.js`);
180
- await Bun.write(runOutput, emitPointCoreJavaScript(program));
181
190
  let entryName: string | null = null;
182
191
  try {
183
- const mod = await import(pathToFileUrl(runOutput));
184
192
  entryName = findRunEntryName(program);
185
193
  if (!entryName) throw new Error("No zero-argument entrypoint found. Define an action or calculation with no inputs.");
186
- const entry = mod[entryName];
187
- if (typeof entry !== "function") throw new Error(`Entrypoint ${entryName} was not exported.`);
188
- 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);
189
201
  if (value !== undefined) console.log(typeof value === "string" ? value : JSON.stringify(value));
190
202
  } catch (error) {
191
203
  console.error(`Runtime error in ${runtimeSourceLocation(program, input, entryName)}: ${error instanceof Error ? error.message : String(error)}`);
@@ -434,6 +446,26 @@ function pathToFileUrl(path: string): string {
434
446
  return `file://${path.replaceAll("\\", "/")}`;
435
447
  }
436
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
+
437
469
  export function findRunEntryName(program: PointCoreProgram): string | null {
438
470
  const zeroArgFunctions = program.declarations.filter((declaration) => declaration.kind === "function" && declaration.params.length === 0);
439
471
  const preferred =
@@ -524,9 +556,12 @@ function publicDeclarations(program: PointCoreProgram): Array<Extract<PointCoreD
524
556
  }
525
557
 
526
558
  function resolveDependencyInput(input: string, from: string): string {
527
- if (from.startsWith("std/")) return from;
528
- const base = dirname(resolve(process.cwd(), input));
529
- return resolve(base, from).replace(resolve(process.cwd()), "").replace(/^[/\\]/, "");
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;
530
565
  }
531
566
 
532
567
  function normalizeInput(input: string): string {
@@ -1,4 +1,5 @@
1
- import { existsSync } from "node:fs";
1
+ import { createRequire } from "node:module";
2
+ import { existsSync, readdirSync } from "node:fs";
2
3
  import { join, relative, resolve } from "node:path";
3
4
 
4
5
  export const POINT_MANIFEST = "point.json";
@@ -29,15 +30,36 @@ export interface ParsedDependencySpec {
29
30
  locator: string;
30
31
  }
31
32
 
33
+ export interface ParsedNpmLocator {
34
+ name: string;
35
+ version?: string;
36
+ }
37
+
38
+ export function parseNpmLocator(locator: string): ParsedNpmLocator {
39
+ if (locator.startsWith("@")) {
40
+ const slash = locator.indexOf("/");
41
+ if (slash < 0) {
42
+ throw new Error(`Invalid npm package name "${locator}". Scoped packages use @scope/name.`);
43
+ }
44
+ const rest = locator.slice(slash + 1);
45
+ const versionAt = rest.indexOf("@");
46
+ if (versionAt >= 0) {
47
+ return { name: `${locator.slice(0, slash + 1 + versionAt)}`, version: rest.slice(versionAt + 1) };
48
+ }
49
+ return { name: locator };
50
+ }
51
+ const versionAt = locator.lastIndexOf("@");
52
+ if (versionAt > 0) {
53
+ return { name: locator.slice(0, versionAt), version: locator.slice(versionAt + 1) };
54
+ }
55
+ return { name: locator };
56
+ }
57
+
32
58
  export function parseDependencySpec(spec: string): ParsedDependencySpec {
33
59
  if (spec.startsWith("workspace:")) return { kind: "workspace", locator: spec.slice("workspace:".length) };
34
60
  if (spec.startsWith("file:")) return { kind: "file", locator: spec.slice("file:".length) };
35
61
  if (spec.startsWith("npm:")) return { kind: "npm", locator: spec.slice("npm:".length) };
36
- throw new Error(`Invalid dependency spec "${spec}". Use workspace:<path>, file:<path>, or npm:<package> (npm not yet supported).`);
37
- }
38
-
39
- export function npmDependencyNotSupportedMessage(spec: string): string {
40
- return `npm: registry dependencies are not supported yet (${spec}). Use workspace:<path> or file:<path> for local Point packages.`;
62
+ throw new Error(`Invalid dependency spec "${spec}". Use workspace:<path>, file:<path>, or npm:<package>[@version].`);
41
63
  }
42
64
 
43
65
  export async function readPointManifest(cwd = process.cwd()): Promise<PointManifest> {
@@ -74,9 +96,71 @@ export function normalizePackagePath(cwd: string, rawPath: string): string {
74
96
  return relative(cwd, absolute).split("\\").join("/") || ".";
75
97
  }
76
98
 
77
- export function resolveDependencySpec(spec: string, cwd = process.cwd()): PointLockPackage {
99
+ export function locatePointPackageRoot(pkgDir: string): string {
100
+ if (existsSync(join(pkgDir, POINT_MANIFEST))) return pkgDir;
101
+ const srcDir = join(pkgDir, "src");
102
+ if (existsSync(srcDir) && readdirSync(srcDir).some((name) => name.endsWith(".point"))) return pkgDir;
103
+ if (readdirSync(pkgDir).some((name) => name.endsWith(".point"))) return pkgDir;
104
+ throw new Error(`Point package at ${pkgDir} has no ${POINT_MANIFEST} or src/*.point modules`);
105
+ }
106
+
107
+ export function resolveNpmPackagePath(cwd: string, packageName: string): string | null {
108
+ const direct = join(cwd, "node_modules", ...packageName.split("/"));
109
+ if (existsSync(join(direct, "package.json"))) return direct;
110
+ try {
111
+ const req = createRequire(join(cwd, "package.json"));
112
+ const pkgJson = req.resolve(`${packageName}/package.json`);
113
+ return resolve(pkgJson, "..");
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+
119
+ async function readInstalledNpmVersion(pkgDir: string): Promise<string | null> {
120
+ const pkgJsonPath = join(pkgDir, "package.json");
121
+ if (!existsSync(pkgJsonPath)) return null;
122
+ const pkg = (await Bun.file(pkgJsonPath).json()) as { version?: string };
123
+ return pkg.version ?? null;
124
+ }
125
+
126
+ export async function ensureNpmPackage(cwd: string, packageName: string, version?: string): Promise<string> {
127
+ const installed = resolveNpmPackagePath(cwd, packageName);
128
+ if (installed) {
129
+ const installedVersion = await readInstalledNpmVersion(installed);
130
+ if (!version || installedVersion === version) return installed;
131
+ }
132
+ const installSpec = version ? `${packageName}@${version}` : packageName;
133
+ const result = await Bun.$`npm install ${installSpec} --prefix ${cwd} --no-save --no-package-lock`.quiet().nothrow();
134
+ if (result.exitCode !== 0) {
135
+ const detail = result.stderr.toString().trim() || result.stdout.toString().trim();
136
+ throw new Error(`npm install failed for ${installSpec}: ${detail || "unknown error"}`);
137
+ }
138
+ const resolved = resolveNpmPackagePath(cwd, packageName);
139
+ if (!resolved) {
140
+ throw new Error(`npm package "${packageName}" was not found under node_modules/ after install`);
141
+ }
142
+ if (version) {
143
+ const installedVersion = await readInstalledNpmVersion(resolved);
144
+ if (installedVersion && installedVersion !== version) {
145
+ throw new Error(`npm package "${packageName}" resolved to ${installedVersion}, expected ${version}`);
146
+ }
147
+ }
148
+ return resolved;
149
+ }
150
+
151
+ export async function resolveNpmDependencySpec(spec: string, cwd = process.cwd()): Promise<PointLockPackage> {
78
152
  const parsed = parseDependencySpec(spec);
79
- if (parsed.kind === "npm") throw new Error(npmDependencyNotSupportedMessage(spec));
153
+ if (parsed.kind !== "npm") throw new Error(`Expected npm: spec, got ${spec}`);
154
+ const { name, version } = parseNpmLocator(parsed.locator);
155
+ const pkgDir = await ensureNpmPackage(cwd, name, version);
156
+ locatePointPackageRoot(pkgDir);
157
+ const npmVersion = (await readInstalledNpmVersion(pkgDir)) ?? version ?? "npm";
158
+ return { version: npmVersion, path: normalizePackagePath(cwd, pkgDir) };
159
+ }
160
+
161
+ export async function resolveDependencySpec(spec: string, cwd = process.cwd()): Promise<PointLockPackage> {
162
+ const parsed = parseDependencySpec(spec);
163
+ if (parsed.kind === "npm") return resolveNpmDependencySpec(spec, cwd);
80
164
  const path = normalizePackagePath(cwd, parsed.locator);
81
165
  return { version: parsed.kind, path };
82
166
  }
@@ -89,7 +173,7 @@ export async function resolveLockFromManifest(manifest: PointManifest, cwd = pro
89
173
  },
90
174
  };
91
175
  for (const [name, spec] of Object.entries(manifest.dependencies ?? {})) {
92
- packages[name] = resolveDependencySpec(spec, cwd);
176
+ packages[name] = await resolveDependencySpec(spec, cwd);
93
177
  const entry = packages[name];
94
178
  if (!entry.path) continue;
95
179
  const nestedManifestPath = join(cwd, entry.path, POINT_MANIFEST);
@@ -123,7 +207,12 @@ export function packageRootFromLock(lock: PointLock | null, packageName: string)
123
207
  return null;
124
208
  }
125
209
 
126
- export function modulePathFromLock(lock: PointLock | null, moduleName: string): string {
210
+ function modulePathCandidates(root: string, modulePath: string): string[] {
211
+ const segments = modulePath.replaceAll(".", "/");
212
+ return [`${root}/${segments}.point`, `${root}/src/${segments}.point`];
213
+ }
214
+
215
+ export function modulePathFromLock(lock: PointLock | null, moduleName: string, cwd = process.cwd()): string {
127
216
  const dot = moduleName.indexOf(".");
128
217
  if (dot < 0) {
129
218
  throw new Error(`Use declarations without from must target package modules: ${moduleName}`);
@@ -134,5 +223,8 @@ export function modulePathFromLock(lock: PointLock | null, moduleName: string):
134
223
  if (!root) {
135
224
  throw new Error(`Unknown package "${packageName}" in ${moduleName}. Add it with: point add ${packageName} <spec>`);
136
225
  }
137
- return `${root}/${modulePath.replaceAll(".", "/")}.point`;
226
+ for (const candidate of modulePathCandidates(root, modulePath)) {
227
+ if (existsSync(join(cwd, candidate))) return candidate;
228
+ }
229
+ return modulePathCandidates(root, modulePath)[0]!;
138
230
  }
@@ -0,0 +1,93 @@
1
+ import type { PointCoreProgram } from "./ast.ts";
2
+ import { emitPointCoreJavaScript } from "./emit-javascript.ts";
3
+ import { isPureLogicProgram } from "./emit-python.ts";
4
+
5
+ /** Pure logic with no imports or externals can run in-memory without a temp module file. */
6
+ export function canBundleRunInMemory(program: PointCoreProgram): boolean {
7
+ if (!isPureLogicProgram(program)) return false;
8
+ if (program.declarations.some((declaration) => declaration.kind === "import" || declaration.kind === "external")) return false;
9
+ if (program.semanticSource?.uses?.length) return false;
10
+ const emitted = emitPointCoreJavaScript(program);
11
+ if (/^\s*import\s/m.test(emitted)) return false;
12
+ return !hasUnresolvedCallTargets(emitted);
13
+ }
14
+
15
+ const BUILTIN_CALL_TARGETS = new Set([
16
+ "Array",
17
+ "Boolean",
18
+ "JSON",
19
+ "Math",
20
+ "Number",
21
+ "Object",
22
+ "Promise",
23
+ "String",
24
+ "console",
25
+ "fetch",
26
+ "parseInt",
27
+ "parseFloat",
28
+ ]);
29
+
30
+ const NON_CALL_KEYWORDS = new Set([
31
+ "async",
32
+ "await",
33
+ "catch",
34
+ "delete",
35
+ "export",
36
+ "for",
37
+ "function",
38
+ "if",
39
+ "import",
40
+ "instanceof",
41
+ "new",
42
+ "return",
43
+ "switch",
44
+ "throw",
45
+ "typeof",
46
+ "void",
47
+ "while",
48
+ "with",
49
+ "yield",
50
+ ]);
51
+
52
+ function hasUnresolvedCallTargets(source: string): boolean {
53
+ const defined = new Set<string>();
54
+ for (const match of source.matchAll(/(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/g)) defined.add(match[1]!);
55
+ for (const match of source.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(/g)) {
56
+ const name = match[1]!;
57
+ if (NON_CALL_KEYWORDS.has(name) || BUILTIN_CALL_TARGETS.has(name) || defined.has(name)) continue;
58
+ return true;
59
+ }
60
+ return false;
61
+ }
62
+
63
+ export function bundleJavaScriptForEval(source: string): { body: string; exports: string[] } {
64
+ const exports: string[] = [];
65
+ const bodyLines: string[] = [];
66
+ for (const line of source.split(/\r?\n/)) {
67
+ if (/^\s*import\s/.test(line)) continue;
68
+ const fnMatch = line.match(/^export\s+(async\s+)?function\s+([A-Za-z_$][\w$]*)/);
69
+ if (fnMatch) {
70
+ exports.push(fnMatch[2]!);
71
+ bodyLines.push(line.replace(/^export\s+/, ""));
72
+ continue;
73
+ }
74
+ const bindingMatch = line.match(/^export\s+(const|let|var)\s+([A-Za-z_$][\w$]*)/);
75
+ if (bindingMatch) {
76
+ exports.push(bindingMatch[2]!);
77
+ bodyLines.push(line.replace(/^export\s+/, ""));
78
+ continue;
79
+ }
80
+ bodyLines.push(line);
81
+ }
82
+ return { body: bodyLines.join("\n"), exports };
83
+ }
84
+
85
+ export async function executeBundledEntry(program: PointCoreProgram, entryName: string): Promise<unknown> {
86
+ const { body, exports } = bundleJavaScriptForEval(emitPointCoreJavaScript(program));
87
+ if (!exports.includes(entryName)) throw new Error(`Entrypoint ${entryName} was not emitted.`);
88
+ const factory = new Function(`"use strict";\n${body}\nreturn { ${exports.join(", ")} };`);
89
+ const mod = factory() as Record<string, unknown>;
90
+ const entry = mod[entryName];
91
+ if (typeof entry !== "function") throw new Error(`Entrypoint ${entryName} was not a function.`);
92
+ return await (entry as () => unknown)();
93
+ }
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,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
+ }