@hypen-space/core 0.3.12 → 0.4.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.
Files changed (47) hide show
  1. package/dist/app.js +7 -3
  2. package/dist/app.js.map +3 -3
  3. package/dist/components/builtin.js +7 -3
  4. package/dist/components/builtin.js.map +3 -3
  5. package/dist/discovery.js +8 -7
  6. package/dist/discovery.js.map +4 -4
  7. package/dist/engine.browser.js.map +2 -2
  8. package/dist/engine.js +3 -7
  9. package/dist/engine.js.map +3 -3
  10. package/dist/index.browser.js +242 -371
  11. package/dist/index.browser.js.map +8 -9
  12. package/dist/index.js +546 -656
  13. package/dist/index.js.map +12 -13
  14. package/dist/remote/client.js.map +1 -1
  15. package/dist/remote/index.js +336 -336
  16. package/dist/remote/index.js.map +8 -8
  17. package/dist/remote/server.js +219 -219
  18. package/dist/remote/server.js.map +7 -7
  19. package/dist/renderer.js.map +1 -1
  20. package/dist/resolver.js +31 -4
  21. package/dist/resolver.js.map +3 -3
  22. package/package.json +13 -1
  23. package/src/app.ts +14 -2
  24. package/src/discovery.ts +15 -12
  25. package/src/engine.browser.ts +4 -32
  26. package/src/engine.ts +8 -44
  27. package/src/index.browser.ts +6 -4
  28. package/src/index.ts +22 -30
  29. package/src/managed-router.ts +195 -0
  30. package/src/module-registry.ts +76 -0
  31. package/src/remote/client.ts +1 -1
  32. package/src/remote/index.ts +2 -2
  33. package/src/remote/server.ts +1 -1
  34. package/src/remote/types.ts +1 -1
  35. package/src/renderer.ts +6 -2
  36. package/src/resolver.ts +56 -9
  37. package/src/types.ts +38 -0
  38. package/wasm-browser/README.md +7 -1
  39. package/wasm-browser/hypen_engine.d.ts +1 -0
  40. package/wasm-browser/hypen_engine.js +1 -0
  41. package/wasm-browser/hypen_engine_bg.wasm +0 -0
  42. package/wasm-browser/package.json +1 -1
  43. package/wasm-node/README.md +7 -1
  44. package/wasm-node/hypen_engine.d.ts +1 -0
  45. package/wasm-node/hypen_engine.js +1 -0
  46. package/wasm-node/hypen_engine_bg.wasm +0 -0
  47. package/wasm-node/package.json +1 -1
