blaizejs 0.1.0 → 0.2.0

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/dist/index.d.cts CHANGED
@@ -15,7 +15,7 @@ declare function compose(middlewareStack: Middleware[]): MiddlewareFunction;
15
15
  /**
16
16
  * Create a plugin with the given name, version, and setup function
17
17
  */
18
- declare function create$1<T = any>(name: string, version: string, setup: (app: Server, options: T) => void | PluginHooks | Promise<void> | Promise<PluginHooks>, _defaultOptions?: Partial<T>): PluginFactory<T>;
18
+ declare function create$1<T = any>(name: string, version: string, setup: (app: Server, options: T) => void | Partial<PluginHooks> | Promise<void> | Promise<Partial<PluginHooks>>, defaultOptions?: Partial<T>): PluginFactory<T>;
19
19
 
20
20
  /**
21
21
  * Create a GET route
package/dist/index.d.ts CHANGED
@@ -15,7 +15,7 @@ declare function compose(middlewareStack: Middleware[]): MiddlewareFunction;
15
15
  /**
16
16
  * Create a plugin with the given name, version, and setup function
17
17
  */
18
- declare function create$1<T = any>(name: string, version: string, setup: (app: Server, options: T) => void | PluginHooks | Promise<void> | Promise<PluginHooks>, _defaultOptions?: Partial<T>): PluginFactory<T>;
18
+ declare function create$1<T = any>(name: string, version: string, setup: (app: Server, options: T) => void | Partial<PluginHooks> | Promise<void> | Promise<Partial<PluginHooks>>, defaultOptions?: Partial<T>): PluginFactory<T>;
19
19
 
