@ecopages/core 0.2.0-alpha.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.
- package/CHANGELOG.md +89 -0
- package/LICENSE +21 -0
- package/README.md +32 -0
- package/package.json +279 -0
- package/src/adapters/abstract/application-adapter.d.ts +168 -0
- package/src/adapters/abstract/application-adapter.js +109 -0
- package/src/adapters/abstract/application-adapter.ts +337 -0
- package/src/adapters/abstract/router-adapter.d.ts +26 -0
- package/src/adapters/abstract/router-adapter.js +5 -0
- package/src/adapters/abstract/router-adapter.ts +30 -0
- package/src/adapters/abstract/server-adapter.d.ts +69 -0
- package/src/adapters/abstract/server-adapter.js +15 -0
- package/src/adapters/abstract/server-adapter.ts +79 -0
- package/src/adapters/bun/client-bridge.d.ts +34 -0
- package/src/adapters/bun/client-bridge.js +48 -0
- package/src/adapters/bun/client-bridge.ts +62 -0
- package/src/adapters/bun/create-app.d.ts +60 -0
- package/src/adapters/bun/create-app.js +117 -0
- package/src/adapters/bun/create-app.ts +189 -0
- package/src/adapters/bun/define-api-handler.d.ts +61 -0
- package/src/adapters/bun/define-api-handler.js +15 -0
- package/src/adapters/bun/define-api-handler.ts +114 -0
- package/src/adapters/bun/hmr-manager.d.ts +84 -0
- package/src/adapters/bun/hmr-manager.js +227 -0
- package/src/adapters/bun/hmr-manager.ts +281 -0
- package/src/adapters/bun/index.d.ts +3 -0
- package/src/adapters/bun/index.js +8 -0
- package/src/adapters/bun/index.ts +3 -0
- package/src/adapters/bun/server-adapter.d.ts +155 -0
- package/src/adapters/bun/server-adapter.js +368 -0
- package/src/adapters/bun/server-adapter.ts +492 -0
- package/src/adapters/bun/server-lifecycle.d.ts +52 -0
- package/src/adapters/bun/server-lifecycle.js +120 -0
- package/src/adapters/bun/server-lifecycle.ts +154 -0
- package/src/adapters/index.d.ts +6 -0
- package/src/adapters/index.js +14 -0
- package/src/adapters/index.ts +6 -0
- package/src/adapters/node/create-app.d.ts +21 -0
- package/src/adapters/node/create-app.js +143 -0
- package/src/adapters/node/create-app.ts +179 -0
- package/src/adapters/node/index.d.ts +4 -0
- package/src/adapters/node/index.js +8 -0
- package/src/adapters/node/index.ts +9 -0
- package/src/adapters/node/node-client-bridge.d.ts +26 -0
- package/src/adapters/node/node-client-bridge.js +66 -0
- package/src/adapters/node/node-client-bridge.ts +79 -0
- package/src/adapters/node/node-hmr-manager.d.ts +62 -0
- package/src/adapters/node/node-hmr-manager.js +221 -0
- package/src/adapters/node/node-hmr-manager.ts +271 -0
- package/src/adapters/node/server-adapter.d.ts +190 -0
- package/src/adapters/node/server-adapter.js +420 -0
- package/src/adapters/node/server-adapter.ts +561 -0
- package/src/adapters/node/static-content-server.d.ts +24 -0
- package/src/adapters/node/static-content-server.js +166 -0
- package/src/adapters/node/static-content-server.ts +203 -0
- package/src/adapters/shared/api-response.d.ts +52 -0
- package/src/adapters/shared/api-response.js +96 -0
- package/src/adapters/shared/api-response.ts +104 -0
- package/src/adapters/shared/application-adapter.d.ts +18 -0
- package/src/adapters/shared/application-adapter.js +90 -0
- package/src/adapters/shared/application-adapter.ts +199 -0
- package/src/adapters/shared/explicit-static-route-matcher.d.ts +38 -0
- package/src/adapters/shared/explicit-static-route-matcher.js +100 -0
- package/src/adapters/shared/explicit-static-route-matcher.ts +134 -0
- package/src/adapters/shared/file-route-middleware-pipeline.d.ts +65 -0
- package/src/adapters/shared/file-route-middleware-pipeline.js +98 -0
- package/src/adapters/shared/file-route-middleware-pipeline.ts +123 -0
- package/src/adapters/shared/fs-server-response-factory.d.ts +19 -0
- package/src/adapters/shared/fs-server-response-factory.js +97 -0
- package/src/adapters/shared/fs-server-response-factory.ts +118 -0
- package/src/adapters/shared/fs-server-response-matcher.d.ts +71 -0
- package/src/adapters/shared/fs-server-response-matcher.js +155 -0
- package/src/adapters/shared/fs-server-response-matcher.ts +198 -0
- package/src/adapters/shared/render-context.d.ts +14 -0
- package/src/adapters/shared/render-context.js +69 -0
- package/src/adapters/shared/render-context.ts +105 -0
- package/src/adapters/shared/server-adapter.d.ts +87 -0
- package/src/adapters/shared/server-adapter.js +353 -0
- package/src/adapters/shared/server-adapter.ts +442 -0
- package/src/adapters/shared/server-route-handler.d.ts +89 -0
- package/src/adapters/shared/server-route-handler.js +120 -0
- package/src/adapters/shared/server-route-handler.ts +166 -0
- package/src/adapters/shared/server-static-builder.d.ts +38 -0
- package/src/adapters/shared/server-static-builder.js +46 -0
- package/src/adapters/shared/server-static-builder.ts +82 -0
- package/src/build/build-adapter.d.ts +74 -0
- package/src/build/build-adapter.js +54 -0
- package/src/build/build-adapter.ts +132 -0
- package/src/build/build-types.d.ts +57 -0
- package/src/build/build-types.js +0 -0
- package/src/build/build-types.ts +83 -0
- package/src/build/esbuild-build-adapter.d.ts +69 -0
- package/src/build/esbuild-build-adapter.js +390 -0
- package/src/build/esbuild-build-adapter.ts +510 -0
- package/src/config/config-builder.d.ts +227 -0
- package/src/config/config-builder.js +392 -0
- package/src/config/config-builder.ts +474 -0
- package/src/constants.d.ts +32 -0
- package/src/constants.js +21 -0
- package/src/constants.ts +39 -0
- package/src/create-app.d.ts +17 -0
- package/src/create-app.js +66 -0
- package/src/create-app.ts +87 -0
- package/src/declarations.d.ts +26 -0
- package/src/define-api-handler.d.ts +25 -0
- package/src/define-api-handler.js +15 -0
- package/src/define-api-handler.ts +66 -0
- package/src/dev/sc-server.d.ts +30 -0
- package/src/dev/sc-server.js +111 -0
- package/src/dev/sc-server.ts +143 -0
- package/src/eco/README.md +636 -0
- package/src/eco/component-render-context.d.ts +105 -0
- package/src/eco/component-render-context.js +77 -0
- package/src/eco/component-render-context.ts +202 -0
- package/src/eco/eco.d.ts +9 -0
- package/src/eco/eco.js +110 -0
- package/src/eco/eco.ts +221 -0
- package/src/eco/eco.types.d.ts +170 -0
- package/src/eco/eco.types.js +0 -0
- package/src/eco/eco.types.ts +202 -0
- package/src/eco/eco.utils.d.ts +40 -0
- package/src/eco/eco.utils.js +40 -0
- package/src/eco/eco.utils.ts +89 -0
- package/src/eco/global-injector-map.d.ts +16 -0
- package/src/eco/global-injector-map.js +80 -0
- package/src/eco/global-injector-map.ts +112 -0
- package/src/eco/lazy-injector-map.d.ts +8 -0
- package/src/eco/lazy-injector-map.js +70 -0
- package/src/eco/lazy-injector-map.ts +120 -0
- package/src/eco/module-dependencies.d.ts +18 -0
- package/src/eco/module-dependencies.js +49 -0
- package/src/eco/module-dependencies.ts +75 -0
- package/src/env.d.ts +20 -0
- package/src/errors/http-error.d.ts +31 -0
- package/src/errors/http-error.js +50 -0
- package/src/errors/http-error.ts +72 -0
- package/src/errors/index.d.ts +2 -0
- package/src/errors/index.js +4 -0
- package/src/errors/index.ts +2 -0
- package/src/errors/locals-access-error.d.ts +4 -0
- package/src/errors/locals-access-error.js +9 -0
- package/src/errors/locals-access-error.ts +7 -0
- package/src/global/app-logger.d.ts +2 -0
- package/src/global/app-logger.js +6 -0
- package/src/global/app-logger.ts +4 -0
- package/src/hmr/client/__screenshots__/hmr-runtime.test.browser.ts/HMR-Runtime-HMR-Server-Integration-should-have-HMR-script-injected-in-page-1.png +0 -0
- package/src/hmr/client/__screenshots__/hmr-runtime.test.browser.ts/HMR-Runtime-HMR-Server-Integration-should-load-fixture-app-page-1.png +0 -0
- package/src/hmr/client/__screenshots__/hmr-runtime.test.browser.ts/HMR-Runtime-WebSocket-Connection-should-connect-to-correct-HMR-endpoint-1.png +0 -0
- package/src/hmr/client/hmr-runtime.d.ts +10 -0
- package/src/hmr/client/hmr-runtime.js +86 -0
- package/src/hmr/client/hmr-runtime.ts +121 -0
- package/src/hmr/hmr-strategy.d.ts +159 -0
- package/src/hmr/hmr-strategy.js +29 -0
- package/src/hmr/hmr-strategy.ts +172 -0
- package/src/hmr/hmr.test.e2e.d.ts +1 -0
- package/src/hmr/hmr.test.e2e.js +50 -0
- package/src/hmr/hmr.test.e2e.ts +75 -0
- package/src/hmr/strategies/default-hmr-strategy.d.ts +43 -0
- package/src/hmr/strategies/default-hmr-strategy.js +34 -0
- package/src/hmr/strategies/default-hmr-strategy.ts +60 -0
- package/src/hmr/strategies/js-hmr-strategy.d.ts +136 -0
- package/src/hmr/strategies/js-hmr-strategy.js +179 -0
- package/src/hmr/strategies/js-hmr-strategy.ts +308 -0
- package/src/index.browser.d.ts +3 -0
- package/src/index.browser.js +4 -0
- package/src/index.browser.ts +3 -0
- package/src/index.d.ts +5 -0
- package/src/index.js +10 -0
- package/src/index.ts +5 -0
- package/src/integrations/ghtml/ghtml-renderer.d.ts +15 -0
- package/src/integrations/ghtml/ghtml-renderer.js +60 -0
- package/src/integrations/ghtml/ghtml-renderer.ts +93 -0
- package/src/integrations/ghtml/ghtml.plugin.d.ts +20 -0
- package/src/integrations/ghtml/ghtml.plugin.js +21 -0
- package/src/integrations/ghtml/ghtml.plugin.ts +32 -0
- package/src/internal-types.d.ts +200 -0
- package/src/internal-types.js +0 -0
- package/src/internal-types.ts +212 -0
- package/src/plugins/alias-resolver-plugin.d.ts +2 -0
- package/src/plugins/alias-resolver-plugin.js +39 -0
- package/src/plugins/alias-resolver-plugin.ts +45 -0
- package/src/plugins/eco-component-meta-plugin.d.ts +95 -0
- package/src/plugins/eco-component-meta-plugin.js +157 -0
- package/src/plugins/eco-component-meta-plugin.ts +474 -0
- package/src/plugins/integration-plugin.d.ts +102 -0
- package/src/plugins/integration-plugin.js +100 -0
- package/src/plugins/integration-plugin.ts +184 -0
- package/src/plugins/processor.d.ts +82 -0
- package/src/plugins/processor.js +122 -0
- package/src/plugins/processor.ts +220 -0
- package/src/public-types.d.ts +1094 -0
- package/src/public-types.js +0 -0
- package/src/public-types.ts +1255 -0
- package/src/route-renderer/GRAPH.md +387 -0
- package/src/route-renderer/README.md +135 -0
- package/src/route-renderer/component-graph-executor.d.ts +32 -0
- package/src/route-renderer/component-graph-executor.js +31 -0
- package/src/route-renderer/component-graph-executor.ts +84 -0
- package/src/route-renderer/component-graph.d.ts +42 -0
- package/src/route-renderer/component-graph.js +72 -0
- package/src/route-renderer/component-graph.ts +159 -0
- package/src/route-renderer/component-marker.d.ts +52 -0
- package/src/route-renderer/component-marker.js +46 -0
- package/src/route-renderer/component-marker.ts +117 -0
- package/src/route-renderer/dependency-resolver.d.ts +24 -0
- package/src/route-renderer/dependency-resolver.js +428 -0
- package/src/route-renderer/dependency-resolver.ts +596 -0
- package/src/route-renderer/html-post-processing.service.d.ts +40 -0
- package/src/route-renderer/html-post-processing.service.js +86 -0
- package/src/route-renderer/html-post-processing.service.ts +103 -0
- package/src/route-renderer/integration-renderer.d.ts +339 -0
- package/src/route-renderer/integration-renderer.js +526 -0
- package/src/route-renderer/integration-renderer.ts +696 -0
- package/src/route-renderer/marker-graph-resolver.d.ts +76 -0
- package/src/route-renderer/marker-graph-resolver.js +93 -0
- package/src/route-renderer/marker-graph-resolver.ts +153 -0
- package/src/route-renderer/page-module-loader.d.ts +61 -0
- package/src/route-renderer/page-module-loader.js +102 -0
- package/src/route-renderer/page-module-loader.ts +153 -0
- package/src/route-renderer/render-execution.service.d.ts +69 -0
- package/src/route-renderer/render-execution.service.js +91 -0
- package/src/route-renderer/render-execution.service.ts +158 -0
- package/src/route-renderer/render-preparation.service.d.ts +112 -0
- package/src/route-renderer/render-preparation.service.js +243 -0
- package/src/route-renderer/render-preparation.service.ts +358 -0
- package/src/route-renderer/route-renderer.d.ts +26 -0
- package/src/route-renderer/route-renderer.js +68 -0
- package/src/route-renderer/route-renderer.ts +80 -0
- package/src/router/fs-router-scanner.d.ts +41 -0
- package/src/router/fs-router-scanner.js +155 -0
- package/src/router/fs-router-scanner.ts +217 -0
- package/src/router/fs-router.d.ts +26 -0
- package/src/router/fs-router.js +100 -0
- package/src/router/fs-router.ts +122 -0
- package/src/services/asset-processing-service/asset-processing.service.d.ts +41 -0
- package/src/services/asset-processing-service/asset-processing.service.js +250 -0
- package/src/services/asset-processing-service/asset-processing.service.ts +306 -0
- package/src/services/asset-processing-service/asset.factory.d.ts +17 -0
- package/src/services/asset-processing-service/asset.factory.js +82 -0
- package/src/services/asset-processing-service/asset.factory.ts +105 -0
- package/src/services/asset-processing-service/assets.types.d.ts +88 -0
- package/src/services/asset-processing-service/assets.types.js +0 -0
- package/src/services/asset-processing-service/assets.types.ts +112 -0
- package/src/services/asset-processing-service/index.d.ts +3 -0
- package/src/services/asset-processing-service/index.js +3 -0
- package/src/services/asset-processing-service/index.ts +3 -0
- package/src/services/asset-processing-service/processor.interface.d.ts +22 -0
- package/src/services/asset-processing-service/processor.interface.js +6 -0
- package/src/services/asset-processing-service/processor.interface.ts +27 -0
- package/src/services/asset-processing-service/processor.registry.d.ts +8 -0
- package/src/services/asset-processing-service/processor.registry.js +15 -0
- package/src/services/asset-processing-service/processor.registry.ts +18 -0
- package/src/services/asset-processing-service/processors/base/base-processor.d.ts +24 -0
- package/src/services/asset-processing-service/processors/base/base-processor.js +59 -0
- package/src/services/asset-processing-service/processors/base/base-processor.ts +76 -0
- package/src/services/asset-processing-service/processors/base/base-script-processor.d.ts +16 -0
- package/src/services/asset-processing-service/processors/base/base-script-processor.js +80 -0
- package/src/services/asset-processing-service/processors/base/base-script-processor.ts +105 -0
- package/src/services/asset-processing-service/processors/index.d.ts +5 -0
- package/src/services/asset-processing-service/processors/index.js +5 -0
- package/src/services/asset-processing-service/processors/index.ts +5 -0
- package/src/services/asset-processing-service/processors/script/content-script.processor.d.ts +5 -0
- package/src/services/asset-processing-service/processors/script/content-script.processor.js +57 -0
- package/src/services/asset-processing-service/processors/script/content-script.processor.ts +66 -0
- package/src/services/asset-processing-service/processors/script/file-script.processor.d.ts +8 -0
- package/src/services/asset-processing-service/processors/script/file-script.processor.js +76 -0
- package/src/services/asset-processing-service/processors/script/file-script.processor.ts +88 -0
- package/src/services/asset-processing-service/processors/script/node-module-script.processor.d.ts +7 -0
- package/src/services/asset-processing-service/processors/script/node-module-script.processor.js +74 -0
- package/src/services/asset-processing-service/processors/script/node-module-script.processor.ts +84 -0
- package/src/services/asset-processing-service/processors/stylesheet/content-stylesheet.processor.d.ts +5 -0
- package/src/services/asset-processing-service/processors/stylesheet/content-stylesheet.processor.js +25 -0
- package/src/services/asset-processing-service/processors/stylesheet/content-stylesheet.processor.ts +27 -0
- package/src/services/asset-processing-service/processors/stylesheet/file-stylesheet.processor.d.ts +9 -0
- package/src/services/asset-processing-service/processors/stylesheet/file-stylesheet.processor.js +63 -0
- package/src/services/asset-processing-service/processors/stylesheet/file-stylesheet.processor.ts +77 -0
- package/src/services/cache/cache.types.d.ts +107 -0
- package/src/services/cache/cache.types.js +0 -0
- package/src/services/cache/cache.types.ts +126 -0
- package/src/services/cache/index.d.ts +7 -0
- package/src/services/cache/index.js +7 -0
- package/src/services/cache/index.ts +18 -0
- package/src/services/cache/memory-cache-store.d.ts +42 -0
- package/src/services/cache/memory-cache-store.js +98 -0
- package/src/services/cache/memory-cache-store.ts +130 -0
- package/src/services/cache/page-cache-service.d.ts +70 -0
- package/src/services/cache/page-cache-service.js +152 -0
- package/src/services/cache/page-cache-service.ts +202 -0
- package/src/services/html-transformer.service.d.ts +50 -0
- package/src/services/html-transformer.service.js +163 -0
- package/src/services/html-transformer.service.ts +217 -0
- package/src/services/page-module-import.service.d.ts +37 -0
- package/src/services/page-module-import.service.js +88 -0
- package/src/services/page-module-import.service.ts +129 -0
- package/src/services/page-request-cache-coordinator.service.d.ts +75 -0
- package/src/services/page-request-cache-coordinator.service.js +107 -0
- package/src/services/page-request-cache-coordinator.service.ts +128 -0
- package/src/services/schema-validation-service.d.ts +122 -0
- package/src/services/schema-validation-service.js +101 -0
- package/src/services/schema-validation-service.ts +204 -0
- package/src/services/validation/standard-schema.types.d.ts +65 -0
- package/src/services/validation/standard-schema.types.js +0 -0
- package/src/services/validation/standard-schema.types.ts +68 -0
- package/src/static-site-generator/static-site-generator.d.ts +57 -0
- package/src/static-site-generator/static-site-generator.js +272 -0
- package/src/static-site-generator/static-site-generator.ts +359 -0
- package/src/utils/css.d.ts +1 -0
- package/src/utils/css.js +7 -0
- package/src/utils/css.ts +5 -0
- package/src/utils/deep-merge.d.ts +14 -0
- package/src/utils/deep-merge.js +32 -0
- package/src/utils/deep-merge.ts +47 -0
- package/src/utils/hash.d.ts +1 -0
- package/src/utils/hash.js +7 -0
- package/src/utils/hash.ts +5 -0
- package/src/utils/html.d.ts +1 -0
- package/src/utils/html.js +4 -0
- package/src/utils/html.ts +1 -0
- package/src/utils/invariant.d.ts +5 -0
- package/src/utils/invariant.js +11 -0
- package/src/utils/invariant.ts +15 -0
- package/src/utils/locals-utils.d.ts +15 -0
- package/src/utils/locals-utils.js +24 -0
- package/src/utils/locals-utils.ts +37 -0
- package/src/utils/parse-cli-args.d.ts +24 -0
- package/src/utils/parse-cli-args.js +47 -0
- package/src/utils/parse-cli-args.ts +83 -0
- package/src/utils/path-utils.module.d.ts +5 -0
- package/src/utils/path-utils.module.js +14 -0
- package/src/utils/path-utils.module.ts +14 -0
- package/src/utils/runtime.d.ts +11 -0
- package/src/utils/runtime.js +40 -0
- package/src/utils/runtime.ts +44 -0
- package/src/utils/server-utils.module.d.ts +19 -0
- package/src/utils/server-utils.module.js +56 -0
- package/src/utils/server-utils.module.ts +67 -0
- package/src/watchers/project-watcher.d.ts +120 -0
- package/src/watchers/project-watcher.js +238 -0
- package/src/watchers/project-watcher.test-helpers.d.ts +4 -0
- package/src/watchers/project-watcher.test-helpers.js +51 -0
- package/src/watchers/project-watcher.test-helpers.ts +40 -0
- package/src/watchers/project-watcher.ts +306 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { parseSync } from "oxc-parser";
|
|
3
|
+
import { fileSystem } from "@ecopages/file-system";
|
|
4
|
+
import { rapidhash } from "../utils/hash.js";
|
|
5
|
+
const REGEX_SPECIAL_CHARS = /[.*+?^${}()|[\]\\]/g;
|
|
6
|
+
const VALID_LOADER_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
7
|
+
function hasValidLoaderExtension(ext) {
|
|
8
|
+
for (const validExt of VALID_LOADER_EXTENSIONS) {
|
|
9
|
+
if (ext.endsWith(validExt)) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
function buildExtensionToIntegrationMap(integrations) {
|
|
16
|
+
const mapping = [];
|
|
17
|
+
for (const integration of integrations) {
|
|
18
|
+
for (const ext of integration.extensions) {
|
|
19
|
+
mapping.push([ext, integration.name]);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
mapping.sort((a, b) => b[0].length - a[0].length);
|
|
23
|
+
return mapping;
|
|
24
|
+
}
|
|
25
|
+
function detectIntegration(filePath, extensionToIntegration) {
|
|
26
|
+
for (const [ext, integration] of extensionToIntegration) {
|
|
27
|
+
if (filePath.endsWith(ext)) {
|
|
28
|
+
return integration;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return "ghtml";
|
|
32
|
+
}
|
|
33
|
+
function createExtensionPattern(extensions) {
|
|
34
|
+
if (extensions.length === 0) {
|
|
35
|
+
throw new Error("[eco-component-meta-plugin] No extensions configured. At least one integration is required.");
|
|
36
|
+
}
|
|
37
|
+
const uniqueExtensions = [...new Set(extensions)];
|
|
38
|
+
const escaped = uniqueExtensions.map((ext) => ext.replace(REGEX_SPECIAL_CHARS, "\\$&"));
|
|
39
|
+
return new RegExp(`(${escaped.join("|")})(\\?.*)?$`);
|
|
40
|
+
}
|
|
41
|
+
function createEcoComponentMetaPlugin(options) {
|
|
42
|
+
return {
|
|
43
|
+
name: "eco-component-meta-plugin",
|
|
44
|
+
setup(build) {
|
|
45
|
+
const allExtensions = options.config.integrations.flatMap((integration) => integration.extensions).filter(hasValidLoaderExtension);
|
|
46
|
+
if (allExtensions.length === 0) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const extensionPattern = createExtensionPattern(allExtensions);
|
|
50
|
+
const extensionToIntegration = buildExtensionToIntegrationMap(options.config.integrations);
|
|
51
|
+
build.onLoad({ filter: extensionPattern }, (args) => {
|
|
52
|
+
const filePath = args.path.split("?")[0];
|
|
53
|
+
const contents = fileSystem.readFileSync(filePath);
|
|
54
|
+
const integration = detectIntegration(filePath, extensionToIntegration);
|
|
55
|
+
const transformedContents = injectEcoMeta(contents, filePath, integration);
|
|
56
|
+
const ext = path.extname(filePath).slice(1);
|
|
57
|
+
return {
|
|
58
|
+
contents: transformedContents,
|
|
59
|
+
loader: ext || "ts"
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function findInjectionPoints(node, insertions, injection, isInsideEcoComponent = false) {
|
|
66
|
+
if (!node || typeof node !== "object") return;
|
|
67
|
+
const n = node;
|
|
68
|
+
if (n.type === "CallExpression") {
|
|
69
|
+
const callee = n.callee;
|
|
70
|
+
if (callee?.type === "MemberExpression" || callee?.type === "StaticMemberExpression") {
|
|
71
|
+
const obj = callee.object;
|
|
72
|
+
const prop = callee.property;
|
|
73
|
+
if (obj?.type === "Identifier" && obj?.name === "eco" && (prop?.name === "page" || prop?.name === "component")) {
|
|
74
|
+
const args = n.arguments;
|
|
75
|
+
const firstArg = args?.[0];
|
|
76
|
+
if (firstArg?.type === "ObjectExpression") {
|
|
77
|
+
const start = firstArg.start;
|
|
78
|
+
if (typeof start === "number") {
|
|
79
|
+
insertions.push({ position: start + 1, text: injection });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (n.type === "AssignmentExpression") {
|
|
86
|
+
const left = n.left;
|
|
87
|
+
const right = n.right;
|
|
88
|
+
if (left?.type === "MemberExpression" || left?.type === "StaticMemberExpression") {
|
|
89
|
+
const prop = left.property;
|
|
90
|
+
if (prop?.name === "config" && right?.type === "ObjectExpression") {
|
|
91
|
+
const start = right.start;
|
|
92
|
+
if (typeof start === "number") {
|
|
93
|
+
insertions.push({ position: start + 1, text: injection });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (n.type === "ObjectProperty" || n.type === "Property") {
|
|
99
|
+
const key = n.key;
|
|
100
|
+
const value = n.value;
|
|
101
|
+
if ((key?.type === "Identifier" || key?.type === "IdentifierName") && key?.name === "config" && value?.type === "ObjectExpression" && isInsideEcoComponent) {
|
|
102
|
+
const start = value.start;
|
|
103
|
+
if (typeof start === "number") {
|
|
104
|
+
insertions.push({ position: start + 1, text: injection });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
let childIsInsideEcoComponent = isInsideEcoComponent;
|
|
109
|
+
if (n.type === "VariableDeclarator") {
|
|
110
|
+
const id = n.id;
|
|
111
|
+
const typeAnnotation = id?.typeAnnotation;
|
|
112
|
+
if (typeAnnotation) {
|
|
113
|
+
const typeStr = JSON.stringify(typeAnnotation);
|
|
114
|
+
if (typeStr.includes("EcoComponent")) {
|
|
115
|
+
childIsInsideEcoComponent = true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const key in n) {
|
|
120
|
+
if (key === "start" || key === "end" || key === "type") continue;
|
|
121
|
+
const value = n[key];
|
|
122
|
+
if (Array.isArray(value)) {
|
|
123
|
+
for (const child of value) {
|
|
124
|
+
findInjectionPoints(child, insertions, injection, childIsInsideEcoComponent);
|
|
125
|
+
}
|
|
126
|
+
} else if (typeof value === "object" && value !== null) {
|
|
127
|
+
findInjectionPoints(value, insertions, injection, childIsInsideEcoComponent);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function injectEcoMeta(contents, filePath, integration) {
|
|
132
|
+
const result = parseSync(filePath, contents);
|
|
133
|
+
if (result.errors.length > 0) {
|
|
134
|
+
console.warn(`[eco-component-meta-plugin] Parse errors in ${filePath}:`, result.errors);
|
|
135
|
+
return contents;
|
|
136
|
+
}
|
|
137
|
+
const ast = result.program;
|
|
138
|
+
const id = rapidhash(filePath).toString(36);
|
|
139
|
+
const injection = ` __eco: { id: "${id}", file: "${filePath}", integration: "${integration}" },`;
|
|
140
|
+
const insertions = [];
|
|
141
|
+
findInjectionPoints(ast, insertions, injection);
|
|
142
|
+
if (insertions.length === 0) {
|
|
143
|
+
return contents;
|
|
144
|
+
}
|
|
145
|
+
insertions.sort((a, b) => b.position - a.position);
|
|
146
|
+
let transformed = contents;
|
|
147
|
+
for (const { position, text } of insertions) {
|
|
148
|
+
transformed = transformed.slice(0, position) + text + transformed.slice(position);
|
|
149
|
+
}
|
|
150
|
+
return transformed;
|
|
151
|
+
}
|
|
152
|
+
var eco_component_meta_plugin_default = createEcoComponentMetaPlugin;
|
|
153
|
+
export {
|
|
154
|
+
createEcoComponentMetaPlugin,
|
|
155
|
+
eco_component_meta_plugin_default as default,
|
|
156
|
+
injectEcoMeta
|
|
157
|
+
};
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun plugin that auto-injects `__eco` metadata into EcoComponent config objects.
|
|
3
|
+
*
|
|
4
|
+
* This plugin uses AST parsing (via oxc-parser) to reliably inject the `__eco` property
|
|
5
|
+
* into EcoComponent config objects at import time. The injected metadata contains:
|
|
6
|
+
* - `dir`: The directory path of the component file (used for dependency resolution)
|
|
7
|
+
* - `integration`: The integration type (e.g., 'react', 'kitajs', 'ghtml', 'lit')
|
|
8
|
+
*
|
|
9
|
+
* The plugin intercepts file loading for all configured integration extensions and
|
|
10
|
+
* transforms component configs before they are executed.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* // Before transformation:
|
|
15
|
+
* export default eco.page({
|
|
16
|
+
* render: () => '<div>Hello</div>',
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* // After transformation:
|
|
20
|
+
* export default eco.page({
|
|
21
|
+
* __eco: { id: "<hash>", file: "/path/to/pages/index.tsx", integration: "react" },
|
|
22
|
+
* render: () => '<div>Hello</div>',
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @module eco-component-meta-plugin
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import { parseSync } from 'oxc-parser';
|
|
31
|
+
import type { EcoBuildPlugin } from '../build/build-types.ts';
|
|
32
|
+
import type { EcoPagesAppConfig } from '../internal-types.ts';
|
|
33
|
+
import { fileSystem } from '@ecopages/file-system';
|
|
34
|
+
import { rapidhash } from '../utils/hash.ts';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Pattern to match regex special characters that need escaping.
|
|
38
|
+
* Used when building the file extension filter pattern.
|
|
39
|
+
*/
|
|
40
|
+
const REGEX_SPECIAL_CHARS = /[.*+?^${}()|[\]\\]/g;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Set of valid Bun loader extensions.
|
|
44
|
+
* Only files with these base extensions can be processed by Bun's loader system.
|
|
45
|
+
*/
|
|
46
|
+
const VALID_LOADER_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx']);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Checks if an extension can be handled by a valid Bun loader.
|
|
50
|
+
*
|
|
51
|
+
* Compound extensions like `.kita.tsx` are valid because the final
|
|
52
|
+
* extension is `.tsx`, which Bun can process.
|
|
53
|
+
*
|
|
54
|
+
* @param ext - The file extension to check (e.g., '.tsx', '.kita.tsx')
|
|
55
|
+
* @returns `true` if the extension ends with a valid loader extension
|
|
56
|
+
*/
|
|
57
|
+
function hasValidLoaderExtension(ext: string): boolean {
|
|
58
|
+
for (const validExt of VALID_LOADER_EXTENSIONS) {
|
|
59
|
+
if (ext.endsWith(validExt)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Builds a mapping from file extensions to integration names.
|
|
68
|
+
*
|
|
69
|
+
* The mapping is sorted by extension length (longest first) to ensure
|
|
70
|
+
* more specific extensions like `.kita.tsx` are matched before generic
|
|
71
|
+
* ones like `.tsx`.
|
|
72
|
+
*
|
|
73
|
+
* @param integrations - Array of integration configurations from EcoPagesAppConfig
|
|
74
|
+
* @returns Array of [extension, integrationName] tuples, sorted by specificity
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* const map = buildExtensionToIntegrationMap([
|
|
79
|
+
* { name: 'kitajs', extensions: ['.kita.tsx'] },
|
|
80
|
+
* { name: 'react', extensions: ['.tsx'] },
|
|
81
|
+
* ]);
|
|
82
|
+
* // Returns: [['.kita.tsx', 'kitajs'], ['.tsx', 'react']]
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
function buildExtensionToIntegrationMap(integrations: EcoPagesAppConfig['integrations']): [string, string][] {
|
|
86
|
+
const mapping: [string, string][] = [];
|
|
87
|
+
|
|
88
|
+
for (const integration of integrations) {
|
|
89
|
+
for (const ext of integration.extensions) {
|
|
90
|
+
mapping.push([ext, integration.name]);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
mapping.sort((a, b) => b[0].length - a[0].length);
|
|
95
|
+
|
|
96
|
+
return mapping;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Detects the integration type for a file based on its extension.
|
|
101
|
+
*
|
|
102
|
+
* Uses the pre-sorted extension-to-integration map to find the most
|
|
103
|
+
* specific matching extension.
|
|
104
|
+
*
|
|
105
|
+
* @param filePath - Absolute path to the file
|
|
106
|
+
* @param extensionToIntegration - Pre-built extension mapping from buildExtensionToIntegrationMap
|
|
107
|
+
* @returns The integration identifier (e.g., 'react', 'kitajs', 'lit', 'ghtml')
|
|
108
|
+
*/
|
|
109
|
+
function detectIntegration(filePath: string, extensionToIntegration: [string, string][]): string {
|
|
110
|
+
for (const [ext, integration] of extensionToIntegration) {
|
|
111
|
+
if (filePath.endsWith(ext)) {
|
|
112
|
+
return integration;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return 'ghtml';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Creates a RegExp pattern that matches files with any of the configured extensions.
|
|
120
|
+
*
|
|
121
|
+
* The pattern also matches optional query strings (e.g., `file.tsx?update=123`)
|
|
122
|
+
* which are used for cache-busting in development mode.
|
|
123
|
+
*
|
|
124
|
+
* @param extensions - Array of file extensions to match
|
|
125
|
+
* @returns RegExp pattern for use with Bun's onLoad filter
|
|
126
|
+
* @throws Error if no extensions are provided
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```typescript
|
|
130
|
+
* const pattern = createExtensionPattern(['.tsx', '.kita.tsx']);
|
|
131
|
+
* pattern.test('component.tsx'); // true
|
|
132
|
+
* pattern.test('component.tsx?v=123'); // true
|
|
133
|
+
* pattern.test('component.ts'); // false
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
function createExtensionPattern(extensions: string[]): RegExp {
|
|
137
|
+
if (extensions.length === 0) {
|
|
138
|
+
throw new Error('[eco-component-meta-plugin] No extensions configured. At least one integration is required.');
|
|
139
|
+
}
|
|
140
|
+
const uniqueExtensions = [...new Set(extensions)];
|
|
141
|
+
const escaped = uniqueExtensions.map((ext) => ext.replace(REGEX_SPECIAL_CHARS, '\\$&'));
|
|
142
|
+
return new RegExp(`(${escaped.join('|')})(\\?.*)?$`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Options for creating the eco-component-meta-plugin.
|
|
147
|
+
*/
|
|
148
|
+
export interface EcoComponentDirPluginOptions {
|
|
149
|
+
/** The EcoPages application configuration containing integration settings */
|
|
150
|
+
config: EcoPagesAppConfig;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Creates a build plugin that auto-injects `__eco` metadata into EcoComponent config objects.
|
|
155
|
+
*
|
|
156
|
+
* This plugin intercepts file loading for all integration-compatible files and:
|
|
157
|
+
* 1. Strips any query string from the file path (for dev mode cache-busting)
|
|
158
|
+
* 2. Reads the file contents
|
|
159
|
+
* 3. Parses the AST using oxc-parser to find injection points
|
|
160
|
+
* 4. Injects `__eco: { id: "...", file: "...", integration: "..." }` into config objects
|
|
161
|
+
* 5. Returns the transformed content with the appropriate loader
|
|
162
|
+
*
|
|
163
|
+
* Supported patterns:
|
|
164
|
+
* - `eco.page({ ... })` - Page component declarations
|
|
165
|
+
* - `eco.component({ ... })` - Reusable component declarations
|
|
166
|
+
* - `Component.config = { ... }` - Config assignment pattern
|
|
167
|
+
* - `config: { ... }` - Config property in object literals
|
|
168
|
+
* - `export const config = { ... }` - Exported config declarations
|
|
169
|
+
*
|
|
170
|
+
* @param options - Plugin options containing the EcoPages config
|
|
171
|
+
* @returns A build plugin instance ready for registration
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```typescript
|
|
175
|
+
* import { createEcoComponentMetaPlugin } from '@ecopages/core';
|
|
176
|
+
*
|
|
177
|
+
* const plugin = createEcoComponentMetaPlugin({ config: appConfig });
|
|
178
|
+
* appConfig.loaders.set(plugin.name, plugin);
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
export function createEcoComponentMetaPlugin(options: EcoComponentDirPluginOptions): EcoBuildPlugin {
|
|
182
|
+
return {
|
|
183
|
+
name: 'eco-component-meta-plugin',
|
|
184
|
+
setup(build) {
|
|
185
|
+
const allExtensions = options.config.integrations
|
|
186
|
+
.flatMap((integration) => integration.extensions)
|
|
187
|
+
.filter(hasValidLoaderExtension);
|
|
188
|
+
|
|
189
|
+
if (allExtensions.length === 0) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const extensionPattern = createExtensionPattern(allExtensions);
|
|
194
|
+
const extensionToIntegration = buildExtensionToIntegrationMap(options.config.integrations);
|
|
195
|
+
|
|
196
|
+
build.onLoad({ filter: extensionPattern }, (args) => {
|
|
197
|
+
const filePath = args.path.split('?')[0];
|
|
198
|
+
const contents = fileSystem.readFileSync(filePath);
|
|
199
|
+
const integration = detectIntegration(filePath, extensionToIntegration);
|
|
200
|
+
const transformedContents = injectEcoMeta(contents, filePath, integration);
|
|
201
|
+
|
|
202
|
+
const ext = path.extname(filePath).slice(1) as 'ts' | 'tsx' | 'js' | 'jsx';
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
contents: transformedContents,
|
|
206
|
+
loader: ext || 'ts',
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Represents a text insertion to be made in the source code.
|
|
215
|
+
*/
|
|
216
|
+
interface Insertion {
|
|
217
|
+
/** Character position in the source where text should be inserted */
|
|
218
|
+
position: number;
|
|
219
|
+
/** The text to insert at the position */
|
|
220
|
+
text: string;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Recursively walks the AST (Abstract Syntax Tree) to find all injection points for `__eco` metadata.
|
|
225
|
+
*
|
|
226
|
+
* ## What is an AST?
|
|
227
|
+
*
|
|
228
|
+
* An AST is a tree representation of source code. Instead of treating code as text,
|
|
229
|
+
* a parser breaks it down into a structured tree where each node represents a
|
|
230
|
+
* syntactic construct (variable, function call, object, etc.).
|
|
231
|
+
*
|
|
232
|
+
* For example, this code:
|
|
233
|
+
* ```typescript
|
|
234
|
+
* eco.page({ render: () => 'hi' })
|
|
235
|
+
* ```
|
|
236
|
+
*
|
|
237
|
+
* Becomes an AST like:
|
|
238
|
+
* ```
|
|
239
|
+
* CallExpression
|
|
240
|
+
* ├── callee: MemberExpression
|
|
241
|
+
* │ ├── object: Identifier (name: "eco")
|
|
242
|
+
* │ └── property: Identifier (name: "page")
|
|
243
|
+
* └── arguments: [
|
|
244
|
+
* └── ObjectExpression (start: 9) <-- We inject here at position 10 (after "{")
|
|
245
|
+
* └── properties: [...]
|
|
246
|
+
* ]
|
|
247
|
+
* ```
|
|
248
|
+
*
|
|
249
|
+
* ## How this function works
|
|
250
|
+
*
|
|
251
|
+
* 1. **Recursive traversal**: Visits every node in the tree, checking each one
|
|
252
|
+
* 2. **Pattern matching**: Checks if the current node matches one of our target patterns
|
|
253
|
+
* 3. **Position tracking**: When a match is found, records the `start` position of the
|
|
254
|
+
* config object (the character index in the original source where `{` appears)
|
|
255
|
+
* 4. **Insertion offset**: Adds +1 to insert right after the opening `{`
|
|
256
|
+
*
|
|
257
|
+
* ## Supported patterns
|
|
258
|
+
*
|
|
259
|
+
* | Pattern | AST Node Type | Example | File Types |
|
|
260
|
+
* |---------|---------------|---------|------------|
|
|
261
|
+
* | `eco.page({...})` | CallExpression | `export default eco.page({ render: () => 'hi' })` | All |
|
|
262
|
+
* | `eco.component({...})` | CallExpression | `export const Btn = eco.component({ render: () => '<button/>' })` | All |
|
|
263
|
+
* | `X.config = {...}` | AssignmentExpression | `MyComponent.config = { dependencies: [] }` | All |
|
|
264
|
+
* | `config: {...}` | ObjectProperty | `const X: EcoComponent = { config: {...} }` | EcoComponent-typed only |
|
|
265
|
+
*
|
|
266
|
+
* ## Why AST over regex?
|
|
267
|
+
*
|
|
268
|
+
* Regex would fail on edge cases like:
|
|
269
|
+
* - `eco.page<ComplexType<(arg: string) => void>>({...})` - generics with arrows
|
|
270
|
+
* - `// eco.page({ commented out })` - comments
|
|
271
|
+
* - `const str = "eco.page({ in a string })"` - string literals
|
|
272
|
+
* - Nested objects that look like config patterns
|
|
273
|
+
*
|
|
274
|
+
* AST parsing understands the actual code structure, not just text patterns.
|
|
275
|
+
*
|
|
276
|
+
* @param node - Current AST node being visited (starts with the root Program node)
|
|
277
|
+
* @param insertions - Array to collect insertion points (mutated by this function)
|
|
278
|
+
* @param injection - The injection text to insert at each point (e.g., ` __eco: {...},`)
|
|
279
|
+
* @param isInsideEcoComponent - Whether we're inside an EcoComponent-typed declaration
|
|
280
|
+
*/
|
|
281
|
+
function findInjectionPoints(
|
|
282
|
+
node: unknown,
|
|
283
|
+
insertions: Insertion[],
|
|
284
|
+
injection: string,
|
|
285
|
+
isInsideEcoComponent = false,
|
|
286
|
+
): void {
|
|
287
|
+
if (!node || typeof node !== 'object') return;
|
|
288
|
+
|
|
289
|
+
const n = node as Record<string, unknown>;
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Pattern 1: eco.page({...}) or eco.component({...})
|
|
293
|
+
* AST structure: CallExpression with MemberExpression callee where object is "eco"
|
|
294
|
+
*/
|
|
295
|
+
if (n.type === 'CallExpression') {
|
|
296
|
+
const callee = n.callee as Record<string, unknown> | undefined;
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* MemberExpression represents "something.property" syntax.
|
|
300
|
+
* StaticMemberExpression is oxc's variant for computed vs non-computed access.
|
|
301
|
+
*/
|
|
302
|
+
if (callee?.type === 'MemberExpression' || callee?.type === 'StaticMemberExpression') {
|
|
303
|
+
const obj = callee.object as Record<string, unknown> | undefined;
|
|
304
|
+
const prop = callee.property as Record<string, unknown> | undefined;
|
|
305
|
+
|
|
306
|
+
/** Check: is this `eco.page(...)` or `eco.component(...)`? */
|
|
307
|
+
if (
|
|
308
|
+
obj?.type === 'Identifier' &&
|
|
309
|
+
obj?.name === 'eco' &&
|
|
310
|
+
(prop?.name === 'page' || prop?.name === 'component')
|
|
311
|
+
) {
|
|
312
|
+
/** Get the first argument - should be an object literal {...} */
|
|
313
|
+
const args = n.arguments as Array<Record<string, unknown>> | undefined;
|
|
314
|
+
const firstArg = args?.[0];
|
|
315
|
+
if (firstArg?.type === 'ObjectExpression') {
|
|
316
|
+
/**
|
|
317
|
+
* `start` is the character index where this object begins (the "{").
|
|
318
|
+
* Insert at position+1 to place content right after "{".
|
|
319
|
+
*/
|
|
320
|
+
const start = firstArg.start as number | undefined;
|
|
321
|
+
if (typeof start === 'number') {
|
|
322
|
+
insertions.push({ position: start + 1, text: injection });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Pattern 2: Something.config = {...}
|
|
331
|
+
* AST structure: AssignmentExpression with MemberExpression left side ending in "config"
|
|
332
|
+
* This pattern is safe for all files because it requires a qualifier (e.g., MyComponent.config).
|
|
333
|
+
*/
|
|
334
|
+
if (n.type === 'AssignmentExpression') {
|
|
335
|
+
const left = n.left as Record<string, unknown> | undefined;
|
|
336
|
+
const right = n.right as Record<string, unknown> | undefined;
|
|
337
|
+
|
|
338
|
+
/** Case: MyComponent.config = {...} */
|
|
339
|
+
if (left?.type === 'MemberExpression' || left?.type === 'StaticMemberExpression') {
|
|
340
|
+
const prop = left.property as Record<string, unknown> | undefined;
|
|
341
|
+
if (prop?.name === 'config' && right?.type === 'ObjectExpression') {
|
|
342
|
+
const start = right.start as number | undefined;
|
|
343
|
+
if (typeof start === 'number') {
|
|
344
|
+
insertions.push({ position: start + 1, text: injection });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Pattern 3: { config: {...} } - config as an object property inside EcoComponent
|
|
352
|
+
* AST structure: ObjectProperty/Property with key "config" and value as ObjectExpression
|
|
353
|
+
*
|
|
354
|
+
* This pattern is matched when the parent VariableDeclarator has a type annotation
|
|
355
|
+
* containing "EcoComponent", e.g., `const X: EcoComponent = { config: {...} }`
|
|
356
|
+
*
|
|
357
|
+
* We track whether we're inside an EcoComponent-typed object via the `isInsideEcoComponent` flag.
|
|
358
|
+
*/
|
|
359
|
+
if (n.type === 'ObjectProperty' || n.type === 'Property') {
|
|
360
|
+
const key = n.key as Record<string, unknown> | undefined;
|
|
361
|
+
const value = n.value as Record<string, unknown> | undefined;
|
|
362
|
+
|
|
363
|
+
if (
|
|
364
|
+
(key?.type === 'Identifier' || key?.type === 'IdentifierName') &&
|
|
365
|
+
key?.name === 'config' &&
|
|
366
|
+
value?.type === 'ObjectExpression' &&
|
|
367
|
+
isInsideEcoComponent
|
|
368
|
+
) {
|
|
369
|
+
const start = value.start as number | undefined;
|
|
370
|
+
if (typeof start === 'number') {
|
|
371
|
+
insertions.push({ position: start + 1, text: injection });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Check if we're entering an EcoComponent-typed variable declaration.
|
|
378
|
+
* This sets a flag for child nodes to know they're inside an EcoComponent.
|
|
379
|
+
*
|
|
380
|
+
* Type annotation is on the `id` (Identifier), not the VariableDeclarator directly.
|
|
381
|
+
* e.g., `const X: EcoComponent = {...}` has the annotation on the "X" Identifier.
|
|
382
|
+
*/
|
|
383
|
+
let childIsInsideEcoComponent = isInsideEcoComponent;
|
|
384
|
+
if (n.type === 'VariableDeclarator') {
|
|
385
|
+
const id = n.id as Record<string, unknown> | undefined;
|
|
386
|
+
const typeAnnotation = id?.typeAnnotation as Record<string, unknown> | undefined;
|
|
387
|
+
if (typeAnnotation) {
|
|
388
|
+
const typeStr = JSON.stringify(typeAnnotation);
|
|
389
|
+
if (typeStr.includes('EcoComponent')) {
|
|
390
|
+
childIsInsideEcoComponent = true;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Recursive traversal: Visit all child nodes in the AST.
|
|
397
|
+
*
|
|
398
|
+
* This is how we "walk" the tree - for each property of the current node,
|
|
399
|
+
* if it's an array (like `body` containing statements) or an object (like `callee`),
|
|
400
|
+
* we recursively call findInjectionPoints on it.
|
|
401
|
+
* We skip metadata properties (start, end, type) that don't contain child nodes.
|
|
402
|
+
*/
|
|
403
|
+
for (const key in n) {
|
|
404
|
+
if (key === 'start' || key === 'end' || key === 'type') continue;
|
|
405
|
+
|
|
406
|
+
const value = n[key];
|
|
407
|
+
if (Array.isArray(value)) {
|
|
408
|
+
for (const child of value) {
|
|
409
|
+
findInjectionPoints(child, insertions, injection, childIsInsideEcoComponent);
|
|
410
|
+
}
|
|
411
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
412
|
+
findInjectionPoints(value, insertions, injection, childIsInsideEcoComponent);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Injects `__eco` metadata into EcoComponent config objects in file content.
|
|
419
|
+
*
|
|
420
|
+
* Uses oxc-parser for robust AST-based code analysis, which handles edge cases
|
|
421
|
+
* that regex-based approaches would miss (e.g., complex generics, nested objects,
|
|
422
|
+
* comments, string literals containing similar patterns).
|
|
423
|
+
*
|
|
424
|
+
* The injection is performed by:
|
|
425
|
+
* 1. Parsing the source code into an AST
|
|
426
|
+
* 2. Walking the AST to find all config object patterns
|
|
427
|
+
* 3. Collecting insertion points (sorted in reverse order to preserve positions)
|
|
428
|
+
* 4. Inserting the `__eco` property at each point
|
|
429
|
+
*
|
|
430
|
+
* @param contents - The source code content to transform
|
|
431
|
+
* @param filePath - Absolute path to the file (used to derive the directory)
|
|
432
|
+
* @param integration - The integration identifier for this file type
|
|
433
|
+
* @returns Transformed source code with `__eco` injected, or original if no patterns found
|
|
434
|
+
*
|
|
435
|
+
* @example
|
|
436
|
+
* ```typescript
|
|
437
|
+
* const result = injectEcoMeta(
|
|
438
|
+
* 'export default eco.page({ render: () => "<div>Hi</div>" });',
|
|
439
|
+
* '/app/src/pages/index.tsx',
|
|
440
|
+
* 'react'
|
|
441
|
+
* );
|
|
442
|
+
* // Result: 'export default eco.page({ __eco: { id: "<hash>", file: "/app/src/pages/index.tsx", integration: "react" }, render: () => "<div>Hi</div>" });'
|
|
443
|
+
* ```
|
|
444
|
+
*/
|
|
445
|
+
export function injectEcoMeta(contents: string, filePath: string, integration: string): string {
|
|
446
|
+
const result = parseSync(filePath, contents);
|
|
447
|
+
|
|
448
|
+
if (result.errors.length > 0) {
|
|
449
|
+
console.warn(`[eco-component-meta-plugin] Parse errors in ${filePath}:`, result.errors);
|
|
450
|
+
return contents;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const ast = result.program;
|
|
454
|
+
const id = rapidhash(filePath).toString(36);
|
|
455
|
+
const injection = ` __eco: { id: "${id}", file: "${filePath}", integration: "${integration}" },`;
|
|
456
|
+
|
|
457
|
+
const insertions: Insertion[] = [];
|
|
458
|
+
findInjectionPoints(ast, insertions, injection);
|
|
459
|
+
|
|
460
|
+
if (insertions.length === 0) {
|
|
461
|
+
return contents;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
insertions.sort((a, b) => b.position - a.position);
|
|
465
|
+
|
|
466
|
+
let transformed = contents;
|
|
467
|
+
for (const { position, text } of insertions) {
|
|
468
|
+
transformed = transformed.slice(0, position) + text + transformed.slice(position);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return transformed;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export default createEcoComponentMetaPlugin;
|