package/dist/resolver.js CHANGED
@@ -31,14 +31,32 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
31
31
  class ComponentResolver {
32
32
  cache = new Map;
33
33
  options;
34
+ moduleRegistry;
34
35
  constructor(options = {}) {
35
36
  this.options = {
36
37
  baseDir: options.baseDir || process.cwd(),
37
38
  cache: options.cache ?? true,
38
- customFetch: options.customFetch || this.defaultFetch.bind(this)
39
+ customFetch: options.customFetch || this.defaultFetch.bind(this),
40
+ moduleRegistry: options.moduleRegistry
39
41
  };
42
+ this.moduleRegistry = options.moduleRegistry;
40
43
  }
41
44
  async resolve(importStmt) {
45
+ if (this.moduleRegistry) {
46
+ const names = importStmt.clause.type === "named" ? importStmt.clause.names : [importStmt.clause.name];
47
+ const allFound = names.every((name) => this.moduleRegistry.has(name));
48
+ if (allFound) {
49
+ const result = {};
50
+ for (const name of names) {
51
+ const def = this.moduleRegistry.get(name);
52
+ result[name] = {
53
+ module: def,
54
+ template: def?.template || ""
55
+ };
56
+ }
57
+ return result;
58
+ }
59
+ }
42
60
  const sourcePath = this.getSourcePath(importStmt.source);
43
61
  if (this.options.cache && this.cache.has(sourcePath)) {
44
62
  const cached = this.cache.get(sourcePath);
@@ -56,8 +74,17 @@ class ComponentResolver {
56
74
  return this.extractComponents(importStmt.clause, component);
57
75
  }
58
76
  async resolveLocal(path) {
59
- throw new Error(`Dynamic local component resolution not yet implemented: ${path}
60
- ` + `Please use the build-components.ts script to generate component imports.`);
77
+ const { resolve, join } = await import("path");
78
+ const { readFile } = await import("fs/promises");
79
+ const basePath = resolve(this.options.baseDir, path);
80
+ const hypenPath = basePath.endsWith(".hypen") ? basePath : `${basePath}.hypen`;
81
+ const template = await readFile(hypenPath, "utf-8");
82
+ const modulePath = hypenPath.replace(/\.hypen$/, ".ts");
83
+ let module = {};
84
+ try {
85
+ module = await import(modulePath);
86
+ } catch {}
87
+ return { module, template };
61
88
  }
62
89
  async resolveUrl(url) {
63
90
  try {
@@ -124,4 +151,4 @@ export {
124
151
  ComponentResolver
125
152
  };
126
153
 
127
- //# debugId=708BFB387356D58864756E2164756E21
154
+ //# debugId=20EC11627252563C64756E2164756E21
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/resolver.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * Component Resolver\n * Resolves component imports from local paths or web URLs\n */\n\nexport interface ImportStatement {\n clause: ImportClause;\n source: ImportSource;\n}\n\nexport type ImportClause =\n | { type: \"named\"; names: string[] }\n | { type: \"default\"; name: string };\n\nexport type ImportSource =\n | { type: \"local\"; path: string }\n | { type: \"url\"; url: string };\n\nexport interface ComponentDefinition {\n module: any;\n template: string;\n}\n\nexport interface ResolverOptions {\n /**\n * Base directory for resolving relative local paths\n * Defaults to current working directory\n */\n baseDir?: string;\n\n /**\n * Cache resolved components to avoid re-fetching\n * Defaults to true\n */\n cache?: boolean;\n\n /**\n * Custom fetch function for URL imports\n * Useful for adding authentication, custom headers, etc.\n */\n customFetch?: (url: string) => Promise<string>;\n}\n\n/**\n * Component Resolver\n * Resolves and loads components from local files or remote URLs\n */\nexport class ComponentResolver {\n private cache = new Map<string, ComponentDefinition>();\n private options: Required<ResolverOptions>;\n\n constructor(options: ResolverOptions = {}) {\n this.options = {\n baseDir: options.baseDir || process.cwd(),\n cache: options.cache ?? true,\n customFetch: options.customFetch || this.defaultFetch.bind(this),\n };\n }\n\n /**\n * Resolve a component from an import statement\n */\n async resolve(\n importStmt: ImportStatement\n ): Promise<Record<string, ComponentDefinition>> {\n const sourcePath = this.getSourcePath(importStmt.source);\n\n // Check cache first\n if (this.options.cache && this.cache.has(sourcePath)) {\n const cached = this.cache.get(sourcePath)!;\n return this.extractComponents(importStmt.clause, cached);\n }\n\n // Load the component\n let component: ComponentDefinition;\n if (importStmt.source.type === \"local\") {\n component = await this.resolveLocal(importStmt.source.path);\n } else {\n component = await this.resolveUrl(importStmt.source.url);\n }\n\n // Cache it\n if (this.options.cache) {\n this.cache.set(sourcePath, component);\n }\n\n return this.extractComponents(importStmt.clause, component);\n }\n\n /**\n * Resolve a component from a local file path\n */\n private async resolveLocal(path: string): Promise<ComponentDefinition> {\n // For local paths, we expect the build system to have already\n // processed them into the generated components file\n // This is a fallback for dynamic resolution\n throw new Error(\n `Dynamic local component resolution not yet implemented: ${path}\\n` +\n `Please use the build-components.ts script to generate component imports.`\n );\n }\n\n /**\n * Resolve a component from a URL\n */\n private async resolveUrl(url: string): Promise<ComponentDefinition> {\n try {\n const response = await this.options.customFetch(url);\n const data = JSON.parse(response);\n\n // Expected format: { module: {...}, template: \"...\" }\n if (!data.module || !data.template) {\n throw new Error(\n `Invalid component format from ${url}. Expected { module, template }`\n );\n }\n\n return data;\n } catch (error) {\n throw new Error(\n `Failed to resolve component from ${url}: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n }\n\n /**\n * Default fetch implementation\n */\n private async defaultFetch(url: string): Promise<string> {\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n return response.text();\n }\n\n /**\n * Extract the requested components based on the import clause\n */\n private extractComponents(\n clause: ImportClause,\n component: ComponentDefinition\n ): Record<string, ComponentDefinition> {\n if (clause.type === \"default\") {\n return {\n [clause.name]: component,\n };\n } else {\n // Named imports - for now, we only support single exports\n // In the future, we could support exporting multiple components\n // from a single file\n const result: Record<string, ComponentDefinition> = {};\n for (const name of clause.names) {\n result[name] = component;\n }\n return result;\n }\n }\n\n /**\n * Get the source path as a string (for caching)\n */\n private getSourcePath(source: ImportSource): string {\n return source.type === \"local\" ? source.path : source.url;\n }\n\n /**\n * Clear the component cache\n */\n clearCache(): void {\n this.cache.clear();\n }\n\n /**\n * Parse import statements from Hypen DSL text\n * This is a simple parser that extracts import statements\n */\n static parseImports(text: string): ImportStatement[] {\n const imports: ImportStatement[] = [];\n const importRegex =\n /import\\s+(?:(\\{[^}]*\\})|(\\w+))\\s+from\\s+[\"']([^\"']+)[\"']/g;\n\n let match;\n while ((match = importRegex.exec(text)) !== null) {\n const [, namedImports, defaultImport, source] = match;\n\n if (!source) continue;\n\n let clause: ImportClause;\n if (namedImports) {\n // Named imports: { Button, Card }\n const names = namedImports\n .slice(1, -1) // Remove { and }\n .split(\",\")\n .map((n) => n.trim())\n .filter((n) => n.length > 0);\n clause = { type: \"named\", names };\n } else if (defaultImport) {\n // Default import: HomePage\n clause = { type: \"default\", name: defaultImport };\n } else {\n continue;\n }\n\n // Determine if source is URL or local path\n const sourceObj: ImportSource = source.startsWith(\"http://\") ||\n source.startsWith(\"https://\")\n ? { type: \"url\", url: source }\n : { type: \"local\", path: source };\n\n imports.push({ clause, source: sourceObj });\n }\n\n return imports;\n }\n}\n"
5
+ "/**\n * Component Resolver\n * Resolves component imports from local paths or web URLs\n */\n\nexport interface ImportStatement {\n clause: ImportClause;\n source: ImportSource;\n}\n\nexport type ImportClause =\n | { type: \"named\"; names: string[] }\n | { type: \"default\"; name: string };\n\nexport type ImportSource =\n | { type: \"local\"; path: string }\n | { type: \"url\"; url: string };\n\nexport interface ComponentDefinition {\n module: any;\n template: string;\n}\n\nexport interface ResolverOptions {\n /**\n * Base directory for resolving relative local paths\n * Defaults to current working directory\n */\n baseDir?: string;\n\n /**\n * Cache resolved components to avoid re-fetching\n * Defaults to true\n */\n cache?: boolean;\n\n /**\n * Custom fetch function for URL imports\n * Useful for adding authentication, custom headers, etc.\n */\n customFetch?: (url: string) => Promise<string>;\n\n /**\n * Module registry for looking up pre-registered module definitions.\n * When set, the resolver will check the registry before doing file I/O.\n */\n moduleRegistry?: { has: (name: string) => boolean; get: (name: string) => any };\n}\n\n/**\n * Component Resolver\n * Resolves and loads components from local files or remote URLs\n */\nexport class ComponentResolver {\n private cache = new Map<string, ComponentDefinition>();\n private options: Required<Pick<ResolverOptions, 'baseDir' | 'cache' | 'customFetch'>> & Pick<ResolverOptions, 'moduleRegistry'>;\n\n private moduleRegistry?: { has: (name: string) => boolean; get: (name: string) => any };\n\n constructor(options: ResolverOptions = {}) {\n this.options = {\n baseDir: options.baseDir || process.cwd(),\n cache: options.cache ?? true,\n customFetch: options.customFetch || this.defaultFetch.bind(this),\n moduleRegistry: options.moduleRegistry,\n };\n this.moduleRegistry = options.moduleRegistry;\n }\n\n /**\n * Resolve a component from an import statement.\n * Checks the module registry first (if available), then falls back to file I/O.\n */\n async resolve(\n importStmt: ImportStatement\n ): Promise<Record<string, ComponentDefinition>> {\n // Check module registry first — if a component is pre-registered,\n // use its template directly (it will be mounted as a full module)\n if (this.moduleRegistry) {\n const names = importStmt.clause.type === \"named\"\n ? importStmt.clause.names\n : [importStmt.clause.name];\n\n const allFound = names.every(name => this.moduleRegistry!.has(name));\n if (allFound) {\n const result: Record<string, ComponentDefinition> = {};\n for (const name of names) {\n const def = this.moduleRegistry!.get(name);\n result[name] = {\n module: def,\n template: def?.template || \"\",\n };\n }\n return result;\n }\n }\n\n const sourcePath = this.getSourcePath(importStmt.source);\n\n // Check cache first\n if (this.options.cache && this.cache.has(sourcePath)) {\n const cached = this.cache.get(sourcePath)!;\n return this.extractComponents(importStmt.clause, cached);\n }\n\n // Load the component\n let component: ComponentDefinition;\n if (importStmt.source.type === \"local\") {\n component = await this.resolveLocal(importStmt.source.path);\n } else {\n component = await this.resolveUrl(importStmt.source.url);\n }\n\n // Cache it\n if (this.options.cache) {\n this.cache.set(sourcePath, component);\n }\n\n return this.extractComponents(importStmt.clause, component);\n }\n\n /**\n * Resolve a component from a local file path\n */\n private async resolveLocal(path: string): Promise<ComponentDefinition> {\n const { resolve, join } = await import(\"path\");\n const { readFile } = await import(\"fs/promises\");\n\n const basePath = resolve(this.options.baseDir, path);\n\n // Try .hypen extension\n const hypenPath = basePath.endsWith(\".hypen\")\n ? basePath\n : `${basePath}.hypen`;\n\n const template = await readFile(hypenPath, \"utf-8\");\n\n // Try loading sibling .ts module (stateless components have no module)\n const modulePath = hypenPath.replace(/\\.hypen$/, \".ts\");\n let module = {};\n try {\n module = await import(modulePath);\n } catch {\n // Stateless component — no module file\n }\n\n return { module, template };\n }\n\n /**\n * Resolve a component from a URL\n */\n private async resolveUrl(url: string): Promise<ComponentDefinition> {\n try {\n const response = await this.options.customFetch(url);\n const data = JSON.parse(response);\n\n // Expected format: { module: {...}, template: \"...\" }\n if (!data.module || !data.template) {\n throw new Error(\n `Invalid component format from ${url}. Expected { module, template }`\n );\n }\n\n return data;\n } catch (error) {\n throw new Error(\n `Failed to resolve component from ${url}: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n }\n\n /**\n * Default fetch implementation\n */\n private async defaultFetch(url: string): Promise<string> {\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n return response.text();\n }\n\n /**\n * Extract the requested components based on the import clause\n */\n private extractComponents(\n clause: ImportClause,\n component: ComponentDefinition\n ): Record<string, ComponentDefinition> {\n if (clause.type === \"default\") {\n return {\n [clause.name]: component,\n };\n } else {\n // Named imports - for now, we only support single exports\n // In the future, we could support exporting multiple components\n // from a single file\n const result: Record<string, ComponentDefinition> = {};\n for (const name of clause.names) {\n result[name] = component;\n }\n return result;\n }\n }\n\n /**\n * Get the source path as a string (for caching)\n */\n private getSourcePath(source: ImportSource): string {\n return source.type === \"local\" ? source.path : source.url;\n }\n\n /**\n * Clear the component cache\n */\n clearCache(): void {\n this.cache.clear();\n }\n\n /**\n * Parse import statements from Hypen DSL text\n * This is a simple parser that extracts import statements\n */\n static parseImports(text: string): ImportStatement[] {\n const imports: ImportStatement[] = [];\n const importRegex =\n /import\\s+(?:(\\{[^}]*\\})|(\\w+))\\s+from\\s+[\"']([^\"']+)[\"']/g;\n\n let match;\n while ((match = importRegex.exec(text)) !== null) {\n const [, namedImports, defaultImport, source] = match;\n\n if (!source) continue;\n\n let clause: ImportClause;\n if (namedImports) {\n // Named imports: { Button, Card }\n const names = namedImports\n .slice(1, -1) // Remove { and }\n .split(\",\")\n .map((n) => n.trim())\n .filter((n) => n.length > 0);\n clause = { type: \"named\", names };\n } else if (defaultImport) {\n // Default import: HomePage\n clause = { type: \"default\", name: defaultImport };\n } else {\n continue;\n }\n\n // Determine if source is URL or local path\n const sourceObj: ImportSource = source.startsWith(\"http://\") ||\n source.startsWith(\"https://\")\n ? { type: \"url\", url: source }\n : { type: \"local\", path: source };\n\n imports.push({ clause, source: sourceObj });\n }\n\n return imports;\n }\n}\n"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "708BFB387356D58864756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "20EC11627252563C64756E2164756E21",
9
9
  "names": []
10
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypen-space/core",
3
- "version": "0.3.12",
3
+ "version": "0.4.1",
4
4
  "description": "Hypen core engine - Platform-agnostic reactive UI runtime",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -129,6 +129,18 @@
129
129
  "bun": "./src/disposable.ts",
130
130
  "import": "./dist/disposable.js",
131
131
  "default": "./dist/disposable.js"
132
+ },
133
+ "./types": {
134
+ "types": "./dist/types.d.ts",
135
+ "bun": "./src/types.ts",
136
+ "import": "./dist/types.js",
137
+ "default": "./dist/types.js"
138
+ },
139
+ "./logger": {
140
+ "types": "./dist/logger.d.ts",
141
+ "bun": "./src/logger.ts",
142
+ "import": "./dist/logger.js",
143
+ "default": "./dist/logger.js"
132
144
  }
133
145
  },
134
146
  "files": [
package/src/app.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * Implements the stateful module system from RFC-0001
4
4
  */
5
5
 
6
- import type { Action } from "./engine.js";
6
+ import type { Action } from "./types.js";
7
7
  import type { Session } from "./remote/types.js";
8
8
  import { type Result, Ok, Err, fromPromise, ActionError, HypenError } from "./result.js";
9
9
 
@@ -383,7 +383,11 @@ export class HypenModuleInstance<T extends object = any> {
383
383
  this.routerContext = routerContext;
384
384
  this.globalContext = globalContext;
385
385
 
386
+ // Named state prefix — all lowercase per convention
387
+ const statePrefix = (definition.name || "").toLowerCase();
388
+
386
389
  // Create observable state that sends only changed paths and values to engine
390
+ // When a prefix is set, all paths are namespaced (e.g., "homepage.count")
387
391
  this.state = createObservableState<T>(definition.initialState as T & object, {
388
392
  onChange: (change: StateChange) => {
389
393
  // Send only the changed paths and their new values (not the whole state)
@@ -391,14 +395,22 @@ export class HypenModuleInstance<T extends object = any> {
391
395
  // Notify all registered callbacks
392
396
  this.stateChangeCallbacks.forEach(cb => cb());
393
397
  },
398
+ pathPrefix: statePrefix || undefined,
394
399
  });
395
400
 
396
401
  // Register with engine (initial state registration)
402
+ // When prefixed, nest the state under the module name so the engine sees
403
+ // e.g., { homepage: { count: 0 } } instead of { count: 0 }
404
+ const snapshot = getStateSnapshot(this.state);
405
+ const prefixedState = statePrefix
406
+ ? { [statePrefix]: snapshot }
407
+ : snapshot;
408
+
397
409
  this.engine.setModule(
398
410
  definition.name || "AnonymousModule",
399
411
  definition.actions,
400
412
  definition.stateKeys,
401
- getStateSnapshot(this.state)
413
+ prefixedState
402
414
  );
403
415
 
404
416
  // Register action handlers with flexible parameter support
package/src/discovery.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import { existsSync, readdirSync, readFileSync, watch } from "fs";
14
- import { join, basename, resolve } from "path";
14
+ import { join, basename, resolve, relative } from "path";
15
15
  import type { HypenModuleDefinition } from "./app.js";
16
16
  import { frameworkLoggers } from "./logger.js";
17
17
 
@@ -123,7 +123,9 @@ export async function discoverComponents(
123
123
 
124
124
  seen.add(name);
125
125
  const templateRaw = readFileSync(hypenPath, "utf-8");
126
- const template = removeImports(templateRaw).trim();
126
+ // Preserve import statements in templates — the engine now processes them
127
+ // via parse_document() and resolves imports through the component resolver
128
+ const template = templateRaw.trim();
127
129
 
128
130
  components.push({
129
131
  name,
@@ -466,10 +468,11 @@ export function watchComponents(
466
468
  */
467
469
  export async function generateComponentsCode(
468
470
  baseDir: string,
469
- options: DiscoveryOptions = {}
471
+ options: DiscoveryOptions & { outputDir?: string } = {}
470
472
  ): Promise<string> {
471
473
  const components = await discoverComponents(baseDir, options);
472
474
  const resolvedDir = resolve(baseDir);
475
+ const outputBase = options.outputDir ? resolve(options.outputDir) : resolvedDir;
473
476
 
474
477
  let code = `/**
475
478
  * Auto-generated component imports
@@ -479,15 +482,15 @@ export async function generateComponentsCode(
479
482
  `;
480
483
 
481
484
  for (const component of components) {
482
- const relativePath = component.modulePath
483
- ? "./" +
484
- component.modulePath
485
- .replace(resolvedDir + "/", "")
486
- .replace(/\.ts$/, ".js")
487
- : null;
488
-
489
- if (relativePath) {
490
- code += `import ${component.name}Module from "${relativePath}";\n`;
485
+ let importPath: string | null = null;
486
+ if (component.modulePath) {
487
+ const rel = relative(outputBase, component.modulePath)
488
+ .replace(/\.ts$/, ".js");
489
+ importPath = rel.startsWith(".") ? rel : "./" + rel;
490
+ }
491
+
492
+ if (importPath) {
493
+ code += `import ${component.name}Module from "${importPath}";\n`;
491
494
  }
492
495
  }
493
496
 
@@ -4,6 +4,10 @@
4
4
  */
5
5
 
6
6
  import { frameworkLoggers } from "./logger.js";
7
+ import type { Patch, Action, RenderCallback, ActionHandler, ResolvedComponent, ComponentResolver } from "./types.js";
8
+
9
+ // Re-export types so existing consumers of "./engine.browser.js" still work
10
+ export type { Patch, Action, RenderCallback, ActionHandler, ResolvedComponent, ComponentResolver } from "./types.js";
7
11
 
8
12
  const log = frameworkLoggers.engine;
9
13
 
@@ -12,38 +16,6 @@ const log = frameworkLoggers.engine;
12
16
  let wasmInit: ((path?: string) => Promise<void>) | null = null;
13
17
  let WasmEngineClass: any = null;
14
18
 
15
- export type Patch = {
16
- type: "create" | "setProp" | "setText" | "insert" | "move" | "remove" | "attachEvent" | "detachEvent";
17
- id?: string;
18
- elementType?: string;
19
- props?: Record<string, any>;
20
- name?: string;
21
- value?: any;
22
- text?: string;
23
- parentId?: string;
24
- beforeId?: string;
25
- eventName?: string;
26
- };
27
-
28
- export type Action = {
29
- name: string;
30
- payload?: any;
31
- sender?: string;
32
- };
33
-
34
- export type RenderCallback = (patches: Patch[]) => void;
35
- export type ActionHandler = (action: Action) => void | Promise<void>;
36
-
37
- export type ResolvedComponent = {
38
- source: string;
39
- path: string;
40
- };
41
-
42
- export type ComponentResolver = (
43
- componentName: string,
44
- contextPath: string | null
45
- ) => ResolvedComponent | null;
46
-
47
19
  export interface EngineInitOptions {
48
20
  /**
49
21
  * URL to the WASM binary file.
package/src/engine.ts CHANGED
@@ -3,12 +3,13 @@
3
3
  * Node.js / Bundler target
4
4
  */
5
5
 
6
- // WASM module loaded lazily to avoid pulling readFileSync into browser bundles
7
- import type { WasmEngine as WasmEngineType } from "../wasm-node/hypen_engine.js";
8
- type WasmEngine = WasmEngineType;
9
- let WasmEngineClass: (new () => WasmEngine) | null = null;
10
-
6
+ // WASM module types
7
+ import { WasmEngine } from "../wasm-node/hypen_engine.js";
11
8
  import { frameworkLoggers } from "./logger.js";
9
+ import type { Patch, Action, RenderCallback, ActionHandler, ResolvedComponent, ComponentResolver } from "./types.js";
10
+
11
+ // Re-export types so existing consumers of "./engine.js" still work
12
+ export type { Patch, Action, RenderCallback, ActionHandler, ResolvedComponent, ComponentResolver } from "./types.js";
12
13
 
13
14
  const log = frameworkLoggers.engine;
14
15
 
@@ -37,38 +38,6 @@ function unwrapForWasm<T>(value: T): T {
37
38
  }
38
39
  }
39
40
 
40
- export type Patch = {
41
- type: "create" | "setProp" | "setText" | "insert" | "move" | "remove" | "attachEvent" | "detachEvent";
42
- id?: string;
43
- elementType?: string;
44
- props?: Record<string, any>;
45
- name?: string;
46
- value?: any;
47
- text?: string;
48
- parentId?: string;
49
- beforeId?: string;
50
- eventName?: string;
51
- };
52
-
53
- export type Action = {
54
- name: string;
55
- payload?: any;
56
- sender?: string;
57
- };
58
-
59
- export type RenderCallback = (patches: Patch[]) => void;
60
- export type ActionHandler = (action: Action) => void | Promise<void>;
61
-
62
- export type ResolvedComponent = {
63
- source: string;
64
- path: string;
65
- };
66
-
67
- export type ComponentResolver = (
68
- componentName: string,
69
- contextPath: string | null
70
- ) => ResolvedComponent | null;
71
-
72
41
  /**
73
42
  * Engine wraps the WASM engine and provides a TypeScript-friendly API
74
43
  */
@@ -82,13 +51,8 @@ export class Engine {
82
51
  async init(): Promise<void> {
83
52
  if (this.initialized) return;
84
53
 
85
- // Lazy-load WASM to avoid pulling readFileSync into browser bundles
86
- if (!WasmEngineClass) {
87
- const wasm = await import("../wasm-node/hypen_engine.js");
88
- WasmEngineClass = wasm.WasmEngine;
89
- }
90
-
91
- this.wasmEngine = new WasmEngineClass();
54
+ // For bundler target, WASM is auto-initialized
55
+ this.wasmEngine = new WasmEngine();
92
56
  this.initialized = true;
93
57
  }
94
58
 
@@ -5,10 +5,9 @@
5
5
  */
6
6
 
7
7
  // ============================================================================
8
- // ENGINE API (Browser version)
8
+ // CORE TYPES (WASM-free)
9
9
  // ============================================================================
10
10
 
11
- export { Engine } from "./engine.browser.js";
12
11
  export type {
13
12
  Patch,
14
13
  Action,
@@ -16,8 +15,11 @@ export type {
16
15
  ActionHandler as EngineActionHandler,
17
16
  ResolvedComponent,
18
17
  ComponentResolver,
19
- EngineInitOptions,
20
- } from "./engine.browser.js";
18
+ } from "./types.js";
19
+
20
+ // NOTE: Engine (BrowserEngine) is NOT exported from the barrel.
21
+ // Import it explicitly from its subpath to avoid pulling in WASM:
22
+ // import { Engine } from "@hypen-space/core/engine/browser"
21
23
 
22
24
  // ============================================================================
23
25
  // APP / MODULE SYSTEM
package/src/index.ts CHANGED
@@ -42,10 +42,9 @@
42
42
  */
43
43
 
44
44
  // ============================================================================
45
- // ENGINE API
45
+ // CORE TYPES (WASM-free)
46
46
  // ============================================================================
47
47
 
48
- export { Engine } from "./engine.js";
49
48
  export type {
50
49
  Patch,
51
50
  Action,
@@ -53,11 +52,12 @@ export type {
53
52
  ActionHandler as EngineActionHandler,
54
53
  ResolvedComponent,
55
54
  ComponentResolver as EngineComponentResolver,
56
- } from "./engine.js";
55
+ } from "./types.js";
57
56
 
58
- // Browser engine (explicit WASM init)
59
- export { Engine as BrowserEngine } from "./engine.browser.js";
60
- export type { EngineInitOptions } from "./engine.browser.js";
57
+ // NOTE: Engine and BrowserEngine are NOT exported from the barrel.
58
+ // Import them explicitly from their subpaths to avoid pulling in WASM:
59
+ // import { Engine } from "@hypen-space/core/engine"
60
+ // import { Engine } from "@hypen-space/core/engine/browser"
61
61
 
62
62
  // ============================================================================
63
63
  // APP / MODULE SYSTEM
@@ -149,7 +149,7 @@ export type { ModuleReference } from "./context.js";
149
149
  // REMOTE UI
150
150
  // ============================================================================
151
151
 
152
- // Client
152
+ // Client (browser-safe)
153
153
  export { RemoteEngine } from "./remote/client.js";
154
154
  export type {
155
155
  RemoteConnectionState,
@@ -158,8 +158,9 @@ export type {
158
158
  SessionInfo,
159
159
  } from "./remote/client.js";
160
160
 
161
- // Server
162
- export { RemoteServer, serve } from "./remote/server.js";
161
+ // NOTE: RemoteServer and serve are NOT exported from the barrel because they
162
+ // import the Node.js WASM engine. Import them from the subpath instead:
163
+ // import { RemoteServer, serve } from "@hypen-space/core/remote/server"
163
164
 
164
165
  // Session Management
165
166
  export { SessionManager } from "./remote/session.js";
@@ -182,14 +183,7 @@ export type {
182
183
  } from "./remote/types.js";
183
184
 
184
185
  // ============================================================================
185
- // COMPONENT LOADER
186
- // ============================================================================
187
-
188
- export { ComponentLoader, componentLoader } from "./loader.js";
189
- export type { ComponentDefinition } from "./loader.js";
190
-
191
- // ============================================================================
192
- // COMPONENT RESOLVER
186
+ // COMPONENT RESOLVER (browser-safe, no fs dependency)
193
187
  // ============================================================================
194
188
 
195
189
  export { ComponentResolver } from "./resolver.js";
@@ -202,26 +196,24 @@ export type {
202
196
  } from "./resolver.js";
203
197
 
204
198
  // ============================================================================
205
- // COMPONENT DISCOVERY
199
+ // NODE-ONLY MODULES (NOT exported from barrel — use subpath imports)
206
200
  // ============================================================================
201
+ //
202
+ // These modules use Node.js APIs (fs.readFileSync) and must NOT be in the
203
+ // browser-safe barrel. Import them from their subpaths instead:
204
+ //
205
+ // import { ComponentLoader, componentLoader } from "@hypen-space/core/loader"
206
+ // import { discoverComponents, ... } from "@hypen-space/core/discovery"
207
+ // import { hypenPlugin, ... } from "@hypen-space/core/plugin"
208
+ //
207
209
 
208
- export {
209
- discoverComponents,
210
- loadDiscoveredComponents,
211
- watchComponents,
212
- generateComponentsCode,
213
- } from "./discovery.js";
210
+ // Re-export types only (types are always safe)
211
+ export type { ComponentDefinition } from "./loader.js";
214
212
  export type {
215
213
  DiscoveredComponent,
216
214
  DiscoveryOptions,
217
215
  WatchOptions,
218
216
  } from "./discovery.js";
219
-
220
- // ============================================================================
221
- // PLUGIN
222
- // ============================================================================
223
-
224
- export { hypenPlugin, registerHypenPlugin, defaultHypenPlugin } from "./plugin.js";
225
217
  export type { HypenPluginOptions } from "./plugin.js";
226
218
 
227
219
  // ============================================================================