20
20
  /**
21
21
  * Create a GET route
package/dist/index.js CHANGED
@@ -80,8 +80,31 @@ function compose(middlewareStack) {
80
80
  }
81
81
 
82
82
  // src/plugins/create.ts
83
- function create2(name, version, setup, _defaultOptions = {}) {
84
- throw new Error("Plugin system not yet available");
83
+ function create2(name, version, setup, defaultOptions = {}) {
84
+ if (!name || typeof name !== "string") {
85
+ throw new Error("Plugin name must be a non-empty string");
86
+ }
87
+ if (!version || typeof version !== "string") {
88
+ throw new Error("Plugin version must be a non-empty string");
89
+ }
90
+ if (typeof setup !== "function") {
91
+ throw new Error("Plugin setup must be a function");
92
+ }
93
+ return function pluginFactory(userOptions) {
94
+ const mergedOptions = { ...defaultOptions, ...userOptions };
95
+ const plugin = {
96
+ name,
97
+ version,
98
+ // The register hook calls the user's setup function
99
+ register: async (app) => {
100
+ const result = await setup(app, mergedOptions);
101
+ if (result && typeof result === "object") {
102
+ Object.assign(plugin, result);
103
+ }
104
+ }
105
+ };
106
+ return plugin;
107
+ };
85
108
  }
86
109
 
87
110
  // src/router/discovery/finder.ts
@@ -121,12 +144,18 @@ async function findRouteFiles(routesDir, options = {}) {
121
144
  return routeFiles;
122
145
  }
123
146
  function isRouteFile(filename) {
124
- return !filename.startsWith("_") && (filename.endsWith(".ts") || filename.endsWith(".js")) && filename !== "index.ts" && filename !== "index.js";
147
+ return !filename.startsWith("_") && (filename.endsWith(".ts") || filename.endsWith(".js"));
125
148
  }
126
149
 
127
150
  // src/router/discovery/parser.ts
128
151
  import * as path2 from "node:path";
129
152
  function parseRoutePath(filePath, basePath) {
153
+ if (filePath.startsWith("file://")) {
154
+ filePath = filePath.replace("file://", "");
155
+ }
156
+ if (basePath.startsWith("file://")) {
157
+ basePath = basePath.replace("file://", "");
158
+ }
130
159
  const forwardSlashFilePath = filePath.replace(/\\/g, "/");
131
160
  const forwardSlashBasePath = basePath.replace(/\\/g, "/");
132
161
  const normalizedBasePath = forwardSlashBasePath.endsWith("/") ? forwardSlashBasePath : `${forwardSlashBasePath}/`;
@@ -660,21 +689,56 @@ function createRouter(options) {
660
689
  const matcher = createMatcher();
661
690
  let initialized = false;
662
691
  let initializationPromise = null;
663
- let _watcher = null;
692
+ let _watchers = null;
693
+ const routeSources = /* @__PURE__ */ new Map();
694
+ const routeDirectories = /* @__PURE__ */ new Set([routerOptions.routesDir]);
695
+ function addRouteWithSource(route, source) {
696
+ const existingSources = routeSources.get(route.path) || [];
697
+ if (existingSources.includes(source)) {
698
+ console.warn(`Skipping duplicate route: ${route.path} from ${source}`);
699
+ return;
700
+ }
701
+ if (existingSources.length > 0) {
702
+ const conflictError = new Error(
703
+ `Route conflict for path "${route.path}": already defined in ${existingSources.join(", ")}, now being added from ${source}`
704
+ );
705
+ console.error(conflictError.message);
706
+ throw conflictError;
707
+ }
708
+ routeSources.set(route.path, [...existingSources, source]);
709
+ addRouteInternal(route);
710
+ }
711
+ async function loadRoutesFromDirectory(directory, source, prefix) {
712
+ try {
713
+ const discoveredRoutes = await findRoutes(directory, {
714
+ basePath: routerOptions.basePath
715
+ });
716
+ for (const route of discoveredRoutes) {
717
+ const finalRoute = prefix ? {
718
+ ...route,
719
+ path: `${prefix}${route.path}`
720
+ } : route;
721
+ addRouteWithSource(finalRoute, source);
722
+ }
723
+ console.log(
724
+ `Loaded ${discoveredRoutes.length} routes from ${source}${prefix ? ` with prefix ${prefix}` : ""}`
725
+ );
726
+ } catch (error) {
727
+ console.error(`Failed to load routes from ${source}:`, error);
728
+ throw error;
729
+ }
730
+ }
664
731
  async function initialize() {
665
732
  if (initialized || initializationPromise) {
666
733
  return initializationPromise;
667
734
  }
668
735
  initializationPromise = (async () => {
669
736
  try {
670
- const discoveredRoutes = await findRoutes(routerOptions.routesDir, {
671
- basePath: routerOptions.basePath
672
- });
673
- for (const route of discoveredRoutes) {
674
- addRouteInternal(route);
737
+ for (const directory of routeDirectories) {
738
+ await loadRoutesFromDirectory(directory, directory);
675
739
  }
676
740
  if (routerOptions.watchMode) {
677
- setupWatcher();
741
+ setupWatcherForAllDirectories();
678
742
  }
679
743
  initialized = true;
680
744
  } catch (error) {
@@ -691,47 +755,81 @@ function createRouter(options) {
691
755
  matcher.add(route.path, method, methodOptions);
692
756
  });
693
757
  }
694
- function setupWatcher() {
695
- _watcher = watchRoutes(routerOptions.routesDir, {
696
- ignore: ["node_modules", ".git"],
758
+ function createWatcherCallbacks(directory, source, prefix) {
759
+ return {
697
760
  onRouteAdded: (addedRoutes) => {
698
761
  console.log(
699
- `${addedRoutes.length} route(s) added:`,
762
+ `${addedRoutes.length} route(s) added from ${directory}:`,
700
763
  addedRoutes.map((r) => r.path)
701
764
  );
702
- addedRoutes.forEach((route) => addRouteInternal(route));
765
+ addedRoutes.forEach((route) => {
766
+ const finalRoute = prefix ? { ...route, path: `${prefix}${route.path}` } : route;
767
+ addRouteWithSource(finalRoute, source);
768
+ });
703
769
  },
704
770
  onRouteChanged: (changedRoutes) => {
705
771
  console.log(
706
- `${changedRoutes.length} route(s) changed:`,
772
+ `${changedRoutes.length} route(s) changed in ${directory}:`,
707
773
  changedRoutes.map((r) => r.path)
708
774
  );
709
775
  changedRoutes.forEach((route) => {
710
- const index = routes.findIndex((r) => r.path === route.path);
776
+ const finalPath = prefix ? `${prefix}${route.path}` : route.path;
777
+ const index = routes.findIndex((r) => r.path === finalPath);
711
778
  if (index >= 0) {
712
779
  routes.splice(index, 1);
780
+ const sources = routeSources.get(finalPath) || [];
781
+ const filteredSources = sources.filter((s) => s !== source);
782
+ if (filteredSources.length > 0) {
783
+ routeSources.set(finalPath, filteredSources);
784
+ } else {
785
+ routeSources.delete(finalPath);
786
+ }
713
787
  }
714
- addRouteInternal(route);
788
+ const finalRoute = prefix ? { ...route, path: finalPath } : route;
789
+ addRouteWithSource(finalRoute, source);
715
790
  });
716
791
  },
717
792
  onRouteRemoved: (filePath, removedRoutes) => {
718
- console.log("-----------------------Routes before removal:", routes);
719
793
  console.log(
720
- `File removed: ${filePath} with ${removedRoutes.length} route(s):`,
794
+ `File removed from ${directory}: ${filePath} with ${removedRoutes.length} route(s):`,
721
795
  removedRoutes.map((r) => r.path)
722
796
  );
723
797
  removedRoutes.forEach((route) => {
724
- const index = routes.findIndex((r) => r.path === route.path);
798
+ const finalPath = prefix ? `${prefix}${route.path}` : route.path;
799
+ const index = routes.findIndex((r) => r.path === finalPath);
725
800
  if (index >= 0) {
726
801
  routes.splice(index, 1);
727
802
  }
803
+ const sources = routeSources.get(finalPath) || [];
804
+ const filteredSources = sources.filter((s) => s !== source);
805
+ if (filteredSources.length > 0) {
806
+ routeSources.set(finalPath, filteredSources);
807
+ } else {
808
+ routeSources.delete(finalPath);
809
+ }
728
810
  });
729
- console.log("-----------------------Routes after removal:", routes);
730
811
  },
731
812
  onError: (error) => {
732
- console.error("Route watcher error:", error);
813
+ console.error(`Route watcher error for ${directory}:`, error);
733
814
  }
815
+ };
816
+ }
817
+ function setupWatcherForDirectory(directory, source, prefix) {
818
+ const callbacks = createWatcherCallbacks(directory, source, prefix);
819
+ const watcher = watchRoutes(directory, {
820
+ ignore: ["node_modules", ".git"],
821
+ ...callbacks
734
822
  });
823
+ if (!_watchers) {
824
+ _watchers = /* @__PURE__ */ new Map();
825
+ }
826
+ _watchers.set(directory, watcher);
827
+ return watcher;
828
+ }
829
+ function setupWatcherForAllDirectories() {
830
+ for (const directory of routeDirectories) {
831
+ setupWatcherForDirectory(directory, directory);
832
+ }
735
833
  }
736
834
  initialize().catch((error) => {
737
835
  console.error("Failed to initialize router on creation:", error);
@@ -781,6 +879,34 @@ function createRouter(options) {
781
879
  */
782
880
  addRoute(route) {
783
881
  addRouteInternal(route);
882
+ },
883
+ /**
884
+ * Add a route directory (for plugins)
885
+ */
886
+ async addRouteDirectory(directory, options2 = {}) {
887
+ if (routeDirectories.has(directory)) {
888
+ console.warn(`Route directory ${directory} already registered`);
889
+ return;
890
+ }
891
+ routeDirectories.add(directory);
892
+ if (initialized) {
893
+ await loadRoutesFromDirectory(directory, directory, options2.prefix);
894
+ if (routerOptions.watchMode) {
895
+ setupWatcherForDirectory(directory, directory, options2.prefix);
896
+ }
897
+ }
898
+ },
899
+ /**
900
+ * Get route conflicts
901
+ */
902
+ getRouteConflicts() {
903
+ const conflicts = [];
904
+ for (const [path5, sources] of routeSources.entries()) {
905
+ if (sources.length > 1) {
906
+ conflicts.push({ path: path5, sources });
907
+ }
908
+ }
909
+ return conflicts;
784
910
  }
785
911
  };
786
912
  }
@@ -803,7 +929,7 @@ function getCallerFilePath() {
803
929
  try {
804
930
  Error.prepareStackTrace = (_, stack2) => stack2;
805
931
  const stack = new Error().stack;
806
- const callerFrame = stack[2];
932
+ const callerFrame = stack[3];
807
933
  if (!callerFrame || typeof callerFrame.getFileName !== "function") {
808
934
  throw new Error("Unable to determine caller file frame");
809
935
  }
@@ -817,9 +943,11 @@ function getCallerFilePath() {
817
943
  }
818
944
  }
819
945
  function getRoutePath() {
946
+ console.log("getRoutePath called");
820
947
  const callerPath = getCallerFilePath();
821
948
  const routesDir = getRoutesDir();
822
949
  const parsedRoute = parseRoutePath(callerPath, routesDir);
950
+ console.log(`Parsed route path: ${parsedRoute.routePath} from file: ${callerPath}`);
823
951
  return parsedRoute.routePath;
824
952
  }
825
953
  var createGetRoute = (config2) => {
@@ -1508,12 +1636,12 @@ async function stopServer(serverInstance, options = {}) {
1508
1636
  return;
1509
1637
  }
1510
1638
  const timeout = options.timeout || 3e4;
1511
- const plugins = serverInstance.plugins;
1512
1639
  try {
1513
1640
  if (options.onStopping) {
1514
1641
  await options.onStopping();
1515
1642
  }
1516
1643
  events.emit("stopping");
1644
+ await serverInstance.pluginManager.onServerStop(serverInstance, server);
1517
1645
  const timeoutPromise = new Promise((_, reject) => {
1518
1646
  setTimeout(() => {
1519
1647
  reject(new Error("Server shutdown timed out waiting for requests to complete"));
@@ -1528,13 +1656,7 @@ async function stopServer(serverInstance, options = {}) {
1528
1656
  });
1529
1657
  });
1530
1658
  await Promise.race([closePromise, timeoutPromise]);
1531
- if (plugins?.length) {
1532
- for (const plugin of [...plugins].reverse()) {
1533
- if (plugin.terminate) {
1534
- await plugin.terminate();
1535
- }
1536
- }
1537
- }
1659
+ await serverInstance.pluginManager.terminatePlugins(serverInstance);
1538
1660
  if (options.onStopped) {
1539
1661
  await options.onStopped();
1540
1662
  }
@@ -1609,6 +1731,158 @@ function validateServerOptions(options) {
1609
1731
  }
1610
1732
  }
1611
1733
 
1734
+ // src/plugins/lifecycle.ts
1735
+ function createPluginLifecycleManager(options = {}) {
1736
+ const { continueOnError = true, debug = false, onError } = options;
1737
+ function log(message, ...args) {
1738
+ if (debug) {
1739
+ console.log(`[PluginLifecycle] ${message}`, ...args);
1740
+ }
1741
+ }
1742
+ function handleError(plugin, phase, error) {
1743
+ const errorMessage = `Plugin ${plugin.name} failed during ${phase}: ${error.message}`;
1744
+ if (onError) {
1745
+ onError(plugin, phase, error);
1746
+ } else {
1747
+ console.error(errorMessage, error);
1748
+ }
1749
+ if (!continueOnError) {
1750
+ throw new Error(errorMessage);
1751
+ }
1752
+ }
1753
+ return {
1754
+ /**
1755
+ * Initialize all plugins
1756
+ */
1757
+ async initializePlugins(server) {
1758
+ log("Initializing plugins...");
1759
+ for (const plugin of server.plugins) {
1760
+ if (plugin.initialize) {
1761
+ try {
1762
+ log(`Initializing plugin: ${plugin.name}`);
1763
+ await plugin.initialize(server);
1764
+ } catch (error) {
1765
+ handleError(plugin, "initialize", error);
1766
+ }
1767
+ }
1768
+ }
1769
+ log(`Initialized ${server.plugins.length} plugins`);
1770
+ },
1771
+ /**
1772
+ * Terminate all plugins in reverse order
1773
+ */
1774
+ async terminatePlugins(server) {
1775
+ log("Terminating plugins...");
1776
+ const pluginsToTerminate = [...server.plugins].reverse();
1777
+ for (const plugin of pluginsToTerminate) {
1778
+ if (plugin.terminate) {
1779
+ try {
1780
+ log(`Terminating plugin: ${plugin.name}`);
1781
+ await plugin.terminate(server);
1782
+ } catch (error) {
1783
+ handleError(plugin, "terminate", error);
1784
+ }
1785
+ }
1786
+ }
1787
+ log(`Terminated ${pluginsToTerminate.length} plugins`);
1788
+ },
1789
+ /**
1790
+ * Notify plugins that the server has started
1791
+ */
1792
+ async onServerStart(server, httpServer) {
1793
+ log("Notifying plugins of server start...");
1794
+ for (const plugin of server.plugins) {
1795
+ if (plugin.onServerStart) {
1796
+ try {
1797
+ log(`Notifying plugin of server start: ${plugin.name}`);
1798
+ await plugin.onServerStart(httpServer);
1799
+ } catch (error) {
1800
+ handleError(plugin, "onServerStart", error);
1801
+ }
1802
+ }
1803
+ }
1804
+ },
1805
+ /**
1806
+ * Notify plugins that the server is stopping
1807
+ */
1808
+ async onServerStop(server, httpServer) {
1809
+ log("Notifying plugins of server stop...");
1810
+ const pluginsToNotify = [...server.plugins].reverse();
1811
+ for (const plugin of pluginsToNotify) {
1812
+ if (plugin.onServerStop) {
1813
+ try {
1814
+ log(`Notifying plugin of server stop: ${plugin.name}`);
1815
+ await plugin.onServerStop(httpServer);
1816
+ } catch (error) {
1817
+ handleError(plugin, "onServerStop", error);
1818
+ }
1819
+ }
1820
+ }
1821
+ }
1822
+ };
1823
+ }
1824
+
1825
+ // src/plugins/errors.ts
1826
+ var PluginValidationError = class extends Error {
1827
+ constructor(pluginName, message) {
1828
+ super(`Plugin validation error${pluginName ? ` for "${pluginName}"` : ""}: ${message}`);
1829
+ this.pluginName = pluginName;
1830
+ this.name = "PluginValidationError";
1831
+ }
1832
+ };
1833
+
1834
+ // src/plugins/validation.ts
1835
+ var RESERVED_NAMES = /* @__PURE__ */ new Set([
1836
+ "core",
1837
+ "server",
1838
+ "router",
1839
+ "middleware",
1840
+ "context",
1841
+ "blaize",
1842
+ "blaizejs"
1843
+ ]);
1844
+ var VALID_NAME_PATTERN = /^[a-z]([a-z0-9-]*[a-z0-9])?$/;
1845
+ var VALID_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[a-zA-Z0-9-.]+)?(?:\+[a-zA-Z0-9-.]+)?$/;
1846
+ function validatePlugin(plugin, options = {}) {
1847
+ const { requireVersion = true, validateNameFormat = true, checkReservedNames = true } = options;
1848
+ if (!plugin || typeof plugin !== "object") {
1849
+ throw new PluginValidationError("", "Plugin must be an object");
1850
+ }
1851
+ const p = plugin;
1852
+ if (!p.name || typeof p.name !== "string") {
1853
+ throw new PluginValidationError("", "Plugin must have a name (string)");
1854
+ }
1855
+ if (validateNameFormat && !VALID_NAME_PATTERN.test(p.name)) {
1856
+ throw new PluginValidationError(
1857
+ p.name,
1858
+ "Plugin name must be lowercase letters, numbers, and hyphens only"
1859
+ );
1860
+ }
1861
+ if (checkReservedNames && RESERVED_NAMES.has(p.name.toLowerCase())) {
1862
+ throw new PluginValidationError(p.name, `Plugin name "${p.name}" is reserved`);
1863
+ }
1864
+ if (requireVersion) {
1865
+ if (!p.version || typeof p.version !== "string") {
1866
+ throw new PluginValidationError(p.name, "Plugin must have a version (string)");
1867
+ }
1868
+ if (!VALID_VERSION_PATTERN.test(p.version)) {
1869
+ throw new PluginValidationError(
1870
+ p.name,
1871
+ 'Plugin version must follow semantic versioning (e.g., "1.0.0")'
1872
+ );
1873
+ }
1874
+ }
1875
+ if (!p.register || typeof p.register !== "function") {
1876
+ throw new PluginValidationError(p.name, "Plugin must have a register method (function)");
1877
+ }
1878
+ const lifecycleMethods = ["initialize", "terminate", "onServerStart", "onServerStop"];
1879
+ for (const method of lifecycleMethods) {
1880
+ if (p[method] && typeof p[method] !== "function") {
1881
+ throw new PluginValidationError(p.name, `Plugin ${method} must be a function if provided`);
1882
+ }
1883
+ }
1884
+ }
1885
+
1612
1886
  // src/server/create.ts
1613
1887
  var DEFAULT_OPTIONS = {
1614
1888
  port: 3e3,
@@ -1639,7 +1913,9 @@ function createServerOptions(options = {}) {
1639
1913
  function createListenMethod(serverInstance, validatedOptions, initialMiddleware, initialPlugins) {
1640
1914
  return async () => {
1641
1915
  await initializeComponents(serverInstance, initialMiddleware, initialPlugins);
1916
+ await serverInstance.pluginManager.initializePlugins(serverInstance);
1642
1917
  await startServer(serverInstance, validatedOptions);
1918
+ await serverInstance.pluginManager.onServerStart(serverInstance, serverInstance.server);
1643
1919
  setupServerLifecycle(serverInstance);
1644
1920
  return serverInstance;
1645
1921
  };
@@ -1685,13 +1961,6 @@ function createRegisterMethod(serverInstance) {
1685
1961
  return serverInstance;
1686
1962
  };
1687
1963
  }
1688
- function validatePlugin(plugin) {
1689
- if (!plugin || typeof plugin !== "object" || !("register" in plugin) || typeof plugin.register !== "function") {
1690
- throw new Error(
1691
- "Invalid plugin. Must be a valid BlaizeJS plugin object with a register method."
1692
- );
1693
- }
1694
- }
1695
1964
  function create3(options = {}) {
1696
1965
  const mergedOptions = createServerOptions(options);
1697
1966
  let validatedOptions;
@@ -1711,6 +1980,10 @@ function create3(options = {}) {
1711
1980
  routesDir: validatedOptions.routesDir,
1712
1981
  watchMode: process.env.NODE_ENV === "development"
1713
1982
  });
1983
+ const pluginManager = createPluginLifecycleManager({
1984
+ debug: process.env.NODE_ENV === "development",
1985
+ continueOnError: true
1986
+ });
1714
1987
  const events = new EventEmitter();
1715
1988
  const serverInstance = {
1716
1989
  server: null,
@@ -1727,7 +2000,8 @@ function create3(options = {}) {
1727
2000
  listen: async () => serverInstance,
1728
2001
  close: async () => {
1729
2002
  },
1730
- router
2003
+ router,
2004
+ pluginManager
1731
2005
  };
1732
2006
  serverInstance.listen = createListenMethod(
1733
2007
  serverInstance,