@harpy-js/core 0.4.7

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 (147) hide show
  1. package/README.md +326 -0
  2. package/dist/cli.d.ts +12 -0
  3. package/dist/cli.js +53 -0
  4. package/dist/client/Link.d.ts +5 -0
  5. package/dist/client/Link.js +62 -0
  6. package/dist/client/__tests__/getActiveItemId.test.d.ts +1 -0
  7. package/dist/client/__tests__/getActiveItemId.test.js +38 -0
  8. package/dist/client/getActiveItemId.d.ts +7 -0
  9. package/dist/client/getActiveItemId.js +55 -0
  10. package/dist/client/use-i18n.d.ts +7 -0
  11. package/dist/client/use-i18n.js +64 -0
  12. package/dist/core/__tests__/component-analyzer.test.d.ts +1 -0
  13. package/dist/core/__tests__/component-analyzer.test.js +151 -0
  14. package/dist/core/__tests__/hydration-manifest.test.d.ts +1 -0
  15. package/dist/core/__tests__/hydration-manifest.test.js +211 -0
  16. package/dist/core/__tests__/jsx.engine.test.d.ts +1 -0
  17. package/dist/core/__tests__/jsx.engine.test.js +118 -0
  18. package/dist/core/app-setup.d.ts +7 -0
  19. package/dist/core/app-setup.js +79 -0
  20. package/dist/core/auto-register.module.d.ts +9 -0
  21. package/dist/core/auto-register.module.js +18 -0
  22. package/dist/core/auto-wrap-middleware.d.ts +4 -0
  23. package/dist/core/auto-wrap-middleware.js +130 -0
  24. package/dist/core/client-component-wrapper.d.ts +5 -0
  25. package/dist/core/client-component-wrapper.js +37 -0
  26. package/dist/core/client-hydration.d.ts +2 -0
  27. package/dist/core/client-hydration.js +93 -0
  28. package/dist/core/client-wrapper-browser.d.ts +2 -0
  29. package/dist/core/client-wrapper-browser.js +22 -0
  30. package/dist/core/component-analyzer.d.ts +4 -0
  31. package/dist/core/component-analyzer.js +98 -0
  32. package/dist/core/component-auto-wrapper.d.ts +2 -0
  33. package/dist/core/component-auto-wrapper.js +63 -0
  34. package/dist/core/component-client-wrapper.d.ts +4 -0
  35. package/dist/core/component-client-wrapper.js +80 -0
  36. package/dist/core/hydration-generator.d.ts +2 -0
  37. package/dist/core/hydration-generator.js +98 -0
  38. package/dist/core/hydration-manifest.d.ts +7 -0
  39. package/dist/core/hydration-manifest.js +83 -0
  40. package/dist/core/hydration.d.ts +16 -0
  41. package/dist/core/hydration.js +72 -0
  42. package/dist/core/jsx.engine.d.ts +9 -0
  43. package/dist/core/jsx.engine.js +161 -0
  44. package/dist/core/live-reload-client.js +32 -0
  45. package/dist/core/live-reload.controller.d.ts +10 -0
  46. package/dist/core/live-reload.controller.js +38 -0
  47. package/dist/core/navigation.service.d.ts +18 -0
  48. package/dist/core/navigation.service.js +206 -0
  49. package/dist/core/router.module.d.ts +2 -0
  50. package/dist/core/router.module.js +21 -0
  51. package/dist/core/static-assets.controller.d.ts +4 -0
  52. package/dist/core/static-assets.controller.js +51 -0
  53. package/dist/core/types/nav.types.d.ts +22 -0
  54. package/dist/core/types/nav.types.js +2 -0
  55. package/dist/core/views/layout.d.ts +8 -0
  56. package/dist/core/views/layout.js +35 -0
  57. package/dist/decorators/jsx.decorator.d.ts +26 -0
  58. package/dist/decorators/jsx.decorator.js +10 -0
  59. package/dist/decorators/layout.decorator.d.ts +4 -0
  60. package/dist/decorators/layout.decorator.js +29 -0
  61. package/dist/i18n/__tests__/i18n.helper.test.d.ts +1 -0
  62. package/dist/i18n/__tests__/i18n.helper.test.js +105 -0
  63. package/dist/i18n/__tests__/i18n.interceptor.test.d.ts +1 -0
  64. package/dist/i18n/__tests__/i18n.interceptor.test.js +195 -0
  65. package/dist/i18n/__tests__/i18n.module.test.d.ts +1 -0
  66. package/dist/i18n/__tests__/i18n.module.test.js +83 -0
  67. package/dist/i18n/__tests__/i18n.service.test.d.ts +1 -0
  68. package/dist/i18n/__tests__/i18n.service.test.js +109 -0
  69. package/dist/i18n/__tests__/t.test.d.ts +1 -0
  70. package/dist/i18n/__tests__/t.test.js +66 -0
  71. package/dist/i18n/i18n-module.options.d.ts +10 -0
  72. package/dist/i18n/i18n-module.options.js +4 -0
  73. package/dist/i18n/i18n-switcher.controller.d.ts +12 -0
  74. package/dist/i18n/i18n-switcher.controller.js +80 -0
  75. package/dist/i18n/i18n-types.d.ts +8 -0
  76. package/dist/i18n/i18n-types.js +2 -0
  77. package/dist/i18n/i18n.helper.d.ts +14 -0
  78. package/dist/i18n/i18n.helper.js +70 -0
  79. package/dist/i18n/i18n.interceptor.d.ts +9 -0
  80. package/dist/i18n/i18n.interceptor.js +99 -0
  81. package/dist/i18n/i18n.module.d.ts +5 -0
  82. package/dist/i18n/i18n.module.js +51 -0
  83. package/dist/i18n/i18n.service.d.ts +12 -0
  84. package/dist/i18n/i18n.service.js +61 -0
  85. package/dist/i18n/index.d.ts +10 -0
  86. package/dist/i18n/index.js +20 -0
  87. package/dist/i18n/locale.decorator.d.ts +1 -0
  88. package/dist/i18n/locale.decorator.js +8 -0
  89. package/dist/i18n/t.d.ts +3 -0
  90. package/dist/i18n/t.js +16 -0
  91. package/dist/index.d.ts +19 -0
  92. package/dist/index.js +40 -0
  93. package/package.json +79 -0
  94. package/scripts/analyze-styles.ts +124 -0
  95. package/scripts/auto-wrap-exports.ts +239 -0
  96. package/scripts/build-css.ts +38 -0
  97. package/scripts/build-hydration.ts +313 -0
  98. package/scripts/build-page-styles.ts +43 -0
  99. package/scripts/copy-assets.ts +34 -0
  100. package/scripts/dev.sh +3 -0
  101. package/scripts/dev.ts +257 -0
  102. package/src/cli.ts +71 -0
  103. package/src/client/Link.tsx +62 -0
  104. package/src/client/__tests__/getActiveItemId.test.ts +49 -0
  105. package/src/client/getActiveItemId.ts +54 -0
  106. package/src/client/use-i18n.ts +111 -0
  107. package/src/core/__tests__/component-analyzer.test.ts +141 -0
  108. package/src/core/__tests__/hydration-manifest.test.ts +223 -0
  109. package/src/core/__tests__/jsx.engine.test.ts +137 -0
  110. package/src/core/app-setup.ts +114 -0
  111. package/src/core/auto-register.module.ts +30 -0
  112. package/src/core/auto-wrap-middleware.ts +165 -0
  113. package/src/core/client-component-wrapper.ts +72 -0
  114. package/src/core/client-hydration.tsx +99 -0
  115. package/src/core/client-wrapper-browser.ts +40 -0
  116. package/src/core/component-analyzer.ts +89 -0
  117. package/src/core/component-auto-wrapper.ts +68 -0
  118. package/src/core/component-client-wrapper.ts +112 -0
  119. package/src/core/hydration-generator.ts +94 -0
  120. package/src/core/hydration-manifest.ts +79 -0
  121. package/src/core/hydration.ts +70 -0
  122. package/src/core/jsx.engine.ts +205 -0
  123. package/src/core/live-reload-client.js +32 -0
  124. package/src/core/live-reload.controller.ts +55 -0
  125. package/src/core/navigation.service.ts +257 -0
  126. package/src/core/router.module.ts +9 -0
  127. package/src/core/static-assets.controller.ts +19 -0
  128. package/src/core/types/nav.types.ts +53 -0
  129. package/src/core/views/layout.tsx +61 -0
  130. package/src/decorators/jsx.decorator.ts +49 -0
  131. package/src/decorators/layout.decorator.ts +66 -0
  132. package/src/i18n/__tests__/i18n.helper.test.ts +126 -0
  133. package/src/i18n/__tests__/i18n.interceptor.test.ts +229 -0
  134. package/src/i18n/__tests__/i18n.module.test.ts +98 -0
  135. package/src/i18n/__tests__/i18n.service.test.ts +129 -0
  136. package/src/i18n/__tests__/t.test.ts +88 -0
  137. package/src/i18n/i18n-module.options.ts +53 -0
  138. package/src/i18n/i18n-switcher.controller.ts +99 -0
  139. package/src/i18n/i18n-types.ts +56 -0
  140. package/src/i18n/i18n.helper.ts +75 -0
  141. package/src/i18n/i18n.interceptor.ts +114 -0
  142. package/src/i18n/i18n.module.ts +45 -0
  143. package/src/i18n/i18n.service.ts +95 -0
  144. package/src/i18n/index.ts +37 -0
  145. package/src/i18n/locale.decorator.ts +10 -0
  146. package/src/i18n/t.ts +62 -0
  147. package/src/index.ts +31 -0
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.I18N_MODULE_OPTIONS = exports.tUnsafe = exports.t = exports.CurrentLocale = exports.I18nSwitcherController = exports.I18nHelper = exports.I18nInterceptor = exports.I18nService = exports.I18nModule = void 0;
4
+ var i18n_module_1 = require("./i18n.module");
5
+ Object.defineProperty(exports, "I18nModule", { enumerable: true, get: function () { return i18n_module_1.I18nModule; } });
6
+ var i18n_service_1 = require("./i18n.service");
7
+ Object.defineProperty(exports, "I18nService", { enumerable: true, get: function () { return i18n_service_1.I18nService; } });
8
+ var i18n_interceptor_1 = require("./i18n.interceptor");
9
+ Object.defineProperty(exports, "I18nInterceptor", { enumerable: true, get: function () { return i18n_interceptor_1.I18nInterceptor; } });
10
+ var i18n_helper_1 = require("./i18n.helper");
11
+ Object.defineProperty(exports, "I18nHelper", { enumerable: true, get: function () { return i18n_helper_1.I18nHelper; } });
12
+ var i18n_switcher_controller_1 = require("./i18n-switcher.controller");
13
+ Object.defineProperty(exports, "I18nSwitcherController", { enumerable: true, get: function () { return i18n_switcher_controller_1.I18nSwitcherController; } });
14
+ var locale_decorator_1 = require("./locale.decorator");
15
+ Object.defineProperty(exports, "CurrentLocale", { enumerable: true, get: function () { return locale_decorator_1.CurrentLocale; } });
16
+ var t_1 = require("./t");
17
+ Object.defineProperty(exports, "t", { enumerable: true, get: function () { return t_1.t; } });
18
+ Object.defineProperty(exports, "tUnsafe", { enumerable: true, get: function () { return t_1.tUnsafe; } });
19
+ var i18n_module_options_1 = require("./i18n-module.options");
20
+ Object.defineProperty(exports, "I18N_MODULE_OPTIONS", { enumerable: true, get: function () { return i18n_module_options_1.I18N_MODULE_OPTIONS; } });
@@ -0,0 +1 @@
1
+ export declare const CurrentLocale: (...dataOrPipes: unknown[]) => ParameterDecorator;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CurrentLocale = void 0;
4
+ const common_1 = require("@nestjs/common");
5
+ exports.CurrentLocale = (0, common_1.createParamDecorator)((data, ctx) => {
6
+ const request = ctx.switchToHttp().getRequest();
7
+ return request.locale;
8
+ });
@@ -0,0 +1,3 @@
1
+ import type { NestedKeyOf, DeepValue, ExtractVariables } from "./i18n-types";
2
+ export declare function t<TDict extends Record<string, any>, TKey extends NestedKeyOf<TDict>>(dict: TDict, key: TKey, vars?: DeepValue<TDict, TKey> extends string ? ExtractVariables<DeepValue<TDict, TKey>> : Record<string, string | number>): string;
3
+ export declare function tUnsafe(dict: Record<string, any>, key: string, vars?: Record<string, string | number>): string;
package/dist/i18n/t.js ADDED
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.t = t;
4
+ exports.tUnsafe = tUnsafe;
5
+ function t(dict, key, vars) {
6
+ const value = key.split(".").reduce((acc, k) => acc?.[k], dict);
7
+ if (typeof value !== "string")
8
+ return "";
9
+ return value.replace(/\{\{(.*?)\}\}/g, (_match, k) => String(vars?.[k.trim()] ?? ""));
10
+ }
11
+ function tUnsafe(dict, key, vars) {
12
+ const value = key.split(".").reduce((acc, k) => acc?.[k], dict);
13
+ if (typeof value !== "string")
14
+ return "";
15
+ return value.replace(/\{\{(.*?)\}\}/g, (_match, k) => String(vars?.[k.trim()] ?? ""));
16
+ }
@@ -0,0 +1,19 @@
1
+ export { autoWrapClientComponent } from "./core/client-component-wrapper";
2
+ export { hydrationContext, initializeHydrationContext } from "./core/hydration";
3
+ export { getChunkPath, getHydrationManifest } from "./core/hydration-manifest";
4
+ export { withJsxEngine } from "./core/jsx.engine";
5
+ export { LiveReloadController } from "./core/live-reload.controller";
6
+ export { StaticAssetsController } from "./core/static-assets.controller";
7
+ export { JsxRender } from "./decorators/jsx.decorator";
8
+ export { WithLayout } from "./decorators/layout.decorator";
9
+ export type { MetaOptions, RenderOptions } from "./decorators/jsx.decorator";
10
+ export type { JsxLayout, JsxLayoutProps } from "./core/jsx.engine";
11
+ export { RouterModule } from "./core/router.module";
12
+ export { NavigationService } from "./core/navigation.service";
13
+ export { AutoRegisterModule } from "./core/auto-register.module";
14
+ export type { NavItem, NavSection } from "./core/types/nav.types";
15
+ export type { NavigationRegistry } from "./core/types/nav.types";
16
+ export { configureHarpyApp, HarpyAppOptions } from "./core/app-setup";
17
+ export { setupHarpyApp } from "./core/app-setup";
18
+ export { default as Link } from "./client/Link";
19
+ export { buildHrefIndex, getActiveItemIdFromIndex, getActiveItemIdFromManifest, } from "./client/getActiveItemId";
package/dist/index.js ADDED
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getActiveItemIdFromManifest = exports.getActiveItemIdFromIndex = exports.buildHrefIndex = exports.Link = exports.setupHarpyApp = exports.configureHarpyApp = exports.AutoRegisterModule = exports.NavigationService = exports.RouterModule = exports.WithLayout = exports.JsxRender = exports.StaticAssetsController = exports.LiveReloadController = exports.withJsxEngine = exports.getHydrationManifest = exports.getChunkPath = exports.initializeHydrationContext = exports.hydrationContext = exports.autoWrapClientComponent = void 0;
7
+ var client_component_wrapper_1 = require("./core/client-component-wrapper");
8
+ Object.defineProperty(exports, "autoWrapClientComponent", { enumerable: true, get: function () { return client_component_wrapper_1.autoWrapClientComponent; } });
9
+ var hydration_1 = require("./core/hydration");
10
+ Object.defineProperty(exports, "hydrationContext", { enumerable: true, get: function () { return hydration_1.hydrationContext; } });
11
+ Object.defineProperty(exports, "initializeHydrationContext", { enumerable: true, get: function () { return hydration_1.initializeHydrationContext; } });
12
+ var hydration_manifest_1 = require("./core/hydration-manifest");
13
+ Object.defineProperty(exports, "getChunkPath", { enumerable: true, get: function () { return hydration_manifest_1.getChunkPath; } });
14
+ Object.defineProperty(exports, "getHydrationManifest", { enumerable: true, get: function () { return hydration_manifest_1.getHydrationManifest; } });
15
+ var jsx_engine_1 = require("./core/jsx.engine");
16
+ Object.defineProperty(exports, "withJsxEngine", { enumerable: true, get: function () { return jsx_engine_1.withJsxEngine; } });
17
+ var live_reload_controller_1 = require("./core/live-reload.controller");
18
+ Object.defineProperty(exports, "LiveReloadController", { enumerable: true, get: function () { return live_reload_controller_1.LiveReloadController; } });
19
+ var static_assets_controller_1 = require("./core/static-assets.controller");
20
+ Object.defineProperty(exports, "StaticAssetsController", { enumerable: true, get: function () { return static_assets_controller_1.StaticAssetsController; } });
21
+ var jsx_decorator_1 = require("./decorators/jsx.decorator");
22
+ Object.defineProperty(exports, "JsxRender", { enumerable: true, get: function () { return jsx_decorator_1.JsxRender; } });
23
+ var layout_decorator_1 = require("./decorators/layout.decorator");
24
+ Object.defineProperty(exports, "WithLayout", { enumerable: true, get: function () { return layout_decorator_1.WithLayout; } });
25
+ var router_module_1 = require("./core/router.module");
26
+ Object.defineProperty(exports, "RouterModule", { enumerable: true, get: function () { return router_module_1.RouterModule; } });
27
+ var navigation_service_1 = require("./core/navigation.service");
28
+ Object.defineProperty(exports, "NavigationService", { enumerable: true, get: function () { return navigation_service_1.NavigationService; } });
29
+ var auto_register_module_1 = require("./core/auto-register.module");
30
+ Object.defineProperty(exports, "AutoRegisterModule", { enumerable: true, get: function () { return auto_register_module_1.AutoRegisterModule; } });
31
+ var app_setup_1 = require("./core/app-setup");
32
+ Object.defineProperty(exports, "configureHarpyApp", { enumerable: true, get: function () { return app_setup_1.configureHarpyApp; } });
33
+ var app_setup_2 = require("./core/app-setup");
34
+ Object.defineProperty(exports, "setupHarpyApp", { enumerable: true, get: function () { return app_setup_2.setupHarpyApp; } });
35
+ var Link_1 = require("./client/Link");
36
+ Object.defineProperty(exports, "Link", { enumerable: true, get: function () { return __importDefault(Link_1).default; } });
37
+ var getActiveItemId_1 = require("./client/getActiveItemId");
38
+ Object.defineProperty(exports, "buildHrefIndex", { enumerable: true, get: function () { return getActiveItemId_1.buildHrefIndex; } });
39
+ Object.defineProperty(exports, "getActiveItemIdFromIndex", { enumerable: true, get: function () { return getActiveItemId_1.getActiveItemIdFromIndex; } });
40
+ Object.defineProperty(exports, "getActiveItemIdFromManifest", { enumerable: true, get: function () { return getActiveItemId_1.getActiveItemIdFromManifest; } });
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@harpy-js/core",
3
+ "version": "0.4.7",
4
+ "description": "Harpy - A powerful NestJS + React/JSX SSR framework with automatic hydration and i18n support",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "./client": {
13
+ "types": "./dist/client/use-i18n.d.ts",
14
+ "default": "./dist/client/use-i18n.js"
15
+ },
16
+ "./client/getActiveItemId": {
17
+ "types": "./dist/client/getActiveItemId.d.ts",
18
+ "default": "./dist/client/getActiveItemId.js"
19
+ },
20
+ "./client/Link": {
21
+ "types": "./dist/client/Link.d.ts",
22
+ "default": "./dist/client/Link.js"
23
+ }
24
+ },
25
+ "bin": {
26
+ "harpy": "./dist/cli.js"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "scripts",
31
+ "src"
32
+ ],
33
+ "scripts": {
34
+ "build": "tsc && cp src/core/live-reload-client.js dist/core/",
35
+ "dev": "tsc --watch",
36
+ "test": "jest",
37
+ "test:watch": "jest --watch",
38
+ "test:coverage": "jest --coverage",
39
+ "prepublishOnly": "npm run build"
40
+ },
41
+ "keywords": [
42
+ "nestjs",
43
+ "react",
44
+ "jsx",
45
+ "ssr",
46
+ "hydration"
47
+ ],
48
+ "author": "",
49
+ "license": "MIT",
50
+ "peerDependencies": {
51
+ "@nestjs/common": "^11.0.0",
52
+ "@nestjs/core": "^11.0.0",
53
+ "@nestjs/platform-fastify": "^11.0.0",
54
+ "react": "^19.0.0",
55
+ "react-dom": "^19.0.0"
56
+ },
57
+ "dependencies": {
58
+ "@fastify/cookie": "^11.0.1",
59
+ "chokidar": "^3.6.0",
60
+ "esbuild": "^0.24.2",
61
+ "rxjs": "^7.8.1",
62
+ "tsx": "^4.20.6"
63
+ },
64
+ "devDependencies": {
65
+ "@nestjs/testing": "^11.0.0",
66
+ "@types/jest": "^29.5.0",
67
+ "@types/node": "^22.19.1",
68
+ "@types/react": "^19.2.2",
69
+ "@types/react-dom": "^19.2.2",
70
+ "fastify": "^5.6.2",
71
+ "jest": "^29.7.0",
72
+ "ts-jest": "^29.1.0",
73
+ "typescript": "^5.7.2"
74
+ },
75
+ "publishConfig": {
76
+ "access": "public"
77
+ },
78
+ "gitHead": "d7a4f2ad108c68f2b441ad0c24385b45b1e046a3"
79
+ }
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Analyze page components to determine which styles are used
4
+ * This helps identify page-specific vs common styles
5
+ */
6
+
7
+ import * as fs from "fs";
8
+ import * as path from "path";
9
+
10
+ const SRC_DIR = path.join(__dirname, "../src");
11
+ const FEATURES_DIR = path.join(SRC_DIR, "features");
12
+
13
+ interface PageInfo {
14
+ name: string;
15
+ viewFiles: string[];
16
+ classes: Set<string>;
17
+ }
18
+
19
+ // Extract Tailwind classes from file content
20
+ function extractClasses(content: string): Set<string> {
21
+ const classes = new Set<string>();
22
+ // Match className="..." and class="..."
23
+ const classMatches = content.match(/(?:className|class)="([^"]*)"/g) || [];
24
+
25
+ classMatches.forEach((match) => {
26
+ const classContent = match
27
+ .replace(/(?:className|class)="/, "")
28
+ .replace(/"$/, "");
29
+ const tokens = classContent.split(/\s+/);
30
+ tokens.forEach((token) => {
31
+ if (token.trim()) classes.add(token);
32
+ });
33
+ });
34
+
35
+ return classes;
36
+ }
37
+
38
+ // Find all pages and their view files
39
+ function analyzePages(): PageInfo[] {
40
+ const pages: PageInfo[] = [];
41
+
42
+ if (!fs.existsSync(FEATURES_DIR)) return pages;
43
+
44
+ const featureDirs = fs
45
+ .readdirSync(FEATURES_DIR)
46
+ .filter((f) => fs.statSync(path.join(FEATURES_DIR, f)).isDirectory());
47
+
48
+ featureDirs.forEach((featureName) => {
49
+ const viewsDir = path.join(FEATURES_DIR, featureName, "views");
50
+ if (fs.existsSync(viewsDir)) {
51
+ const viewFiles = fs
52
+ .readdirSync(viewsDir)
53
+ .filter((f) => f.endsWith(".tsx") || f.endsWith(".ts"))
54
+ .map((f) => path.join(viewsDir, f));
55
+
56
+ const classes = new Set<string>();
57
+ viewFiles.forEach((file) => {
58
+ const content = fs.readFileSync(file, "utf-8");
59
+ const fileClasses = extractClasses(content);
60
+ fileClasses.forEach((c) => classes.add(c));
61
+ });
62
+
63
+ pages.push({
64
+ name: featureName,
65
+ viewFiles,
66
+ classes,
67
+ });
68
+ }
69
+ });
70
+
71
+ return pages;
72
+ }
73
+
74
+ // Also extract classes from layout
75
+ function getLayoutClasses(): Set<string> {
76
+ const layoutFile = path.join(SRC_DIR, "core/views/layout.tsx");
77
+ if (fs.existsSync(layoutFile)) {
78
+ const content = fs.readFileSync(layoutFile, "utf-8");
79
+ return extractClasses(content);
80
+ }
81
+ return new Set();
82
+ }
83
+
84
+ function main() {
85
+ const pages = analyzePages();
86
+ const layoutClasses = getLayoutClasses();
87
+
88
+ console.log("📊 Page Style Analysis:\n");
89
+
90
+ pages.forEach((page) => {
91
+ console.log(`${page.name}: ${page.classes.size} classes`);
92
+ console.log(
93
+ ` Files: ${page.viewFiles.map((f) => path.basename(f)).join(", ")}`,
94
+ );
95
+ });
96
+
97
+ console.log(`\nlayout: ${layoutClasses.size} classes`);
98
+ console.log("\nCommon classes (used across multiple pages):");
99
+
100
+ const commonClasses = new Set<string>();
101
+ const classUsage = new Map<string, number>();
102
+
103
+ // Count usage across all pages and layout
104
+ layoutClasses.forEach((c) => classUsage.set(c, (classUsage.get(c) || 0) + 1));
105
+ pages.forEach((page) => {
106
+ page.classes.forEach((c) => {
107
+ classUsage.set(c, (classUsage.get(c) || 0) + 1);
108
+ });
109
+ });
110
+
111
+ classUsage.forEach((count, className) => {
112
+ if (count > 1) {
113
+ commonClasses.add(className);
114
+ }
115
+ });
116
+
117
+ console.log(`Total common: ${commonClasses.size}`);
118
+ console.log(
119
+ "Sample common classes:",
120
+ Array.from(commonClasses).slice(0, 10).join(", "),
121
+ );
122
+ }
123
+
124
+ main();
@@ -0,0 +1,239 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Post-build script to automatically wrap client component exports
5
+ *
6
+ * This script:
7
+ * 1. Identifies components with 'use client' directive in source
8
+ * 2. Automatically wraps their compiled exports with autoWrapClientComponent
9
+ * 3. Allows developers to write components without manual wrapping
10
+ */
11
+
12
+ import * as fs from "fs";
13
+ import * as path from "path";
14
+
15
+ const PROJECT_ROOT = process.cwd();
16
+ const SRC_DIR = path.join(PROJECT_ROOT, "src");
17
+ const DIST_DIR = path.join(PROJECT_ROOT, "dist");
18
+
19
+ interface ClientComponentInfo {
20
+ sourceFile: string;
21
+ compiledFile: string;
22
+ componentName: string;
23
+ }
24
+
25
+ /**
26
+ * Find all TypeScript/TSX files with 'use client' directive
27
+ */
28
+ function findClientComponentsInSource(): Map<string, string> {
29
+ const clientComponents = new Map<string, string>(); // compiledFile -> componentName
30
+ const extensions = [".ts", ".tsx"];
31
+
32
+ function walkDir(dir: string) {
33
+ if (!fs.existsSync(dir)) return;
34
+
35
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
36
+
37
+ for (const entry of entries) {
38
+ const fullPath = path.join(dir, entry.name);
39
+
40
+ if (entry.isDirectory()) {
41
+ if (!entry.name.startsWith(".") && entry.name !== "node_modules") {
42
+ walkDir(fullPath);
43
+ }
44
+ } else if (extensions.includes(path.extname(entry.name))) {
45
+ try {
46
+ const content = fs.readFileSync(fullPath, "utf-8");
47
+
48
+ // Check for 'use client' directive at the start of the file
49
+ if (/^['"]use client['"]/.test(content.trim())) {
50
+ const fileName = path.basename(fullPath, path.extname(fullPath));
51
+ // Convert kebab-case to PascalCase
52
+ const componentName = fileName
53
+ .split("-")
54
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
55
+ .join("");
56
+
57
+ // Calculate the corresponding compiled file path
58
+ const relativePath = path.relative(SRC_DIR, fullPath);
59
+ const compiledPath = path.join(
60
+ DIST_DIR,
61
+ relativePath.replace(/\.tsx?$/, ".js"),
62
+ );
63
+
64
+ clientComponents.set(compiledPath, componentName);
65
+ }
66
+ } catch (error) {
67
+ console.error(`Error reading file ${fullPath}:`, error);
68
+ }
69
+ }
70
+ }
71
+ }
72
+
73
+ walkDir(SRC_DIR);
74
+ return clientComponents;
75
+ }
76
+
77
+ /**
78
+ * Transform a compiled JavaScript file to wrap the default export
79
+ * Handles SWC's Object.defineProperty pattern
80
+ */
81
+ function transformCompiledFile(
82
+ filePath: string,
83
+ componentName: string,
84
+ ): boolean {
85
+ try {
86
+ if (!fs.existsSync(filePath)) {
87
+ console.warn(`⚠️ File not found: ${filePath}`);
88
+ return false;
89
+ }
90
+
91
+ let content = fs.readFileSync(filePath, "utf-8");
92
+
93
+ // Skip if already wrapped (contains autoWrapClientComponent)
94
+ if (content.includes("autoWrapClientComponent")) {
95
+ console.log(` ✓ Already wrapped: ${path.basename(filePath)}`);
96
+ return true;
97
+ }
98
+
99
+ // Import the wrapper from @harpy-js/core package instead of relative path
100
+ const normalizedRelativePath = "@harpy-js/core";
101
+
102
+ // Add the require at the top of the file (after the 'use strict' and initial Object.defineProperty)
103
+ const requireStatement = `var { autoWrapClientComponent: _autoWrapClientComponent } = require("${normalizedRelativePath}");`;
104
+
105
+ // Ensure the require statement is added before any wrapping
106
+ if (
107
+ !content.includes(requireStatement) &&
108
+ !content.includes("_autoWrapClientComponent")
109
+ ) {
110
+ // Check if we have the 'use client' and Object.defineProperty pattern
111
+ if (
112
+ content.includes('"use strict"') &&
113
+ content.includes("Object.defineProperty")
114
+ ) {
115
+ // Find the last Object.defineProperty line before any exports
116
+ const lines = content.split("\n");
117
+ let insertIndex = -1;
118
+
119
+ for (let i = 0; i < lines.length; i++) {
120
+ if (lines[i].includes("Object.defineProperty(exports")) {
121
+ insertIndex = i + 1;
122
+ break;
123
+ }
124
+ }
125
+
126
+ if (insertIndex !== -1) {
127
+ lines.splice(insertIndex, 0, requireStatement);
128
+ content = lines.join("\n");
129
+ }
130
+ } else {
131
+ // Fallback: insert at the beginning after any initial comments
132
+ const lines = content.split("\n");
133
+ let insertIndex = 0;
134
+ for (let i = 0; i < lines.length; i++) {
135
+ if (lines[i].includes('"use strict"')) {
136
+ insertIndex = i + 1;
137
+ break;
138
+ }
139
+ }
140
+ lines.splice(insertIndex, 0, requireStatement);
141
+ content = lines.join("\n");
142
+ }
143
+ }
144
+
145
+ // SWC pattern: const _default = ComponentName;
146
+ const swcPattern = new RegExp(
147
+ `const\\s+_default\\s*=\\s*${componentName}\\s*;`,
148
+ );
149
+
150
+ if (swcPattern.test(content)) {
151
+ // Replace the _default assignment with a wrapped version
152
+ content = content.replace(
153
+ `const _default = ${componentName};`,
154
+ `const _default = _autoWrapClientComponent(${componentName}, '${componentName}');`,
155
+ );
156
+
157
+ fs.writeFileSync(filePath, content, "utf-8");
158
+ console.log(` ✓ Wrapped: ${path.basename(filePath)}`);
159
+ return true;
160
+ }
161
+
162
+ // CommonJS pattern: exports.default = ComponentName;
163
+ const pattern1 = new RegExp(
164
+ `exports\\.default\\s*=\\s*${componentName}\\s*;`,
165
+ );
166
+ if (pattern1.test(content)) {
167
+ content = content.replace(
168
+ pattern1,
169
+ `var { autoWrapClientComponent } = require("${normalizedRelativePath}");\nexports.default = autoWrapClientComponent(${componentName}, '${componentName}');`,
170
+ );
171
+ fs.writeFileSync(filePath, content, "utf-8");
172
+ console.log(` ✓ Wrapped: ${path.basename(filePath)}`);
173
+ return true;
174
+ }
175
+
176
+ // ES6 pattern: export default ComponentName;
177
+ const pattern2 = new RegExp(`export\\s+default\\s+${componentName}\\s*;`);
178
+ if (pattern2.test(content)) {
179
+ content = content.replace(
180
+ `export default ${componentName};`,
181
+ `import { autoWrapClientComponent as _autoWrapClientComponent } from "${normalizedRelativePath}";\nexport default _autoWrapClientComponent(${componentName}, '${componentName}');`,
182
+ );
183
+ fs.writeFileSync(filePath, content, "utf-8");
184
+ console.log(` ✓ Wrapped: ${path.basename(filePath)}`);
185
+ return true;
186
+ }
187
+
188
+ // Named export pattern: exports.ComponentName = ComponentName;
189
+ const namedExportPattern = new RegExp(
190
+ `exports\\.${componentName}\\s*=\\s*${componentName}\\s*;`,
191
+ );
192
+ if (namedExportPattern.test(content)) {
193
+ content = content.replace(
194
+ namedExportPattern,
195
+ `exports.${componentName} = _autoWrapClientComponent(${componentName}, '${componentName}');`,
196
+ );
197
+ fs.writeFileSync(filePath, content, "utf-8");
198
+ console.log(` ✓ Wrapped: ${path.basename(filePath)}`);
199
+ return true;
200
+ }
201
+
202
+ console.warn(
203
+ ` ⚠️ Could not find export pattern for ${componentName} in ${path.basename(filePath)}`,
204
+ );
205
+ return false;
206
+ } catch (error) {
207
+ console.error(` ✗ Error transforming ${path.basename(filePath)}:`, error);
208
+ return false;
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Main function
214
+ */
215
+ function main(): void {
216
+ console.log("🔄 Auto-wrapping client component exports...\n");
217
+
218
+ const clientComponents = findClientComponentsInSource();
219
+
220
+ if (clientComponents.size === 0) {
221
+ console.log("⚠️ No client components found\n");
222
+ return;
223
+ }
224
+
225
+ console.log(`Found ${clientComponents.size} client component(s):\n`);
226
+
227
+ let wrapped = 0;
228
+ for (const [compiledPath, componentName] of clientComponents) {
229
+ if (transformCompiledFile(compiledPath, componentName)) {
230
+ wrapped++;
231
+ }
232
+ }
233
+
234
+ console.log(
235
+ `\n✨ Auto-wrap complete: ${wrapped}/${clientComponents.size} components wrapped\n`,
236
+ );
237
+ }
238
+
239
+ main();
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Build script for Tailwind CSS v4
4
+ * Compiles src/assets/styles.css to public/styles.css
5
+ */
6
+
7
+ import * as fs from "fs";
8
+ import * as path from "path";
9
+
10
+ const inputFile = path.join(__dirname, "../src/assets/styles.css");
11
+ const outputFile = path.join(__dirname, "../public/styles.css");
12
+
13
+ // Create public directory if it doesn't exist
14
+ const publicDir = path.dirname(outputFile);
15
+ if (!fs.existsSync(publicDir)) {
16
+ fs.mkdirSync(publicDir, { recursive: true });
17
+ }
18
+
19
+ // Read the input CSS file
20
+ const inputCSS = fs.readFileSync(inputFile, "utf8");
21
+
22
+ // Import and compile with Tailwind v4
23
+ (async () => {
24
+ const tailwindModule = await import("@tailwindcss/postcss");
25
+ const compile =
26
+ (tailwindModule as any).default || (tailwindModule as any).compile;
27
+
28
+ try {
29
+ const output = await compile(inputCSS, {
30
+ file: inputFile,
31
+ });
32
+ fs.writeFileSync(outputFile, output.toString());
33
+ console.log("✅ CSS compiled successfully");
34
+ } catch (err) {
35
+ console.error("❌ CSS compilation failed:", err);
36
+ process.exit(1);
37
+ }
38
+ })();