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.cjs CHANGED
@@ -132,8 +132,31 @@ function compose(middlewareStack) {
132
132
  }
133
133
 
134
134
  // src/plugins/create.ts
135
- function create2(name, version, setup, _defaultOptions = {}) {
136
- throw new Error("Plugin system not yet available");
135
+ function create2(name, version, setup, defaultOptions = {}) {
136
+ if (!name || typeof name !== "string") {
137
+ throw new Error("Plugin name must be a non-empty string");
138
+ }
139
+ if (!version || typeof version !== "string") {
140
+ throw new Error("Plugin version must be a non-empty string");
141
+ }
142
+ if (typeof setup !== "function") {
143
+ throw new Error("Plugin setup must be a function");
144
+ }
145
+ return function pluginFactory(userOptions) {
146
+ const mergedOptions = { ...defaultOptions, ...userOptions };
147
+ const plugin = {
148
+ name,
149
+ version,
150
+ // The register hook calls the user's setup function
151
+ register: async (app) => {
152
+ const result = await setup(app, mergedOptions);
153
+ if (result && typeof result === "object") {
154
+ Object.assign(plugin, result);
155
+ }
156
+ }
157
+ };
158
+ return plugin;
159
+ };
137
160
  }
138
161
 
139
162
  // src/router/discovery/finder.ts
@@ -173,12 +196,18 @@ async function findRouteFiles(routesDir, options = {}) {
173
196
  return routeFiles;
174
197
  }
175
198
  function isRouteFile(filename) {
176
- return !filename.startsWith("_") && (filename.endsWith(".ts") || filename.endsWith(".js")) && filename !== "index.ts" && filename !== "index.js";
199
+ return !filename.startsWith("_") && (filename.endsWith(".ts") || filename.endsWith(".js"));
177
200
  }
178
201
 
179
202
  // src/router/discovery/parser.ts
180
203
  var path2 = __toESM(require("path"), 1);
181
204
  function parseRoutePath(filePath, basePath) {
205
+ if (filePath.startsWith("file://")) {
206
+ filePath = filePath.replace("file://", "");
207
+ }
208
+ if (basePath.startsWith("file://")) {
209
+ basePath = basePath.replace("file://", "");
210
+ }
182
211
  const forwardSlashFilePath = filePath.replace(/\\/g, "/");
183
212
  const forwardSlashBasePath = basePath.replace(/\\/g, "/");
184
213
  const normalizedBasePath = forwardSlashBasePath.endsWith("/") ? forwardSlashBasePath : `${forwardSlashBasePath}/`;
@@ -712,21 +741,56 @@ function createRouter(options) {
712
741
  const matcher = createMatcher();
713
742
  let initialized = false;
714
743
  let initializationPromise = null;
715
- let _watcher = null;
744
+ let _watchers = null;
745
+ const routeSources = /* @__PURE__ */ new Map();
746
+ const routeDirectories = /* @__PURE__ */ new Set([routerOptions.routesDir]);
747
+ function addRouteWithSource(route, source) {
748
+ const existingSources = routeSources.get(route.path) || [];
749
+ if (existingSources.includes(source)) {
750
+ console.warn(`Skipping duplicate route: ${route.path} from ${source}`);
751
+ return;
752
+ }
753
+ if (existingSources.length > 0) {
754
+ const conflictError = new Error(
755
+ `Route conflict for path "${route.path}": already defined in ${existingSources.join(", ")}, now being added from ${source}`
756
+ );
757
+ console.error(conflictError.message);
758
+ throw conflictError;
759
+ }
760
+ routeSources.set(route.path, [...existingSources, source]);
761
+ addRouteInternal(route);
762
+ }
763
+ async function loadRoutesFromDirectory(directory, source, prefix) {
764
+ try {
765
+ const discoveredRoutes = await findRoutes(directory, {
766
+ basePath: routerOptions.basePath
767
+ });
768
+ for (const route of discoveredRoutes) {
769
+ const finalRoute = prefix ? {
770
+ ...route,
771
+ path: `${prefix}${route.path}`
772
+ } : route;
773
+ addRouteWithSource(finalRoute, source);
774
+ }
775
+ console.log(
776
+ `Loaded ${discoveredRoutes.length} routes from ${source}${prefix ? ` with prefix ${prefix}` : ""}`
777
+ );
778
+ } catch (error) {
779
+ console.error(`Failed to load routes from ${source}:`, error);
780
+ throw error;
781
+ }
782
+ }
716
783
  async function initialize() {
717
784
  if (initialized || initializationPromise) {
718
785
  return initializationPromise;
719
786
  }
720
787
  initializationPromise = (async () => {
721
788
  try {
722
- const discoveredRoutes = await findRoutes(routerOptions.routesDir, {
723
- basePath: routerOptions.basePath
724
- });
725
- for (const route of discoveredRoutes) {
726
- addRouteInternal(route);
789
+ for (const directory of routeDirectories) {
790
+ await loadRoutesFromDirectory(directory, directory);
727
791
  }
728
792
  if (routerOptions.watchMode) {
729
- setupWatcher();
793
+ setupWatcherForAllDirectories();
730
794
  }
731
795
  initialized = true;
732
796
  } catch (error) {
@@ -743,47 +807,81 @@ function createRouter(options) {
743
807
  matcher.add(route.path, method, methodOptions);
744
808
  });
745
809
  }
746
- function setupWatcher() {
747
- _watcher = watchRoutes(routerOptions.routesDir, {
748
- ignore: ["node_modules", ".git"],
810
+ function createWatcherCallbacks(directory, source, prefix) {
811
+ return {
749
812
  onRouteAdded: (addedRoutes) => {
750
813
  console.log(
751
- `${addedRoutes.length} route(s) added:`,
814
+ `${addedRoutes.length} route(s) added from ${directory}:`,
752
815
  addedRoutes.map((r) => r.path)
753
816
  );
754
- addedRoutes.forEach((route) => addRouteInternal(route));
817
+ addedRoutes.forEach((route) => {
818
+ const finalRoute = prefix ? { ...route, path: `${prefix}${route.path}` } : route;
819
+ addRouteWithSource(finalRoute, source);
820
+ });
755
821
  },
756
822
  onRouteChanged: (changedRoutes) => {
757
823
  console.log(
758
- `${changedRoutes.length} route(s) changed:`,
824
+ `${changedRoutes.length} route(s) changed in ${directory}:`,
759
825
  changedRoutes.map((r) => r.path)
760
826
  );
761
827
  changedRoutes.forEach((route) => {
762
- const index = routes.findIndex((r) => r.path === route.path);
828
+ const finalPath = prefix ? `${prefix}${route.path}` : route.path;
829
+ const index = routes.findIndex((r) => r.path === finalPath);
763
830
  if (index >= 0) {
764
831
  routes.splice(index, 1);
832
+ const sources = routeSources.get(finalPath) || [];
833
+ const filteredSources = sources.filter((s) => s !== source);
834
+ if (filteredSources.length > 0) {
835
+ routeSources.set(finalPath, filteredSources);
836
+ } else {
837
+ routeSources.delete(finalPath);
838
+ }
765
839
  }
766
- addRouteInternal(route);
840
+ const finalRoute = prefix ? { ...route, path: finalPath } : route;
841
+ addRouteWithSource(finalRoute, source);
767
842
  });
768
843
  },
769
844
  onRouteRemoved: (filePath, removedRoutes) => {
770
- console.log("-----------------------Routes before removal:", routes);
771
845
  console.log(
772
- `File removed: ${filePath} with ${removedRoutes.length} route(s):`,
846
+ `File removed from ${directory}: ${filePath} with ${removedRoutes.length} route(s):`,
773
847
  removedRoutes.map((r) => r.path)
774
848
  );
775
849
  removedRoutes.forEach((route) => {
776
- const index = routes.findIndex((r) => r.path === route.path);
850
+ const finalPath = prefix ? `${prefix}${route.path}` : route.path;
851
+ const index = routes.findIndex((r) => r.path === finalPath);
777
852
  if (index >= 0) {
778
853
  routes.splice(index, 1);
779
854
  }
855
+ const sources = routeSources.get(finalPath) || [];
856
+ const filteredSources = sources.filter((s) => s !== source);
857
+ if (filteredSources.length > 0) {
858
+ routeSources.set(finalPath, filteredSources);
859
+ } else {
860
+ routeSources.delete(finalPath);
861
+ }
780
862
  });
781
- console.log("-----------------------Routes after removal:", routes);
782
863
  },
783
864
  onError: (error) => {
784
- console.error("Route watcher error:", error);
865
+ console.error(`Route watcher error for ${directory}:`, error);
785
866
  }
867
+ };
868
+ }
869
+ function setupWatcherForDirectory(directory, source, prefix) {
870
+ const callbacks = createWatcherCallbacks(directory, source, prefix);
871
+ const watcher = watchRoutes(directory, {
872
+ ignore: ["node_modules", ".git"],
873
+ ...callbacks
786
874
  });
875
+ if (!_watchers) {
876
+ _watchers = /* @__PURE__ */ new Map();
877
+ }
878
+ _watchers.set(directory, watcher);
879
+ return watcher;
880
+ }
881
+ function setupWatcherForAllDirectories() {
882
+ for (const directory of routeDirectories) {
883
+ setupWatcherForDirectory(directory, directory);
884
+ }
787
885
  }
788
886
  initialize().catch((error) => {
789
887
  console.error("Failed to initialize router on creation:", error);
@@ -833,6 +931,34 @@ function createRouter(options) {
833
931
  */
834
932
  addRoute(route) {
835
933
  addRouteInternal(route);
934
+ },
935
+ /**
936
+ * Add a route directory (for plugins)
937
+ */
938
+ async addRouteDirectory(directory, options2 = {}) {
939
+ if (routeDirectories.has(directory)) {
940
+ console.warn(`Route directory ${directory} already registered`);
941
+ return;
942
+ }
943
+ routeDirectories.add(directory);
944
+ if (initialized) {
945
+ await loadRoutesFromDirectory(directory, directory, options2.prefix);
946
+ if (routerOptions.watchMode) {
947
+ setupWatcherForDirectory(directory, directory, options2.prefix);
948
+ }
949
+ }
950
+ },
951
+ /**
952
+ * Get route conflicts
953
+ */
954
+ getRouteConflicts() {
955
+ const conflicts = [];
956
+ for (const [path5, sources] of routeSources.entries()) {
957
+ if (sources.length > 1) {
958
+ conflicts.push({ path: path5, sources });
959
+ }
960
+ }
961
+ return conflicts;
836
962
  }
837
963
  };
838
964
  }
@@ -855,7 +981,7 @@ function getCallerFilePath() {
855
981
  try {
856
982
  Error.prepareStackTrace = (_, stack2) => stack2;
857
983
  const stack = new Error().stack;
858
- const callerFrame = stack[2];
984
+ const callerFrame = stack[3];
859
985
  if (!callerFrame || typeof callerFrame.getFileName !== "function") {
860
986
  throw new Error("Unable to determine caller file frame");
861
987
  }
@@ -869,9 +995,11 @@ function getCallerFilePath() {
869
995
  }
870
996
  }
871
997
  function getRoutePath() {
998
+ console.log("getRoutePath called");
872
999
  const callerPath = getCallerFilePath();
873
1000
  const routesDir = getRoutesDir();
874
1001
  const parsedRoute = parseRoutePath(callerPath, routesDir);
1002
+ console.log(`Parsed route path: ${parsedRoute.routePath} from file: ${callerPath}`);
875
1003
  return parsedRoute.routePath;
876
1004
  }
877
1005
  var createGetRoute = (config2) => {
@@ -1560,12 +1688,12 @@ async function stopServer(serverInstance, options = {}) {
1560
1688
  return;
1561
1689
  }
1562
1690
  const timeout = options.timeout || 3e4;
1563
- const plugins = serverInstance.plugins;
1564
1691
  try {
1565
1692
  if (options.onStopping) {
1566
1693
  await options.onStopping();
1567
1694
  }
1568
1695
  events.emit("stopping");
1696
+ await serverInstance.pluginManager.onServerStop(serverInstance, server);
1569
1697
  const timeoutPromise = new Promise((_, reject) => {
1570
1698
  setTimeout(() => {
1571
1699
  reject(new Error("Server shutdown timed out waiting for requests to complete"));
@@ -1580,13 +1708,7 @@ async function stopServer(serverInstance, options = {}) {
1580
1708
  });
1581
1709
  });
1582
1710
  await Promise.race([closePromise, timeoutPromise]);
1583
- if (plugins?.length) {
1584
- for (const plugin of [...plugins].reverse()) {
1585
- if (plugin.terminate) {
1586
- await plugin.terminate();
1587
- }
1588
- }
1589
- }
1711
+ await serverInstance.pluginManager.terminatePlugins(serverInstance);
1590
1712
  if (options.onStopped) {
1591
1713
  await options.onStopped();
1592
1714
  }
@@ -1661,6 +1783,158 @@ function validateServerOptions(options) {
1661
1783
  }
1662
1784
  }
1663
1785
 
1786
+ // src/plugins/lifecycle.ts
1787
+ function createPluginLifecycleManager(options = {}) {
1788
+ const { continueOnError = true, debug = false, onError } = options;
1789
+ function log(message, ...args) {
1790
+ if (debug) {
1791
+ console.log(`[PluginLifecycle] ${message}`, ...args);
1792
+ }
1793
+ }
1794
+ function handleError(plugin, phase, error) {
1795
+ const errorMessage = `Plugin ${plugin.name} failed during ${phase}: ${error.message}`;
1796
+ if (onError) {
1797
+ onError(plugin, phase, error);
1798
+ } else {
1799
+ console.error(errorMessage, error);
1800
+ }
1801
+ if (!continueOnError) {
1802
+ throw new Error(errorMessage);
1803
+ }
1804
+ }
1805
+ return {
1806
+ /**
1807
+ * Initialize all plugins
1808
+ */
1809
+ async initializePlugins(server) {
1810
+ log("Initializing plugins...");
1811
+ for (const plugin of server.plugins) {
1812
+ if (plugin.initialize) {
1813
+ try {
1814
+ log(`Initializing plugin: ${plugin.name}`);
1815
+ await plugin.initialize(server);
1816
+ } catch (error) {
1817
+ handleError(plugin, "initialize", error);
1818
+ }
1819
+ }
1820
+ }
1821
+ log(`Initialized ${server.plugins.length} plugins`);
1822
+ },
1823
+ /**
1824
+ * Terminate all plugins in reverse order
1825
+ */
1826
+ async terminatePlugins(server) {
1827
+ log("Terminating plugins...");
1828
+ const pluginsToTerminate = [...server.plugins].reverse();
1829
+ for (const plugin of pluginsToTerminate) {
1830
+ if (plugin.terminate) {
1831
+ try {
1832
+ log(`Terminating plugin: ${plugin.name}`);
1833
+ await plugin.terminate(server);
1834
+ } catch (error) {
1835
+ handleError(plugin, "terminate", error);
1836
+ }
1837
+ }
1838
+ }
1839
+ log(`Terminated ${pluginsToTerminate.length} plugins`);
1840
+ },
1841
+ /**
1842
+ * Notify plugins that the server has started
1843
+ */
1844
+ async onServerStart(server, httpServer) {
1845
+ log("Notifying plugins of server start...");
1846
+ for (const plugin of server.plugins) {
1847
+ if (plugin.onServerStart) {
1848
+ try {
1849
+ log(`Notifying plugin of server start: ${plugin.name}`);
1850
+ await plugin.onServerStart(httpServer);
1851
+ } catch (error) {
1852
+ handleError(plugin, "onServerStart", error);
1853
+ }
1854
+ }
1855
+ }
1856
+ },
1857
+ /**
1858
+ * Notify plugins that the server is stopping
1859
+ */
1860
+ async onServerStop(server, httpServer) {
1861
+ log("Notifying plugins of server stop...");
1862
+ const pluginsToNotify = [...server.plugins].reverse();
1863
+ for (const plugin of pluginsToNotify) {
1864
+ if (plugin.onServerStop) {
1865
+ try {
1866
+ log(`Notifying plugin of server stop: ${plugin.name}`);
1867
+ await plugin.onServerStop(httpServer);
1868
+ } catch (error) {
1869
+ handleError(plugin, "onServerStop", error);
1870
+ }
1871
+ }
1872
+ }
1873
+ }
1874
+ };
1875
+ }
1876
+
1877
+ // src/plugins/errors.ts
1878
+ var PluginValidationError = class extends Error {
1879
+ constructor(pluginName, message) {
1880
+ super(`Plugin validation error${pluginName ? ` for "${pluginName}"` : ""}: ${message}`);
1881
+ this.pluginName = pluginName;
1882
+ this.name = "PluginValidationError";
1883
+ }
1884
+ };
1885
+
1886
+ // src/plugins/validation.ts
1887
+ var RESERVED_NAMES = /* @__PURE__ */ new Set([
1888
+ "core",
1889
+ "server",
1890
+ "router",
1891
+ "middleware",
1892
+ "context",
1893
+ "blaize",
1894
+ "blaizejs"
1895
+ ]);
1896
+ var VALID_NAME_PATTERN = /^[a-z]([a-z0-9-]*[a-z0-9])?$/;
1897
+ var VALID_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[a-zA-Z0-9-.]+)?(?:\+[a-zA-Z0-9-.]+)?$/;
1898
+ function validatePlugin(plugin, options = {}) {
1899
+ const { requireVersion = true, validateNameFormat = true, checkReservedNames = true } = options;
1900
+ if (!plugin || typeof plugin !== "object") {
1901
+ throw new PluginValidationError("", "Plugin must be an object");
1902
+ }
1903
+ const p = plugin;
1904
+ if (!p.name || typeof p.name !== "string") {
1905
+ throw new PluginValidationError("", "Plugin must have a name (string)");
1906
+ }
1907
+ if (validateNameFormat && !VALID_NAME_PATTERN.test(p.name)) {
1908
+ throw new PluginValidationError(
1909
+ p.name,
1910
+ "Plugin name must be lowercase letters, numbers, and hyphens only"
1911
+ );
1912
+ }
1913
+ if (checkReservedNames && RESERVED_NAMES.has(p.name.toLowerCase())) {
1914
+ throw new PluginValidationError(p.name, `Plugin name "${p.name}" is reserved`);
1915
+ }
1916
+ if (requireVersion) {
1917
+ if (!p.version || typeof p.version !== "string") {
1918
+ throw new PluginValidationError(p.name, "Plugin must have a version (string)");
1919
+ }
1920
+ if (!VALID_VERSION_PATTERN.test(p.version)) {
1921
+ throw new PluginValidationError(
1922
+ p.name,
1923
+ 'Plugin version must follow semantic versioning (e.g., "1.0.0")'
1924
+ );
1925
+ }
1926
+ }
1927
+ if (!p.register || typeof p.register !== "function") {
1928
+ throw new PluginValidationError(p.name, "Plugin must have a register method (function)");
1929
+ }
1930
+ const lifecycleMethods = ["initialize", "terminate", "onServerStart", "onServerStop"];
1931
+ for (const method of lifecycleMethods) {
1932
+ if (p[method] && typeof p[method] !== "function") {
1933
+ throw new PluginValidationError(p.name, `Plugin ${method} must be a function if provided`);
1934
+ }
1935
+ }
1936
+ }
1937
+
1664
1938
  // src/server/create.ts
1665
1939
  var DEFAULT_OPTIONS = {
1666
1940
  port: 3e3,
@@ -1691,7 +1965,9 @@ function createServerOptions(options = {}) {
1691
1965
  function createListenMethod(serverInstance, validatedOptions, initialMiddleware, initialPlugins) {
1692
1966
  return async () => {
1693
1967
  await initializeComponents(serverInstance, initialMiddleware, initialPlugins);
1968
+ await serverInstance.pluginManager.initializePlugins(serverInstance);
1694
1969
  await startServer(serverInstance, validatedOptions);
1970
+ await serverInstance.pluginManager.onServerStart(serverInstance, serverInstance.server);
1695
1971
  setupServerLifecycle(serverInstance);
1696
1972
  return serverInstance;
1697
1973
  };
@@ -1737,13 +2013,6 @@ function createRegisterMethod(serverInstance) {
1737
2013
  return serverInstance;
1738
2014
  };
1739
2015
  }
1740
- function validatePlugin(plugin) {
1741
- if (!plugin || typeof plugin !== "object" || !("register" in plugin) || typeof plugin.register !== "function") {
1742
- throw new Error(
1743
- "Invalid plugin. Must be a valid BlaizeJS plugin object with a register method."
1744
- );
1745
- }
1746
- }
1747
2016
  function create3(options = {}) {
1748
2017
  const mergedOptions = createServerOptions(options);
1749
2018
  let validatedOptions;
@@ -1763,6 +2032,10 @@ function create3(options = {}) {
1763
2032
  routesDir: validatedOptions.routesDir,
1764
2033
  watchMode: process.env.NODE_ENV === "development"
1765
2034
  });
2035
+ const pluginManager = createPluginLifecycleManager({
2036
+ debug: process.env.NODE_ENV === "development",
2037
+ continueOnError: true
2038
+ });
1766
2039
  const events = new import_node_events.default();
1767
2040
  const serverInstance = {
1768
2041
  server: null,
@@ -1779,7 +2052,8 @@ function create3(options = {}) {
1779
2052
  listen: async () => serverInstance,
1780
2053
  close: async () => {
1781
2054
  },
1782
- router
2055
+ router,
2056
+ pluginManager
1783
2057
  };
1784
2058
  serverInstance.listen = createListenMethod(
1785
2059
  serverInstance,